diff --git a/CHANGELOG.md b/CHANGELOG.md index 32babdb1..b0c36ca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/miniconf/Cargo.toml b/miniconf/Cargo.toml index bbf24d3d..fa991e64 100644 --- a/miniconf/Cargo.toml +++ b/miniconf/Cargo.toml @@ -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" @@ -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"] diff --git a/miniconf/README.md b/miniconf/README.md index 1f4e1c32..967ebe5c 100644 --- a/miniconf/README.md +++ b/miniconf/README.md @@ -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 @@ -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 @@ -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 diff --git a/miniconf/src/error.rs b/miniconf/src/error.rs index da0d7fff..f0bc8916 100644 --- a/miniconf/src/error.rs +++ b/miniconf/src/error.rs @@ -1,4 +1,4 @@ -use core::fmt::{Display, Formatter}; +use core::fmt::{Debug, Display, Formatter}; /// Errors that can occur when using the Tree traits. /// @@ -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. @@ -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})") @@ -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] @@ -116,10 +119,10 @@ pub enum Error { Finalization(E), } -impl Display for Error { +impl Display for Error { 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}") } @@ -130,6 +133,15 @@ impl Display for Error { } } +impl core::error::Error for Error { + 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 TryFrom> for Traversal { type Error = Error; diff --git a/miniconf/src/key.rs b/miniconf/src/key.rs index cec1f95a..fdb91bba 100644 --- a/miniconf/src/key.rs +++ b/miniconf/src/key.rs @@ -28,20 +28,20 @@ pub trait KeyLookup { fn name_to_index(value: &str) -> Option; } -/// 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(&self) -> Option; } -// `usize` index as Key +// index impl Key for usize { fn find(&self) -> Option { Some(*self) } } -// &str name as Key +// name impl Key for &str { fn find(&self) -> Option { M::name_to_index(self) diff --git a/miniconf/src/node.rs b/miniconf/src/node.rs index f5e3a12c..1255dfcd 100644 --- a/miniconf/src/node.rs +++ b/miniconf/src/node.rs @@ -86,7 +86,7 @@ impl From 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>> for Node { type Error = Traversal; fn try_from(value: Result>) -> Result { diff --git a/miniconf/src/packed.rs b/miniconf/src/packed.rs index 55d15338..8cdd77ad 100644 --- a/miniconf/src/packed.rs +++ b/miniconf/src/packed.rs @@ -208,14 +208,12 @@ impl Packed { pub fn pop_msb(&mut self, bits: u32) -> Option { 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. @@ -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 + }) } } diff --git a/miniconf/src/tree.rs b/miniconf/src/tree.rs index ce811f41..e4f5a6f8 100644 --- a/miniconf/src/tree.rs +++ b/miniconf/src/tree.rs @@ -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. diff --git a/miniconf/tests/common/mod.rs b/miniconf/tests/common/mod.rs new file mode 100644 index 00000000..d21cd196 --- /dev/null +++ b/miniconf/tests/common/mod.rs @@ -0,0 +1,26 @@ +use miniconf::{JsonCoreSlashOwned, Path, TreeKey}; + +pub fn paths() -> Vec +where + M: TreeKey, +{ + M::nodes::>() + .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(s: &mut M, path: &str, value: &[u8]) +where + M: JsonCoreSlashOwned, +{ + 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); +} diff --git a/miniconf/tests/compiletest.rs b/miniconf/tests/compiletest.rs new file mode 100644 index 00000000..870c2f95 --- /dev/null +++ b/miniconf/tests/compiletest.rs @@ -0,0 +1,5 @@ +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/*.rs"); +} diff --git a/miniconf/tests/enum.rs b/miniconf/tests/enum.rs index fa3014d4..09f798f8 100644 --- a/miniconf/tests/enum.rs +++ b/miniconf/tests/enum.rs @@ -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, @@ -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::>() - .exact_size() - .map(|pn| { - let (p, n) = pn.unwrap(); - assert!(n.is_leaf()); - p.into_inner() - }) - .collect::>(), - vec!["/tag", "/en/foo", "/en/B/a"] - ); + assert_eq!(paths::(), ["/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::(), ["/A"]); +} + +#[test] +fn option() { + // Also tests macro hygiene a bit + #[allow(dead_code)] + #[derive(Tree, Copy, Clone, PartialEq, Default, Debug)] + #[tree(flatten)] + enum Option { + #[default] + None, + // #192 + Some(#[tree(depth = 1)] T), + } + assert_eq!(paths::, 1>(), ["/0"]); + assert_eq!(paths::>, 1>(), [""]); } diff --git a/miniconf/tests/flatten.rs b/miniconf/tests/flatten.rs new file mode 100644 index 00000000..b82f4253 --- /dev/null +++ b/miniconf/tests/flatten.rs @@ -0,0 +1,73 @@ +use miniconf::{JsonCoreSlash, Tree}; + +mod common; +use common::*; + +#[derive(Tree, Default, PartialEq, Debug)] +struct Inner { + a: i32, +} + +#[test] +fn struct_flatten() { + #[derive(Tree, Default, PartialEq, Debug)] + #[tree(flatten)] + struct S1 { + a: i32, + } + assert_eq!(paths::(), [""]); + let mut s = S1::default(); + set_get(&mut s, "", b"1"); + assert_eq!(s.a, 1); + + #[derive(Tree, Default, PartialEq, Debug)] + #[tree(flatten)] + struct S2(i32); + assert_eq!(paths::(), [""]); + let mut s = S2::default(); + set_get(&mut s, "", b"1"); + assert_eq!(s.0, 1); + + #[derive(Tree, Default, PartialEq, Debug)] + #[tree(flatten)] + struct S3(#[tree(depth = 1)] Inner); + assert_eq!(paths::(), ["/a"]); + let mut s = S3::default(); + set_get(&mut s, "/a", b"1"); + assert_eq!(s.0.a, 1); +} + +#[test] +fn enum_flatten() { + #[derive(Tree, Default, PartialEq, Debug)] + #[tree(flatten)] + enum E1 { + #[default] + None, + A(i32), + } + assert_eq!(paths::(), [""]); + let mut e = E1::A(0); + set_get(&mut e, "", b"1"); + assert_eq!(e, E1::A(1)); + assert_eq!( + E1::None.set_json("", b"1").unwrap_err(), + miniconf::Traversal::Absent(0).into() + ); + + #[derive(Tree, Default, PartialEq, Debug)] + #[tree(flatten)] + enum E2 { + #[default] + None, + A(#[tree(depth = 1)] Inner), + } + assert_eq!(paths::(), ["/a"]); + let mut e = E2::A(Inner::default()); + set_get(&mut e, "/a", b"1"); + assert_eq!(e, E2::A(Inner { a: 1 })); + assert_eq!( + E2::None.set_json("/a", b"1").unwrap_err(), + miniconf::Traversal::Absent(0).into() + ); +} diff --git a/miniconf/tests/skipped.rs b/miniconf/tests/skipped.rs index d18cbd22..ed42f71b 100644 --- a/miniconf/tests/skipped.rs +++ b/miniconf/tests/skipped.rs @@ -30,3 +30,20 @@ fn path() { Err(Traversal::NotFound(1)) ); } + +#[test] +fn skip_enum() { + #[allow(dead_code)] + #[derive(Tree)] + pub enum E { + A(i32, #[tree(skip)] i32), + } +} + +#[test] +fn skip_struct() { + #[allow(dead_code)] + #[derive(Tree)] + #[tree(flatten)] + pub struct S(i32, #[tree(skip)] i32); +} diff --git a/miniconf/tests/ui/enum-named.rs b/miniconf/tests/ui/enum-named.rs new file mode 100644 index 00000000..3e193c46 --- /dev/null +++ b/miniconf/tests/ui/enum-named.rs @@ -0,0 +1,8 @@ +use miniconf::Tree; + +#[derive(Tree)] +pub enum E { + A { a: i32 }, +} + +fn main() {} diff --git a/miniconf/tests/ui/enum-named.stderr b/miniconf/tests/ui/enum-named.stderr new file mode 100644 index 00000000..8454ec19 --- /dev/null +++ b/miniconf/tests/ui/enum-named.stderr @@ -0,0 +1,7 @@ +error: Unsupported shape `named fields`. Expected unnamed fields or no fields. + --> tests/ui/enum-named.rs:3:10 + | +3 | #[derive(Tree)] + | ^^^^ + | + = note: this error originates in the derive macro `Tree` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/miniconf/tests/ui/enum-non-newtype.rs b/miniconf/tests/ui/enum-non-newtype.rs new file mode 100644 index 00000000..45b57588 --- /dev/null +++ b/miniconf/tests/ui/enum-non-newtype.rs @@ -0,0 +1,13 @@ +use miniconf::Tree; + +#[derive(Tree)] +pub enum E1 { + A(i32, i32), +} + +#[derive(Tree)] +pub enum E2 { + A { a: i32, b: i32 }, +} + +fn main() {} diff --git a/miniconf/tests/ui/enum-non-newtype.stderr b/miniconf/tests/ui/enum-non-newtype.stderr new file mode 100644 index 00000000..f40b96e2 --- /dev/null +++ b/miniconf/tests/ui/enum-non-newtype.stderr @@ -0,0 +1,13 @@ +error: Only newtype (single field tuple) enum variants are supported. + --> tests/ui/enum-non-newtype.rs:5:5 + | +5 | A(i32, i32), + | ^ + +error: Unsupported shape `named fields`. Expected unnamed fields or no fields. + --> tests/ui/enum-non-newtype.rs:8:10 + | +8 | #[derive(Tree)] + | ^^^^ + | + = note: this error originates in the derive macro `Tree` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/miniconf/tests/ui/enum-skip.rs b/miniconf/tests/ui/enum-skip.rs new file mode 100644 index 00000000..fc3ff081 --- /dev/null +++ b/miniconf/tests/ui/enum-skip.rs @@ -0,0 +1,11 @@ +use miniconf::Tree; + +#[derive(Tree)] +pub struct S(#[tree(skip)] i32, i32); + +#[derive(Tree)] +pub enum E { + A(#[tree(skip)] i32, i32), +} + +fn main() {} diff --git a/miniconf/tests/ui/enum-skip.stderr b/miniconf/tests/ui/enum-skip.stderr new file mode 100644 index 00000000..661cac0f --- /dev/null +++ b/miniconf/tests/ui/enum-skip.stderr @@ -0,0 +1,11 @@ +error: Can only `skip` terminal tuple struct fields + --> tests/ui/enum-skip.rs:4:21 + | +4 | pub struct S(#[tree(skip)] i32, i32); + | ^^^^ + +error: Can only `skip` terminal tuple struct fields + --> tests/ui/enum-skip.rs:8:14 + | +8 | A(#[tree(skip)] i32, i32), + | ^^^^ diff --git a/miniconf/tests/ui/flatten-ambiguous.rs b/miniconf/tests/ui/flatten-ambiguous.rs new file mode 100644 index 00000000..d0e8760f --- /dev/null +++ b/miniconf/tests/ui/flatten-ambiguous.rs @@ -0,0 +1,17 @@ +use miniconf::Tree; + +#[derive(Tree)] +#[tree(flatten)] +pub struct S { + a: i32, + b: i32, +} + +#[derive(Tree)] +#[tree(flatten)] +pub enum E { + A(i32), + B(i32), +} + +fn main() {} diff --git a/miniconf/tests/ui/flatten-ambiguous.stderr b/miniconf/tests/ui/flatten-ambiguous.stderr new file mode 100644 index 00000000..9f870d15 --- /dev/null +++ b/miniconf/tests/ui/flatten-ambiguous.stderr @@ -0,0 +1,11 @@ +error: Can't flatten multiple fields/variants + --> tests/ui/flatten-ambiguous.rs:4:8 + | +4 | #[tree(flatten)] + | ^^^^^^^ + +error: Can't flatten multiple fields/variants + --> tests/ui/flatten-ambiguous.rs:11:8 + | +11 | #[tree(flatten)] + | ^^^^^^^ diff --git a/miniconf_derive/src/field.rs b/miniconf_derive/src/field.rs index 52b74062..aaa52654 100644 --- a/miniconf_derive/src/field.rs +++ b/miniconf_derive/src/field.rs @@ -57,42 +57,48 @@ impl TreeField { } } - pub fn metadata(&self, i: usize) -> Option { + pub fn metadata(&self, i: usize) -> TokenStream { // Quote context is a match of the field index with `metadata()` args available. let depth = self.depth; if depth > 0 { let typ = self.typ(); - Some(quote_spanned! { self.span()=> - #i => <#typ as ::miniconf::TreeKey<#depth>>::metadata() - }) + quote_spanned! { self.span()=> + let m = <#typ as ::miniconf::TreeKey<#depth>>::metadata(); + meta.max_length = meta.max_length.max(ident_len(#i) + m.max_length); + meta.max_depth = meta.max_depth.max(m.max_depth); + meta.count += m.count; + } } else { - None + quote_spanned! { self.span()=> + meta.max_length = meta.max_length.max(ident_len(#i)); + meta.count += 1; + } } } - fn getter(&self, i: usize, is_enum: bool) -> TokenStream { + fn getter(&self, i: Option) -> TokenStream { if let Some(get) = &self.get { quote_spanned! { get.span()=> #get(self).map_err(|msg| ::miniconf::Traversal::Access(0, msg).into()) } - } else if is_enum { - quote_spanned!(self.span()=> Ok(value)) - } else { + } else if let Some(i) = i { let ident = self.ident_or_index(i); - quote_spanned!(self.span()=> Ok(&self.#ident)) + quote_spanned!(self.span()=> ::core::result::Result::Ok(&self.#ident)) + } else { + quote_spanned!(self.span()=> ::core::result::Result::Ok(value)) } } - fn getter_mut(&self, i: usize, is_enum: bool) -> TokenStream { + fn getter_mut(&self, i: Option) -> TokenStream { if let Some(get_mut) = &self.get_mut { quote_spanned! { get_mut.span()=> #get_mut(self).map_err(|msg| ::miniconf::Traversal::Access(0, msg).into()) } - } else if is_enum { - quote_spanned!(self.span()=> Ok(value)) - } else { + } else if let Some(i) = i { let ident = self.ident_or_index(i); - quote_spanned!(self.span()=> Ok(&mut self.#ident)) + quote_spanned!(self.span()=> ::core::result::Result::Ok(&mut self.#ident)) + } else { + quote_spanned!(self.span()=> ::core::result::Result::Ok(value)) } } @@ -108,46 +114,36 @@ impl TreeField { } } - fn lhs(&self, i: usize, ident: Option<&syn::Ident>) -> TokenStream { - if let Some(ident) = ident { - quote_spanned!(ident.span()=> (Self::#ident(value), #i)) - } else { - quote_spanned!(self.span()=> #i) - } - } - - pub fn serialize_by_key(&self, i: usize, ident: Option<&syn::Ident>) -> TokenStream { + pub fn serialize_by_key(&self, i: Option) -> TokenStream { // Quote context is a match of the field index with `serialize_by_key()` args available. - let lhs = self.lhs(i, ident); let depth = self.depth; - let getter = self.getter(i, ident.is_some()); + let getter = self.getter(i); if depth > 0 { quote_spanned! { self.span()=> - #lhs => #getter + #getter .and_then(|value| ::miniconf::TreeSerialize::<#depth>::serialize_by_key(value, keys, ser)) } } else { quote_spanned! { self.span()=> - #lhs => #getter + #getter .and_then(|value| ::miniconf::Serialize::serialize(value, ser) .map_err(|err| ::miniconf::Error::Inner(0, err)) - .and(Ok(0)) + .and(::core::result::Result::Ok(0)) ) } } } - pub fn deserialize_by_key(&self, i: usize, ident: Option<&syn::Ident>) -> TokenStream { + pub fn deserialize_by_key(&self, i: Option) -> TokenStream { // Quote context is a match of the field index with `deserialize_by_key()` args available. - let lhs = self.lhs(i, ident); let depth = self.depth; - let getter_mut = self.getter_mut(i, ident.is_some()); + let getter_mut = self.getter_mut(i); let validator = self.validator(); if depth > 0 { quote_spanned! { self.span()=> - #lhs => #getter_mut + #getter_mut .and_then(|item| ::miniconf::TreeDeserialize::<'de, #depth>::deserialize_by_key(item, keys, de) ) @@ -155,7 +151,7 @@ impl TreeField { } } else { quote_spanned! { self.span()=> - #lhs => ::miniconf::Deserialize::deserialize(de) + ::miniconf::Deserialize::deserialize(de) .map_err(|err| ::miniconf::Error::Inner(0, err)) #validator .and_then(|new| @@ -168,36 +164,34 @@ impl TreeField { } } - pub fn ref_any_by_key(&self, i: usize, ident: Option<&syn::Ident>) -> TokenStream { + pub fn ref_any_by_key(&self, i: Option) -> TokenStream { // Quote context is a match of the field index with `get_mut_by_key()` args available. - let lhs = self.lhs(i, ident); let depth = self.depth; - let getter = self.getter(i, ident.is_some()); + let getter = self.getter(i); if depth > 0 { quote_spanned! { self.span()=> - #lhs => #getter + #getter .and_then(|item| ::miniconf::TreeAny::<#depth>::ref_any_by_key(item, keys)) } } else { quote_spanned! { self.span()=> - #lhs => #getter.map(|item| item as &dyn ::core::any::Any) + #getter.map(|item| item as &dyn ::core::any::Any) } } } - pub fn mut_any_by_key(&self, i: usize, ident: Option<&syn::Ident>) -> TokenStream { + pub fn mut_any_by_key(&self, i: Option) -> TokenStream { // Quote context is a match of the field index with `get_mut_by_key()` args available. - let lhs = self.lhs(i, ident); let depth = self.depth; - let getter_mut = self.getter_mut(i, ident.is_some()); + let getter_mut = self.getter_mut(i); if depth > 0 { quote_spanned! { self.span()=> - #lhs => #getter_mut + #getter_mut .and_then(|item| ::miniconf::TreeAny::<#depth>::mut_any_by_key(item, keys)) } } else { quote_spanned! { self.span()=> - #lhs => #getter_mut.map(|item| item as &mut dyn ::core::any::Any) + #getter_mut.map(|item| item as &mut dyn ::core::any::Any) } } } diff --git a/miniconf_derive/src/lib.rs b/miniconf_derive/src/lib.rs index afa06a26..231cf343 100644 --- a/miniconf_derive/src/lib.rs +++ b/miniconf_derive/src/lib.rs @@ -1,3 +1,4 @@ +use darling::FromDeriveInput; use proc_macro::TokenStream; use syn::{parse_macro_input, DeriveInput}; @@ -8,7 +9,7 @@ use tree::Tree; /// Derive the `TreeKey` trait for a struct. #[proc_macro_derive(TreeKey, attributes(tree))] pub fn derive_tree_key(input: TokenStream) -> TokenStream { - match Tree::parse(&parse_macro_input!(input as DeriveInput)) { + match Tree::from_derive_input(&parse_macro_input!(input as DeriveInput)) { Ok(t) => t.tree_key(), Err(e) => e.write_errors(), } @@ -18,7 +19,7 @@ pub fn derive_tree_key(input: TokenStream) -> TokenStream { /// Derive the `TreeSerialize` trait for a struct. #[proc_macro_derive(TreeSerialize, attributes(tree))] pub fn derive_tree_serialize(input: TokenStream) -> TokenStream { - match Tree::parse(&parse_macro_input!(input as DeriveInput)) { + match Tree::from_derive_input(&parse_macro_input!(input as DeriveInput)) { Ok(t) => t.tree_serialize(), Err(e) => e.write_errors(), } @@ -28,7 +29,7 @@ pub fn derive_tree_serialize(input: TokenStream) -> TokenStream { /// Derive the `TreeDeserialize` trait for a struct. #[proc_macro_derive(TreeDeserialize, attributes(tree))] pub fn derive_tree_deserialize(input: TokenStream) -> TokenStream { - match Tree::parse(&parse_macro_input!(input as DeriveInput)) { + match Tree::from_derive_input(&parse_macro_input!(input as DeriveInput)) { Ok(t) => t.tree_deserialize(), Err(e) => e.write_errors(), } @@ -38,7 +39,7 @@ pub fn derive_tree_deserialize(input: TokenStream) -> TokenStream { /// Derive the `TreeAny` trait for a struct. #[proc_macro_derive(TreeAny, attributes(tree))] pub fn derive_tree_any(input: TokenStream) -> TokenStream { - match Tree::parse(&parse_macro_input!(input as DeriveInput)) { + match Tree::from_derive_input(&parse_macro_input!(input as DeriveInput)) { Ok(t) => t.tree_any(), Err(e) => e.write_errors(), } @@ -48,7 +49,7 @@ pub fn derive_tree_any(input: TokenStream) -> TokenStream { /// Shorthand to derive the `TreeKey`, `TreeAny`, `TreeSerialize`, and `TreeDeserialize` traits for a struct. #[proc_macro_derive(Tree, attributes(tree))] pub fn derive_tree(input: TokenStream) -> TokenStream { - match Tree::parse(&parse_macro_input!(input as DeriveInput)) { + match Tree::from_derive_input(&parse_macro_input!(input as DeriveInput)) { Ok(t) => [ t.tree_key(), t.tree_any(), diff --git a/miniconf_derive/src/tree.rs b/miniconf_derive/src/tree.rs index 510fd88c..a8c3e8f3 100644 --- a/miniconf_derive/src/tree.rs +++ b/miniconf_derive/src/tree.rs @@ -1,6 +1,6 @@ use darling::{ ast::{self, Data}, - util::{Flag, SpannedValue}, + util::Flag, Error, FromDeriveInput, FromVariant, }; use proc_macro2::TokenStream; @@ -10,17 +10,39 @@ use syn::parse_quote; use crate::field::TreeField; #[derive(Debug, FromVariant, Clone)] -#[darling(attributes(tree))] +#[darling(attributes(tree), supports(newtype, tuple, unit), and_then=Self::parse)] pub struct TreeVariant { ident: syn::Ident, rename: Option, skip: Flag, - fields: ast::Fields>, + fields: ast::Fields, } impl TreeVariant { - fn field(&self) -> &SpannedValue { - assert!(self.fields.len() == 1); + fn parse(mut self) -> darling::Result { + assert!(!self.fields.is_struct()); + // unnamed fields can only be skipped if they are terminal + while self + .fields + .fields + .last() + .map(|f| f.skip.is_present()) + .unwrap_or_default() + { + self.fields.fields.pop(); + } + if let Some(f) = self.fields.iter().find(|f| f.skip.is_present()) { + return Err( + Error::custom("Can only `skip` terminal tuple struct fields") + .with_span(&f.skip.span()), + ); + } + Ok(self) + } + + fn field(&self) -> &TreeField { + // assert!(self.fields.is_newtype()); // Don't do this since we modified it with skip + assert!(self.fields.len() == 1); // Only newtypes currently self.fields.fields.first().unwrap() } @@ -30,38 +52,24 @@ impl TreeVariant { } #[derive(Debug, FromDeriveInput, Clone)] -#[darling(attributes(tree))] -#[darling(supports(any))] +#[darling(attributes(tree), supports(any), and_then=Self::parse)] +#[darling()] pub struct Tree { ident: syn::Ident, - // pub flatten: Flag, // FIXME: implement generics: syn::Generics, - data: Data, SpannedValue>, + flatten: Flag, + data: Data, } impl Tree { - fn depth(&self) -> usize { - match &self.data { - Data::Struct(fields) => fields.fields.iter().fold(0, |d, field| d.max(field.depth)) + 1, - Data::Enum(variants) => { - variants - .iter() - .fold(0, |d, variant| d.max(variant.field().depth)) - + 1 - } - } - } - - pub fn parse(input: &syn::DeriveInput) -> Result { - let mut tree = Self::from_derive_input(input)?; - - match &mut tree.data { + fn parse(mut self) -> darling::Result { + match &mut self.data { Data::Struct(fields) => { // unnamed fields can only be skipped if they are terminal while fields .fields .last() - .map(|f| f.skip.is_present()) + .map(|f| f.ident.is_none() && f.skip.is_present()) .unwrap_or_default() { fields.fields.pop(); @@ -71,44 +79,46 @@ impl Tree { .retain(|f| f.ident.is_none() || !f.skip.is_present()); if let Some(f) = fields.fields.iter().find(|f| f.skip.is_present()) { return Err( + // Note(design) If non-terminal fields are skipped, there is a gap in the indices. + // This could be lifted with a index map. Error::custom("Can only `skip` terminal tuple struct fields") .with_span(&f.skip.span()), ); } } Data::Enum(variants) => { - variants.retain(|v| !(v.skip.is_present() || v.fields.is_unit())); - for v in variants.iter_mut() { - if v.fields.is_struct() { - // Note(design) For tuple or named struct variants we'd have to create proxy - // structs anyway to support KeyLookup on that level. - return Err( - Error::custom("Struct variants not supported").with_span(&v.span()) - ); - } - // unnamed fields can only be skipped if they are terminal - while v - .fields - .fields - .last() - .map(|f| f.skip.is_present()) - .unwrap_or_default() - { - v.fields.fields.pop(); - } - if let Some(f) = v.fields.iter().find(|f| f.skip.is_present()) { - return Err( - Error::custom("Can only `skip` terminal tuple struct fields") - .with_span(&f.skip.span()), - ); + variants.retain(|v| !(v.skip.is_present() || v.fields.is_empty())); + for v in variants.iter() { + if v.fields.len() != 1 { + return Err(Error::custom( + "Only newtype (single field tuple) enum variants are supported.", + ) + .with_span(&v.ident.span())); } } } } - Ok(tree) + if self.flatten.is_present() && self.fields().len() != 1 { + return Err(Error::custom("Can't flatten multiple fields/variants") + .with_span(&self.flatten.span())); + } + Ok(self) } - fn fields(&self) -> Vec<&SpannedValue> { + fn level(&self) -> usize { + if self.flatten.is_present() { + 0 + } else { + 1 + } + } + + fn depth(&self) -> usize { + let inner = self.fields().iter().fold(0, |d, field| d.max(field.depth)); + (self.level() + inner).max(1) // We need to eat at least one level. C.f. impl TreeKey for Option. + } + + fn fields(&self) -> Vec<&TreeField> { match &self.data { Data::Struct(fields) => fields.iter().collect(), Data::Enum(variants) => variants.iter().map(|v| v.field()).collect(), @@ -128,6 +138,7 @@ impl Tree { pub fn tree_key(&self) -> TokenStream { let depth = self.depth(); + let level = self.level(); let ident = &self.ident; let generics = self.bound_generics(&mut |depth| { (depth > 0).then_some(parse_quote!(::miniconf::TreeKey<#depth>)) @@ -135,7 +146,7 @@ impl Tree { let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let fields = self.fields(); let fields_len = fields.len(); - let metadata_arms = fields.iter().enumerate().filter_map(|(i, f)| f.metadata(i)); + let metadata_arms = fields.iter().enumerate().map(|(i, f)| f.metadata(i)); let traverse_arms = fields .iter() .enumerate() @@ -163,13 +174,13 @@ impl Tree { ), _ => None, }; - let (names, name_to_index, index_to_name, index_len) = if let Some(names) = names { + let (names, name_to_index, index_to_name, mut ident_len) = if let Some(names) = names { ( Some(quote!( const __MINICONF_NAMES: [&'static str; #fields_len] = [#(#names ,)*]; )), quote!(Self::__MINICONF_NAMES.iter().position(|&n| n == value)), - quote!(Some( + quote!(::core::option::Option::Some( *Self::__MINICONF_NAMES .get(index) .ok_or(::miniconf::Traversal::NotFound(1))? @@ -181,87 +192,86 @@ impl Tree { None, quote!(str::parse(value).ok()), quote!(if index >= #fields_len { - Err(::miniconf::Traversal::NotFound(1))? + ::core::result::Result::Err(::miniconf::Traversal::NotFound(1))? } else { - None + ::core::option::Option::None }), quote!(index.checked_ilog10().unwrap_or_default() as usize + 1), ) }; + let (index, traverse, increment, lookup) = if self.flatten.is_present() { + ident_len = quote!(0); + (quote!(0), None, None, None) + } else { + ( + quote!(::miniconf::Keys::next::(&mut keys)?), + Some(quote! { + func(index, #index_to_name, #fields_len).map_err(|err| ::miniconf::Error::Inner(1, err))?; + }), + Some(quote!(::miniconf::Error::increment_result)), + Some(quote! { + #[automatically_derived] + impl #impl_generics #ident #ty_generics #where_clause { + #names + } + + #[automatically_derived] + impl #impl_generics ::miniconf::KeyLookup for #ident #ty_generics #where_clause { + const LEN: usize = #fields_len; + + #[inline] + fn name_to_index(value: &str) -> ::core::option::Option { + #name_to_index + } + } + }), + ) + }; + quote! { #[automatically_derived] impl #impl_generics #ident #ty_generics #where_clause { // TODO: can these be hidden and disambiguated w.r.t. collision? - fn __miniconf_lookup(keys: &mut K) -> Result { + fn __miniconf_lookup(mut keys: K) -> ::core::result::Result { const DEFERS: [bool; #fields_len] = [#(#defers ,)*]; - let index = ::miniconf::Keys::next::(keys)?; + let index = #index; let defer = DEFERS.get(index) .ok_or(::miniconf::Traversal::NotFound(1))?; if !defer && !keys.finalize() { - Err(::miniconf::Traversal::TooLong(1)) + ::core::result::Result::Err(::miniconf::Traversal::TooLong(1)) } else { - Ok(index) + ::core::result::Result::Ok(index) } } - - #names } - #[automatically_derived] - impl #impl_generics ::miniconf::KeyLookup for #ident #ty_generics #where_clause { - const LEN: usize = #fields_len; - - #[inline] - fn name_to_index(value: &str) -> Option { - #name_to_index - } - } + #lookup #[automatically_derived] impl #impl_generics ::miniconf::TreeKey<#depth> for #ident #ty_generics #where_clause { fn metadata() -> ::miniconf::Metadata { let mut meta = ::miniconf::Metadata::default(); - for index in 0..#fields_len { - let item_meta = match index { - #(#metadata_arms ,)* - _ => { - let mut m = ::miniconf::Metadata::default(); - m.count = 1; - m - } - }; - meta.max_length = meta.max_length.max( - #index_len + - item_meta.max_length - ); - meta.max_depth = meta.max_depth.max( - item_meta.max_depth - ); - meta.count += item_meta.count; - } - meta.max_depth += 1; + let ident_len = |index: usize| { #ident_len }; + #(#metadata_arms)* + meta.max_depth += #level; meta } - fn traverse_by_key( - mut keys: K, - mut func: F, - ) -> Result> + fn traverse_by_key(mut keys: K, mut func: F) -> ::core::result::Result> where K: ::miniconf::Keys, - F: FnMut(usize, Option<&'static str>, usize) -> Result<(), E>, + F: ::core::ops::FnMut(usize, ::core::option::Option<&'static str>, usize) -> ::core::result::Result<(), E>, { - let index = ::miniconf::Keys::next::(&mut keys)?; - let name = #index_to_name; - func(index, name, #fields_len).map_err(|err| ::miniconf::Error::Inner(1, err))?; - ::miniconf::Error::increment_result(match index { + let index = #index; + #traverse + #increment(match index { #(#traverse_arms ,)* _ => { if !keys.finalize() { - Err(::miniconf::Traversal::TooLong(0).into()) + ::core::result::Result::Err(::miniconf::Traversal::TooLong(0).into()) } else { - Ok(0) + ::core::result::Result::Ok(0) } } }) @@ -282,31 +292,46 @@ impl Tree { }); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let (mat, arms, default): (_, Vec<_>, _) = match &self.data { + let (mat, arms, default) = match &self.data { Data::Struct(fields) => ( quote!(index), fields .iter() .enumerate() - .map(|(i, f)| f.serialize_by_key(i, None)) - .collect(), - quote!(unreachable!()), + .map(|(i, f)| { + let rhs = f.serialize_by_key(Some(i)); + quote!(#i => #rhs) + }) + .collect::>(), + quote!(::core::unreachable!()), ), Data::Enum(variants) => ( quote!((self, index)), variants .iter() .enumerate() - .map(|(i, v)| v.field().serialize_by_key(i, Some(&v.ident))) + .map(|(i, v)| { + let ident = &v.ident; + let rhs = v.field().serialize_by_key(None); + quote!((Self::#ident(value, ..), #i) => #rhs) + }) .collect(), - quote!(Err(::miniconf::Traversal::Absent(0).into())), + quote!(::core::result::Result::Err( + ::miniconf::Traversal::Absent(0).into() + )), ), }; + let increment = if self.flatten.is_present() { + quote!() + } else { + quote!(::miniconf::Error::increment_result) + }; + quote! { #[automatically_derived] impl #impl_generics ::miniconf::TreeSerialize<#depth> for #ident #ty_generics #where_clause { - fn serialize_by_key(&self, mut keys: K, ser: S) -> Result> + fn serialize_by_key(&self, mut keys: K, ser: S) -> ::core::result::Result> where K: ::miniconf::Keys, S: ::miniconf::Serializer, @@ -314,7 +339,7 @@ impl Tree { let index = Self::__miniconf_lookup(&mut keys)?; // Note(unreachable) empty structs have diverged by now #[allow(unreachable_code)] - ::miniconf::Error::increment_result(match #mat { + #increment(match #mat { #(#arms ,)* _ => #default }) @@ -348,31 +373,46 @@ impl Tree { } let (impl_generics, _, _) = generics.split_for_impl(); - let (mat, arms, default): (_, Vec<_>, _) = match &self.data { + let (mat, arms, default) = match &self.data { Data::Struct(fields) => ( quote!(index), fields .iter() .enumerate() - .map(|(i, f)| f.deserialize_by_key(i, None)) - .collect(), - quote!(unreachable!()), + .map(|(i, f)| { + let rhs = f.deserialize_by_key(Some(i)); + quote!(#i => #rhs) + }) + .collect::>(), + quote!(::core::unreachable!()), ), Data::Enum(variants) => ( quote!((self, index)), variants .iter() .enumerate() - .map(|(i, v)| v.field().deserialize_by_key(i, Some(&v.ident))) + .map(|(i, v)| { + let ident = &v.ident; + let rhs = v.field().deserialize_by_key(None); + quote!((Self::#ident(value, ..), #i) => #rhs) + }) .collect(), - quote!(Err(::miniconf::Traversal::Absent(0).into())), + quote!(::core::result::Result::Err( + ::miniconf::Traversal::Absent(0).into() + )), ), }; + let increment = if self.flatten.is_present() { + quote!() + } else { + quote!(::miniconf::Error::increment_result) + }; + quote! { #[automatically_derived] impl #impl_generics ::miniconf::TreeDeserialize<'de, #depth> for #ident #ty_generics #where_clause { - fn deserialize_by_key(&mut self, mut keys: K, de: D) -> Result> + fn deserialize_by_key(&mut self, mut keys: K, de: D) -> ::core::result::Result> where K: ::miniconf::Keys, D: ::miniconf::Deserializer<'de>, @@ -380,7 +420,7 @@ impl Tree { let index = Self::__miniconf_lookup(&mut keys)?; // Note(unreachable) empty structs have diverged by now #[allow(unreachable_code)] - ::miniconf::Error::increment_result(match #mat { + #increment(match #mat { #(#arms ,)* _ => #default }) @@ -402,41 +442,53 @@ impl Tree { let ident = &self.ident; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let (mat, ref_arms, mut_arms, default): (_, Vec<_>, Vec<_>, _) = match &self.data { + let (mat, (ref_arms, mut_arms), default) = match &self.data { Data::Struct(fields) => ( quote!(index), fields .iter() .enumerate() - .map(|(i, f)| f.ref_any_by_key(i, None)) - .collect(), - fields - .iter() - .enumerate() - .map(|(i, f)| f.mut_any_by_key(i, None)) - .collect(), - quote!(unreachable!()), + .map(|(i, f)| { + let (ref_rhs, mut_rhs) = + (f.ref_any_by_key(Some(i)), f.mut_any_by_key(Some(i))); + (quote!(#i => #ref_rhs), quote!(#i => #mut_rhs)) + }) + .unzip::<_, _, Vec<_>, Vec<_>>(), + quote!(::core::unreachable!()), ), Data::Enum(variants) => ( quote!((self, index)), variants .iter() .enumerate() - .map(|(i, v)| v.field().ref_any_by_key(i, Some(&v.ident))) - .collect(), - variants - .iter() - .enumerate() - .map(|(i, v)| v.field().mut_any_by_key(i, Some(&v.ident))) - .collect(), - quote!(Err(::miniconf::Traversal::Absent(0).into())), + .map(|(i, v)| { + let ident = &v.ident; + let (ref_rhs, mut_rhs) = ( + v.field().ref_any_by_key(None), + v.field().mut_any_by_key(None), + ); + ( + quote!((Self::#ident(value, ..), #i) => #ref_rhs), + quote!((Self::#ident(value, ..), #i) => #mut_rhs), + ) + }) + .unzip(), + quote!(::core::result::Result::Err( + ::miniconf::Traversal::Absent(0).into() + )), ), }; + let increment = if self.flatten.is_present() { + quote!(ret) + } else { + quote!(ret.map_err(::miniconf::Traversal::increment)) + }; + quote! { #[automatically_derived] impl #impl_generics ::miniconf::TreeAny<#depth> for #ident #ty_generics #where_clause { - fn ref_any_by_key(&self, mut keys: K) -> Result<&dyn ::core::any::Any, ::miniconf::Traversal> + fn ref_any_by_key(&self, mut keys: K) -> ::core::result::Result<&dyn ::core::any::Any, ::miniconf::Traversal> where K: ::miniconf::Keys, { @@ -444,15 +496,15 @@ impl Tree { // Note(unreachable) empty structs have diverged by now #[allow(unreachable_code)] { - let ret: Result<_, _> = match #mat { + let ret: ::core::result::Result<_, _> = match #mat { #(#ref_arms ,)* _ => #default }; - ret.map_err(::miniconf::Traversal::increment) + #increment } } - fn mut_any_by_key(&mut self, mut keys: K) -> Result<&mut dyn ::core::any::Any, ::miniconf::Traversal> + fn mut_any_by_key(&mut self, mut keys: K) -> ::core::result::Result<&mut dyn ::core::any::Any, ::miniconf::Traversal> where K: ::miniconf::Keys, { @@ -460,11 +512,11 @@ impl Tree { // Note(unreachable) empty structs have diverged by now #[allow(unreachable_code)] { - let ret: Result<_, _> = match #mat { + let ret: ::core::result::Result<_, _> = match #mat { #(#mut_arms ,)* _ => #default }; - ret.map_err(::miniconf::Traversal::increment) + #increment } } }