Skip to content

Commit

Permalink
Merge pull request #233 from quartiq/enum
Browse files Browse the repository at this point in the history
derive support for enums with newtype/unit variants
  • Loading branch information
jordens authored Sep 25, 2024
2 parents dfcd8c6 + da09a86 commit 6a93011
Show file tree
Hide file tree
Showing 18 changed files with 841 additions and 635 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

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

### Added
* Derive support for enums with newtype/unit/skipped variants

### Removed

* The `KeyLookup::NAMES` associated constant is now an implementation
detail


## [0.13.0](https://github.com/quartiq/miniconf/compare/v0.12.0...v0.13.0) - 2024-07-10

### Changed
Expand Down
7 changes: 5 additions & 2 deletions miniconf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ description = "Serialize/deserialize/access reflection for trees"
repository = "https://github.com/quartiq/miniconf"
keywords = ["config", "serde", "no_std", "reflection", "graph"]
categories = ["embedded", "config", "data-structures", "parsing"]
resolver = "2"

[dependencies]
serde = { version = "1.0.120", default-features = false }
Expand All @@ -23,7 +22,6 @@ default = ["derive"]
json-core = ["dep:serde-json-core"]
postcard = ["dep:postcard"]
derive = ["dep:miniconf_derive", "serde/derive"]
std = []

[package.metadata.docs.rs]
all-features = true
Expand All @@ -37,6 +35,7 @@ embedded-io-adapters = { version = "0.6.1", features = ["tokio-1"] }
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"] }

[[test]]
name = "arrays"
Expand Down Expand Up @@ -74,6 +73,10 @@ required-features = ["derive"]
name = "structs"
required-features = ["json-core", "derive"]

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

[[test]]
name = "validate"
required-features = ["json-core", "derive"]
Expand Down
11 changes: 6 additions & 5 deletions miniconf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ let len = settings.get_json("/struct_", &mut buf).unwrap();
assert_eq!(&buf[..len], br#"{"a":3,"b":3}"#);

// Iterating over all paths
for path in Settings::nodes::<Path<String, '/'>>() {
for path in Settings::nodes::<Path<heapless::String<32>, '/'>>() {
let (path, node) = path.unwrap();
assert!(node.is_leaf());
// Serialize each
Expand Down Expand Up @@ -168,7 +168,7 @@ Fields/items that form internal nodes (non-leaf) need to implement the respectiv
Leaf fields/items need to support the respective [`serde`] trait (and the desired `serde::Serializer`/`serde::Deserializer`
backend).

Structs, arrays, and Options can then be cascaded to construct more complex trees.
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
struct field using the `#[tree(depth(Y))]` attribute.

Expand All @@ -187,10 +187,11 @@ It implements [`Keys`].

## Limitations

Access to inner fields of some types is not yet supported, e.g. enums
other than [`Option`]. These are still however usable in their atomic `serde` form as leaf nodes.
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.

Many `std` smart pointers are not supported or handled in any special way: `Box`, `Rc`, `Arc`.
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`).

## Features

Expand Down
5 changes: 2 additions & 3 deletions miniconf/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#![cfg_attr(not(any(feature = "std", test, doctest)), no_std)]
#![cfg_attr(feature = "json-core", doc = include_str!("../README.md"))]
#![cfg_attr(not(feature = "json-core"), doc = "miniconf")]
#![no_std]
#![doc = include_str!("../README.md")]
#![deny(rust_2018_compatibility)]
#![deny(rust_2018_idioms)]
#![warn(missing_docs)]
Expand Down
16 changes: 9 additions & 7 deletions miniconf/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,12 @@ impl<T: Write + ?Sized, const S: char> Transcode for Path<T, S> {
{
M::traverse_by_key(keys.into_keys(), |index, name, _len| {
self.0
.write_char(self.separator())
.and_then(|()| match name {
Some(name) => self.0.write_str(name),
None => self.0.write_str(itoa::Buffer::new().format(index)),
.write_char(S)
.and_then(|()| {
let mut buf = itoa::Buffer::new();
let name = name.unwrap_or_else(|| buf.format(index));
debug_assert!(!name.contains(S));
self.0.write_str(name)
})
.or(Err(()))
})
Expand Down Expand Up @@ -311,10 +313,10 @@ mod test {

#[test]
fn strsplit() {
use heapless::Vec;
for p in ["/d/1", "/a/bccc//d/e/", "", "/", "a/b", "a"] {
let a: Vec<_> = PathIter::<'_, '/'>::new(p).collect();
println!("{p} {:?}", a);
let b: Vec<_> = p.split('/').skip(1).collect();
let a: Vec<_, 10> = PathIter::<'_, '/'>::new(p).collect();
let b: Vec<_, 10> = p.split('/').skip(1).collect();
assert_eq!(a, b);
}
}
Expand Down
69 changes: 69 additions & 0 deletions miniconf/tests/enum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use miniconf::{JsonCoreSlash, Path, Tree, TreeDeserialize, TreeKey, TreeSerialize};
use strum::{AsRefStr, EnumString};

#[derive(Tree, Default, PartialEq, Debug)]
struct Inner {
a: i32,
}

#[derive(Tree, Default, EnumString, AsRefStr, PartialEq, Debug)]
enum Enum {
#[default]
None,
#[strum(serialize = "foo")]
#[tree(rename = "foo")]
A(i32),
B(#[tree(depth = 1)] Inner),
}

#[derive(TreeKey, TreeSerialize, TreeDeserialize, Default)]
struct Settings {
#[tree(typ = "&str", get = Self::get_tag, validate = Self::set_tag)]
tag: (),
#[tree(depth = 2)]
en: Enum,
}

impl Settings {
fn get_tag(&self) -> Result<&str, &'static str> {
Ok(self.en.as_ref())
}

fn set_tag(&mut self, tag: &str) -> Result<(), &'static str> {
self.en = Enum::try_from(tag).or(Err("invalid tag"))?;
Ok(())
}
}

#[test]
fn enum_switch() {
let mut s = Settings::default();
assert_eq!(s.en, Enum::None);
s.set_json("/tag", b"\"foo\"").unwrap();
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();
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();
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"]
);
}
2 changes: 2 additions & 0 deletions miniconf/tests/index.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// This follows arrays.rs, just with indices.

use miniconf::{Deserialize, JsonCoreSlash, Serialize, Traversal, Tree};

#[derive(Debug, Copy, Clone, Default, Tree, Deserialize, Serialize)]
Expand Down
21 changes: 12 additions & 9 deletions miniconf/tests/iter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,23 @@ fn struct_iter() {

#[test]
fn struct_iter_indices() {
let mut paths = [
let paths = [
([0, 0, 0], 2),
([0, 1, 0], 2),
([1, 0, 0], 2),
([2, 0, 0], 3),
([3, 0, 0], 1),
]
.into_iter();
for (have, expect) in Settings::nodes::<Indices<_>>().exact_size().zip(&mut paths) {
let (idx, node) = have.unwrap();
assert_eq!((idx.into_inner(), node.depth()), expect);
}
// Ensure that all fields were iterated.
assert_eq!(paths.next(), None);
];
assert_eq!(
Settings::nodes::<Indices<[usize; 3]>>()
.exact_size()
.map(|have| {
let (idx, node) = have.unwrap();
(idx.into_inner(), node.depth())
})
.collect::<Vec<_>>(),
paths
);
}

#[test]
Expand Down
54 changes: 19 additions & 35 deletions miniconf/tests/option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ struct Settings {
value: Option<Inner>,
}

fn nodes<M: miniconf::TreeKey<Y>, const Y: usize>(want: &[&str]) {
assert_eq!(
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::<Vec<_>>(),
want
);
}

#[test]
fn just_option() {
let mut it = Option::<u32>::nodes::<Path<String, '/'>>().exact_size();
Expand All @@ -33,6 +48,7 @@ fn option_get_set_none() {
settings.get_json("/value", &mut data),
Err(Traversal::Absent(1).into())
);
// The Absent field indicates at which depth the variant was absent
assert_eq!(
settings.set_json("/value/data", b"5"),
Err(Traversal::Absent(1).into())
Expand All @@ -56,21 +72,7 @@ fn option_get_set_some() {

#[test]
fn option_iterate_some_none() {
let mut settings = Settings::default();

// When the value is None, it will still be iterated over as a topic but may not exist at runtime.
settings.value.take();
let mut iterator = Settings::nodes::<Path<String, '/'>>().exact_size();
let (path, _node) = iterator.next().unwrap().unwrap();
assert_eq!(path.as_str(), "/value/data");
assert!(iterator.next().is_none());

// When the value is Some, it should be iterated over.
settings.value.replace(Inner { data: 5 });
let mut iterator = Settings::nodes::<Path<String, '/'>>().exact_size();
let (path, _node) = iterator.next().unwrap().unwrap();
assert_eq!(path.as_str(), "/value/data");
assert_eq!(iterator.next(), None);
nodes::<Settings, 3>(&["/value/data"]);
}

#[test]
Expand All @@ -79,23 +81,14 @@ fn option_test_normal_option() {
struct S {
data: Option<u32>,
}
nodes::<S, 1>(&["/data"]);

let mut s = S::default();
assert!(s.data.is_none());

let mut iterator = S::nodes::<Path<String, '/'>>().exact_size();
let (path, _node) = iterator.next().unwrap().unwrap();
assert_eq!(path.as_str(), "/data");
assert!(iterator.next().is_none());

s.set_json("/data", b"7").unwrap();
assert_eq!(s.data, Some(7));

let mut iterator = S::nodes::<Path<String, '/'>>().exact_size();
let (path, _node) = iterator.next().unwrap().unwrap();
assert_eq!(path.as_str(), "/data");
assert!(iterator.next().is_none());

s.set_json("/data", b"null").unwrap();
assert!(s.data.is_none());
}
Expand All @@ -107,25 +100,16 @@ fn option_test_defer_option() {
#[tree(depth = 1)]
data: Option<u32>,
}
nodes::<S, 2>(&["/data"]);

let mut s = S::default();
assert!(s.data.is_none());

let mut iterator = S::nodes::<Path<String, '/'>>().exact_size();
let (path, _node) = iterator.next().unwrap().unwrap();
assert_eq!(path.as_str(), "/data");
assert!(iterator.next().is_none());

assert!(s.set_json("/data", b"7").is_err());
s.data = Some(0);
s.set_json("/data", b"7").unwrap();
assert_eq!(s.data, Some(7));

let mut iterator = S::nodes::<Path<String, '/'>>().exact_size();
let (path, _node) = iterator.next().unwrap().unwrap();
assert_eq!(path.as_str(), "/data");
assert!(iterator.next().is_none());

assert!(s.set_json("/data", b"null").is_err());
}

Expand Down
2 changes: 1 addition & 1 deletion miniconf/tests/packed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ fn size() {
// Bit-hungriest type is [T;0] but that doesn't have any keys so won't recurse/consume with any Transcode/Keys
// Then [T; 1] which takes one bit per level (not 0 bits, to distinguish empty packed)
// Worst case for a 32 bit usize we need 31 array levels (marker bit) but TreeKey is only implemented to 16
// Easiest 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.
// Easiest 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.
// With the unit as type, we need 0 storage but can't do much.
type A16 = [[[[[[[[[[[[[[[[(); 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 3]; 1];
assert_eq!(core::mem::size_of::<A16>(), 0);
Expand Down
Loading

0 comments on commit 6a93011

Please sign in to comment.