Skip to content

Commit

Permalink
Merge pull request #240 from quartiq/flatten
Browse files Browse the repository at this point in the history
flatten
  • Loading branch information
jordens authored Sep 30, 2024
2 parents aabfd0d + 7353e43 commit 7093d18
Show file tree
Hide file tree
Showing 24 changed files with 539 additions and 242 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased](https://github.com/quartiq/miniconf/compare/v0.13.0...HEAD) - DATE

### Added

* `flatten` support for structs/enums with a single non-skip/non-unit variant/field.
* `core::error::Error` implementations added to `Error` and `Traversal`

## [0.14.0](https://github.com/quartiq/miniconf/compare/v0.13.0...v0.14.0) - 2024-09-26

### Added
Expand Down
5 changes: 5 additions & 0 deletions miniconf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ heapless = "0.8.0"
yafnv = "3.0.0"
tokio = { version = "1.38.0", features = ["io-std", "rt", "macros"] }
strum = { version = "0.26.3", features = ["derive"] }
trybuild = { version = "1.0.99", features = ["diff"] }

[[test]]
name = "arrays"
Expand Down Expand Up @@ -81,6 +82,10 @@ required-features = ["json-core", "derive"]
name = "validate"
required-features = ["json-core", "derive"]

[[test]]
name = "flatten"
required-features = ["json-core", "derive"]

[[example]]
name = "common"
crate-type = ["lib"]
Expand Down
22 changes: 10 additions & 12 deletions miniconf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ One possible use of `miniconf` is a backend for run-time settings management in
It was originally designed to work with JSON ([`serde_json_core`](https://docs.rs/serde-json-core))
payloads over MQTT ([`minimq`](https://docs.rs/minimq)) and provides a MQTT settings management
client in the `miniconf_mqtt` crate and a Python reference implementation to interact with it.
Miniconf is agnostic of the `serde` backend/format, hierarchy separator, and transport/protocol.
Miniconf is agnostic of the `serde` backend/format, key type/format, and transport/protocol.

## Formats

Expand Down Expand Up @@ -162,11 +162,11 @@ python -m miniconf -d quartiq/application/+ /foo=true

## Derive macros

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

Structs, enums, arrays, and Options can then be cascaded to construct more complex trees.
When using the derive macro, the behavior and tree recursion depth can be configured for each
Expand All @@ -180,18 +180,16 @@ Lookup into the tree is done using a [`Keys`] implementation. A blanket implemen
is provided for `IntoIterator`s over [`Key`] items. The [`Key`] lookup capability is implemented
for `usize` indices and `&str` names.

Path iteration is supported with arbitrary separator between names.
Path iteration is supported with arbitrary separator `char`s between names.

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

## Limitations

The derive macros don't support enums with record (named fields) variants or tuple (non-newtype) variants.
These are still however usable in their atomic `serde` form as leaf nodes.

The derive macros don't handle `std`/`alloc` smart pointers ( `Box`, `Rc`, `Arc`) in any special way.
They however still be handled with accessors (`get`, `get_mut`, `validate`).
* `enum`: The derive macros don't support enums with record (named fields) variants or tuple variants with more than one field. Only unit, newtype and skipped variants are supported. Without the derive macros, these `enums` are still however usable in their atomic `serde` form as leaf nodes.
* The derive macros don't handle `std`/`alloc` smart pointers ( `Box`, `Rc`, `Arc`) in any special way. They however still be handled with accessors (`get`, `get_mut`, `validate`).
* The derive macros only support flattening in non-ambiguous situations (single field structs and single variant enums, both modulo skipped fields/variants and unit variants).

## Features

Expand Down
26 changes: 19 additions & 7 deletions miniconf/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use core::fmt::{Display, Formatter};
use core::fmt::{Debug, Display, Formatter};

/// Errors that can occur when using the Tree traits.
///
Expand All @@ -13,10 +13,11 @@ use core::fmt::{Display, Formatter};
#[non_exhaustive]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Traversal {
/// The key is valid, but does not exist at runtime.
/// The does not exist at runtime.
///
/// This is the case if an [`Option`] using the `Tree*` traits
/// is `None` at runtime. See also [`crate::TreeKey#option`].
/// The `enum` variant is currently absent.
/// This is for example the case if an [`Option`] using the `Tree*`
/// traits is `None` at runtime. See also [`crate::TreeKey#option`].
Absent(usize),

/// The key ends early and does not reach a leaf node.
Expand Down Expand Up @@ -44,7 +45,7 @@ impl Display for Traversal {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
Traversal::Absent(depth) => {
write!(f, "Key not currently present (depth: {depth})")
write!(f, "Variant absent (depth: {depth})")
}
Traversal::TooShort(depth) => {
write!(f, "Key too short (depth: {depth})")
Expand All @@ -65,6 +66,8 @@ impl Display for Traversal {
}
}

impl ::core::error::Error for Traversal {}

impl Traversal {
/// Pass it up one hierarchy depth level, incrementing its usize depth field by one.
#[inline]
Expand Down Expand Up @@ -116,10 +119,10 @@ pub enum Error<E> {
Finalization(E),
}

impl<E: core::fmt::Display> Display for Error<E> {
impl<E: Display> Display for Error<E> {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
Self::Traversal(t) => t.fmt(f),
Self::Traversal(t) => Display::fmt(t, f),
Self::Inner(depth, error) => {
write!(f, "(De)serialization error (depth: {depth}): {error}")
}
Expand All @@ -130,6 +133,15 @@ impl<E: core::fmt::Display> Display for Error<E> {
}
}

impl<E: core::error::Error + 'static> core::error::Error for Error<E> {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
Some(match self {
Self::Traversal(t) => t,
Self::Inner(_, e) | Self::Finalization(e) => e,
})
}
}

// Try to extract the Traversal from an Error
impl<E> TryFrom<Error<E>> for Traversal {
type Error = Error<E>;
Expand Down
6 changes: 3 additions & 3 deletions miniconf/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,20 @@ pub trait KeyLookup {
fn name_to_index(value: &str) -> Option<usize>;
}

/// Convert a `&str` key into a node index on a `TreeKey`
/// Convert a `&str` key into a node index on a `KeyLookup`
pub trait Key {
/// Convert the key `self` to a `usize` index
fn find<M: KeyLookup + ?Sized>(&self) -> Option<usize>;
}

// `usize` index as Key
// index
impl Key for usize {
fn find<M: KeyLookup + ?Sized>(&self) -> Option<usize> {
Some(*self)
}
}

// &str name as Key
// name
impl Key for &str {
fn find<M: KeyLookup + ?Sized>(&self) -> Option<usize> {
M::name_to_index(self)
Expand Down
2 changes: 1 addition & 1 deletion miniconf/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ impl From<Node> for usize {
}
}

/// Map a `TreeKey::traverse_by_key()` `Result` to a `NodeLookup::lookup()` `Result`.
/// Map a `TreeKey::traverse_by_key()` `Result` to a `Transcode::transcode()` `Result`.
impl TryFrom<Result<usize, Error<()>>> for Node {
type Error = Traversal;
fn try_from(value: Result<usize, Error<()>>) -> Result<Self, Traversal> {
Expand Down
16 changes: 6 additions & 10 deletions miniconf/src/packed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,14 +208,12 @@ impl Packed {
pub fn pop_msb(&mut self, bits: u32) -> Option<usize> {
let s = self.get();
// Remove value from self
if let Some(new) = Self::new(s << bits) {
Self::new(s << bits).map(|new| {
*self = new;
// Extract value from old self
// Done in two steps as bits + 1 can be Self::BITS which would wrap.
Some((s >> (Self::CAPACITY - bits)) >> 1)
} else {
None
}
(s >> (Self::CAPACITY - bits)) >> 1
})
}

/// Push the given number `bits` of `value` as new LSBs.
Expand All @@ -229,17 +227,15 @@ impl Packed {
debug_assert_eq!(value >> bits, 0);
let mut n = self.trailing_zeros();
let old_marker = 1 << n;
if let Some(new_marker) = Self::new(old_marker >> bits) {
Self::new(old_marker >> bits).map(|new_marker| {
n -= bits;
// * Remove old marker
// * Add value at offset n + 1
// Done in two steps as n + 1 can be Self::BITS, which would wrap.
// * Add new marker
self.0 = (self.get() ^ old_marker) | ((value << n) << 1) | new_marker.0;
Some(n)
} else {
None
}
n
})
}
}

Expand Down
3 changes: 0 additions & 3 deletions miniconf/src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,6 @@ impl Metadata {
/// to access it (e.g. [`TreeSerialize::serialize_by_key()`], [`TreeDeserialize::deserialize_by_key()`],
/// [`TreeAny::ref_any_by_key()`], or [`TreeAny::mut_any_by_key()`])
/// return the special [`Traversal::Absent`].
/// This is intended as a mechanism to provide run-time construction of the namespace. In some
/// cases, run-time detection may indicate that some component is not present. In this case,
/// the nodes will not be exposed for serialization/deserialization.
///
/// If the depth specified by the `#[tree(depth=Y)]` attribute exceeds 1,
/// the `Option` can be used to access the inner type using its `TreeKey<{Y - 1}>` trait.
Expand Down
26 changes: 26 additions & 0 deletions miniconf/tests/common/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use miniconf::{JsonCoreSlashOwned, Path, TreeKey};

pub fn paths<M, const Y: usize>() -> Vec<String>
where
M: TreeKey<Y>,
{
M::nodes::<Path<String, '/'>>()
.exact_size()
.map(|pn| {
let (p, n) = pn.unwrap();
assert!(n.is_leaf());
assert_eq!(p.chars().filter(|c| *c == p.separator()).count(), n.depth());
p.into_inner()
})
.collect()
}

pub fn set_get<M, const Y: usize>(s: &mut M, path: &str, value: &[u8])
where
M: JsonCoreSlashOwned<Y>,
{
s.set_json(path, value).unwrap();
let mut buf = vec![0; value.len()];
let len = s.get_json(path, &mut buf[..]).unwrap();
assert_eq!(&buf[..len], value);
}
5 changes: 5 additions & 0 deletions miniconf/tests/compiletest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/*.rs");
}
55 changes: 39 additions & 16 deletions miniconf/tests/enum.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use miniconf::{JsonCoreSlash, Path, Tree, TreeDeserialize, TreeKey, TreeSerialize};
use miniconf::{JsonCoreSlash, Tree, TreeDeserialize, TreeKey, TreeSerialize};
use strum::{AsRefStr, EnumString};

mod common;
use common::*;

#[derive(Tree, Default, PartialEq, Debug)]
struct Inner {
a: i32,
Expand Down Expand Up @@ -39,31 +42,51 @@ impl Settings {
fn enum_switch() {
let mut s = Settings::default();
assert_eq!(s.en, Enum::None);
s.set_json("/tag", b"\"foo\"").unwrap();
set_get(&mut s, "/tag", b"\"foo\"");
assert_eq!(
s.set_json("/tag", b"\"bar\""),
Err(miniconf::Traversal::Invalid(1, "invalid tag").into())
);
assert_eq!(s.en, Enum::A(0));
s.set_json("/en/foo", b"99").unwrap();
set_get(&mut s, "/en/foo", b"99");
assert_eq!(s.en, Enum::A(99));
assert_eq!(
s.set_json("/en/B/a", b"99"),
Err(miniconf::Traversal::Absent(2).into())
);
s.set_json("/tag", b"\"B\"").unwrap();
s.set_json("/en/B/a", b"8").unwrap();
set_get(&mut s, "/tag", b"\"B\"");
set_get(&mut s, "/en/B/a", b"8");
assert_eq!(s.en, Enum::B(Inner { a: 8 }));

assert_eq!(
Settings::nodes::<Path<String, '/'>>()
.exact_size()
.map(|pn| {
let (p, n) = pn.unwrap();
assert!(n.is_leaf());
p.into_inner()
})
.collect::<Vec<_>>(),
vec!["/tag", "/en/foo", "/en/B/a"]
);
assert_eq!(paths::<Settings, 3>(), ["/tag", "/en/foo", "/en/B/a"]);
}

#[test]
fn enum_skip() {
struct S;

#[allow(dead_code)]
#[derive(Tree)]
enum E {
A(i32, #[tree(skip)] i32),
#[tree(skip)]
B(S),
}
assert_eq!(paths::<E, 1>(), ["/A"]);
}

#[test]
fn option() {
// Also tests macro hygiene a bit
#[allow(dead_code)]
#[derive(Tree, Copy, Clone, PartialEq, Default, Debug)]
#[tree(flatten)]
enum Option<T> {
#[default]
None,
// #192
Some(#[tree(depth = 1)] T),
}
assert_eq!(paths::<Option<[i32; 1]>, 1>(), ["/0"]);
assert_eq!(paths::<Option<::core::option::Option<i32>>, 1>(), [""]);
}
Loading

0 comments on commit 7093d18

Please sign in to comment.