Specifications and Checks - ConfigMate/configmate GitHub Wiki
ConfigMate is a tool engineered to scrutinize the contents of configuration files against a user-defined specification. These specifications are crafted by engineers employing ConfigMate, customized to the distinct qualities of their configurations. Learning how to write these specifications is the entry point to using ConfigMate.
A specification is the main file employed when executing ConfigMate. They are written in a DSL (Domain Specific Language) that we call ConfigMate Specification Language (CMSL). It encapsulates a list of field definitions along with metadata and the configuration file destined for validation.
Each definition references a specific field within a configuration file and defines some structural information and metadata that we use to validate the field and give detailed error reports. A field definition specifies the field name, the type of the field, whether the field is optional, a default value and some notes. Definitions also contain checks, which is another cornerstone feature of ConfigMate, we will talk more about checks later. The default and notes information do not have any effect on the validation process, but they are used to give more information to the user and assist in the process of fixing problematic configurations. These can help someone less familiar with the configuration files to understand what the field is used for and the thinking behind the checks being run.
Checks are programmatic expressions written in a minimalistic DSL called ConfigMate Check Language (CMCL). The details of CMCL are explained in Checks. But in short, checks are composed of simple expressions direclty invoking functions that fields support based on their specified type. The language also supports logical operators, grouping of expressions, if-elseif-else
control structures and foreach
statements.
Specifications are files written in a DSL (Domain Specific Language) that we call ConfigMate Specification Language (CMSL); we recognize these files by the extension .cms
.
The structure of a specification is the following:
-
Configuration file declaration:
This is the first statement that must be found in a specification file, and it determines which configuration file this specification is for. This sections looks like this:
config: "/path/to/config/file" format
ConfigMate finds the specified file from the current directory, so absolute paths are recommended for deployment and non-development environments in general. The
format
refers to the configuration file language; we currently only support:json
andtoml
. -
Imports:
This section allows us to import other specifications and thus reference fields defined in them when writting checks in the current specification. This section looks like this:
import ( alias0: "/path/to/another/spec.cms", alias1: "/path/to/yet/another/spec.cms" )
When referencing fields defined in the imported specs, the field names will be prefixed with the alias.
-
Specification Body:
The specification body is where we describe our configuration file. This specification body is contained inside this structure:
spec { // Here goes the configuration field definitions }
-
Custom Object Types:
In most cases, object fields should be defined by creating a definition for the object and nesting the definitions of its fields; however, if we have a list of objects, we need to define what those objects looks like separately. This section allows us to do that. This section is contained inside this structure:
objects { // Custom object definitions }
Each custom object definition consists of the type name, followed by curly braces, and inside the curly braces we write the object's fields as a field name and a short metadata form (see the Definitions Format section).
Here we describe how to define field in our configuration file that we want ConfigMate to know about. This starts by specifing the field name, then the field metadata, optionally a series of checks we want to perform on this field, and a curly-braces-enclosed subsection of what this field contains when the field is an object. Looking at the examples at the end of the page can be very helpful while going through this section. This looks like this:
fieldName metadata checks nestedFields
The field name is a .
separated path that identifies the field in the configuration. More formally Each path segment can be an unquoted string (e.g. server, cache), or a literal string for when spaces, quotes or dots are needed (e.g. 'example.com', 'John Doe').
Each definition must specify a type. Definitions can also indicate whether the field is optional. Just for informational purposes when debugging with ConfigMate, we can also add a default value and some notes. Depending on the amount of information we want to define, CMSL supports a short and a long version of the field metadata(type, optional, default, and notes).
Metadata Short Form:
<type> optional
Metadata Long Form:
<
type: type,
optional: boolean,
default: any,
notes: string
>
After the field metadata, we can specify a list of semicolon-terminated checks we want to perform on the field. The checks section in a definition looks like this:
(
check0;
check1;
)
After this, if the field in question is an object, you can open another curly-braces section and define nested fields there:
{
// Nested configuration fields definitions
}
Putting all of this together, a field definition looks like this:
fieldName <object> optional (
somecheck;
) {
nestedField <
type: int,
optional: false,
default: 55,
notes: "What is this field?"
>
anotherNestField <string> ( someOtherCheck; )
}
A custom object definition looks like this:
dnsProvider {
name <string>
address <host>
}
Configuration:
{
"server": {
"hostname": "localhost",
"application_port": 128,
"ssl_enabled": false,
"ssl_cert_path": "/path/to/cert.pem",
"ssl_key_path": "/path/to/key.pem"
},
"dns_servers": [
"8.8.8.8",
"8.8.4.4"
]
}
ConfigMate Specification:
config: "config.json" json
spec {
server <object> {
hostname <host> (
reachable();
)
application_port <port> (
toInt().range(150, 200);
)
ssl_enabled <
type: bool,
optional: true,
default: false,
notes: "This flag determines whether SSL should be used to connect to the server"
>
ssl_cert_path <file> optional (
if (server.ssl_enabled) { exists() };
)
ssl_key_path <file> optional (
if (server.ssl_enabled) { exists() };
)
}
dns_servers <list<host>> optional (
len().gte(2);
foreach(s : this) { s.reachable() };
)
}