# miniconf: serialize/deserialize/access reflection for trees [![crates.io](https://img.shields.io/crates/v/miniconf.svg)](https://crates.io/crates/miniconf) [![docs](https://docs.rs/miniconf/badge.svg)](https://docs.rs/miniconf) [![QUARTIQ Matrix Chat](https://img.shields.io/matrix/quartiq:matrix.org)](https://matrix.to/#/#quartiq:matrix.org) [![Continuous Integration](https://github.com/vertigo-designs/miniconf/workflows/Continuous%20Integration/badge.svg)](https://github.com/quartiq/miniconf/actions) `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. ```rust 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, b: Leaf, } #[derive(Deserialize, Serialize, Default, Tree)] pub enum Either { #[default] Bad, Good, A(Leaf), B(Inner), C([Inner; 2]), } #[derive(Tree, Default)] pub struct Settings { foo: Leaf, enum_: Leaf, struct_: Leaf, array: Leaf<[i32; 2]>, option: Leaf>, #[tree(skip)] #[allow(unused)] skipped: (), struct_tree: Inner, enum_tree: Either, array_tree: [Leaf; 2], array_tree2: [Inner; 2], tuple_tree: (Leaf, Inner), option_tree: Option>, option_tree2: Option, array_option_tree: [Option; 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::, '/'>, 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>(()) ``` ## 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`](https://docs.rs/serde-json-core)) payloads over MQTT ([`minimq`](https://docs.rs/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](https://mqtt.org) 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`. ```sh 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 `IntoIterator`s over [`Key`] items. The [`Key`] lookup capability is implemented for `usize` indices and `&str` names. Path iteration is supported with arbitrary separator `char`s between names. Very compact hierarchical indices encodings can be obtained from the [`Packed`] structure. It implements [`Keys`]. ## Limitations * `enum`: The derive macros don't support enums with record (named fields) variants or tuple variants with more than one (non-skip) field. Only unit, newtype and skipped variants are supported. Without the derive macros, any `enum` is still however usable as a `Leaf` node. Note also that netwype variants with a single inline tuple are supported. * The derive macros only support flattening in non-ambiguous situations (single field structs and single variant enums, both modulo skipped fields/variants and unit variants). ## Features * `json-core`: Enable helper functions for serializing from and into json slices (using the `serde_json_core` crate). * `postcard`: Enable helper functions for serializing from and into the postcard compact binary format (using the `postcard` crate). * `derive`: Enable the derive macros in `miniconf_derive`. Enabled by default. ## Reflection `miniconf` enables certain kinds of reflective access to heterogeneous trees. Let's compare it to [`bevy_reflect`](https://crates.io/crates/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. `miniconf`supports at least the following reflection features mentioned in the `bevy_reflect` README: * ➕ Derive the traits: `miniconf` has `Tree*` derive macros and blanket implementations for arrays and Options. Leaf nodes just need some impls of `Serialize/Deserialize/Any` where desired. * ➕ Interact with fields using their names * ➖ "Patch" your types with new values: `miniconf` only supports limited changes to the tree structure at runtime (`Option` and custom accessors) while `bevy_reflect` has powerful dynamic typing tools. * ➕ Look up nested fields using "path strings": In addition to a superset of JSON path style "path strings" `miniconf` supports hierarchical indices and bit-packed ordered keys. * ➕ Iterate over struct fields: `miniconf` Supports recursive iteration over node keys. * ➕ Automatically serialize and deserialize via Serde without explicit serde impls: `miniconf` supports automatic serializing/deserializing into key-value pairs without an explicit container serde impls. * ➕ Trait "reflection": Together with [`crosstrait`](https://crates.io/crates/crosstrait) supports building the type registry and enables casting from `dyn Any` returned by `TreeAny` to other desired trait objects. Together with [`erased-serde`](https://crates.io/crates/erased-serde) it can be used to implement node serialization/deserialization using `miniconf`'s `TreeAny` without using `TreeSerialize`/`TreeDeserialize` similar to `bevy_reflect`. Some tangential crates: * [`serde-reflection`](https://crates.io/crates/serde-reflection): extract schemata from serde impls * [`typetag`](https://crates.io/crates/typetag): "derive serde for trait objects" (local traits and impls) * [`deflect`](https://crates.io/crates/deflect): reflection on trait objects using adjacent DWARF debug info as the type registry * [`intertrait`](https://crates.io/crates/intertrait): inspiration and source of ideas for `crosstrait` ## Functional Programming, Polymorphism The type-heterogeneity of `miniconf` also borders on functional programming features. For that crates like the following may also be relevant: * [`frunk`](https://crates.io/crates/frunk) * [`lens-rs`](https://crates.io/crates/lens-rs)/[`rovv`](https://crates.io/crates/rovv)