#configuration-language #json #serde-json #language #pest #serde #config-file

nightly bin+lib conflag

A simple and powreful configuration language, extending JSON with declarative and functional language features

2 releases

0.1.1 Apr 14, 2023
0.1.0 Apr 14, 2023

#374 in Configuration

MIT/Apache

79KB
2K SLoC

Conflag 🔥🏁

Auto-deploy

Conflag is a data language. It's perfect for application configuration, once those configurations grow beyond a few command line flags or a 5-line JSON file.

Example use cases for Conflag:

  • Service deployment -- eg. replacing json/yaml for Kubernetes or nginx configs
  • Continuous integration -- eg. replacing yaml for Github Actions
  • ML experiments -- eg. replacing code for repeatable and replicable experiment parameters)
  • Developer tooling -- eg. replacing JSON for VS Code or Sublime text configs

Write your data declaratively, but with table-stakes like references, comments, and multiple files.

Any valid JSON file is already a valid Conflag file, so migration is easy, and the syntax is clear and easy to read. Get started in minutes and leave JSON behind.

Getting started

Rust with Serde:

cargo add conflag --features serde

Python:

git clone https://github.com/bethebunny/conflag
cd conflag
cargo build --release --features python
cp target/release/libconflag.so conflag.so

Meet the language

Write a config file: object.cfg

{
    hello: (name) => "Hello, " + name,
    name: "Stef",
    message: hello(name),
    double: (x) => x + x,
    data: map(double, [1, 2, 3, 4, 5]),
}

Read into Rust struct with Serde

#[derive(serde::Deserializer, Debug)]
struct Stuff {
    message: String,
    data: Vec<u64>,
}

fn main() {
    let raw: &str = fs::read_to_string("object.cfg").unwrap();
    let stuff: Stuff = conflag::serde::from_str(raw).unwrap();
    println!("{stuff:?}");
}

Read from Python

>>> import conflag
>>> s = conflag.loads(open("object.cfg").read())
>>> s.name
"Stef"
>>> s.message
"Hello, Stef"
>>> s.data
[2, 4, 6, 8, 10]

More language details

A more complex example

The following config will load to a list of model hyperparameter configs, running a parameter sweep over values for a dropout parameter.

It demonstrates a few features, such as

  • the builtin if and map functions
  • anonymous scopes -- the output is an array, rather than an object, and see how the arange function creates an anonymous scope
  • patching -- notice how we can easily make variants of model_params with fine-grained adjustments
{
    // Base hyperparams for a simple model
    model_params: {
        decoder_mlp: {
            hidden_layers: [1024, 512, 256],
            dropout: 0.2,
            activation: "relu",
            norm: null,
        }
    },
    
    // Let's make a helper function to help us with the sweep.
    // Like numpy's arange, create a list with a range of float values
    arange: (start, stop, step) => {
        // Name an internal computation in an anonymous scope
        done: if(step > 0, start >= stop, start <= stop),
        // Recursive calls allow building any necessary helpers
        result: if(done, [], [start] + arange(start + step, stop, step)),
    }.result,
    
    // Create a list that contains a copy of model_params for each value in our sweep
    experiments: map(
        // & is the patch operator, letting us override just the parts we want to change
        (_dropout) => model_params + {decoder_mlp: &{dropout: _dropout}},
        arange(0, 0.4, 0.01),
    ),

// We don't need to expose any of the other junk to our program!
// The data hides it in this anonymous scope and only produces the output.
}.experiments

Dependencies

~2.2–9.5MB
~72K SLoC