TECL

People have complaints about configuration languages all the time; properties are too simple, XML is too verbose, JSON doesn’t allow comments, YAML is only suited for small configuration files, TOML’s tables are a bit weird (IMHO). So how about we throw all the good aspects onto a heap and see what comes out of it? And with only one task in mind: configuration. (Just like JSON is intended for datatransfer, hence it does not support comments.) Therefor I’m introducing TECL, the Totally Encompassing Configuration Language. But the name is open for suggestions. 😀

The goals are:

  • Simple, like properties, but supporting a hierarchy
  • Compact, like JSON, but allowing comments
  • Formal hierarchy, unlike YAML’s indentation based one
  • Schema support
  • Conditions
  • Multi line strings
  • Tables

Before starting with anything resembling implementation, let’s examine how we want it to look.

# you can specify the version of the TECL file (for future use)
@version 1

# This is the most simple configuration, if you need spaces or new lines you'll need quotes
title : "TECL rulez"

# We use ISO notations only, so no local styles
releaseDateTime : 2020-09-12T06:34

# Multiline strings
description : "So,
I'm curious 
where this will end."

# Shorcut string; no quotes are needed if the value is just one word.
protocol : http

# Conditions allow for overriding, best match wins (most conditions)
# If multiple condition sets equally match, the first one will win.
title[env=production] : "One config file to rule them all"
title[env=production & os=osx] : "Even on Mac"

# Lists
hosts : [alpha, beta]

# Hierarchy is implemented using groups denoted by curly brackets
database {

    # indenting is allowed and encouraged, but has no semantic meaning
    url : jdbc://...
    user : "admin"
    timeout: 10

    # Strings support default encryption with a external key file, like maven
    password : "FGFGGHDRG#$BRTHT%G%GFGHFH%twercgfg"

    # groups can nest
    dialect {
        database : postgres
    }
}

servers {
    # This is a table:
    # - the first row is a header, containing the id's
    # - the remaining rows are values
    | name     | datacenter | maxSessions | settings                    |
    | alpha    | A          | 12          |                             |
    | beta     | XYZ        | 24          |                             |
    | "sys 2"  | B          | 6           |                             |
    # lists are allowed in a table
    | many     | [D,E,F]    |             |                             |
    # you can have sub groups, by referencing another group
    | tango    | D          | 24          | $environment                |
}

# environments can be easily done using conditions
environment[env=development] {
    datasource : tst
}
environment[env=production] {
    datesource : prd
}

So given that this is the way it will look, how will you use it?

// Conditions are resolved at parse time.
// So in memory there will be only one version of an id like "environment".
TECL tecl = TECL.parser()
    .addParameter("env", "production") // to use in conditions
    .parse("..filename..");

// The initial object will point to the toplevel TECL, also called root.
String title = tecl.str("title"); 
LocalDateTime releaseDateTime = tecl.localDateTime("releaseDateTime");
List<String> hosts = tecl.strs("hosts");

// You can specify defaults in value methods
String port = tecl.integer("port", 80);

// The grp method takes it one level / group deeper.
// You simply get a new TECL object for inside that group.
TECL databaseTECL = tecl.grp("database");

// So fields are accessed just like in the toplevel
String url = databaseTECL.str("url");
String password = databaseTECL.decrypt("password");

// Or directly using a path
String url2 = tecl.str("database/url");

// The path starts at the current node, or root if it starts with a slash
String url3 = tecl.str("/database/url");

// And you can move back up
String title2 = databaseTECL.str("../title");

// In case a group was not defined, an empty TECL is ALWAYS returned.
// This will prevent null pointers in call chains.
// Only a leaf (value) method will return a null if undefined.
// So in the case below, field will be null, but no NPE will be thrown.
String field = tecl.str("/notThere/alsoNotThere/field");

// Tables are accessed using indexes/
// Index is the first parameter in order not to confuse with defaults
String ip = tecl.str(0, "/servers/ip");
// But indexes can also be written in the path
String ip2 = tecl.str("/servers/ip[0]");
int timeout = tecl.integer("/servers/settings[3]/timeout");

// Practical for tables is a lookup function: search for the row where name=gamma and get the value for maxSessions
int maxSessions = tecl.integer("/servers/name", "gamma", "/server/maxSessions"); // returns 12
// For readability it is better to first scope on the group
int maxSessions2 = tecl.grp("/servers").integer("name", "gamma", "maxSessions"); // returns 12

The schema will be simple, just like JSON schema, but using tables of course.

version = 1

| id              | type          | subtype  | minValues | maxValues |
| title           | string        |          | 1         |           |
| description     | string        |          |           |           |
| releaseDateTime | localDateTime |          |           |           |
| hosts           | list          | string   | 1         | 5         |
| database        | database      |          |           |           |
| servers         | table         | servers  |           | 10        |
| protocol        | protos        |          |           |           |
| protocols       | list          | protos   |           |           |

database {
    | id       | type      | minLen | maxLen |
    | url      | string    | 10     | 255    |
    | username | string    |        |        |
    | password | encrypted |        |        |
}

servers {
    | id          | type   | min | max |
    | id          | string |     |     |
    | datacenter  | string |     |     |
    | maxSessions | int    | 0   | 50  |
}

protos = [http, https]

The min- and maxValues are interesting. A standard property can only be assigned one value, so maxValues makes totally no sense to set, but setting minValues to 1 makes it mandatory. For properties which can hold more values, like lists and tables, min- and maxValues define the number of values that are allowed.

So. Suggestions are very welcome.

Some interesting issues have popped up massaging and implementing TECL:

  • Tables have no identifier, which means that there can be only one table in a group or at the toplevel. If you need more, you need to put them in separate groups (which is kinda like labeling them).
  • httpd.conf uses a scripting style with its IfDefine, IfModule, FileMatch. But that format was written for the purpose of configuring Apache, and TECL would be a generic language. But still it raised the question if such a support is needed, and if conditions can be used for that. If you do not want to copy it httpd.conf one to one, then conditions on groups could work well. Conditions inside tables is more interesting.
  • Github workflow is YAML, which can be rewritten to JSON or TECL just using groups and properties. But steps would be very suited to write in tables, if only “with” did not have children. Maybe a list of properties inside a table cell? Or references?

Oh, parsing the table is quite easily using ANTLR. I’m still working on how to stored and access it; truely as a table of TECLs, or as indexed properties.

Source code can be found here

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.