├── README.md ├── miniconf_derive ├── .gitignore ├── README.md ├── Cargo.toml └── src │ ├── lib.rs │ └── field.rs ├── .gitignore ├── miniconf ├── tests │ ├── embedded │ │ ├── .gitignore │ │ ├── memory.x │ │ ├── .cargo │ │ │ └── config.toml │ │ ├── build.rs │ │ ├── Cargo.toml │ │ └── src │ │ │ └── main.rs │ ├── compiletest.rs │ ├── ui │ │ ├── enum-named.rs │ │ ├── enum-non-newtype.rs │ │ ├── empty-array.rs │ │ ├── exact_size_iter.rs │ │ ├── enum-non-newtype.stderr │ │ ├── skip-unnamed.rs │ │ ├── inherited-meta.rs │ │ ├── flatten-ambiguous.rs │ │ ├── inherited-meta.stderr │ │ ├── enum-named.stderr │ │ ├── flatten-ambiguous.stderr │ │ ├── skip-unnamed.stderr │ │ ├── internal_no_leaf.rs │ │ ├── empty-array.stderr │ │ ├── internal_no_leaf.stderr │ │ └── exact_size_iter.stderr │ ├── skipped.rs │ ├── common │ │ └── mod.rs │ ├── flatten.rs │ ├── iter.rs │ ├── enum.rs │ ├── generics.rs │ ├── basic.rs │ ├── structs.rs │ ├── arrays.rs │ ├── option.rs │ ├── validate.rs │ └── packed.rs ├── src │ ├── impls │ │ ├── mod.rs │ │ └── key.rs │ ├── lib.rs │ ├── postcard.rs │ ├── json.rs │ ├── json_core.rs │ ├── shape.rs │ ├── jsonpath.rs │ ├── error.rs │ ├── iter.rs │ ├── trace.rs │ ├── packed.rs │ ├── key.rs │ └── tree.rs ├── examples │ ├── trace.rs │ ├── cli.rs │ ├── common.rs │ └── scpi.rs ├── Cargo.toml └── README.md ├── rustfmt.toml ├── py ├── miniconf-mqtt │ ├── .gitignore │ ├── .pylintrc │ ├── miniconf │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── common.py │ │ ├── sync.py │ │ └── async_.py │ ├── README.md │ └── pyproject.toml └── test.sh ├── Cargo.toml ├── miniconf_mqtt ├── README.md ├── Cargo.toml └── examples │ └── mqtt.rs ├── flake.lock ├── LICENSE-MIT ├── flake.nix └── .github └── workflows └── ci.yml /README.md: -------------------------------------------------------------------------------- 1 | miniconf/README.md -------------------------------------------------------------------------------- /miniconf_derive/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | /Cargo.lock 3 | 4 | -------------------------------------------------------------------------------- /miniconf/tests/embedded/.gitignore: -------------------------------------------------------------------------------- 1 | /Cargo.lock 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # format_code_in_doc_comments = true 2 | -------------------------------------------------------------------------------- /py/miniconf-mqtt/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.egg 3 | *.egg-info/ 4 | -------------------------------------------------------------------------------- /py/miniconf-mqtt/.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=logging-fstring-interpolation 3 | -------------------------------------------------------------------------------- /py/miniconf-mqtt/miniconf/__init__.py: -------------------------------------------------------------------------------- 1 | """Miniconf MQTT Client""" 2 | 3 | from .async_ import * 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["miniconf_derive", "miniconf", "miniconf_mqtt"] 3 | resolver = "2" 4 | -------------------------------------------------------------------------------- /miniconf/src/impls/mod.rs: -------------------------------------------------------------------------------- 1 | mod internal; 2 | mod key; 3 | pub use key::*; 4 | mod leaves; 5 | pub use leaves::*; 6 | -------------------------------------------------------------------------------- /miniconf/tests/compiletest.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn ui() { 3 | let t = trybuild::TestCases::new(); 4 | t.compile_fail("tests/ui/*.rs"); 5 | } 6 | -------------------------------------------------------------------------------- /miniconf/tests/ui/enum-named.rs: -------------------------------------------------------------------------------- 1 | use miniconf::Tree; 2 | 3 | #[derive(Tree)] 4 | pub enum E { 5 | A { a: i32 }, 6 | } 7 | 8 | fn main() {} 9 | -------------------------------------------------------------------------------- /miniconf/tests/ui/enum-non-newtype.rs: -------------------------------------------------------------------------------- 1 | use miniconf::Tree; 2 | 3 | #[derive(Tree)] 4 | pub enum E1 { 5 | A(i32, i32), 6 | } 7 | 8 | fn main() {} 9 | -------------------------------------------------------------------------------- /miniconf_derive/README.md: -------------------------------------------------------------------------------- 1 | # `miniconf` Derive Macros 2 | 3 | This package contains derive macros for [`miniconf`](https://crates.io/crates/miniconf). 4 | -------------------------------------------------------------------------------- /miniconf/tests/ui/empty-array.rs: -------------------------------------------------------------------------------- 1 | use miniconf::TreeSchema; 2 | 3 | fn main() { 4 | const _: usize = <[usize; 0] as TreeSchema>::SCHEMA.shape().max_depth; 5 | } 6 | -------------------------------------------------------------------------------- /miniconf/tests/ui/exact_size_iter.rs: -------------------------------------------------------------------------------- 1 | use miniconf::{ExactSize, NodeIter}; 2 | 3 | const _: ExactSize> = NodeIter::exact_size::<[(); 1]>(); 4 | 5 | fn main() {} 6 | -------------------------------------------------------------------------------- /py/miniconf-mqtt/miniconf/__main__.py: -------------------------------------------------------------------------------- 1 | """Miniconf default CLI (async)""" 2 | 3 | import asyncio 4 | from .async_ import _main 5 | 6 | if __name__ == "__main__": 7 | asyncio.run(_main()) 8 | -------------------------------------------------------------------------------- /miniconf/tests/ui/enum-non-newtype.stderr: -------------------------------------------------------------------------------- 1 | error: Only newtype (single field tuple) and unit enum variants are supported. 2 | --> tests/ui/enum-non-newtype.rs:5:5 3 | | 4 | 5 | A(i32, i32), 5 | | ^ 6 | -------------------------------------------------------------------------------- /miniconf/tests/ui/skip-unnamed.rs: -------------------------------------------------------------------------------- 1 | use miniconf::Tree; 2 | 3 | #[derive(Tree)] 4 | pub struct S(#[tree(skip)] i32, i32); 5 | 6 | #[derive(Tree)] 7 | pub enum E { 8 | A(#[tree(skip)] i32, i32), 9 | } 10 | 11 | fn main() {} 12 | -------------------------------------------------------------------------------- /miniconf/tests/ui/inherited-meta.rs: -------------------------------------------------------------------------------- 1 | use miniconf::Tree; 2 | 3 | #[derive(Tree)] 4 | #[tree(meta(foo))] 5 | struct S(i32); 6 | 7 | #[derive(Tree)] 8 | #[tree(meta(doc = "foo"))] 9 | /// Docs 10 | struct T(i32); 11 | 12 | fn main() {} 13 | -------------------------------------------------------------------------------- /miniconf/tests/ui/flatten-ambiguous.rs: -------------------------------------------------------------------------------- 1 | use miniconf::Tree; 2 | 3 | #[derive(Tree)] 4 | #[tree(flatten)] 5 | pub struct S { 6 | a: i32, 7 | b: i32, 8 | } 9 | 10 | #[derive(Tree)] 11 | #[tree(flatten)] 12 | pub enum E { 13 | A(i32), 14 | B(i32), 15 | } 16 | 17 | fn main() {} 18 | -------------------------------------------------------------------------------- /miniconf/tests/ui/inherited-meta.stderr: -------------------------------------------------------------------------------- 1 | error: 'foo' is not supported as inherited meta 2 | --> tests/ui/inherited-meta.rs:3:10 3 | | 4 | 3 | #[derive(Tree)] 5 | | ^^^^ 6 | | 7 | = note: this error originates in the derive macro `Tree` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | -------------------------------------------------------------------------------- /miniconf/tests/embedded/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY 2 | { 3 | FLASH : ORIGIN = 0x00000000, LENGTH = 256K 4 | RAM : ORIGIN = 0x20000000, LENGTH = 64K 5 | } 6 | 7 | SECTIONS { 8 | linkme_REGISTRY_KV : { *(linkme_REGISTRY_KV) } > FLASH 9 | linkm2_REGISTRY_KV : { *(linkm2_REGISTRY_KV) } > FLASH 10 | } 11 | INSERT AFTER .rodata 12 | -------------------------------------------------------------------------------- /miniconf/tests/ui/enum-named.stderr: -------------------------------------------------------------------------------- 1 | error: Unsupported shape `named fields`. Expected unnamed fields or no fields. 2 | --> tests/ui/enum-named.rs:3:10 3 | | 4 | 3 | #[derive(Tree)] 5 | | ^^^^ 6 | | 7 | = note: this error originates in the derive macro `Tree` (in Nightly builds, run with -Z macro-backtrace for more info) 8 | -------------------------------------------------------------------------------- /miniconf/tests/ui/flatten-ambiguous.stderr: -------------------------------------------------------------------------------- 1 | error: Can't flatten multiple fields/variants 2 | --> tests/ui/flatten-ambiguous.rs:4:8 3 | | 4 | 4 | #[tree(flatten)] 5 | | ^^^^^^^ 6 | 7 | error: Can't flatten multiple fields/variants 8 | --> tests/ui/flatten-ambiguous.rs:11:8 9 | | 10 | 11 | #[tree(flatten)] 11 | | ^^^^^^^ 12 | -------------------------------------------------------------------------------- /miniconf/tests/embedded/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.thumbv7m-none-eabi] 2 | runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel" 3 | 4 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 5 | rustflags = ["-C", "link-arg=-Tlink.x"] 6 | 7 | [build] 8 | target = "thumbv7m-none-eabi" 9 | -------------------------------------------------------------------------------- /miniconf/tests/ui/skip-unnamed.stderr: -------------------------------------------------------------------------------- 1 | error: Can only `skip` terminal tuple struct fields 2 | --> tests/ui/skip-unnamed.rs:4:21 3 | | 4 | 4 | pub struct S(#[tree(skip)] i32, i32); 5 | | ^^^^ 6 | 7 | error: Can only `skip` terminal tuple variant fields 8 | --> tests/ui/skip-unnamed.rs:8:14 9 | | 10 | 8 | A(#[tree(skip)] i32, i32), 11 | | ^^^^ 12 | -------------------------------------------------------------------------------- /py/miniconf-mqtt/README.md: -------------------------------------------------------------------------------- 1 | # `miniconf` Python Utility 2 | 3 | Python package for interacting with `miniconf-mqtt` targets. 4 | 5 | ## Installation 6 | 7 | Run `pip install .` from this directory to install the `miniconf-mqtt` package. 8 | 9 | Alternatively, run `python -m pip install 10 | git+https://github.com/quartiq/miniconf#subdirectory=py/miniconf-mqtt` to avoid cloning locally. 11 | -------------------------------------------------------------------------------- /miniconf/tests/ui/internal_no_leaf.rs: -------------------------------------------------------------------------------- 1 | use miniconf::Tree; 2 | 3 | #[derive(Tree)] 4 | pub enum EnumUninhab {} 5 | 6 | #[derive(Tree)] 7 | pub enum EnumEmpty {#[tree(skip)] V} 8 | 9 | #[derive(Tree)] 10 | pub struct StructUnit; 11 | 12 | #[derive(Tree)] 13 | pub struct StructUnitTuple (); 14 | 15 | #[derive(Tree)] 16 | pub struct StructEmptyTuple (#[tree(skip)] ()); 17 | 18 | #[derive(Tree)] 19 | pub struct StructEmpty {#[tree(skip)] _f: ()} 20 | 21 | fn main() {} 22 | -------------------------------------------------------------------------------- /miniconf_mqtt/README.md: -------------------------------------------------------------------------------- 1 | # `miniconf` MQTT Client 2 | 3 | This package contains a MQTT client exposing a [`miniconf`](https://crates.io/crates/miniconf) interface via MQTT using [`minimq`](https://crates.io/crates/minimq). 4 | 5 | ## Command types 6 | 7 | | Command | Node | Response Topic | Payload | 8 | | --- | --- | --- | --- | 9 | | Get | Leaf | set | empty | 10 | | List | Internal | set | empty | 11 | | Dump | (any) | not set | empty | 12 | | Set | Leaf | | some | 13 | | (Error) | Internal | | some | 14 | 15 | ## Notes 16 | 17 | * `List` lists paths that would result in `miniconf::ValueError::Absent` on `Get` or `Set`. 18 | -------------------------------------------------------------------------------- /miniconf/tests/embedded/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::File; 3 | use std::io::Write; 4 | use std::path::PathBuf; 5 | 6 | fn main() { 7 | // Put the linker script somewhere the linker can find it 8 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 9 | File::create(out.join("memory.x")) 10 | .unwrap() 11 | .write_all(include_bytes!("memory.x")) 12 | .unwrap(); 13 | println!("cargo:rustc-link-search={}", out.display()); 14 | 15 | // Only re-run the build script when memory.x is changed, 16 | // instead of when any part of the source code changes. 17 | println!("cargo:rerun-if-changed=memory.x"); 18 | } 19 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1728979988, 6 | "narHash": "sha256-GBJRnbFLDg0y7ridWJHAP4Nn7oss50/VNgqoXaf/RVk=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "7881fbfd2e3ed1dfa315fca889b2cfd94be39337", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /miniconf_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "miniconf_derive" 3 | version = "0.20.0" 4 | authors = ["James Irwin ", "Ryan Summers ", "Robert Jördens "] 5 | edition = "2024" 6 | license = "MIT" 7 | description = "Derive macros for `miniconf`" 8 | repository = "https://github.com/quartiq/miniconf" 9 | keywords = ["settings", "serde", "no_std", "json", "mqtt"] 10 | categories = ["no-std", "config", "rust-patterns", "parsing"] 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [features] 16 | default = [] 17 | meta-str = [] 18 | 19 | [dependencies] 20 | syn = "2.0" 21 | quote = "1.0" 22 | proc-macro2 = "1.0" 23 | darling = "0.21" 24 | -------------------------------------------------------------------------------- /miniconf/tests/embedded/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crosstrait-embedded-test" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | cortex-m = { version = "0.7.6", features = ["critical-section-single-core"] } 9 | cortex-m-rt = "0.7" 10 | cortex-m-semihosting = "0.5" 11 | panic-semihosting = { version = "0.6", features = ["exit"] } 12 | 13 | crosstrait = { version = "0.1.3", default-features = false, features = [ 14 | "global_registry", 15 | ] } 16 | miniconf = { path = "../../../miniconf", features = [ 17 | "json-core", 18 | "postcard", 19 | "derive", 20 | ], default-features = false } 21 | 22 | [features] 23 | used_linker = ["crosstrait/used_linker"] 24 | default = ["used_linker"] 25 | 26 | [workspace] 27 | -------------------------------------------------------------------------------- /py/miniconf-mqtt/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "miniconf-mqtt" 7 | # Note: keep this in sync with Cargo.toml 8 | version = "0.19.0" 9 | description = "Utilities for configuring Miniconf-configurable devices" 10 | authors = [ 11 | { name = "Ryan Summers", email = "ryan.summers@vertigo-designs.com" }, 12 | { name = "Robert Jördens", email = "rj@quartiq.de" }, 13 | ] 14 | dependencies = ["aiomqtt >= 2.1.0", "typing_extensions >= 4.11"] 15 | classifiers = [ 16 | "License :: OSI Approved :: MIT License", 17 | ] 18 | 19 | [project.urls] 20 | Homepage = "https://github.com/quartiq/miniconf" 21 | Issues = "https://github.com/quartiq/miniconf/issues" 22 | Repository = "https://github.com/quartiq/miniconf/tree/main/py/miniconf-mqtt" 23 | Changelog = "https://github.com/quartiq/miniconf/blob/main/CHANGELOG.md" 24 | -------------------------------------------------------------------------------- /miniconf/tests/skipped.rs: -------------------------------------------------------------------------------- 1 | use miniconf::{KeyError, Path, Shape, Tree, TreeSchema}; 2 | 3 | #[derive(Default)] 4 | pub struct SkippedType; 5 | 6 | #[derive(Tree, Default)] 7 | struct Settings { 8 | #[tree(skip)] 9 | _long_skipped_type: SkippedType, 10 | 11 | value: f32, 12 | } 13 | 14 | #[test] 15 | fn meta() { 16 | const SHAPE: Shape = Settings::SCHEMA.shape(); 17 | assert_eq!(SHAPE.max_depth, 1); 18 | assert_eq!(SHAPE.max_length("/"), "/value".len()); 19 | assert_eq!(SHAPE.count.get(), 1); 20 | } 21 | 22 | #[test] 23 | fn path() { 24 | assert_eq!( 25 | Settings::SCHEMA.transcode::>([0usize]), 26 | Ok(Path("/value".to_owned())) 27 | ); 28 | assert_eq!( 29 | Settings::SCHEMA.transcode::>([1usize]), 30 | Err(KeyError::NotFound.into()) 31 | ); 32 | } 33 | 34 | #[test] 35 | fn skip_struct() { 36 | #[allow(dead_code)] 37 | #[derive(Tree)] 38 | #[tree(flatten)] 39 | pub struct S(i32, #[tree(skip)] i32); 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 QUARTIQ GmbH 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /miniconf/examples/trace.rs: -------------------------------------------------------------------------------- 1 | //! Showcase for reflection and schema building 2 | 3 | use miniconf::{json::to_json_value, json_schema::TreeJsonSchema}; 4 | 5 | mod common; 6 | use common::Settings; 7 | 8 | fn main() -> anyhow::Result<()> { 9 | let s = Settings::new(); 10 | 11 | let value = to_json_value(&s)?; 12 | println!("JSON Tree:\n{}", serde_json::to_string_pretty(&value)?); 13 | 14 | let mut schema = TreeJsonSchema::new(Some(&s)).unwrap(); 15 | 16 | schema 17 | .root 18 | .insert("title".to_string(), "Miniconf example: Settings".into()); 19 | 20 | use schemars::transform::Transform; 21 | //miniconf::json_schema::Strictify.transform(&mut schema.root); 22 | miniconf::json_schema::AllowAbsent.transform(&mut schema.root); 23 | 24 | println!( 25 | "JSON Schema:\n{}", 26 | serde_json::to_string_pretty(&schema.root)? 27 | ); 28 | 29 | jsonschema::meta::validate(schema.root.as_value()).unwrap(); 30 | 31 | let validator = jsonschema::validator_for(schema.root.as_value())?; 32 | for e in validator.iter_errors(&value) { 33 | eprintln!("{e} {e:?}"); 34 | } 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /miniconf/tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | use miniconf::{ 4 | DescendError, IntoKeys, KeyError, Keys, Packed, Path, Schema, Track, Transcode, 5 | TreeDeserialize, TreeSchema, TreeSerialize, json_core, 6 | }; 7 | 8 | pub fn paths() -> Vec { 9 | assert!( 10 | T::SCHEMA 11 | .nodes::() 12 | .collect::, _>>() 13 | .unwrap() 14 | .is_sorted() 15 | ); 16 | T::SCHEMA 17 | .nodes::>, D>() 18 | .map(|pn| { 19 | let pn = pn.unwrap(); 20 | println!("{pn:?}"); 21 | // assert_eq!(p.chars().filter(|c| *c == p.separator()).count(), n); 22 | pn.into_inner().0.into_inner() 23 | }) 24 | .collect() 25 | } 26 | 27 | pub fn set_get<'de, M>(s: &mut M, path: &str, value: &'de [u8]) 28 | where 29 | M: TreeDeserialize<'de> + TreeSerialize + ?Sized, 30 | { 31 | json_core::set(s, path, value).unwrap(); 32 | let mut buf = vec![0; value.len()]; 33 | let len = json_core::get(s, path, &mut buf[..]).unwrap(); 34 | assert_eq!(&buf[..len], value); 35 | } 36 | -------------------------------------------------------------------------------- /miniconf/tests/ui/empty-array.stderr: -------------------------------------------------------------------------------- 1 | error[E0080]: evaluation panicked: Must have at least one child 2 | --> src/impls/internal.rs 3 | | 4 | | const SCHEMA: &'static Schema = &Schema::homogeneous(Homogeneous::new(N, T::SCHEMA)); 5 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of `miniconf::impls::internal::::SCHEMA` failed inside this call 6 | | 7 | note: inside `miniconf::Homogeneous::new` 8 | --> src/schema.rs 9 | | 10 | | len: NonZero::new(len).expect("Must have at least one child"), 11 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the failure occurred here 12 | 13 | note: erroneous constant encountered 14 | --> src/impls/internal.rs 15 | | 16 | | const SCHEMA: &'static Schema = &Schema::homogeneous(Homogeneous::new(N, T::SCHEMA)); 17 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 18 | 19 | note: erroneous constant encountered 20 | --> tests/ui/empty-array.rs:4:22 21 | | 22 | 4 | const _: usize = <[usize; 0] as TreeSchema>::SCHEMA.shape().max_depth; 23 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 24 | -------------------------------------------------------------------------------- /miniconf/tests/ui/internal_no_leaf.stderr: -------------------------------------------------------------------------------- 1 | error: Internal nodes must have at least one leaf 2 | --> tests/ui/internal_no_leaf.rs:4:10 3 | | 4 | 4 | pub enum EnumUninhab {} 5 | | ^^^^^^^^^^^ 6 | 7 | error: Internal nodes must have at least one leaf 8 | --> tests/ui/internal_no_leaf.rs:7:10 9 | | 10 | 7 | pub enum EnumEmpty {#[tree(skip)] V} 11 | | ^^^^^^^^^ 12 | 13 | error: Unsupported shape `no fields`. Expected named fields or unnamed fields. 14 | --> tests/ui/internal_no_leaf.rs:9:10 15 | | 16 | 9 | #[derive(Tree)] 17 | | ^^^^ 18 | | 19 | = note: this error originates in the derive macro `Tree` (in Nightly builds, run with -Z macro-backtrace for more info) 20 | 21 | error: Internal nodes must have at least one leaf 22 | --> tests/ui/internal_no_leaf.rs:13:12 23 | | 24 | 13 | pub struct StructUnitTuple (); 25 | | ^^^^^^^^^^^^^^^ 26 | 27 | error: Internal nodes must have at least one leaf 28 | --> tests/ui/internal_no_leaf.rs:16:12 29 | | 30 | 16 | pub struct StructEmptyTuple (#[tree(skip)] ()); 31 | | ^^^^^^^^^^^^^^^^ 32 | 33 | error: Internal nodes must have at least one leaf 34 | --> tests/ui/internal_no_leaf.rs:19:12 35 | | 36 | 19 | pub struct StructEmpty {#[tree(skip)] _f: ()} 37 | | ^^^^^^^^^^^ 38 | -------------------------------------------------------------------------------- /miniconf/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(any(test, feature = "std")), no_std)] 2 | #![cfg_attr(all(feature = "derive", feature = "json-core"), doc = include_str!("../README.md"))] 3 | #![cfg_attr(not(all(feature = "derive", feature = "json-core")), doc = "Miniconf")] 4 | #![deny(rust_2018_compatibility)] 5 | #![deny(rust_2018_idioms)] 6 | #![warn(missing_docs)] 7 | #![forbid(unsafe_code)] 8 | 9 | mod error; 10 | pub use error::*; 11 | mod key; 12 | pub use key::*; 13 | mod schema; 14 | pub use schema::*; 15 | mod shape; 16 | pub use shape::*; 17 | mod packed; 18 | pub use packed::*; 19 | mod jsonpath; 20 | pub use jsonpath::*; 21 | mod tree; 22 | pub use tree::*; 23 | mod iter; 24 | pub use iter::*; 25 | mod impls; 26 | pub use impls::*; 27 | 28 | #[cfg(feature = "derive")] 29 | pub use miniconf_derive::*; 30 | 31 | #[cfg(feature = "json-core")] 32 | pub mod json_core; 33 | 34 | #[cfg(feature = "json")] 35 | pub mod json; 36 | 37 | #[cfg(feature = "postcard")] 38 | pub mod postcard; 39 | 40 | #[cfg(feature = "alloc")] 41 | extern crate alloc; 42 | 43 | #[cfg(feature = "trace")] 44 | pub mod trace; 45 | 46 | #[cfg(feature = "schema")] 47 | pub mod json_schema; 48 | 49 | // re-export for proc-macro 50 | #[doc(hidden)] 51 | pub use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeSeed}; 52 | -------------------------------------------------------------------------------- /miniconf_mqtt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "miniconf_mqtt" 3 | version = "0.20.0" 4 | authors = [ 5 | "James Irwin ", 6 | "Ryan Summers ", 7 | "Robert Jördens ", 8 | ] 9 | edition = "2024" 10 | license = "MIT" 11 | description = "MQTT interface for `miniconf`, using `minimq`" 12 | repository = "https://github.com/quartiq/miniconf" 13 | keywords = ["settings", "serde", "no_std", "json", "mqtt"] 14 | categories = ["no-std", "config", "rust-patterns", "parsing"] 15 | 16 | [lib] 17 | 18 | [dependencies] 19 | miniconf = { version = "0.20.0", features = [ 20 | "json-core", 21 | "heapless", 22 | ], default-features = false, path = "../miniconf" } 23 | minimq = "0.10.0" 24 | smlang = "0.8" 25 | embedded-io = "0.6" 26 | log = "0.4" 27 | heapless = "0.8" 28 | serde-json-core = "0.6.0" 29 | strum = { version = "0.27.1", features = ["derive"], default-features = false } 30 | 31 | [[example]] 32 | name = "mqtt" 33 | 34 | [dev-dependencies] 35 | machine = "0.3" 36 | env_logger = "0.11" 37 | std-embedded-nal = "0.4" 38 | tokio = { version = "1.9", features = ["rt-multi-thread", "time", "macros"] } 39 | std-embedded-time = "0.1" 40 | miniconf = { features = ["json-core", "derive"], path = "../miniconf" } 41 | serde = "1" 42 | heapless = { version = "0.8", features = ["serde"] } 43 | -------------------------------------------------------------------------------- /miniconf/tests/ui/exact_size_iter.stderr: -------------------------------------------------------------------------------- 1 | error[E0107]: associated function takes 0 generic arguments but 1 generic argument was supplied 2 | --> tests/ui/exact_size_iter.rs:3:49 3 | | 4 | 3 | const _: ExactSize> = NodeIter::exact_size::<[(); 1]>(); 5 | | ^^^^^^^^^^----------- help: remove the unnecessary generics 6 | | | 7 | | expected 0 generic arguments 8 | | 9 | note: associated function defined here, with 0 generic parameters 10 | --> src/iter.rs 11 | | 12 | | pub const fn exact_size(schema: &'static Schema) -> ExactSize { 13 | | ^^^^^^^^^^ 14 | 15 | error[E0061]: this function takes 1 argument but 0 arguments were supplied 16 | --> tests/ui/exact_size_iter.rs:3:39 17 | | 18 | 3 | const _: ExactSize> = NodeIter::exact_size::<[(); 1]>(); 19 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-- argument #1 of type `&'static Schema` is missing 20 | | 21 | note: associated function defined here 22 | --> src/iter.rs 23 | | 24 | | pub const fn exact_size(schema: &'static Schema) -> ExactSize { 25 | | ^^^^^^^^^^ 26 | help: provide the argument 27 | | 28 | 3 | const _: ExactSize> = NodeIter::exact_size::<[(); 1]>(/* &'static Schema */); 29 | | +++++++++++++++++++++ 30 | -------------------------------------------------------------------------------- /py/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | set -x 5 | 6 | python -m venv .venv 7 | . .venv/bin/activate 8 | python -m pip install -e py/miniconf-mqtt 9 | 10 | PREFIX=test 11 | 12 | # test no residual DUTs alive 13 | ALIVE=$(timeout --foreground 1 mosquitto_sub -t "$PREFIX/+/alive" -h localhost -F '%p' || true) 14 | test "$ALIVE" = "" -o "$ALIVE" = "0" 15 | 16 | # build and start DUT 17 | cargo build -p miniconf_mqtt --example mqtt 18 | cargo run -p miniconf_mqtt --example mqtt & 19 | DUT_PID=$! 20 | 21 | # check initial dump (9 settings) 22 | # 3 > DUMP_TIMEOUT_SECONDS 23 | DUMP=$(timeout --foreground 3 mosquitto_sub -t "$PREFIX/+/settings/#" -h localhost -F '%t' | wc -l) 24 | test $DUMP = 9 25 | 26 | # test alive-ness 27 | ALIVE=$(timeout --foreground 1 mosquitto_sub -t "$PREFIX/+/alive" -h localhost -F '%p' || true) 28 | test "$ALIVE" = "\"hello\"" 29 | 30 | # no discover SET 31 | python -m miniconf -b localhost $PREFIX/id '/stream="192.0.2.16:9293"' 32 | # discover miniconf command 33 | MC="python -m miniconf -b localhost -d $PREFIX/+" 34 | # GET SET CLEAR LIST DUMP 35 | $MC '/afe/0' '/afe/0="G10"' '/afe/0=' '/afe?' '?' '/afe!' 36 | sleep 1 # DUMP is asynchronous 37 | 38 | # validation ok 39 | $MC '/four=5' 40 | # validation error 41 | $MC '/four=2' && exit 1 42 | 43 | # request exit 44 | $MC '/exit=true' 45 | wait $DUT_PID 46 | 47 | # test exited 48 | ($MC 2>/dev/null && exit 1) || true 49 | # without miniconf 50 | ALIVE=$(timeout --foreground 1 mosquitto_sub -t "$PREFIX/+/alive" -h localhost -F '%p' || true) 51 | test "$ALIVE" = "" 52 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Miniconf"; 3 | inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 | outputs = 5 | { self, nixpkgs }: 6 | let 7 | pkgs = import nixpkgs { system = "x86_64-linux"; }; 8 | aiomqtt22 = pkgs.python3Packages.aiomqtt.overrideAttrs rec { 9 | version = "2.2.0"; 10 | src = pkgs.fetchFromGitHub { 11 | owner = "sbtinstruments"; 12 | repo = "aiomqtt"; 13 | rev = "refs/tags/v${version}"; 14 | hash = "sha256-Sn9wGN93g61tPxuUZbGuElBXqnMEzJilfl3uvnKdIG0="; 15 | }; 16 | propagatedBuildInputs = [ 17 | pkgs.python3Packages.paho-mqtt_2 18 | pkgs.python3Packages.typing-extensions 19 | ]; 20 | }; 21 | miniconf-mqtt-py = pkgs.python3Packages.buildPythonPackage { 22 | pname = "miniconf"; 23 | version = "0.18.0"; 24 | src = self + "/py/miniconf-mqtt"; 25 | format = "pyproject"; 26 | buildInputs = [ 27 | pkgs.python3Packages.setuptools 28 | ]; 29 | propagatedBuildInputs = [ 30 | # pkgs.python3Packages.aiomqtt 31 | aiomqtt22 32 | pkgs.python3Packages.typing-extensions 33 | ]; 34 | # checkPhase = "python -m miniconf"; 35 | }; 36 | in 37 | { 38 | packages.x86_64-linux = { 39 | inherit miniconf-mqtt-py aiomqtt22; 40 | default = miniconf-mqtt-py; 41 | }; 42 | devShells.x86_64-linux.default = pkgs.mkShellNoCC { 43 | name = "miniconf-dev-shell"; 44 | packages = [ miniconf-mqtt-py ]; 45 | }; 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /miniconf/examples/cli.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use miniconf::{IntoKeys, Keys, Path, SerdeError, TreeSchema, ValueError, json_core}; 3 | 4 | mod common; 5 | use common::Settings; 6 | 7 | /// Simple command line interface example for miniconf 8 | /// 9 | /// This exposes the leaf nodes in `Settings` as long options, parses the command line, 10 | /// and then prints the settings struct as a list of option key-value pairs. 11 | 12 | fn main() -> anyhow::Result<()> { 13 | let mut settings = Settings::new(); 14 | settings.enable(); 15 | // Parse args 16 | let mut args = std::env::args().skip(1); 17 | while let Some(key) = args.next() { 18 | let key = key.strip_prefix('-').context("stripping initial dash")?; 19 | let value = args.next().context("looking for value")?; 20 | json_core::set_by_key(&mut settings, Path::<_, '-'>(key), value.as_bytes()) 21 | .context("lookup/deserialize")?; 22 | } 23 | 24 | // Dump settings 25 | let mut buf = vec![0; 1024]; 26 | const MAX_DEPTH: usize = Settings::SCHEMA.shape().max_depth; 27 | for item in Settings::SCHEMA.nodes::, MAX_DEPTH>() { 28 | let key = item.unwrap(); 29 | let mut k = key.into_keys().track(); 30 | match json_core::get_by_key(&settings, &mut k, &mut buf[..]) { 31 | Ok(len) => { 32 | println!("-{} {}", key, core::str::from_utf8(&buf[..len]).unwrap()); 33 | } 34 | Err(SerdeError::Value(ValueError::Absent)) => { 35 | println!("-{} absent (depth: {})", key, k.depth()); 36 | } 37 | err => { 38 | err.unwrap(); 39 | } 40 | } 41 | } 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /miniconf/tests/flatten.rs: -------------------------------------------------------------------------------- 1 | use miniconf::{Tree, json_core}; 2 | 3 | mod common; 4 | use common::*; 5 | 6 | #[derive(Tree, Default, PartialEq, Debug)] 7 | struct Inner { 8 | a: i32, 9 | } 10 | 11 | #[test] 12 | fn struct_flatten() { 13 | #[derive(Tree, Default, PartialEq, Debug)] 14 | #[tree(flatten)] 15 | struct S1 { 16 | a: i32, 17 | } 18 | assert_eq!(paths::(), [""]); 19 | let mut s = S1::default(); 20 | set_get(&mut s, "", b"1"); 21 | assert_eq!(s.a, 1); 22 | 23 | #[derive(Tree, Default, PartialEq, Debug)] 24 | #[tree(flatten)] 25 | struct S2(i32); 26 | assert_eq!(paths::(), [""]); 27 | let mut s = S2::default(); 28 | set_get(&mut s, "", b"1"); 29 | assert_eq!(s.0, 1); 30 | 31 | #[derive(Tree, Default, PartialEq, Debug)] 32 | #[tree(flatten)] 33 | struct S3(Inner); 34 | assert_eq!(paths::(), ["/a"]); 35 | let mut s = S3::default(); 36 | set_get(&mut s, "/a", b"1"); 37 | assert_eq!(s.0.a, 1); 38 | } 39 | 40 | #[test] 41 | fn enum_flatten() { 42 | #[derive(Tree, Default, PartialEq, Debug)] 43 | #[tree(flatten)] 44 | enum E1 { 45 | #[default] 46 | None, 47 | A(i32), 48 | } 49 | assert_eq!(paths::(), [""]); 50 | let mut e = E1::A(0); 51 | set_get(&mut e, "", b"1"); 52 | assert_eq!(e, E1::A(1)); 53 | assert_eq!( 54 | json_core::set(&mut E1::None, "", b"1").unwrap_err(), 55 | miniconf::ValueError::Absent.into() 56 | ); 57 | 58 | #[derive(Tree, Default, PartialEq, Debug)] 59 | #[tree(flatten)] 60 | enum E2 { 61 | #[default] 62 | None, 63 | A(Inner), 64 | } 65 | assert_eq!(paths::(), ["/a"]); 66 | let mut e = E2::A(Inner::default()); 67 | set_get(&mut e, "/a", b"1"); 68 | assert_eq!(e, E2::A(Inner { a: 1 })); 69 | assert_eq!( 70 | json_core::set(&mut E2::None, "/a", b"1").unwrap_err(), 71 | miniconf::ValueError::Absent.into() 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /miniconf/examples/common.rs: -------------------------------------------------------------------------------- 1 | use miniconf::{Leaf, Tree, leaf}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | // Either/Inner/Settings are straight from README.md 5 | 6 | /// Inner doc 7 | #[derive(Deserialize, Serialize, Default, Tree)] 8 | #[tree(meta(doc, typename))] 9 | pub struct MyStruct { 10 | #[tree(meta(max = "10"))] 11 | a: i32, 12 | /// Outer doc 13 | b: u8, 14 | } 15 | 16 | /// Inner doc 17 | #[derive(Deserialize, Serialize, Default, Tree)] 18 | #[tree(meta(doc, typename))] 19 | pub enum MyEnum { 20 | #[default] 21 | Bad, 22 | Good, 23 | A(i32), 24 | /// Outer doc 25 | B(MyStruct), 26 | C([MyStruct; 2]), 27 | } 28 | 29 | #[derive(Deserialize, Serialize, Default)] 30 | pub struct Uni; 31 | 32 | #[derive(Tree, Default)] 33 | #[tree(meta(typename))] 34 | pub struct Settings { 35 | foo: bool, 36 | #[tree(with=leaf)] 37 | enum_: MyEnum, 38 | #[tree(with=leaf)] 39 | struct_: MyStruct, 40 | #[tree(with=leaf)] 41 | array: [i32; 2], 42 | #[tree(with=leaf)] 43 | option: Option, 44 | #[tree(with=leaf)] 45 | uni: Uni, 46 | 47 | #[tree(skip)] 48 | #[allow(unused)] 49 | skipped: (), 50 | 51 | struct_tree: MyStruct, 52 | enum_tree: MyEnum, 53 | array_tree: [i32; 2], 54 | array_tree2: [MyStruct; 2], 55 | tuple_tree: (i32, MyStruct), 56 | option_tree: Option, 57 | option_tree2: Option, 58 | array_option_tree: [Option; 2], 59 | option_array: Option>, 60 | } 61 | 62 | #[allow(unused)] 63 | impl Settings { 64 | /// Create a new enabled Settings 65 | pub fn new() -> Self { 66 | let mut s = Self::default(); 67 | s.enable(); 68 | s 69 | } 70 | 71 | /// Fill some of the Options 72 | pub fn enable(&mut self) { 73 | self.option_tree = Some(8); 74 | self.enum_tree = MyEnum::C(Default::default()); 75 | self.option_tree2 = Some(Default::default()); 76 | self.array_option_tree[1] = Some(Default::default()); 77 | self.option_array = Some(Leaf([1, 2])); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /miniconf/tests/embedded/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #![no_std] 3 | #![cfg_attr(feature = "used_linker", feature(used_with_arg))] 4 | extern crate panic_semihosting; 5 | 6 | use core::any::Any; 7 | 8 | use cortex_m_rt::entry; 9 | use cortex_m_semihosting::{debug, hprintln}; 10 | 11 | use crosstrait::{Cast, register}; 12 | use miniconf::{ 13 | self, IntoKeys, JsonPath, Packed, Path, Shape, Tree, TreeAny, TreeSchema, json_core, 14 | }; 15 | 16 | use core::ops::{AddAssign, SubAssign}; 17 | register! { i32 => dyn AddAssign } 18 | register! { u32 => dyn SubAssign } 19 | 20 | #[derive(Default, Tree)] 21 | struct Inner { 22 | val: i32, 23 | } 24 | 25 | #[derive(Default, Tree)] 26 | struct Settings { 27 | a: [i32; 2], 28 | i: [Inner; 3], 29 | b: Option, 30 | } 31 | 32 | #[entry] 33 | fn main() -> ! { 34 | assert_eq!(crosstrait::REGISTRY_KV.len(), 2); 35 | hprintln!( 36 | "registry RAM: {}", 37 | core::mem::size_of_val(&crosstrait::REGISTRY) 38 | ); 39 | 40 | let mut a = 3i32; 41 | let any: &mut dyn Any = &mut a; 42 | 43 | let val: &mut dyn AddAssign = any.cast().unwrap(); 44 | *val += 5; 45 | assert_eq!(a, 3 + 5); 46 | 47 | let mut s = Settings::default(); 48 | 49 | let path = Path::<_, '/'>("/i/1/val"); 50 | json_core::set_by_key(&mut s, &path, b"3").unwrap(); 51 | 52 | let packed: Packed = Settings::SCHEMA.transcode(&path).unwrap(); 53 | assert_eq!(packed.into_lsb().get(), 0b1_01_01_0); 54 | 55 | let mut buf = [0; 10]; 56 | let len = json_core::get_by_key(&s, packed, &mut buf).unwrap(); 57 | assert_eq!(&buf[..len], b"3"); 58 | 59 | let key = JsonPath(".i[1].val"); 60 | let any = s.mut_any_by_key(key.into_keys()).unwrap(); 61 | 62 | let val: &mut dyn AddAssign = any.cast().unwrap(); 63 | *val += 5; 64 | assert_eq!(s.i[1].val, 3 + 5); 65 | 66 | const SHAPE: Shape = Settings::SCHEMA.shape(); 67 | hprintln!("Settings: {:?}", SHAPE); 68 | 69 | hprintln!("success!"); 70 | 71 | // exit QEMU 72 | debug::exit(debug::EXIT_SUCCESS); 73 | 74 | loop {} 75 | } 76 | -------------------------------------------------------------------------------- /miniconf/src/postcard.rs: -------------------------------------------------------------------------------- 1 | //! `TreeSerialize`/`TreeDeserialize` with `postcard`. 2 | //! 3 | //! ``` 4 | //! use ::postcard::{de_flavors::Slice, ser_flavors::AllocVec}; 5 | //! use miniconf::{postcard, Leaf, Packed, Tree, TreeSchema}; 6 | //! 7 | //! #[derive(Tree, Default, PartialEq, Debug)] 8 | //! struct S { 9 | //! foo: u32, 10 | //! bar: [u16; 2], 11 | //! }; 12 | //! 13 | //! let source = S { 14 | //! foo: 9, 15 | //! bar: [7, 11], 16 | //! }; 17 | //! let kv: Vec<_> = S::SCHEMA.nodes::() 18 | //! .map(|p| { 19 | //! let p = p.unwrap(); 20 | //! let v = postcard::get_by_key(&source, p, AllocVec::new()).unwrap(); 21 | //! (p.into_lsb().get(), v) 22 | //! }) 23 | //! .collect(); 24 | //! assert_eq!(kv, [(2, vec![9]), (6, vec![7]), (7, vec![11])]); 25 | //! 26 | //! let mut target = S::default(); 27 | //! for (k, v) in kv { 28 | //! let p = Packed::from_lsb(k.try_into().unwrap()); 29 | //! postcard::set_by_key(&mut target, p, Slice::new(&v[..])).unwrap(); 30 | //! } 31 | //! assert_eq!(source, target); 32 | //! ``` 33 | 34 | use postcard::{Deserializer, Serializer, de_flavors, ser_flavors}; 35 | 36 | use crate::{IntoKeys, SerdeError, TreeDeserialize, TreeSerialize}; 37 | 38 | /// Deserialize and set a node value from a `postcard` flavor. 39 | pub fn set_by_key<'de, F: de_flavors::Flavor<'de>>( 40 | tree: &mut (impl TreeDeserialize<'de> + ?Sized), 41 | keys: impl IntoKeys, 42 | flavor: F, 43 | ) -> Result> { 44 | let mut de = Deserializer::from_flavor(flavor); 45 | tree.deserialize_by_key(keys.into_keys(), &mut de)?; 46 | de.finalize().map_err(SerdeError::Finalization) 47 | } 48 | 49 | /// Get and serialize a node value into a `postcard` flavor. 50 | pub fn get_by_key( 51 | tree: &(impl TreeSerialize + ?Sized), 52 | keys: impl IntoKeys, 53 | flavor: F, 54 | ) -> Result> { 55 | let mut ser = Serializer { output: flavor }; 56 | tree.serialize_by_key(keys.into_keys(), &mut ser)?; 57 | ser.output.finalize().map_err(SerdeError::Finalization) 58 | } 59 | -------------------------------------------------------------------------------- /miniconf/tests/iter.rs: -------------------------------------------------------------------------------- 1 | use miniconf::{Indices, NodeIter, Path, Short, Tree, TreeSchema}; 2 | 3 | mod common; 4 | use common::*; 5 | 6 | #[derive(Tree, Default, PartialEq, Debug)] 7 | struct Inner { 8 | inner: bool, 9 | } 10 | 11 | #[derive(Tree, Default, PartialEq, Debug)] 12 | struct Settings { 13 | b: [bool; 2], 14 | c: Inner, 15 | d: [Inner; 1], 16 | a: bool, 17 | } 18 | 19 | #[test] 20 | fn struct_iter() { 21 | assert_eq!( 22 | paths::(), 23 | ["/b/0", "/b/1", "/c/inner", "/d/0/inner", "/a"] 24 | ); 25 | } 26 | 27 | #[test] 28 | fn struct_iter_indices() { 29 | let paths = [ 30 | ([0, 0, 0], 2), 31 | ([0, 1, 0], 2), 32 | ([1, 0, 0], 2), 33 | ([2, 0, 0], 3), 34 | ([3, 0, 0], 1), 35 | ]; 36 | assert_eq!( 37 | Settings::SCHEMA 38 | .nodes::, 3>() 39 | .map(|have| have.unwrap().into_inner()) 40 | .collect::>(), 41 | paths 42 | ); 43 | } 44 | 45 | #[test] 46 | fn array_iter() { 47 | let mut s = Settings::default(); 48 | 49 | for field in paths::() { 50 | set_get(&mut s, &field, b"true"); 51 | } 52 | 53 | assert!(s.a); 54 | assert!(s.b.iter().all(|x| *x)); 55 | assert!(s.c.inner); 56 | assert!(s.d.iter().all(|i| i.inner)); 57 | } 58 | 59 | #[test] 60 | fn short_iter() { 61 | assert_eq!( 62 | NodeIter::>, 1>::new(Settings::SCHEMA) 63 | .map(|p| p.unwrap().into_inner().0.into_inner()) 64 | .collect::>(), 65 | ["/b", "/c", "/d", "/a"] 66 | ); 67 | 68 | assert_eq!( 69 | NodeIter::>, 0>::new(Settings::SCHEMA) 70 | .next() 71 | .unwrap() 72 | .unwrap() 73 | .into_inner() 74 | .0 75 | .into_inner(), 76 | "" 77 | ); 78 | } 79 | 80 | #[test] 81 | #[should_panic] 82 | fn panic_short_iter() { 83 | <[[u32; 1]; 1]>::SCHEMA.nodes::<(), 1>(); 84 | } 85 | 86 | #[test] 87 | fn root() { 88 | assert_eq!( 89 | NodeIter::, 3>::with_root(Settings::SCHEMA, ["b"]) 90 | .unwrap() 91 | .map(|p| p.unwrap().into_inner()) 92 | .collect::>(), 93 | ["/b/0", "/b/1"] 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /miniconf_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use darling::FromDeriveInput; 2 | use proc_macro::TokenStream; 3 | use syn::{DeriveInput, parse_macro_input}; 4 | 5 | mod field; 6 | mod tree; 7 | use tree::Tree; 8 | 9 | /// Derive the `TreeSchema` trait for a struct or enum. 10 | /// 11 | /// This also derives `KeyLookup` if necessary. 12 | #[proc_macro_derive(TreeSchema, attributes(tree))] 13 | pub fn derive_tree_schema(input: TokenStream) -> TokenStream { 14 | match Tree::from_derive_input(&parse_macro_input!(input as DeriveInput)) { 15 | Ok(t) => t.tree_schema(), 16 | Err(e) => e.write_errors(), 17 | } 18 | .into() 19 | } 20 | 21 | /// Derive the `TreeSerialize` trait for a struct or enum. 22 | #[proc_macro_derive(TreeSerialize, attributes(tree))] 23 | pub fn derive_tree_serialize(input: TokenStream) -> TokenStream { 24 | match Tree::from_derive_input(&parse_macro_input!(input as DeriveInput)) { 25 | Ok(t) => t.tree_serialize(), 26 | Err(e) => e.write_errors(), 27 | } 28 | .into() 29 | } 30 | 31 | /// Derive the `TreeDeserialize` trait for a struct or enum. 32 | #[proc_macro_derive(TreeDeserialize, attributes(tree))] 33 | pub fn derive_tree_deserialize(input: TokenStream) -> TokenStream { 34 | match Tree::from_derive_input(&parse_macro_input!(input as DeriveInput)) { 35 | Ok(t) => t.tree_deserialize(), 36 | Err(e) => e.write_errors(), 37 | } 38 | .into() 39 | } 40 | 41 | /// Derive the `TreeAny` trait for a struct or enum. 42 | #[proc_macro_derive(TreeAny, attributes(tree))] 43 | pub fn derive_tree_any(input: TokenStream) -> TokenStream { 44 | match Tree::from_derive_input(&parse_macro_input!(input as DeriveInput)) { 45 | Ok(t) => t.tree_any(), 46 | Err(e) => e.write_errors(), 47 | } 48 | .into() 49 | } 50 | 51 | /// Derive the `TreeSchema`, `TreeSerialize`, `TreeDeserialize`, and `TreeAny` traits for a struct or enum. 52 | /// 53 | /// This is a shorthand to derive multiple traits. 54 | #[proc_macro_derive(Tree, attributes(tree))] 55 | pub fn derive_tree(input: TokenStream) -> TokenStream { 56 | match Tree::from_derive_input(&parse_macro_input!(input as DeriveInput)) { 57 | Ok(t) => [ 58 | t.tree_schema(), 59 | t.tree_serialize(), 60 | t.tree_deserialize(), 61 | t.tree_any(), 62 | ] 63 | .into_iter() 64 | .collect(), 65 | Err(e) => e.write_errors(), 66 | } 67 | .into() 68 | } 69 | -------------------------------------------------------------------------------- /miniconf_mqtt/examples/mqtt.rs: -------------------------------------------------------------------------------- 1 | use heapless::String; 2 | use miniconf::{Leaf, Tree, TreeSchema, leaf}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::time::Duration; 5 | use std_embedded_nal::Stack; 6 | use std_embedded_time::StandardClock; 7 | 8 | #[derive(Clone, Default, Tree, Debug)] 9 | struct Inner { 10 | a: u32, 11 | } 12 | 13 | #[derive(Copy, Clone, Default, Debug, Serialize, Deserialize)] 14 | enum Gain { 15 | #[default] 16 | G1, 17 | G10, 18 | G100, 19 | } 20 | 21 | #[derive(Clone, Default, Tree, Debug)] 22 | struct Settings { 23 | stream: String<32>, 24 | afe: [Leaf; 2], 25 | inner: Inner, 26 | values: [f32; 2], 27 | #[tree(with=leaf)] 28 | array: [i32; 4], 29 | opt: Option, 30 | #[tree(with=four)] 31 | four: f32, 32 | exit: bool, 33 | } 34 | 35 | mod four { 36 | use miniconf::{Deserializer, Keys, SerdeError, TreeDeserialize, ValueError, leaf}; 37 | 38 | pub use leaf::{SCHEMA, mut_any_by_key, probe_by_key, ref_any_by_key, serialize_by_key}; 39 | 40 | pub fn deserialize_by_key<'de, D: Deserializer<'de>>( 41 | value: &mut f32, 42 | keys: impl Keys, 43 | de: D, 44 | ) -> Result<(), SerdeError> { 45 | let mut old = *value; 46 | old.deserialize_by_key(keys, de)?; 47 | if old < 4.0 { 48 | Err(ValueError::Access("Less than four").into()) 49 | } else { 50 | *value = old; 51 | Ok(()) 52 | } 53 | } 54 | } 55 | 56 | #[tokio::main] 57 | async fn main() { 58 | env_logger::init(); 59 | 60 | let mut buffer = [0u8; 1024]; 61 | let localhost: core::net::IpAddr = "127.0.0.1".parse().unwrap(); 62 | 63 | const MAX_DEPTH: usize = Settings::SCHEMA.shape().max_depth; 64 | 65 | // Construct a settings configuration interface. 66 | let mut client = miniconf_mqtt::MqttClient::<_, _, _, _, MAX_DEPTH>::new( 67 | Stack, 68 | "test/id", 69 | StandardClock::default(), 70 | minimq::ConfigBuilder::::new(localhost.into(), &mut buffer) 71 | .keepalive_interval(60), 72 | ) 73 | .unwrap(); 74 | client.set_alive("\"hello\""); 75 | 76 | let mut settings = Settings::default(); 77 | while !settings.exit { 78 | tokio::time::sleep(Duration::from_millis(10)).await; 79 | if client.update(&mut settings).unwrap() { 80 | println!("Settings updated: {:?}", settings); 81 | } 82 | } 83 | println!("Exiting on request"); 84 | } 85 | -------------------------------------------------------------------------------- /miniconf/tests/enum.rs: -------------------------------------------------------------------------------- 1 | use miniconf::{SerdeError, Tree, ValueError, json_core, str_leaf}; 2 | 3 | mod common; 4 | use common::*; 5 | 6 | #[derive(Tree, Default, PartialEq, Debug)] 7 | struct Inner { 8 | a: i32, 9 | } 10 | 11 | #[derive( 12 | Tree, 13 | Default, 14 | PartialEq, 15 | Debug, 16 | strum::EnumString, 17 | strum::AsRefStr, 18 | strum::FromRepr, 19 | strum::EnumDiscriminants, 20 | )] 21 | #[strum_discriminants(derive(Default, serde::Serialize, serde::Deserialize))] 22 | enum Enum { 23 | #[default] 24 | #[strum_discriminants(default)] 25 | None, 26 | #[strum(serialize = "foo")] 27 | #[strum_discriminants(serde(rename = "foo"))] 28 | #[tree(rename = "foo")] 29 | A(i32), 30 | B(Inner), 31 | } 32 | 33 | #[derive(Tree, Default, Debug)] 34 | struct Settings { 35 | // note the order allows sequential deseserialization 36 | #[tree(rename="tag", with=str_leaf, defer=self.enu, typ="Enum")] 37 | _tag: (), 38 | enu: Enum, 39 | } 40 | 41 | #[test] 42 | fn enum_switch() { 43 | let mut s = Settings::default(); 44 | assert_eq!(s.enu, Enum::None); 45 | set_get(&mut s, "/tag", b"\"foo\""); 46 | assert!(matches!( 47 | json_core::set(&mut s, "/tag", b"\"bar\""), 48 | Err(SerdeError::Value(ValueError::Access(_))) 49 | )); 50 | assert_eq!(s.enu, Enum::A(0)); 51 | set_get(&mut s, "/enu/foo", b"99"); 52 | assert_eq!(s.enu, Enum::A(99)); 53 | assert_eq!( 54 | json_core::set(&mut s, "/enu/B/a", b"99"), 55 | Err(ValueError::Absent.into()) 56 | ); 57 | set_get(&mut s, "/tag", b"\"B\""); 58 | set_get(&mut s, "/enu/B/a", b"8"); 59 | assert_eq!(s.enu, Enum::B(Inner { a: 8 })); 60 | 61 | assert_eq!(paths::(), ["/tag", "/enu/foo", "/enu/B/a",]); 62 | } 63 | 64 | #[test] 65 | fn enum_skip() { 66 | struct S; 67 | 68 | #[allow(dead_code)] 69 | #[derive(Tree)] 70 | enum E { 71 | A(i32, #[tree(skip)] i32), 72 | #[tree(skip)] 73 | B(S), 74 | C, 75 | D, 76 | } 77 | assert_eq!(paths::(), ["/A"]); 78 | } 79 | 80 | #[test] 81 | fn option() { 82 | // Also tests macro hygiene a bit 83 | #[allow(dead_code)] 84 | #[derive(Tree, Copy, Clone, PartialEq, Default, Debug)] 85 | #[tree(flatten)] 86 | enum Option { 87 | #[default] 88 | None, 89 | Some(T), 90 | } 91 | assert_eq!(paths::, 1>(), ["/0"]); 92 | assert_eq!(paths::>, 1>(), [""]); 93 | } 94 | -------------------------------------------------------------------------------- /miniconf/src/json.rs: -------------------------------------------------------------------------------- 1 | //! Utilities using `serde_json` 2 | use serde_json::value::{Serializer as ValueSerializer, Value}; 3 | 4 | use crate::{ 5 | Internal, IntoKeys, KeyError, Schema, SerdeError, TreeSerialize, ValueError, 6 | json_schema::{TREE_ABSENT, TREE_ACCESS}, 7 | }; 8 | 9 | /// Serialize a TreeSerialize into a JSON Value 10 | pub fn to_json_value( 11 | value: &T, 12 | ) -> Result::Error>> { 13 | fn visit( 14 | idx: &mut [usize], 15 | depth: usize, 16 | schema: &Schema, 17 | value: &T, 18 | ) -> Result::Error>> { 19 | match value.serialize_by_key((&idx[..depth]).into_keys(), ValueSerializer) { 20 | Ok(v) => Ok(v), 21 | Err(SerdeError::Value(ValueError::Absent)) => { 22 | Ok(Value::String(TREE_ABSENT.to_string())) 23 | } 24 | Err(SerdeError::Value(ValueError::Access(_msg))) => { 25 | Ok(Value::String(TREE_ACCESS.to_string())) 26 | } 27 | Err(SerdeError::Value(ValueError::Key(KeyError::TooShort))) => { 28 | Ok(match schema.internal.as_ref().unwrap() { 29 | Internal::Homogeneous(h) => Value::Array( 30 | (0..h.len.get()) 31 | .map(|i| { 32 | idx[depth] = i; 33 | visit(idx, depth + 1, h.schema, value) 34 | }) 35 | .collect::>()?, 36 | ), 37 | Internal::Named(n) => Value::Object( 38 | n.iter() 39 | .enumerate() 40 | .map(|(i, n)| { 41 | idx[depth] = i; 42 | Ok((n.name.to_string(), visit(idx, depth + 1, n.schema, value)?)) 43 | }) 44 | .collect::>>()?, 45 | ), 46 | Internal::Numbered(n) => Value::Array( 47 | n.iter() 48 | .enumerate() 49 | .map(|(i, n)| { 50 | idx[depth] = i; 51 | visit(idx, depth + 1, n.schema, value) 52 | }) 53 | .collect::>()?, 54 | ), 55 | }) 56 | } 57 | Err(err) => Err(err), 58 | } 59 | } 60 | visit( 61 | &mut vec![0; T::SCHEMA.shape().max_depth], 62 | 0, 63 | T::SCHEMA, 64 | value, 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /miniconf/src/json_core.rs: -------------------------------------------------------------------------------- 1 | //! `TreeSerialize`/`TreeDeserialize` with "JSON and `/`". 2 | //! 3 | //! Access items with `'/'` as path separator and JSON (from `serde-json-core`) 4 | //! as serialization/deserialization payload format. 5 | //! 6 | //! Paths used here are reciprocal to `TreeSchema::lookup::, _>(...)`/ 7 | //! `TreeSchema::SCHEMA.nodes::>()`. 8 | //! 9 | //! ``` 10 | //! use miniconf::{json_core, Tree}; 11 | //! #[derive(Tree, Default)] 12 | //! struct S { 13 | //! foo: u32, 14 | //! bar: [u16; 2], 15 | //! }; 16 | //! let mut s = S::default(); 17 | //! json_core::set(&mut s, "/bar/1", b"9").unwrap(); 18 | //! assert_eq!(s.bar[1], 9); 19 | //! let mut buf = [0u8; 10]; 20 | //! let len = json_core::get(&mut s, "/bar/1", &mut buf[..]).unwrap(); 21 | //! assert_eq!(&buf[..len], b"9"); 22 | //! ``` 23 | 24 | use serde_json_core::{de, ser}; 25 | 26 | use crate::{IntoKeys, Path, SerdeError, TreeDeserialize, TreeSerialize}; 27 | 28 | /// Update a node by path. 29 | /// 30 | /// # Args 31 | /// * `tree` - The `TreeDeserialize` to operate on. 32 | /// * `path` - The path to the node. Everything before the first `'/'` is ignored. 33 | /// * `data` - The serialized data making up the content. 34 | /// 35 | /// # Returns 36 | /// The number of bytes consumed from `data` or an [SerdeError]. 37 | #[inline] 38 | pub fn set<'de>( 39 | tree: &mut (impl TreeDeserialize<'de> + ?Sized), 40 | path: &str, 41 | data: &'de [u8], 42 | ) -> Result> { 43 | set_by_key(tree, Path::<_, '/'>(path), data) 44 | } 45 | 46 | /// Retrieve a serialized value by path. 47 | /// 48 | /// # Args 49 | /// * `tree` - The `TreeDeserialize` to operate on. 50 | /// * `path` - The path to the node. Everything before the first `'/'` is ignored. 51 | /// * `data` - The buffer to serialize the data into. 52 | /// 53 | /// # Returns 54 | /// The number of bytes used in the `data` buffer or an [SerdeError]. 55 | #[inline] 56 | pub fn get( 57 | tree: &(impl TreeSerialize + ?Sized), 58 | path: &str, 59 | data: &mut [u8], 60 | ) -> Result> { 61 | get_by_key(tree, Path::<_, '/'>(path), data) 62 | } 63 | 64 | /// Update a node by key. 65 | /// 66 | /// # Returns 67 | /// The number of bytes consumed from `data` or an [SerdeError]. 68 | pub fn set_by_key<'de>( 69 | tree: &mut (impl TreeDeserialize<'de> + ?Sized), 70 | keys: impl IntoKeys, 71 | data: &'de [u8], 72 | ) -> Result> { 73 | let mut de = de::Deserializer::new(data, None); 74 | tree.deserialize_by_key(keys.into_keys(), &mut de)?; 75 | de.end().map_err(SerdeError::Finalization) 76 | } 77 | 78 | /// Retrieve a serialized value by key. 79 | /// 80 | /// # Returns 81 | /// The number of bytes used in the `data` buffer or an [SerdeError]. 82 | pub fn get_by_key( 83 | tree: &(impl TreeSerialize + ?Sized), 84 | keys: impl IntoKeys, 85 | data: &mut [u8], 86 | ) -> Result> { 87 | let mut ser = ser::Serializer::new(data); 88 | tree.serialize_by_key(keys.into_keys(), &mut ser)?; 89 | Ok(ser.end()) 90 | } 91 | -------------------------------------------------------------------------------- /miniconf/tests/generics.rs: -------------------------------------------------------------------------------- 1 | use miniconf::{ 2 | Deserialize, ExactSize, Leaf, NodeIter, Serialize, Shape, Tree, TreeSchema, json_core, 3 | }; 4 | 5 | #[test] 6 | fn generic_type() { 7 | #[derive(Tree, Default)] 8 | struct Settings { 9 | pub data: T, 10 | } 11 | 12 | let mut settings = Settings::::default(); 13 | json_core::set(&mut settings, "/data", b"3.0").unwrap(); 14 | assert_eq!(settings.data, 3.0); 15 | 16 | const SHAPE: Shape = Settings::::SCHEMA.shape(); 17 | assert_eq!(SHAPE.max_depth, 1); 18 | assert_eq!(SHAPE.max_length, "data".len()); 19 | assert_eq!(SHAPE.count.get(), 1); 20 | } 21 | 22 | #[test] 23 | fn generic_array() { 24 | #[derive(Tree, Default)] 25 | struct Settings { 26 | pub data: [T; 2], 27 | } 28 | 29 | let mut settings = Settings::::default(); 30 | json_core::set(&mut settings, "/data/0", b"3.0").unwrap(); 31 | 32 | assert_eq!(settings.data[0], 3.0); 33 | 34 | const SHAPE: Shape = Settings::::SCHEMA.shape(); 35 | assert_eq!(SHAPE.max_depth, 2); 36 | assert_eq!(SHAPE.max_length("/"), "/data/0".len()); 37 | assert_eq!(SHAPE.count.get(), 2); 38 | } 39 | 40 | #[test] 41 | fn generic_struct() { 42 | #[derive(Tree, Default)] 43 | struct Settings { 44 | pub inner: T, 45 | } 46 | 47 | #[derive(Serialize, Deserialize, Default)] 48 | struct Inner { 49 | pub data: f32, 50 | } 51 | 52 | let mut settings = Settings::>::default(); 53 | json_core::set(&mut settings, "/inner", b"{\"data\": 3.0}").unwrap(); 54 | 55 | assert_eq!(settings.inner.data, 3.0); 56 | 57 | const SHAPE: Shape = Settings::::SCHEMA.shape(); 58 | assert_eq!(SHAPE.max_depth, 1); 59 | assert_eq!(SHAPE.max_length("/"), "/inner".len()); 60 | assert_eq!(SHAPE.count.get(), 1); 61 | } 62 | 63 | #[test] 64 | fn generic_atomic() { 65 | #[derive(Tree, Default)] 66 | struct Settings { 67 | atomic: Leaf>, 68 | opt: [[Leaf>; 1]; 1], 69 | opt1: [[Option; 1]; 1], 70 | } 71 | 72 | #[derive(Deserialize, Serialize, Default)] 73 | struct Inner { 74 | inner: [T; 5], 75 | } 76 | 77 | let mut settings = Settings::::default(); 78 | json_core::set(&mut settings, "/atomic", b"{\"inner\": [3.0, 0, 0, 0, 0]}").unwrap(); 79 | 80 | assert_eq!(settings.atomic.inner[0], 3.0); 81 | 82 | // Test metadata 83 | const SHAPE: Shape = Settings::::SCHEMA.shape(); 84 | assert_eq!(SHAPE.max_depth, 3); 85 | assert_eq!(SHAPE.max_length("/"), "/opt1/0/0".len()); 86 | } 87 | 88 | #[test] 89 | fn test_depth() { 90 | #[derive(Tree)] 91 | struct S(Option>); 92 | 93 | // This works as array implements TreeSchema 94 | let _ = S::<[u32; 1]>::SCHEMA.shape(); 95 | 96 | // u32 implements TreeSchema as well 97 | let _ = S::::SCHEMA.shape(); 98 | 99 | // Depth is always statically known 100 | // .. but can't be used in const generics yet, 101 | // i.e. we can't name the type. 102 | let _ = [0usize; S::<[u32; 1]>::SCHEMA.shape().max_depth]; 103 | 104 | const _: ExactSize> = >::SCHEMA.nodes(); 105 | } 106 | -------------------------------------------------------------------------------- /miniconf/tests/basic.rs: -------------------------------------------------------------------------------- 1 | use miniconf::{Indices, KeyError, Path, Shape, Short, Track, Tree, TreeSchema}; 2 | mod common; 3 | 4 | #[test] 5 | fn borrowed() { 6 | let mut a = ""; 7 | miniconf::json_core::set(&mut a, "", "\"foo\"".as_bytes()).unwrap(); 8 | assert_eq!(a, "foo"); 9 | } 10 | 11 | #[cfg(feature = "postcard")] 12 | #[test] 13 | fn borrowed_u8() { 14 | use postcard::{de_flavors::Slice, to_slice}; 15 | 16 | let mut a = &[0u8; 0][..]; 17 | let mut buf = [0u8; 32]; 18 | let data = to_slice(&[1u8, 2, 3][..], &mut buf).unwrap(); 19 | miniconf::postcard::set_by_key(&mut a, [0; 0], Slice::new(data)).unwrap(); 20 | assert_eq!(a, &[1, 2, 3]); 21 | } 22 | 23 | #[derive(Tree, Default)] 24 | struct Inner { 25 | inner: f32, 26 | } 27 | 28 | #[derive(Tree, Default)] 29 | struct Settings { 30 | a: f32, 31 | b: i32, 32 | c: Inner, 33 | } 34 | 35 | #[test] 36 | fn meta() { 37 | const SHAPE: Shape = Settings::SCHEMA.shape(); 38 | assert_eq!(SHAPE.max_depth, 2); 39 | assert_eq!(SHAPE.max_length("/"), "/c/inner".len()); 40 | assert_eq!(SHAPE.count.get(), 3); 41 | } 42 | 43 | #[test] 44 | fn path() { 45 | for (keys, path, depth, leaf) in [ 46 | (&[1usize][..], "/b", 1, true), 47 | (&[2, 0], "/c/inner", 2, true), 48 | (&[2], "/c", 1, false), 49 | (&[], "", 0, false), 50 | ] { 51 | let s = Settings::SCHEMA 52 | .transcode::>>>(keys) 53 | .unwrap(); 54 | assert_eq!(depth, s.inner().depth()); 55 | assert_eq!(leaf, s.leaf()); 56 | assert_eq!(s.inner().inner().as_ref(), path); 57 | } 58 | } 59 | 60 | #[test] 61 | fn indices() { 62 | for (keys, idx, leaf) in [ 63 | ("", &[][..], false), 64 | ("/b", &[1], true), 65 | ("/c/inner", &[2, 0], true), 66 | ("/c", &[2], false), 67 | ] { 68 | let indices = Settings::SCHEMA 69 | .transcode::>>(Path::<_, '/'>(keys)) 70 | .unwrap(); 71 | println!("{keys} {indices:?}"); 72 | assert_eq!(indices.leaf(), leaf); 73 | assert_eq!(indices.inner().as_ref(), idx); 74 | } 75 | let indices = Option::::SCHEMA 76 | .transcode::>>([0usize; 0]) 77 | .unwrap(); 78 | assert_eq!(indices.inner().as_ref(), [0usize; 0]); 79 | assert_eq!(indices.leaf(), true); 80 | assert_eq!(indices.inner().len(), 0); 81 | 82 | let mut it = [0usize; 4].into_iter(); 83 | assert_eq!( 84 | Settings::SCHEMA.transcode::>(&mut it), 85 | Err(KeyError::TooLong.into()) 86 | ); 87 | assert_eq!(it.count(), 2); 88 | } 89 | 90 | #[test] 91 | fn tuple() { 92 | type T = (u32, (i32, u8), [u16; 3]); 93 | let paths = common::paths::(); 94 | assert_eq!(paths.len(), 6); 95 | let mut s: T = Default::default(); 96 | for p in paths { 97 | common::set_get(&mut s, p.as_str(), b"9"); 98 | } 99 | assert_eq!(s, (9, (9, 9), [9; 3])); 100 | } 101 | 102 | #[test] 103 | fn cell() { 104 | use core::cell::RefCell; 105 | 106 | let c: RefCell = Default::default(); 107 | let mut r = &c; 108 | common::set_get(&mut r, "", b"9"); 109 | } 110 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: [main, develop] 6 | pull_request: 7 | branches: [main, develop] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | style: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: dtolnay/rust-toolchain@stable 18 | with: 19 | components: rustfmt, clippy 20 | 21 | - run: cargo fmt -- --check 22 | - run: cargo check 23 | 24 | - run: cargo clippy --all-features 25 | 26 | - uses: actions/setup-python@v4 27 | - name: Install Pylint 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install pylint 31 | 32 | - name: Run Pylint 33 | working-directory: py/miniconf-mqtt 34 | run: | 35 | python -m pip install . 36 | python -m pylint miniconf 37 | 38 | documentation: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v4 42 | 43 | - uses: dtolnay/rust-toolchain@stable 44 | 45 | - run: cargo doc 46 | 47 | compile: 48 | runs-on: ubuntu-latest 49 | strategy: 50 | matrix: 51 | toolchain: 52 | - stable 53 | 54 | steps: 55 | - uses: actions/checkout@v4 56 | - name: Install Rust ${{ matrix.toolchain }} 57 | uses: dtolnay/rust-toolchain@master 58 | with: 59 | toolchain: ${{ matrix.toolchain }} 60 | - run: cargo check --verbose 61 | - run: cargo build --no-default-features 62 | - run: cargo build 63 | - run: cargo build --release 64 | - run: cargo build --examples 65 | 66 | test: 67 | runs-on: ubuntu-latest 68 | strategy: 69 | matrix: 70 | toolchain: 71 | - stable 72 | args: 73 | - "" 74 | - --no-default-features 75 | steps: 76 | - uses: actions/checkout@v4 77 | - name: Start Broker 78 | run: | 79 | sudo apt-get install mosquitto mosquitto-clients 80 | sudo service mosquitto start 81 | - uses: dtolnay/rust-toolchain@master 82 | with: 83 | toolchain: ${{matrix.toolchain}} 84 | - run: cargo test ${{matrix.args }} 85 | 86 | embedded: 87 | runs-on: ubuntu-latest 88 | timeout-minutes: 45 89 | steps: 90 | - uses: actions/checkout@v4 91 | - uses: dtolnay/rust-toolchain@nightly 92 | with: 93 | target: thumbv7m-none-eabi 94 | - name: Install QEMU 95 | run: | 96 | sudo apt-get update 97 | sudo apt-get install -y qemu-system-arm 98 | - run: cargo run --release 99 | env: 100 | RUSTFLAGS: -C link-arg=-Tlink.x -D warnings 101 | working-directory: miniconf/tests/embedded 102 | continue-on-error: true 103 | - run: cargo run --release 104 | env: 105 | RUSTFLAGS: -C link-arg=-Tlink.x -D warnings 106 | working-directory: miniconf/tests/embedded 107 | 108 | test-mqtt: 109 | runs-on: ubuntu-latest 110 | strategy: 111 | matrix: 112 | example: 113 | - mqtt 114 | steps: 115 | - uses: actions/checkout@v4 116 | - name: Start Broker 117 | run: | 118 | sudo apt-get install mosquitto mosquitto-clients 119 | sudo service mosquitto start 120 | - uses: dtolnay/rust-toolchain@stable 121 | - run: sh py/test.sh 122 | -------------------------------------------------------------------------------- /py/miniconf-mqtt/miniconf/common.py: -------------------------------------------------------------------------------- 1 | """Common code for miniconf.async_ and miniconf.sync""" 2 | 3 | # pylint: disable=R0801,C0415,W1203,R0903,W0707,C0103 4 | 5 | from typing import Dict, Any, Tuple 6 | import logging 7 | import json 8 | 9 | import paho.mqtt 10 | 11 | MQTTv5 = paho.mqtt.enums.MQTTProtocolVersion.MQTTv5 12 | 13 | LOGGER = logging.getLogger("miniconf") 14 | 15 | 16 | def json_dumps(value): 17 | """Like json.dumps but without whitespace in separators""" 18 | return json.dumps(value, separators=(",", ":")) 19 | 20 | 21 | class MiniconfException(Exception): 22 | """Miniconf Error""" 23 | 24 | def __init__(self, code, message): 25 | self.code = code 26 | self.message = message 27 | 28 | def __repr__(self): 29 | return f"{self.code}: {self.message}" 30 | 31 | 32 | def one(devices: Dict[str, Any]) -> Tuple[str, Any]: 33 | """Return the prefix for the unique alive Miniconf device. 34 | 35 | See `discover()` for arguments. 36 | """ 37 | try: 38 | (device,) = devices.items() 39 | except ValueError: 40 | raise MiniconfException( 41 | "Discover", f"No unique Miniconf device (found `{devices}`)." 42 | ) 43 | LOGGER.info("Found device: %s", device) 44 | return device 45 | 46 | 47 | class _Path: 48 | def __init__(self): 49 | self.current = "" 50 | 51 | def normalize(self, path): 52 | """Return an absolute normalized path and update current absolute reference.""" 53 | if path.startswith("/") or not path: 54 | self.current = path[: path.rfind("/")] 55 | else: 56 | path = f"{self.current}/{path}" 57 | assert path.startswith("/") or not path 58 | return path 59 | 60 | 61 | def _cli(): 62 | import argparse 63 | 64 | parser = argparse.ArgumentParser( 65 | description="Miniconf command line interface.", 66 | formatter_class=argparse.RawDescriptionHelpFormatter, 67 | epilog="""Examples (with a target at prefix 'app/id' and id discovery): 68 | %(prog)s -d app/+ /path # GET 69 | %(prog)s -d app/+ /path=value # SET 70 | %(prog)s -d app/+ /path= # CLEAR 71 | %(prog)s -d app/+ /path? # LIST-GET 72 | %(prog)s -d app/+ /path! # DUMP 73 | """, 74 | ) 75 | parser.add_argument( 76 | "-v", "--verbose", action="count", default=0, help="Increase logging verbosity" 77 | ) 78 | parser.add_argument( 79 | "--broker", "-b", default="mqtt", type=str, help="The MQTT broker address" 80 | ) 81 | parser.add_argument( 82 | "--retain", 83 | "-r", 84 | default=False, 85 | action="store_true", 86 | help="Retain the settings that are being set on the broker", 87 | ) 88 | parser.add_argument( 89 | "--discover", "-d", action="store_true", help="Detect device prefix" 90 | ) 91 | parser.add_argument( 92 | "prefix", 93 | type=str, 94 | help="The MQTT topic prefix of the target or a prefix filter for discovery", 95 | ) 96 | parser.add_argument( 97 | "commands", 98 | metavar="CMD", 99 | nargs="*", 100 | help="Path to get ('PATH') or path and JSON encoded value to set " 101 | "('PATH=VALUE') or path to clear ('PATH=') or path to list ('PATH?') or " 102 | "path to dump ('PATH!'). " 103 | "Use sufficient shell quoting/escaping. " 104 | "Absolute PATHs are empty or start with a '/'. " 105 | "All other PATHs are relative to the last absolute PATH.", 106 | ) 107 | return parser 108 | -------------------------------------------------------------------------------- /miniconf/tests/structs.rs: -------------------------------------------------------------------------------- 1 | use miniconf::{ 2 | Deserialize, IntoKeys, Leaf, Serialize, Shape, Tree, TreeAny, TreeDeserialize, TreeSchema, 3 | TreeSerialize, ValueError, json_core, 4 | }; 5 | 6 | mod common; 7 | use common::*; 8 | 9 | #[test] 10 | fn structs() { 11 | #[derive(Serialize, Deserialize, Tree, Default, PartialEq, Debug)] 12 | struct Inner { 13 | a: u32, 14 | } 15 | 16 | #[derive(Tree, Default, PartialEq, Debug)] 17 | struct Settings { 18 | a: f32, 19 | b: bool, 20 | c: Leaf, 21 | d: Inner, 22 | } 23 | 24 | let mut settings = Settings::default(); 25 | 26 | // Inner settings structure is atomic, so cannot be set. 27 | assert!(json_core::set(&mut settings, "/c/a", b"4").is_err()); 28 | 29 | // Inner settings can be updated atomically. 30 | set_get(&mut settings, "/c", b"{\"a\":5}"); 31 | 32 | // Deferred inner settings can be updated individually. 33 | set_get(&mut settings, "/d/a", b"3"); 34 | 35 | // It is not allowed to set a non-terminal node. 36 | assert!(json_core::set(&mut settings, "/d", b"{\"a\": 5").is_err()); 37 | 38 | assert_eq!(*settings.c, Inner { a: 5 }); 39 | assert_eq!(settings.d, Inner { a: 3 }); 40 | 41 | // Check that metadata is correct. 42 | const SHAPE: Shape = Settings::SCHEMA.shape(); 43 | assert_eq!(SHAPE.max_depth, 2); 44 | assert_eq!(SHAPE.max_length("/"), "/d/a".len()); 45 | assert_eq!(SHAPE.count.get(), 4); 46 | 47 | assert_eq!(paths::(), ["/a", "/b", "/c", "/d/a"]); 48 | } 49 | 50 | #[test] 51 | fn borrowed() { 52 | // Can't derive TreeAny 53 | #[derive(TreeSchema, TreeDeserialize, TreeSerialize)] 54 | struct S<'a> { 55 | a: &'a str, 56 | } 57 | let mut s = S { a: "foo" }; 58 | set_get(&mut s, "/a", br#""bar""#); 59 | assert_eq!(s.a, "bar"); 60 | } 61 | 62 | #[test] 63 | fn tuple_struct() { 64 | #[derive(Tree, Default)] 65 | struct Settings(i32, f32); 66 | 67 | let mut s = Settings::default(); 68 | 69 | set_get(&mut s, "/0", br#"2"#); 70 | assert_eq!(s.0, 2); 71 | set_get(&mut s, "/1", br#"3.0"#); 72 | assert_eq!(s.1, 3.0); 73 | json_core::set(&mut s, "/2", b"3.0").unwrap_err(); 74 | json_core::set(&mut s, "/foo", b"3.0").unwrap_err(); 75 | 76 | assert_eq!(paths::(), ["/0", "/1"]); 77 | } 78 | 79 | #[test] 80 | fn deny_access() { 81 | use core::cell::RefCell; 82 | #[derive(Tree)] 83 | struct S<'a> { 84 | #[tree(with=deny_write)] 85 | field: i32, 86 | #[tree(with=deny_ref)] 87 | cell: &'a RefCell, 88 | } 89 | let cell = RefCell::new(2); 90 | let mut s = S { 91 | field: 1, 92 | cell: &cell, 93 | }; 94 | mod deny_write { 95 | pub use miniconf::{ 96 | deny::{deserialize_by_key, mut_any_by_key}, 97 | leaf::{SCHEMA, probe_by_key, ref_any_by_key, serialize_by_key}, 98 | }; 99 | } 100 | mod deny_ref { 101 | use std::cell::RefCell; 102 | 103 | use miniconf::{Schema, TreeSchema}; 104 | 105 | pub const SCHEMA: &Schema = <&RefCell>::SCHEMA; 106 | pub use miniconf::{ 107 | deny::{mut_any_by_key, ref_any_by_key}, 108 | passthrough::{deserialize_by_key, probe_by_key, serialize_by_key}, 109 | }; 110 | } 111 | 112 | common::set_get(&mut s, "/cell", b"3"); 113 | s.ref_any_by_key([0].into_keys()).unwrap(); 114 | assert!(matches!( 115 | s.mut_any_by_key([0].into_keys()), 116 | Err(ValueError::Access("Denied")) 117 | )); 118 | } 119 | -------------------------------------------------------------------------------- /miniconf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "miniconf" 3 | # Sync all crate versions and the py client 4 | version = "0.20.0" 5 | authors = [ 6 | "James Irwin ", 7 | "Ryan Summers ", 8 | "Robert Jördens ", 9 | ] 10 | edition = "2024" 11 | license = "MIT" 12 | description = "Serialize/deserialize/access reflection for trees" 13 | repository = "https://github.com/quartiq/miniconf" 14 | keywords = ["config", "serde", "no_std", "reflection", "graph"] 15 | categories = ["embedded", "config", "data-structures", "parsing"] 16 | 17 | [dependencies] 18 | serde = { version = "1.0.120", default-features = false } 19 | miniconf_derive = { path = "../miniconf_derive", version = "0.20.0", optional = true } 20 | itoa = "1.0.4" 21 | serde-json-core = { version = "0.6.0", optional = true } 22 | postcard = { version = "1.0.8", optional = true } 23 | thiserror = { version = "2", default-features = false } 24 | serde-reflection = { version = "0.5", git = "https://github.com/zefchain/serde-reflection.git", optional = true } 25 | schemars = { version = "1.0.4", optional = true, default-features = false } 26 | serde_json = { version = "1.0.133", features = [ 27 | "preserve_order", 28 | ], optional = true } 29 | heapless = { version = "0.8", optional = true } 30 | 31 | [features] 32 | default = ["derive", "meta-str"] 33 | alloc = [] 34 | std = ["alloc"] 35 | meta-str = ["miniconf_derive/meta-str"] 36 | json-core = ["dep:serde-json-core"] 37 | json = ["dep:serde_json"] 38 | postcard = ["dep:postcard"] 39 | heapless = ["dep:heapless"] 40 | derive = ["dep:miniconf_derive", "serde/derive"] 41 | trace = ["std", "dep:serde-reflection"] 42 | schema = ["trace", "dep:schemars", "json"] 43 | 44 | [package.metadata.docs.rs] 45 | all-features = true 46 | 47 | [dev-dependencies] 48 | anyhow = "1.0.86" 49 | postcard = { version = "1.0.8", features = ["use-std"] } 50 | crosstrait = { version = "0.1", default-features = false } 51 | embedded-io = "0.6.1" 52 | embedded-io-async = "0.6.1" 53 | embedded-io-adapters = { version = "0.6.1", features = ["tokio-1"] } 54 | heapless = "0.8.0" 55 | yafnv = "3.0.0" 56 | tokio = { version = "1.38.0", features = ["io-std", "rt", "macros"] } 57 | strum = { version = "0.27.1", features = ["derive"] } 58 | trybuild = { version = "1.0.99", features = ["diff"] } 59 | serde_json = "1.0.133" 60 | jsonschema = { version = "0.33.0", default-features = false } 61 | 62 | [[test]] 63 | name = "arrays" 64 | required-features = ["json-core", "derive"] 65 | 66 | [[test]] 67 | name = "basic" 68 | required-features = ["json-core", "derive"] 69 | 70 | [[test]] 71 | name = "generics" 72 | required-features = ["json-core", "derive"] 73 | 74 | [[test]] 75 | name = "iter" 76 | required-features = ["json-core", "derive"] 77 | 78 | [[test]] 79 | name = "option" 80 | required-features = ["json-core", "derive"] 81 | 82 | [[test]] 83 | name = "packed" 84 | required-features = ["json-core", "derive"] 85 | 86 | [[test]] 87 | name = "skipped" 88 | required-features = ["derive"] 89 | 90 | [[test]] 91 | name = "structs" 92 | required-features = ["json-core", "derive"] 93 | 94 | [[test]] 95 | name = "enum" 96 | required-features = ["json-core", "derive"] 97 | 98 | [[test]] 99 | name = "validate" 100 | required-features = ["json-core", "derive"] 101 | 102 | [[test]] 103 | name = "flatten" 104 | required-features = ["json-core", "derive"] 105 | 106 | [[test]] 107 | name = "compiletest" 108 | required-features = ["derive"] 109 | 110 | [[example]] 111 | name = "common" 112 | crate-type = ["lib"] 113 | required-features = ["derive"] 114 | 115 | [[example]] 116 | name = "cli" 117 | required-features = ["json-core", "derive"] 118 | 119 | [[example]] 120 | name = "scpi" 121 | required-features = ["json-core", "derive"] 122 | 123 | [[example]] 124 | name = "trace" 125 | required-features = ["derive", "schema"] 126 | -------------------------------------------------------------------------------- /miniconf/tests/arrays.rs: -------------------------------------------------------------------------------- 1 | use miniconf::{ 2 | Deserialize, Indices, KeyError, Leaf, Packed, Path, SerdeError, Serialize, Shape, Track, Tree, 3 | TreeSchema, json_core, leaf, 4 | }; 5 | 6 | mod common; 7 | 8 | #[derive(Debug, Copy, Clone, Default, Tree, Deserialize, Serialize)] 9 | struct Inner { 10 | c: u8, 11 | } 12 | 13 | #[derive(Debug, Default, Tree)] 14 | struct Settings { 15 | #[tree(with=leaf)] 16 | a: [u8; 2], 17 | d: [u8; 2], 18 | dm: [Leaf; 2], 19 | am: [Inner; 2], 20 | aam: [[Inner; 2]; 2], 21 | } 22 | 23 | fn set_get( 24 | tree: &mut Settings, 25 | path: &str, 26 | value: &[u8], 27 | ) -> Result> { 28 | // Path 29 | common::set_get(tree, path, value); 30 | 31 | // Indices 32 | let (idx, depth) = Settings::SCHEMA 33 | .transcode::>>(Path::<_, '/'>(path)) 34 | .unwrap() 35 | .into_inner(); 36 | 37 | assert_eq!(depth, idx.len()); 38 | json_core::set_by_key(tree, &idx, value)?; 39 | let mut buf = vec![0; value.len()]; 40 | let len = json_core::get_by_key(tree, &idx, &mut buf[..]).unwrap(); 41 | assert_eq!(&buf[..len], value); 42 | 43 | // Packed 44 | let (packed, depth) = Settings::SCHEMA 45 | .transcode::>(&idx) 46 | .unwrap() 47 | .into_inner(); 48 | assert_eq!(depth, idx.len()); 49 | json_core::set_by_key(tree, packed, value)?; 50 | let mut buf = vec![0; value.len()]; 51 | let len = json_core::get_by_key(tree, packed, &mut buf[..]).unwrap(); 52 | assert_eq!(&buf[..len], value); 53 | 54 | Ok(depth) 55 | } 56 | 57 | #[test] 58 | fn paths() { 59 | common::paths::(); 60 | } 61 | 62 | #[test] 63 | fn atomic() { 64 | let mut s = Settings::default(); 65 | set_get(&mut s, "/a", b"[1,2]").unwrap(); 66 | assert_eq!(s.a, [1, 2]); 67 | } 68 | 69 | #[test] 70 | fn defer() { 71 | let mut s = Settings::default(); 72 | set_get(&mut s, "/d/1", b"99").unwrap(); 73 | assert_eq!(s.d[1], 99); 74 | } 75 | 76 | #[test] 77 | fn defer_miniconf() { 78 | let mut s = Settings::default(); 79 | set_get(&mut s, "/am/0/c", b"1").unwrap(); 80 | assert_eq!(s.am[0].c, 1); 81 | set_get(&mut s, "/aam/0/0/c", b"3").unwrap(); 82 | assert_eq!(s.aam[0][0].c, 3); 83 | } 84 | 85 | #[test] 86 | fn too_short() { 87 | let mut s = Settings::default(); 88 | assert_eq!( 89 | json_core::set(&mut s, "/d", b"[1,2]"), 90 | Err(KeyError::TooShort.into()) 91 | ); 92 | // Check precedence over `Inner`. 93 | assert_eq!( 94 | json_core::set(&mut s, "/d", b"[1,2,3]"), 95 | Err(KeyError::TooShort.into()) 96 | ); 97 | } 98 | 99 | #[test] 100 | fn too_long() { 101 | let mut s = Settings::default(); 102 | assert_eq!( 103 | json_core::set(&mut s, "/a/1", b"7"), 104 | Err(KeyError::TooLong.into()) 105 | ); 106 | assert_eq!( 107 | json_core::set(&mut s, "/d/0/b", b"7"), 108 | Err(KeyError::TooLong.into()) 109 | ); 110 | assert_eq!( 111 | json_core::set(&mut s, "/dm/0/c", b"7"), 112 | Err(KeyError::TooLong.into()) 113 | ); 114 | assert_eq!( 115 | json_core::set(&mut s, "/dm/0/d", b"7"), 116 | Err(KeyError::TooLong.into()) 117 | ); 118 | } 119 | 120 | #[test] 121 | fn not_found() { 122 | let mut s = Settings::default(); 123 | assert_eq!( 124 | json_core::set(&mut s, "/d/3", b"7"), 125 | Err(KeyError::NotFound.into()) 126 | ); 127 | assert_eq!( 128 | json_core::set(&mut s, "/b", b"7"), 129 | Err(KeyError::NotFound.into()) 130 | ); 131 | assert_eq!( 132 | json_core::set(&mut s, "/aam/0/0/d", b"7"), 133 | Err(KeyError::NotFound.into()) 134 | ); 135 | } 136 | 137 | #[test] 138 | fn metadata() { 139 | const SHAPE: Shape = Settings::SCHEMA.shape(); 140 | assert_eq!(SHAPE.max_depth, 4); 141 | assert_eq!(SHAPE.max_length("/"), "/aam/0/0/c".len()); 142 | assert_eq!(SHAPE.count.get(), 11); 143 | } 144 | -------------------------------------------------------------------------------- /miniconf/src/shape.rs: -------------------------------------------------------------------------------- 1 | use core::num::NonZero; 2 | 3 | use crate::{Internal, Packed, Schema}; 4 | 5 | /// Metadata about a `TreeSchema` namespace. 6 | /// 7 | /// Metadata includes paths that may be [`crate::ValueError::Absent`] at runtime. 8 | #[non_exhaustive] 9 | #[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] 10 | pub struct Shape { 11 | /// The maximum length of a path in bytes. 12 | /// 13 | /// This is the exact maximum of the length of the concatenation of the node names 14 | /// in a [`crate::Path`] excluding the separators. See [`Self::max_length()`] for 15 | /// the maximum length including separators. 16 | pub max_length: usize, 17 | 18 | /// The maximum node depth. 19 | /// 20 | /// This is equal to the exact maximum number of path hierarchy separators. 21 | /// It's the exact maximum number of key indices. 22 | pub max_depth: usize, 23 | 24 | /// The exact total number of leaf nodes. 25 | pub count: NonZero, 26 | 27 | /// The maximum number of bits (see [`crate::Packed`]) 28 | pub max_bits: u32, 29 | } 30 | 31 | // const a = a.max(b) 32 | macro_rules! assign_max { 33 | ($a:expr, $b:expr) => {{ 34 | let b = $b; 35 | if $a < b { 36 | $a = b; 37 | } 38 | }}; 39 | } 40 | 41 | impl Shape { 42 | /// Add separator length to the maximum path length. 43 | /// 44 | /// To obtain an upper bound on the maximum length of all paths 45 | /// including separators, this adds `max_depth*separator_length`. 46 | #[inline] 47 | pub const fn max_length(&self, separator: &str) -> usize { 48 | self.max_length + self.max_depth * separator.len() 49 | } 50 | 51 | /// Recursively compute Shape for a Schema. 52 | pub const fn new(schema: &Schema) -> Self { 53 | let mut m = Self { 54 | max_depth: 0, 55 | max_length: 0, 56 | count: NonZero::::MIN, 57 | max_bits: 0, 58 | }; 59 | if let Some(internal) = schema.internal.as_ref() { 60 | match internal { 61 | Internal::Named(nameds) => { 62 | let bits = Packed::bits_for(nameds.len() - 1); 63 | let mut index = 0; 64 | let mut count = 0; 65 | while index < nameds.len() { 66 | let named = &nameds[index]; 67 | let child = Self::new(named.schema); 68 | assign_max!(m.max_depth, 1 + child.max_depth); 69 | assign_max!(m.max_length, named.name.len() + child.max_length); 70 | assign_max!(m.max_bits, bits + child.max_bits); 71 | count += child.count.get(); 72 | index += 1; 73 | } 74 | m.count = NonZero::new(count).unwrap(); 75 | } 76 | Internal::Numbered(numbereds) => { 77 | let bits = Packed::bits_for(numbereds.len() - 1); 78 | let mut index = 0; 79 | let mut count = 0; 80 | while index < numbereds.len() { 81 | let numbered = &numbereds[index]; 82 | let len = 1 + match index.checked_ilog10() { 83 | None => 0, 84 | Some(len) => len as usize, 85 | }; 86 | let child = Self::new(numbered.schema); 87 | assign_max!(m.max_depth, 1 + child.max_depth); 88 | assign_max!(m.max_length, len + child.max_length); 89 | assign_max!(m.max_bits, bits + child.max_bits); 90 | count += child.count.get(); 91 | index += 1; 92 | } 93 | m.count = NonZero::new(count).unwrap(); 94 | } 95 | Internal::Homogeneous(homogeneous) => { 96 | m = Self::new(homogeneous.schema); 97 | m.max_depth += 1; 98 | m.max_length += 1 + homogeneous.len.ilog10() as usize; 99 | m.max_bits += Packed::bits_for(homogeneous.len.get() - 1); 100 | m.count = m.count.checked_mul(homogeneous.len).unwrap(); 101 | } 102 | } 103 | } 104 | m 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /miniconf/tests/option.rs: -------------------------------------------------------------------------------- 1 | use miniconf::{KeyError, Leaf, SerdeError, Tree, ValueError, json_core}; 2 | 3 | mod common; 4 | use common::*; 5 | 6 | #[derive(PartialEq, Debug, Clone, Default, Tree)] 7 | struct Inner { 8 | data: u32, 9 | } 10 | 11 | #[derive(Debug, Clone, Default, Tree)] 12 | struct Settings { 13 | value: Option, 14 | } 15 | 16 | #[test] 17 | fn just_option() { 18 | assert_eq!(paths::, 1>(), [""]); 19 | } 20 | 21 | #[test] 22 | fn option_get_set_none() { 23 | let mut settings = Settings::default(); 24 | let mut data = [0; 100]; 25 | 26 | // Check that if the option is None, the value cannot be get or set. 27 | settings.value.take(); 28 | assert_eq!( 29 | json_core::get(&settings, "/value_foo", &mut data), 30 | Err(KeyError::NotFound.into()) 31 | ); 32 | assert_eq!( 33 | json_core::get(&settings, "/value", &mut data), 34 | Err(ValueError::Absent.into()) 35 | ); 36 | // The Absent field indicates at which depth the variant was absent 37 | assert_eq!( 38 | json_core::set(&mut settings, "/value/data", b"5"), 39 | Err(ValueError::Absent.into()) 40 | ); 41 | } 42 | 43 | #[test] 44 | fn option_get_set_some() { 45 | let mut settings = Settings::default(); 46 | 47 | // Check that if the option is Some, the value can be get or set. 48 | settings.value.replace(Inner { data: 5 }); 49 | 50 | set_get(&mut settings, "/value/data", b"7"); 51 | assert_eq!(settings.value.unwrap().data, 7); 52 | } 53 | 54 | #[test] 55 | fn option_iterate_some_none() { 56 | assert_eq!(paths::(), ["/value/data"]); 57 | } 58 | 59 | #[test] 60 | fn option_test_normal_option() { 61 | #[derive(Copy, Clone, Default, Tree)] 62 | struct S { 63 | data: Leaf>, 64 | } 65 | assert_eq!(paths::(), ["/data"]); 66 | 67 | let mut s = S::default(); 68 | assert!(s.data.is_none()); 69 | 70 | set_get(&mut s, "/data", b"7"); 71 | assert_eq!(*s.data, Some(7)); 72 | 73 | set_get(&mut s, "/data", b"null"); 74 | assert!(s.data.is_none()); 75 | } 76 | 77 | #[test] 78 | fn option_test_defer_option() { 79 | #[derive(Copy, Clone, Default, Tree)] 80 | struct S { 81 | data: Option, 82 | } 83 | assert_eq!(paths::(), ["/data"]); 84 | 85 | let mut s = S::default(); 86 | assert!(s.data.is_none()); 87 | 88 | assert!(json_core::set(&mut s, "/data", b"7").is_err()); 89 | s.data = Some(0); 90 | set_get(&mut s, "/data", b"7"); 91 | assert_eq!(s.data, Some(7)); 92 | 93 | assert!(json_core::set(&mut s, "/data", b"null").is_err()); 94 | } 95 | 96 | #[test] 97 | fn option_absent() { 98 | #[derive(Copy, Clone, Default, Tree)] 99 | struct I(()); 100 | 101 | #[derive(Copy, Clone, Default, Tree)] 102 | struct S { 103 | d: Option, 104 | dm: Option, 105 | } 106 | 107 | let mut s = S::default(); 108 | assert_eq!( 109 | json_core::set(&mut s, "/d", b"7"), 110 | Err(ValueError::Absent.into()) 111 | ); 112 | // Check precedence 113 | assert_eq!( 114 | json_core::set(&mut s, "/d", b""), 115 | Err(ValueError::Absent.into()) 116 | ); 117 | assert_eq!( 118 | json_core::set(&mut s, "/d/foo", b"7"), 119 | Err(ValueError::Absent.into()) 120 | ); 121 | assert_eq!( 122 | json_core::set(&mut s, "", b"7"), 123 | Err(KeyError::TooShort.into()) 124 | ); 125 | s.d = Some(3); 126 | assert_eq!(json_core::set(&mut s, "/d", b"7"), Ok(1)); 127 | assert_eq!( 128 | json_core::set(&mut s, "/d/foo", b"7"), 129 | Err(KeyError::TooLong.into()) 130 | ); 131 | assert!(matches!( 132 | json_core::set(&mut s, "/d", b""), 133 | Err(SerdeError::Inner(_)) 134 | )); 135 | assert_eq!(json_core::set(&mut s, "/d", b"7 "), Ok(2)); 136 | assert_eq!(json_core::set(&mut s, "/d", b" 7"), Ok(2)); 137 | assert!(matches!( 138 | json_core::set(&mut s, "/d", b"7i"), 139 | Err(SerdeError::Finalization(_)) 140 | )); 141 | } 142 | 143 | #[test] 144 | fn array_option() { 145 | // This tests that no invalid bounds are inferred for Options and Options in arrays. 146 | #[allow(dead_code)] 147 | #[derive(Copy, Clone, Default, Tree)] 148 | struct S { 149 | a: Option, 150 | b: [Leaf>; 1], 151 | c: [Option; 1], 152 | d: [Option>; 1], 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /miniconf/src/jsonpath.rs: -------------------------------------------------------------------------------- 1 | use core::{fmt::Write, ops::ControlFlow::*}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::{DescendError, IntoKeys, KeysIter, Schema, Transcode}; 6 | 7 | /// JSON style path notation iterator 8 | /// 9 | /// This is only styled after JSON notation, it does not adhere to it. 10 | /// Supported are both dot and key notation with and without 11 | /// names enclosed by `'` as well as various mixtures: 12 | /// 13 | /// ``` 14 | /// use miniconf::JsonPathIter; 15 | /// let path = ["foo", "bar", "4", "baz", "5", "6"]; 16 | /// for valid in [ 17 | /// ".foo.bar[4].baz[5][6]", 18 | /// "['foo']['bar'][4]['baz'][5][6]", 19 | /// ".foo['bar'].4.'baz'['5'].'6'", 20 | /// ] { 21 | /// assert_eq!(&path[..], JsonPathIter::new(valid).collect::>()); 22 | /// } 23 | /// 24 | /// for short in ["'", "[", "['"] { 25 | /// assert!(JsonPathIter::new(short).next().is_none()); 26 | /// } 27 | /// ``` 28 | /// 29 | /// # Limitations 30 | /// 31 | /// * No attempt at validating conformance 32 | /// * Does not support any escaping 33 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize, Hash)] 34 | #[repr(transparent)] 35 | #[serde(transparent)] 36 | pub struct JsonPathIter<'a>(&'a str); 37 | 38 | impl<'a> JsonPathIter<'a> { 39 | /// Interpret a str as a JSON path to be iterated over. 40 | pub fn new(value: &'a str) -> Self { 41 | Self(value) 42 | } 43 | } 44 | 45 | impl core::fmt::Display for JsonPathIter<'_> { 46 | #[inline] 47 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 48 | self.0.fmt(f) 49 | } 50 | } 51 | 52 | impl<'a> Iterator for JsonPathIter<'a> { 53 | type Item = &'a str; 54 | 55 | fn next(&mut self) -> Option { 56 | for (open, close) in [ 57 | (".'", Continue("'")), // "'" inclusive 58 | (".", Break(['.', '['])), // '.' or '[' exclusive 59 | ("['", Continue("']")), // "']" inclusive 60 | ("[", Continue("]")), // "]" inclusive 61 | ] { 62 | if let Some(rest) = self.0.strip_prefix(open) { 63 | let (end, sep) = match close { 64 | Break(close) => (rest.find(close).unwrap_or(rest.len()), 0), 65 | Continue(close) => (rest.find(close)?, close.len()), 66 | }; 67 | let (next, rest) = rest.split_at(end); 68 | self.0 = &rest[sep..]; 69 | return Some(next); 70 | } 71 | } 72 | None 73 | } 74 | } 75 | 76 | impl core::iter::FusedIterator for JsonPathIter<'_> {} 77 | 78 | /// JSON style path notation 79 | /// 80 | /// `T` can be `Write` for `Transcode` with the following behavior: 81 | /// * Named fields (struct) are encoded in dot notation. 82 | /// * Indices (tuple struct, array) are encoded in index notation 83 | /// 84 | /// `T` can be `AsRef` for `IntoKeys` with the behavior described in [`JsonPathIter`]. 85 | #[derive( 86 | Clone, Copy, Debug, Default, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize, Hash, 87 | )] 88 | #[repr(transparent)] 89 | #[serde(transparent)] 90 | pub struct JsonPath(pub T); 91 | 92 | impl JsonPath { 93 | /// Extract the inner value 94 | #[inline] 95 | pub fn into_inner(self) -> T { 96 | self.0 97 | } 98 | } 99 | 100 | impl core::fmt::Display for JsonPath { 101 | #[inline] 102 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 103 | self.0.fmt(f) 104 | } 105 | } 106 | 107 | impl<'a, T: AsRef + ?Sized> IntoKeys for JsonPath<&'a T> { 108 | type IntoKeys = KeysIter>; 109 | #[inline] 110 | fn into_keys(self) -> Self::IntoKeys { 111 | JsonPathIter(self.0.as_ref()).into_keys() 112 | } 113 | } 114 | 115 | impl<'a, T: AsRef + ?Sized> IntoKeys for &'a JsonPath { 116 | type IntoKeys = as IntoKeys>::IntoKeys; 117 | 118 | #[inline] 119 | fn into_keys(self) -> Self::IntoKeys { 120 | JsonPathIter(self.0.as_ref()).into_keys() 121 | } 122 | } 123 | 124 | impl Transcode for JsonPath { 125 | type Error = core::fmt::Error; 126 | 127 | fn transcode( 128 | &mut self, 129 | schema: &Schema, 130 | keys: impl IntoKeys, 131 | ) -> Result<(), DescendError> { 132 | schema.descend(keys.into_keys(), |_meta, idx_internal| { 133 | if let Some((index, internal)) = idx_internal { 134 | if let Some(name) = internal.get_name(index) { 135 | debug_assert!(!name.contains(['.', '\'', '[', ']'])); 136 | self.0.write_char('.')?; 137 | self.0.write_str(name)?; 138 | } else { 139 | self.0.write_char('[')?; 140 | self.0.write_str(itoa::Buffer::new().format(index))?; 141 | self.0.write_char(']')?; 142 | } 143 | } 144 | Ok(()) 145 | }) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /miniconf/tests/validate.rs: -------------------------------------------------------------------------------- 1 | use miniconf::{Tree, ValueError, json_core}; 2 | 3 | #[derive(Tree, Default)] 4 | struct Check { 5 | #[tree(with=check)] 6 | v: f32, 7 | } 8 | 9 | mod check { 10 | use miniconf::{Deserializer, Keys, SerdeError, TreeDeserialize, ValueError}; 11 | 12 | pub use miniconf::leaf::{ 13 | SCHEMA, mut_any_by_key, probe_by_key, ref_any_by_key, serialize_by_key, 14 | }; 15 | 16 | pub fn deserialize_by_key<'de, D: Deserializer<'de>>( 17 | value: &mut f32, 18 | keys: impl Keys, 19 | de: D, 20 | ) -> Result<(), SerdeError> { 21 | let mut old = *value; 22 | old.deserialize_by_key(keys, de)?; 23 | if old < 0.0 { 24 | Err(ValueError::Access("").into()) 25 | } else { 26 | *value = old; 27 | Ok(()) 28 | } 29 | } 30 | } 31 | 32 | #[test] 33 | fn validate() { 34 | let mut s = Check::default(); 35 | json_core::set(&mut s, "/v", b"1.0").unwrap(); 36 | assert_eq!(s.v, 1.0); 37 | assert_eq!( 38 | json_core::set(&mut s, "/v", b"-1.0"), 39 | Err(ValueError::Access("").into()) 40 | ); 41 | assert_eq!(s.v, 1.0); // remains unchanged 42 | } 43 | 44 | // Demonstrate and test how a variable length `Vec` can be accessed 45 | // through a variable offset, fixed length array. 46 | #[derive(Default, Tree)] 47 | struct Page { 48 | #[tree(typ="[i32; 4]", rename=arr, defer=*self, with=page4)] 49 | vec: Vec, 50 | offset: usize, 51 | } 52 | 53 | mod page4 { 54 | use super::Page; 55 | 56 | use miniconf::{ 57 | Deserializer, Keys, Schema, SerdeError, Serializer, TreeDeserialize, TreeSchema, 58 | TreeSerialize, ValueError, 59 | }; 60 | 61 | const LENGTH: usize = 4; 62 | 63 | pub use miniconf::deny::{mut_any_by_key, probe_by_key, ref_any_by_key}; 64 | 65 | pub const SCHEMA: &Schema = <[i32; LENGTH] as TreeSchema>::SCHEMA; 66 | 67 | pub fn serialize_by_key( 68 | value: &Page, 69 | keys: impl Keys, 70 | ser: S, 71 | ) -> Result> { 72 | let arr: &[i32; LENGTH] = value 73 | .vec 74 | .get(value.offset..value.offset + LENGTH) 75 | .ok_or(ValueError::Access("range"))? 76 | .try_into() 77 | .unwrap(); 78 | arr.serialize_by_key(keys, ser) 79 | } 80 | 81 | pub fn deserialize_by_key<'de, D: Deserializer<'de>>( 82 | value: &mut Page, 83 | keys: impl Keys, 84 | de: D, 85 | ) -> Result<(), SerdeError> { 86 | let arr: &mut [i32; LENGTH] = value 87 | .vec 88 | .get_mut(value.offset..value.offset + LENGTH) 89 | .ok_or(ValueError::Access("range"))? 90 | .try_into() 91 | .unwrap(); 92 | arr.deserialize_by_key(keys, de) 93 | } 94 | } 95 | 96 | #[test] 97 | fn paging() { 98 | let mut s = Page::default(); 99 | s.vec.resize(10, 0); 100 | json_core::set(&mut s, "/offset", b"3").unwrap(); 101 | json_core::set(&mut s, "/arr/1", b"5").unwrap(); 102 | assert_eq!(s.vec[s.offset + 1], 5); 103 | let mut buf = [0; 10]; 104 | let len = json_core::get(&s, "/arr/1", &mut buf[..]).unwrap(); 105 | assert_eq!(buf[..len], b"5"[..]); 106 | json_core::set(&mut s, "/offset", b"100").unwrap(); 107 | assert_eq!( 108 | json_core::set(&mut s, "/arr/1", b"5"), 109 | Err(ValueError::Access("range").into()) 110 | ); 111 | } 112 | 113 | #[derive(Default, Tree)] 114 | struct Lock { 115 | #[tree(with=lock, defer=*self)] 116 | val: i32, 117 | read: bool, 118 | write: bool, 119 | } 120 | 121 | mod lock { 122 | use super::Lock; 123 | use miniconf::{ 124 | Deserializer, Keys, SerdeError, Serializer, TreeDeserialize, TreeSerialize, ValueError, 125 | }; 126 | 127 | pub use miniconf::deny::{mut_any_by_key, probe_by_key, ref_any_by_key}; 128 | pub use miniconf::leaf::SCHEMA; 129 | 130 | pub fn serialize_by_key( 131 | value: &Lock, 132 | keys: impl Keys, 133 | ser: S, 134 | ) -> Result> { 135 | if !value.read { 136 | return Err(ValueError::Access("not readable").into()); 137 | } 138 | value.val.serialize_by_key(keys, ser) 139 | } 140 | 141 | pub fn deserialize_by_key<'de, D: Deserializer<'de>>( 142 | value: &mut Lock, 143 | keys: impl Keys, 144 | de: D, 145 | ) -> Result<(), SerdeError> { 146 | if !value.write { 147 | return Err(ValueError::Access("not writable").into()); 148 | } 149 | value.val.deserialize_by_key(keys, de) 150 | } 151 | } 152 | 153 | #[test] 154 | fn locked() { 155 | let mut s = Lock::default(); 156 | json_core::set(&mut s, "/write", b"true").unwrap(); 157 | json_core::set(&mut s, "/val", b"1").unwrap(); 158 | assert_eq!(s.val, 1); 159 | json_core::set(&mut s, "/write", b"false").unwrap(); 160 | assert_eq!(s.val, 1); 161 | json_core::set(&mut s, "/val", b"1").unwrap_err(); 162 | } 163 | -------------------------------------------------------------------------------- /miniconf/src/error.rs: -------------------------------------------------------------------------------- 1 | /// Errors that can occur when using the Tree traits. 2 | /// 3 | /// A `usize` member indicates the key depth where the error occurred. 4 | /// The depth here is the number of names or indices consumed. 5 | /// It is also the number of separators in a path or the length 6 | /// of an indices slice. 7 | /// 8 | /// If multiple errors are applicable simultaneously the precedence 9 | /// is as per the order in the enum definition (from high to low). 10 | #[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] 11 | pub enum KeyError { 12 | /// The key ends early and does not reach a leaf node. 13 | #[error("Key does not reach a leaf")] 14 | TooShort, 15 | 16 | /// The key was not found (index parse failure or too large, 17 | /// name not found or invalid). 18 | #[error("Key not found")] 19 | NotFound, 20 | 21 | /// The key is too long and goes beyond a leaf node. 22 | #[error("Key goes beyond a leaf")] 23 | TooLong, 24 | } 25 | 26 | /// Errors that can occur while visting nodes with [`crate::Schema::descend`]. 27 | #[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] 28 | pub enum DescendError { 29 | /// The key is invalid. 30 | #[error(transparent)] 31 | Key(#[from] KeyError), 32 | 33 | /// The visitor callback returned an error. 34 | #[error("Visitor failed")] 35 | Inner(#[source] E), 36 | } 37 | 38 | /// Errors that can occur while accessing a value. 39 | #[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] 40 | pub enum ValueError { 41 | /// Tree traversal error 42 | #[error(transparent)] 43 | Key(#[from] KeyError), 44 | 45 | /// A node does not exist at runtime. 46 | /// 47 | /// An `enum` variant in the tree towards the node is currently absent. 48 | /// This is for example the case if an [`Option`] using the `Tree*` 49 | /// traits is `None` at runtime. See also [`crate::TreeSchema#option`]. 50 | #[error("Variant absent")] 51 | Absent, 52 | 53 | /// A node could not be accessed or is invalid. 54 | /// 55 | /// This is returned from custom implementations. 56 | #[error("Access/validation failure: {0}")] 57 | Access(&'static str), 58 | } 59 | 60 | /// Compound errors 61 | #[derive(Debug, Copy, Clone, PartialEq, Eq, thiserror::Error)] 62 | pub enum SerdeError { 63 | /// The value could not be accessed. 64 | #[error(transparent)] 65 | Value(#[from] ValueError), 66 | 67 | /// The value provided could not be serialized or deserialized 68 | /// or the traversal callback returned an error. 69 | #[error("(De)serialization")] 70 | Inner(#[source] E), 71 | 72 | /// There was an error during finalization. 73 | /// 74 | /// This is not to be returned by a TreeSerialize/TreeDeserialize 75 | /// implementation but only from a wrapper that creates and finalizes the 76 | /// the serializer/deserializer. 77 | /// 78 | /// The `Deserializer` has encountered an error only after successfully 79 | /// deserializing a value. This is the case if there is additional unexpected data. 80 | /// The `deserialize_by_key()` update takes place but this 81 | /// error will be returned. 82 | /// 83 | /// A `Serializer` may write checksums or additional framing data and fail with 84 | /// this error during finalization after the value has been serialized. 85 | #[error("(De)serializer finalization")] 86 | Finalization(#[source] E), 87 | } 88 | 89 | impl From for SerdeError { 90 | #[inline] 91 | fn from(value: KeyError) -> Self { 92 | SerdeError::Value(value.into()) 93 | } 94 | } 95 | 96 | // Try to extract the Traversal from an Error 97 | impl TryFrom> for KeyError { 98 | type Error = SerdeError; 99 | #[inline] 100 | fn try_from(value: SerdeError) -> Result { 101 | match value { 102 | SerdeError::Value(ValueError::Key(e)) => Ok(e), 103 | e => Err(e), 104 | } 105 | } 106 | } 107 | 108 | // Try to extract the Traversal from an Error 109 | impl TryFrom for KeyError { 110 | type Error = ValueError; 111 | #[inline] 112 | fn try_from(value: ValueError) -> Result { 113 | match value { 114 | ValueError::Key(e) => Ok(e), 115 | e => Err(e), 116 | } 117 | } 118 | } 119 | 120 | // Try to extract the Traversal from an Error 121 | impl TryFrom> for KeyError { 122 | type Error = E; 123 | #[inline] 124 | fn try_from(value: DescendError) -> Result { 125 | match value { 126 | DescendError::Key(e) => Ok(e), 127 | DescendError::Inner(e) => Err(e), 128 | } 129 | } 130 | } 131 | 132 | // Try to extract the Traversal from an Error 133 | impl TryFrom> for ValueError { 134 | type Error = E; 135 | #[inline] 136 | fn try_from(value: SerdeError) -> Result { 137 | match value { 138 | SerdeError::Value(e) => Ok(e), 139 | SerdeError::Finalization(e) | SerdeError::Inner(e) => Err(e), 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /miniconf/examples/scpi.rs: -------------------------------------------------------------------------------- 1 | use core::str; 2 | 3 | use miniconf::{ 4 | IntoKeys, Keys, Path, PathIter, SerdeError, TreeDeserializeOwned, TreeSchema, TreeSerialize, 5 | ValueError, json_core, 6 | }; 7 | 8 | mod common; 9 | use common::Settings; 10 | 11 | /// This show-cases the implementation of a custom [`miniconf::Key`] 12 | /// along the lines of SCPI style hierarchies. It is case-insensitive and 13 | /// distinguishes relative/absolute paths. 14 | /// It then proceeds to implement a naive SCPI command parser that supports 15 | /// setting and getting values. 16 | /// 17 | /// This is just a sketch. 18 | 19 | #[derive(Copy, Clone)] 20 | struct ScpiKey(T); 21 | 22 | impl + ?Sized> miniconf::Key for ScpiKey { 23 | fn find(&self, lookup: &miniconf::Internal) -> Option { 24 | use miniconf::Internal::*; 25 | let s = self.0.as_ref(); 26 | match lookup { 27 | Named(n) => { 28 | let mut truncated = None; 29 | let mut ambiguous = false; 30 | for (i, miniconf::Named { name, .. }) in n.iter().enumerate() { 31 | if name.len() < s.len() 32 | || !name 33 | .chars() 34 | .zip(s.chars()) 35 | .all(|(n, s)| n.to_ascii_lowercase() == s.to_ascii_lowercase()) 36 | { 37 | continue; 38 | } 39 | if name.len() == s.len() { 40 | // Exact match: return immediately 41 | return Some(i); 42 | } 43 | if truncated.is_some() { 44 | // Multiple truncated matches: ambiguous unless there is an additional exact match 45 | ambiguous = true; 46 | } else { 47 | // First truncated match: fine if there is only one. 48 | truncated = Some(i); 49 | } 50 | } 51 | if ambiguous { None } else { truncated } 52 | } 53 | Numbered(n) => s.parse().ok().filter(|i| *i < n.len()), 54 | Homogeneous(h) => s.parse().ok().filter(|i| *i < h.len.get()), 55 | } 56 | } 57 | } 58 | 59 | #[derive(Clone)] 60 | struct ScpiPathIter<'a>(PathIter<'a>); 61 | 62 | impl<'a> Iterator for ScpiPathIter<'a> { 63 | type Item = ScpiKey<&'a str>; 64 | fn next(&mut self) -> Option { 65 | self.0.next().map(ScpiKey) 66 | } 67 | } 68 | 69 | #[derive(Copy, Clone)] 70 | struct ScpiPath<'a>(Option<&'a str>); 71 | 72 | impl<'a> IntoIterator for ScpiPath<'a> { 73 | type IntoIter = ScpiPathIter<'a>; 74 | type Item = ScpiKey<&'a str>; 75 | fn into_iter(self) -> Self::IntoIter { 76 | ScpiPathIter(PathIter::new(self.0, ':')) 77 | } 78 | } 79 | 80 | #[derive(thiserror::Error, Debug, Copy, Clone)] 81 | enum Error { 82 | #[error("While setting value")] 83 | Set(#[from] miniconf::SerdeError), 84 | #[error("While getting value")] 85 | Get(#[from] miniconf::SerdeError), 86 | #[error("Parse failure: {0}")] 87 | Parse(&'static str), 88 | #[error("Could not print value")] 89 | Utf8(#[from] core::str::Utf8Error), 90 | } 91 | 92 | fn scpi(target: &mut M, cmds: &str) -> Result<(), Error> { 93 | let mut buf = vec![0; 1024]; 94 | let root = ScpiPath(None); 95 | let mut abs = root; 96 | for cmd in cmds.split_terminator(';').map(|cmd| cmd.trim()) { 97 | let (path, value) = if let Some(path) = cmd.strip_suffix('?') { 98 | (path, None) 99 | } else if let Some((path, value)) = cmd.split_once(' ') { 100 | (path, Some(value)) 101 | } else { 102 | Err(Error::Parse("Missing `?` to get or value to set"))? 103 | }; 104 | let rel; 105 | (abs, rel) = if let Some(path) = path.strip_prefix(':') { 106 | path.rsplit_once(':') 107 | .map(|(a, r)| (ScpiPath(Some(a)), ScpiPath(Some(r)))) 108 | .unwrap_or((root, ScpiPath(Some(path)))) 109 | } else { 110 | (abs, ScpiPath(Some(path))) 111 | }; 112 | let path = abs.into_keys().chain(rel); 113 | if let Some(value) = value { 114 | json_core::set_by_key(target, path, value.as_bytes())?; 115 | println!("OK"); 116 | } else { 117 | let len = json_core::get_by_key(target, path, &mut buf[..])?; 118 | println!("{}", str::from_utf8(&buf[..len])?); 119 | } 120 | } 121 | Ok(()) 122 | } 123 | 124 | fn main() -> anyhow::Result<()> { 125 | let mut settings = Settings::new(); 126 | 127 | scpi( 128 | &mut settings, 129 | "fO?; foo?; FOO?; :FOO?; :ARRAY_OPT:1:A?; A?; A?; A 1; A?; :FOO?", 130 | )?; 131 | scpi(&mut settings, "FO?; STRUCT_TREE:B 3; STRUCT_TREE:B?")?; 132 | 133 | scpi(&mut settings, ":STRUCT_ 42").unwrap_err(); 134 | 135 | let mut buf = vec![0; 1024]; 136 | const MAX_DEPTH: usize = Settings::SCHEMA.shape().max_depth; 137 | for path in Settings::SCHEMA.nodes::, MAX_DEPTH>() { 138 | let path = path?; 139 | match json_core::get_by_key(&settings, &path, &mut buf) { 140 | Ok(len) => println!( 141 | "{} {}", 142 | path.0.to_uppercase(), 143 | core::str::from_utf8(&buf[..len])? 144 | ), 145 | Err(SerdeError::Value(ValueError::Absent)) => { 146 | continue; 147 | } 148 | err => { 149 | err?; 150 | } 151 | } 152 | } 153 | Ok(()) 154 | } 155 | -------------------------------------------------------------------------------- /miniconf/tests/packed.rs: -------------------------------------------------------------------------------- 1 | use miniconf::{ 2 | Indices, KeyError, Packed, Path, Shape, Short, Track, Tree, TreeSchema, TreeSerialize, 3 | }; 4 | mod common; 5 | 6 | #[derive(Tree, Default)] 7 | struct Settings { 8 | a: f32, 9 | b: [f32; 2], 10 | } 11 | 12 | #[test] 13 | fn packed() { 14 | // Check empty being too short 15 | assert_eq!( 16 | Settings::SCHEMA 17 | .transcode::>>(Packed::EMPTY) 18 | .unwrap(), 19 | Short::default() 20 | ); 21 | 22 | // Check path-packed round trip. 23 | for iter_path in Settings::SCHEMA 24 | .nodes::, 2>() 25 | .map(Result::unwrap) 26 | { 27 | let packed: Track = Settings::SCHEMA.transcode(&iter_path).unwrap(); 28 | let path: Path = Settings::SCHEMA.transcode(*packed.inner()).unwrap(); 29 | assert_eq!(path, iter_path); 30 | println!( 31 | "{path:?} {iter_path:?}, {:#06b} {} {:?}", 32 | packed.inner().get() >> 60, 33 | packed.inner().into_lsb(), 34 | packed.depth() 35 | ); 36 | } 37 | println!( 38 | "{:?}", 39 | Settings::SCHEMA 40 | .nodes::() 41 | .map(|p| p.unwrap().into_lsb()) 42 | .collect::>() 43 | ); 44 | 45 | // Check that Packed `marker + 0b0` is equivalent to `/a` 46 | let a = Packed::from_lsb(0b10.try_into().unwrap()); 47 | let path: Track> = Settings::SCHEMA.transcode(a).unwrap(); 48 | assert_eq!(path.depth(), 1); 49 | assert_eq!(path.inner().as_ref(), "/a"); 50 | } 51 | 52 | #[test] 53 | fn top() { 54 | #[derive(Tree)] 55 | struct S { 56 | baz: [i32; 1], 57 | foo: i32, 58 | } 59 | assert_eq!( 60 | S::SCHEMA 61 | .nodes::, 2>() 62 | .map(|p| p.unwrap().into_inner()) 63 | .collect::>(), 64 | ["/baz/0", "/foo"] 65 | ); 66 | assert_eq!( 67 | S::SCHEMA 68 | .nodes::, 2>() 69 | .map(|p| p.unwrap()) 70 | .collect::>(), 71 | [Indices::new([0, 0], 2), Indices::new([1, 0], 1)] 72 | ); 73 | let p: Track = S::SCHEMA.transcode([1usize]).unwrap(); 74 | assert_eq!((p.inner().into_lsb().get(), p.depth()), (0b11, 1)); 75 | assert_eq!( 76 | S::SCHEMA 77 | .nodes::() 78 | .map(|p| p.unwrap().into_lsb().get()) 79 | .collect::>(), 80 | [0b100, 0b11] 81 | ); 82 | } 83 | 84 | #[test] 85 | fn zero_key() { 86 | assert_eq!( 87 | Option::<()>::SCHEMA 88 | .nodes::() 89 | .next() 90 | .unwrap() 91 | .unwrap() 92 | .into_lsb() 93 | .get(), 94 | 0b1 95 | ); 96 | 97 | assert_eq!( 98 | <[usize; 1]>::SCHEMA 99 | .nodes::() 100 | .next() 101 | .unwrap() 102 | .unwrap() 103 | .into_lsb() 104 | .get(), 105 | 0b10 106 | ); 107 | 108 | // Check the corner case of a len=1 index where (len - 1) = 0 and zero bits would be required to encode. 109 | // Hence the Packed values for len=1 and len=2 are the same. 110 | let mut a11 = [[0]]; 111 | let mut a22 = [[0; 2]; 2]; 112 | let mut buf = [0u8; 100]; 113 | let mut ser = serde_json_core::ser::Serializer::new(&mut buf); 114 | for (depth, result) in [ 115 | Err(KeyError::TooShort.into()), 116 | Err(KeyError::TooShort.into()), 117 | Ok(()), 118 | ] 119 | .iter() 120 | .enumerate() 121 | { 122 | assert_eq!( 123 | TreeSerialize::serialize_by_key( 124 | &mut a11, 125 | Packed::from_lsb((0b1 << depth).try_into().unwrap()), 126 | &mut ser 127 | ), 128 | *result 129 | ); 130 | assert_eq!( 131 | TreeSerialize::serialize_by_key( 132 | &mut a22, 133 | Packed::from_lsb((0b1 << depth).try_into().unwrap()), 134 | &mut ser 135 | ), 136 | *result 137 | ); 138 | } 139 | } 140 | 141 | #[test] 142 | fn size() { 143 | // The max width claims below only apply to 32 bit architectures 144 | 145 | // Play with the worst cases for 32 bit Packed 146 | // Bit-hungriest type would be [T; 0] or () but (a) those are forbidden (internal without leaves) and (b) they don't have any keys 147 | // so won't recurse in Transcode or consume from Keys 148 | // Then [T; 1] which takes one bit per level (not 0 bits, to distinguish empty Packed) 149 | // Worst case for a 32 bit usize we need 31 array levels (marker bit). 150 | type A31 = [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[(); 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 151 | 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 1]; 152 | assert_eq!(core::mem::size_of::(), 0); 153 | let packed = Packed::new_from_lsb(1 << 31).unwrap(); 154 | let path = A31::SCHEMA 155 | .transcode::>>(packed) 156 | .unwrap(); 157 | assert_eq!(path.depth(), 31); 158 | assert_eq!(path.inner().as_ref().len(), 2 * 31); 159 | const META: Shape = A31::SCHEMA.shape(); 160 | assert_eq!(META.max_bits, 31); 161 | assert_eq!(META.max_depth, 31); 162 | assert_eq!(META.count.get(), 1usize.pow(31)); 163 | assert_eq!(META.max_length, 31); 164 | 165 | // Another way to get to 32 bit is to take 15 length-3 (2 bit) levels and one length-1 (1 bit) level to fill it, needing (3**15 ~ 14 M) storage. 166 | // With the unit as type, we need 0 storage but can't do much. 167 | type A16 = [[[[[[[[[[[[[[[[(); 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 1]; 168 | assert_eq!(core::mem::size_of::(), 0); 169 | let packed = Packed::new_from_lsb(1 << 31).unwrap(); 170 | let path = A16::SCHEMA 171 | .transcode::>>(packed) 172 | .unwrap(); 173 | assert_eq!(path.depth(), 16); 174 | assert_eq!(path.inner().as_ref().len(), 2 * 16); 175 | const META16: Shape = A16::SCHEMA.shape(); 176 | assert_eq!(META16.max_bits, 31); 177 | assert_eq!(META16.max_depth, 16); 178 | assert_eq!(META16.count.get(), 3usize.pow(15)); 179 | assert_eq!(META16.max_length, 16); 180 | } 181 | -------------------------------------------------------------------------------- /miniconf_derive/src/field.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | 3 | use darling::{ 4 | FromField, FromMeta, 5 | usage::{IdentSet, Purpose, UsesTypeParams}, 6 | uses_lifetimes, uses_type_params, 7 | util::Flag, 8 | util::Override, 9 | }; 10 | use proc_macro2::{Span, TokenStream}; 11 | use quote::{quote, quote_spanned}; 12 | use syn::{parse_quote, parse_quote_spanned, spanned::Spanned}; 13 | 14 | #[derive(Clone, Copy, Debug, PartialEq)] 15 | pub(crate) enum TreeTrait { 16 | Schema, 17 | Serialize, 18 | Deserialize, 19 | Any, 20 | } 21 | 22 | #[derive(Debug, FromField, Clone)] 23 | #[darling(attributes(tree), forward_attrs(doc))] 24 | pub(crate) struct TreeField { 25 | pub ident: Option, 26 | ty: syn::Type, 27 | pub skip: Flag, 28 | typ: Option, // Type to defer to 29 | rename: Option, 30 | defer: Option, // Value to defer to 31 | #[darling(default)] 32 | with: Option, 33 | #[darling(default)] 34 | bounds: Bounds, 35 | #[darling(default)] 36 | pub meta: BTreeMap>, 37 | pub attrs: Vec, 38 | } 39 | 40 | uses_type_params!(TreeField, ty, typ); 41 | uses_lifetimes!(TreeField, ty, typ); 42 | 43 | #[derive(Debug, Default, FromMeta, Clone)] 44 | struct Bounds { 45 | schema: Option>, 46 | serialize: Option>, 47 | deserialize: Option>, 48 | any: Option>, 49 | } 50 | 51 | impl TreeField { 52 | pub fn span(&self) -> Span { 53 | self.ident 54 | .as_ref() 55 | .map(|i| i.span()) 56 | .unwrap_or(self.ty.span()) 57 | } 58 | 59 | fn typ(&self) -> &syn::Type { 60 | self.typ.as_ref().unwrap_or(&self.ty) 61 | } 62 | 63 | pub fn schema(&self) -> TokenStream { 64 | if let Some(all) = self.with.as_ref() { 65 | quote_spanned!(self.span()=> #all::SCHEMA) 66 | } else { 67 | let typ = self.typ(); 68 | quote_spanned!(self.span()=> <#typ as ::miniconf::TreeSchema>::SCHEMA) 69 | } 70 | } 71 | 72 | pub fn bound(&self, trtr: TreeTrait, type_set: &IdentSet) -> Option { 73 | if let Some(bounds) = match trtr { 74 | TreeTrait::Schema => &self.bounds.schema, 75 | TreeTrait::Serialize => &self.bounds.serialize, 76 | TreeTrait::Deserialize => &self.bounds.deserialize, 77 | TreeTrait::Any => &self.bounds.any, 78 | } { 79 | Some(bounds.iter().map(|b| quote!(#b, )).collect()) 80 | } else if self 81 | .uses_type_params(&Purpose::BoundImpl.into(), type_set) 82 | .is_empty() 83 | || self.with.is_some() 84 | { 85 | None 86 | } else { 87 | let bound: syn::TraitBound = match trtr { 88 | TreeTrait::Schema => parse_quote!(::miniconf::TreeSchema), 89 | TreeTrait::Serialize => parse_quote!(::miniconf::TreeSerialize), 90 | TreeTrait::Deserialize => parse_quote!(::miniconf::TreeDeserialize<'de>), 91 | TreeTrait::Any => parse_quote!(::miniconf::TreeAny), 92 | }; 93 | let ty = self.typ(); 94 | Some(quote_spanned!(self.span()=> #ty: #bound,)) 95 | } 96 | } 97 | 98 | pub fn name(&self) -> Option<&syn::Ident> { 99 | self.rename.as_ref().or(self.ident.as_ref()) 100 | } 101 | 102 | fn value(&self, i: Option) -> syn::Expr { 103 | let def = if let Some(i) = i { 104 | // named or tuple struct field 105 | if let Some(name) = &self.ident { 106 | parse_quote_spanned!(self.span()=> self.#name) 107 | } else { 108 | let index = syn::Index::from(i); 109 | parse_quote_spanned!(self.span()=> self.#index) 110 | } 111 | } else { 112 | // enum variant newtype value 113 | parse_quote_spanned!(self.span()=> (*value)) 114 | }; 115 | self.defer.clone().unwrap_or(def) 116 | } 117 | 118 | pub fn serialize_by_key(&self, i: Option) -> TokenStream { 119 | // Quote context is a match of the field index with `serialize_by_key()` args available. 120 | let value = self.value(i); 121 | let imp = self 122 | .with 123 | .as_ref() 124 | .map(|m| quote!(#m::serialize_by_key(&#value, keys, ser))) 125 | .unwrap_or(quote!(#value.serialize_by_key(keys, ser))); 126 | quote_spanned! { self.span()=> #imp } 127 | } 128 | 129 | pub fn deserialize_by_key(&self, i: Option) -> TokenStream { 130 | // Quote context is a match of the field index with `deserialize_by_key()` args available. 131 | let value = self.value(i); 132 | let imp = self 133 | .with 134 | .as_ref() 135 | .map(|m| quote!(#m::deserialize_by_key(&mut #value, keys, de))) 136 | .unwrap_or(quote!(#value.deserialize_by_key(keys, de))); 137 | quote_spanned! { self.span()=> #imp } 138 | } 139 | 140 | pub fn probe_by_key(&self, i: usize) -> TokenStream { 141 | // Quote context is a match of the field index with `probe_by_key()` args available. 142 | let typ = self.typ(); 143 | let imp = self 144 | .with 145 | .as_ref() 146 | .map(|m| quote!(#m::probe_by_key::<'_, #typ, _>(keys, de))) 147 | .unwrap_or( 148 | quote!(<#typ as ::miniconf::TreeDeserialize::<'de>>::probe_by_key(keys, de)), 149 | ); 150 | quote_spanned! { self.span()=> #i => #imp } 151 | } 152 | 153 | pub fn ref_any_by_key(&self, i: Option) -> TokenStream { 154 | // Quote context is a match of the field index with `get_mut_by_key()` args available. 155 | let value = self.value(i); 156 | let imp = self 157 | .with 158 | .as_ref() 159 | .map(|m| quote!(#m::ref_any_by_key(&#value, keys))) 160 | .unwrap_or(quote!(#value.ref_any_by_key(keys))); 161 | quote_spanned! { self.span()=> #imp } 162 | } 163 | 164 | pub fn mut_any_by_key(&self, i: Option) -> TokenStream { 165 | // Quote context is a match of the field index with `get_mut_by_key()` args available. 166 | let value = self.value(i); 167 | let imp = self 168 | .with 169 | .as_ref() 170 | .map(|m| quote!(#m::mut_any_by_key(&mut #value, keys))) 171 | .unwrap_or(quote!(#value.mut_any_by_key(keys))); 172 | quote_spanned! { self.span()=> #imp } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /miniconf/src/iter.rs: -------------------------------------------------------------------------------- 1 | use core::marker::PhantomData; 2 | 3 | use crate::{DescendError, IntoKeys, KeyError, Keys, Schema, Short, Track, Transcode}; 4 | 5 | /// Counting wrapper for iterators with known exact size 6 | #[derive(Clone, Debug, PartialEq, Eq)] 7 | pub struct ExactSize { 8 | iter: T, 9 | count: usize, 10 | } 11 | 12 | impl Iterator for ExactSize { 13 | type Item = T::Item; 14 | 15 | #[inline] 16 | fn next(&mut self) -> Option { 17 | if let Some(v) = self.iter.next() { 18 | self.count -= 1; // checks for overflow in debug 19 | Some(v) 20 | } else { 21 | debug_assert!(self.count == 0); 22 | None 23 | } 24 | } 25 | 26 | #[inline] 27 | fn size_hint(&self) -> (usize, Option) { 28 | (self.count, Some(self.count)) 29 | } 30 | } 31 | 32 | // Even though general TreeSchema iterations may well be longer than usize::MAX 33 | // we are sure that the aren't in this case since self.count <= usize::MAX 34 | impl ExactSizeIterator for ExactSize {} 35 | 36 | // `Iterator` is sufficient to fuse 37 | impl core::iter::FusedIterator for ExactSize {} 38 | 39 | // https://github.com/rust-lang/rust/issues/37572 40 | // unsafe impl core::iter::TrustedLen for ExactSize {} 41 | 42 | /// Node iterator 43 | /// 44 | /// A managed indices state for iteration of nodes `N` in a `TreeSchema`. 45 | /// 46 | /// `D` is the depth limit. Internal nodes will be returned on iteration where 47 | /// the depth limit is exceeded. 48 | /// 49 | /// The `Err(usize)` variant of the `Iterator::Item` indicates that `N` does 50 | /// not have sufficient capacity and failed to encode the key at the given depth. 51 | #[derive(Clone, Debug, PartialEq)] 52 | pub struct NodeIter { 53 | // We can't use Packed as state since we need to be able to modify the 54 | // indices directly. Packed erases knowledge of the bit widths of the individual 55 | // indices. 56 | schema: &'static Schema, 57 | state: [usize; D], 58 | root: usize, 59 | depth: usize, 60 | _n: PhantomData, 61 | } 62 | 63 | impl NodeIter { 64 | /// Create a new iterator. 65 | /// 66 | /// # Panic 67 | /// If the root depth exceeds the state length. 68 | #[inline] 69 | pub const fn with(schema: &'static Schema, state: [usize; D], root: usize) -> Self { 70 | assert!(root <= D); 71 | Self { 72 | schema, 73 | state, 74 | root, 75 | // Marker to prevent initial index increment in `next()` 76 | depth: D + 1, 77 | _n: PhantomData, 78 | } 79 | } 80 | 81 | /// Create a new iterator with default root and initial state. 82 | #[inline] 83 | pub const fn new(schema: &'static Schema) -> Self { 84 | Self::with(schema, [0; D], 0) 85 | } 86 | 87 | /// Limit and start iteration to at and below the provided root key. 88 | /// 89 | /// This requires moving `self` to ensure `FusedIterator`. 90 | pub fn with_root( 91 | schema: &'static Schema, 92 | root: impl IntoKeys, 93 | ) -> Result> { 94 | let mut state = [0; D]; 95 | let mut root = root.into_keys().track(); 96 | let mut tr = Short::new(state.as_mut()); 97 | tr.transcode(schema, &mut root)?; 98 | Ok(Self::with(schema, state, root.depth())) 99 | } 100 | 101 | /// Wrap the iterator in an exact size counting iterator that is 102 | /// `FusedIterator` and `ExactSizeIterator`. 103 | /// 104 | /// Note(panic): Panics, if the iterator had `next()` called or 105 | /// if the iteration depth has been limited or if the iteration root 106 | /// is not the tree root. 107 | /// 108 | #[inline] 109 | pub const fn exact_size(schema: &'static Schema) -> ExactSize { 110 | let shape = schema.shape(); 111 | if D < shape.max_depth { 112 | panic!("insufficient depth for exact size iteration"); 113 | } 114 | ExactSize { 115 | iter: Self::new(schema), 116 | count: shape.count.get(), 117 | } 118 | } 119 | 120 | /// Return the underlying schema 121 | #[inline] 122 | pub const fn schema(&self) -> &'static Schema { 123 | self.schema 124 | } 125 | 126 | /// Return the current state 127 | #[inline] 128 | pub fn state(&self) -> Option<&[usize]> { 129 | self.state.get(..self.depth) 130 | } 131 | 132 | /// Return the root depth 133 | #[inline] 134 | pub const fn root(&self) -> usize { 135 | self.root 136 | } 137 | } 138 | 139 | impl Iterator for NodeIter { 140 | type Item = Result; 141 | 142 | fn next(&mut self) -> Option { 143 | loop { 144 | debug_assert!(self.depth >= self.root); 145 | debug_assert!(self.depth <= D + 1); 146 | if self.depth == self.root { 147 | // Iteration done 148 | return None; 149 | } 150 | if self.depth <= D { 151 | // Not initial state: increment 152 | self.state[self.depth - 1] += 1; 153 | } 154 | let mut item = Track::new(N::default()); 155 | let ret = item.transcode(self.schema, &self.state[..]); 156 | // Track counts is the number of successful Keys::next() 157 | let (item, depth) = item.into_inner(); 158 | return match ret { 159 | Err(DescendError::Key(KeyError::NotFound)) => { 160 | // Reset index at NotFound depth, then retry with incremented earlier index or terminate 161 | self.state[depth] = 0; 162 | self.depth = depth.max(self.root); 163 | continue; 164 | } 165 | Err(DescendError::Key(KeyError::TooLong)) | Ok(()) => { 166 | // Leaf node found, save depth for increment at next iteration 167 | self.depth = depth; 168 | Some(Ok(item)) 169 | } 170 | Err(DescendError::Key(KeyError::TooShort)) => { 171 | // Use Short to suppress this branch and also get internal short nodes 172 | self.depth = depth; 173 | continue; 174 | } 175 | Err(DescendError::Inner(e)) => { 176 | // Target type can not hold keys 177 | Some(Err(e)) 178 | } 179 | }; 180 | } 181 | } 182 | } 183 | 184 | // Contract: Do not allow manipulation of `depth` other than through iteration. 185 | impl core::iter::FusedIterator for NodeIter {} 186 | -------------------------------------------------------------------------------- /miniconf/src/trace.rs: -------------------------------------------------------------------------------- 1 | //! Schema tracing 2 | 3 | use core::marker::PhantomData; 4 | 5 | use serde::Serialize; 6 | use serde_reflection::{Format, FormatHolder, Samples, Tracer, Value}; 7 | use std::sync::LazyLock; 8 | 9 | use crate::{ 10 | Internal, IntoKeys, Schema, SerdeError, TreeDeserialize, TreeSchema, TreeSerialize, ValueError, 11 | }; 12 | 13 | /// Trace a leaf value 14 | pub fn trace_value( 15 | tracer: &mut Tracer, 16 | samples: &mut Samples, 17 | keys: impl IntoKeys, 18 | value: impl TreeSerialize, 19 | ) -> Result<(Format, Value), SerdeError> { 20 | let (mut format, sample) = value.serialize_by_key( 21 | keys.into_keys(), 22 | serde_reflection::Serializer::new(tracer, samples), 23 | )?; 24 | format.reduce(); 25 | Ok((format, sample)) 26 | } 27 | 28 | /// Trace a leaf type once 29 | pub fn trace_type_once<'de, T: TreeDeserialize<'de>>( 30 | tracer: &mut Tracer, 31 | samples: &'de Samples, 32 | keys: impl IntoKeys, 33 | ) -> Result> { 34 | let mut format = Format::unknown(); 35 | T::probe_by_key( 36 | keys.into_keys(), 37 | serde_reflection::Deserializer::new(tracer, samples, &mut format), 38 | )?; 39 | format.reduce(); 40 | Ok(format) 41 | } 42 | 43 | /// Trace a leaf type until complete 44 | pub fn trace_type<'de, T: TreeDeserialize<'de>>( 45 | tracer: &mut Tracer, 46 | samples: &'de Samples, 47 | keys: impl IntoKeys + Clone, 48 | ) -> Result> { 49 | loop { 50 | let format = trace_type_once::(tracer, samples, keys.clone())?; 51 | if let Format::TypeName(name) = &format 52 | && let Some(_reason) = tracer.check_incomplete_enum(name) 53 | { 54 | // assert!( 55 | // !matches!(reason, serde_reflection::IncompleteEnumReason::Pending), 56 | // "failed to make progress tracing enum {name}" 57 | // ); 58 | // Restart the analysis to find more variants. 59 | continue; 60 | } 61 | return Ok(format); 62 | } 63 | } 64 | 65 | /// A node in a graph 66 | #[derive(Clone, Debug, PartialEq, PartialOrd, Hash, Serialize)] 67 | pub struct Node { 68 | /// Data associated witht this node 69 | pub data: D, 70 | /// Children of this node. 71 | /// 72 | /// Empty for leaf nodes. 73 | pub children: Vec>, 74 | } 75 | 76 | impl Node { 77 | /// Visit all nodes 78 | pub fn visit( 79 | &self, 80 | idx: &mut [usize], 81 | depth: usize, 82 | func: &mut impl FnMut(&[usize], &D) -> Result, 83 | ) -> Result { 84 | if depth < idx.len() { 85 | for (i, c) in self.children.iter().enumerate() { 86 | idx[depth] = i; 87 | c.visit(idx, depth + 1, func)?; 88 | } 89 | } 90 | (*func)(&idx[..depth], &self.data) 91 | } 92 | 93 | /// Mutably visit all nodes 94 | pub fn visit_mut( 95 | &mut self, 96 | idx: &mut [usize], 97 | depth: usize, 98 | func: &mut impl FnMut(&[usize], &mut D) -> Result, 99 | ) -> Result { 100 | if depth < idx.len() { 101 | for (i, c) in self.children.iter_mut().enumerate() { 102 | idx[depth] = i; 103 | c.visit_mut(idx, depth + 1, func)?; 104 | } 105 | } 106 | (*func)(&idx[..depth], &mut self.data) 107 | } 108 | } 109 | 110 | // Convert a Schema graph into a Node graph to be able to attach additional data to nodes. 111 | impl From<&'static Schema> for Node<(&'static Schema, L)> { 112 | fn from(value: &'static Schema) -> Self { 113 | Self { 114 | data: (value, L::default()), 115 | children: value 116 | .internal 117 | .as_ref() 118 | .map(|internal| match internal { 119 | Internal::Named(n) => n.iter().map(|n| Self::from(n.schema)).collect(), 120 | Internal::Numbered(n) => n.iter().map(|n| Self::from(n.schema)).collect(), 121 | Internal::Homogeneous(n) => vec![Self::from(n.schema)], 122 | }) 123 | .unwrap_or_default(), 124 | } 125 | } 126 | } 127 | 128 | /// Graph of `Node`s for a Tree type 129 | #[derive(Debug, Clone, PartialEq, Serialize)] 130 | pub struct Types { 131 | pub(crate) root: Node<(&'static Schema, Option)>, 132 | _t: PhantomData, 133 | } 134 | 135 | impl Types { 136 | /// Borrow the root node 137 | pub fn root(&self) -> &Node<(&'static Schema, Option)> { 138 | &self.root 139 | } 140 | } 141 | 142 | impl Default for Types { 143 | fn default() -> Self { 144 | Self { 145 | root: Node::from(T::SCHEMA), 146 | _t: PhantomData, 147 | } 148 | } 149 | } 150 | 151 | impl Types { 152 | /// Trace all leaf values 153 | pub fn trace_values( 154 | &mut self, 155 | tracer: &mut Tracer, 156 | samples: &mut Samples, 157 | value: &T, 158 | ) -> Result<(), serde_reflection::Error> 159 | where 160 | T: TreeSerialize, 161 | { 162 | let mut idx = vec![0; T::SCHEMA.shape().max_depth]; 163 | self.root 164 | .visit_mut(&mut idx, 0, &mut |idx, (schema, format)| { 165 | if schema.is_leaf() { 166 | match trace_value(tracer, samples, idx, value) { 167 | Ok((fmt, _value)) => { 168 | if let Some(f) = format { 169 | f.unify(fmt)?; 170 | } else { 171 | *format = Some(fmt); 172 | } 173 | } 174 | Err(SerdeError::Value(ValueError::Absent | ValueError::Access(_))) => {} 175 | // Eat serde errors 176 | Err(SerdeError::Inner(serde_reflection::Error::Custom(_))) => {} 177 | Err(SerdeError::Inner(e) | SerdeError::Finalization(e)) => Err(e)?, 178 | // KeyError: Keys are all valid leaves by construction 179 | Err(SerdeError::Value(ValueError::Key(_))) => unreachable!(), 180 | } 181 | } 182 | Ok(()) 183 | }) 184 | } 185 | 186 | /// Trace all leaf types until complete 187 | pub fn trace_types<'de>( 188 | &mut self, 189 | tracer: &mut Tracer, 190 | samples: &'de Samples, 191 | ) -> Result<(), serde_reflection::Error> 192 | where 193 | T: TreeDeserialize<'de>, 194 | { 195 | let mut idx = vec![0; T::SCHEMA.shape().max_depth]; 196 | self.root 197 | .visit_mut(&mut idx, 0, &mut |idx, (schema, format)| { 198 | if schema.is_leaf() { 199 | match trace_type::(tracer, samples, idx) { 200 | Ok(fmt) => { 201 | *format = Some(fmt); 202 | } 203 | // probe access denied 204 | Err(SerdeError::Value(ValueError::Access(_))) => {} 205 | // Eat serde errors 206 | Err(SerdeError::Inner(serde_reflection::Error::Custom(_))) => {} 207 | Err(SerdeError::Inner(e) | SerdeError::Finalization(e)) => Err(e)?, 208 | // ValueError::Absent: Nodes are never absent on probe 209 | // KeyError: Keys are all valid leaves by construction 210 | Err(SerdeError::Value(ValueError::Absent | ValueError::Key(_))) => { 211 | unreachable!() 212 | } 213 | } 214 | } 215 | Ok(()) 216 | }) 217 | } 218 | 219 | /// Normalize known formats 220 | pub fn normalize(&mut self) -> Result<(), serde_reflection::Error> 221 | where 222 | T: TreeSchema, 223 | { 224 | let mut idx = vec![0; T::SCHEMA.shape().max_depth]; 225 | self.root 226 | .visit_mut(&mut idx, 0, &mut |_idx, (_schema, format)| { 227 | if let Some(format) = format.as_mut() { 228 | format.normalize()?; 229 | } 230 | Ok(()) 231 | }) 232 | } 233 | 234 | /// Trace all leaf types assuming no samples are needed 235 | pub fn trace_types_simple<'de>( 236 | &mut self, 237 | tracer: &mut Tracer, 238 | ) -> Result<(), serde_reflection::Error> 239 | where 240 | T: TreeDeserialize<'de>, 241 | { 242 | static SAMPLES: LazyLock = LazyLock::new(Samples::new); 243 | self.trace_types(tracer, &SAMPLES) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /miniconf/src/packed.rs: -------------------------------------------------------------------------------- 1 | use core::num::NonZero; 2 | 3 | use crate::{DescendError, Internal, IntoKeys, Key, KeyError, Keys, Schema, Transcode}; 4 | 5 | /// A bit-packed representation of multiple indices. 6 | /// 7 | /// Given known bit width of each index, the bits are 8 | /// concatenated above a marker bit. 9 | /// 10 | /// The value consists of (from storage MSB to LSB): 11 | /// 12 | /// * Zero or more groups of variable bit length, concatenated, each containing 13 | /// one index. The first is aligned with the storage MSB. 14 | /// * A set bit to mark the end of the used bits. 15 | /// * Zero or more cleared bits corresponding to unused index space. 16 | /// 17 | /// [`Packed::EMPTY`] has the marker at the MSB. 18 | /// During [`Packed::push_lsb()`] the indices are inserted with their MSB 19 | /// where the marker was and the marker moves toward the storage LSB. 20 | /// During [`Packed::pop_msb()`] the indices are removed with their MSB 21 | /// aligned with the storage MSB and the remaining bits and the marker move 22 | /// toward the storage MSB. 23 | /// 24 | /// The representation is MSB aligned to make `PartialOrd`/`Ord` more natural and stable. 25 | /// The `Packed` key `Ord` matches the ordering of nodes in a horizontal leaf tree 26 | /// traversal. New nodes can be added/removed to the tree without changing the implicit 27 | /// encoding (and ordering!) as long no new bits need to be allocated/deallocated ( 28 | /// as long as the number of child nodes of an internal node does not cross a 29 | /// power-of-two boundary). 30 | /// Under this condition the mapping between indices/paths and `Packed` representation 31 | /// is stable even if child nodes are added/removed. 32 | /// 33 | /// "Small numbers" in LSB-aligned representation can be obtained through 34 | /// [`Packed::into_lsb()`]/[`Packed::from_lsb()`] but don't have the ordering 35 | /// and stability properties. 36 | /// 37 | /// `Packed` can be used to uniquely identify 38 | /// nodes in a `TreeSchema` using only a very small amount of bits. 39 | /// For many realistic `TreeSchema`s a `u16` or even a `u8` is sufficient 40 | /// to hold a `Packed` in LSB notation. Together with the 41 | /// `postcard` `serde` format, this then gives access to any node in a nested 42 | /// heterogeneous `Tree` with just a `u16` or `u8` as compact key and `[u8]` as 43 | /// compact value. 44 | /// 45 | /// ``` 46 | /// use miniconf::Packed; 47 | /// 48 | /// let mut p = Packed::EMPTY; 49 | /// let mut p_lsb = 0b1; // marker 50 | /// for (bits, value) in [(2, 0b11), (1, 0b0), (0, 0b0), (3, 0b101)] { 51 | /// p.push_lsb(bits, value).unwrap(); 52 | /// p_lsb <<= bits; 53 | /// p_lsb |= value; 54 | /// } 55 | /// assert_eq!(p_lsb, 0b1_11_0__101); 56 | /// // ^ marker 57 | /// assert_eq!(p, Packed::from_lsb(p_lsb.try_into().unwrap())); 58 | /// assert_eq!(p.get(), 0b11_0__101_1 << (Packed::CAPACITY - p.len())); 59 | /// // ^ marker 60 | /// ``` 61 | #[derive( 62 | Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash, serde::Serialize, serde::Deserialize, 63 | )] 64 | #[repr(transparent)] 65 | #[serde(transparent)] 66 | pub struct Packed(pub NonZero); 67 | 68 | impl Default for Packed { 69 | #[inline] 70 | fn default() -> Self { 71 | Self::EMPTY 72 | } 73 | } 74 | 75 | const TWO: NonZero = NonZero::::MIN.saturating_add(1); 76 | 77 | impl Packed { 78 | /// Number of bits in the representation including the marker bit 79 | pub const BITS: u32 = NonZero::::BITS; 80 | 81 | /// The total number of bits this representation can store. 82 | pub const CAPACITY: u32 = Self::BITS - 1; 83 | 84 | /// The empty value 85 | pub const EMPTY: Self = Self( 86 | // Slightly cumbersome to generate it with `const` 87 | TWO.saturating_pow(Self::CAPACITY), 88 | ); 89 | 90 | /// Create a new `Packed` from a `usize`. 91 | /// 92 | /// The value must not be zero. 93 | #[inline] 94 | pub const fn new(value: usize) -> Option { 95 | match NonZero::new(value) { 96 | Some(value) => Some(Self(value)), 97 | None => None, 98 | } 99 | } 100 | 101 | /// Create a new `Packed` from LSB aligned `usize` 102 | /// 103 | /// The value must not be zero. 104 | #[inline] 105 | pub const fn new_from_lsb(value: usize) -> Option { 106 | match NonZero::new(value) { 107 | Some(value) => Some(Self::from_lsb(value)), 108 | None => None, 109 | } 110 | } 111 | 112 | /// The primitive value 113 | #[inline] 114 | pub const fn get(&self) -> usize { 115 | self.0.get() 116 | } 117 | 118 | /// The value is empty. 119 | #[inline] 120 | pub const fn is_empty(&self) -> bool { 121 | matches!(*self, Self::EMPTY) 122 | } 123 | 124 | /// Number of bits stored. 125 | #[inline] 126 | pub const fn len(&self) -> u32 { 127 | Self::CAPACITY - self.0.trailing_zeros() 128 | } 129 | 130 | /// Return the representation aligned to the LSB with the marker bit 131 | /// moved from the LSB to the MSB. 132 | #[inline] 133 | pub const fn into_lsb(self) -> NonZero { 134 | TWO.saturating_pow(self.len()) 135 | .saturating_add((self.get() >> 1) >> self.0.trailing_zeros()) 136 | } 137 | 138 | /// Build a `Packed` from a LSB-aligned representation with the marker bit 139 | /// moved from the MSB the LSB. 140 | #[inline] 141 | pub const fn from_lsb(value: NonZero) -> Self { 142 | Self( 143 | TWO.saturating_pow(value.leading_zeros()) 144 | .saturating_add((value.get() << 1) << value.leading_zeros()), 145 | ) 146 | } 147 | 148 | /// Return the number of bits required to represent `num`. 149 | /// 150 | /// Ensures that at least one bit is allocated. 151 | #[inline] 152 | pub const fn bits_for(num: usize) -> u32 { 153 | match usize::BITS - num.leading_zeros() { 154 | 0 => 1, 155 | v => v, 156 | } 157 | } 158 | 159 | /// Remove the given number of MSBs and return them. 160 | /// 161 | /// If the value does not contain sufficient bits 162 | /// it is left unchanged and `None` is returned. 163 | /// 164 | /// # Args 165 | /// * `bits`: Number of bits to pop. `bits <= Self::CAPACITY` 166 | pub fn pop_msb(&mut self, bits: u32) -> Option { 167 | let s = self.get(); 168 | // Remove value from self 169 | Self::new(s << bits).map(|new| { 170 | *self = new; 171 | // Extract value from old self 172 | // Done in two steps as bits + 1 can be Self::BITS which would wrap. 173 | (s >> (Self::CAPACITY - bits)) >> 1 174 | }) 175 | } 176 | 177 | /// Push the given number `bits` of `value` as new LSBs. 178 | /// 179 | /// Returns the remaining number of unused bits on success. 180 | /// 181 | /// # Args 182 | /// * `bits`: Number of bits to push. `bits <= Self::CAPACITY` 183 | /// * `value`: Value to push. `value >> bits == 0` 184 | pub fn push_lsb(&mut self, bits: u32, value: usize) -> Option { 185 | debug_assert_eq!(value >> bits, 0); 186 | let mut n = self.0.trailing_zeros(); 187 | let old_marker = 1 << n; 188 | Self::new(old_marker >> bits).map(|new_marker| { 189 | n -= bits; 190 | // * Remove old marker 191 | // * Add value at offset n + 1 192 | // Done in two steps as n + 1 can be Self::BITS, which would wrap. 193 | // * Add new marker 194 | self.0 = (self.get() ^ old_marker) | ((value << n) << 1) | new_marker.0; 195 | n 196 | }) 197 | } 198 | } 199 | 200 | impl core::fmt::Display for Packed { 201 | #[inline] 202 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 203 | self.0.fmt(f) 204 | } 205 | } 206 | 207 | impl Keys for Packed { 208 | #[inline] 209 | fn next(&mut self, internal: &Internal) -> Result { 210 | let bits = Self::bits_for(internal.len().get() - 1); 211 | let index = self.pop_msb(bits).ok_or(KeyError::TooShort)?; 212 | index.find(internal).ok_or(KeyError::NotFound) 213 | } 214 | 215 | #[inline] 216 | fn finalize(&mut self) -> Result<(), KeyError> { 217 | if self.is_empty() { 218 | Ok(()) 219 | } else { 220 | Err(KeyError::TooLong) 221 | } 222 | } 223 | } 224 | 225 | impl IntoKeys for Packed { 226 | type IntoKeys = Self; 227 | 228 | #[inline] 229 | fn into_keys(self) -> Self::IntoKeys { 230 | self 231 | } 232 | } 233 | 234 | impl Transcode for Packed { 235 | type Error = (); 236 | 237 | fn transcode( 238 | &mut self, 239 | schema: &Schema, 240 | keys: impl IntoKeys, 241 | ) -> Result<(), DescendError> { 242 | schema.descend(keys.into_keys(), |_meta, idx_schema| { 243 | if let Some((index, internal)) = idx_schema { 244 | let bits = Packed::bits_for(internal.len().get() - 1); 245 | self.push_lsb(bits, index).ok_or(())?; 246 | } 247 | Ok(()) 248 | }) 249 | } 250 | } 251 | 252 | #[cfg(test)] 253 | mod test { 254 | use super::*; 255 | 256 | #[test] 257 | fn test() { 258 | // Check path encoding round trip. 259 | let t = [1usize, 3, 4, 0, 1]; 260 | let mut p = Packed::EMPTY; 261 | for t in t { 262 | let bits = Packed::bits_for(t); 263 | p.push_lsb(bits, t).unwrap(); 264 | } 265 | for t in t { 266 | let bits = Packed::bits_for(t); 267 | assert_eq!(p.pop_msb(bits).unwrap(), t); 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /miniconf/src/impls/key.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Write; 2 | 3 | #[cfg(feature = "alloc")] 4 | use alloc::vec::Vec; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::{DescendError, Internal, IntoKeys, Key, Schema, Track, Transcode}; 9 | 10 | // index 11 | macro_rules! impl_key_integer { 12 | ($($t:ty)+) => {$( 13 | impl Key for $t { 14 | #[inline] 15 | fn find(&self, internal: &Internal) -> Option { 16 | (*self).try_into().ok().filter(|i| *i < internal.len().get()) 17 | } 18 | } 19 | )+}; 20 | } 21 | impl_key_integer!(usize u8 u16 u32 u64 u128 isize i8 i16 i32 i64 i128); 22 | 23 | /// Indices of `usize` to identify a node in a `TreeSchema` 24 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)] 25 | pub struct Indices { 26 | len: usize, 27 | data: T, 28 | } 29 | 30 | impl Indices { 31 | /// Create a new `Indices` 32 | pub fn new(data: T, len: usize) -> Self { 33 | Self { len, data } 34 | } 35 | 36 | /// The length of the indices keys 37 | #[inline] 38 | pub fn len(&self) -> usize { 39 | self.len 40 | } 41 | 42 | /// See [`Self::len()`] 43 | #[inline] 44 | pub fn is_empty(&self) -> bool { 45 | self.len == 0 46 | } 47 | 48 | /// Split indices into data and length 49 | pub fn into_inner(self) -> (T, usize) { 50 | (self.data, self.len) 51 | } 52 | } 53 | 54 | impl From for Indices { 55 | #[inline] 56 | fn from(value: T) -> Self { 57 | Self { 58 | len: 0, 59 | data: value, 60 | } 61 | } 62 | } 63 | 64 | impl + ?Sized> AsRef<[U]> for Indices { 65 | #[inline] 66 | fn as_ref(&self) -> &[U] { 67 | &self.data.as_ref()[..self.len] 68 | } 69 | } 70 | 71 | impl<'a, U, T: ?Sized> IntoIterator for &'a Indices 72 | where 73 | &'a T: IntoIterator, 74 | { 75 | type Item = U; 76 | 77 | type IntoIter = core::iter::Take<<&'a T as IntoIterator>::IntoIter>; 78 | 79 | #[inline] 80 | fn into_iter(self) -> Self::IntoIter { 81 | (&self.data).into_iter().take(self.len) 82 | } 83 | } 84 | 85 | impl + ?Sized> Transcode for Indices { 86 | type Error = <[usize] as Transcode>::Error; 87 | 88 | fn transcode( 89 | &mut self, 90 | schema: &Schema, 91 | keys: impl IntoKeys, 92 | ) -> Result<(), DescendError> { 93 | let mut slic = Track::new(self.data.as_mut()); 94 | let ret = slic.transcode(schema, keys); 95 | self.len = slic.depth(); 96 | ret 97 | } 98 | } 99 | 100 | macro_rules! impl_transcode_slice { 101 | ($($t:ty)+) => {$( 102 | impl Transcode for [$t] { 103 | type Error = (); 104 | 105 | fn transcode(&mut self, schema: &Schema, keys: impl IntoKeys) -> Result<(), DescendError> { 106 | let mut it = self.iter_mut(); 107 | schema.descend(keys.into_keys(), |_meta, idx_schema| { 108 | if let Some((index, internal)) = idx_schema { 109 | debug_assert!(internal.len().get() <= <$t>::MAX as _); 110 | let i = index.try_into().or(Err(()))?; 111 | let idx = it.next().ok_or(())?; 112 | *idx = i; 113 | } 114 | Ok(()) 115 | }) 116 | } 117 | } 118 | )+}; 119 | } 120 | impl_transcode_slice!(usize u8 u16 u32 u64 u128 isize i8 i16 i32 i64 i128); 121 | 122 | #[cfg(feature = "alloc")] 123 | impl Transcode for Vec 124 | where 125 | usize: TryInto, 126 | { 127 | type Error = >::Error; 128 | 129 | fn transcode( 130 | &mut self, 131 | schema: &Schema, 132 | keys: impl IntoKeys, 133 | ) -> Result<(), DescendError> { 134 | schema.descend(keys.into_keys(), |_meta, idx_schema| { 135 | if let Some((index, _schema)) = idx_schema { 136 | self.push(index.try_into()?); 137 | } 138 | Ok(()) 139 | }) 140 | } 141 | } 142 | 143 | #[cfg(feature = "heapless")] 144 | impl Transcode for heapless::Vec 145 | where 146 | usize: TryInto, 147 | { 148 | type Error = (); 149 | 150 | fn transcode( 151 | &mut self, 152 | schema: &Schema, 153 | keys: impl IntoKeys, 154 | ) -> Result<(), DescendError> { 155 | schema.descend(keys.into_keys(), |_meta, idx_schema| { 156 | if let Some((index, _schema)) = idx_schema { 157 | let i = index.try_into().or(Err(()))?; 158 | self.push(i).or(Err(()))?; 159 | } 160 | Ok(()) 161 | }) 162 | } 163 | } 164 | 165 | //////////////////////////////////////////////////////////////////// 166 | 167 | // name 168 | impl Key for str { 169 | #[inline] 170 | fn find(&self, internal: &Internal) -> Option { 171 | internal.get_index(self) 172 | } 173 | } 174 | 175 | /// Path with named keys separated by a separator char 176 | /// 177 | /// The path will either be empty or start with the separator. 178 | /// 179 | /// * `path: T`: A `Write` to write the separators and node names into during `Transcode`. 180 | /// See also [Schema::transcode()] and `Shape.max_length` for upper bounds 181 | /// on path length. Can also be a `AsRef` to implement `IntoKeys` (see [`crate::KeysIter`]). 182 | /// * `const S: char`: The path hierarchy separator to be inserted before each name, 183 | /// e.g. `'/'`. 184 | #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] 185 | #[repr(transparent)] 186 | #[serde(transparent)] 187 | pub struct Path(pub T); 188 | 189 | impl Path { 190 | /// The path hierarchy separator 191 | #[inline] 192 | pub const fn separator(&self) -> char { 193 | S 194 | } 195 | } 196 | 197 | impl Path { 198 | /// Extract just the path 199 | #[inline] 200 | pub fn into_inner(self) -> T { 201 | self.0 202 | } 203 | } 204 | 205 | impl + ?Sized, const S: char> AsRef for Path { 206 | fn as_ref(&self) -> &str { 207 | self.0.as_ref() 208 | } 209 | } 210 | 211 | impl core::fmt::Display for Path { 212 | #[inline] 213 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 214 | self.0.fmt(f) 215 | } 216 | } 217 | 218 | /// String split/skip wrapper, smaller/simpler than `.split(S).skip(1)` 219 | #[derive(Copy, Clone, Default, Debug, PartialEq, PartialOrd, Serialize, Deserialize)] 220 | pub struct PathIter<'a> { 221 | data: Option<&'a str>, 222 | sep: char, 223 | } 224 | 225 | impl<'a> PathIter<'a> { 226 | /// Create a new `PathIter` 227 | #[inline] 228 | pub fn new(data: Option<&'a str>, sep: char) -> Self { 229 | Self { data, sep } 230 | } 231 | 232 | /// Create a new `PathIter` starting at the root. 233 | /// 234 | /// This calls `next()` once to pop everything up to and including the first separator. 235 | #[inline] 236 | pub fn root(data: &'a str, sep: char) -> Self { 237 | let mut s = Self::new(Some(data), sep); 238 | // Skip the first part to disambiguate between 239 | // the one-Key Keys `[""]` and the zero-Key Keys `[]`. 240 | // This is relevant in the case of e.g. `Option` and newtypes. 241 | // See the corresponding unittests (`just_option`). 242 | // It implies that Paths start with the separator 243 | // or are empty. Everything before the first separator is ignored. 244 | // This also means that paths can always be concatenated without having to 245 | // worry about adding/trimming leading or trailing separators. 246 | s.next(); 247 | s 248 | } 249 | } 250 | 251 | impl<'a> Iterator for PathIter<'a> { 252 | type Item = &'a str; 253 | 254 | fn next(&mut self) -> Option { 255 | self.data.map(|s| { 256 | let pos = s 257 | .chars() 258 | .map_while(|c| (c != self.sep).then_some(c.len_utf8())) 259 | .sum(); 260 | let (left, right) = s.split_at(pos); 261 | self.data = right.get(self.sep.len_utf8()..); 262 | left 263 | }) 264 | } 265 | } 266 | 267 | impl core::iter::FusedIterator for PathIter<'_> {} 268 | 269 | impl<'a, T: AsRef + ?Sized, const S: char> IntoKeys for Path<&'a T, S> { 270 | type IntoKeys = as IntoKeys>::IntoKeys; 271 | 272 | #[inline] 273 | fn into_keys(self) -> Self::IntoKeys { 274 | PathIter::root(self.0.as_ref(), S).into_keys() 275 | } 276 | } 277 | 278 | impl<'a, T: AsRef + ?Sized, const S: char> IntoKeys for &'a Path { 279 | type IntoKeys = as IntoKeys>::IntoKeys; 280 | 281 | #[inline] 282 | fn into_keys(self) -> Self::IntoKeys { 283 | PathIter::root(self.0.as_ref(), S).into_keys() 284 | } 285 | } 286 | 287 | impl Transcode for Path { 288 | type Error = core::fmt::Error; 289 | 290 | fn transcode( 291 | &mut self, 292 | schema: &Schema, 293 | keys: impl IntoKeys, 294 | ) -> Result<(), DescendError> { 295 | schema.descend(keys.into_keys(), |_meta, idx_schema| { 296 | if let Some((index, internal)) = idx_schema { 297 | self.0.write_char(S)?; 298 | let mut buf = itoa::Buffer::new(); 299 | let name = internal 300 | .get_name(index) 301 | .unwrap_or_else(|| buf.format(index)); 302 | debug_assert!(!name.contains(S)); 303 | self.0.write_str(name) 304 | } else { 305 | Ok(()) 306 | } 307 | }) 308 | } 309 | } 310 | 311 | #[cfg(test)] 312 | mod test { 313 | use super::*; 314 | 315 | #[test] 316 | fn strsplit() { 317 | use heapless::Vec; 318 | for p in ["/d/1", "/a/bccc//d/e/", "", "/", "a/b", "a"] { 319 | let a: Vec<_, 10> = PathIter::root(p, '/').collect(); 320 | let b: Vec<_, 10> = p.split('/').skip(1).collect(); 321 | assert_eq!(a, b); 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /miniconf/src/key.rs: -------------------------------------------------------------------------------- 1 | use core::{convert::Infallible, iter::Fuse}; 2 | 3 | use serde::Serialize; 4 | 5 | use crate::{DescendError, Internal, KeyError, Schema}; 6 | 7 | /// Convert a key into a node index given an internal node schema 8 | pub trait Key { 9 | /// Convert the key `self` to a `usize` index 10 | fn find(&self, internal: &Internal) -> Option; 11 | } 12 | 13 | impl Key for &T { 14 | #[inline] 15 | fn find(&self, internal: &Internal) -> Option { 16 | (**self).find(internal) 17 | } 18 | } 19 | 20 | impl Key for &mut T { 21 | #[inline] 22 | fn find(&self, internal: &Internal) -> Option { 23 | (**self).find(internal) 24 | } 25 | } 26 | 27 | /// Capability to yield and look up [`Key`]s 28 | pub trait Keys { 29 | /// Look up the next key in a [`Internal`] and convert to `usize` index. 30 | /// 31 | /// This must be fused (like [`core::iter::FusedIterator`]). 32 | fn next(&mut self, internal: &Internal) -> Result; 33 | 34 | /// Finalize the keys, ensure there are no more. 35 | /// 36 | /// This must be fused. 37 | fn finalize(&mut self) -> Result<(), KeyError>; 38 | 39 | /// Chain another `Keys` to this one. 40 | #[inline] 41 | fn chain(self, other: U) -> Chain 42 | where 43 | Self: Sized, 44 | { 45 | Chain(self, other.into_keys()) 46 | } 47 | 48 | /// Track depth 49 | #[inline] 50 | fn track(self) -> Track 51 | where 52 | Self: Sized, 53 | { 54 | Track { 55 | inner: self, 56 | depth: 0, 57 | } 58 | } 59 | 60 | /// Track whether a leaf node is reached 61 | #[inline] 62 | fn short(self) -> Short 63 | where 64 | Self: Sized, 65 | { 66 | Short { 67 | inner: self, 68 | leaf: false, 69 | } 70 | } 71 | } 72 | 73 | impl Keys for &mut T { 74 | #[inline] 75 | fn next(&mut self, internal: &Internal) -> Result { 76 | (**self).next(internal) 77 | } 78 | 79 | #[inline] 80 | fn finalize(&mut self) -> Result<(), KeyError> { 81 | (**self).finalize() 82 | } 83 | } 84 | 85 | /// Be converted into a `Keys` 86 | pub trait IntoKeys { 87 | /// The specific `Keys` implementor. 88 | type IntoKeys: Keys; 89 | 90 | /// Convert `self` into a `Keys` implementor. 91 | fn into_keys(self) -> Self::IntoKeys; 92 | } 93 | 94 | /// Look up an `IntoKeys` in a `Schema` and transcode it. 95 | pub trait Transcode { 96 | /// The possible error when transcoding. 97 | /// 98 | /// Use this to indicate no space or unencodable/invalid values 99 | type Error; 100 | 101 | /// Perform a node lookup of a `K: IntoKeys` on a `Schema` and transcode it. 102 | /// 103 | /// This modifies `self` such that afterwards `Self: IntoKeys` can be used on `M` again. 104 | /// It returns a `Node` with node type and depth information. 105 | /// 106 | /// Returning `Err(ValueError::Absent)` indicates that there was insufficient 107 | /// capacity and a key could not be encoded at the given depth. 108 | fn transcode( 109 | &mut self, 110 | schema: &Schema, 111 | keys: impl IntoKeys, 112 | ) -> Result<(), DescendError>; 113 | } 114 | 115 | impl Transcode for &mut T { 116 | type Error = T::Error; 117 | #[inline] 118 | fn transcode( 119 | &mut self, 120 | schema: &Schema, 121 | keys: impl IntoKeys, 122 | ) -> Result<(), DescendError> { 123 | (**self).transcode(schema, keys) 124 | } 125 | } 126 | 127 | /// Track leaf node encounter 128 | /// 129 | /// This records whether a leaf node has been reached during [`Keys`] and [`Transcode`]. 130 | #[derive(Clone, Debug, Default, PartialEq, PartialOrd, Hash, Serialize)] 131 | pub struct Short { 132 | /// The inner Keys 133 | inner: K, 134 | /// The inner keys terminates at a leaf node 135 | leaf: bool, 136 | } 137 | 138 | impl Short { 139 | /// Create a new `Short` 140 | #[inline] 141 | pub fn new(inner: K) -> Self { 142 | Self { inner, leaf: false } 143 | } 144 | 145 | /// Whether a leaf node as been encountered 146 | #[inline] 147 | pub fn leaf(&self) -> bool { 148 | self.leaf 149 | } 150 | 151 | /// Borrow the inner `Keys` 152 | #[inline] 153 | pub fn inner(&self) -> &K { 154 | &self.inner 155 | } 156 | 157 | /// Split into inner `Keys` and leaf node flag 158 | #[inline] 159 | pub fn into_inner(self) -> (K, bool) { 160 | (self.inner, self.leaf) 161 | } 162 | } 163 | 164 | impl IntoKeys for &mut Short { 165 | type IntoKeys = Self; 166 | 167 | #[inline] 168 | fn into_keys(self) -> Self::IntoKeys { 169 | self.leaf = false; 170 | self 171 | } 172 | } 173 | 174 | impl Keys for Short { 175 | #[inline] 176 | fn next(&mut self, internal: &Internal) -> Result { 177 | self.inner.next(internal) 178 | } 179 | 180 | #[inline] 181 | fn finalize(&mut self) -> Result<(), KeyError> { 182 | self.inner.finalize()?; 183 | self.leaf = true; 184 | Ok(()) 185 | } 186 | } 187 | 188 | impl Transcode for Short { 189 | type Error = T::Error; 190 | 191 | #[inline] 192 | fn transcode( 193 | &mut self, 194 | schema: &Schema, 195 | keys: impl IntoKeys, 196 | ) -> Result<(), DescendError> { 197 | self.leaf = false; 198 | match self.inner.transcode(schema, keys) { 199 | Err(DescendError::Key(KeyError::TooShort)) => Ok(()), 200 | Ok(()) | Err(DescendError::Key(KeyError::TooLong)) => { 201 | self.leaf = true; 202 | Ok(()) 203 | } 204 | ret => ret, 205 | } 206 | } 207 | } 208 | 209 | /// Track key depth 210 | /// 211 | /// This tracks the depth during [`Keys`] and [`Transcode`]. 212 | #[derive(Clone, Debug, Default, PartialEq, PartialOrd, Hash, Serialize)] 213 | pub struct Track { 214 | /// The inner keys 215 | inner: K, 216 | /// The keys terminate at the given depth 217 | depth: usize, 218 | } 219 | 220 | impl Track { 221 | /// Create a new `Track` 222 | #[inline] 223 | pub fn new(inner: K) -> Self { 224 | Self { inner, depth: 0 } 225 | } 226 | 227 | /// Whether a leaf node as been encountered 228 | #[inline] 229 | pub fn depth(&self) -> usize { 230 | self.depth 231 | } 232 | 233 | /// Borrow the inner `Keys` 234 | #[inline] 235 | pub fn inner(&self) -> &K { 236 | &self.inner 237 | } 238 | 239 | /// Split into inner `Keys` and leaf node flag 240 | #[inline] 241 | pub fn into_inner(self) -> (K, usize) { 242 | (self.inner, self.depth) 243 | } 244 | } 245 | 246 | impl IntoKeys for &mut Track { 247 | type IntoKeys = Self; 248 | 249 | #[inline] 250 | fn into_keys(self) -> Self::IntoKeys { 251 | self.depth = 0; 252 | self 253 | } 254 | } 255 | 256 | impl Keys for Track { 257 | #[inline] 258 | fn next(&mut self, internal: &Internal) -> Result { 259 | let k = self.inner.next(internal); 260 | if k.is_ok() { 261 | self.depth += 1; 262 | } 263 | k 264 | } 265 | 266 | #[inline] 267 | fn finalize(&mut self) -> Result<(), KeyError> { 268 | self.inner.finalize() 269 | } 270 | } 271 | 272 | impl Transcode for Track { 273 | type Error = T::Error; 274 | 275 | #[inline] 276 | fn transcode( 277 | &mut self, 278 | schema: &Schema, 279 | keys: impl IntoKeys, 280 | ) -> Result<(), DescendError> { 281 | self.depth = 0; 282 | let mut tracked = keys.into_keys().track(); 283 | let ret = self.inner.transcode(schema, &mut tracked); 284 | self.depth = tracked.depth; 285 | ret 286 | } 287 | } 288 | 289 | /// Shim to provide the bare lookup/Track/Short without transcoding target 290 | impl Transcode for () { 291 | type Error = Infallible; 292 | #[inline] 293 | fn transcode( 294 | &mut self, 295 | schema: &Schema, 296 | keys: impl IntoKeys, 297 | ) -> Result<(), DescendError> { 298 | schema.descend(keys.into_keys(), |_, _| Ok(())) 299 | } 300 | } 301 | 302 | /// [`Keys`]/[`IntoKeys`] for Iterators of [`Key`] 303 | #[derive(Debug, Clone)] 304 | #[repr(transparent)] 305 | pub struct KeysIter(Fuse); 306 | 307 | impl KeysIter { 308 | #[inline] 309 | fn new(inner: T) -> Self { 310 | Self(inner.fuse()) 311 | } 312 | } 313 | 314 | impl Keys for KeysIter 315 | where 316 | T: Iterator, 317 | T::Item: Key, 318 | { 319 | #[inline] 320 | fn next(&mut self, internal: &Internal) -> Result { 321 | let n = self.0.next().ok_or(KeyError::TooShort)?; 322 | n.find(internal).ok_or(KeyError::NotFound) 323 | } 324 | 325 | #[inline] 326 | fn finalize(&mut self) -> Result<(), KeyError> { 327 | match self.0.next() { 328 | Some(_) => Err(KeyError::TooLong), 329 | None => Ok(()), 330 | } 331 | } 332 | } 333 | 334 | impl IntoKeys for T 335 | where 336 | T: IntoIterator, 337 | ::Item: Key, 338 | { 339 | type IntoKeys = KeysIter; 340 | 341 | #[inline] 342 | fn into_keys(self) -> Self::IntoKeys { 343 | KeysIter::new(self.into_iter()) 344 | } 345 | } 346 | 347 | impl IntoKeys for KeysIter 348 | where 349 | T: Iterator, 350 | T::Item: Key, 351 | { 352 | type IntoKeys = KeysIter; 353 | 354 | #[inline] 355 | fn into_keys(self) -> Self::IntoKeys { 356 | self 357 | } 358 | } 359 | 360 | /// Concatenate two `Keys` of different types 361 | pub struct Chain(T, U); 362 | 363 | impl Keys for Chain { 364 | #[inline] 365 | fn next(&mut self, internal: &Internal) -> Result { 366 | match self.0.next(internal) { 367 | Err(KeyError::TooShort) => self.1.next(internal), 368 | ret => ret, 369 | } 370 | } 371 | 372 | #[inline] 373 | fn finalize(&mut self) -> Result<(), KeyError> { 374 | self.0.finalize().and_then(|_| self.1.finalize()) 375 | } 376 | } 377 | 378 | impl IntoKeys for Chain { 379 | type IntoKeys = Self; 380 | 381 | #[inline] 382 | fn into_keys(self) -> Self::IntoKeys { 383 | self 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /py/miniconf-mqtt/miniconf/sync.py: -------------------------------------------------------------------------------- 1 | """ 2 | Synchronous Miniconf-over-MQTT utilities 3 | """ 4 | 5 | # pylint: disable=R0801,C0415,W1203,R0903,W0707 6 | 7 | import json 8 | import uuid 9 | import threading 10 | import time 11 | from typing import Dict, Any 12 | 13 | import paho.mqtt 14 | from paho.mqtt.properties import Properties, PacketTypes 15 | from paho.mqtt.client import Client, MQTTMessage 16 | 17 | from .common import MiniconfException, LOGGER, json_dumps 18 | 19 | 20 | class Miniconf: 21 | """Miniconf over MQTT (synchronous)""" 22 | 23 | def __init__(self, client: Client, prefix: str): 24 | """ 25 | Args: 26 | client: A connected MQTT5 client. 27 | prefix: The MQTT toptic prefix of the device to control. 28 | """ 29 | self.client = client 30 | self.prefix = prefix 31 | self._inflight = {} 32 | self.response_topic = f"{prefix}/response" 33 | self._subscribe() 34 | 35 | def _subscribe(self): 36 | cond = threading.Event() 37 | self.client.on_subscribe = ( 38 | lambda _client, _userdata, _mid, _reason, _prop: cond.set() 39 | ) 40 | try: 41 | self.client.subscribe(self.response_topic) 42 | self.client.on_message = self._dispatch 43 | cond.wait() 44 | finally: 45 | self.client.on_subscribe = None 46 | LOGGER.debug(f"Subscribed to {self.response_topic}") 47 | 48 | def close(self): 49 | """Unsubscribe from the response topic""" 50 | cond = threading.Event() 51 | self.client.on_unsubscribe = ( 52 | lambda _client, _userdata, _mid, _reason, _prop: cond.set() 53 | ) 54 | try: 55 | self.client.unsubscribe(self.response_topic) 56 | self.client.on_message = None 57 | cond.wait() 58 | finally: 59 | self.client.on_unsubscribe = None 60 | LOGGER.debug(f"Unsubscribed from {self.response_topic}") 61 | 62 | def _dispatch(self, _client, _userdara, message: MQTTMessage): 63 | if message.topic != self.response_topic: 64 | LOGGER.warning( 65 | "Discarding message with unexpected topic: %s", message.topic 66 | ) 67 | return 68 | 69 | try: 70 | properties = message.properties.json() 71 | except AttributeError: 72 | properties = {} 73 | # lazy formatting 74 | LOGGER.debug("Received %s: %s [%s]", message.topic, message.payload, properties) 75 | 76 | try: 77 | cd = bytes.fromhex(properties["CorrelationData"]) 78 | except KeyError: 79 | LOGGER.debug("Discarding message without CorrelationData") 80 | return 81 | try: 82 | event, ret = self._inflight[cd] 83 | except KeyError: 84 | LOGGER.debug(f"Discarding message with unexpected CorrelationData: {cd}") 85 | return 86 | 87 | try: 88 | code = dict(properties["UserProperty"])["code"] 89 | except KeyError: 90 | LOGGER.warning("Discarding message without response code user property") 91 | return 92 | 93 | resp = message.payload.decode("utf-8") 94 | if code == "Continue": 95 | ret.append(resp) 96 | elif code == "Ok": 97 | if resp: 98 | ret.append(resp) 99 | event.set() 100 | del self._inflight[cd] 101 | else: 102 | ret[:] = [MiniconfException(code, resp)] 103 | event.set() 104 | del self._inflight[cd] 105 | 106 | def _do(self, path: str, *, response=1, timeout=None, **kwargs): 107 | response = int(response) 108 | 109 | props = Properties(PacketTypes.PUBLISH) 110 | 111 | if response: 112 | event = threading.Event() 113 | ret = [] 114 | cd = uuid.uuid1().bytes 115 | props.ResponseTopic = self.response_topic 116 | props.CorrelationData = cd 117 | assert cd not in self._inflight 118 | self._inflight[cd] = event, ret 119 | 120 | topic = f"{self.prefix}/settings{path}" 121 | LOGGER.debug("Publishing %s: %s, [%s]", topic, kwargs.get("payload"), props) 122 | _pub = self.client.publish(topic, properties=props, **kwargs) 123 | 124 | if response: 125 | event.wait(timeout) 126 | if len(ret) == 1 and isinstance(ret[0], MiniconfException): 127 | raise ret[0] 128 | if response == 1: 129 | if len(ret) != 1: 130 | raise MiniconfException("Not a leaf", ret) 131 | return ret[0] 132 | assert ret 133 | return ret 134 | # pub.wait_for_publish(timeout) 135 | return None 136 | 137 | def set(self, path: str, value, retain=False, response=True, **kwargs): 138 | """Write the provided data to the specified path. 139 | 140 | Args: 141 | path: The path to set. 142 | value: The value to set. 143 | retain: Retain the the setting on the broker. 144 | response: Request and await the result of the operation. 145 | """ 146 | return self._do( 147 | path, 148 | payload=json_dumps(value), 149 | response=response, 150 | retain=retain, 151 | **kwargs, 152 | ) 153 | 154 | def list(self, path: str = "", **kwargs): 155 | """Get a list of all the paths below a given root. 156 | 157 | Args: 158 | path: Path to the root node to list. 159 | """ 160 | return self._do(path, response=2, **kwargs) 161 | 162 | def dump(self, path: str = "", **kwargs): 163 | """Dump all the paths at or below a given root into the settings namespace. 164 | 165 | Note that the target may be unable to respond to messages when a multipart 166 | operation (list or dump) is in progress. 167 | This method does not wait for completion. 168 | 169 | Args: 170 | path: Path to the root node to dump. Can be a leaf or an internal node. 171 | """ 172 | return self._do(path, response=0, **kwargs) 173 | 174 | def get(self, path: str, **kwargs): 175 | """Get the specific value of a given path. 176 | 177 | Args: 178 | path: The path to get. Must be a leaf node. 179 | """ 180 | return json.loads(self._do(path, **kwargs)) 181 | 182 | def clear(self, path: str, response=True, **kwargs): 183 | """Clear retained value from a path. 184 | 185 | This does not change (`set()`) or reset/clear the value on the device. 186 | 187 | Args: 188 | path: The path to clear. Must be a leaf node. 189 | response: Obtain and await the result of the operation. 190 | """ 191 | return json.loads( 192 | self._do( 193 | path, 194 | retain=True, 195 | response=response, 196 | **kwargs, 197 | ) 198 | ) 199 | 200 | 201 | def discover( 202 | client: Client, 203 | prefix: str, 204 | rel_timeout: float = 3.0, 205 | abs_timeout: float = 0.1, 206 | ) -> Dict[str, Any]: 207 | """Get a list of available Miniconf devices. 208 | 209 | Args: 210 | * `client` - The MQTT client to search for clients on. Connected to a broker 211 | * `prefix` - An MQTT-specific topic filter for device prefixes. Note that this will 212 | be appended to with the default status topic name `/alive`. 213 | * `rel_timeout` - The duration to search for clients in units of the time it takes 214 | to ack the subscribe to the alive topic. 215 | * `abs_timeout` - Additional absolute duration to wait for client discovery 216 | in seconds. 217 | 218 | Returns: 219 | A dictionary of discovered client prefixes and metadata payload. 220 | """ 221 | discovered = {} 222 | suffix = "/alive" 223 | topic = f"{prefix}{suffix}" 224 | 225 | def on_message(_client, _userdata, message): 226 | LOGGER.debug(f"Got message from {message.topic}: {message.payload}") 227 | peer = message.topic.removesuffix(suffix) 228 | try: 229 | payload = json.loads(message.payload) 230 | except json.JSONDecodeError: 231 | LOGGER.info(f"Ignoring {peer} not/invalid alive") 232 | else: 233 | LOGGER.debug(f"Discovered {peer} alive") 234 | discovered[peer] = payload 235 | 236 | client.on_message = on_message 237 | 238 | t_start = time.monotonic() 239 | cond = threading.Event() 240 | client.on_subscribe = lambda client, userdata, mid, reason, prop: cond.set() 241 | client.subscribe(topic) 242 | cond.wait() 243 | client.on_subscribe = None 244 | t_subscribe = time.monotonic() - t_start 245 | 246 | time.sleep(rel_timeout * t_subscribe + abs_timeout) 247 | client.unsubscribe(topic) 248 | client.on_message = None 249 | return discovered 250 | 251 | 252 | def _main(): 253 | import logging 254 | from .common import _cli, MQTTv5, one 255 | 256 | args = _cli().parse_args() 257 | 258 | logging.basicConfig( 259 | format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", 260 | level=logging.WARN - 10 * args.verbose, 261 | ) 262 | 263 | client = Client(paho.mqtt.enums.CallbackAPIVersion.VERSION2, protocol=MQTTv5) 264 | client.connect(args.broker) 265 | client.loop_start() 266 | 267 | if args.discover: 268 | prefix, _alive = one(discover(client, args.prefix)) 269 | else: 270 | prefix = args.prefix 271 | 272 | interface = Miniconf(client, prefix) 273 | 274 | try: 275 | _handle_commands(interface, args.commands, args.retain) 276 | finally: 277 | interface.close() 278 | client.disconnect() 279 | client.loop_stop() 280 | 281 | 282 | def _handle_commands(interface, commands, retain): 283 | import sys 284 | from .common import _Path 285 | 286 | current = _Path() 287 | for arg in commands: 288 | try: 289 | if arg.endswith("?"): 290 | path = current.normalize(arg.removesuffix("?")) 291 | paths = interface.list(path) 292 | # Note: There is no way for the CLI tool to reliably 293 | # distinguish a one-element leaf get responce from a 294 | # one-element inner list response without looking at 295 | # the payload. 296 | # The only way is to note that a JSON payload of a 297 | # get can not start with the / that a list response 298 | # starts with. 299 | if len(paths) == 1 and not paths[0].startswith("/"): 300 | value = paths[0] 301 | print(f"{path}={value}") 302 | continue 303 | for p in paths: 304 | try: 305 | value = json_dumps(interface.get(p)) 306 | print(f"{p}={value}") 307 | except MiniconfException as err: 308 | print(f"{p}: {err!r}") 309 | elif arg.endswith("!"): 310 | path = current.normalize(arg.removesuffix("!")) 311 | interface.dump(path) 312 | print(f"DUMP {path}") 313 | elif "=" in arg: 314 | path, value = arg.split("=", 1) 315 | path = current.normalize(path) 316 | if not value: 317 | value = json_dumps(interface.clear(path)) 318 | print(f"CLEAR {path}={value}") 319 | else: 320 | interface.set(path, json.loads(value), retain) 321 | print(f"{path}={value}") 322 | else: 323 | path = current.normalize(arg) 324 | value = json_dumps(interface.get(path)) 325 | print(f"{path}={value!r}") 326 | except MiniconfException as err: 327 | print(f"{arg}: {err!r}") 328 | sys.exit(1) 329 | 330 | 331 | if __name__ == "__main__": 332 | _main() 333 | -------------------------------------------------------------------------------- /miniconf/README.md: -------------------------------------------------------------------------------- 1 | # miniconf: serialize/deserialize/access reflection for trees 2 | 3 | [![crates.io](https://img.shields.io/crates/v/miniconf.svg)](https://crates.io/crates/miniconf) 4 | [![docs](https://docs.rs/miniconf/badge.svg)](https://docs.rs/miniconf) 5 | [![QUARTIQ Matrix Chat](https://img.shields.io/matrix/quartiq:matrix.org)](https://matrix.to/#/#quartiq:matrix.org) 6 | [![Continuous Integration](https://github.com/vertigo-designs/miniconf/workflows/Continuous%20Integration/badge.svg)](https://github.com/quartiq/miniconf/actions) 7 | 8 | `miniconf` enables lightweight (`no_std`/no alloc) serialization, deserialization, 9 | and access within a tree of heterogeneous types by keys. 10 | 11 | ## Example 12 | 13 | See below for an example showing some of the features of the `Tree*` traits. 14 | See also the documentation and doctests of the [`TreeSchema`] trait for a detailed description. 15 | 16 | Note that the example below focuses on JSON and slash-separated paths while in fact 17 | any `serde` backend (or `dyn Any` trait objects) and many different `Keys`/`Transcode` 18 | providers are supported. 19 | 20 | ```rust 21 | use serde::{Deserialize, Serialize}; 22 | use miniconf::{SerdeError, json_core, JsonPath, ValueError, KeyError, Tree, TreeSchema, Path, Packed, Shape, leaf}; 23 | 24 | #[derive(Deserialize, Serialize, Default, Tree)] 25 | pub struct Inner { 26 | a: i32, 27 | b: u16, 28 | } 29 | 30 | #[derive(Deserialize, Serialize, Default, Tree)] 31 | pub enum Either { 32 | #[default] 33 | Bad, 34 | Good, 35 | A(i32), 36 | B(Inner), 37 | C([Inner; 2]), 38 | } 39 | 40 | #[derive(Tree, Default)] 41 | pub struct Settings { 42 | foo: bool, 43 | #[tree(with=leaf)] 44 | enum_: Either, 45 | #[tree(with=leaf)] 46 | struct_: Inner, 47 | #[tree(with=leaf)] 48 | array: [i32; 2], 49 | #[tree(with=leaf)] 50 | option: Option, 51 | 52 | #[tree(skip)] 53 | #[allow(unused)] 54 | skipped: (), 55 | 56 | struct_tree: Inner, 57 | enum_tree: Either, 58 | array_tree: [i32; 2], 59 | array_tree2: [Inner; 2], 60 | tuple_tree: (i32, Inner), 61 | option_tree: Option, 62 | option_tree2: Option, 63 | array_option_tree: [Option; 2], 64 | } 65 | 66 | let mut settings = Settings::default(); 67 | 68 | // Access nodes by field name 69 | json_core::set(&mut settings,"/foo", b"true")?; 70 | assert_eq!(settings.foo, true); 71 | json_core::set(&mut settings, "/enum_", br#""Good""#)?; 72 | json_core::set(&mut settings, "/struct_", br#"{"a": 3, "b": 3}"#)?; 73 | json_core::set(&mut settings, "/array", b"[6, 6]")?; 74 | json_core::set(&mut settings, "/option", b"12")?; 75 | json_core::set(&mut settings, "/option", b"null")?; 76 | 77 | // Nodes inside containers 78 | // ... by field name in a struct 79 | json_core::set(&mut settings, "/struct_tree/a", b"4")?; 80 | // ... or by index in an array 81 | json_core::set(&mut settings, "/array_tree/0", b"7")?; 82 | // ... or by index and then struct field name 83 | json_core::set(&mut settings, "/array_tree2/0/a", b"11")?; 84 | // ... or by hierarchical index 85 | json_core::set_by_key(&mut settings, [8, 0, 1], b"8")?; 86 | // ... or by packed index 87 | let packed: Packed = Settings::SCHEMA.transcode([8, 1, 0]).unwrap(); 88 | assert_eq!(packed.into_lsb().get(), 0b1_1000_1_0); 89 | json_core::set_by_key(&mut settings, packed, b"9")?; 90 | // ... or by JSON path 91 | json_core::set_by_key(&mut settings, JsonPath(".array_tree2[1].b"), b"10")?; 92 | 93 | // Hiding paths by setting an Option to `None` at runtime 94 | assert_eq!(json_core::set(&mut settings, "/option_tree", b"13"), Err(ValueError::Absent.into())); 95 | settings.option_tree = Some(0); 96 | json_core::set(&mut settings, "/option_tree", b"13")?; 97 | // Hiding a path and descending into the inner `Tree` 98 | settings.option_tree2 = Some(Inner::default()); 99 | json_core::set(&mut settings, "/option_tree2/a", b"14")?; 100 | // Hiding items of an array of `Tree`s 101 | settings.array_option_tree[1] = Some(Inner::default()); 102 | json_core::set(&mut settings, "/array_option_tree/1/a", b"15")?; 103 | 104 | let mut buf = [0; 16]; 105 | 106 | // Serializing nodes by path 107 | let len = json_core::get(&settings, "/struct_", &mut buf).unwrap(); 108 | assert_eq!(&buf[..len], br#"{"a":3,"b":3}"#); 109 | 110 | // Tree metadata 111 | const MAX_DEPTH: usize = Settings::SCHEMA.shape().max_depth; 112 | const MAX_LENGTH: usize = Settings::SCHEMA.shape().max_length("/"); 113 | assert!(MAX_DEPTH <= 6); 114 | assert!(MAX_LENGTH <= 32); 115 | 116 | // Iterating over all leaf paths 117 | for path in Settings::SCHEMA.nodes::, '/'>, MAX_DEPTH>() { 118 | let path = path.unwrap().0; 119 | // Serialize each 120 | match json_core::get(&settings, &path, &mut buf) { 121 | // Full round-trip: deserialize and set again 122 | Ok(len) => { json_core::set(&mut settings, &path, &buf[..len])?; } 123 | // Some Options are `None`, some enum variants are absent 124 | Err(SerdeError::Value(ValueError::Absent)) => {} 125 | e => { e.unwrap(); } 126 | } 127 | } 128 | 129 | # Ok::<(), SerdeError>(()) 130 | ``` 131 | 132 | ## Settings management 133 | 134 | One possible use of `miniconf` is a backend for run-time settings management in embedded devices. 135 | 136 | It was originally designed to work with JSON ([`serde_json_core`](https://docs.rs/serde-json-core)) 137 | payloads over MQTT ([`minimq`](https://docs.rs/minimq)) and provides a MQTT settings management 138 | client in the `miniconf_mqtt` crate and a Python reference implementation to interact with it. 139 | Miniconf is agnostic of the `serde` backend/format, key type/format, and transport/protocol. 140 | 141 | ## Formats 142 | 143 | `miniconf` can be used with any `serde::Serializer`/`serde::Deserializer` backend, and key format. 144 | 145 | Explicit support for `/` as the path hierarchy separator and JSON (`serde_json_core`) is implemented. 146 | 147 | Support for the `postcard` wire format with any `postcard` flavor and 148 | any [`Keys`] type is implemented. Combined with the [`Packed`] key representation, this is a very 149 | space-efficient serde-by-key API. 150 | 151 | Blanket implementations are provided for all 152 | `TreeSerialize`+`TreeDeserialize` types for all formats. 153 | 154 | ## Transport 155 | 156 | `miniconf` is also protocol-agnostic. Any means that can receive or emit serialized key-value data 157 | can be used to access nodes by path. 158 | 159 | The `MqttClient` in the `miniconf_mqtt` crate implements settings management over the [MQTT 160 | protocol](https://mqtt.org) with JSON payloads. A Python reference library is provided that 161 | interfaces with it. This example discovers the unique prefix of an application listening to messages 162 | under the topic `quartiq/application/12345` and set its `/foo` setting to `true`. 163 | 164 | ```sh 165 | python -m miniconf -d quartiq/application/+ /foo=true 166 | ``` 167 | 168 | ## Derive macros 169 | 170 | For structs `miniconf` offers derive macros for [`macro@TreeSchema`], [`macro@TreeSerialize`], [`macro@TreeDeserialize`], and [`macro@TreeAny`]. 171 | The macros implements the [`TreeSchema`], [`TreeSerialize`], [`TreeDeserialize`], and [`TreeAny`] traits. 172 | Fields/variants that form internal nodes (non-leaf) need to implement the respective `Tree{Key,Serialize,Deserialize,Any}` trait. 173 | Leaf fields/items need to support the respective [`serde`] (and the desired `serde::Serializer`/`serde::Deserializer` 174 | backend) or [`core::any`] trait. 175 | 176 | Structs, enums, arrays, Options, and many other containers can then be cascaded to construct more complex trees. 177 | 178 | See also the [`TreeSchema`] trait documentation for details. 179 | 180 | ## Keys and paths 181 | 182 | Lookup into the tree is done using a [`Keys`] implementation. A blanket implementation through [`IntoKeys`] 183 | is provided for `IntoIterator`s over [`Key`] items. The [`Key`] lookup capability is implemented 184 | for `usize` indices and `&str` names. 185 | 186 | Path iteration is supported with arbitrary separator `char`s between names. 187 | 188 | Very compact hierarchical indices encodings can be obtained from the [`Packed`] structure. 189 | It implements [`Keys`]. 190 | 191 | ## Limitations 192 | 193 | * `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. 194 | * 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). 195 | 196 | ## Features 197 | 198 | * `json-core`: Enable helper functions for serializing from and 199 | into json slices (using the `serde_json_core` crate). 200 | * `postcard`: Enable helper functions for serializing from and 201 | into the postcard compact binary format (using the `postcard` crate). 202 | * `derive`: Enable the derive macros in `miniconf_derive`. Enabled by default. 203 | 204 | ## Reflection 205 | 206 | `miniconf` enables certain kinds of reflective access to heterogeneous trees. 207 | Let's compare it to [`bevy_reflect`](https://crates.io/crates/bevy_reflect) 208 | which is a comprehensive and mature reflection crate: 209 | 210 | `bevy_reflect` is thoroughly `std` while `miniconf` aims at `no_std`. 211 | `bevy_reflect` uses its `Reflect` trait to operate on and pass nodes as trait objects. 212 | `miniconf` uses serialized data or `Any` to access leaf nodes and pure "code" to traverse through internal nodes. 213 | The `Tree*` traits like `Reflect` thus give access to nodes but unlike `Reflect` they are all decidedly not object-safe 214 | and can not be used as trait objects. This allows `miniconf` to support non-`'static` borrowed data 215 | (only for `TreeAny` the leaf nodes need to be `'static`) 216 | while `bevy_reflect` requires `'static` for `Reflect` types. 217 | 218 | `miniconf`supports at least the following reflection features mentioned in the `bevy_reflect` README: 219 | 220 | * ➕ Derive the traits: `miniconf` has `Tree*` derive macros and blanket implementations for arrays and Options. 221 | Leaf nodes just need some impls of `Serialize/Deserialize/Any` where desired. 222 | * ➕ Interact with fields using their names 223 | * ➖ "Patch" your types with new values: `miniconf` only supports limited changes to the tree structure at runtime 224 | (`Option` and custom accessors) while `bevy_reflect` has powerful dynamic typing tools. 225 | * ➕ Look up nested fields using "path strings": In addition to a superset of JSON path style 226 | "path strings" `miniconf` supports hierarchical indices and bit-packed ordered keys. 227 | * ➕ Iterate over struct fields: `miniconf` Supports recursive iteration over node keys. 228 | * ➕ Automatically serialize and deserialize via Serde without explicit serde impls: 229 | `miniconf` supports automatic serializing/deserializing into key-value pairs without an explicit container serde impls. 230 | * ➕ Trait "reflection": Together with [`crosstrait`](https://crates.io/crates/crosstrait) supports building the 231 | type registry and enables casting from `dyn Any` returned by `TreeAny` to other desired trait objects. 232 | Together with [`erased-serde`](https://crates.io/crates/erased-serde) it can be used to implement node serialization/deserialization 233 | using `miniconf`'s `TreeAny` without using `TreeSerialize`/`TreeDeserialize` similar to `bevy_reflect`. 234 | 235 | Some tangential crates: 236 | 237 | * [`serde-reflection`](https://crates.io/crates/serde-reflection): extract schemata from serde impls. 238 | The `trace` example uses `serde-reflection` to extract a graph and schema for `Tree*`. 239 | * [`typetag`](https://crates.io/crates/typetag): "derive serde for trait objects" (local traits and impls) 240 | * [`deflect`](https://crates.io/crates/deflect): reflection on trait objects using adjacent DWARF debug info as the type registry 241 | * [`intertrait`](https://crates.io/crates/intertrait): inspiration and source of ideas for `crosstrait` 242 | 243 | ## Functional Programming, Polymorphism 244 | 245 | The type-heterogeneity of `miniconf` also borders on functional programming features. For that crates like the 246 | following may also be relevant: 247 | 248 | * [`frunk`](https://crates.io/crates/frunk) 249 | * [`lens-rs`](https://crates.io/crates/lens-rs)/[`rovv`](https://crates.io/crates/rovv) 250 | -------------------------------------------------------------------------------- /py/miniconf-mqtt/miniconf/async_.py: -------------------------------------------------------------------------------- 1 | """ 2 | Asynchronous Miniconf-over-MQTT utilities 3 | """ 4 | 5 | # pylint: disable=R0801,C0415,W1203,R0903,W0707 6 | 7 | import asyncio 8 | import json 9 | import uuid 10 | from typing import Dict, Any 11 | 12 | from paho.mqtt.properties import Properties, PacketTypes 13 | from aiomqtt import Client, Message, MqttError 14 | 15 | from .common import MiniconfException, LOGGER, json_dumps 16 | 17 | 18 | class Miniconf: 19 | """Miniconf over MQTT (asynchronous)""" 20 | 21 | def __init__(self, client: Client, prefix: str): 22 | """ 23 | Args: 24 | client: A connected MQTT5 client. 25 | prefix: The MQTT toptic prefix of the device to control. 26 | """ 27 | self.client = client 28 | self.prefix = prefix 29 | # A dispatcher is required since mqtt does not guarantee in-order processing 30 | # across topics (within a topic processing is mostly in-order). 31 | # Responses to requests on different topics may arrive out-of-order. 32 | self._inflight = {} 33 | self.response_topic = f"{prefix}/response" 34 | self.listener = asyncio.create_task(self._listen()) 35 | self.subscribed = asyncio.Event() 36 | 37 | async def close(self): 38 | """Cancel the response listener and all in-flight requests""" 39 | self.listener.cancel() 40 | for fut, _ret in self._inflight.values(): 41 | fut.cancel() 42 | try: 43 | await self.listener 44 | except asyncio.CancelledError: 45 | pass 46 | if len(self._inflight) > 0: 47 | await asyncio.wait(self._inflight.values()) 48 | 49 | async def _listen(self): 50 | await self.client.subscribe(self.response_topic) 51 | LOGGER.debug(f"Subscribed to {self.response_topic}") 52 | self.subscribed.set() 53 | try: 54 | async for message in self.client.messages: 55 | self._dispatch(message) 56 | except asyncio.CancelledError: 57 | pass 58 | except MqttError as e: 59 | LOGGER.debug(f"MQTT Error {e}", exc_info=True) 60 | finally: 61 | try: 62 | await self.client.unsubscribe(self.response_topic) 63 | self.subscribed.clear() 64 | LOGGER.debug(f"Unsubscribed from {self.response_topic}") 65 | except MqttError as e: 66 | LOGGER.debug(f"MQTT Error {e}", exc_info=True) 67 | 68 | def _dispatch(self, message: Message): 69 | if message.topic.value != self.response_topic: 70 | LOGGER.warning( 71 | "Discarding message with unexpected topic: %s", message.topic.value 72 | ) 73 | return 74 | 75 | try: 76 | properties = message.properties.json() 77 | except AttributeError: 78 | properties = {} 79 | # lazy formatting 80 | LOGGER.debug("Received %s: %s [%s]", message.topic, message.payload, properties) 81 | 82 | try: 83 | cd = bytes.fromhex(properties["CorrelationData"]) 84 | except KeyError: 85 | LOGGER.debug("Discarding message without CorrelationData") 86 | return 87 | try: 88 | fut, ret = self._inflight[cd] 89 | except KeyError: 90 | LOGGER.debug(f"Discarding message with unexpected CorrelationData: {cd}") 91 | return 92 | 93 | try: 94 | code = dict(properties["UserProperty"])["code"] 95 | except KeyError: 96 | LOGGER.warning("Discarding message without response code user property") 97 | return 98 | 99 | resp = message.payload.decode("utf-8") 100 | if code == "Continue": 101 | ret.append(resp) 102 | elif code == "Ok": 103 | if resp: 104 | ret.append(resp) 105 | fut.set_result(ret) 106 | del self._inflight[cd] 107 | else: 108 | fut.set_exception(MiniconfException(code, resp)) 109 | del self._inflight[cd] 110 | 111 | async def _do(self, path: str, *, response=1, **kwargs): 112 | response = int(response) 113 | props = Properties(PacketTypes.PUBLISH) 114 | if response: 115 | await self.subscribed.wait() 116 | props.ResponseTopic = self.response_topic 117 | cd = uuid.uuid1().bytes 118 | props.CorrelationData = cd 119 | fut = asyncio.get_event_loop().create_future() 120 | assert cd not in self._inflight 121 | self._inflight[cd] = fut, [] 122 | 123 | topic = f"{self.prefix}/settings{path}" 124 | LOGGER.debug("Publishing %s: %s, [%s]", topic, kwargs.get("payload"), props) 125 | await self.client.publish( 126 | topic, 127 | properties=props, 128 | **kwargs, 129 | ) 130 | if response: 131 | ret = await fut 132 | if response == 1: 133 | if len(ret) != 1: 134 | raise MiniconfException("Not a leaf", ret) 135 | return ret[0] 136 | assert ret 137 | return ret 138 | return None 139 | 140 | async def set(self, path: str, value: Any, retain=False, response=True, **kwargs): 141 | """Write the provided data to the specified path. 142 | 143 | Args: 144 | path: The path to set. 145 | value: The value to set. 146 | retain: Retain the the setting on the broker. 147 | response: Request and await the result of the operation. 148 | """ 149 | return await self._do( 150 | path, 151 | payload=json_dumps(value), 152 | response=response, 153 | retain=retain, 154 | **kwargs, 155 | ) 156 | 157 | async def list(self, path: str = "", **kwargs): 158 | """Get a list of all the paths below a given root. 159 | 160 | Args: 161 | path: Path to the root node to list. Can be a leaf or an internal node. 162 | """ 163 | return await self._do(path, response=2, **kwargs) 164 | 165 | async def dump(self, path: str = "", **kwargs): 166 | """Dump all the paths at or below a given root into the settings namespace. 167 | 168 | Note that the target may be unable to respond to messages when a multipart 169 | operation (list or dump) is in progress. 170 | This method does not wait for a response or completion or indicate an error. 171 | 172 | Args: 173 | path: Path to the root node to dump. Can be a leaf or an internal node. 174 | """ 175 | await self._do(path, response=0, **kwargs) 176 | 177 | async def get(self, path: str, **kwargs): 178 | """Get the specific value of a given path. 179 | 180 | Args: 181 | path: The path to get. Must be a leaf node. 182 | """ 183 | return json.loads(await self._do(path, **kwargs)) 184 | 185 | async def clear(self, path: str, response=True, **kwargs): 186 | """Clear retained value from a path. 187 | 188 | This does not change (`set()`) or reset/clear the value on the device. 189 | 190 | Args: 191 | path: The path to clear. Must be a leaf node. 192 | response: Obtain and await the result of the operation. 193 | """ 194 | return json.loads( 195 | await self._do( 196 | path, 197 | retain=True, 198 | response=response, 199 | **kwargs, 200 | ) 201 | ) 202 | 203 | 204 | async def discover( 205 | client: Client, 206 | prefix: str, 207 | rel_timeout: float = 3.0, 208 | abs_timeout: float = 0.1, 209 | ) -> Dict[str, Any]: 210 | """Get a list of available Miniconf devices. 211 | 212 | Args: 213 | * `client` - The MQTT client to search for clients on. Connected to a broker 214 | * `prefix` - An MQTT-specific topic filter for device prefixes. Note that this will 215 | be appended to with the default status topic name `/alive`. 216 | * `rel_timeout` - The duration to search for clients in units of the time it takes 217 | to ack the subscribe to the alive topic. 218 | * `abs_timeout` - Additional absolute duration to wait for client discovery 219 | in seconds. 220 | 221 | Returns: 222 | A dictionary of discovered client prefixes and metadata payload. 223 | """ 224 | discovered = {} 225 | suffix = "/alive" 226 | topic = f"{prefix}{suffix}" 227 | 228 | async def listen(): 229 | async for message in client.messages: 230 | peer = message.topic.value.removesuffix(suffix) 231 | try: 232 | payload = json.loads(message.payload) 233 | except json.JSONDecodeError: 234 | LOGGER.info(f"Ignoring {peer} not/invalid alive") 235 | else: 236 | LOGGER.debug(f"Discovered {peer} alive") 237 | discovered[peer] = payload 238 | 239 | t_start = asyncio.get_running_loop().time() 240 | await client.subscribe(topic) 241 | t_subscribe = asyncio.get_running_loop().time() - t_start 242 | 243 | try: 244 | await asyncio.wait_for( 245 | listen(), timeout=rel_timeout * t_subscribe + abs_timeout 246 | ) 247 | except asyncio.TimeoutError: 248 | pass 249 | finally: 250 | await client.unsubscribe(topic) 251 | return discovered 252 | 253 | 254 | async def _main(): 255 | import sys 256 | import os 257 | import logging 258 | from .common import _cli, MQTTv5, one 259 | 260 | if sys.platform.lower() == "win32" or os.name.lower() == "nt": 261 | from asyncio import set_event_loop_policy, WindowsSelectorEventLoopPolicy 262 | 263 | set_event_loop_policy(WindowsSelectorEventLoopPolicy()) 264 | 265 | args = _cli().parse_args() 266 | 267 | logging.basicConfig( 268 | format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", 269 | level=logging.WARN - 10 * args.verbose, 270 | ) 271 | 272 | async with Client(args.broker, protocol=MQTTv5) as client: 273 | if args.discover: 274 | prefix, _alive = one(await discover(client, args.prefix)) 275 | else: 276 | prefix = args.prefix 277 | 278 | interface = Miniconf(client, prefix) 279 | 280 | try: 281 | await _handle_commands(interface, args.commands, args.retain) 282 | finally: 283 | await interface.close() 284 | 285 | 286 | async def _handle_commands(interface, commands, retain): 287 | import sys 288 | from .common import _Path 289 | 290 | current = _Path() 291 | for arg in commands: 292 | try: 293 | if arg.endswith("?"): 294 | path = current.normalize(arg.removesuffix("?")) 295 | paths = await interface.list(path) 296 | # Note: There is no way for the CLI tool to reliably 297 | # distinguish a one-element leaf get responce from a 298 | # one-element inner list response without looking at 299 | # the payload. 300 | # The only way is to note that a JSON payload of a 301 | # get can not start with the / that a list response 302 | # starts with. 303 | if len(paths) == 1 and not paths[0].startswith("/"): 304 | value = paths[0] 305 | print(f"{path}={value}") 306 | continue 307 | for p in paths: 308 | try: 309 | value = json_dumps(await interface.get(p)) 310 | print(f"{p}={value}") 311 | except MiniconfException as err: 312 | print(f"{p}: {err!r}") 313 | elif arg.endswith("!"): 314 | path = current.normalize(arg.removesuffix("!")) 315 | await interface.dump(path) 316 | print(f"DUMP {path}") 317 | elif "=" in arg: 318 | path, value = arg.split("=", 1) 319 | path = current.normalize(path) 320 | if not value: 321 | value = json_dumps(await interface.clear(path)) 322 | print(f"CLEAR {path}={value}") 323 | else: 324 | await interface.set(path, json.loads(value), retain) 325 | print(f"{path}={value}") 326 | else: 327 | path = current.normalize(arg) 328 | value = json_dumps(await interface.get(path)) 329 | print(f"{path}={value}") 330 | except MiniconfException as err: 331 | print(f"{arg}: {err!r}") 332 | sys.exit(1) 333 | 334 | 335 | if __name__ == "__main__": 336 | asyncio.run(_main()) 337 | -------------------------------------------------------------------------------- /miniconf/src/tree.rs: -------------------------------------------------------------------------------- 1 | use core::any::Any; 2 | 3 | use serde::{Deserializer, Serializer}; 4 | 5 | use crate::{IntoKeys, Keys, Schema, SerdeError, ValueError}; 6 | 7 | /// Traversal, iteration of keys in a tree. 8 | /// 9 | /// See also the sub-traits [`TreeSerialize`], [`TreeDeserialize`], [`TreeAny`]. 10 | /// 11 | /// # Keys 12 | /// 13 | /// There is a one-to-one relationship between nodes and keys. 14 | /// The keys used to identify nodes support [`Keys`]/[`IntoKeys`]. They can be 15 | /// obtained from other [`IntoKeys`] through [`Schema::transcode()`]. 16 | /// An iterator of keys for the nodes is available through [`Schema::nodes()`]. 17 | /// 18 | /// * `usize` is modelled after ASN.1 Object Identifiers, see [`crate::Indices`]. 19 | /// * `&str` keys are sequences of names, like path names. When concatenated, they are separated 20 | /// by some path hierarchy separator, e.g. `'/'`, see [`crate::Path`], or by some more 21 | /// complex notation, see [`crate::JsonPath`]. 22 | /// * [`crate::Packed`] is a bit-packed compact compressed notation of 23 | /// hierarchical compound indices. 24 | /// * See the `scpi` example for how to implement case-insensitive, relative, and abbreviated/partial 25 | /// matches. 26 | /// 27 | /// # Derive macros 28 | /// 29 | /// Derive macros to automatically implement the correct traits on a struct or enum are available through 30 | /// [`macro@crate::TreeSchema`], [`macro@crate::TreeSerialize`], [`macro@crate::TreeDeserialize`], 31 | /// and [`macro@crate::TreeAny`]. 32 | /// A shorthand derive macro that derives all four trait implementations is also available at 33 | /// [`macro@crate::Tree`]. 34 | /// 35 | /// The derive macros support per-field/per-variant attributes to control the derived trait implementations. 36 | /// 37 | /// ## Rename 38 | /// 39 | /// The key for named struct fields or enum variants may be changed from the default field ident using 40 | /// the `rename` derive macro attribute. 41 | /// 42 | /// ``` 43 | /// use miniconf::{Path, Tree, TreeSchema}; 44 | /// #[derive(Tree, Default)] 45 | /// struct S { 46 | /// #[tree(rename = "OTHER")] 47 | /// a: f32, 48 | /// }; 49 | /// let name = S::SCHEMA.transcode::>([0usize]).unwrap(); 50 | /// assert_eq!(name.0.as_str(), "/OTHER"); 51 | /// ``` 52 | /// 53 | /// ## Skip 54 | /// 55 | /// Named fields/variants may be omitted from the derived `Tree` trait implementations using the 56 | /// `skip` attribute. 57 | /// Note that for tuple structs skipping is only supported for terminal fields: 58 | /// 59 | /// ``` 60 | /// use miniconf::{Tree}; 61 | /// #[derive(Tree)] 62 | /// struct S(i32, #[tree(skip)] ()); 63 | /// ``` 64 | /// 65 | /// ```compile_fail 66 | /// use miniconf::{Tree}; 67 | /// #[derive(Tree)] 68 | /// struct S(#[tree(skip)] (), i32); 69 | /// ``` 70 | /// 71 | /// ## Type 72 | /// 73 | /// The type to use when accessing the field/variant through `TreeDeserialize::probe` 74 | /// can be overridden using the `typ` derive macro attribute (`#[tree(typ="[f32; 4]")]`). 75 | /// 76 | /// ## Implementation overrides 77 | /// 78 | /// `#[tree(with=path)]` 79 | /// 80 | /// This overrides the calls to the child node/variant traits using pub functions 81 | /// and constants in the module at the given path: 82 | /// (`SCHEMA`, `serialize_by_key`, `deserialize_by_key`, `probe_by_key`, 83 | /// `ref_any_by_key`, `mut_any_by_key`). 84 | /// 85 | /// Also use this to relax bounds and deny operations. 86 | /// ``` 87 | /// # use miniconf::{SerdeError, Tree, Keys, ValueError, TreeDeserialize}; 88 | /// # use serde::Deserializer; 89 | /// #[derive(Tree, Default)] 90 | /// struct S { 91 | /// #[tree(with=check)] 92 | /// b: f32, 93 | /// } 94 | /// mod check { 95 | /// use miniconf::{SerdeError, Deserializer, TreeDeserialize, ValueError, Keys}; 96 | /// pub use miniconf::leaf::{SCHEMA, serialize_by_key, probe_by_key, ref_any_by_key, mut_any_by_key}; 97 | /// 98 | /// pub fn deserialize_by_key<'de, D: Deserializer<'de>>( 99 | /// value: &mut f32, 100 | /// keys: impl Keys, 101 | /// de: D 102 | /// ) -> Result<(), SerdeError> { 103 | /// let mut new = *value; 104 | /// new.deserialize_by_key(keys, de)?; 105 | /// if new < 0.0 { 106 | /// Err(ValueError::Access("fail").into()) 107 | /// } else { 108 | /// *value = new; 109 | /// Ok(()) 110 | /// } 111 | /// } 112 | /// } 113 | /// ``` 114 | /// 115 | /// ### `defer` 116 | /// 117 | /// The `defer` attribute is a shorthand for `with()` that defers 118 | /// child trait implementations to a given expression. 119 | /// 120 | /// # Array 121 | /// 122 | /// Blanket implementations of the `Tree*` traits are provided for homogeneous arrays 123 | /// [`[T; N]`](core::array). 124 | /// 125 | /// # Option 126 | /// 127 | /// Blanket implementations of the `Tree*` traits are provided for [`Option`]. 128 | /// 129 | /// These implementations do not alter the path hierarchy and do not consume any items from the `keys` 130 | /// iterators. The `TreeSchema` behavior of an [`Option`] is such that the `None` variant makes the 131 | /// corresponding part of the tree inaccessible at run-time. It will still be iterated over (e.g. 132 | /// by [`Schema::nodes()`]) but attempts to access it (e.g. [`TreeSerialize::serialize_by_key()`], 133 | /// [`TreeDeserialize::deserialize_by_key()`], [`TreeAny::ref_any_by_key()`], or 134 | /// [`TreeAny::mut_any_by_key()`]) return the special [`ValueError::Absent`]. 135 | /// 136 | /// This is the same behavior as for other `enums` that have the `Tree*` traits derived. 137 | /// 138 | /// # Tuples 139 | /// 140 | /// Blanket impementations for the `Tree*` traits are provided for heterogeneous tuples `(T0, T1, ...)` 141 | /// up to length eight. 142 | /// 143 | /// # Examples 144 | /// 145 | /// See the [`crate`] documentation for a longer example showing how the traits and the derive 146 | /// macros work. 147 | pub trait TreeSchema { 148 | /// Schema for this tree level 149 | // Reference for Option to copy T::SCHEMA 150 | const SCHEMA: &Schema; 151 | } 152 | 153 | /// Access any node by keys. 154 | /// 155 | /// This uses the `dyn Any` trait object. 156 | /// 157 | /// ``` 158 | /// use core::any::Any; 159 | /// use miniconf::{Indices, IntoKeys, JsonPath, TreeAny, TreeSchema}; 160 | /// #[derive(TreeSchema, TreeAny, Default)] 161 | /// struct S { 162 | /// foo: u32, 163 | /// bar: [u16; 2], 164 | /// }; 165 | /// let mut s = S::default(); 166 | /// 167 | /// for key in S::SCHEMA.nodes::, 2>() { 168 | /// let a = s.ref_any_by_key(key.unwrap().into_keys()).unwrap(); 169 | /// assert!([0u32.type_id(), 0u16.type_id()].contains(&(&*a).type_id())); 170 | /// } 171 | /// 172 | /// let val: &mut u16 = s.mut_by_key(&JsonPath(".bar[1]")).unwrap(); 173 | /// *val = 3; 174 | /// assert_eq!(s.bar[1], 3); 175 | /// 176 | /// let val: &u16 = s.ref_by_key(&JsonPath(".bar[1]")).unwrap(); 177 | /// assert_eq!(*val, 3); 178 | /// ``` 179 | pub trait TreeAny: TreeSchema { 180 | /// Obtain a reference to a `dyn Any` trait object for a leaf node. 181 | fn ref_any_by_key(&self, keys: impl Keys) -> Result<&dyn Any, ValueError>; 182 | 183 | /// Obtain a mutable reference to a `dyn Any` trait object for a leaf node. 184 | fn mut_any_by_key(&mut self, keys: impl Keys) -> Result<&mut dyn Any, ValueError>; 185 | 186 | /// Obtain a reference to a leaf of known type by key. 187 | #[inline] 188 | fn ref_by_key(&self, keys: impl IntoKeys) -> Result<&T, ValueError> { 189 | self.ref_any_by_key(keys.into_keys())? 190 | .downcast_ref() 191 | .ok_or(ValueError::Access("Incorrect type")) 192 | } 193 | 194 | /// Obtain a mutable reference to a leaf of known type by key. 195 | #[inline] 196 | fn mut_by_key(&mut self, keys: impl IntoKeys) -> Result<&mut T, ValueError> { 197 | self.mut_any_by_key(keys.into_keys())? 198 | .downcast_mut() 199 | .ok_or(ValueError::Access("Incorrect type")) 200 | } 201 | } 202 | 203 | /// Serialize a leaf node by its keys. 204 | /// 205 | /// See also [`crate::json_core`] or `crate::postcard` for convenient wrappers using this trait. 206 | /// 207 | /// # Derive macro 208 | /// 209 | /// See [`macro@crate::TreeSerialize`]. 210 | /// The derive macro attributes are described in the [`TreeSchema`] trait. 211 | pub trait TreeSerialize: TreeSchema { 212 | /// Serialize a node by keys. 213 | /// 214 | /// ``` 215 | /// # #[cfg(feature = "json-core")] { 216 | /// use miniconf::{IntoKeys, TreeSchema, TreeSerialize}; 217 | /// #[derive(TreeSchema, TreeSerialize)] 218 | /// struct S { 219 | /// foo: u32, 220 | /// bar: [u16; 2], 221 | /// }; 222 | /// let s = S { 223 | /// foo: 9, 224 | /// bar: [11, 3], 225 | /// }; 226 | /// let mut buf = [0u8; 10]; 227 | /// let mut ser = serde_json_core::ser::Serializer::new(&mut buf); 228 | /// s.serialize_by_key(["bar", "0"].into_keys(), &mut ser).unwrap(); 229 | /// let len = ser.end(); 230 | /// assert_eq!(&buf[..len], b"11"); 231 | /// # } 232 | /// ``` 233 | /// 234 | /// # Args 235 | /// * `keys`: A `Keys` identifying the node. 236 | /// * `ser`: A `Serializer` to to serialize the value. 237 | fn serialize_by_key( 238 | &self, 239 | keys: impl Keys, 240 | ser: S, 241 | ) -> Result>; 242 | } 243 | 244 | /// Deserialize a leaf node by its keys. 245 | /// 246 | /// See also [`crate::json_core`] or `crate::postcard` for convenient wrappers using this trait. 247 | /// 248 | /// # Derive macro 249 | /// 250 | /// See [`macro@crate::TreeDeserialize`]. 251 | /// The derive macro attributes are described in the [`TreeSchema`] trait. 252 | pub trait TreeDeserialize<'de>: TreeSchema { 253 | /// Deserialize a leaf node by its keys. 254 | /// 255 | /// ``` 256 | /// # #[cfg(feature = "derive")] { 257 | /// use miniconf::{IntoKeys, TreeDeserialize, TreeSchema}; 258 | /// #[derive(Default, TreeSchema, TreeDeserialize)] 259 | /// struct S { 260 | /// foo: u32, 261 | /// bar: [u16; 2], 262 | /// }; 263 | /// let mut s = S::default(); 264 | /// let mut de = serde_json::de::Deserializer::from_slice(b"7"); 265 | /// s.deserialize_by_key(["bar", "0"].into_keys(), &mut de).unwrap(); 266 | /// de.end().unwrap(); 267 | /// assert_eq!(s.bar[0], 7); 268 | /// # } 269 | /// ``` 270 | /// 271 | /// # Args 272 | /// * `keys`: A `Keys` identifying the node. 273 | /// * `de`: A `Deserializer` to deserialize the value. 274 | fn deserialize_by_key>( 275 | &mut self, 276 | keys: impl Keys, 277 | de: D, 278 | ) -> Result<(), SerdeError>; 279 | 280 | /// Blind deserialize a leaf node by its keys. 281 | /// 282 | /// This method should succeed at least in those cases where 283 | /// `deserialize_by_key()` succeeds. 284 | /// 285 | /// ``` 286 | /// # #[cfg(feature = "derive")] { 287 | /// use miniconf::{IntoKeys, TreeDeserialize, TreeSchema}; 288 | /// #[derive(Default, TreeSchema, TreeDeserialize)] 289 | /// struct S { 290 | /// foo: u32, 291 | /// bar: [u16; 2], 292 | /// }; 293 | /// let mut de = serde_json::de::Deserializer::from_slice(b"7"); 294 | /// S::probe_by_key(["bar", "0"].into_keys(), &mut de) 295 | /// .unwrap(); 296 | /// de.end().unwrap(); 297 | /// # } 298 | /// ``` 299 | /// 300 | /// # Args 301 | /// * `keys`: A `Keys` identifying the node. 302 | /// * `de`: A `Deserializer` to deserialize the value. 303 | fn probe_by_key>( 304 | keys: impl Keys, 305 | de: D, 306 | ) -> Result<(), SerdeError>; 307 | } 308 | 309 | /// Shorthand for owned deserialization through [`TreeDeserialize`]. 310 | pub trait TreeDeserializeOwned: for<'de> TreeDeserialize<'de> {} 311 | impl TreeDeserializeOwned for T where T: for<'de> TreeDeserialize<'de> {} 312 | 313 | // Blanket impls for refs and muts 314 | 315 | impl TreeSchema for &T { 316 | const SCHEMA: &'static Schema = T::SCHEMA; 317 | } 318 | 319 | impl TreeSchema for &mut T { 320 | const SCHEMA: &'static Schema = T::SCHEMA; 321 | } 322 | 323 | impl TreeSerialize for &T { 324 | #[inline] 325 | fn serialize_by_key( 326 | &self, 327 | keys: impl Keys, 328 | ser: S, 329 | ) -> Result> { 330 | (**self).serialize_by_key(keys, ser) 331 | } 332 | } 333 | 334 | impl TreeSerialize for &mut T { 335 | #[inline] 336 | fn serialize_by_key( 337 | &self, 338 | keys: impl Keys, 339 | ser: S, 340 | ) -> Result> { 341 | (**self).serialize_by_key(keys, ser) 342 | } 343 | } 344 | 345 | impl<'de, T: TreeDeserialize<'de> + ?Sized> TreeDeserialize<'de> for &mut T { 346 | #[inline] 347 | fn deserialize_by_key>( 348 | &mut self, 349 | keys: impl Keys, 350 | de: D, 351 | ) -> Result<(), SerdeError> { 352 | (**self).deserialize_by_key(keys, de) 353 | } 354 | 355 | #[inline] 356 | fn probe_by_key>( 357 | keys: impl Keys, 358 | de: D, 359 | ) -> Result<(), SerdeError> { 360 | T::probe_by_key(keys, de) 361 | } 362 | } 363 | 364 | impl TreeAny for &mut T { 365 | #[inline] 366 | fn ref_any_by_key(&self, keys: impl Keys) -> Result<&dyn Any, ValueError> { 367 | (**self).ref_any_by_key(keys) 368 | } 369 | 370 | #[inline] 371 | fn mut_any_by_key(&mut self, keys: impl Keys) -> Result<&mut dyn Any, ValueError> { 372 | (**self).mut_any_by_key(keys) 373 | } 374 | } 375 | --------------------------------------------------------------------------------