libt3config
Schemas

Introduction

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.

Basics

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"
      }
    }
  }
}

Data types

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.

Constraints

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.

Constraint basics

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.

Aggregate types

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"

References

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.

Descriptions

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.