miniconf makes typed Rust data addressable: read a value, change it, discover
what else is there. Derive one tree and reuse it in a shell, a snapshot, an
inspector, or a protocol.
The core is no_std and needs no allocator. Serde encodes the values;
Miniconf selects them by path.
Create a host executable (no hardware or network required):
cargo new miniconf-demo
cd miniconf-demo
cargo add miniconf@0.21.1Replace src/main.rs with the following and run cargo run.
Derive [Tree] to expose the fields of each settings struct.
use miniconf::{json_core, ConstPath, Tree, TreeSchema};
#[derive(Default, Tree)]
struct Settings {
enabled: bool,
output: Output,
}
#[derive(Default, Tree)]
struct Output {
gain: [u16; 2],
}
fn main() {
let mut settings = Settings::default();
json_core::set(&mut settings, "/enabled", b"true").unwrap();
json_core::set(&mut settings, "/output/gain/1", b"42").unwrap();
let mut buf = [0; 8];
let len = json_core::get(&settings, "/output/gain/1", &mut buf).unwrap();
assert!(settings.enabled);
assert_eq!(&buf[..len], b"42");
const DEPTH: usize = Settings::SCHEMA.max_depth();
for path in Settings::SCHEMA.nodes::<ConstPath<String, '/'>, DEPTH>() {
println!("{}", path.unwrap());
}
}This prints /enabled, /output/gain/0, and /output/gain/1.
Try adding a field: it gets a path without another dispatch table. The final
loop discovers those paths from the type's schema. Here it uses the host's
String; a fixed-capacity string also works when no allocator is available.
Nested trees expose their fields separately, as Output does above. A leaf is
read or written as one Serde value. On the gain field,
#[tree(with = miniconf::leaf)] would make the array one leaf:
/output/gain would read or write [0, 42] as a whole instead of exposing each
element. #[tree(rename = "level")] would change its path segment to level.
Structs, enums, arrays, tuples, Option<T>, and standard container types can be
combined into larger trees. Option branches and inactive enum variants remain
in the static schema but may return [ValueError::Absent] at runtime.
To reject invalid settings without changing the live tree, deserialize into a candidate and commit only after the complete call succeeds. A failed call can leave partial changes, including on payload finalization errors. Applying hardware changes and saving settings remain application decisions.
Use #[tree(with = module)] to enforce rules for a field. The
integration fixture (examples/common.rs)
shows read-only fields and DAC range checking with a scratch copy. Metadata such
as max = "4095" describes the range; the custom deserializer enforces it.
Stabilizer's miniconf-settings
uses one tree for a shell and snapshots: get and set address live values,
while snapshots walk the leaves to save and restore them. Add a field and both
consumers can reach it. The application handles USB framing, hardware updates,
and flash storage. This upper-layer crate is currently unpublished.
The checkout examples explore other consumers using the same integration fixture. Its paths and values are also used by tests and the embedded benchmark; use the small quickstart tree above for experiments.
| Example | Run | What it adds |
|---|---|---|
examples/cli.rs |
cargo run --example cli -- --output-dac-1 2048 |
Command-line options |
examples/packed.rs |
cargo run --example packed --features postcard |
A binary leaf round trip in fixed buffers |
examples/trace.rs |
cargo run --example trace --features schema |
Host-side JSON and JSON Schema |
examples/scpi.rs |
cargo run --example scpi |
Custom command syntax, not a complete SCPI implementation |
Tree derives four independent capabilities: [TreeSchema] describes the
leaves, [TreeSerialize] reads them, [TreeDeserialize] writes them, and
[TreeAny] borrows their values through core::any::Any. An inspector can
require only the first two. The caller supplies framing, buffers, and scheduling.
Paths are one way to select a leaf. Index slices and [Packed] keys use the same
[IntoKeys] interface; [Schema::transcode()] converts between representations.
Compact keys belong to a particular schema, so resolve them again when the tree
changes. [Schema::nodes()] discovers leaves and [Schema::get()] looks up one
key.
Choose the leaf codec independently: [json_core] uses JSON byte slices,
postcard uses compact
binary payloads. miniconf_mqtt and
miniconf_coap are ready-made protocol consumers.
The embedded benchmark in tests/benchmark
compares the same get/set workload and codec against handwritten dispatch.
It reports program size, schema bytes, and observed stack use. The manual
handler omits discovery and reflection; the results are workload-specific,
not a worst-case stack bound. Run it with your tree when size matters.
- Internal tree enums support unit, newtype, and skipped variants only. Enums with named fields or multi-field tuple variants should stay leaves or use a manual/custom implementation.
- Flattening is accepted only when generated lookup stays structurally unambiguous.
&strkey input is always slash-separated. Use explicit iterator types for other syntaxes or separators.- Schema semantics and metadata are feature-gated reflection data. Do not depend
on them unless
sem,meta-node, ormeta-edgeis enabled as needed.
derive: re-export derive macros fromminiconf_derive; enabled by default.json-core:serde_json_corehelpers for JSON byte slices.json:serde_jsonhelpers.postcard: compact binary helpers usingpostcard.sem,meta-node,meta-edge: retain structured schema semantics, node metadata, and parent-child edge metadata. Constructors and derive output accept these payloads in all builds; without the matching feature, they are discarded and schema accessors returnNoneor empty metadata.trace,schema: serde-reflection tracing and JSON Schema generation.heapless,heapless-09,alloc,std: support for the corresponding storage and platform layers.
miniconf follows Cargo's SemVer compatibility guidelines,
including Rust's policy for trait implementations. For 0.y.z
releases, breaking changes bump y; compatible changes bump z.