#tree #config #language #block #tree-like #path #enums

confiner

A config language for things that look like trees

2 releases

0.2.1 Mar 16, 2024
0.2.0 Feb 14, 2024

#670 in Data structures

MIT license

30KB
831 lines

Confiner - A tree-like config language.

             /\
            <  >
             \/
            CONF
            CONF
         CONF||CONF
         CONF||CONF
        CONF||||CONF
       CONF CONF CONF
      CONF  CONF  CONF
   CONF  CONF  CONF  CONF
      CONF  ||||  CONF
            ||||

(Config + Conifer, get it?)

For example

A simple webserver config...

my_server: server
{
    addr = "127.0.0.1:80",

    my_uri_handler: uri
    {
        mount = "/files",
        pattern = [ "" ],

        file_service: raw_files
        {
            path = ./files
        }
    }
}

Features

Blocks

Confiner files contain a set of blocks, each of which has a set of key-value properties, and child blocks. This is similar to HTML or device-tree files.

Blocks have a kind, and an optional globally unique name. The syntax for a block begins with an optional name identifier, followed by a colon and the block kind, then a pair of braces containing comma-separated properties and children. e.g.

block_name: block_kind
{
    property="expr",
    : child_1_kind
    {
        property="expr",
        subchild: subchild_kind {}
    },
    child_2: child_2_kind {}
}

Expression types

Basic data types can be expressed in a sensible way:

  • Integers (1, -2, 0x03, ...)
  • Floats (1.0, 1e-3, ...)
  • Strings ("hello world", "Line 1\nLine 2", "\"quoted\"", ...)
  • Lists ([1, 2, 3], ...)
  • Maps with string keys ({ key = "value" }, ...)

There are a couple of more unique types of expressions.

Enums

These help to unambiguously serialize Rust's enum types. They consist of an enum identifier prefixed with an exclamation mark, followed optionally by data for that enum variant.

For example, consider the following enum:

enum MyEnum
{
    Unit,
    NewType(String),
    Tuple(i32, i32),
    Struct { field: bool }
}

This could be serialized as follows:

  • !Unit
  • !NewType "value"
  • !Tuple [i32, i32]
  • !Struct { field = true }

Paths

Paths are expressions that expand to the absolute path of a file, specified relative to the location of the confiner file. These expressions respect @include directives, and always evaluate to the same bytes, regardless of whether the file was included or deserialized directly.

They are written as a relative path, prefixed with .. They are not quoted, and cannot contain unescaped spaces. For example:

  • . (The directory containing the confiner file)
  • ./file.txt (A file in the same directory as the confiner file)
  • ./directory\ with\ spaces/file.txt

References

At the top-level of a confiner file, references can be defined. Rather than diretly creating a block, these blocks can be placed elsewhere by "using" the reference. The same block can be placed in multiple places, allowing for more complex structures than simple trees.

The syntax for defining a reference is the same as for defining a normal top-level block, but with a prefixed "&". Reference blocks must be named.

The syntax for using a reference is simply an "&" followed by the name of the reference.

&my_reference: kind { }

normal_block: kind
{
    &my_reference,
    child_block: kind
    {
        &my_reference
    }
}

Included files

Files can be included using the @include <path> syntax. Each included file is evaluated at most once, and any references or top-level blocks from the included file are treated equivalently to those in the file containing the @include <path>.

The @include <path> syntax uses for same relative paths logic as the path expression.

@include ./other_file.conf
@include ./subdir/file.conf

Dependencies

~0.4–1MB
~22K SLoC