Uclicious
What is Uclicious
Uclicious is a flexible reduced boilerplate configuration framework.
Uclicious is built on top of libucl. If you ever wrote an nginx configurtion and though "Damn, I wish all configuration files were like this" this is the library for you. Internal parser supports both: nginx-like and json-like formats. JSON parser is a little bit more permissive than - every json file is a valid UCL file, but not other way around. It is much more complex than json or TOML, so I recommend reading documentaiton about it. Author of UCL did a great job documenting it. This library provides both: derive-driven and raw-api driven usage patterns.
Usage
Raw API
Raw API involves interacting with libucl
parser via safe api:
use uclicious::*;
let mut parser = Parser::default();
let input = r#"
test_string = "no scope"
a_float = 3.14
an_integer = 69420
is_it_good = yes
buffer_size = 1KB
interval = 1s
"#;
parser.add_chunk_full(input, Priority::default(), DEFAULT_DUPLICATE_STRATEGY).unwrap();
let result = parser.get_object().unwrap();
let lookup_result = result.lookup("test_string").unwrap().as_string().unwrap();
assert_eq!(lookup_result.as_str(), "no scope");
let lookup_result = result.lookup("a_float").unwrap().as_f64().unwrap();
assert_eq!(lookup_result, 3.14f64);
let lookup_result = result.lookup("an_integer").unwrap().as_i64().unwrap();
assert_eq!(lookup_result, 69420i64);
let lookup_result = result.lookup("is_it_good").unwrap().as_bool().unwrap();
assert_eq!(lookup_result, true);
let lookup_result = result.lookup("buffer_size").unwrap().as_i64().unwrap();
assert_eq!(lookup_result, 1024);
let lookup_result = result.lookup("interval").unwrap().as_time().unwrap();
assert_eq!(lookup_result, 1.0f64);
In order to get around rust rules library implemets its own trait FromObject for some basic types:
use uclicious::*;
let mut parser = Parser::default();
let input = r#"
test_string = "no scope"
a_float = 3.14
an_integer = 69420
is_it_good = yes
buffer_size = 1KB
"#;
parser.add_chunk_full(input, Priority::default(), DEFAULT_DUPLICATE_STRATEGY).unwrap();
let result = parser.get_object().unwrap();
let lookup_result = result.lookup("is_it_good").unwrap();
let maybe: Option<bool> = FromObject::try_from(lookup_result).unwrap();
assert_eq!(Some(true), maybe);
Derive-driven
On top of "raw" interface to libUCL, Uclicious provides an easy way to derive constructor for strucs:
use uclicious::*; use std::path::PathBuf; use std::net::SocketAddr; use std::collections::HashMap; use std::time::Duration; #[derive(Debug,Uclicious)] #[ucl(var(name = "test", value = "works"))] struct Connection { #[ucl(default)] enabled: bool, host: String, #[ucl(default = "420")] port: i64, buffer: u64, #[ucl(path = "type")] kind: String, locations: Vec<PathBuf>, addr: SocketAddr, extra: Extra, #[ucl(path = "subsection.host")] hosts: Vec<String>, #[ucl(default)] option: Option<String>, gates: HashMap<String, bool>, interval: Duration, } #[derive(Debug,Uclicious)] #[ucl(skip_builder)] struct Extra { enabled: bool } let mut builder = Connection::builder().unwrap(); let input = r#" enabled = yes host = "some.fake.url" buffer = 1mb type = $test locations = "/etc/" addr = "127.0.0.1:80" extra = { enabled = on } subsection { host = [host1, host2] } interval = 10ms gates { feature_1 = on feature_2 = off feature_3 = on }"#; builder.add_chunk_full(input, Priority::default(), DEFAULT_DUPLICATE_STRATEGY).unwrap(); let connection: Connection = builder.build().unwrap();
If you choose to derive builder then ::builder()
method will be added to target struct.
Validators
Library supports running optional validators on values before building the resulting struct:
use uclicious::*;
mod validators {
use uclicious::ObjectError;
pub fn is_positive(lookup_path: &str, value: &i64) -> Result<(), ObjectError> {
if *value > 0 {
Ok(())
} else {
Err(ObjectError::other(format!("{} is not a positive number", lookup_path)))
}
}
}
#[derive(Debug,Uclicious)]
struct Validated {
#[ucl(default, validate="validators::is_positive")]
number: i64
}
let mut builder = Validated::builder().unwrap();
let input = "number = -1";
builder.add_chunk_full(input, Priority::default(), DEFAULT_DUPLICATE_STRATEGY).unwrap();
assert!(builder.build().is_err())
Type Mapping
If your target structure has types that don't implement FromObject
you can use From
or TryFrom
via intermediate that does:
use uclicious::*; use std::convert::{From,TryFrom}; #[derive(Debug, Eq, PartialEq)] enum Mode { On, Off, } impl TryFrom<String> for Mode { type Error = ObjectError; fn try_from(src: String) -> Result{ match src.to_lowercase().as_str() { "on" => Ok(Mode::On), "off" => Ok(Mode::Off), _ => Err(ObjectError::other(format!("{} is not supported value", src))) } } } #[derive(Debug, Eq, PartialEq)] struct WrappedInt(i64); impl From<i64> for WrappedInt { fn from(src: i64) -> WrappedInt { WrappedInt(src) } } #[derive(Debug,Uclicious, Eq, PartialEq)] struct Mapped { #[ucl(from="i64")] number: WrappedInt, #[ucl(try_from="String")] mode: Mode } let mut builder = Mapped::builder().unwrap(); let input = r#" number = -1, mode = "on" "#; builder.add_chunk_full(input, Priority::default(), DEFAULT_DUPLICATE_STRATEGY).unwrap(); let actual = builder.build().unwrap(); let expected = Mapped { number: WrappedInt(-1), mode: Mode::On }; assert_eq!(expected, actual);
Additionally you can provide mapping to your type from ObjectRef:
use uclicious::*;
#[derive(Debug, Eq, PartialEq)]
pub enum Mode {
On,
Off,
}
pub fn map_bool(src: ObjectRef) -> Result {
let bool: bool = src.try_into()?;
if bool {
Ok(Mode::On)
} else {
Ok(Mode::Off)
}
}
#[derive(Debug,Uclicious, Eq, PartialEq)]
struct Mapped {
#[ucl(map="map_bool")]
mode: Mode
}
let mut builder = Mapped::builder().unwrap();
let input = r#"
mode = on
"#;
builder.add_chunk_full(input, Priority::default(), DEFAULT_DUPLICATE_STRATEGY).unwrap();
let actual = builder.build().unwrap();
let expected = Mapped {
mode: Mode::On
};
#[ucl(..)]
)
Supported attributes (Structure level
skip_builder
- if set, then builder and builder methods won't be generated.
parser(..)
- Optional attribute to configure inner parser.
- Has following nested attributes:
flags
- a path to function that returns flags.
filevars(..)
- call
set_filevars
on a parser. - Has following nested attributes:
path
- a string representation of filepath.
expand
- (optional) if set, then variables would be expanded to absolute.
- call
pre_source_hook(...)
- Optional attribute to run a function before sources are added
- Can be used to register vars handler
- Must take
&mut Parser
as argument and returnResult<(), Into
>
var(..)
- Optional attribute to register string variables with the parser.
- Has following nested attributes:
name
- A name of the variable without
$
part.
- A name of the variable without
value
- A string values for the variable.
- Onlt string variables are supported by libUCL.
include(..)
- Used to add files into the parser.
- If file doesn't exist or failed to parse, then error will be returned in a constructor.
- Must specify exactly one of following sources:
path
,chunk
orchunk_static
- Has following nested attirbutes:
- (semi-optional)
path = string
- File path. Can be absolute or relative to CWD.
- (semi-optional)
chunk = string
- A string that will be added to parser as a chunk.
- (semi-optional)
chunk_static = string
- A path to a file that will be included into binary with
include_str!()
- A path to a file that will be included into binary with
- (optional)
priority = u32
- 0-15 priority for the source. Consult the libUCL documentation for more information.
- (optional)
strategy = uclicious::DuplicateStrategy
- Strategy to use for duplicate keys. Consult the libUCL documentation for more information.
- (semi-optional)
Field level
All field level options are optional.
default
- Use Default::default if key not found in object.
default = expression
- Use this expression as value if key not found.
- Could be a value or a function call.
path = string
- By default field name is used as path.
- If set that would be used as a key.
- dot notation for key is supported.
validate = path::to_method
Fn(key: &str, value: &T) -> Result<(), E>
- Error needs to be convertable into
ObjectError
from = Type
- Try to convert
ObjectRef
toType
and then usestd::convert::From
to convert into target type
- Try to convert
try_from = Type
- Try to convert
ObjectRef
toType
and then usestd::convert::TryFrom
to convert into target type - Error will be converted into
ObjectError::Other
- Try to convert
map = path::to_method
Fn(src: ObjectRef) -> Result
- A way to map foreign objects that can't implement
From
orTryFrom
or when error is not convertable intoObjectError
Additional notes
- If target type is an array, but key is a single value — an implicit list is created.
- Automatic derive on enums is not supported, but you can implement it yourself.
- I have a few more features I want to implement before publishing this crate:
- Ability to add variables.
- Ability to add macross handlers.
- (maybe) configure parser that us used for derived builder with atrributes.
- (done) add sources to parser with attributes.
Contributing
PRs, feature requests, bug reports are welcome. I won't be adding CoC — be civilized.
Particular Contributions of Interest
- Optimize derive code.
- Improve documentation — I often write late and night and some it might look like a word soup.
- Better tests
- Glob support in derive parser section
- Variable handler
Goals
- Provider safe and convient configuration library
- Automatic derive, so you don't have to think about parser object
Not Goals
- Providing UCL Object generation tools is not a goal for this project
- 1:1 interface to libUCL
- sugar inside
raw
module
Special thanks
- draft6 and hauleth
- libucl-rs was a good starting point
- Type wrappers pretty much copied from there
- colin-kiegel
- Rust-derive-builder was used as a starting point for uclicious-derive
- Very well documented proc_macro crate, do recommend