Uclicious is a flexible reduced boilerplate configuration framework.

Overview

Uclicious Build Status codecov docs.rs Crates.io

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:

= FromObject::try_from(lookup_result).unwrap(); assert_eq!(Some(true), maybe); ">
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:

, addr: SocketAddr, extra: Extra, #[ucl(path = "subsection.host")] hosts: Vec, #[ucl(default)] option: Option, gates: HashMap, 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(); ">
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:

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 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); ">
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
};

Supported attributes (#[ucl(..)])

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.
  • 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 return Result<(), Into>
  • var(..)
    • Optional attribute to register string variables with the parser.
    • Has following nested attributes:
      • name
        • A name of the variable without $ part.
      • 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 or chunk_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!()
      • (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.

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 to Type and then use std::convert::From to convert into target type
  • try_from = Type
    • Try to convert ObjectRef to Type and then use std::convert::TryFrom to convert into target type
    • Error will be converted into ObjectError::Other
  • map = path::to_method
    • Fn(src: ObjectRef) -> Result
    • A way to map foreign objects that can't implement From or TryFrom or when error is not convertable into ObjectError

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

LICENSE

BSD-2-Clause.

You might also like...
TurboSHAKE: A Family of XOFs based on round reduced ( 12 rounds ) Keccak[1600] Permutation

turboshake TurboSHAKE: A Family of eXtendable Output Functions based on round reduced ( 12 rounds ) Keccak[1600] Permutation Overview TurboSHAKE is a

A systemd-boot configuration and boot entry configuration parser library

A systemd-boot configuration and boot entry configuration parser library

Kusion Configuration Language (KCL) is an open source configuration language mainly used in Kusion Stack

Kusion Configuration Language (KCL) is an open source configuration language mainly used in Kusion Stack. KCL is a statically typed language for configuration and policy scenarios, based on concepts such as declarative and Object-Oriented Programming (OOP) paradigms.

wireguard tool to manage / generate configuration. Maintain one yaml configuration file to quickly build wireguard network.

wgx wireguard tool to manage / generate configuration. Maintain one yaml configuration file to quickly build wireguard network. Usage wgx --h USAGE:

A fast, boilerplate free, web framework for Rust

Tower Web A web framework for Rust with a focus on removing boilerplate. API Documentation Tower Web is: Fast: Fully asynchronous, built on Tokio and

A minimal boilerplate for Astro / Vite with the Nannou creative framework (Rust → WASM). Supports multiple sketches + hot-reload.
A minimal boilerplate for Astro / Vite with the Nannou creative framework (Rust → WASM). Supports multiple sketches + hot-reload.

Astro x Nannou Starter astro-nannou-demo-1c.mov 🕹 Try it online! # 0a. Rust language tools open https://www.rust-lang.org/tools/install # 0b. wasm-p

A flexible web framework that promotes stability, safety, security and speed.

A flexible web framework that promotes stability, safety, security and speed. Features Stability focused. All releases target stable Rust. This will n

Minimal, flexible framework for implementing solutions to Advent of Code in Rust

This is advent_of_code_traits, a minimal, flexible framework for implementing solutions to Advent of Code in Rust.

A lightweight and flexible framework to build your tailored blockchain applications.

TRINCI Blockchain Core A lightweight and flexible framework to build your tailored blockchain applications. Requirements The required dependencies to

Organized, flexible testing framework for Rust

Stainless Stainless is a lightweight, flexible, unopinionated testing framework. Note that stainless currently requires the nightly version of the Rus

A backend framework for building fast and flexible APIs rapidly.

Andromeda Andromeda is a backend framework for Rust, to simplify the development of the kinds of basic API services that we developers have to build s

High-performance, low-level framework for composing flexible web integrations

High-performance, low-level framework for composing flexible web integrations. Used mainly as a dependency of `barter-rs` project

App Engine Rust boilerplate

Rust App Engine This projects is a minimal boilerplate ro run Rust web application inside Google App Engine. To deploy it use Google Cloud Shell: ```s

Improve and strengthen your strings by making them strongly-typed with less boilerplate

aliri_braid Improve and strengthen your strings Strongly-typed APIs reduce errors and confusion over passing around un-typed strings.

Quickly create boilerplate projects and templates.
Quickly create boilerplate projects and templates.

boyl boyl is a command-line tool written in Rust to manage template folders. boyl can copy existing folders (with support for glob-like ignore pattern

Build a config structure from environment variables in Rust without boilerplate

Yasec Yet another stupid environment config (YASEC) creates settings from environment variables. (Envconig-rs fork) Features Nested configuration stru

One-Stop Solution for all boilerplate needs!
One-Stop Solution for all boilerplate needs!

One Stop Solution for all boilerplate needs! Consider leaving a ⭐ if you found the project helpful. Templa-rs Templa-rs is a one-of-a-kind TUI tool wr

A fast & simple boilerplate generator, built with Rust. 🦀
A fast & simple boilerplate generator, built with Rust. 🦀

Boom 💥 A fast & simple boilerplate generator, built with Rust. Installing boom This package is not yet downloadable on Brew or other package managers

A simple code boilerplate generator written in Rust.

💻 Cgen What is Cgen? A modern, cross-platform, multi-language boilerplate generator aimed to make your code generation less hectic! If you wish to su

Comments
  • feat(derive): Fields mappers and validators

    feat(derive): Fields mappers and validators

    feat(derive): Ability to add optional validators feat(derive): Mapping from intermidiate type with "From" feat(derive): Mapping from intermidiate type with "TryFrom" fix: Remove Error trait boundry for ObjectError::other method

    opened by andoriyu 0
Releases(v0.1.6)
Owner
Andrey Cherkashin
Andrey Cherkashin
Kusion Configuration Language (KCL) is an open source configuration language mainly used in Kusion Stack

Kusion Configuration Language (KCL) is an open source configuration language mainly used in Kusion Stack. KCL is a statically typed language for configuration and policy scenarios, based on concepts such as declarative and Object-Oriented Programming (OOP) paradigms.

KusionStack 264 Dec 30, 2022
Build a config structure from environment variables in Rust without boilerplate

Yasec Yet another stupid environment config (YASEC) creates settings from environment variables. (Envconig-rs fork) Features Nested configuration stru

null 4 Dec 28, 2021
Zap - A simple cross-platform configuration management and orchestration tool

Zap - A simple cross-platform orchestration and configuration management tool. The main goal for Zap is to a simple mechanism for managing groups of com

R. Tyler Croy 50 Oct 29, 2022
Just-config is a configuration library for rust

Config Library for Rust Just-config is a configuration library for rust. It strives for the old Unix mantra "Do one thing and to it well".

FlashSystems 7 Apr 15, 2022
A Rust library for processing application configuration easily

Configure me A Rust library for processing application configuration easily About This crate aims to help with reading configuration of application fr

Martin Habovštiak 48 Dec 18, 2022
⚙️ Layered configuration system for Rust applications (with strong support for 12-factor applications).

config-rs Layered configuration system for Rust applications (with strong support for 12-factor applications). Set defaults Set explicit values (to pr

Ryan Leckey 1.8k Jan 9, 2023
cfg-rs: A Configuration Library for Rust Applications

cfg-rs: A Configuration Library for Rust Applications Major Features One method to get all config objects, see get. Automatic derive config object, se

Daniel YU 20 Dec 16, 2022
🌽 A simple and pain-free configuration language.

?? Corn A simple and pain-free configuration language. Corn has been designed using inspiration from JSON and Nix to produce a language that's easy an

Jake Stanger 3 Nov 28, 2022
A rust layered configuration loader with zero-boilerplate configuration management.

salak A layered configuration loader with zero-boilerplate configuration management. About Features Placeholder Key Convension Cargo Features Default

Daniel YU 28 Sep 20, 2022
kindly is a simple Rust implementation of a set-user-ID-root program, similar to sudo but in a much reduced way.

kindly is a simple Rust implementation of a set-user-ID-root program, similar to sudo but in a much reduced way.

Vinícius Miguel 26 Dec 5, 2022