diff --git a/macro/src/gen/meta.rs b/macro/src/gen/meta.rs index 83d9e7d..c6e3755 100644 --- a/macro/src/gen/meta.rs +++ b/macro/src/gen/meta.rs @@ -2,7 +2,7 @@ use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::Ident; -use crate::ir::{self, Expr, FieldKind, LeafKind}; +use crate::ir::{self, Expr, FieldKind, LeafKind, MapKey}; @@ -21,7 +21,7 @@ pub(super) fn gen(input: &ir::Input) -> TokenStream { let name = f.name.to_string(); let doc = &f.doc; let kind = match &f.kind { - FieldKind::Nested { ty }=> { + FieldKind::Nested { ty } => { quote! { confique::meta::FieldKind::Nested { meta: &<#ty as confique::Config>::META } } @@ -73,27 +73,50 @@ pub(super) fn gen(input: &ir::Input) -> TokenStream { } } +/// Helper macro to deduplicate logic for literals. Only used in the function +/// below. +macro_rules! match_literals { + ($v:expr, $ty:expr, $ns:ident, { $($other_arms:tt)* }) => { + match $v { + $ns::Bool(v) => quote! { confique::meta::$ns::Bool(#v) }, + $ns::Str(s) => quote! { confique::meta::$ns::Str(#s) }, + $ns::Int(i) => { + let variant = infer_type(i.suffix(), $ty, "I32", int_type_to_variant); + quote! { confique::meta::$ns::Integer(confique::meta::Integer::#variant(#i)) } + } + $ns::Float(f) => { + let variant = infer_type(f.suffix(), $ty, "F64", float_type_to_variant); + quote! { confique::meta::$ns::Float(confique::meta::Float::#variant(#f)) } + } + $($other_arms)* + } + }; +} + /// Generates the meta expression of type `meta::Expr` to be used for the /// `default` field. `ty` is the type of the field that is used to better infer /// the exact type of the default value. fn default_value_to_meta_expr(default: &Expr, ty: Option<&syn::Type>) -> TokenStream { - match default { - Expr::Bool(v) => quote! { confique::meta::Expr::Bool(#v) }, - Expr::Str(s) => quote! { confique::meta::Expr::Str(#s) }, - Expr::Int(i) => { - let variant = infer_type(i.suffix(), ty, "I32", int_type_to_variant); - quote! { confique::meta::Expr::Integer(confique::meta::Integer::#variant(#i)) } - } - Expr::Float(f) => { - let variant = infer_type(f.suffix(), ty, "F64", float_type_to_variant); - quote! { confique::meta::Expr::Float(confique::meta::Float::#variant(#f)) } - } + match_literals!(default, ty, Expr, { Expr::Array(items) => { - let item_type = ty.and_then(get_item_ty); + let item_type = ty.and_then(get_array_item_type); let items = items.iter().map(|item| default_value_to_meta_expr(item, item_type)); quote! { confique::meta::Expr::Array(&[#( #items ),*]) } } - } + Expr::Map(entries) => { + // TODO: use `Option::unzip` once stable + let types = ty.and_then(get_map_entry_types); + let key_type = types.map(|(t, _)| t); + let value_type = types.map(|(_, v)| v); + + let pairs = entries.iter().map(|e| { + let key = match_literals!(&e.key, key_type, MapKey, {}); + let value = default_value_to_meta_expr(&e.value, value_type); + quote! { confique::meta::MapEntry { key: #key, value: #value } } + }); + quote! { confique::meta::Expr::Map(&[#( #pairs ),*]) } + } + }) } /// Maps an integer type to the `meta::Expr` variant (e.g. `u32` -> `U32`). @@ -149,10 +172,9 @@ fn infer_type( Ident::new(variant, Span::call_site()) } - /// Tries to extract the type of the item of a field with an array default /// value. Examples: `&[u32]` -> `u32`, `Vec` -> `String`. -fn get_item_ty(ty: &syn::Type) -> Option<&syn::Type> { +fn get_array_item_type(ty: &syn::Type) -> Option<&syn::Type> { match ty { // The easy types. syn::Type::Slice(slice) => Some(&slice.elem), @@ -184,9 +206,40 @@ fn get_item_ty(ty: &syn::Type) -> Option<&syn::Type> { }, // Just recurse on inner type. - syn::Type::Reference(r) => get_item_ty(&r.elem), - syn::Type::Group(g) => get_item_ty(&g.elem), - syn::Type::Paren(p) => get_item_ty(&p.elem), + syn::Type::Reference(r) => get_array_item_type(&r.elem), + syn::Type::Group(g) => get_array_item_type(&g.elem), + syn::Type::Paren(p) => get_array_item_type(&p.elem), + + _ => None, + } +} + +/// Tries to extract the key and value types from a map value. Examples: +/// `HashMap` -> `(String, u32)`. +fn get_map_entry_types(ty: &syn::Type) -> Option<(&syn::Type, &syn::Type)> { + match ty { + // We simply check if the last element in the path has exactly two + // generic type arguments, in which case we use those. Otherwise we + // can't really know. + syn::Type::Path(p) => { + let args = match &p.path.segments.last().expect("empty type path").arguments { + syn::PathArguments::AngleBracketed(args) => &args.args, + _ => return None, + }; + + if args.len() != 2 { + return None; + } + + match (&args[0], &args[1]) { + (syn::GenericArgument::Type(k), syn::GenericArgument::Type(v)) => Some((k, v)), + _ => None, + } + }, + + // Just recurse on inner type. + syn::Type::Group(g) => get_map_entry_types(&g.elem), + syn::Type::Paren(p) => get_map_entry_types(&p.elem), _ => None, } diff --git a/macro/src/gen/mod.rs b/macro/src/gen/mod.rs index 342236d..55303d2 100644 --- a/macro/src/gen/mod.rs +++ b/macro/src/gen/mod.rs @@ -304,6 +304,23 @@ fn default_value_to_deserializable_expr(expr: &ir::Expr) -> TokenStream { }; quote! { confique::internal::ArrayIntoDeserializer([ #(#items),* ] #type_annotation) } }, + ir::Expr::Map(entries) => { + let items = entries.iter().map(|e| { + let key = default_value_to_deserializable_expr(&e.key.clone().into()); + let value = default_value_to_deserializable_expr(&e.value); + quote! { (#key, #value) } + }); + + // Empty arrays cause "cannot infer type" errors here. However, it + // really doesn't matter what type the array has as there are 0 + // elements anyway. So we just pick `()`. + let type_annotation = if entries.is_empty() { + quote! { as Vec<((), ())> } + } else { + quote! {} + }; + quote! { confique::internal::MapIntoDeserializer(vec![ #(#items),* ] #type_annotation) } + }, } } diff --git a/macro/src/ir.rs b/macro/src/ir.rs index 577e80b..f04708f 100644 --- a/macro/src/ir.rs +++ b/macro/src/ir.rs @@ -67,4 +67,18 @@ pub(crate) enum Expr { Float(syn::LitFloat), Bool(syn::LitBool), Array(Vec), + Map(Vec), +} + +pub(crate) struct MapEntry { + pub(crate) key: MapKey, + pub(crate) value: Expr, +} + +#[derive(Clone)] +pub(crate) enum MapKey { + Str(syn::LitStr), + Int(syn::LitInt), + Float(syn::LitFloat), + Bool(syn::LitBool), } diff --git a/macro/src/parse.rs b/macro/src/parse.rs index 60a45e5..6167785 100644 --- a/macro/src/parse.rs +++ b/macro/src/parse.rs @@ -1,7 +1,7 @@ use syn::{Error, Token, parse::{Parse, ParseStream}, spanned::Spanned, punctuated::Punctuated}; use crate::{ - ir::{Input, Field, FieldKind, LeafKind, Expr}, + ir::{Input, Field, FieldKind, LeafKind, Expr, MapEntry, MapKey}, util::{unwrap_option, is_option}, }; @@ -112,21 +112,54 @@ impl Parse for Expr { (string, integer, float, bool), and arrays"; if input.peek(syn::token::Bracket) { + // ----- Array ----- let content; syn::bracketed!(content in input); let items = >::parse_terminated(&content)?; Ok(Self::Array(items.into_iter().collect())) + } else if input.peek(syn::token::Brace) { + // ----- Map ----- + let content; + syn::braced!(content in input); + + let items = >::parse_terminated(&content)?; + Ok(Self::Map(items.into_iter().collect())) } else { - let lit: syn::Lit = input.parse() - .map_err(|_| Error::new(input.span(), msg))?; - match lit { - syn::Lit::Str(l) => Ok(Self::Str(l)), - syn::Lit::Int(l) => Ok(Self::Int(l)), - syn::Lit::Float(l) => Ok(Self::Float(l)), - syn::Lit::Bool(l) => Ok(Self::Bool(l)), - _ => Err(Error::new(lit.span(), msg)), - } + // ----- Literal ----- + + // We just use `MapKey` here as it's exactly what we want, despite + // this not having anything to do with maps. + input.parse::() + .map_err(|_| Error::new(input.span(), msg)) + .map(Into::into) + } + } +} + + + +impl Parse for MapEntry { + fn parse(input: ParseStream) -> Result { + let key: MapKey = input.parse()?; + let _: syn::Token![:] = input.parse()?; + let value: Expr = input.parse()?; + Ok(Self { key, value }) + } +} + +impl Parse for MapKey { + fn parse(input: ParseStream) -> Result { + let lit: syn::Lit = input.parse()?; + match lit { + syn::Lit::Str(l) => Ok(Self::Str(l)), + syn::Lit::Int(l) => Ok(Self::Int(l)), + syn::Lit::Float(l) => Ok(Self::Float(l)), + syn::Lit::Bool(l) => Ok(Self::Bool(l)), + _ => Err(Error::new( + lit.span(), + "only string, integer, float, and Boolean literals allowed as map key", + )), } } } @@ -297,3 +330,14 @@ fn assert_empty_or_comma(input: ParseStream) -> Result<(), Error> { Err(Error::new(input.span(), "unexpected tokens, expected no more tokens in this context")) } } + +impl From for Expr { + fn from(src: MapKey) -> Self { + match src { + MapKey::Str(v) => Self::Str(v), + MapKey::Int(v) => Self::Int(v), + MapKey::Float(v) => Self::Float(v), + MapKey::Bool(v) => Self::Bool(v), + } + } +} diff --git a/src/internal.rs b/src/internal.rs index 1c5ca65..a8ecaa5 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -82,3 +82,21 @@ where serde::de::value::SeqDeserializer::new(self.0.into_iter()) } } + +/// `serde` does implement `IntoDeserializer` for `HashMap` and `BTreeMap` but +/// we want to keep the exact source code order of entries, so we need our own +/// type. +pub struct MapIntoDeserializer(pub Vec<(K, V)>); + +impl<'de, K, V, E> serde::de::IntoDeserializer<'de, E> for MapIntoDeserializer +where + K: serde::de::IntoDeserializer<'de, E>, + V: serde::de::IntoDeserializer<'de, E>, + E: serde::de::Error, +{ + type Deserializer = serde::de::value::MapDeserializer<'de, std::vec::IntoIter<(K, V)>, E>; + + fn into_deserializer(self) -> Self::Deserializer { + serde::de::value::MapDeserializer::new(self.0.into_iter()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 4e64a99..e4864e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -284,9 +284,19 @@ pub use crate::{ /// - **`#[config(default = ...)]`**: sets a default value for this field. This /// is returned by [`Partial::default_values`] and, in most circumstances, /// used as a last "layer" to pull values from that have not been set in a -/// layer of higher-priority. Currently, Boolean, float, integer, string, and -/// array values are allowed. The same set of types is allowed as type for -/// array items. +/// layer of higher-priority. Currently, the following expressions are +/// allowed: +/// +/// - Booleans, e.g. `default = true` +/// - Integers, e.g. `default = 900` +/// - Floats, e.g. `default = 3.14` +/// - Strings, e.g. `default = "fox"` +/// - Arrays, e.g. `default = ["foo", "bar"]` +/// - Key value maps, e.g. `default = { "cat": 3.14, "bear": 9.0 }` +/// +/// Map keys can be Booleans, integers, floats, and strings. For array and map +/// values, you can use any of the expressions in the list above (i.e. you +/// can nest arrays/maps). /// /// The field value is deserialized from the specified default value /// (via `serde::de::IntoDeserializer`). So the expression after `default =` diff --git a/src/meta.rs b/src/meta.rs index 72af5a5..f344fbf 100644 --- a/src/meta.rs +++ b/src/meta.rs @@ -56,6 +56,26 @@ pub enum Expr { Integer(Integer), Bool(bool), Array(&'static [Expr]), + + /// A key value map, stored as slice in source code order. + #[serde(serialize_with = "serialize_map")] + Map(&'static [MapEntry]) +} + +#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)] +#[serde(untagged)] +#[non_exhaustive] +pub enum MapKey { + Str(&'static str), + Float(Float), + Integer(Integer), + Bool(bool), +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct MapEntry { + pub key: MapKey, + pub value: Expr, } #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize)] @@ -110,3 +130,27 @@ impl fmt::Display for Integer { } } } + +impl From for Expr { + fn from(src: MapKey) -> Self { + match src { + MapKey::Str(v) => Self::Str(v), + MapKey::Integer(v) => Self::Integer(v), + MapKey::Float(v) => Self::Float(v), + MapKey::Bool(v) => Self::Bool(v), + } + } +} + +fn serialize_map(map: &&'static [MapEntry], serializer: S) -> Result +where + S: serde::Serializer, +{ + use serde::ser::SerializeMap; + + let mut s = serializer.serialize_map(Some(map.len()))?; + for entry in *map { + s.serialize_entry(&entry.key, &entry.value)?; + } + s.end() +} diff --git a/src/test_utils/example1.rs b/src/test_utils/example1.rs index c1b8812..e2c330f 100644 --- a/src/test_utils/example1.rs +++ b/src/test_utils/example1.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::{ net::IpAddr, path::PathBuf, @@ -50,6 +51,10 @@ pub struct Headers { /// Headers that are allowed. #[config(default = ["content-type", "content-encoding"])] pub allowed: Vec, + + /// Assigns a score to some headers. + #[config(default = { "cookie": 1.5, "server": 12.7 })] + pub score: HashMap, } diff --git a/src/toml.rs b/src/toml.rs index 552de27..5b3ba0e 100644 --- a/src/toml.rs +++ b/src/toml.rs @@ -6,7 +6,7 @@ use std::fmt::{self, Write}; use crate::{ Config, template::{self, Formatter}, - meta::Expr, + meta::{Expr, MapKey}, }; @@ -119,7 +119,7 @@ impl TomlFormatter { } impl Formatter for TomlFormatter { - type ExprPrinter = PrintExpr; + type ExprPrinter = PrintExpr<'static>; fn buffer(&mut self) -> &mut String { &mut self.buffer @@ -159,22 +159,50 @@ impl Formatter for TomlFormatter { } /// Helper to emit `meta::Expr` into TOML. -struct PrintExpr(&'static Expr); +struct PrintExpr<'a>(&'a Expr); -impl From<&'static Expr> for PrintExpr { +impl From<&'static Expr> for PrintExpr<'static> { fn from(expr: &'static Expr) -> Self { Self(expr) } } -impl fmt::Display for PrintExpr { +impl fmt::Display for PrintExpr<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - toml::to_string(&self.0) - .expect("string serialization to TOML failed") - .fmt(f) + match self.0 { + Expr::Map(entries) => { + // TODO: pretty printing of long arrays onto multiple lines? + f.write_str("{ ")?; + for (i, entry) in entries.iter().enumerate() { + if i != 0 { + f.write_str(", ")?; + } + + match entry.key { + MapKey::Str(s) if is_valid_bare_key(s) => f.write_str(s)?, + _ => PrintExpr(&entry.key.clone().into()).fmt(f)?, + } + f.write_str(" = ")?; + PrintExpr(&entry.value).fmt(f)?; + } + f.write_str(" }")?; + Ok(()) + }, + + // All these other types can simply be serialized as is. + Expr::Str(_) | Expr::Float(_) | Expr::Integer(_) | Expr::Bool(_) | Expr::Array(_) => { + toml::to_string(&self.0) + .expect("string serialization to TOML failed") + .fmt(f) + } + } } } +fn is_valid_bare_key(s: &str) -> bool { + s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') +} + #[cfg(test)] mod tests { use crate::test_utils::{self, include_format_output}; diff --git a/src/yaml.rs b/src/yaml.rs index 86501b8..25b2ef9 100644 --- a/src/yaml.rs +++ b/src/yaml.rs @@ -121,7 +121,7 @@ impl YamlFormatter { } impl Formatter for YamlFormatter { - type ExprPrinter = PrintExpr; + type ExprPrinter = PrintExpr<'static>; fn buffer(&mut self) -> &mut String { &mut self.buffer @@ -161,15 +161,15 @@ impl Formatter for YamlFormatter { } /// Helper to emit `meta::Expr` into YAML. -struct PrintExpr(&'static Expr); +struct PrintExpr<'a>(&'a Expr); -impl From<&'static Expr> for PrintExpr { +impl From<&'static Expr> for PrintExpr<'static> { fn from(expr: &'static Expr) -> Self { Self(expr) } } -impl fmt::Display for PrintExpr { +impl fmt::Display for PrintExpr<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self.0 { // We have to special case arrays as the normal formatter only emits @@ -187,6 +187,21 @@ impl fmt::Display for PrintExpr { Ok(()) } + Expr::Map(entries) => { + // TODO: pretty printing of long arrays onto multiple lines? + f.write_str("{ ")?; + for (i, entry) in entries.iter().enumerate() { + if i != 0 { + f.write_str(", ")?; + } + PrintExpr(&entry.key.clone().into()).fmt(f)?; + f.write_str(": ")?; + PrintExpr(&entry.value).fmt(f)?; + } + f.write_str(" }")?; + Ok(()) + }, + // All these other types can simply be serialized as is. Expr::Str(_) | Expr::Float(_) | Expr::Integer(_) | Expr::Bool(_) => { let out = serde_yaml::to_string(&self.0).expect("string serialization to YAML failed"); diff --git a/tests/array_default.rs b/tests/array_default.rs index e0c8576..6170a53 100644 --- a/tests/array_default.rs +++ b/tests/array_default.rs @@ -92,4 +92,3 @@ fn inferred_type() { assert_eq!(def.parens, vec![1.0, 2.0]); assert_eq!(def.fallback, std::time::Duration::new(13, 27)); } - diff --git a/tests/format-output/1-default.json5 b/tests/format-output/1-default.json5 index 819f99d..de32c8f 100644 --- a/tests/format-output/1-default.json5 +++ b/tests/format-output/1-default.json5 @@ -35,6 +35,11 @@ // // Default value: ["content-type","content-encoding"] //allowed: ["content-type","content-encoding"], + + // Assigns a score to some headers. + // + // Default value: {"cookie":1.5,"server":12.7} + //score: {"cookie":1.5,"server":12.7}, }, }, diff --git a/tests/format-output/1-default.toml b/tests/format-output/1-default.toml index fdaf269..d6fee19 100644 --- a/tests/format-output/1-default.toml +++ b/tests/format-output/1-default.toml @@ -36,6 +36,11 @@ # Default value: ["content-type", "content-encoding"] #allowed = ["content-type", "content-encoding"] +# Assigns a score to some headers. +# +# Default value: { cookie = 1.5, server = 12.7 } +#score = { cookie = 1.5, server = 12.7 } + # Configuring the logging. [log] # If set to `true`, the app will log to stdout. diff --git a/tests/format-output/1-default.yaml b/tests/format-output/1-default.yaml index 666cfa3..b4e42b8 100644 --- a/tests/format-output/1-default.yaml +++ b/tests/format-output/1-default.yaml @@ -36,6 +36,11 @@ http: # Default value: [content-type, content-encoding] #allowed: [content-type, content-encoding] + # Assigns a score to some headers. + # + # Default value: { cookie: 1.5, server: 12.7 } + #score: { cookie: 1.5, server: 12.7 } + # Configuring the logging. log: # If set to `true`, the app will log to stdout. diff --git a/tests/format-output/1-indent-2.toml b/tests/format-output/1-indent-2.toml index 72739b6..d5871b7 100644 --- a/tests/format-output/1-indent-2.toml +++ b/tests/format-output/1-indent-2.toml @@ -36,6 +36,11 @@ # Default value: ["content-type", "content-encoding"] #allowed = ["content-type", "content-encoding"] + # Assigns a score to some headers. + # + # Default value: { cookie = 1.5, server = 12.7 } + #score = { cookie = 1.5, server = 12.7 } + # Configuring the logging. [log] # If set to `true`, the app will log to stdout. diff --git a/tests/format-output/1-nested-gap-2.toml b/tests/format-output/1-nested-gap-2.toml index 3865dca..b93a084 100644 --- a/tests/format-output/1-nested-gap-2.toml +++ b/tests/format-output/1-nested-gap-2.toml @@ -38,6 +38,11 @@ # Default value: ["content-type", "content-encoding"] #allowed = ["content-type", "content-encoding"] +# Assigns a score to some headers. +# +# Default value: { cookie = 1.5, server = 12.7 } +#score = { cookie = 1.5, server = 12.7 } + # Configuring the logging. [log] diff --git a/tests/format-output/1-no-comments.json5 b/tests/format-output/1-no-comments.json5 index 5ef0a5e..878bd4f 100644 --- a/tests/format-output/1-no-comments.json5 +++ b/tests/format-output/1-no-comments.json5 @@ -9,6 +9,7 @@ //username: "x-username", //display_name: "x-display-name", //allowed: ["content-type","content-encoding"], + //score: {"cookie":1.5,"server":12.7}, }, }, diff --git a/tests/format-output/1-no-comments.toml b/tests/format-output/1-no-comments.toml index 7947964..cae52fa 100644 --- a/tests/format-output/1-no-comments.toml +++ b/tests/format-output/1-no-comments.toml @@ -8,6 +8,7 @@ #username = "x-username" #display_name = "x-display-name" #allowed = ["content-type", "content-encoding"] +#score = { cookie = 1.5, server = 12.7 } [log] #stdout = true diff --git a/tests/format-output/1-no-comments.yaml b/tests/format-output/1-no-comments.yaml index dab951e..88de832 100644 --- a/tests/format-output/1-no-comments.yaml +++ b/tests/format-output/1-no-comments.yaml @@ -8,6 +8,7 @@ http: #username: x-username #display_name: x-display-name #allowed: [content-type, content-encoding] + #score: { cookie: 1.5, server: 12.7 } log: #stdout: true diff --git a/tests/general.rs b/tests/general.rs index 2e2a771..6df5ae1 100644 --- a/tests/general.rs +++ b/tests/general.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, net::IpAddr}; +use std::{path::PathBuf, net::IpAddr, collections::HashMap}; use pretty_assertions::assert_eq; use confique::{Config, meta, Partial}; @@ -279,3 +279,18 @@ pub(crate) fn deserialize_dummy<'de, D>(deserializer: D) -> Result, + + #[config(default = {})] + dog: HashMap, + } +} diff --git a/tests/map_default.rs b/tests/map_default.rs new file mode 100644 index 0000000..c18128d --- /dev/null +++ b/tests/map_default.rs @@ -0,0 +1,44 @@ +use std::collections::HashMap; + +use pretty_assertions::assert_eq; +use confique::{Config, meta}; + + +#[test] +fn string_to_u32() { + #[derive(Config)] + struct Foo { + /// A nice doc comment. + #[config(default = { "peter": 3, "anna": 27 })] + bar: HashMap, + } + + assert_eq!(Foo::META, meta::Meta { + name: "Foo", + doc: &[], + fields: &[ + meta::Field { + name: "bar", + doc: &[" A nice doc comment."], + kind: meta::FieldKind::Leaf { + env: None, + kind: meta::LeafKind::Required { + default: Some(meta::Expr::Map(&[ + meta::MapEntry { + key: meta::MapKey::Str("peter"), + value: meta::Expr::Integer(meta::Integer::U32(3)), + }, + meta::MapEntry { + key: meta::MapKey::Str("anna"), + value: meta::Expr::Integer(meta::Integer::U32(27)), + }, + ])), + }, + }, + }, + ], + }); + + let def = Foo::builder().load().unwrap(); + assert_eq!(def.bar, HashMap::from([("peter".into(), 3), ("anna".into(), 27)])); +}