libt3config
|
When using libt3config to read configuration files, it is possible to define a schema to which the configuration data must conform. This schema can define the type of the different entries, permitted values, conflicts between keys and more. This page describes the schema syntax and semantics.
Schemas are expressed as configuration files. They therefore use the same syntax as normal configuration files. However, because they describe other configuration files, they must also obey a specific structure (or schema).
At the top level, four specific items are allowed: first a types
section describing new types. Second, an allowed-keys
section defining the permissible keys and their types. Third an item-type
string defining the required type of keys not specifically allowed by the allowed-keys
section. Finally, a list of strings named constraint
which describes further restrictions.
The allowed-keys
section defines the different key names that are allowed. Each key used in the allowed-keys
section must itself be a section with at least the type
key set to a string describing the type. For example, to define a configuration which may have two keys, version
and value
, both of integer type, one might write the following:
allowed-keys { version { type = "int" } number { type = "int" } }
Each key description section, such as version
in the example above, has a set of permissible keys, depending on the value of its type
key. The constraint
list is always allowed. If type
is set to list
or section
, the item-type
key is allowed, with type string. If type
is set to section
, an allowed-keys
section, exactly like the allowed-keys
section at the top level, is allowed.
The example below declares that the only allowed key is named car
, which may contain any of the keys make
, model
and registration
, all with string type.
allowed-keys { car { type = "section" allowed-keys { make { type = "string" } model { type = "string" } registration { type = "string" } } } }
There are seven pre-defined data type names: int
, number
, bool
, string
, list
, section
and any
. However, it is often useful to create user defined types. The example above allowed a single car to be defined. However, maybe we want to define a list of cars, each of which should be described by a section with the make
, model
and registration
keys. This can be easily achieved by defining a car
type in the types
top-level section:
types { car { type = "section" allowed-keys { make { type = "string" } model { type = "string" } registration { type = "string" } } } } allowed-keys { car { type = "list" item-type = "car" } }
If a type is listed in the types
section, it may be used in the type
key of any definition, be it a types
definition, a definition in allowed-keys
or an item-type
definition.
Although the allowed-keys
and typed keys mechanisms works well to restrict the structure of the configuration file, it is by no means a complete method for limiting the possible inputs. Therefore, further constraints can be added in a separate language. These constraints are expressed within the schema in strings, in a list named constraint
, which may be added both at the top level of the schema, or at the same nesting level as the type
keys.
When constraints are placed on simple types (integer, floating point number, string or boolean), the value of the key may be accessed by using a percent sign (%). For example:
allowed-keys { version { type = "int" %constraint = "% > 0" } }
indicates that the value of the key version
must be greater than 0.
There are six comparison operators: = (equals), != (not equals), <, <=, > and >=. Note that for booleans and strings, only the = and != comparison operators are valid. Constants follow the same rules as described above for constants in the configuration file.
When the type of the constant does not match the type of the value, the comparison is always false. For constraints on simple types as above this will be detected when loading the schema, and an error will be reported. However, for more complicated constraints it is not always possible to determine the type of the comparison operands from the schema, in which case the comparison will simply result in a false result.
The schema language also supports the boolean operators & (and), | (or), ^ (exclusive or) and ! (not). These can be used to build more complex constraints.
For lists it is also possible to put a constraint on their size. To do this, use the hash sign (#) instead of the percent sign, and treat its value as an integer:
allowed-keys { cars { type = "list" %constraint = "# >= 2" } }
Constraints on section types can use the names of the constituent keys. When used in a comparison, the key is replaced with its value. When used alone, it evaluates to true if the key is present, and to false if it is absent:
allowed-keys { version { type = "int" } number { type = "int" } %constraint = "version" # Assert that the version key is present %constraint = "number = 1" # Assert that the number key must have value 1
Note that if number
is absent, the evaluation of number = 1
would result in false
, thereby deeming the configuration invalid. If the key may be absent, then the constraint should be !number | number = 1
.
For constraints on sections, the % symbol is invalid. The # symbol however evaluates to the number of keys in the section. Sometimes it is also useful to limit the number of keys in a subset of the possible keys. This is possible using the #(key1, key2, ...)
syntax. When evaluated, it will be replaced by the number of keys out of the listed set that is present in the configuration.
The # operator can also be used on expressions that denote a list. This allows one to compare sizes of different lists in a section, or by using references (see next sub-section), even with values from a completely different part of the configuration file.
allowed-keys { cars { type = "list" } } %constraint = "#cars >= 2"
One can refer to other keys in the configuration file, using a syntax similar to the Un*x file-name syntax:
allowed-keys { foo { type = "int" %constraint = "/bar" } bar { type = "int" } }
This indicates that if foo
is present, then so must bar
. In this specific example the constraints can of course also be expressed by the top-level constraint !foo | bar
, but remember that these types of expressions may also be used on types. Furthermore, the values of string keys may be used in the path by enclosing them in square brackets ([]):
allowed-keys { car { type = "section" allowed-keys { owner { type = "string" } make { type = "string" } mode { type = "string" } } %constraint = "/owner/[owner]/name" } owner { type = "section" item-type ="section" } }
The constraint in the example indicates that if the car
section is present, there must be a section in the top-level owner section with the name indicated by the string in /car/owner
, which must further contain a name
key.
Note that references that do not start with a slash (/) will be resolved relative to the current section, instead of as an absolute reference.
The t3_config_validate function can return the text of the constraint on detection of a constraint validation. However, in most cases these constraints will not make much sense to users. Therefore it is possible to include a descriptive string in a constraint, which will be returned instead:
allowed-keys { foo { type = "int" %constraint = "{'foo' may only be used simultaneously with 'bar'} /bar" } bar { type = "int" } }
The descriptive string must be enclosed in curly braces ({}), and must be the first item in the constraint.