Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

derive support for enums with newtype/unit variants #233

Merged
merged 39 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
cd3576c
Path: debug_assert separator presence in names
jordens Sep 20, 2024
d43f0ab
derive/lib: single parsed tree
jordens Sep 20, 2024
dbb1d6f
[wip] enum: parse
jordens Sep 20, 2024
a8f281a
[wip] enum: test
jordens Sep 20, 2024
653f5d1
Revert "derive/lib: single parsed tree"
jordens Sep 20, 2024
8652052
[wip]: enum: tag, reorg
jordens Sep 20, 2024
38e4929
[wip] derive: move derive expansion to Tree
jordens Sep 21, 2024
609ba84
[wip] enum: support only unit/newtype variants
jordens Sep 22, 2024
f0aaa47
[wip] enum TreeKey/KeyLookup
jordens Sep 23, 2024
d59eeaa
[wip] enum deserialize
jordens Sep 23, 2024
2422032
[wip] enum: any
jordens Sep 23, 2024
6ca49b9
derive: reorganize
jordens Sep 23, 2024
396755f
automatically derived
jordens Sep 23, 2024
7236bb9
CHANGELOG: update
jordens Sep 23, 2024
cf0bd44
derive: refactor
jordens Sep 23, 2024
d4a60d2
README: update
jordens Sep 23, 2024
61f09ea
derive: pub, refactor
jordens Sep 23, 2024
dd38a55
derive: refactor
jordens Sep 23, 2024
ea15501
derive: refactor
jordens Sep 23, 2024
7afce8f
derive: any fixes
jordens Sep 23, 2024
17b8bae
derive: move flatten
jordens Sep 23, 2024
2d9918c
derive: less tokenstream conversion
jordens Sep 23, 2024
f87a5ee
tests: add tag switching example
jordens Sep 24, 2024
3da919b
test: try enum tag rename
jordens Sep 24, 2024
77e2dc2
derive: add automatically_derived also to inherent
jordens Sep 24, 2024
495d5d7
derive: add spans, fix skipping
jordens Sep 24, 2024
60fba13
expand enum tag example
jordens Sep 24, 2024
86cf4c2
enum: cleanup test
jordens Sep 24, 2024
62f3276
cleanup readme/changelog
jordens Sep 24, 2024
3b5a727
tests: remove redundant
jordens Sep 24, 2024
e1944c6
global: refactor/simplify features and dependencies
jordens Sep 24, 2024
d87588c
mqtt: std-embedded-nal 0.3
jordens Sep 24, 2024
2ef2f19
try slice NAMES
jordens Sep 24, 2024
46b49ab
Revert "try slice NAMES"
jordens Sep 24, 2024
3e5d60b
derive: clippy
jordens Sep 24, 2024
390eece
derive: code style
jordens Sep 25, 2024
6922e3a
enum example: no need to quote
jordens Sep 25, 2024
0d87ac8
py: fix single-element list
jordens Sep 25, 2024
da09a86
py: style, remove backticks
jordens Sep 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading