quartiq / miniconf

Rust serialize/deserialize/access reflection for trees (no_std, no alloc)
MIT License
27 stars 3 forks source link
embedded graph no-alloc no-std reflection rust serde settings-management

miniconf: serialize/deserialize/access reflection for trees

crates.io docs QUARTIQ Matrix Chat Continuous Integration

miniconf enables lightweight (no_std/no alloc) serialization, deserialization, and access within a tree of heretogeneous types by keys.

Example

See below for an example showing some of the features of the Tree* traits. See also the documentation and doctests of the [TreeKey] trait for a detailed description.

Note that the example below focuses on JSON and slash-separated paths while in fact any serde backend (or dyn Any trait objects) and many different Keys/Transcode providers are supported.

use serde::{Deserialize, Serialize};
use miniconf::{Error, json, JsonPath, Traversal, Tree, TreeKey, Path, Packed, Node, Leaf, Metadata};

#[derive(Deserialize, Serialize, Default, Tree)]
pub struct Inner {
    a: Leaf<i32>,
    b: Leaf<i32>,
}

#[derive(Deserialize, Serialize, Default, Tree)]
pub enum Either {
    #[default]
    Bad,
    Good,
    A(Leaf<i32>),
    B(Inner),
    C([Inner; 2]),
}

#[derive(Tree, Default)]
pub struct Settings {
    foo: Leaf<bool>,
    enum_: Leaf<Either>,
    struct_: Leaf<Inner>,
    array: Leaf<[i32; 2]>,
    option: Leaf<Option<i32>>,

    #[tree(skip)]
    #[allow(unused)]
    skipped: (),

    struct_tree: Inner,
    enum_tree: Either,
    array_tree: [Leaf<i32>; 2],
    array_tree2: [Inner; 2],
    tuple_tree: (Leaf<i32>, Inner),
    option_tree: Option<Leaf<i32>>,
    option_tree2: Option<Inner>,
    array_option_tree: [Option<Inner>; 2],
}

let mut settings = Settings::default();

// Access nodes by field name
json::set(&mut settings,"/foo", b"true")?;
assert_eq!(*settings.foo, true);
json::set(&mut settings, "/enum_", br#""Good""#)?;
json::set(&mut settings, "/struct_", br#"{"a": 3, "b": 3}"#)?;
json::set(&mut settings, "/array", b"[6, 6]")?;
json::set(&mut settings, "/option", b"12")?;
json::set(&mut settings, "/option", b"null")?;

// Nodes inside containers
// ... by field name in a struct
json::set(&mut settings, "/struct_tree/a", b"4")?;
// ... or by index in an array
json::set(&mut settings, "/array_tree/0", b"7")?;
// ... or by index and then struct field name
json::set(&mut settings, "/array_tree2/0/a", b"11")?;
// ... or by hierarchical index
json::set_by_key(&mut settings, [8, 0, 1], b"8")?;
// ... or by packed index
let (packed, node): (Packed, _) = Settings::transcode([8, 1, 0]).unwrap();
assert_eq!(packed.into_lsb().get(), 0b1_1000_1_0);
assert_eq!(node, Node::leaf(3));
json::set_by_key(&mut settings, packed, b"9")?;
// ... or by JSON path
json::set_by_key(&mut settings, &JsonPath(".array_tree2[1].b"), b"10")?;

// Hiding paths by setting an Option to `None` at runtime
assert_eq!(json::set(&mut settings, "/option_tree", b"13"), Err(Traversal::Absent(1).into()));
settings.option_tree = Some(0.into());
json::set(&mut settings, "/option_tree", b"13")?;
// Hiding a path and descending into the inner `Tree`
settings.option_tree2 = Some(Inner::default());
json::set(&mut settings, "/option_tree2/a", b"14")?;
// Hiding items of an array of `Tree`s
settings.array_option_tree[1] = Some(Inner::default());
json::set(&mut settings, "/array_option_tree/1/a", b"15")?;

let mut buf = [0; 16];

// Serializing nodes by path
let len = json::get(&settings, "/struct_", &mut buf).unwrap();
assert_eq!(&buf[..len], br#"{"a":3,"b":3}"#);

// Tree metadata
let meta: Metadata = Settings::traverse_all().unwrap();
assert!(meta.max_depth <= 6);
assert!(meta.max_length("/") <= 32);

// Iterating over all leaf paths
for path in Settings::nodes::<Path<heapless::String<32>, '/'>, 6>() {
    let (path, node) = path.unwrap();
    assert!(node.is_leaf());
    // Serialize each
    match json::get(&settings, &path, &mut buf) {
        // Full round-trip: deserialize and set again
        Ok(len) => { json::set(&mut settings, &path, &buf[..len])?; }
        // Some Options are `None`, some enum variants are absent
        Err(Error::Traversal(Traversal::Absent(_))) => {}
        e => { e.unwrap(); }
    }
}

# Ok::<(), Error<serde_json_core::de::Error>>(())

Settings management

One possible use of miniconf is a backend for run-time settings management in embedded devices.

It was originally designed to work with JSON (serde_json_core) payloads over MQTT (minimq) and provides a MQTT settings management client in the miniconf_mqtt crate and a Python reference implementation to interact with it. Miniconf is agnostic of the serde backend/format, key type/format, and transport/protocol.

Formats

miniconf can be used with any serde::Serializer/serde::Deserializer backend, and key format.

Explicit support for / as the path hierarchy separator and JSON (serde_json_core) is implemented.

Support for the postcard wire format with any postcard flavor and any [Keys] type is implemented. Combined with the [Packed] key representation, this is a very space-efficient serde-by-key API.

Blanket implementations are provided for all TreeSerialize+TreeDeserialize types for all formats.

Transport

miniconf is also protocol-agnostic. Any means that can receive or emit serialized key-value data can be used to access nodes by path.

The MqttClient in the miniconf_mqtt crate implements settings management over the MQTT protocol with JSON payloads. A Python reference library is provided that interfaces with it. This example discovers the unique prefix of an application listening to messages under the topic quartiq/application/12345 and set its /foo setting to true.

python -m miniconf -d quartiq/application/+ /foo=true

Derive macros

For structs miniconf offers derive macros for [macro@TreeKey], [macro@TreeSerialize], [macro@TreeDeserialize], and [macro@TreeAny]. The macros implements the [TreeKey], [TreeSerialize], [TreeDeserialize], and [TreeAny] traits. Fields/variants that form internal nodes (non-leaf) need to implement the respective Tree{Key,Serialize,Deserialize,Any} trait. Leaf fields/items need to support the respective [serde] (and the desired serde::Serializer/serde::Deserializer backend) or [core::any] trait.

Structs, enums, arrays, Options, and many other containers can then be cascaded to construct more complex trees.

See also the [TreeKey] trait documentation for details.

Keys and paths

Lookup into the tree is done using a [Keys] implementation. A blanket implementation through [IntoKeys] is provided for IntoIterators over [Key] items. The [Key] lookup capability is implemented for usize indices and &str names.

Path iteration is supported with arbitrary separator chars between names.

Very compact hierarchical indices encodings can be obtained from the [Packed] structure. It implements [Keys].

Limitations

Features

Reflection

miniconf enables certain kinds of reflective access to heterogeneous trees. Let's compare it to bevy_reflect which is a comprehensive and mature reflection crate:

bevy_reflect is thoroughly std while miniconf aims at no_std. bevy_reflect uses its Reflect trait to operate on and pass nodes as trait objects. miniconf uses serialized data or Any to access leaf nodes and pure "code" to traverse through internal nodes. The Tree* traits like Reflect thus give access to nodes but unlike Reflect they are all decidedly not object-safe and can not be used as trait objects. This allows miniconf to support non-'static borrowed data (only for TreeAny the leaf nodes need to be 'static) while bevy_reflect requires 'static for Reflect types.

miniconfsupports at least the following reflection features mentioned in the bevy_reflect README:

Some tangential crates:

Functional Programming, Polymorphism

The type-heterogeneity of miniconf also borders on functional programming features. For that crates like the following may also be relevant: