1 unstable release
new 0.1.0 | Mar 10, 2025 |
---|
#765 in Configuration
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
- The original implementation in OCaml.
serde_ccl
for integration withserde
.
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