1 unstable release

new 0.1.0 Mar 10, 2025

#765 in Configuration

MIT license

45KB
1K SLoC

CCL Rust Parser

This is a crate for parsing of CCL (Categorical Configuration Language) documents in rust.

For a description of the language, see the original blog post or the docs.

See also


lib.rs:

ccl_rs

This is a parser for the Categorical Configuration Language.

The structure of the language is that every parsed CCL document is a (possibly empty) map from [String]s to CCL documents.

CCL = Mapping[String -> CCL]

This crate makes the following change from the language as described here:

We assume that a string without a = sign parses as a key with empty value. That is,

parse_kv("some string") == KeyValue{key = "some string", value="" };

Interesingly this then means that no UTF-8 string is invalid as a CCL document, and so the load function is error free. There is no other change to the language apart from the fact that strings without = are now valid:

let model = load("this is just a key");
assert_eq!(model.as_singleton(), Some("this is just a key"));

Examples

To parse a CCL document

let model = load("
/= This is a CCL document
language = rust
library = ccl_rs
author =
  name = Robert Spencer
  species = human
");

Scalars in CCL don't exist, and the nearest we have are "singletons": maps from strings to the empty map. We can try cast a model to a singleton with Model::as_singleton.

let singleton = load("
a singleton =
");
assert_eq!(singleton.as_singleton(), Some("a singleton"));

We can destructure the document with Model::get

assert_eq!(model.get("author")?.get("species")?.as_singleton(), Some("human"));
assert_eq!(model.at(["author", "species"])?.as_singleton(), Some("human"));

However, Model::as_singleton should rarely actually be used. You should prefer Model::parse_value which casts the singleton value (as a string) to its generic parameter using FromStr.

let model = load("
listen =
  host = 127.0.0.1
  port = 80
daemon = true
");
// We can use the turbo fish to force the type ...
assert_eq!(model.at(["listen", "port"])?.parse_value::<u16>()?, 80u16);
// ... or leave it inferred.
let host : std::net::Ipv4Addr = model.at(["listen", "host"])?.parse_value()?;
//         ^^^^^^^^^^^^^^^^^^ Here we've type hinted, but this might be inferred in other ways
// Even bools are parsed
if !model.get("daemon")?.parse_value()? {
  panic!()
}

There are two suggested methods for denoting lists and ccl_rs provides Model::as_list that handles both. Either a list can be valuless keys:

let model = load("
fruits =
 apples =
 pears =
 tomatoes =
");
assert_eq!(
  model.get("fruits")?.as_list().map(|x| x.value().unwrap().to_owned()).collect::<Vec<_>>(),
  ["apples", "pears", "tomatoes"]
);

Or it can be keyless values

let model = load("
fruits =
 = apples
 = pears
 = tomatoes
");
assert_eq!(
  model.get("fruits")?.as_list().map(|x| x.value().unwrap().to_owned()).collect::<Vec<_>>(),
  ["apples", "pears", "tomatoes"]
);

Fold

Let us suppose you have two configurations: one from the user and one from the system settings.

let system = load("
font size = 12px
colour scheme = gruvbox
");
let user = load("
colour scheme = dracula
");

This gives the model: The CCL method of combining these is either Model::merge, or the equivalent of concatting the strings and then parsing. This gives:

let configuration = Model::merge(system, user);

This gives

colour scheme =
  dracula =
  gruvbox =
font size = 12px

However, we could do

let configuration = Model::merge(
  Model::intro("system".to_string(), system),
  Model::intro("user".to_string(), user),
);

which is

system =
  font size = 12px
  colour scheme = gruvbox
user =
  colour scheme = dracula

Now if the application wants to know which colour scheme to use, it could query ["user", "colour scheme"] and ["system", "colour scheme"] and apply precidence. But if we have the rule that user configuration always trumps system configuration, we can apply the Model::fold operator instead as follows:

let configuration = Model::merge(
  Model::intro("system".to_string(), system),
  Model::intro("user".to_string(), user),
).fold();

which gives

font size =
  12px = system
colour scheme =
  gruvbox = system
  dracula = user

and then the application code can simply do

let colour_scheme_item : Model = configuration
    .get("colour scheme")?
    .as_list()
    .last()
    .unwrap();
let colour_scheme : &str = colour_scheme_item.value()?;
assert_eq!(colour_scheme, "dracula");

Features

By default all strings are interned using ustr. If you want to disable this, you can use the feature flag no-intern.

Dependencies

~3–8MB
~56K SLoC