From 7f8136a8bd0805a5af618952790a5ac818a413bf Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 15 Jun 2025 15:06:39 -0700 Subject: [PATCH 01/33] Add args and crate. --- CHANGELOG.md | 8 ++ Cargo.lock | 33 +++++++- crates/macros-next/Cargo.toml | 30 +++++++ crates/macros-next/src/args.rs | 107 ++++++++++++++++++++++++ crates/macros-next/src/lib.rs | 43 ++++++++++ crates/macros-next/src/schematic/mod.rs | 0 6 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 crates/macros-next/Cargo.toml create mode 100644 crates/macros-next/src/args.rs create mode 100644 crates/macros-next/src/lib.rs create mode 100644 crates/macros-next/src/schematic/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b801f07d..8e6a3b0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Next + +#### 🚀 Updates + +- Added support for explicit Serde deserialize and serialize renaming on containers and fields: + `#[serde(rename(deserialize = "de_name", serialize = "ser_name"))]`. +- Updated Serde `alias` to support multiple aliases: `#[serde(alias = "alias1", alias = "alias2")]` + ## 0.19.7 #### ⚙️ Internal diff --git a/Cargo.lock b/Cargo.lock index d3a6833f..533e89bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -394,8 +394,22 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.21.0", + "darling_macro 0.21.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.104", ] [[package]] @@ -417,7 +431,7 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.21.0", "quote", "syn 2.0.114", ] @@ -1936,7 +1950,18 @@ name = "schematic_macros" version = "0.19.4" dependencies = [ "convert_case", - "darling", + "darling 0.21.0", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "schematic_macros_next" +version = "0.18.7" +dependencies = [ + "convert_case", + "darling 0.20.11", "proc-macro2", "quote", "syn 2.0.114", diff --git a/crates/macros-next/Cargo.toml b/crates/macros-next/Cargo.toml new file mode 100644 index 00000000..cc5fb7f5 --- /dev/null +++ b/crates/macros-next/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "schematic_macros_next" +version = "0.18.7" +edition = "2024" +license = "MIT" +description = "Macros for the schematic crate." +homepage = "https://moonrepo.github.io/schematic" +repository = "https://github.com/moonrepo/schematic" + +[package.metadata.docs.rs] +all-features = true + +[lib] +proc-macro = true + +[dependencies] +convert_case = "0.8.0" +darling = "0.20.11" +proc-macro2 = "1.0.95" +quote = "1.0.40" +syn = { version = "2.0.101", features = ["full"] } + +[features] +default = ["schema"] +config = [] +env = [] +extends = [] +schema = [] +tracing = [] +validate = [] diff --git a/crates/macros-next/src/args.rs b/crates/macros-next/src/args.rs new file mode 100644 index 00000000..7c80d907 --- /dev/null +++ b/crates/macros-next/src/args.rs @@ -0,0 +1,107 @@ +use darling::ast::NestedMeta; +use darling::{FromAttributes, FromDeriveInput, FromMeta}; +use proc_macro2::{Ident, TokenStream}; +use quote::{ToTokens, quote}; +use syn::{Attribute, Data, DeriveInput, ExprPath, Fields}; + +#[derive(Clone, Copy)] +pub enum SerdeIoDirection { + From, // read / de + To, // write / ser +} + +#[derive(Clone)] +pub enum SerdeTagFormat { + Untagged, + External, + Internal(String), + Adjacent(String, String), + // Special case for unit only enums + Unit, +} + +// #[serde(rename(deserialize = "de_name", serialize = "ser_name"))] +#[derive(FromMeta)] +pub enum SerdeRenameField { + Both(String), + Either { + deserialize: Option, + serialize: Option, + }, +} + +impl SerdeRenameField { + pub fn get_name(&self, dir: SerdeIoDirection) -> Option<&str> { + match self { + Self::Both(inner) => Some(inner.as_str()), + Self::Either { + deserialize, + serialize, + } => match dir { + SerdeIoDirection::From => deserialize.as_deref(), + SerdeIoDirection::To => serialize.as_deref(), + }, + } + } +} + +// #[serde()] +#[derive(Default, FromDeriveInput)] +#[darling(default, allow_unknown_fields, attributes(serde))] +pub struct SerdeContainerArgs { + pub default: bool, + pub deny_unknown_fields: bool, + + // struct + pub rename: Option, + pub rename_all: Option, + pub rename_all_fields: Option, + + // enum + pub content: Option, + pub expecting: Option, + pub tag: Option, + pub untagged: bool, +} + +// #[serde()] +#[derive(Default, FromAttributes)] +#[darling(default, allow_unknown_fields, attributes(serde))] +pub struct SerdeFieldArgs { + #[darling(multiple)] + pub alias: Vec, + pub default: bool, + pub flatten: bool, + pub rename: Option, + pub skip: bool, + pub skip_deserializing: bool, + pub skip_deserializing_if: Option, + pub skip_serializing: bool, + pub skip_serializing_if: Option, + + // variant + pub other: bool, + pub untagged: bool, +} + +// // #[config()], #[schematic()] +// #[derive(FromDeriveInput, Default)] +// #[darling( +// default, +// attributes(config, schematic), +// supports(struct_named, enum_any) +// )] +// pub struct MacroArgs { +// // config +// pub allow_unknown_fields: bool, +// pub context: Option, +// pub partial: PartialAttr, +// #[cfg(feature = "env")] +// pub env_prefix: Option, + +// // serde +// pub rename: Option, +// pub rename_all: Option, +// pub rename_all_fields: Option, +// pub serde: SerdeMeta, +// } diff --git a/crates/macros-next/src/lib.rs b/crates/macros-next/src/lib.rs new file mode 100644 index 00000000..5624cde1 --- /dev/null +++ b/crates/macros-next/src/lib.rs @@ -0,0 +1,43 @@ +mod args; + +// mod common; +// mod utils; + +#[cfg(feature = "config")] +mod config; +#[cfg(feature = "config")] +mod config_enum; +#[cfg(feature = "schema")] +mod schematic; + +// use common::Macro; +use proc_macro::TokenStream; +use quote::quote; +use syn::{DeriveInput, parse_macro_input}; + +// #[derive(Config)] +#[cfg(feature = "config")] +#[proc_macro_derive(Config, attributes(config, setting))] +pub fn config(item: TokenStream) -> TokenStream { + let input: DeriveInput = parse_macro_input!(item); + let output = config::ConfigMacro(Macro::from(&input)); + + quote! { #output }.into() +} + +// #[derive(ConfigEnum)] +#[cfg(feature = "config")] +#[proc_macro_derive(ConfigEnum, attributes(config, variant))] +pub fn config_enum(item: TokenStream) -> TokenStream { + config_enum::macro_impl(item) +} + +// #[derive(Schematic)] +#[cfg(feature = "schema")] +#[proc_macro_derive(Schematic, attributes(schematic, schema))] +pub fn schematic(item: TokenStream) -> TokenStream { + let input: DeriveInput = parse_macro_input!(item); + let output = schematic::SchematicMacro(Macro::from(&input)); + + quote! { #output }.into() +} diff --git a/crates/macros-next/src/schematic/mod.rs b/crates/macros-next/src/schematic/mod.rs new file mode 100644 index 00000000..e69de29b From a087438c4c0598a30f9bf319d571d1c5ba40ca41 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 15 Jun 2025 22:35:26 -0700 Subject: [PATCH 02/33] Start on pieces. --- crates/macros-next/src/args.rs | 224 +++++++++++++++++++++++++--- crates/macros-next/src/container.rs | 109 ++++++++++++++ crates/macros-next/src/field.rs | 52 +++++++ crates/macros-next/src/lib.rs | 19 ++- crates/macros-next/src/variant.rs | 44 ++++++ 5 files changed, 417 insertions(+), 31 deletions(-) create mode 100644 crates/macros-next/src/container.rs create mode 100644 crates/macros-next/src/field.rs create mode 100644 crates/macros-next/src/variant.rs diff --git a/crates/macros-next/src/args.rs b/crates/macros-next/src/args.rs index 7c80d907..eb33cd22 100644 --- a/crates/macros-next/src/args.rs +++ b/crates/macros-next/src/args.rs @@ -6,8 +6,8 @@ use syn::{Attribute, Data, DeriveInput, ExprPath, Fields}; #[derive(Clone, Copy)] pub enum SerdeIoDirection { - From, // read / de - To, // write / ser + From, // read / deserialize + To, // write / serialize } #[derive(Clone)] @@ -20,27 +20,48 @@ pub enum SerdeTagFormat { Unit, } +// #[serde(rename = "name")] // #[serde(rename(deserialize = "de_name", serialize = "ser_name"))] -#[derive(FromMeta)] -pub enum SerdeRenameField { - Both(String), - Either { - deserialize: Option, - serialize: Option, - }, +#[derive(Debug, Default, PartialEq)] +pub struct SerdeRenameArg { + deserialize: Option, + serialize: Option, } -impl SerdeRenameField { +impl FromMeta for SerdeRenameArg { + fn from_string(value: &str) -> darling::Result { + Ok(Self { + deserialize: Some(value.into()), + serialize: Some(value.into()), + }) + } + + fn from_list(items: &[NestedMeta]) -> darling::Result { + #[derive(Default, FromMeta)] + #[darling(default)] + struct Rename { + deserialize: Option, + serialize: Option, + } + + impl From for SerdeRenameArg { + fn from(value: Rename) -> Self { + Self { + deserialize: value.deserialize, + serialize: value.serialize, + } + } + } + + Rename::from_list(items).map(SerdeRenameArg::from) + } +} + +impl SerdeRenameArg { pub fn get_name(&self, dir: SerdeIoDirection) -> Option<&str> { - match self { - Self::Both(inner) => Some(inner.as_str()), - Self::Either { - deserialize, - serialize, - } => match dir { - SerdeIoDirection::From => deserialize.as_deref(), - SerdeIoDirection::To => serialize.as_deref(), - }, + match dir { + SerdeIoDirection::From => self.deserialize.as_deref(), + SerdeIoDirection::To => self.serialize.as_deref(), } } } @@ -53,9 +74,9 @@ pub struct SerdeContainerArgs { pub deny_unknown_fields: bool, // struct - pub rename: Option, - pub rename_all: Option, - pub rename_all_fields: Option, + pub rename: Option, + pub rename_all: Option, + pub rename_all_fields: Option, // enum pub content: Option, @@ -72,7 +93,7 @@ pub struct SerdeFieldArgs { pub alias: Vec, pub default: bool, pub flatten: bool, - pub rename: Option, + pub rename: Option, pub skip: bool, pub skip_deserializing: bool, pub skip_deserializing_if: Option, @@ -105,3 +126,160 @@ pub struct SerdeFieldArgs { // pub rename_all_fields: Option, // pub serde: SerdeMeta, // } + +#[cfg(test)] +mod tests { + use super::*; + use darling::FromMeta; + use syn::parse_quote; + + mod serde_rename_arg { + use super::*; + + #[test] + fn both_value_string() { + let meta = SerdeRenameArg::from_string("name").unwrap(); + + assert_eq!( + meta, + SerdeRenameArg { + deserialize: Some("name".into()), + serialize: Some("name".into()), + } + ); + } + + #[test] + fn both_value() { + let meta = SerdeRenameArg::from_list(&[ + parse_quote! { + deserialize = "de_name" + }, + parse_quote! { + serialize = "ser_name" + }, + ]) + .unwrap(); + + assert_eq!( + meta, + SerdeRenameArg { + deserialize: Some("de_name".into()), + serialize: Some("ser_name".into()), + } + ); + } + + #[test] + fn de_value() { + let meta = SerdeRenameArg::from_list(&[parse_quote! { + deserialize = "de_name" + }]) + .unwrap(); + + assert_eq!( + meta, + SerdeRenameArg { + deserialize: Some("de_name".into()), + serialize: None, + } + ); + } + + #[test] + fn ser_value() { + let meta = SerdeRenameArg::from_list(&[parse_quote! { + serialize = "ser_name" + }]) + .unwrap(); + + assert_eq!( + meta, + SerdeRenameArg { + deserialize: None, + serialize: Some("ser_name".into()), + } + ); + } + } + + mod serde_container { + use super::*; + + #[test] + fn normal_args() { + let container = SerdeContainerArgs::from_derive_input(&parse_quote! { + #[serde(default, deny_unknown_fields)] + struct Example; + }) + .unwrap(); + + assert!(container.default); + assert!(container.deny_unknown_fields); + } + + #[test] + fn enum_tagged_args() { + let container = SerdeContainerArgs::from_derive_input(&parse_quote! { + #[serde(tag = "tag", content = "content")] + struct Example; + }) + .unwrap(); + + assert_eq!(container.content.unwrap(), "content"); + assert_eq!(container.tag.unwrap(), "tag"); + assert!(container.expecting.is_none()); + assert!(!container.untagged); + } + + #[test] + fn enum_untagged_args() { + let container = SerdeContainerArgs::from_derive_input(&parse_quote! { + #[serde(untagged, expecting = "expecting")] + struct Example; + }) + .unwrap(); + + assert!(container.content.is_none()); + assert!(container.tag.is_none()); + assert_eq!(container.expecting.unwrap(), "expecting"); + assert!(container.untagged); + } + + #[test] + fn rename_args() { + let container = SerdeContainerArgs::from_derive_input(&parse_quote! { + #[serde( + rename = "name", + rename_all(deserialize = "de_name"), + rename_all_fields(serialize = "ser_name") + )] + struct Example; + }) + .unwrap(); + + assert_eq!( + container.rename.unwrap(), + SerdeRenameArg { + deserialize: Some("name".into()), + serialize: Some("name".into()), + } + ); + assert_eq!( + container.rename_all.unwrap(), + SerdeRenameArg { + deserialize: Some("de_name".into()), + serialize: None, + } + ); + + assert_eq!( + container.rename_all_fields.unwrap(), + SerdeRenameArg { + deserialize: None, + serialize: Some("ser_name".into()), + } + ); + } + } +} diff --git a/crates/macros-next/src/container.rs b/crates/macros-next/src/container.rs new file mode 100644 index 00000000..1b01d90b --- /dev/null +++ b/crates/macros-next/src/container.rs @@ -0,0 +1,109 @@ +use crate::args::{SerdeContainerArgs, SerdeRenameArg}; +use crate::field::Field; +use crate::variant::Variant; +use darling::FromDeriveInput; +use std::rc::Rc; +use syn::{Attribute, Data, DeriveInput, ExprPath, Fields, Ident, Visibility}; + +// #[config()], #[schematic()] +#[derive(Default, FromDeriveInput)] +#[darling( + default, + attributes(config, schematic), + supports(struct_named, enum_any) +)] +pub struct ContainerArgs { + // config + pub allow_unknown_fields: bool, + pub context: Option, + // pub partial: PartialAttr, // TODO + #[cfg(feature = "env")] + pub env_prefix: Option, + + // serde + pub rename: Option, + pub rename_all: Option, + pub rename_all_fields: Option, + // pub serde: SerdeMeta, // TODO +} + +pub struct Container { + pub args: Rc, + pub inner: ContainerInner, + pub serde_args: Rc, + + // inherited + pub attrs: Vec, + pub ident: Ident, + pub vis: Visibility, +} + +impl Container { + pub fn from(input: DeriveInput) -> Self { + let args = Rc::new(ContainerArgs::from_derive_input(&input).unwrap_or_default()); + let serde_args = Rc::new(SerdeContainerArgs::from_derive_input(&input).unwrap_or_default()); + + let inner = match input.data { + Data::Struct(data) => match data.fields { + Fields::Named(fields) => ContainerInner::NamedStruct { + fields: fields + .named + .into_iter() + .map(|data| Field::new(data, args.clone(), serde_args.clone())) + .collect(), + }, + Fields::Unnamed(fields) => ContainerInner::UnnamedStruct { + fields: fields + .unnamed + .into_iter() + .enumerate() + .map(|(index, data)| { + let mut field = Field::new(data, args.clone(), serde_args.clone()); + field.index = index; + field + }) + .collect(), + }, + Fields::Unit => { + panic!("Unit structs are not supported."); + } + }, + Data::Enum(data) => { + let all_unit = data + .variants + .iter() + .all(|variant| matches!(variant.fields, Fields::Unit)); + let variants = data + .variants + .into_iter() + .map(|data| Variant::new(data, args.clone(), serde_args.clone())) + .collect::>(); + + if all_unit { + ContainerInner::UnitEnum { variants } + } else { + ContainerInner::Enum { variants } + } + } + Data::Union(_) => { + panic!("Unions are not supported."); + } + }; + + Self { + args, + attrs: input.attrs, + ident: input.ident, + inner, + serde_args, + vis: input.vis, + } + } +} + +pub enum ContainerInner { + NamedStruct { fields: Vec }, + UnnamedStruct { fields: Vec }, + Enum { variants: Vec }, + UnitEnum { variants: Vec }, +} diff --git a/crates/macros-next/src/field.rs b/crates/macros-next/src/field.rs new file mode 100644 index 00000000..4fa17447 --- /dev/null +++ b/crates/macros-next/src/field.rs @@ -0,0 +1,52 @@ +use crate::args::{SerdeContainerArgs, SerdeFieldArgs}; +use crate::container::ContainerArgs; +use darling::FromAttributes; +use std::rc::Rc; +use syn::{Attribute, Field as NativeField, FieldMutability, Ident, Type, Visibility}; + +// #[schema()], #[setting()] +#[derive(FromAttributes, Default)] +#[darling(default, attributes(schema, setting))] +pub struct FieldArgs {} + +pub struct Field { + // args + pub args: FieldArgs, + pub container_args: Rc, + pub serde_args: SerdeFieldArgs, + pub serde_container_args: Rc, + + // inherited + pub attrs: Vec, + pub ident: Option, // Named + pub index: usize, // Unnamed + pub mutability: FieldMutability, + pub ty: Type, + pub vis: Visibility, + // data + // pub value_type: FieldValue<'l>, +} + +impl Field { + pub fn new( + field: NativeField, + container_args: Rc, + serde_container_args: Rc, + ) -> Field { + let args = FieldArgs::from_attributes(&field.attrs).unwrap_or_default(); + let serde_args = SerdeFieldArgs::from_attributes(&field.attrs).unwrap_or_default(); + + Field { + args, + attrs: field.attrs, + container_args, + ident: field.ident, + index: 0, + mutability: field.mutability, + serde_args, + serde_container_args, + ty: field.ty, + vis: field.vis, + } + } +} diff --git a/crates/macros-next/src/lib.rs b/crates/macros-next/src/lib.rs index 5624cde1..ef446386 100644 --- a/crates/macros-next/src/lib.rs +++ b/crates/macros-next/src/lib.rs @@ -1,4 +1,7 @@ mod args; +mod container; +mod field; +mod variant; // mod common; // mod utils; @@ -33,11 +36,11 @@ pub fn config_enum(item: TokenStream) -> TokenStream { } // #[derive(Schematic)] -#[cfg(feature = "schema")] -#[proc_macro_derive(Schematic, attributes(schematic, schema))] -pub fn schematic(item: TokenStream) -> TokenStream { - let input: DeriveInput = parse_macro_input!(item); - let output = schematic::SchematicMacro(Macro::from(&input)); - - quote! { #output }.into() -} +// #[cfg(feature = "schema")] +// #[proc_macro_derive(Schematic, attributes(schematic, schema))] +// pub fn schematic(item: TokenStream) -> TokenStream { +// let input: DeriveInput = parse_macro_input!(item); +// let output = schematic::SchematicMacro(Macro::from(&input)); + +// quote! { #output }.into() +// } diff --git a/crates/macros-next/src/variant.rs b/crates/macros-next/src/variant.rs new file mode 100644 index 00000000..846d4609 --- /dev/null +++ b/crates/macros-next/src/variant.rs @@ -0,0 +1,44 @@ +use crate::args::{SerdeContainerArgs, SerdeFieldArgs}; +use crate::container::ContainerArgs; +use darling::FromAttributes; +use std::rc::Rc; +use syn::{Attribute, FieldMutability, Fields, Ident, Type, Variant as NativeVariant, Visibility}; + +// #[setting()], #[schema()] +#[derive(FromAttributes, Default)] +#[darling(default, attributes(setting, schema))] +pub struct VariantArgs {} + +pub struct Variant { + // args + pub args: VariantArgs, + pub container_args: Rc, + pub serde_args: SerdeFieldArgs, + pub serde_container_args: Rc, + + // inherited + pub attrs: Vec, + pub ident: Ident, + pub value: Fields, +} + +impl Variant { + pub fn new( + variant: NativeVariant, + container_args: Rc, + serde_container_args: Rc, + ) -> Variant { + let args = VariantArgs::from_attributes(&variant.attrs).unwrap_or_default(); + let serde_args = SerdeFieldArgs::from_attributes(&variant.attrs).unwrap_or_default(); + + Variant { + args, + attrs: variant.attrs, + container_args, + ident: variant.ident, + serde_args, + serde_container_args, + value: variant.fields, + } + } +} From 5c308035375c575cfc1f14a350d13968fcc73992 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 15 Jun 2025 22:52:41 -0700 Subject: [PATCH 03/33] Add serde meta. --- CHANGELOG.md | 4 +++ crates/macros-next/src/args.rs | 23 +++++++++--- crates/macros-next/src/container.rs | 56 ++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e6a3b0a..ebe0d241 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Next +#### 💥 Breaking + +- Removed `#[config(serde(...))]` on containers. Use `#[serde(...)]` instead. + #### 🚀 Updates - Added support for explicit Serde deserialize and serialize renaming on containers and fields: diff --git a/crates/macros-next/src/args.rs b/crates/macros-next/src/args.rs index eb33cd22..6c51093c 100644 --- a/crates/macros-next/src/args.rs +++ b/crates/macros-next/src/args.rs @@ -58,10 +58,25 @@ impl FromMeta for SerdeRenameArg { } impl SerdeRenameArg { - pub fn get_name(&self, dir: SerdeIoDirection) -> Option<&str> { - match dir { - SerdeIoDirection::From => self.deserialize.as_deref(), - SerdeIoDirection::To => self.serialize.as_deref(), + // pub fn get_name(&self, dir: SerdeIoDirection) -> Option<&str> { + // match dir { + // SerdeIoDirection::From => self.deserialize.as_deref(), + // SerdeIoDirection::To => self.serialize.as_deref(), + // } + // } + + pub fn get_meta(&self, key: &str) -> TokenStream { + match (self.deserialize.as_deref(), self.serialize.as_deref()) { + (Some(de), Some(ser)) => { + if de == ser { + quote! { #key = #de } + } else { + quote! { #key(deserialize = #de, serialize = #ser) } + } + } + (None, Some(ser)) => quote! { #key(serialize = #ser) }, + (Some(de), None) => quote! { #key(deserialize = #de) }, + _ => quote! {}, } } } diff --git a/crates/macros-next/src/container.rs b/crates/macros-next/src/container.rs index 1b01d90b..8cb9670c 100644 --- a/crates/macros-next/src/container.rs +++ b/crates/macros-next/src/container.rs @@ -2,6 +2,8 @@ use crate::args::{SerdeContainerArgs, SerdeRenameArg}; use crate::field::Field; use crate::variant::Variant; use darling::FromDeriveInput; +use proc_macro2::TokenStream; +use quote::quote; use std::rc::Rc; use syn::{Attribute, Data, DeriveInput, ExprPath, Fields, Ident, Visibility}; @@ -24,7 +26,6 @@ pub struct ContainerArgs { pub rename: Option, pub rename_all: Option, pub rename_all_fields: Option, - // pub serde: SerdeMeta, // TODO } pub struct Container { @@ -99,6 +100,59 @@ impl Container { vis: input.vis, } } + + pub fn get_partial_serde_meta(&self) -> TokenStream { + let mut meta = vec![]; + + match &self.inner { + ContainerInner::NamedStruct { .. } => { + meta.push(quote! { default }); + + if self.serde_args.deny_unknown_fields || !self.args.allow_unknown_fields { + meta.push(quote! { deny_unknown_fields }); + } + } + ContainerInner::UnnamedStruct { .. } => { + meta.push(quote! { default }); + } + ContainerInner::Enum { .. } => { + if let Some(tag) = &self.serde_args.tag { + meta.push(quote! { tag = #tag }); + } + + if let Some(content) = &self.serde_args.content { + meta.push(quote! { content = #content }); + } + + if self.serde_args.untagged { + meta.push(quote! { untagged }); + } + } + ContainerInner::UnitEnum { .. } => { + meta.push(quote! { untagged }); + } + }; + + if let Some(expecting) = &self.serde_args.expecting { + meta.push(quote! { expecting = #expecting }); + } + + if let Some(rename) = &self.serde_args.rename { + meta.push(rename.get_meta("rename")); + } + + if let Some(rename_all) = &self.serde_args.rename_all { + meta.push(rename_all.get_meta("rename_all")); + } + + if let Some(rename_all_fields) = &self.serde_args.rename_all_fields { + meta.push(rename_all_fields.get_meta("rename_all_fields")); + } + + quote! { + #(#meta),* + } + } } pub enum ContainerInner { From 2508047eff3918ba389fa5321c8d353d7e4140fe Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Mon, 16 Jun 2025 15:23:59 -0700 Subject: [PATCH 04/33] Create core crate. --- Cargo.lock | 40 ++- Cargo.toml | 4 + crates/core/Cargo.toml | 39 +++ crates/core/src/args.rs | 143 +++++++++ crates/{macros-next => core}/src/container.rs | 32 +- crates/{macros-next => core}/src/field.rs | 3 +- crates/core/src/field_value.rs | 13 + crates/core/src/lib.rs | 13 + .../src/schematic/mod.rs | 0 crates/core/src/utils.rs | 17 + crates/{macros-next => core}/src/variant.rs | 3 +- crates/core/tests/args_test.rs | 153 +++++++++ crates/core/tests/container_test.rs | 135 ++++++++ crates/macros-next/Cargo.toml | 8 +- crates/macros-next/src/args.rs | 300 ------------------ crates/macros-next/src/lib.rs | 35 +- 16 files changed, 586 insertions(+), 352 deletions(-) create mode 100644 crates/core/Cargo.toml create mode 100644 crates/core/src/args.rs rename crates/{macros-next => core}/src/container.rs (86%) rename crates/{macros-next => core}/src/field.rs (96%) create mode 100644 crates/core/src/field_value.rs create mode 100644 crates/core/src/lib.rs rename crates/{macros-next => core}/src/schematic/mod.rs (100%) create mode 100644 crates/core/src/utils.rs rename crates/{macros-next => core}/src/variant.rs (95%) create mode 100644 crates/core/tests/args_test.rs create mode 100644 crates/core/tests/container_test.rs delete mode 100644 crates/macros-next/src/args.rs diff --git a/Cargo.lock b/Cargo.lock index 533e89bb..630d5a40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -394,22 +394,8 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core 0.21.0", - "darling_macro 0.21.0", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.104", + "darling_core", + "darling_macro", ] [[package]] @@ -431,7 +417,7 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core 0.21.0", + "darling_core", "quote", "syn 2.0.114", ] @@ -1945,25 +1931,35 @@ dependencies = [ "uuid", ] +[[package]] +name = "schematic_core" +version = "0.18.7" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "schematic_core", + "starbase_sandbox", + "syn 2.0.114", +] + [[package]] name = "schematic_macros" version = "0.19.4" dependencies = [ "convert_case", - "darling 0.21.0", + "darling", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.114", ] [[package]] name = "schematic_macros_next" version = "0.18.7" dependencies = [ - "convert_case", - "darling 0.20.11", - "proc-macro2", "quote", + "schematic_core", "syn 2.0.114", ] diff --git a/Cargo.toml b/Cargo.toml index 2b8fc253..54a119da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,8 +5,11 @@ members = ["crates/*"] [workspace.dependencies] chrono = "0.4.42" convert_case = "0.10.0" +darling = "0.23.0" indexmap = "2.13.0" miette = "7.6.0" +proc-macro2 = "1.0.95" +quote = "1.0.40" regex = "1.12.2" relative-path = "2.0.1" reqwest = { version = "0.13.1", default-features = false } @@ -19,6 +22,7 @@ serde_json = "1.0.149" serde_yaml = "0.9.33" serde_norway = "0.9.42" starbase_sandbox = "0.10.7" +syn = "2.0.101" toml = "1.1.2" tracing = "0.1.44" url = "2.5.8" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml new file mode 100644 index 00000000..813207f3 --- /dev/null +++ b/crates/core/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "schematic_core" +version = "0.18.7" +edition = "2024" +license = "MIT" +description = "Core building blocks for schematic macros." +homepage = "https://moonrepo.github.io/schematic" +repository = "https://github.com/moonrepo/schematic" + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +# convert_case = { workspace = true } +darling = { workspace = true } +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true, features = ["full"] } + +[dev-dependencies] +schematic_core = { path = ".", features = [ + "config", + "env", + "extends", + "schema", + "tracing", + "validate", +] } +syn = { workspace = true, features = ["full", "extra-traits"] } +starbase_sandbox = { workspace = true } + +[features] +default = [] +config = [] +env = [] +extends = [] +schema = [] +tracing = [] +validate = [] diff --git a/crates/core/src/args.rs b/crates/core/src/args.rs new file mode 100644 index 00000000..ed40a72f --- /dev/null +++ b/crates/core/src/args.rs @@ -0,0 +1,143 @@ +use darling::ast::NestedMeta; +use darling::{FromAttributes, FromDeriveInput, FromMeta}; +use proc_macro2::{Ident, TokenStream}; +use quote::{ToTokens, quote}; +use syn::{Attribute, Data, DeriveInput, ExprPath, Fields}; + +#[derive(Clone, Copy, Debug)] +pub enum SerdeIoDirection { + From, // read / deserialize + To, // write / serialize +} + +#[derive(Clone, Debug)] +pub enum SerdeTagFormat { + Untagged, + External, + Internal(String), + Adjacent(String, String), + // Special case for unit only enums + Unit, +} + +// #[serde(rename = "name")] +// #[serde(rename(deserialize = "de_name", serialize = "ser_name"))] +#[derive(Debug, Default, PartialEq)] +pub struct SerdeRenameArg { + pub deserialize: Option, + pub serialize: Option, +} + +impl FromMeta for SerdeRenameArg { + fn from_string(value: &str) -> darling::Result { + Ok(Self { + deserialize: Some(value.into()), + serialize: Some(value.into()), + }) + } + + fn from_list(items: &[NestedMeta]) -> darling::Result { + #[derive(Default, FromMeta)] + #[darling(default)] + struct Rename { + deserialize: Option, + serialize: Option, + } + + impl From for SerdeRenameArg { + fn from(value: Rename) -> Self { + Self { + deserialize: value.deserialize, + serialize: value.serialize, + } + } + } + + Rename::from_list(items).map(SerdeRenameArg::from) + } +} + +impl SerdeRenameArg { + // pub fn get_name(&self, dir: SerdeIoDirection) -> Option<&str> { + // match dir { + // SerdeIoDirection::From => self.deserialize.as_deref(), + // SerdeIoDirection::To => self.serialize.as_deref(), + // } + // } + + pub fn get_meta(&self, key: &str) -> TokenStream { + match (self.deserialize.as_deref(), self.serialize.as_deref()) { + (Some(de), Some(ser)) => { + if de == ser { + quote! { #key = #de } + } else { + quote! { #key(deserialize = #de, serialize = #ser) } + } + } + (None, Some(ser)) => quote! { #key(serialize = #ser) }, + (Some(de), None) => quote! { #key(deserialize = #de) }, + _ => quote! {}, + } + } +} + +// #[serde()] +#[derive(Debug, Default, FromDeriveInput)] +#[darling(default, allow_unknown_fields, attributes(serde))] +pub struct SerdeContainerArgs { + pub default: bool, + pub deny_unknown_fields: bool, + + // struct + pub rename: Option, + pub rename_all: Option, + pub rename_all_fields: Option, + + // enum + pub content: Option, + pub expecting: Option, + pub tag: Option, + pub untagged: bool, +} + +// #[serde()] +#[derive(Debug, Default, FromAttributes)] +#[darling(default, allow_unknown_fields, attributes(serde))] +pub struct SerdeFieldArgs { + #[darling(multiple)] + pub alias: Vec, + pub default: bool, + pub flatten: bool, + pub rename: Option, + pub skip: bool, + pub skip_deserializing: bool, + pub skip_deserializing_if: Option, + pub skip_serializing: bool, + pub skip_serializing_if: Option, + + // variant + pub other: bool, + pub untagged: bool, +} + +// // #[config()], #[schematic()] +// #[derive(FromDeriveInput, Default)] +// #[darling( +// default, +// attributes(config, schematic), +// supports(struct_named, enum_any) +// )] +// pub struct MacroArgs { +// // config +// pub allow_unknown_fields: bool, +// pub context: Option, +// pub partial: PartialAttr, +// #[cfg(feature = "env")] +// pub env_prefix: Option, + +// // serde +// pub rename: Option, +// pub rename_all: Option, +// pub rename_all_fields: Option, +// pub serde: SerdeMeta, +// } diff --git a/crates/macros-next/src/container.rs b/crates/core/src/container.rs similarity index 86% rename from crates/macros-next/src/container.rs rename to crates/core/src/container.rs index 8cb9670c..53d4646e 100644 --- a/crates/macros-next/src/container.rs +++ b/crates/core/src/container.rs @@ -1,14 +1,15 @@ use crate::args::{SerdeContainerArgs, SerdeRenameArg}; use crate::field::Field; +use crate::utils::is_inheritable_attribute; use crate::variant::Variant; use darling::FromDeriveInput; use proc_macro2::TokenStream; -use quote::quote; +use quote::{ToTokens, quote}; use std::rc::Rc; use syn::{Attribute, Data, DeriveInput, ExprPath, Fields, Ident, Visibility}; // #[config()], #[schematic()] -#[derive(Default, FromDeriveInput)] +#[derive(Debug, Default, FromDeriveInput)] #[darling( default, attributes(config, schematic), @@ -28,6 +29,7 @@ pub struct ContainerArgs { pub rename_all_fields: Option, } +#[derive(Debug)] pub struct Container { pub args: Rc, pub inner: ContainerInner, @@ -101,7 +103,24 @@ impl Container { } } - pub fn get_partial_serde_meta(&self) -> TokenStream { + pub fn get_partial_attributes(&self) -> Vec { + let serde_args = self.get_partial_serde_attribute_args(); + let mut attrs = vec![quote! { #[serde(#serde_args) ]}]; + + for attr in &self.attrs { + if is_inheritable_attribute(attr) { + attrs.push(quote! { #attr }); + } + } + + // TODO + // let partial = &self.args.partial; + // attrs.push(quote! { #partial }); + + attrs + } + + pub fn get_partial_serde_attribute_args(&self) -> TokenStream { let mut meta = vec![]; match &self.inner { @@ -155,6 +174,13 @@ impl Container { } } +impl ToTokens for Container { + fn to_tokens(&self, _tokens: &mut TokenStream) { + // TODO + } +} + +#[derive(Debug)] pub enum ContainerInner { NamedStruct { fields: Vec }, UnnamedStruct { fields: Vec }, diff --git a/crates/macros-next/src/field.rs b/crates/core/src/field.rs similarity index 96% rename from crates/macros-next/src/field.rs rename to crates/core/src/field.rs index 4fa17447..def00b82 100644 --- a/crates/macros-next/src/field.rs +++ b/crates/core/src/field.rs @@ -5,10 +5,11 @@ use std::rc::Rc; use syn::{Attribute, Field as NativeField, FieldMutability, Ident, Type, Visibility}; // #[schema()], #[setting()] -#[derive(FromAttributes, Default)] +#[derive(Debug, FromAttributes, Default)] #[darling(default, attributes(schema, setting))] pub struct FieldArgs {} +#[derive(Debug)] pub struct Field { // args pub args: FieldArgs, diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs new file mode 100644 index 00000000..94c571c5 --- /dev/null +++ b/crates/core/src/field_value.rs @@ -0,0 +1,13 @@ +use syn::Type; + +pub enum WrapperType { + Arc, + Box, + Option, + Rc, +} + +pub struct FieldValue { + pub original_ty: Type, + pub wrappers: Vec, +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs new file mode 100644 index 00000000..c153d921 --- /dev/null +++ b/crates/core/src/lib.rs @@ -0,0 +1,13 @@ +pub mod args; +pub mod container; +pub mod field; +pub mod field_value; +pub mod utils; +pub mod variant; + +// #[cfg(feature = "config")] +// pub mod config; +// #[cfg(feature = "config")] +// pub mod config_enum; +#[cfg(feature = "schema")] +pub mod schematic; diff --git a/crates/macros-next/src/schematic/mod.rs b/crates/core/src/schematic/mod.rs similarity index 100% rename from crates/macros-next/src/schematic/mod.rs rename to crates/core/src/schematic/mod.rs diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs new file mode 100644 index 00000000..03d1e0ac --- /dev/null +++ b/crates/core/src/utils.rs @@ -0,0 +1,17 @@ +use syn::{Attribute, Meta, Path}; + +pub fn get_meta_path(meta: &Meta) -> &Path { + match meta { + Meta::Path(path) => path, + Meta::List(list) => &list.path, + Meta::NameValue(nv) => &nv.path, + } +} + +pub fn is_inheritable_attribute(attr: &Attribute) -> bool { + let path = get_meta_path(&attr.meta); + + ["allow", "default", "deprecated", "doc", "warn"] + .into_iter() + .any(|n| path.is_ident(n)) +} diff --git a/crates/macros-next/src/variant.rs b/crates/core/src/variant.rs similarity index 95% rename from crates/macros-next/src/variant.rs rename to crates/core/src/variant.rs index 846d4609..faf23d77 100644 --- a/crates/macros-next/src/variant.rs +++ b/crates/core/src/variant.rs @@ -5,10 +5,11 @@ use std::rc::Rc; use syn::{Attribute, FieldMutability, Fields, Ident, Type, Variant as NativeVariant, Visibility}; // #[setting()], #[schema()] -#[derive(FromAttributes, Default)] +#[derive(Debug, Default, FromAttributes)] #[darling(default, attributes(setting, schema))] pub struct VariantArgs {} +#[derive(Debug)] pub struct Variant { // args pub args: VariantArgs, diff --git a/crates/core/tests/args_test.rs b/crates/core/tests/args_test.rs new file mode 100644 index 00000000..eb54b0eb --- /dev/null +++ b/crates/core/tests/args_test.rs @@ -0,0 +1,153 @@ +use darling::{FromDeriveInput, FromMeta}; +use schematic_core::args::*; +use syn::parse_quote; + +mod serde_rename_arg { + use super::*; + + #[test] + fn both_value_string() { + let meta = SerdeRenameArg::from_string("name").unwrap(); + + assert_eq!( + meta, + SerdeRenameArg { + deserialize: Some("name".into()), + serialize: Some("name".into()), + } + ); + } + + #[test] + fn both_value() { + let meta = SerdeRenameArg::from_list(&[ + parse_quote! { + deserialize = "de_name" + }, + parse_quote! { + serialize = "ser_name" + }, + ]) + .unwrap(); + + assert_eq!( + meta, + SerdeRenameArg { + deserialize: Some("de_name".into()), + serialize: Some("ser_name".into()), + } + ); + } + + #[test] + fn de_value() { + let meta = SerdeRenameArg::from_list(&[parse_quote! { + deserialize = "de_name" + }]) + .unwrap(); + + assert_eq!( + meta, + SerdeRenameArg { + deserialize: Some("de_name".into()), + serialize: None, + } + ); + } + + #[test] + fn ser_value() { + let meta = SerdeRenameArg::from_list(&[parse_quote! { + serialize = "ser_name" + }]) + .unwrap(); + + assert_eq!( + meta, + SerdeRenameArg { + deserialize: None, + serialize: Some("ser_name".into()), + } + ); + } +} + +mod serde_container { + use super::*; + + #[test] + fn normal_args() { + let container = SerdeContainerArgs::from_derive_input(&parse_quote! { + #[serde(default, deny_unknown_fields)] + struct Example; + }) + .unwrap(); + + assert!(container.default); + assert!(container.deny_unknown_fields); + } + + #[test] + fn enum_tagged_args() { + let container = SerdeContainerArgs::from_derive_input(&parse_quote! { + #[serde(tag = "tag", content = "content")] + struct Example; + }) + .unwrap(); + + assert_eq!(container.content.unwrap(), "content"); + assert_eq!(container.tag.unwrap(), "tag"); + assert!(container.expecting.is_none()); + assert!(!container.untagged); + } + + #[test] + fn enum_untagged_args() { + let container = SerdeContainerArgs::from_derive_input(&parse_quote! { + #[serde(untagged, expecting = "expecting")] + struct Example; + }) + .unwrap(); + + assert!(container.content.is_none()); + assert!(container.tag.is_none()); + assert_eq!(container.expecting.unwrap(), "expecting"); + assert!(container.untagged); + } + + #[test] + fn rename_args() { + let container = SerdeContainerArgs::from_derive_input(&parse_quote! { + #[serde( + rename = "name", + rename_all(deserialize = "de_name"), + rename_all_fields(serialize = "ser_name") + )] + struct Example; + }) + .unwrap(); + + assert_eq!( + container.rename.unwrap(), + SerdeRenameArg { + deserialize: Some("name".into()), + serialize: Some("name".into()), + } + ); + assert_eq!( + container.rename_all.unwrap(), + SerdeRenameArg { + deserialize: Some("de_name".into()), + serialize: None, + } + ); + + assert_eq!( + container.rename_all_fields.unwrap(), + SerdeRenameArg { + deserialize: None, + serialize: Some("ser_name".into()), + } + ); + } +} diff --git a/crates/core/tests/container_test.rs b/crates/core/tests/container_test.rs new file mode 100644 index 00000000..a3151482 --- /dev/null +++ b/crates/core/tests/container_test.rs @@ -0,0 +1,135 @@ +use schematic_core::args::*; +use schematic_core::container::Container; +use syn::parse_quote; + +mod container { + use super::*; + + #[test] + fn basic_args() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(allow_unknown_fields, env_prefix = "PREFIX_", context = ExampleContext)] + struct Example {} + }); + + assert!(container.args.allow_unknown_fields); + assert!(container.args.context.is_some()); + assert_eq!(container.args.env_prefix.as_ref().unwrap(), "PREFIX_"); + } + + #[test] + fn rename_args() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config( + rename = "name", + rename_all(deserialize = "de_name"), + rename_all_fields(serialize = "ser_name") + )] + struct Example {} + }); + + assert_eq!( + container.args.rename.as_ref().unwrap(), + &SerdeRenameArg { + deserialize: Some("name".into()), + serialize: Some("name".into()), + } + ); + assert_eq!( + container.args.rename_all.as_ref().unwrap(), + &SerdeRenameArg { + deserialize: Some("de_name".into()), + serialize: None, + } + ); + + assert_eq!( + container.args.rename_all_fields.as_ref().unwrap(), + &SerdeRenameArg { + deserialize: None, + serialize: Some("ser_name".into()), + } + ); + } + + #[test] + fn serde_args() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(default, deny_unknown_fields)] + struct Example {} + }); + + assert!(container.serde_args.default); + assert!(container.serde_args.deny_unknown_fields); + } + + #[test] + fn serde_tagged_args() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(tag = "tag", content = "content")] + struct Example {} + }); + + assert_eq!(container.serde_args.content.as_ref().unwrap(), "content"); + assert_eq!(container.serde_args.tag.as_ref().unwrap(), "tag"); + assert!(container.serde_args.expecting.is_none()); + assert!(!container.serde_args.untagged); + } + + #[test] + fn serde_untagged_args() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(untagged, expecting = "expecting")] + struct Example {} + }); + + assert!(container.serde_args.content.is_none()); + assert!(container.serde_args.tag.is_none()); + assert_eq!( + container.serde_args.expecting.as_ref().unwrap(), + "expecting" + ); + assert!(container.serde_args.untagged); + } + + #[test] + fn serde_rename_args() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde( + rename = "name", + rename_all(deserialize = "de_name"), + rename_all_fields(serialize = "ser_name") + )] + struct Example {} + }); + + assert_eq!( + container.serde_args.rename.as_ref().unwrap(), + &SerdeRenameArg { + deserialize: Some("name".into()), + serialize: Some("name".into()), + } + ); + assert_eq!( + container.serde_args.rename_all.as_ref().unwrap(), + &SerdeRenameArg { + deserialize: Some("de_name".into()), + serialize: None, + } + ); + + assert_eq!( + container.serde_args.rename_all_fields.as_ref().unwrap(), + &SerdeRenameArg { + deserialize: None, + serialize: Some("ser_name".into()), + } + ); + } +} diff --git a/crates/macros-next/Cargo.toml b/crates/macros-next/Cargo.toml index cc5fb7f5..d283526e 100644 --- a/crates/macros-next/Cargo.toml +++ b/crates/macros-next/Cargo.toml @@ -14,11 +14,9 @@ all-features = true proc-macro = true [dependencies] -convert_case = "0.8.0" -darling = "0.20.11" -proc-macro2 = "1.0.95" -quote = "1.0.40" -syn = { version = "2.0.101", features = ["full"] } +schematic_core = { version = "0.18.7", path = "../core" } +quote = { workspace = true } +syn = { workspace = true } [features] default = ["schema"] diff --git a/crates/macros-next/src/args.rs b/crates/macros-next/src/args.rs deleted file mode 100644 index 6c51093c..00000000 --- a/crates/macros-next/src/args.rs +++ /dev/null @@ -1,300 +0,0 @@ -use darling::ast::NestedMeta; -use darling::{FromAttributes, FromDeriveInput, FromMeta}; -use proc_macro2::{Ident, TokenStream}; -use quote::{ToTokens, quote}; -use syn::{Attribute, Data, DeriveInput, ExprPath, Fields}; - -#[derive(Clone, Copy)] -pub enum SerdeIoDirection { - From, // read / deserialize - To, // write / serialize -} - -#[derive(Clone)] -pub enum SerdeTagFormat { - Untagged, - External, - Internal(String), - Adjacent(String, String), - // Special case for unit only enums - Unit, -} - -// #[serde(rename = "name")] -// #[serde(rename(deserialize = "de_name", serialize = "ser_name"))] -#[derive(Debug, Default, PartialEq)] -pub struct SerdeRenameArg { - deserialize: Option, - serialize: Option, -} - -impl FromMeta for SerdeRenameArg { - fn from_string(value: &str) -> darling::Result { - Ok(Self { - deserialize: Some(value.into()), - serialize: Some(value.into()), - }) - } - - fn from_list(items: &[NestedMeta]) -> darling::Result { - #[derive(Default, FromMeta)] - #[darling(default)] - struct Rename { - deserialize: Option, - serialize: Option, - } - - impl From for SerdeRenameArg { - fn from(value: Rename) -> Self { - Self { - deserialize: value.deserialize, - serialize: value.serialize, - } - } - } - - Rename::from_list(items).map(SerdeRenameArg::from) - } -} - -impl SerdeRenameArg { - // pub fn get_name(&self, dir: SerdeIoDirection) -> Option<&str> { - // match dir { - // SerdeIoDirection::From => self.deserialize.as_deref(), - // SerdeIoDirection::To => self.serialize.as_deref(), - // } - // } - - pub fn get_meta(&self, key: &str) -> TokenStream { - match (self.deserialize.as_deref(), self.serialize.as_deref()) { - (Some(de), Some(ser)) => { - if de == ser { - quote! { #key = #de } - } else { - quote! { #key(deserialize = #de, serialize = #ser) } - } - } - (None, Some(ser)) => quote! { #key(serialize = #ser) }, - (Some(de), None) => quote! { #key(deserialize = #de) }, - _ => quote! {}, - } - } -} - -// #[serde()] -#[derive(Default, FromDeriveInput)] -#[darling(default, allow_unknown_fields, attributes(serde))] -pub struct SerdeContainerArgs { - pub default: bool, - pub deny_unknown_fields: bool, - - // struct - pub rename: Option, - pub rename_all: Option, - pub rename_all_fields: Option, - - // enum - pub content: Option, - pub expecting: Option, - pub tag: Option, - pub untagged: bool, -} - -// #[serde()] -#[derive(Default, FromAttributes)] -#[darling(default, allow_unknown_fields, attributes(serde))] -pub struct SerdeFieldArgs { - #[darling(multiple)] - pub alias: Vec, - pub default: bool, - pub flatten: bool, - pub rename: Option, - pub skip: bool, - pub skip_deserializing: bool, - pub skip_deserializing_if: Option, - pub skip_serializing: bool, - pub skip_serializing_if: Option, - - // variant - pub other: bool, - pub untagged: bool, -} - -// // #[config()], #[schematic()] -// #[derive(FromDeriveInput, Default)] -// #[darling( -// default, -// attributes(config, schematic), -// supports(struct_named, enum_any) -// )] -// pub struct MacroArgs { -// // config -// pub allow_unknown_fields: bool, -// pub context: Option, -// pub partial: PartialAttr, -// #[cfg(feature = "env")] -// pub env_prefix: Option, - -// // serde -// pub rename: Option, -// pub rename_all: Option, -// pub rename_all_fields: Option, -// pub serde: SerdeMeta, -// } - -#[cfg(test)] -mod tests { - use super::*; - use darling::FromMeta; - use syn::parse_quote; - - mod serde_rename_arg { - use super::*; - - #[test] - fn both_value_string() { - let meta = SerdeRenameArg::from_string("name").unwrap(); - - assert_eq!( - meta, - SerdeRenameArg { - deserialize: Some("name".into()), - serialize: Some("name".into()), - } - ); - } - - #[test] - fn both_value() { - let meta = SerdeRenameArg::from_list(&[ - parse_quote! { - deserialize = "de_name" - }, - parse_quote! { - serialize = "ser_name" - }, - ]) - .unwrap(); - - assert_eq!( - meta, - SerdeRenameArg { - deserialize: Some("de_name".into()), - serialize: Some("ser_name".into()), - } - ); - } - - #[test] - fn de_value() { - let meta = SerdeRenameArg::from_list(&[parse_quote! { - deserialize = "de_name" - }]) - .unwrap(); - - assert_eq!( - meta, - SerdeRenameArg { - deserialize: Some("de_name".into()), - serialize: None, - } - ); - } - - #[test] - fn ser_value() { - let meta = SerdeRenameArg::from_list(&[parse_quote! { - serialize = "ser_name" - }]) - .unwrap(); - - assert_eq!( - meta, - SerdeRenameArg { - deserialize: None, - serialize: Some("ser_name".into()), - } - ); - } - } - - mod serde_container { - use super::*; - - #[test] - fn normal_args() { - let container = SerdeContainerArgs::from_derive_input(&parse_quote! { - #[serde(default, deny_unknown_fields)] - struct Example; - }) - .unwrap(); - - assert!(container.default); - assert!(container.deny_unknown_fields); - } - - #[test] - fn enum_tagged_args() { - let container = SerdeContainerArgs::from_derive_input(&parse_quote! { - #[serde(tag = "tag", content = "content")] - struct Example; - }) - .unwrap(); - - assert_eq!(container.content.unwrap(), "content"); - assert_eq!(container.tag.unwrap(), "tag"); - assert!(container.expecting.is_none()); - assert!(!container.untagged); - } - - #[test] - fn enum_untagged_args() { - let container = SerdeContainerArgs::from_derive_input(&parse_quote! { - #[serde(untagged, expecting = "expecting")] - struct Example; - }) - .unwrap(); - - assert!(container.content.is_none()); - assert!(container.tag.is_none()); - assert_eq!(container.expecting.unwrap(), "expecting"); - assert!(container.untagged); - } - - #[test] - fn rename_args() { - let container = SerdeContainerArgs::from_derive_input(&parse_quote! { - #[serde( - rename = "name", - rename_all(deserialize = "de_name"), - rename_all_fields(serialize = "ser_name") - )] - struct Example; - }) - .unwrap(); - - assert_eq!( - container.rename.unwrap(), - SerdeRenameArg { - deserialize: Some("name".into()), - serialize: Some("name".into()), - } - ); - assert_eq!( - container.rename_all.unwrap(), - SerdeRenameArg { - deserialize: Some("de_name".into()), - serialize: None, - } - ); - - assert_eq!( - container.rename_all_fields.unwrap(), - SerdeRenameArg { - deserialize: None, - serialize: Some("ser_name".into()), - } - ); - } - } -} diff --git a/crates/macros-next/src/lib.rs b/crates/macros-next/src/lib.rs index ef446386..e15d4257 100644 --- a/crates/macros-next/src/lib.rs +++ b/crates/macros-next/src/lib.rs @@ -1,17 +1,11 @@ -mod args; -mod container; -mod field; -mod variant; +use schematic_core::container::Container; -// mod common; -// mod utils; - -#[cfg(feature = "config")] -mod config; -#[cfg(feature = "config")] -mod config_enum; -#[cfg(feature = "schema")] -mod schematic; +// #[cfg(feature = "config")] +// mod config; +// #[cfg(feature = "config")] +// mod config_enum; +// #[cfg(feature = "schema")] +// mod schematic; // use common::Macro; use proc_macro::TokenStream; @@ -36,11 +30,12 @@ pub fn config_enum(item: TokenStream) -> TokenStream { } // #[derive(Schematic)] -// #[cfg(feature = "schema")] -// #[proc_macro_derive(Schematic, attributes(schematic, schema))] -// pub fn schematic(item: TokenStream) -> TokenStream { -// let input: DeriveInput = parse_macro_input!(item); -// let output = schematic::SchematicMacro(Macro::from(&input)); +#[cfg(feature = "schema")] +#[proc_macro_derive(Schematic, attributes(schematic, schema))] +pub fn schematic(item: TokenStream) -> TokenStream { + let input: DeriveInput = parse_macro_input!(item); + // let output = schematic::SchematicMacro(Macro::from(&input)); + let container = Container::from(input); -// quote! { #output }.into() -// } + quote! { #container }.into() +} From a242399a0880bafe320c949a6033937027f58a34 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Mon, 16 Jun 2025 20:57:32 -0700 Subject: [PATCH 05/33] Start on tests. --- CHANGELOG.md | 3 + crates/core/src/args.rs | 5 +- crates/core/src/container.rs | 18 +++ crates/core/src/field.rs | 96 +++++++++-- crates/core/src/field_value.rs | 165 ++++++++++++++++++- crates/core/src/utils.rs | 12 ++ crates/core/src/variant.rs | 2 +- crates/core/tests/field_nested_test.rs | 131 +++++++++++++++ crates/core/tests/field_test.rs | 210 +++++++++++++++++++++++++ 9 files changed, 624 insertions(+), 18 deletions(-) create mode 100644 crates/core/tests/field_nested_test.rs create mode 100644 crates/core/tests/field_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ebe0d241..6a2942da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ #### 🚀 Updates +- Added support for `#[setting(nested = NestedConfig)]` on fields, where the nested config name can + be explicitly defined if we fail to detect it. This is useful for extremely complex/composed + types. - Added support for explicit Serde deserialize and serialize renaming on containers and fields: `#[serde(rename(deserialize = "de_name", serialize = "ser_name"))]`. - Updated Serde `alias` to support multiple aliases: `#[serde(alias = "alias1", alias = "alias2")]` diff --git a/crates/core/src/args.rs b/crates/core/src/args.rs index ed40a72f..ec96ba4d 100644 --- a/crates/core/src/args.rs +++ b/crates/core/src/args.rs @@ -1,8 +1,7 @@ use darling::ast::NestedMeta; use darling::{FromAttributes, FromDeriveInput, FromMeta}; -use proc_macro2::{Ident, TokenStream}; -use quote::{ToTokens, quote}; -use syn::{Attribute, Data, DeriveInput, ExprPath, Fields}; +use proc_macro2::TokenStream; +use quote::quote; #[derive(Clone, Copy, Debug)] pub enum SerdeIoDirection { diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 53d4646e..c7771325 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -187,3 +187,21 @@ pub enum ContainerInner { Enum { variants: Vec }, UnitEnum { variants: Vec }, } + +impl ContainerInner { + pub fn get_fields(&self) -> Vec<&Field> { + match self { + Self::NamedStruct { fields } | Self::UnnamedStruct { fields } => { + fields.iter().collect() + } + _ => vec![], + } + } + + pub fn get_variants(&self) -> Vec<&Variant> { + match self { + Self::Enum { variants } | Self::UnitEnum { variants } => variants.iter().collect(), + _ => vec![], + } + } +} diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index def00b82..ead5345e 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -1,16 +1,64 @@ use crate::args::{SerdeContainerArgs, SerdeFieldArgs}; use crate::container::ContainerArgs; -use darling::FromAttributes; +use crate::field_value::FieldValue; +use crate::utils::to_type_string; +use darling::{FromAttributes, FromMeta}; +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; use std::rc::Rc; -use syn::{Attribute, Field as NativeField, FieldMutability, Ident, Type, Visibility}; +use syn::{Attribute, Expr, Field as NativeField, FieldMutability, Ident, Visibility, parse_str}; + +// #[setting(nested)] +#[derive(Debug)] +pub enum FieldNestedArg { + Detect(bool), + Ident(Ident), +} + +impl FromMeta for FieldNestedArg { + // #[setting(nested)] + fn from_word() -> darling::Result { + Ok(Self::Detect(true)) + } + + // #[setting(nested = true)] + fn from_bool(value: bool) -> darling::Result { + Ok(Self::Detect(value)) + } + + // #[setting(nested = NestedConfig)] + fn from_expr(expr: &Expr) -> darling::Result { + match expr { + Expr::Lit(lit) => Self::from_value(&lit.lit), + Expr::Path(path) => { + if path.path.segments.len() > 1 { + Err(darling::Error::custom(format!( + "Too many segments for `{}`, only a single identifier is allowed.", + to_type_string(path.to_token_stream()) + ))) + } else { + Ok(Self::Ident( + path.path.segments.last().unwrap().ident.to_owned(), + )) + } + } + _ => Err(darling::Error::unexpected_expr_type(expr)), + } + .map_err(|e| e.with_span(expr)) + } +} // #[schema()], #[setting()] #[derive(Debug, FromAttributes, Default)] #[darling(default, attributes(schema, setting))] -pub struct FieldArgs {} +pub struct FieldArgs { + pub nested: Option, +} #[derive(Debug)] pub struct Field { + pub value: FieldValue, + // args pub args: FieldArgs, pub container_args: Rc, @@ -22,10 +70,7 @@ pub struct Field { pub ident: Option, // Named pub index: usize, // Unnamed pub mutability: FieldMutability, - pub ty: Type, pub vis: Visibility, - // data - // pub value_type: FieldValue<'l>, } impl Field { @@ -33,12 +78,11 @@ impl Field { field: NativeField, container_args: Rc, serde_container_args: Rc, - ) -> Field { - let args = FieldArgs::from_attributes(&field.attrs).unwrap_or_default(); - let serde_args = SerdeFieldArgs::from_attributes(&field.attrs).unwrap_or_default(); + ) -> Self { + let args = FieldArgs::from_attributes(&field.attrs).unwrap(); + let serde_args = SerdeFieldArgs::from_attributes(&field.attrs).unwrap(); - Field { - args, + let field = Self { attrs: field.attrs, container_args, ident: field.ident, @@ -46,8 +90,36 @@ impl Field { mutability: field.mutability, serde_args, serde_container_args, - ty: field.ty, vis: field.vis, + value: FieldValue::new(field.ty, args.nested.as_ref()), + args, + }; + + // dbg!(&field); + + field + } +} + +impl ToTokens for Field { + fn to_tokens(&self, tokens: &mut TokenStream) { + let mut value = self.value.ty_string.clone(); + + if let Some(nested_ident) = &self.value.nested_ident { + let ident = nested_ident.get_ident().to_string(); + + value = value.replace(&ident, &format!("<{ident} as schematic::Config>::Partial")); + } + + if !self.value.is_outer_option_wrapped() { + value = format!("Option<{value}>"); } + + let key = self.ident.as_ref().unwrap(); + let value: TokenStream = parse_str(&value).unwrap(); + + tokens.extend(quote! { + pub #key: #value, + }); } } diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index 94c571c5..af1b8d64 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -1,5 +1,9 @@ -use syn::Type; +use crate::field::FieldNestedArg; +use crate::utils::to_type_string; +use quote::ToTokens; +use syn::{GenericArgument, Ident, PathArguments, PathSegment, Type}; +#[derive(Debug, PartialEq)] pub enum WrapperType { Arc, Box, @@ -7,7 +11,164 @@ pub enum WrapperType { Rc, } +#[derive(Debug)] pub struct FieldValue { - pub original_ty: Type, + pub nested: bool, + pub nested_ident: Option, + pub ty: Type, + pub ty_string: String, pub wrappers: Vec, } + +impl FieldValue { + pub fn new(ty: Type, nested_arg: Option<&FieldNestedArg>) -> Self { + let mut nested = false; + let mut nested_ident = None; + let mut wrappers = vec![]; + let ty_string = to_type_string(ty.to_token_stream()); + + // Determine nested state + if let Some(nested_arg) = nested_arg { + match nested_arg { + FieldNestedArg::Detect(state) => { + nested = *state; + } + FieldNestedArg::Ident(ident) => { + nested = true; + nested_ident = Some(FieldNestedIdent::Unknown(ident.to_owned())); + + if !ty_string.contains(&ident.to_string()) { + panic!( + "Nested configuration identifier `{ident}` does not exist within `{ty_string}`." + ) + } + } + }; + } + + // Extract type information + if let Some(custom_ident) = + extract_type_information(&ty, &mut wrappers, None, nested && nested_ident.is_none()) + { + nested_ident = Some(custom_ident); + } + + if nested_ident.is_none() && nested { + panic!( + "Unable to extract the nested configuration identifier from `{ty_string}`. Try explicitly passing the identifier with `nested = ConfigName`." + ) + } + + let value = Self { + nested, + nested_ident, + wrappers, + ty_string, + ty, + }; + + // dbg!(&value); + + value + } + + pub fn is_outer_option_wrapped(&self) -> bool { + self.wrappers + .first() + .is_some_and(|wrapper| *wrapper == WrapperType::Option) + } +} + +fn extract_type_information( + ty: &Type, + wrappers: &mut Vec, + parent_segment: Option<&PathSegment>, + nested_ident: bool, +) -> Option { + // We don't need to traverse other types, just paths + let Type::Path(ty_path) = ty else { + return None; + }; + + // Extract the last segment of the path, for example `Option`, + // instead of the full path `std::option::Option` + let last_segment = ty_path.path.segments.last().unwrap(); + + if last_segment.ident == "Option" { + wrappers.push(WrapperType::Option); + } else if last_segment.ident == "Arc" { + wrappers.push(WrapperType::Arc); + } else if last_segment.ident == "Box" { + wrappers.push(WrapperType::Box); + } else if last_segment.ident == "Rc" { + wrappers.push(WrapperType::Rc); + } + + match &last_segment.arguments { + // We've reached the final segment + PathArguments::None => { + if nested_ident { + return extract_nested_ident(&last_segment, parent_segment); + } + } + + // Attempt to drill deeper down + PathArguments::AngleBracketed(args) => { + if let Some(GenericArgument::Type(inner_ty)) = args.args.last() { + return extract_type_information( + inner_ty, + wrappers, + Some(last_segment), + nested_ident, + ); + } + } + + // What to do here, anything? + PathArguments::Parenthesized(_) => {} + }; + + None +} + +fn extract_nested_ident( + segment: &PathSegment, + parent_segment: Option<&PathSegment>, +) -> Option { + let ident = segment.ident.to_owned(); + + if let Some(parent) = parent_segment { + let parent_id = parent.ident.to_string(); + + if parent_id.ends_with("Vec") { + return Some(FieldNestedIdent::Vec(ident)); + } else if parent_id.ends_with("Set") { + return Some(FieldNestedIdent::Set(ident)); + } else if parent_id.ends_with("Map") { + return Some(FieldNestedIdent::Map(ident)); + } else { + return None; + } + } + + Some(FieldNestedIdent::Unknown(ident)) +} + +#[derive(Debug, PartialEq)] +pub enum FieldNestedIdent { + Unknown(Ident), + Map(Ident), + Set(Ident), + Vec(Ident), +} + +impl FieldNestedIdent { + pub fn get_ident(&self) -> &Ident { + match self { + Self::Unknown(ident) => ident, + Self::Map(ident) => ident, + Self::Set(ident) => ident, + Self::Vec(ident) => ident, + } + } +} diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index 03d1e0ac..f5d3b051 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -1,3 +1,4 @@ +use proc_macro2::TokenStream; use syn::{Attribute, Meta, Path}; pub fn get_meta_path(meta: &Meta) -> &Path { @@ -15,3 +16,14 @@ pub fn is_inheritable_attribute(attr: &Attribute) -> bool { .into_iter() .any(|n| path.is_ident(n)) } + +pub fn to_type_string(ts: TokenStream) -> String { + format!("{}", ts) + .replace(" :: ", "::") + .replace(" < ", "<") + .replace("< ", "<") + .replace(" <", "<") + .replace(" > ", ">") + .replace("> ", ">") + .replace(" >", ">") +} diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index faf23d77..ab806629 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -2,7 +2,7 @@ use crate::args::{SerdeContainerArgs, SerdeFieldArgs}; use crate::container::ContainerArgs; use darling::FromAttributes; use std::rc::Rc; -use syn::{Attribute, FieldMutability, Fields, Ident, Type, Variant as NativeVariant, Visibility}; +use syn::{Attribute, Fields, Ident, Variant as NativeVariant}; // #[setting()], #[schema()] #[derive(Debug, Default, FromAttributes)] diff --git a/crates/core/tests/field_nested_test.rs b/crates/core/tests/field_nested_test.rs new file mode 100644 index 00000000..32181d31 --- /dev/null +++ b/crates/core/tests/field_nested_test.rs @@ -0,0 +1,131 @@ +use quote::format_ident; +use schematic_core::container::Container; +use schematic_core::field_value::FieldNestedIdent; +use syn::parse_quote; + +mod field_nested { + use super::*; + + #[test] + fn word() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: NestedConfig, + } + }); + let fields = container.inner.get_fields(); + + assert!(fields[0].value.nested); + assert_eq!( + fields[0].value.nested_ident.as_ref().unwrap(), + &FieldNestedIdent::Unknown(format_ident!("NestedConfig")) + ); + } + + #[test] + fn bool_true() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested = true)] + a: NestedConfig, + } + }); + let fields = container.inner.get_fields(); + + assert!(fields[0].value.nested); + assert_eq!( + fields[0].value.nested_ident.as_ref().unwrap(), + &FieldNestedIdent::Unknown(format_ident!("NestedConfig")) + ); + } + + #[test] + fn bool_false() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested = false)] + a: NestedConfig, + } + }); + let fields = container.inner.get_fields(); + + assert!(!fields[0].value.nested); + assert!(fields[0].value.nested_ident.is_none()); + } + + #[test] + fn explicit_ident() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested = NestedConfig)] + a: NestedConfig, + } + }); + let fields = container.inner.get_fields(); + + assert!(fields[0].value.nested); + assert_eq!( + fields[0].value.nested_ident.as_ref().unwrap(), + &FieldNestedIdent::Unknown(format_ident!("NestedConfig")) + ); + } + + #[test] + #[should_panic(expected = "UnexpectedType(\"paren\")")] + fn panics_invalid_expr() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested = (1 + 1))] + a: NestedConfig, + } + }); + } + + #[test] + #[should_panic( + expected = "Unable to extract the nested configuration identifier from `Vec>>`. Try explicitly passing the identifier with `nested = ConfigName`." + )] + fn panics_cant_find_ident() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: Vec>>, + } + }); + } + + #[test] + #[should_panic( + expected = "Too many segments for `sub::NestedConfig`, only a single identifier is allowed." + )] + fn panics_too_many_segments() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested = sub::NestedConfig)] + a: NestedConfig, + } + }); + } + + #[test] + #[should_panic( + expected = "Nested configuration identifier `OtherConfig` does not exist within `Vec`." + )] + fn panics_cant_find_custom_ident() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested = OtherConfig)] + a: Vec, + } + }); + } +} diff --git a/crates/core/tests/field_test.rs b/crates/core/tests/field_test.rs new file mode 100644 index 00000000..842c4687 --- /dev/null +++ b/crates/core/tests/field_test.rs @@ -0,0 +1,210 @@ +use schematic_core::container::Container; +use schematic_core::field::Field; +use schematic_core::field_value::WrapperType; +use syn::{Ident, parse_quote}; + +fn get_field<'a>(fields: &'a [&'a Field], key: &str) -> &'a Field { + fields + .iter() + .find(|field| field.ident.as_ref().is_some_and(|ident| ident == key)) + .unwrap() +} + +fn get_field_nested_ident(field: &Field) -> &Ident { + field.value.nested_ident.as_ref().unwrap().get_ident() +} + +mod field { + use super::*; + + #[test] + fn extracts_wrappers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: bool, + b: Option, + c: Arc, + d: Box, + e: Rc, + f: Option>, + g: Option>, + h: Option>, + i: Arc>, + j: Box>, + k: Rc>, + l: Option>>, + } + }); + let fields = container.inner.get_fields(); + + // a + let field = get_field(&fields, "a"); + assert_eq!(field.value.ty_string, "bool"); + assert_eq!(field.value.wrappers, vec![]); + + // b + let field = get_field(&fields, "b"); + assert_eq!(field.value.ty_string, "Option"); + assert_eq!(field.value.wrappers, vec![WrapperType::Option]); + + // c + let field = get_field(&fields, "c"); + assert_eq!(field.value.ty_string, "Arc"); + assert_eq!(field.value.wrappers, vec![WrapperType::Arc]); + + // d + let field = get_field(&fields, "d"); + assert_eq!(field.value.ty_string, "Box"); + assert_eq!(field.value.wrappers, vec![WrapperType::Box]); + + // e + let field = get_field(&fields, "e"); + assert_eq!(field.value.ty_string, "Rc"); + assert_eq!(field.value.wrappers, vec![WrapperType::Rc]); + + // f + let field = get_field(&fields, "f"); + assert_eq!(field.value.ty_string, "Option>"); + assert_eq!( + field.value.wrappers, + vec![WrapperType::Option, WrapperType::Arc] + ); + + // g + let field = get_field(&fields, "g"); + assert_eq!(field.value.ty_string, "Option>"); + assert_eq!( + field.value.wrappers, + vec![WrapperType::Option, WrapperType::Box] + ); + + // h + let field = get_field(&fields, "h"); + assert_eq!(field.value.ty_string, "Option>"); + assert_eq!( + field.value.wrappers, + vec![WrapperType::Option, WrapperType::Rc] + ); + + // i + let field = get_field(&fields, "i"); + assert_eq!(field.value.ty_string, "Arc>"); + assert_eq!( + field.value.wrappers, + vec![WrapperType::Arc, WrapperType::Option] + ); + + // j + let field = get_field(&fields, "j"); + assert_eq!(field.value.ty_string, "Box>"); + assert_eq!( + field.value.wrappers, + vec![WrapperType::Box, WrapperType::Option] + ); + + // k + let field = get_field(&fields, "k"); + assert_eq!(field.value.ty_string, "Rc>"); + assert_eq!( + field.value.wrappers, + vec![WrapperType::Rc, WrapperType::Option] + ); + + // l + let field = get_field(&fields, "l"); + assert_eq!(field.value.ty_string, "Option>>"); + assert_eq!( + field.value.wrappers, + vec![WrapperType::Option, WrapperType::Arc, WrapperType::Option] + ); + } + + #[test] + fn extracts_vec_types() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: Vec, + b: Option>, + c: SmallVec, + d: Vec>, + e: Vec>, + } + }); + let fields = container.inner.get_fields(); + + // a + let field = get_field(&fields, "a"); + assert_eq!(field.value.ty_string, "Vec"); + assert!(field.value.nested_ident.is_none()); + + // b + let field = get_field(&fields, "b"); + assert_eq!(field.value.ty_string, "Option>"); + assert!(field.value.nested_ident.is_none()); + + // c + let field = get_field(&fields, "c"); + assert_eq!(field.value.ty_string, "SmallVec"); + assert!(field.value.nested_ident.is_none()); + + // d + let field = get_field(&fields, "d"); + assert_eq!(field.value.ty_string, "Vec>"); + assert!(field.value.nested_ident.is_none()); + + // e + let field = get_field(&fields, "e"); + assert_eq!(field.value.ty_string, "Vec>"); + assert!(field.value.nested_ident.is_none()); + } + + #[test] + fn extracts_vec_types_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: Vec, + #[setting(nested)] + b: Option>, + #[setting(nested)] + c: SmallVec, + #[setting(nested = CustomNestedConfig)] + d: Vec>, + #[setting(nested)] + e: Vec>, + } + }); + let fields = container.inner.get_fields(); + + // a + let field = get_field(&fields, "a"); + assert_eq!(field.value.ty_string, "Vec"); + assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + + // b + let field = get_field(&fields, "b"); + assert_eq!(field.value.ty_string, "Option>"); + assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + + // c + let field = get_field(&fields, "c"); + assert_eq!(field.value.ty_string, "SmallVec"); + assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + + // d + let field = get_field(&fields, "d"); + assert_eq!(field.value.ty_string, "Vec>"); + assert_eq!( + get_field_nested_ident(&field).to_string(), + "CustomNestedConfig" + ); + + // e + let field = get_field(&fields, "e"); + assert_eq!(field.value.ty_string, "Vec>"); + assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + } +} From 6ce7be0be64a53589336b657bca4908f3b99b741 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Thu, 19 Jun 2025 21:54:18 -0700 Subject: [PATCH 06/33] Rework layers. --- crates/core/src/field.rs | 2 +- crates/core/src/field_value.rs | 107 +++----- crates/core/src/utils.rs | 1 + crates/core/tests/field_nested_test.rs | 47 ++-- crates/core/tests/field_test.rs | 330 ++++++++++++++++++++++--- 5 files changed, 367 insertions(+), 120 deletions(-) diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index ead5345e..add1868c 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -106,7 +106,7 @@ impl ToTokens for Field { let mut value = self.value.ty_string.clone(); if let Some(nested_ident) = &self.value.nested_ident { - let ident = nested_ident.get_ident().to_string(); + let ident = nested_ident.to_string(); value = value.replace(&ident, &format!("<{ident} as schematic::Config>::Partial")); } diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index af1b8d64..d7d8076e 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -4,27 +4,32 @@ use quote::ToTokens; use syn::{GenericArgument, Ident, PathArguments, PathSegment, Type}; #[derive(Debug, PartialEq)] -pub enum WrapperType { +pub enum Layer { Arc, Box, Option, Rc, + // Collections + Map(String), + Set(String), + Vec(String), + Unknown(String), } #[derive(Debug)] pub struct FieldValue { + pub layers: Vec, pub nested: bool, - pub nested_ident: Option, + pub nested_ident: Option, pub ty: Type, pub ty_string: String, - pub wrappers: Vec, } impl FieldValue { pub fn new(ty: Type, nested_arg: Option<&FieldNestedArg>) -> Self { let mut nested = false; let mut nested_ident = None; - let mut wrappers = vec![]; + let mut layers = vec![]; let ty_string = to_type_string(ty.to_token_stream()); // Determine nested state @@ -35,7 +40,7 @@ impl FieldValue { } FieldNestedArg::Ident(ident) => { nested = true; - nested_ident = Some(FieldNestedIdent::Unknown(ident.to_owned())); + nested_ident = Some(ident.to_owned()); if !ty_string.contains(&ident.to_string()) { panic!( @@ -48,7 +53,7 @@ impl FieldValue { // Extract type information if let Some(custom_ident) = - extract_type_information(&ty, &mut wrappers, None, nested && nested_ident.is_none()) + extract_type_information(&ty, &mut layers, nested && nested_ident.is_none()) { nested_ident = Some(custom_ident); } @@ -62,7 +67,7 @@ impl FieldValue { let value = Self { nested, nested_ident, - wrappers, + layers, ty_string, ty, }; @@ -73,18 +78,17 @@ impl FieldValue { } pub fn is_outer_option_wrapped(&self) -> bool { - self.wrappers + self.layers .first() - .is_some_and(|wrapper| *wrapper == WrapperType::Option) + .is_some_and(|wrapper| *wrapper == Layer::Option) } } fn extract_type_information( ty: &Type, - wrappers: &mut Vec, - parent_segment: Option<&PathSegment>, + layers: &mut Vec, nested_ident: bool, -) -> Option { +) -> Option { // We don't need to traverse other types, just paths let Type::Path(ty_path) = ty else { return None; @@ -94,33 +98,20 @@ fn extract_type_information( // instead of the full path `std::option::Option` let last_segment = ty_path.path.segments.last().unwrap(); - if last_segment.ident == "Option" { - wrappers.push(WrapperType::Option); - } else if last_segment.ident == "Arc" { - wrappers.push(WrapperType::Arc); - } else if last_segment.ident == "Box" { - wrappers.push(WrapperType::Box); - } else if last_segment.ident == "Rc" { - wrappers.push(WrapperType::Rc); - } - match &last_segment.arguments { // We've reached the final segment PathArguments::None => { if nested_ident { - return extract_nested_ident(&last_segment, parent_segment); + return Some(last_segment.ident.clone()); } } // Attempt to drill deeper down PathArguments::AngleBracketed(args) => { + extract_layer(last_segment, layers); + if let Some(GenericArgument::Type(inner_ty)) = args.args.last() { - return extract_type_information( - inner_ty, - wrappers, - Some(last_segment), - nested_ident, - ); + return extract_type_information(inner_ty, layers, nested_ident); } } @@ -131,44 +122,28 @@ fn extract_type_information( None } -fn extract_nested_ident( - segment: &PathSegment, - parent_segment: Option<&PathSegment>, -) -> Option { - let ident = segment.ident.to_owned(); - - if let Some(parent) = parent_segment { - let parent_id = parent.ident.to_string(); - - if parent_id.ends_with("Vec") { - return Some(FieldNestedIdent::Vec(ident)); - } else if parent_id.ends_with("Set") { - return Some(FieldNestedIdent::Set(ident)); - } else if parent_id.ends_with("Map") { - return Some(FieldNestedIdent::Map(ident)); +fn extract_layer(last_segment: &PathSegment, layers: &mut Vec) { + let layer = if last_segment.ident == "Option" { + Layer::Option + } else if last_segment.ident == "Arc" { + Layer::Arc + } else if last_segment.ident == "Box" { + Layer::Box + } else if last_segment.ident == "Rc" { + Layer::Rc + } else { + let ident = last_segment.ident.to_string(); + + if ident.ends_with("Vec") { + Layer::Vec(ident) + } else if ident.ends_with("Set") { + Layer::Set(ident) + } else if ident.ends_with("Map") { + Layer::Map(ident) } else { - return None; + Layer::Unknown(ident) } - } - - Some(FieldNestedIdent::Unknown(ident)) -} - -#[derive(Debug, PartialEq)] -pub enum FieldNestedIdent { - Unknown(Ident), - Map(Ident), - Set(Ident), - Vec(Ident), -} + }; -impl FieldNestedIdent { - pub fn get_ident(&self) -> &Ident { - match self { - Self::Unknown(ident) => ident, - Self::Map(ident) => ident, - Self::Set(ident) => ident, - Self::Vec(ident) => ident, - } - } + layers.push(layer); } diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index f5d3b051..737052c2 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -20,6 +20,7 @@ pub fn is_inheritable_attribute(attr: &Attribute) -> bool { pub fn to_type_string(ts: TokenStream) -> String { format!("{}", ts) .replace(" :: ", "::") + .replace(" , ", ", ") .replace(" < ", "<") .replace("< ", "<") .replace(" <", "<") diff --git a/crates/core/tests/field_nested_test.rs b/crates/core/tests/field_nested_test.rs index 32181d31..7cf4eee5 100644 --- a/crates/core/tests/field_nested_test.rs +++ b/crates/core/tests/field_nested_test.rs @@ -1,6 +1,5 @@ use quote::format_ident; use schematic_core::container::Container; -use schematic_core::field_value::FieldNestedIdent; use syn::parse_quote; mod field_nested { @@ -20,7 +19,7 @@ mod field_nested { assert!(fields[0].value.nested); assert_eq!( fields[0].value.nested_ident.as_ref().unwrap(), - &FieldNestedIdent::Unknown(format_ident!("NestedConfig")) + &format_ident!("NestedConfig") ); } @@ -38,7 +37,7 @@ mod field_nested { assert!(fields[0].value.nested); assert_eq!( fields[0].value.nested_ident.as_ref().unwrap(), - &FieldNestedIdent::Unknown(format_ident!("NestedConfig")) + &format_ident!("NestedConfig") ); } @@ -71,36 +70,54 @@ mod field_nested { assert!(fields[0].value.nested); assert_eq!( fields[0].value.nested_ident.as_ref().unwrap(), - &FieldNestedIdent::Unknown(format_ident!("NestedConfig")) + &format_ident!("NestedConfig") ); } #[test] - #[should_panic(expected = "UnexpectedType(\"paren\")")] - fn panics_invalid_expr() { - Container::from(parse_quote! { + fn detect_in_vec() { + let container = Container::from(parse_quote! { #[derive(Config)] struct Example { - #[setting(nested = (1 + 1))] - a: NestedConfig, + #[setting(nested)] + a: Vec, } }); + let fields = container.inner.get_fields(); + + assert!(fields[0].value.nested); + assert_eq!( + fields[0].value.nested_ident.as_ref().unwrap(), + &format_ident!("NestedConfig") + ); } #[test] - #[should_panic( - expected = "Unable to extract the nested configuration identifier from `Vec>>`. Try explicitly passing the identifier with `nested = ConfigName`." - )] - fn panics_cant_find_ident() { + #[should_panic(expected = "UnexpectedType(\"paren\")")] + fn panics_invalid_expr() { Container::from(parse_quote! { #[derive(Config)] struct Example { - #[setting(nested)] - a: Vec>>, + #[setting(nested = (1 + 1))] + a: NestedConfig, } }); } + // #[test] + // #[should_panic( + // expected = "Unable to extract the nested configuration identifier from `Vec>>`. Try explicitly passing the identifier with `nested = ConfigName`." + // )] + // fn panics_cant_find_ident() { + // Container::from(parse_quote! { + // #[derive(Config)] + // struct Example { + // #[setting(nested)] + // a: Vec>>, + // } + // }); + // } + #[test] #[should_panic( expected = "Too many segments for `sub::NestedConfig`, only a single identifier is allowed." diff --git a/crates/core/tests/field_test.rs b/crates/core/tests/field_test.rs index 842c4687..ec7c41a3 100644 --- a/crates/core/tests/field_test.rs +++ b/crates/core/tests/field_test.rs @@ -1,6 +1,6 @@ use schematic_core::container::Container; use schematic_core::field::Field; -use schematic_core::field_value::WrapperType; +use schematic_core::field_value::Layer; use syn::{Ident, parse_quote}; fn get_field<'a>(fields: &'a [&'a Field], key: &str) -> &'a Field { @@ -11,14 +11,14 @@ fn get_field<'a>(fields: &'a [&'a Field], key: &str) -> &'a Field { } fn get_field_nested_ident(field: &Field) -> &Ident { - field.value.nested_ident.as_ref().unwrap().get_ident() + field.value.nested_ident.as_ref().unwrap() } mod field { use super::*; #[test] - fn extracts_wrappers() { + fn extracts_layers() { let container = Container::from(parse_quote! { #[derive(Config)] struct Example { @@ -41,82 +41,64 @@ mod field { // a let field = get_field(&fields, "a"); assert_eq!(field.value.ty_string, "bool"); - assert_eq!(field.value.wrappers, vec![]); + assert_eq!(field.value.layers, vec![]); // b let field = get_field(&fields, "b"); assert_eq!(field.value.ty_string, "Option"); - assert_eq!(field.value.wrappers, vec![WrapperType::Option]); + assert_eq!(field.value.layers, vec![Layer::Option]); // c let field = get_field(&fields, "c"); assert_eq!(field.value.ty_string, "Arc"); - assert_eq!(field.value.wrappers, vec![WrapperType::Arc]); + assert_eq!(field.value.layers, vec![Layer::Arc]); // d let field = get_field(&fields, "d"); assert_eq!(field.value.ty_string, "Box"); - assert_eq!(field.value.wrappers, vec![WrapperType::Box]); + assert_eq!(field.value.layers, vec![Layer::Box]); // e let field = get_field(&fields, "e"); assert_eq!(field.value.ty_string, "Rc"); - assert_eq!(field.value.wrappers, vec![WrapperType::Rc]); + assert_eq!(field.value.layers, vec![Layer::Rc]); // f let field = get_field(&fields, "f"); assert_eq!(field.value.ty_string, "Option>"); - assert_eq!( - field.value.wrappers, - vec![WrapperType::Option, WrapperType::Arc] - ); + assert_eq!(field.value.layers, vec![Layer::Option, Layer::Arc]); // g let field = get_field(&fields, "g"); assert_eq!(field.value.ty_string, "Option>"); - assert_eq!( - field.value.wrappers, - vec![WrapperType::Option, WrapperType::Box] - ); + assert_eq!(field.value.layers, vec![Layer::Option, Layer::Box]); // h let field = get_field(&fields, "h"); assert_eq!(field.value.ty_string, "Option>"); - assert_eq!( - field.value.wrappers, - vec![WrapperType::Option, WrapperType::Rc] - ); + assert_eq!(field.value.layers, vec![Layer::Option, Layer::Rc]); // i let field = get_field(&fields, "i"); assert_eq!(field.value.ty_string, "Arc>"); - assert_eq!( - field.value.wrappers, - vec![WrapperType::Arc, WrapperType::Option] - ); + assert_eq!(field.value.layers, vec![Layer::Arc, Layer::Option]); // j let field = get_field(&fields, "j"); assert_eq!(field.value.ty_string, "Box>"); - assert_eq!( - field.value.wrappers, - vec![WrapperType::Box, WrapperType::Option] - ); + assert_eq!(field.value.layers, vec![Layer::Box, Layer::Option]); // k let field = get_field(&fields, "k"); assert_eq!(field.value.ty_string, "Rc>"); - assert_eq!( - field.value.wrappers, - vec![WrapperType::Rc, WrapperType::Option] - ); + assert_eq!(field.value.layers, vec![Layer::Rc, Layer::Option]); // l let field = get_field(&fields, "l"); assert_eq!(field.value.ty_string, "Option>>"); assert_eq!( - field.value.wrappers, - vec![WrapperType::Option, WrapperType::Arc, WrapperType::Option] + field.value.layers, + vec![Layer::Option, Layer::Arc, Layer::Option] ); } @@ -129,7 +111,7 @@ mod field { b: Option>, c: SmallVec, d: Vec>, - e: Vec>, + e: Vec>, } }); let fields = container.inner.get_fields(); @@ -137,26 +119,40 @@ mod field { // a let field = get_field(&fields, "a"); assert_eq!(field.value.ty_string, "Vec"); + assert_eq!(field.value.layers, vec![Layer::Vec("Vec".into())]); assert!(field.value.nested_ident.is_none()); // b let field = get_field(&fields, "b"); assert_eq!(field.value.ty_string, "Option>"); + assert_eq!( + field.value.layers, + vec![Layer::Option, Layer::Vec("Vec".into())] + ); assert!(field.value.nested_ident.is_none()); // c let field = get_field(&fields, "c"); assert_eq!(field.value.ty_string, "SmallVec"); + assert_eq!(field.value.layers, vec![Layer::Vec("SmallVec".into())]); assert!(field.value.nested_ident.is_none()); // d let field = get_field(&fields, "d"); assert_eq!(field.value.ty_string, "Vec>"); + assert_eq!( + field.value.layers, + vec![Layer::Vec("Vec".into()), Layer::Option] + ); assert!(field.value.nested_ident.is_none()); // e let field = get_field(&fields, "e"); - assert_eq!(field.value.ty_string, "Vec>"); + assert_eq!(field.value.ty_string, "Vec>"); + assert_eq!( + field.value.layers, + vec![Layer::Vec("Vec".into()), Layer::Vec("SmallVec".into())] + ); assert!(field.value.nested_ident.is_none()); } @@ -174,7 +170,7 @@ mod field { #[setting(nested = CustomNestedConfig)] d: Vec>, #[setting(nested)] - e: Vec>, + e: Vec>, } }); let fields = container.inner.get_fields(); @@ -182,21 +178,272 @@ mod field { // a let field = get_field(&fields, "a"); assert_eq!(field.value.ty_string, "Vec"); + assert_eq!(field.value.layers, vec![Layer::Vec("Vec".into())]); assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); // b let field = get_field(&fields, "b"); assert_eq!(field.value.ty_string, "Option>"); + assert_eq!( + field.value.layers, + vec![Layer::Option, Layer::Vec("Vec".into())] + ); assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); // c let field = get_field(&fields, "c"); assert_eq!(field.value.ty_string, "SmallVec"); + assert_eq!(field.value.layers, vec![Layer::Vec("SmallVec".into())]); assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); // d let field = get_field(&fields, "d"); assert_eq!(field.value.ty_string, "Vec>"); + assert_eq!( + field.value.layers, + vec![Layer::Vec("Vec".into()), Layer::Option] + ); + assert_eq!( + get_field_nested_ident(&field).to_string(), + "CustomNestedConfig" + ); + + // e + let field = get_field(&fields, "e"); + assert_eq!(field.value.ty_string, "Vec>"); + assert_eq!( + field.value.layers, + vec![Layer::Vec("Vec".into()), Layer::Vec("SmallVec".into())] + ); + assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + } + + #[test] + fn extracts_set_types() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: HashSet, + b: Option>, + c: BTreeSet, + d: HashSet>, + e: HashSet>, + } + }); + let fields = container.inner.get_fields(); + + // a + let field = get_field(&fields, "a"); + assert_eq!(field.value.ty_string, "HashSet"); + assert_eq!(field.value.layers, vec![Layer::Set("HashSet".into())]); + assert!(field.value.nested_ident.is_none()); + + // b + let field = get_field(&fields, "b"); + assert_eq!(field.value.ty_string, "Option>"); + assert_eq!( + field.value.layers, + vec![Layer::Option, Layer::Set("HashSet".into())] + ); + assert!(field.value.nested_ident.is_none()); + + // c + let field = get_field(&fields, "c"); + assert_eq!(field.value.ty_string, "BTreeSet"); + assert_eq!(field.value.layers, vec![Layer::Set("BTreeSet".into())]); + assert!(field.value.nested_ident.is_none()); + + // d + let field = get_field(&fields, "d"); + assert_eq!(field.value.ty_string, "HashSet>"); + assert_eq!( + field.value.layers, + vec![Layer::Set("HashSet".into()), Layer::Option] + ); + assert!(field.value.nested_ident.is_none()); + + // e + let field = get_field(&fields, "e"); + assert_eq!(field.value.ty_string, "HashSet>"); + assert_eq!( + field.value.layers, + vec![Layer::Set("HashSet".into()), Layer::Set("FxHashSet".into())] + ); + assert!(field.value.nested_ident.is_none()); + } + + #[test] + fn extracts_set_types_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: HashSet, + #[setting(nested)] + b: Option>, + #[setting(nested)] + c: BTreeSet, + #[setting(nested = CustomNestedConfig)] + d: HashSet>, + #[setting(nested)] + e: HashSet>, + } + }); + let fields = container.inner.get_fields(); + + // a + let field = get_field(&fields, "a"); + assert_eq!(field.value.ty_string, "HashSet"); + assert_eq!(field.value.layers, vec![Layer::Set("HashSet".into())]); + assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + + // b + let field = get_field(&fields, "b"); + assert_eq!(field.value.ty_string, "Option>"); + assert_eq!( + field.value.layers, + vec![Layer::Option, Layer::Set("HashSet".into())] + ); + assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + + // c + let field = get_field(&fields, "c"); + assert_eq!(field.value.ty_string, "BTreeSet"); + assert_eq!(field.value.layers, vec![Layer::Set("BTreeSet".into())]); + assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + + // d + let field = get_field(&fields, "d"); + assert_eq!(field.value.ty_string, "HashSet>"); + assert_eq!( + field.value.layers, + vec![Layer::Set("HashSet".into()), Layer::Option] + ); + assert_eq!( + get_field_nested_ident(&field).to_string(), + "CustomNestedConfig" + ); + + // e + let field = get_field(&fields, "e"); + assert_eq!(field.value.ty_string, "HashSet>"); + assert_eq!( + field.value.layers, + vec![Layer::Set("HashSet".into()), Layer::Set("FxHashSet".into())] + ); + assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + } + + #[test] + fn extracts_map_types() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: HashMap, + b: Option>, + c: BTreeMap, + d: HashMap>, + e: HashMap>, + } + }); + let fields = container.inner.get_fields(); + + // a + let field = get_field(&fields, "a"); + assert_eq!(field.value.ty_string, "HashMap"); + assert_eq!(field.value.layers, vec![Layer::Map("HashMap".into())]); + assert!(field.value.nested_ident.is_none()); + + // b + let field = get_field(&fields, "b"); + assert_eq!(field.value.ty_string, "Option>"); + assert_eq!( + field.value.layers, + vec![Layer::Option, Layer::Map("HashMap".into())] + ); + assert!(field.value.nested_ident.is_none()); + + // c + let field = get_field(&fields, "c"); + assert_eq!(field.value.ty_string, "BTreeMap"); + assert_eq!(field.value.layers, vec![Layer::Map("BTreeMap".into())]); + assert!(field.value.nested_ident.is_none()); + + // d + let field = get_field(&fields, "d"); + assert_eq!(field.value.ty_string, "HashMap>"); + assert_eq!( + field.value.layers, + vec![Layer::Map("HashMap".into()), Layer::Option] + ); + assert!(field.value.nested_ident.is_none()); + + // e + let field = get_field(&fields, "e"); + assert_eq!( + field.value.ty_string, + "HashMap>" + ); + assert_eq!( + field.value.layers, + vec![Layer::Map("HashMap".into()), Layer::Map("FxHashMap".into())] + ); + assert!(field.value.nested_ident.is_none()); + } + + #[test] + fn extracts_map_types_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: HashMap, + #[setting(nested)] + b: Option>, + #[setting(nested)] + c: BTreeMap, + #[setting(nested = CustomNestedConfig)] + d: HashMap>, + #[setting(nested)] + e: HashMap>, + } + }); + let fields = container.inner.get_fields(); + + // a + let field = get_field(&fields, "a"); + assert_eq!(field.value.ty_string, "HashMap"); + assert_eq!(field.value.layers, vec![Layer::Map("HashMap".into())]); + assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + + // b + let field = get_field(&fields, "b"); + assert_eq!( + field.value.ty_string, + "Option>" + ); + assert_eq!( + field.value.layers, + vec![Layer::Option, Layer::Map("HashMap".into())] + ); + assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + + // c + let field = get_field(&fields, "c"); + assert_eq!(field.value.ty_string, "BTreeMap"); + assert_eq!(field.value.layers, vec![Layer::Map("BTreeMap".into())]); + assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + + // d + let field = get_field(&fields, "d"); + assert_eq!( + field.value.ty_string, + "HashMap>" + ); + assert_eq!( + field.value.layers, + vec![Layer::Map("HashMap".into()), Layer::Option] + ); assert_eq!( get_field_nested_ident(&field).to_string(), "CustomNestedConfig" @@ -204,7 +451,14 @@ mod field { // e let field = get_field(&fields, "e"); - assert_eq!(field.value.ty_string, "Vec>"); + assert_eq!( + field.value.ty_string, + "HashMap>" + ); + assert_eq!( + field.value.layers, + vec![Layer::Map("HashMap".into()), Layer::Map("FxHashMap".into())] + ); assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); } } From acbc718020ad2c8efb366905253b14f355877f07 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Tue, 24 Jun 2025 14:37:16 -0700 Subject: [PATCH 07/33] Add field serde. --- CHANGELOG.md | 8 +- crates/core/src/container.rs | 4 +- crates/core/src/field.rs | 13 +- crates/core/tests/field_serde_test.rs | 221 ++++++++++++++++++++++++++ 4 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 crates/core/tests/field_serde_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a2942da..5e85eb5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,13 @@ - Added support for `#[setting(nested = NestedConfig)]` on fields, where the nested config name can be explicitly defined if we fail to detect it. This is useful for extremely complex/composed types. -- Added support for explicit Serde deserialize and serialize renaming on containers and fields: + +##### Serde + +- Added support for explicit deserialize and serialize renaming on containers and fields: `#[serde(rename(deserialize = "de_name", serialize = "ser_name"))]`. -- Updated Serde `alias` to support multiple aliases: `#[serde(alias = "alias1", alias = "alias2")]` +- Added support for `skip_deserializing_if` and `skip_serializing_if` on fields. +- Updated `alias` to support multiple aliases: `#[serde(alias = "alias1", alias = "alias2")]` ## 0.19.7 diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index c7771325..1c9db44b 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -43,8 +43,8 @@ pub struct Container { impl Container { pub fn from(input: DeriveInput) -> Self { - let args = Rc::new(ContainerArgs::from_derive_input(&input).unwrap_or_default()); - let serde_args = Rc::new(SerdeContainerArgs::from_derive_input(&input).unwrap_or_default()); + let args = Rc::new(ContainerArgs::from_derive_input(&input).unwrap()); + let serde_args = Rc::new(SerdeContainerArgs::from_derive_input(&input).unwrap()); let inner = match input.data { Data::Struct(data) => match data.fields { diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index add1868c..5f0c9d24 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -1,4 +1,4 @@ -use crate::args::{SerdeContainerArgs, SerdeFieldArgs}; +use crate::args::{SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; use crate::container::ContainerArgs; use crate::field_value::FieldValue; use crate::utils::to_type_string; @@ -53,6 +53,17 @@ impl FromMeta for FieldNestedArg { #[darling(default, attributes(schema, setting))] pub struct FieldArgs { pub nested: Option, + + // serde + #[darling(multiple)] + pub alias: Vec, + pub flatten: bool, + pub rename: Option, + pub skip: bool, + pub skip_deserializing: bool, + pub skip_deserializing_if: Option, + pub skip_serializing: bool, + pub skip_serializing_if: Option, } #[derive(Debug)] diff --git a/crates/core/tests/field_serde_test.rs b/crates/core/tests/field_serde_test.rs new file mode 100644 index 00000000..cd64429b --- /dev/null +++ b/crates/core/tests/field_serde_test.rs @@ -0,0 +1,221 @@ +use schematic_core::args::SerdeRenameArg; +use schematic_core::container::Container; +use syn::parse_quote; + +mod field_serde { + use super::*; + + mod native { + use super::*; + + #[test] + fn basic() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[serde(alias = "b", flatten, skip)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert_eq!(field.serde_args.alias, vec!["b"]); + assert!(field.serde_args.flatten); + assert!(field.serde_args.skip); + } + + #[test] + fn multiple_alias() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[serde(alias = "b", alias = "c")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert_eq!(field.serde_args.alias, vec!["b", "c"]); + } + + #[test] + fn rename() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[serde(rename = "b")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert_eq!( + field.serde_args.rename.as_ref().unwrap(), + &SerdeRenameArg { + deserialize: Some("b".into()), + serialize: Some("b".into()), + } + ); + } + + #[test] + fn rename_both() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[serde(rename(deserialize = "de_name", serialize = "ser_name"))] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert_eq!( + field.serde_args.rename.as_ref().unwrap(), + &SerdeRenameArg { + deserialize: Some("de_name".into()), + serialize: Some("ser_name".into()), + } + ); + } + + #[test] + fn skip_de() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[serde(skip_deserializing, skip_deserializing_if = "value")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.serde_args.skip_deserializing); + assert_eq!( + field.serde_args.skip_deserializing_if.as_ref().unwrap(), + "value" + ); + } + + #[test] + fn skip_ser() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[serde(skip_serializing, skip_serializing_if = "value")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.serde_args.skip_serializing); + assert_eq!( + field.serde_args.skip_serializing_if.as_ref().unwrap(), + "value" + ); + } + } + + mod setting { + use super::*; + + #[test] + fn basic() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(alias = "b", flatten, skip)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert_eq!(field.args.alias, vec!["b"]); + assert!(field.args.flatten); + assert!(field.args.skip); + } + + #[test] + fn multiple_alias() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(alias = "b", alias = "c")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert_eq!(field.args.alias, vec!["b", "c"]); + } + + #[test] + fn rename() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(rename = "b")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert_eq!( + field.args.rename.as_ref().unwrap(), + &SerdeRenameArg { + deserialize: Some("b".into()), + serialize: Some("b".into()), + } + ); + } + + #[test] + fn rename_both() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(rename(deserialize = "de_name", serialize = "ser_name"))] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert_eq!( + field.args.rename.as_ref().unwrap(), + &SerdeRenameArg { + deserialize: Some("de_name".into()), + serialize: Some("ser_name".into()), + } + ); + } + + #[test] + fn skip_de() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(skip_deserializing, skip_deserializing_if = "value")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.skip_deserializing); + assert_eq!(field.args.skip_deserializing_if.as_ref().unwrap(), "value"); + } + + #[test] + fn skip_ser() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(skip_serializing, skip_serializing_if = "value")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.skip_serializing); + assert_eq!(field.args.skip_serializing_if.as_ref().unwrap(), "value"); + } + } +} From 174efad5509dcbdcdad6c00e491451c862a1b318 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Tue, 24 Jun 2025 15:05:31 -0700 Subject: [PATCH 08/33] Add expr fields. --- crates/core/src/field.rs | 12 ++- crates/core/src/utils.rs | 10 +- crates/core/tests/field_default_test.rs | 97 +++++++++++++++++++ crates/core/tests/field_env_test.rs | 46 +++++++++ crates/core/tests/field_merge_test.rs | 46 +++++++++ crates/core/tests/field_transform_test.rs | 46 +++++++++ ...t_test__field_default__supports_array.snap | 60 ++++++++++++ ...lt_test__field_default__supports_bool.snap | 10 ++ ..._test__field_default__supports_number.snap | 10 ++ ..._test__field_default__supports_string.snap | 10 ++ ...t_test__field_default__supports_tuple.snap | 36 +++++++ ...ult_test__field_default__supports_vec.snap | 43 ++++++++ 12 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 crates/core/tests/field_default_test.rs create mode 100644 crates/core/tests/field_env_test.rs create mode 100644 crates/core/tests/field_merge_test.rs create mode 100644 crates/core/tests/field_transform_test.rs create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__supports_array.snap create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__supports_bool.snap create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__supports_number.snap create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__supports_string.snap create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__supports_tuple.snap create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__supports_vec.snap diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 5f0c9d24..ae4ee5aa 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -1,12 +1,14 @@ use crate::args::{SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; use crate::container::ContainerArgs; use crate::field_value::FieldValue; -use crate::utils::to_type_string; +use crate::utils::{preserve_str_literal, to_type_string}; use darling::{FromAttributes, FromMeta}; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; use std::rc::Rc; -use syn::{Attribute, Expr, Field as NativeField, FieldMutability, Ident, Visibility, parse_str}; +use syn::{ + Attribute, Expr, ExprPath, Field as NativeField, FieldMutability, Ident, Visibility, parse_str, +}; // #[setting(nested)] #[derive(Debug)] @@ -52,7 +54,13 @@ impl FromMeta for FieldNestedArg { #[derive(Debug, FromAttributes, Default)] #[darling(default, attributes(schema, setting))] pub struct FieldArgs { + #[darling(with = preserve_str_literal, map = "Some")] + pub default: Option, + pub merge: Option, pub nested: Option, + #[cfg(feature = "env")] + pub parse_env: Option, + pub transform: Option, // serde #[darling(multiple)] diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index 737052c2..5943b6c0 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream; -use syn::{Attribute, Meta, Path}; +use syn::{Attribute, Expr, Meta, Path}; pub fn get_meta_path(meta: &Meta) -> &Path { match meta { @@ -9,6 +9,14 @@ pub fn get_meta_path(meta: &Meta) -> &Path { } } +pub fn preserve_str_literal(meta: &Meta) -> darling::Result { + match meta { + Meta::Path(_) => Err(darling::Error::unsupported_format("path").with_span(meta)), + Meta::List(_) => Err(darling::Error::unsupported_format("list").with_span(meta)), + Meta::NameValue(nv) => Ok(nv.value.clone()), + } +} + pub fn is_inheritable_attribute(attr: &Attribute) -> bool { let path = get_meta_path(&attr.meta); diff --git a/crates/core/tests/field_default_test.rs b/crates/core/tests/field_default_test.rs new file mode 100644 index 00000000..5270f64f --- /dev/null +++ b/crates/core/tests/field_default_test.rs @@ -0,0 +1,97 @@ +use schematic_core::container::Container; +use starbase_sandbox::assert_debug_snapshot; +use syn::parse_quote; + +mod field_default { + use super::*; + + #[test] + fn supports_bool() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = true)] + a: bool, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.default.is_some()); + assert_debug_snapshot!(field.args.default.as_ref().unwrap()); + } + + #[test] + fn supports_number() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = 100)] + a: usize, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.default.is_some()); + assert_debug_snapshot!(field.args.default.as_ref().unwrap()); + } + + #[test] + fn supports_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = "abc")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.default.is_some()); + assert_debug_snapshot!(field.args.default.as_ref().unwrap()); + } + + #[test] + fn supports_array() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = ["a".into(), "b".into(), "c".into()])] + a: [String; 3], + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.default.is_some()); + assert_debug_snapshot!(field.args.default.as_ref().unwrap()); + } + + #[test] + fn supports_vec() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = vec!["a", "b", "c"])] + a: Vec, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.default.is_some()); + assert_debug_snapshot!(field.args.default.as_ref().unwrap()); + } + + #[test] + fn supports_tuple() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = (10, -10, 0))] + a: (usize, isize, u8), + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.default.is_some()); + assert_debug_snapshot!(field.args.default.as_ref().unwrap()); + } +} diff --git a/crates/core/tests/field_env_test.rs b/crates/core/tests/field_env_test.rs new file mode 100644 index 00000000..88c237b1 --- /dev/null +++ b/crates/core/tests/field_env_test.rs @@ -0,0 +1,46 @@ +use schematic_core::container::Container; +use syn::parse_quote; + +mod field_parse_env { + use super::*; + + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(parse_env = func_ref)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.parse_env.is_some()); + } + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(parse_env = "func_ref")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.parse_env.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(parse_env = 123)] + a: String, + } + }); + } +} diff --git a/crates/core/tests/field_merge_test.rs b/crates/core/tests/field_merge_test.rs new file mode 100644 index 00000000..a8c10ef5 --- /dev/null +++ b/crates/core/tests/field_merge_test.rs @@ -0,0 +1,46 @@ +use schematic_core::container::Container; +use syn::parse_quote; + +mod field_merge { + use super::*; + + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(merge = func_ref)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.merge.is_some()); + } + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(merge = "func_ref")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.merge.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(merge = 123)] + a: String, + } + }); + } +} diff --git a/crates/core/tests/field_transform_test.rs b/crates/core/tests/field_transform_test.rs new file mode 100644 index 00000000..b83d14ff --- /dev/null +++ b/crates/core/tests/field_transform_test.rs @@ -0,0 +1,46 @@ +use schematic_core::container::Container; +use syn::parse_quote; + +mod field_transform { + use super::*; + + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(transform = func_ref)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.transform.is_some()); + } + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(transform = "func_ref")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.transform.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(transform = 123)] + a: String, + } + }); + } +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__supports_array.snap b/crates/core/tests/snapshots/field_default_test__field_default__supports_array.snap new file mode 100644 index 00000000..344510ec --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__supports_array.snap @@ -0,0 +1,60 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: field.args.default.as_ref().unwrap() +--- +Expr::Array { + attrs: [], + bracket_token: Bracket, + elems: [ + Expr::MethodCall { + attrs: [], + receiver: Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "a", + }, + }, + dot_token: Dot, + method: Ident( + into, + ), + turbofish: None, + paren_token: Paren, + args: [], + }, + Comma, + Expr::MethodCall { + attrs: [], + receiver: Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "b", + }, + }, + dot_token: Dot, + method: Ident( + into, + ), + turbofish: None, + paren_token: Paren, + args: [], + }, + Comma, + Expr::MethodCall { + attrs: [], + receiver: Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "c", + }, + }, + dot_token: Dot, + method: Ident( + into, + ), + turbofish: None, + paren_token: Paren, + args: [], + }, + ], +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__supports_bool.snap b/crates/core/tests/snapshots/field_default_test__field_default__supports_bool.snap new file mode 100644 index 00000000..1c831e90 --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__supports_bool.snap @@ -0,0 +1,10 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: field.args.default.as_ref().unwrap() +--- +Expr::Lit { + attrs: [], + lit: Lit::Bool { + value: true, + }, +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__supports_number.snap b/crates/core/tests/snapshots/field_default_test__field_default__supports_number.snap new file mode 100644 index 00000000..6c7c4d49 --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__supports_number.snap @@ -0,0 +1,10 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: field.args.default.as_ref().unwrap() +--- +Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 100, + }, +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__supports_string.snap b/crates/core/tests/snapshots/field_default_test__field_default__supports_string.snap new file mode 100644 index 00000000..351325cf --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__supports_string.snap @@ -0,0 +1,10 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: field.args.default.as_ref().unwrap() +--- +Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "abc", + }, +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__supports_tuple.snap b/crates/core/tests/snapshots/field_default_test__field_default__supports_tuple.snap new file mode 100644 index 00000000..b6734170 --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__supports_tuple.snap @@ -0,0 +1,36 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: field.args.default.as_ref().unwrap() +--- +Expr::Tuple { + attrs: [], + paren_token: Paren, + elems: [ + Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 10, + }, + }, + Comma, + Expr::Unary { + attrs: [], + op: UnOp::Neg( + Minus, + ), + expr: Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 10, + }, + }, + }, + Comma, + Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 0, + }, + }, + ], +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__supports_vec.snap b/crates/core/tests/snapshots/field_default_test__field_default__supports_vec.snap new file mode 100644 index 00000000..07fd458d --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__supports_vec.snap @@ -0,0 +1,43 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: field.args.default.as_ref().unwrap() +--- +Expr::Macro { + attrs: [], + mac: Macro { + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident( + vec, + ), + arguments: PathArguments::None, + }, + ], + }, + bang_token: Not, + delimiter: MacroDelimiter::Bracket( + Bracket, + ), + tokens: TokenStream [ + Literal { + lit: "a", + }, + Punct { + char: ',', + spacing: Alone, + }, + Literal { + lit: "b", + }, + Punct { + char: ',', + spacing: Alone, + }, + Literal { + lit: "c", + }, + ], + }, +} From d54756778ef18a9040913bd925cef8f3f6ef0e34 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Tue, 24 Jun 2025 15:16:32 -0700 Subject: [PATCH 09/33] Add env tests. --- CHANGELOG.md | 3 + crates/core/src/field.rs | 25 ++++++++ crates/core/tests/field_env_test.rs | 90 ++++++++++++++++++++++++++++- 3 files changed, 115 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e85eb5e..582d5b78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ - Added support for `#[setting(nested = NestedConfig)]` on fields, where the nested config name can be explicitly defined if we fail to detect it. This is useful for extremely complex/composed types. +- Added support for env prefixes at the field level when the field is also nested. This will + override the env prefix defined on the nested container: + `#[setting(nested, env_prefix = "OVERRIDE_")]`. ##### Serde diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index ae4ee5aa..9cad87cc 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -56,6 +56,10 @@ impl FromMeta for FieldNestedArg { pub struct FieldArgs { #[darling(with = preserve_str_literal, map = "Some")] pub default: Option, + #[cfg(feature = "env")] + pub env: Option, + #[cfg(feature = "env")] + pub env_prefix: Option, pub merge: Option, pub nested: Option, #[cfg(feature = "env")] @@ -116,6 +120,27 @@ impl Field { // dbg!(&field); + if field.args.env.as_ref().is_some_and(|key| key.is_empty()) { + panic!("Attribute `env` cannot be empty."); + } + + if field + .args + .env_prefix + .as_ref() + .is_some_and(|key| key.is_empty()) + { + panic!("Attribute `env_prefix` cannot be empty."); + } + + if field.args.parse_env.is_some() && field.args.env.is_none() { + panic!("Cannot use `parse_env` without `env`."); + } + + if field.args.env_prefix.is_some() && field.args.nested.is_none() { + panic!("Cannot use `env_prefix` without `nested`."); + } + field } } diff --git a/crates/core/tests/field_env_test.rs b/crates/core/tests/field_env_test.rs index 88c237b1..c1ea7703 100644 --- a/crates/core/tests/field_env_test.rs +++ b/crates/core/tests/field_env_test.rs @@ -1,6 +1,78 @@ use schematic_core::container::Container; use syn::parse_quote; +mod field_env { + use super::*; + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env = "KEY")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert_eq!(field.args.env.as_ref().unwrap(), "KEY"); + } + + #[test] + #[should_panic(expected = "Attribute `env` cannot be empty.")] + fn errors_if_empty() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env = "")] + a: String, + } + }); + } +} + +mod field_env_prefix { + use super::*; + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env_prefix = "KEY", nested)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert_eq!(field.args.env_prefix.as_ref().unwrap(), "KEY"); + } + + #[test] + #[should_panic(expected = "Attribute `env_prefix` cannot be empty.")] + fn errors_if_empty() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env_prefix = "", nested)] + a: String, + } + }); + } + + #[test] + #[should_panic(expected = "Cannot use `env_prefix` without `nested`.")] + fn errors_if_not_nested() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env_prefix = "KEY")] + a: String, + } + }); + } +} + mod field_parse_env { use super::*; @@ -9,7 +81,7 @@ mod field_parse_env { let container = Container::from(parse_quote! { #[derive(Config)] struct Example { - #[setting(parse_env = func_ref)] + #[setting(env = "KEY", parse_env = func_ref)] a: String, } }); @@ -23,7 +95,7 @@ mod field_parse_env { let container = Container::from(parse_quote! { #[derive(Config)] struct Example { - #[setting(parse_env = "func_ref")] + #[setting(env = "KEY", parse_env = "func_ref")] a: String, } }); @@ -38,7 +110,19 @@ mod field_parse_env { Container::from(parse_quote! { #[derive(Config)] struct Example { - #[setting(parse_env = 123)] + #[setting(env = "KEY", parse_env = 123)] + a: String, + } + }); + } + + #[test] + #[should_panic(expected = "Cannot use `parse_env` without `env`.")] + fn errors_without_env() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(parse_env = func_ref)] a: String, } }); From 4c3ed3f9c7cdfe8a66c3f6e0d4193acdf64908b2 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Tue, 24 Jun 2025 15:55:50 -0700 Subject: [PATCH 10/33] Add newtype for validate. --- CHANGELOG.md | 1 + crates/core/src/field.rs | 87 ++++++++++++++++++++---- crates/core/tests/field_test.rs | 46 +++++++++---- crates/core/tests/field_validate_test.rs | 46 +++++++++++++ 4 files changed, 150 insertions(+), 30 deletions(-) create mode 100644 crates/core/tests/field_validate_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 582d5b78..dcf0c262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Added support for env prefixes at the field level when the field is also nested. This will override the env prefix defined on the nested container: `#[setting(nested, env_prefix = "OVERRIDE_")]`. +- Improved the parse, handling, and validation of container and field attributes. ##### Serde diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 9cad87cc..9425ed7f 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -5,6 +5,7 @@ use crate::utils::{preserve_str_literal, to_type_string}; use darling::{FromAttributes, FromMeta}; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; +use std::ops::Deref; use std::rc::Rc; use syn::{ Attribute, Expr, ExprPath, Field as NativeField, FieldMutability, Ident, Visibility, parse_str, @@ -50,6 +51,28 @@ impl FromMeta for FieldNestedArg { } } +// #[setting(validate)] +#[derive(Debug)] +pub struct FieldValidateArg(Expr); + +impl FromMeta for FieldValidateArg { + fn from_expr(expr: &Expr) -> darling::Result { + match expr { + Expr::Call(_) | Expr::Path(_) => Ok(Self(expr.to_owned())), + _ => Err(darling::Error::unexpected_expr_type(expr)), + } + .map_err(|e| e.with_span(expr)) + } +} + +impl Deref for FieldValidateArg { + type Target = Expr; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + // #[schema()], #[setting()] #[derive(Debug, FromAttributes, Default)] #[darling(default, attributes(schema, setting))] @@ -60,11 +83,17 @@ pub struct FieldArgs { pub env: Option, #[cfg(feature = "env")] pub env_prefix: Option, + pub exclude: bool, + #[cfg(feature = "extends")] + pub extend: bool, pub merge: Option, pub nested: Option, #[cfg(feature = "env")] pub parse_env: Option, + pub required: bool, pub transform: Option, + #[cfg(feature = "validate")] + pub validate: Option, // serde #[darling(multiple)] @@ -120,28 +149,56 @@ impl Field { // dbg!(&field); - if field.args.env.as_ref().is_some_and(|key| key.is_empty()) { - panic!("Attribute `env` cannot be empty."); - } + field.validate_args(); + field + } - if field - .args - .env_prefix - .as_ref() - .is_some_and(|key| key.is_empty()) + fn validate_args(&self) { + #[cfg(feature = "env")] { - panic!("Attribute `env_prefix` cannot be empty."); - } + // env + if self.args.env.as_ref().is_some_and(|key| key.is_empty()) { + panic!("Attribute `env` cannot be empty."); + } + + if self.args.env.is_some() && self.args.env_prefix.is_some() { + panic!("Cannot use `env` and `env_prefix` together."); + } + + // env_prefix + if self + .args + .env_prefix + .as_ref() + .is_some_and(|key| key.is_empty()) + { + panic!("Attribute `env_prefix` cannot be empty."); + } - if field.args.parse_env.is_some() && field.args.env.is_none() { - panic!("Cannot use `parse_env` without `env`."); + if self.args.env_prefix.is_some() && self.args.nested.is_none() { + panic!("Cannot use `env_prefix` without `nested`."); + } } - if field.args.env_prefix.is_some() && field.args.nested.is_none() { - panic!("Cannot use `env_prefix` without `nested`."); + // nested + if self.args.nested.is_some() { + if self.args.default.is_some() { + panic!("Cannot use `default` with `nested`."); + } + + #[cfg(feature = "env")] + if self.args.env.is_some() { + panic!("Cannot use `env` with `nested`, use `env_prefix` instead?"); + } } - field + #[cfg(feature = "env")] + { + // parse_env + if self.args.parse_env.is_some() && self.args.env.is_none() { + panic!("Cannot use `parse_env` without `env`."); + } + } } } diff --git a/crates/core/tests/field_test.rs b/crates/core/tests/field_test.rs index ec7c41a3..460a041f 100644 --- a/crates/core/tests/field_test.rs +++ b/crates/core/tests/field_test.rs @@ -17,6 +17,22 @@ fn get_field_nested_ident(field: &Field) -> &Ident { mod field { use super::*; + #[test] + fn basic_args() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(exclude, extend, required)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.exclude); + assert!(field.args.extend); + assert!(field.args.required); + } + #[test] fn extracts_layers() { let container = Container::from(parse_quote! { @@ -179,7 +195,7 @@ mod field { let field = get_field(&fields, "a"); assert_eq!(field.value.ty_string, "Vec"); assert_eq!(field.value.layers, vec![Layer::Vec("Vec".into())]); - assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + assert_eq!(get_field_nested_ident(field).to_string(), "NestedConfig"); // b let field = get_field(&fields, "b"); @@ -188,13 +204,13 @@ mod field { field.value.layers, vec![Layer::Option, Layer::Vec("Vec".into())] ); - assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + assert_eq!(get_field_nested_ident(field).to_string(), "NestedConfig"); // c let field = get_field(&fields, "c"); assert_eq!(field.value.ty_string, "SmallVec"); assert_eq!(field.value.layers, vec![Layer::Vec("SmallVec".into())]); - assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + assert_eq!(get_field_nested_ident(field).to_string(), "NestedConfig"); // d let field = get_field(&fields, "d"); @@ -204,7 +220,7 @@ mod field { vec![Layer::Vec("Vec".into()), Layer::Option] ); assert_eq!( - get_field_nested_ident(&field).to_string(), + get_field_nested_ident(field).to_string(), "CustomNestedConfig" ); @@ -215,7 +231,7 @@ mod field { field.value.layers, vec![Layer::Vec("Vec".into()), Layer::Vec("SmallVec".into())] ); - assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + assert_eq!(get_field_nested_ident(field).to_string(), "NestedConfig"); } #[test] @@ -295,7 +311,7 @@ mod field { let field = get_field(&fields, "a"); assert_eq!(field.value.ty_string, "HashSet"); assert_eq!(field.value.layers, vec![Layer::Set("HashSet".into())]); - assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + assert_eq!(get_field_nested_ident(field).to_string(), "NestedConfig"); // b let field = get_field(&fields, "b"); @@ -304,13 +320,13 @@ mod field { field.value.layers, vec![Layer::Option, Layer::Set("HashSet".into())] ); - assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + assert_eq!(get_field_nested_ident(field).to_string(), "NestedConfig"); // c let field = get_field(&fields, "c"); assert_eq!(field.value.ty_string, "BTreeSet"); assert_eq!(field.value.layers, vec![Layer::Set("BTreeSet".into())]); - assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + assert_eq!(get_field_nested_ident(field).to_string(), "NestedConfig"); // d let field = get_field(&fields, "d"); @@ -320,7 +336,7 @@ mod field { vec![Layer::Set("HashSet".into()), Layer::Option] ); assert_eq!( - get_field_nested_ident(&field).to_string(), + get_field_nested_ident(field).to_string(), "CustomNestedConfig" ); @@ -331,7 +347,7 @@ mod field { field.value.layers, vec![Layer::Set("HashSet".into()), Layer::Set("FxHashSet".into())] ); - assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + assert_eq!(get_field_nested_ident(field).to_string(), "NestedConfig"); } #[test] @@ -414,7 +430,7 @@ mod field { let field = get_field(&fields, "a"); assert_eq!(field.value.ty_string, "HashMap"); assert_eq!(field.value.layers, vec![Layer::Map("HashMap".into())]); - assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + assert_eq!(get_field_nested_ident(field).to_string(), "NestedConfig"); // b let field = get_field(&fields, "b"); @@ -426,13 +442,13 @@ mod field { field.value.layers, vec![Layer::Option, Layer::Map("HashMap".into())] ); - assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + assert_eq!(get_field_nested_ident(field).to_string(), "NestedConfig"); // c let field = get_field(&fields, "c"); assert_eq!(field.value.ty_string, "BTreeMap"); assert_eq!(field.value.layers, vec![Layer::Map("BTreeMap".into())]); - assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + assert_eq!(get_field_nested_ident(field).to_string(), "NestedConfig"); // d let field = get_field(&fields, "d"); @@ -445,7 +461,7 @@ mod field { vec![Layer::Map("HashMap".into()), Layer::Option] ); assert_eq!( - get_field_nested_ident(&field).to_string(), + get_field_nested_ident(field).to_string(), "CustomNestedConfig" ); @@ -459,6 +475,6 @@ mod field { field.value.layers, vec![Layer::Map("HashMap".into()), Layer::Map("FxHashMap".into())] ); - assert_eq!(get_field_nested_ident(&field).to_string(), "NestedConfig"); + assert_eq!(get_field_nested_ident(field).to_string(), "NestedConfig"); } } diff --git a/crates/core/tests/field_validate_test.rs b/crates/core/tests/field_validate_test.rs new file mode 100644 index 00000000..29c45e40 --- /dev/null +++ b/crates/core/tests/field_validate_test.rs @@ -0,0 +1,46 @@ +use schematic_core::container::Container; +use syn::parse_quote; + +mod field_validate { + use super::*; + + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(validate = func_ref)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.validate.is_some()); + } + + #[test] + fn accepts_curried_func() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(validate = func_call())] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.validate.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(validate = 123)] + a: String, + } + }); + } +} From 6442ccdcaee87ee165f04a53adc4e9c1793d73b4 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Tue, 24 Jun 2025 16:08:37 -0700 Subject: [PATCH 11/33] Add partial tests. --- crates/core/src/args.rs | 25 ++++++++- crates/core/src/container.rs | 4 +- crates/core/src/field.rs | 3 +- crates/core/tests/container_partial_test.rs | 19 +++++++ crates/core/tests/field_partial_test.rs | 22 ++++++++ ...tial_test__container_partial__can_set.snap | 54 +++++++++++++++++++ ..._partial_test__field_partial__can_set.snap | 54 +++++++++++++++++++ 7 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 crates/core/tests/container_partial_test.rs create mode 100644 crates/core/tests/field_partial_test.rs create mode 100644 crates/core/tests/snapshots/container_partial_test__container_partial__can_set.snap create mode 100644 crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap diff --git a/crates/core/src/args.rs b/crates/core/src/args.rs index ec96ba4d..0097fe8f 100644 --- a/crates/core/src/args.rs +++ b/crates/core/src/args.rs @@ -1,7 +1,7 @@ use darling::ast::NestedMeta; use darling::{FromAttributes, FromDeriveInput, FromMeta}; use proc_macro2::TokenStream; -use quote::quote; +use quote::{ToTokens, quote}; #[derive(Clone, Copy, Debug)] pub enum SerdeIoDirection { @@ -119,6 +119,29 @@ pub struct SerdeFieldArgs { pub untagged: bool, } +#[derive(Debug, Default)] +pub struct PartialArg { + meta: Vec, +} + +impl ToTokens for PartialArg { + fn to_tokens(&self, tokens: &mut TokenStream) { + let attrs: Vec<_> = self.meta.iter().map(|m| m.to_token_stream()).collect(); + + if !attrs.is_empty() { + tokens.extend(quote! {#[#(#attrs),*]}); + } + } +} + +impl FromMeta for PartialArg { + fn from_list(items: &[NestedMeta]) -> darling::Result { + Ok(Self { + meta: items.to_vec(), + }) + } +} + // // #[config()], #[schematic()] // #[derive(FromDeriveInput, Default)] // #[darling( diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 1c9db44b..0343d200 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -1,4 +1,4 @@ -use crate::args::{SerdeContainerArgs, SerdeRenameArg}; +use crate::args::{PartialArg, SerdeContainerArgs, SerdeRenameArg}; use crate::field::Field; use crate::utils::is_inheritable_attribute; use crate::variant::Variant; @@ -19,9 +19,9 @@ pub struct ContainerArgs { // config pub allow_unknown_fields: bool, pub context: Option, - // pub partial: PartialAttr, // TODO #[cfg(feature = "env")] pub env_prefix: Option, + pub partial: Option, // serde pub rename: Option, diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 9425ed7f..3541b23c 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -1,4 +1,4 @@ -use crate::args::{SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; +use crate::args::{PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; use crate::container::ContainerArgs; use crate::field_value::FieldValue; use crate::utils::{preserve_str_literal, to_type_string}; @@ -90,6 +90,7 @@ pub struct FieldArgs { pub nested: Option, #[cfg(feature = "env")] pub parse_env: Option, + pub partial: Option, pub required: bool, pub transform: Option, #[cfg(feature = "validate")] diff --git a/crates/core/tests/container_partial_test.rs b/crates/core/tests/container_partial_test.rs new file mode 100644 index 00000000..50dc3d69 --- /dev/null +++ b/crates/core/tests/container_partial_test.rs @@ -0,0 +1,19 @@ +use schematic_core::container::Container; +use starbase_sandbox::assert_debug_snapshot; +use syn::parse_quote; + +mod container_partial { + use super::*; + + #[test] + fn can_set() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(partial(derive(Other), serde(another)))] + struct Example {} + }); + + assert!(container.args.partial.is_some()); + assert_debug_snapshot!(container.args.partial.as_ref().unwrap()); + } +} diff --git a/crates/core/tests/field_partial_test.rs b/crates/core/tests/field_partial_test.rs new file mode 100644 index 00000000..8530c202 --- /dev/null +++ b/crates/core/tests/field_partial_test.rs @@ -0,0 +1,22 @@ +use schematic_core::container::Container; +use starbase_sandbox::assert_debug_snapshot; +use syn::parse_quote; + +mod field_partial { + use super::*; + + #[test] + fn can_set() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(partial(other(attribute), and(another)))] + a: bool, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.partial.is_some()); + assert_debug_snapshot!(field.args.partial.as_ref().unwrap()); + } +} diff --git a/crates/core/tests/snapshots/container_partial_test__container_partial__can_set.snap b/crates/core/tests/snapshots/container_partial_test__container_partial__can_set.snap new file mode 100644 index 00000000..30e64c13 --- /dev/null +++ b/crates/core/tests/snapshots/container_partial_test__container_partial__can_set.snap @@ -0,0 +1,54 @@ +--- +source: crates/core/tests/container_partial_test.rs +expression: container.args.partial.as_ref().unwrap() +--- +PartialArg { + meta: [ + Meta( + Meta::List { + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident( + derive, + ), + arguments: PathArguments::None, + }, + ], + }, + delimiter: MacroDelimiter::Paren( + Paren, + ), + tokens: TokenStream [ + Ident { + sym: Other, + }, + ], + }, + ), + Meta( + Meta::List { + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident( + serde, + ), + arguments: PathArguments::None, + }, + ], + }, + delimiter: MacroDelimiter::Paren( + Paren, + ), + tokens: TokenStream [ + Ident { + sym: another, + }, + ], + }, + ), + ], +} diff --git a/crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap b/crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap new file mode 100644 index 00000000..35d03902 --- /dev/null +++ b/crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap @@ -0,0 +1,54 @@ +--- +source: crates/core/tests/field_partial_test.rs +expression: field.args.partial.as_ref().unwrap() +--- +PartialArg { + meta: [ + Meta( + Meta::List { + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident( + other, + ), + arguments: PathArguments::None, + }, + ], + }, + delimiter: MacroDelimiter::Paren( + Paren, + ), + tokens: TokenStream [ + Ident { + sym: attribute, + }, + ], + }, + ), + Meta( + Meta::List { + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident( + and, + ), + arguments: PathArguments::None, + }, + ], + }, + delimiter: MacroDelimiter::Paren( + Paren, + ), + tokens: TokenStream [ + Ident { + sym: another, + }, + ], + }, + ), + ], +} From e4cebf49884a98330a223b861b73590458b8394a Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sat, 28 Jun 2025 11:19:44 -0700 Subject: [PATCH 12/33] Add default impls. --- CHANGELOG.md | 4 ++++ crates/schematic/src/config/configs.rs | 20 +++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcf0c262..ae610bd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,16 @@ #### 🚀 Updates +##### Config + - Added support for `#[setting(nested = NestedConfig)]` on fields, where the nested config name can be explicitly defined if we fail to detect it. This is useful for extremely complex/composed types. - Added support for env prefixes at the field level when the field is also nested. This will override the env prefix defined on the nested container: `#[setting(nested, env_prefix = "OVERRIDE_")]`. +- Updated the methods of `PartialConfig` to all have a default implementation. This helps to greatly + reduce the amount of macro generated code. - Improved the parse, handling, and validation of container and field attributes. ##### Serde diff --git a/crates/schematic/src/config/configs.rs b/crates/schematic/src/config/configs.rs index e7bcf85a..5553a72e 100644 --- a/crates/schematic/src/config/configs.rs +++ b/crates/schematic/src/config/configs.rs @@ -18,7 +18,9 @@ pub trait PartialConfig: /// marked with `#[setting(default)]`. Unmarked settings will be [`None`]. /// /// If a default value fails to parse or cast into the correct type, an error is returned. - fn default_values(context: &Self::Context) -> Result, ConfigError>; + fn default_values(_context: &Self::Context) -> Result, ConfigError> { + Ok(None) + } /// Return a partial configuration with values populated from environment variables /// for settings marked with `#[setting(env)]`. Unmarked settings will be [`None`]. @@ -26,19 +28,25 @@ pub trait PartialConfig: /// If an environment variable does not exist, the value will be [`None`]. If /// the variable fails to parse or cast into the correct type, an error is returned. #[cfg(feature = "env")] - fn env_values() -> Result, ConfigError>; + fn env_values() -> Result, ConfigError> { + Ok(None) + } /// When a setting is marked as extendable with `#[setting(extend)]`, this returns /// [`ExtendsFrom`] with the extended sources, either a list of strings or a single string. /// When no setting is extendable, this returns [`None`]. #[cfg(feature = "extends")] - fn extends_from(&self) -> Option; + fn extends_from(&self) -> Option { + None + } /// Finalize the partial configuration by consuming it and populating all fields with a value. /// Defaults values from [`PartialConfig::default_values`] will be applied first, followed /// by merging the current partial, and lastly environment variable values from /// [`PartialConfig::env_values`]. - fn finalize(self, context: &Self::Context) -> Result; + fn finalize(self, _context: &Self::Context) -> Result { + Ok(self) + } /// Merge another partial configuration into this one and clone values when applicable. The /// following merge strategies are applied: @@ -46,7 +54,9 @@ pub trait PartialConfig: /// - Current [`None`] values are replaced with the next value if [`Some`]. /// - Current [`Some`] values are merged with the next value if [`Some`], /// using the merge function from `#[setting(merge)]`. - fn merge(&mut self, context: &Self::Context, next: Self) -> Result<(), ConfigError>; + fn merge(&mut self, _context: &Self::Context, _next: Self) -> Result<(), ConfigError> { + Ok(()) + } /// Recursively validate the configuration with the provided context. /// Validation should be done on the final state, after merging partials. From 042075d71982e1d715e4a746b30442265236e738 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sat, 28 Jun 2025 16:07:04 -0700 Subject: [PATCH 13/33] Start default impl. --- Cargo.lock | 11 ++ Cargo.toml | 2 +- crates/core/Cargo.toml | 1 + crates/core/src/container.rs | 103 +++++++++- crates/core/src/field.rs | 46 ++--- crates/core/src/field_value.rs | 67 ++++++- crates/core/src/utils.rs | 9 + crates/core/tests/field_default_test.rs | 187 ++++++++++-------- ...tial_test__container_partial__can_set.snap | 54 ----- ...fault__named_struct__supports_array-2.snap | 13 ++ ...efault__named_struct__supports_array.snap} | 0 ...efault__named_struct__supports_bool-2.snap | 9 + ...default__named_struct__supports_bool.snap} | 0 ...ault__named_struct__supports_number-2.snap | 9 + ...fault__named_struct__supports_number.snap} | 0 ...ault__named_struct__supports_string-2.snap | 17 ++ ...fault__named_struct__supports_string.snap} | 0 ...fault__named_struct__supports_tuple-2.snap | 9 + ...efault__named_struct__supports_tuple.snap} | 0 ...default__named_struct__supports_vec-2.snap | 9 + ..._default__named_struct__supports_vec.snap} | 0 ..._partial_test__field_partial__can_set.snap | 54 ----- crates/core/tests/utils.rs | 5 + crates/schematic/src/config/configs.rs | 10 + 24 files changed, 394 insertions(+), 221 deletions(-) delete mode 100644 crates/core/tests/snapshots/container_partial_test__container_partial__can_set.snap create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_array-2.snap rename crates/core/tests/snapshots/{field_default_test__field_default__supports_array.snap => field_default_test__field_default__named_struct__supports_array.snap} (100%) create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool-2.snap rename crates/core/tests/snapshots/{field_default_test__field_default__supports_bool.snap => field_default_test__field_default__named_struct__supports_bool.snap} (100%) create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_number-2.snap rename crates/core/tests/snapshots/{field_default_test__field_default__supports_number.snap => field_default_test__field_default__named_struct__supports_number.snap} (100%) create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_string-2.snap rename crates/core/tests/snapshots/{field_default_test__field_default__supports_string.snap => field_default_test__field_default__named_struct__supports_string.snap} (100%) create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple-2.snap rename crates/core/tests/snapshots/{field_default_test__field_default__supports_tuple.snap => field_default_test__field_default__named_struct__supports_tuple.snap} (100%) create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_vec-2.snap rename crates/core/tests/snapshots/{field_default_test__field_default__supports_vec.snap => field_default_test__field_default__named_struct__supports_vec.snap} (100%) delete mode 100644 crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap create mode 100644 crates/core/tests/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 630d5a40..5d6104b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1341,6 +1341,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +dependencies = [ + "proc-macro2", + "syn 2.0.114", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -1936,6 +1946,7 @@ name = "schematic_core" version = "0.18.7" dependencies = [ "darling", + "prettyplease", "proc-macro2", "quote", "schematic_core", diff --git a/Cargo.toml b/Cargo.toml index 54a119da..aef46406 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ serde_json = "1.0.149" serde_yaml = "0.9.33" serde_norway = "0.9.42" starbase_sandbox = "0.10.7" -syn = "2.0.101" +syn = "2.0.111" toml = "1.1.2" tracing = "0.1.44" url = "2.5.8" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 813207f3..c5d19b6a 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -26,6 +26,7 @@ schematic_core = { path = ".", features = [ "tracing", "validate", ] } +prettyplease = "0.2.35" syn = { workspace = true, features = ["full", "extra-traits"] } starbase_sandbox = { workspace = true } diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 0343d200..99760b67 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -1,10 +1,10 @@ use crate::args::{PartialArg, SerdeContainerArgs, SerdeRenameArg}; use crate::field::Field; -use crate::utils::is_inheritable_attribute; +use crate::utils::{impl_struct_default, is_inheritable_attribute}; use crate::variant::Variant; use darling::FromDeriveInput; use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; +use quote::{ToTokens, format_ident, quote}; use std::rc::Rc; use syn::{Attribute, Data, DeriveInput, ExprPath, Fields, Ident, Visibility}; @@ -172,6 +172,105 @@ impl Container { #(#meta),* } } + + pub fn impl_partial(&self) -> TokenStream { + let base_name = &self.ident; + let partial_name = format_ident!("Partial{base_name}"); + let context = match self.args.context.as_ref() { + Some(ctx) => quote! { #ctx }, + None => quote! { () }, + }; + + let default_values_method = self.impl_partial_default_values(); + + quote! { + #[automatically_derived] + impl schematic::PartialConfig for #partial_name { + type Context = #context; + + #default_values_method + } + + #[automatically_derived] + impl schematic::Config for #base_name { + // TODO + } + + #[automatically_derived] + impl Default for #base_name { + fn default() -> Self { + ::from_partial( + ::default_partial() + ) + } + } + } + } + + pub fn impl_partial_default_values(&self) -> TokenStream { + let inner = match &self.inner { + ContainerInner::NamedStruct { fields } => { + let mut rows = vec![]; + + for field in fields { + if let Some(value) = field.impl_partial_default_value() { + let name = field.ident.as_ref().unwrap(); + + rows.push(quote! { + #name: #value, + }); + } + } + + if rows.is_empty() { + return quote! {}; + } + + let default_row = impl_struct_default(rows.len() != fields.len()); + + quote! { + Ok(Some(Self { + #(#rows)* + #default_row + })) + } + } + ContainerInner::UnnamedStruct { fields } => { + let mut rows = vec![]; + let mut all_none = true; + + for field in fields { + if let Some(value) = field.impl_partial_default_value() { + all_none = false; + + rows.push(quote! { + #value + }); + } else { + rows.push(quote! { None }) + } + } + + if all_none { + return quote! {}; + } + + quote! { + Ok(Some(Self( + #(#rows),* + ))) + } + } + ContainerInner::Enum { .. } => todo!(), + ContainerInner::UnitEnum { .. } => todo!(), + }; + + quote! { + fn default_values(context: &Self::Context) -> std::result::Result, schematic::ConfigError> { + #inner + } + } + } } impl ToTokens for Container { diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 3541b23c..55489d36 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -4,12 +4,10 @@ use crate::field_value::FieldValue; use crate::utils::{preserve_str_literal, to_type_string}; use darling::{FromAttributes, FromMeta}; use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; +use quote::ToTokens; use std::ops::Deref; use std::rc::Rc; -use syn::{ - Attribute, Expr, ExprPath, Field as NativeField, FieldMutability, Ident, Visibility, parse_str, -}; +use syn::{Attribute, Expr, ExprPath, Field as NativeField, FieldMutability, Ident, Visibility}; // #[setting(nested)] #[derive(Debug)] @@ -183,10 +181,6 @@ impl Field { // nested if self.args.nested.is_some() { - if self.args.default.is_some() { - panic!("Cannot use `default` with `nested`."); - } - #[cfg(feature = "env")] if self.args.env.is_some() { panic!("Cannot use `env` with `nested`, use `env_prefix` instead?"); @@ -203,25 +197,31 @@ impl Field { } } -impl ToTokens for Field { - fn to_tokens(&self, tokens: &mut TokenStream) { - let mut value = self.value.ty_string.clone(); +// impl ToTokens for Field { +// fn to_tokens(&self, tokens: &mut TokenStream) { +// let mut value = self.value.ty_string.clone(); - if let Some(nested_ident) = &self.value.nested_ident { - let ident = nested_ident.to_string(); +// if let Some(nested_ident) = &self.value.nested_ident { +// let ident = nested_ident.to_string(); - value = value.replace(&ident, &format!("<{ident} as schematic::Config>::Partial")); - } +// value = value.replace(&ident, &format!("<{ident} as schematic::Config>::Partial")); +// } - if !self.value.is_outer_option_wrapped() { - value = format!("Option<{value}>"); - } +// if !self.value.is_outer_option_wrapped() { +// value = format!("Option<{value}>"); +// } + +// let key = self.ident.as_ref().unwrap(); +// let value: TokenStream = parse_str(&value).unwrap(); - let key = self.ident.as_ref().unwrap(); - let value: TokenStream = parse_str(&value).unwrap(); +// tokens.extend(quote! { +// pub #key: #value, +// }); +// } +// } - tokens.extend(quote! { - pub #key: #value, - }); +impl Field { + pub fn impl_partial_default_value(&self) -> Option { + self.value.impl_partial_default_value(&self.args) } } diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index d7d8076e..764b9ac7 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -1,7 +1,8 @@ -use crate::field::FieldNestedArg; +use crate::field::{FieldArgs, FieldNestedArg}; use crate::utils::to_type_string; -use quote::ToTokens; -use syn::{GenericArgument, Ident, PathArguments, PathSegment, Type}; +use proc_macro2::TokenStream; +use quote::{ToTokens, format_ident, quote}; +use syn::{Expr, GenericArgument, Ident, Lit, PathArguments, PathSegment, Type}; #[derive(Debug, PartialEq)] pub enum Layer { @@ -82,6 +83,66 @@ impl FieldValue { .first() .is_some_and(|wrapper| *wrapper == Layer::Option) } + + pub fn impl_partial_default_value(&self, field_args: &FieldArgs) -> Option { + if self.is_outer_option_wrapped() { + return None; + }; + + // Extract the inner value first + let mut value = if let Some(nested_ident) = &self.nested_ident { + if field_args.default.is_some() { + panic!("Cannot use `default` with `nested`."); + } + + quote! { + <#nested_ident as schematic::PartialConfig>::default_values(content)? + } + } else if let Some(expr) = &field_args.default { + match expr { + Expr::Array(_) | Expr::Call(_) | Expr::Macro(_) | Expr::Tuple(_) => { + quote! { #expr } + } + Expr::Path(func) => { + quote! { schematic::internal::handle_default_result(#func(context))? } + } + Expr::Lit(lit) => match &lit.lit { + Lit::Str(string) => quote! { + schematic::internal::handle_default_result(std::convert::TryFrom::try_from(#string))? + }, + other => quote! { #other }, + }, + invalid => { + panic!( + "Unsupported default value ({invalid:?}). May only provide literals, primitives, arrays, or tuples." + ); + } + } + } else { + quote! { + Default::default() + } + }; + + // Then wrap with each layer + for layer in self.layers.iter().rev() { + value = match layer { + Layer::Arc => quote! { Arc::new(#value) }, + Layer::Box => quote! { Box::new(#value) }, + Layer::Option => quote! { Some(#value) }, + Layer::Rc => quote! { Rc::new(#value) }, + Layer::Map(name) | Layer::Set(name) | Layer::Vec(name) | Layer::Unknown(name) => { + let collection = format_ident!("{name}"); + + quote! { #collection::default() } + } + }; + } + + Some(quote! { + Some(#value) + }) + } } fn extract_type_information( diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index 5943b6c0..c89534a5 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -1,4 +1,5 @@ use proc_macro2::TokenStream; +use quote::quote; use syn::{Attribute, Expr, Meta, Path}; pub fn get_meta_path(meta: &Meta) -> &Path { @@ -36,3 +37,11 @@ pub fn to_type_string(ts: TokenStream) -> String { .replace("> ", ">") .replace(" >", ">") } + +pub fn impl_struct_default(show: bool) -> TokenStream { + if show { + quote! { ..Default::default() } + } else { + quote! {} + } +} diff --git a/crates/core/tests/field_default_test.rs b/crates/core/tests/field_default_test.rs index 5270f64f..9460128a 100644 --- a/crates/core/tests/field_default_test.rs +++ b/crates/core/tests/field_default_test.rs @@ -1,97 +1,116 @@ +mod utils; + use schematic_core::container::Container; -use starbase_sandbox::assert_debug_snapshot; +use starbase_sandbox::{assert_debug_snapshot, assert_snapshot}; use syn::parse_quote; +use utils::pretty; mod field_default { use super::*; - #[test] - fn supports_bool() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(default = true)] - a: bool, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.default.is_some()); - assert_debug_snapshot!(field.args.default.as_ref().unwrap()); - } + mod named_struct { + use super::*; - #[test] - fn supports_number() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(default = 100)] - a: usize, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.default.is_some()); - assert_debug_snapshot!(field.args.default.as_ref().unwrap()); - } + #[test] + fn supports_bool() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = true)] + a: bool, + } + }); + let field = container.inner.get_fields()[0]; - #[test] - fn supports_string() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(default = "abc")] - a: String, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.default.is_some()); - assert_debug_snapshot!(field.args.default.as_ref().unwrap()); - } + assert!(field.args.default.is_some()); + assert_debug_snapshot!(field.args.default.as_ref().unwrap()); - #[test] - fn supports_array() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(default = ["a".into(), "b".into(), "c".into()])] - a: [String; 3], - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.default.is_some()); - assert_debug_snapshot!(field.args.default.as_ref().unwrap()); - } + assert_snapshot!(pretty(container.impl_partial_default_values())); + } - #[test] - fn supports_vec() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(default = vec!["a", "b", "c"])] - a: Vec, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.default.is_some()); - assert_debug_snapshot!(field.args.default.as_ref().unwrap()); - } + #[test] + fn supports_number() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = 100)] + a: usize, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.default.is_some()); + assert_debug_snapshot!(field.args.default.as_ref().unwrap()); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + + #[test] + fn supports_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = "abc")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.default.is_some()); + assert_debug_snapshot!(field.args.default.as_ref().unwrap()); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + + #[test] + fn supports_array() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = ["a".into(), "b".into(), "c".into()])] + a: [String; 3], + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.default.is_some()); + assert_debug_snapshot!(field.args.default.as_ref().unwrap()); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + + #[test] + fn supports_vec() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = vec!["a", "b", "c"])] + a: Vec, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.default.is_some()); + assert_debug_snapshot!(field.args.default.as_ref().unwrap()); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + + #[test] + fn supports_tuple() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = (10, -10, 0))] + a: (usize, isize, u8), + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.default.is_some()); + assert_debug_snapshot!(field.args.default.as_ref().unwrap()); - #[test] - fn supports_tuple() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(default = (10, -10, 0))] - a: (usize, isize, u8), - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.default.is_some()); - assert_debug_snapshot!(field.args.default.as_ref().unwrap()); + assert_snapshot!(pretty(container.impl_partial_default_values())); + } } } diff --git a/crates/core/tests/snapshots/container_partial_test__container_partial__can_set.snap b/crates/core/tests/snapshots/container_partial_test__container_partial__can_set.snap deleted file mode 100644 index 30e64c13..00000000 --- a/crates/core/tests/snapshots/container_partial_test__container_partial__can_set.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: crates/core/tests/container_partial_test.rs -expression: container.args.partial.as_ref().unwrap() ---- -PartialArg { - meta: [ - Meta( - Meta::List { - path: Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident( - derive, - ), - arguments: PathArguments::None, - }, - ], - }, - delimiter: MacroDelimiter::Paren( - Paren, - ), - tokens: TokenStream [ - Ident { - sym: Other, - }, - ], - }, - ), - Meta( - Meta::List { - path: Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident( - serde, - ), - arguments: PathArguments::None, - }, - ], - }, - delimiter: MacroDelimiter::Paren( - Paren, - ), - tokens: TokenStream [ - Ident { - sym: another, - }, - ], - }, - ), - ], -} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_array-2.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_array-2.snap new file mode 100644 index 00000000..13190c3a --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_array-2.snap @@ -0,0 +1,13 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- +fn default_values( + context: &Self::Context, +) -> std::result::Result, schematic::ConfigError> { + Ok( + Some(Self { + a: Some(["a".into(), "b".into(), "c".into()]), + }), + ) +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__supports_array.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_array.snap similarity index 100% rename from crates/core/tests/snapshots/field_default_test__field_default__supports_array.snap rename to crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_array.snap diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool-2.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool-2.snap new file mode 100644 index 00000000..29dd2fdb --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool-2.snap @@ -0,0 +1,9 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- +fn default_values( + context: &Self::Context, +) -> std::result::Result, schematic::ConfigError> { + Ok(Some(Self { a: Some(true) })) +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__supports_bool.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool.snap similarity index 100% rename from crates/core/tests/snapshots/field_default_test__field_default__supports_bool.snap rename to crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool.snap diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_number-2.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_number-2.snap new file mode 100644 index 00000000..8cf68d77 --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_number-2.snap @@ -0,0 +1,9 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- +fn default_values( + context: &Self::Context, +) -> std::result::Result, schematic::ConfigError> { + Ok(Some(Self { a: Some(100) })) +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__supports_number.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_number.snap similarity index 100% rename from crates/core/tests/snapshots/field_default_test__field_default__supports_number.snap rename to crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_number.snap diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_string-2.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_string-2.snap new file mode 100644 index 00000000..32d821aa --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_string-2.snap @@ -0,0 +1,17 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- +fn default_values( + context: &Self::Context, +) -> std::result::Result, schematic::ConfigError> { + Ok( + Some(Self { + a: Some( + schematic::internal::handle_default_result( + std::convert::TryFrom::try_from("abc"), + )?, + ), + }), + ) +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__supports_string.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_string.snap similarity index 100% rename from crates/core/tests/snapshots/field_default_test__field_default__supports_string.snap rename to crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_string.snap diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple-2.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple-2.snap new file mode 100644 index 00000000..2c2e7405 --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple-2.snap @@ -0,0 +1,9 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- +fn default_values( + context: &Self::Context, +) -> std::result::Result, schematic::ConfigError> { + Ok(Some(Self { a: Some((10, -10, 0)) })) +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__supports_tuple.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple.snap similarity index 100% rename from crates/core/tests/snapshots/field_default_test__field_default__supports_tuple.snap rename to crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple.snap diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_vec-2.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_vec-2.snap new file mode 100644 index 00000000..73bdc075 --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_vec-2.snap @@ -0,0 +1,9 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- +fn default_values( + context: &Self::Context, +) -> std::result::Result, schematic::ConfigError> { + Ok(Some(Self { a: Some(Vec::default()) })) +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__supports_vec.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_vec.snap similarity index 100% rename from crates/core/tests/snapshots/field_default_test__field_default__supports_vec.snap rename to crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_vec.snap diff --git a/crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap b/crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap deleted file mode 100644 index 35d03902..00000000 --- a/crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: crates/core/tests/field_partial_test.rs -expression: field.args.partial.as_ref().unwrap() ---- -PartialArg { - meta: [ - Meta( - Meta::List { - path: Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident( - other, - ), - arguments: PathArguments::None, - }, - ], - }, - delimiter: MacroDelimiter::Paren( - Paren, - ), - tokens: TokenStream [ - Ident { - sym: attribute, - }, - ], - }, - ), - Meta( - Meta::List { - path: Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident( - and, - ), - arguments: PathArguments::None, - }, - ], - }, - delimiter: MacroDelimiter::Paren( - Paren, - ), - tokens: TokenStream [ - Ident { - sym: another, - }, - ], - }, - ), - ], -} diff --git a/crates/core/tests/utils.rs b/crates/core/tests/utils.rs new file mode 100644 index 00000000..aa8e023b --- /dev/null +++ b/crates/core/tests/utils.rs @@ -0,0 +1,5 @@ +use proc_macro2::TokenStream; + +pub fn pretty(tokens: TokenStream) -> String { + prettyplease::unparse(&syn::parse_file(&tokens.to_string()).unwrap()) +} diff --git a/crates/schematic/src/config/configs.rs b/crates/schematic/src/config/configs.rs index 5553a72e..45d8b11c 100644 --- a/crates/schematic/src/config/configs.rs +++ b/crates/schematic/src/config/configs.rs @@ -92,6 +92,16 @@ pub trait PartialConfig: pub trait Config: Sized + Schematic { type Partial: PartialConfig; + /// Return default values for the partial configuration. + fn default_partial() -> Self::Partial { + let context = <::Partial as PartialConfig>::Context::default(); + let defaults = <::Partial as PartialConfig>::default_values(&context) + .unwrap() + .unwrap_or_default(); + + defaults + } + /// Convert a partial configuration into a full configuration, with all values populated. fn from_partial(partial: Self::Partial) -> Self; From 9d34182986236540a3a8435e24cc99354ee199f9 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 29 Jun 2025 11:01:14 -0700 Subject: [PATCH 14/33] Test default values. --- CHANGELOG.md | 2 + crates/core/src/container.rs | 74 ++++++-- crates/core/src/field.rs | 5 +- crates/core/src/field_value.rs | 134 ++++++++----- crates/core/src/utils.rs | 34 +++- crates/core/src/variant.rs | 48 ++++- crates/core/tests/field_default_test.rs | 162 ++++++++++------ ...__field_default__handles_collections.snap} | 10 +- ...t_test__field_default__handles_layers.snap | 17 ++ ...renders_nothing_if_all_option_wrapped.snap | 5 + ...default__named_struct__supports_array.snap | 60 ------ ...efault__named_struct__supports_bool-2.snap | 9 - ..._default__named_struct__supports_bool.snap | 10 - ...efault__named_struct__supports_number.snap | 10 - ...efault__named_struct__supports_string.snap | 10 - ...fault__named_struct__supports_tuple-2.snap | 9 - ...default__named_struct__supports_tuple.snap | 36 ---- ...fault__named_struct__supports_types-2.snap | 178 ++++++++++++++++++ ...default__named_struct__supports_types.snap | 20 ++ ...d_default__named_struct__supports_vec.snap | 43 ----- ...field_default__supports_handler_func.snap} | 3 +- ...renders_nothing_if_all_option_wrapped.snap | 5 + ...ult__unnamed_struct__supports_types-2.snap | 164 ++++++++++++++++ ...fault__unnamed_struct__supports_types.snap | 22 +++ ...ult_test__variant_default__unit_enum.snap} | 4 +- ..._test__variant_default__unnamed_enum.snap} | 4 +- crates/core/tests/utils.rs | 11 ++ crates/core/tests/variant_default_test.rs | 80 ++++++++ crates/schematic/src/internal.rs | 1 - 29 files changed, 841 insertions(+), 329 deletions(-) rename crates/core/tests/snapshots/{field_default_test__field_default__named_struct__supports_string-2.snap => field_default_test__field_default__handles_collections.snap} (56%) create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__handles_layers.snap create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__renders_nothing_if_all_option_wrapped.snap delete mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_array.snap delete mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool-2.snap delete mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool.snap delete mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_number.snap delete mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_string.snap delete mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple-2.snap delete mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple.snap create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types-2.snap create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types.snap delete mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_vec.snap rename crates/core/tests/snapshots/{field_default_test__field_default__named_struct__supports_array-2.snap => field_default_test__field_default__supports_handler_func.snap} (75%) create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types-2.snap create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types.snap rename crates/core/tests/snapshots/{field_default_test__field_default__named_struct__supports_number-2.snap => variant_default_test__variant_default__unit_enum.snap} (68%) rename crates/core/tests/snapshots/{field_default_test__field_default__named_struct__supports_vec-2.snap => variant_default_test__variant_default__unnamed_enum.snap} (61%) create mode 100644 crates/core/tests/variant_default_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ae610bd7..67aba1a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ ##### Config +- Added support for unnamed tuple and newtype structs. Unnamed fields within the struct support + `#[setting]`. - Added support for `#[setting(nested = NestedConfig)]` on fields, where the nested config name can be explicitly defined if we fail to detect it. This is useful for extremely complex/composed types. diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 99760b67..f381b149 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -1,6 +1,6 @@ use crate::args::{PartialArg, SerdeContainerArgs, SerdeRenameArg}; use crate::field::Field; -use crate::utils::{impl_struct_default, is_inheritable_attribute}; +use crate::utils::{ImplResult, is_inheritable_attribute}; use crate::variant::Variant; use darling::FromDeriveInput; use proc_macro2::TokenStream; @@ -10,11 +10,7 @@ use syn::{Attribute, Data, DeriveInput, ExprPath, Fields, Ident, Visibility}; // #[config()], #[schematic()] #[derive(Debug, Default, FromDeriveInput)] -#[darling( - default, - attributes(config, schematic), - supports(struct_named, enum_any) -)] +#[darling(default, attributes(config, schematic), supports(struct_any, enum_any))] pub struct ContainerArgs { // config pub allow_unknown_fields: bool, @@ -208,25 +204,34 @@ impl Container { } pub fn impl_partial_default_values(&self) -> TokenStream { + let mut requires_internal = false; + let inner = match &self.inner { ContainerInner::NamedStruct { fields } => { let mut rows = vec![]; for field in fields { - if let Some(value) = field.impl_partial_default_value() { + let res = field.impl_partial_default_value(); + + if !res.no_value { let name = field.ident.as_ref().unwrap(); + let value = res.value; rows.push(quote! { #name: #value, }); } + + if res.requires_internal { + requires_internal = true; + } } if rows.is_empty() { return quote! {}; } - let default_row = impl_struct_default(rows.len() != fields.len()); + let default_row = ImplResult::impl_struct_default(rows.len() != fields.len()); quote! { Ok(Some(Self { @@ -240,14 +245,21 @@ impl Container { let mut all_none = true; for field in fields { - if let Some(value) = field.impl_partial_default_value() { + let res = field.impl_partial_default_value(); + + if res.no_value { + rows.push(quote! { None }); + } else { all_none = false; + let value = res.value; rows.push(quote! { #value }); - } else { - rows.push(quote! { None }) + } + + if res.requires_internal { + requires_internal = true; } } @@ -261,12 +273,48 @@ impl Container { ))) } } - ContainerInner::Enum { .. } => todo!(), - ContainerInner::UnitEnum { .. } => todo!(), + ContainerInner::Enum { variants } | ContainerInner::UnitEnum { variants } => { + let default_variants = variants + .iter() + .filter(|v| v.is_default()) + .collect::>(); + + if default_variants.len() > 1 { + panic!("Only 1 variant may be marked as default."); + } + + match default_variants.get(0) { + Some(default_variant) => { + let res = default_variant.impl_partial_default_value(); + + if res.requires_internal { + requires_internal = true; + } + + if res.no_value { + quote! { + Ok(None) + } + } else { + let value = res.value; + + quote! { + Ok(Some(Self::#value)) + } + } + } + None => quote! { + Ok(None) + }, + } + } }; + let internal = ImplResult::impl_use_internal(requires_internal); + quote! { fn default_values(context: &Self::Context) -> std::result::Result, schematic::ConfigError> { + #internal #inner } } diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 55489d36..16566c38 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -1,9 +1,8 @@ use crate::args::{PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; use crate::container::ContainerArgs; use crate::field_value::FieldValue; -use crate::utils::{preserve_str_literal, to_type_string}; +use crate::utils::{ImplResult, preserve_str_literal, to_type_string}; use darling::{FromAttributes, FromMeta}; -use proc_macro2::TokenStream; use quote::ToTokens; use std::ops::Deref; use std::rc::Rc; @@ -221,7 +220,7 @@ impl Field { // } impl Field { - pub fn impl_partial_default_value(&self) -> Option { + pub fn impl_partial_default_value(&self) -> ImplResult { self.value.impl_partial_default_value(&self.args) } } diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index 764b9ac7..f322ab10 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -1,6 +1,5 @@ use crate::field::{FieldArgs, FieldNestedArg}; -use crate::utils::to_type_string; -use proc_macro2::TokenStream; +use crate::utils::{ImplResult, to_type_string}; use quote::{ToTokens, format_ident, quote}; use syn::{Expr, GenericArgument, Ident, Lit, PathArguments, PathSegment, Type}; @@ -19,6 +18,7 @@ pub enum Layer { #[derive(Debug)] pub struct FieldValue { + pub inner_ty: Option, pub layers: Vec, pub nested: bool, pub nested_ident: Option, @@ -30,7 +30,6 @@ impl FieldValue { pub fn new(ty: Type, nested_arg: Option<&FieldNestedArg>) -> Self { let mut nested = false; let mut nested_ident = None; - let mut layers = vec![]; let ty_string = to_type_string(ty.to_token_stream()); // Determine nested state @@ -52,30 +51,37 @@ impl FieldValue { }; } - // Extract type information - if let Some(custom_ident) = - extract_type_information(&ty, &mut layers, nested && nested_ident.is_none()) - { - nested_ident = Some(custom_ident); - } - - if nested_ident.is_none() && nested { - panic!( - "Unable to extract the nested configuration identifier from `{ty_string}`. Try explicitly passing the identifier with `nested = ConfigName`." - ) - } - - let value = Self { + let mut value = FieldValue { + inner_ty: None, nested, nested_ident, - layers, + layers: vec![], ty_string, ty, }; + value.extract_type_information(); + value + } - // dbg!(&value); + pub fn extract_type_information(&mut self) { + extract_type_information(&self.ty, &mut self.layers, |ty, segment| { + self.inner_ty = Some(ty.to_owned()); - value + if self.nested && self.nested_ident.is_none() { + self.nested_ident = Some(segment.ident.clone()); + } + }); + + if self.nested && self.nested_ident.is_none() { + panic!( + "Unable to extract the nested configuration identifier from `{}`. Try explicitly passing the identifier with `nested = ConfigName`.", + self.ty_string + ) + } + } + + pub fn get_inner_type(&self) -> &Type { + self.inner_ty.as_ref().unwrap_or(&self.ty) } pub fn is_outer_option_wrapped(&self) -> bool { @@ -84,11 +90,14 @@ impl FieldValue { .is_some_and(|wrapper| *wrapper == Layer::Option) } - pub fn impl_partial_default_value(&self, field_args: &FieldArgs) -> Option { + pub fn impl_partial_default_value(&self, field_args: &FieldArgs) -> ImplResult { if self.is_outer_option_wrapped() { - return None; + return ImplResult::skipped(); }; + let mut res = ImplResult::default(); + let mut wrap_with_some = false; + // Extract the inner value first let mut value = if let Some(nested_ident) = &self.nested_ident { if field_args.default.is_some() { @@ -99,18 +108,32 @@ impl FieldValue { <#nested_ident as schematic::PartialConfig>::default_values(content)? } } else if let Some(expr) = &field_args.default { + let ty = self.get_inner_type(); + match expr { Expr::Array(_) | Expr::Call(_) | Expr::Macro(_) | Expr::Tuple(_) => { + wrap_with_some = true; + quote! { #expr } } Expr::Path(func) => { - quote! { schematic::internal::handle_default_result(#func(context))? } + res.requires_internal = true; + + quote! { handle_default_result(#func(context))? } } Expr::Lit(lit) => match &lit.lit { - Lit::Str(string) => quote! { - schematic::internal::handle_default_result(std::convert::TryFrom::try_from(#string))? - }, - other => quote! { #other }, + Lit::Str(string) => { + res.requires_internal = true; + + quote! { + handle_default_result(#ty::try_from(#string))? + } + } + other => { + wrap_with_some = true; + + quote! { #other } + } }, invalid => { panic!( @@ -119,40 +142,52 @@ impl FieldValue { } } } else { + wrap_with_some = true; + quote! { Default::default() } }; // Then wrap with each layer - for layer in self.layers.iter().rev() { - value = match layer { - Layer::Arc => quote! { Arc::new(#value) }, - Layer::Box => quote! { Box::new(#value) }, - Layer::Option => quote! { Some(#value) }, - Layer::Rc => quote! { Rc::new(#value) }, - Layer::Map(name) | Layer::Set(name) | Layer::Vec(name) | Layer::Unknown(name) => { - let collection = format_ident!("{name}"); - - quote! { #collection::default() } - } - }; + if !self.layers.is_empty() { + wrap_with_some = true; + + for layer in self.layers.iter().rev() { + value = match layer { + Layer::Arc => quote! { Arc::new(#value) }, + Layer::Box => quote! { Box::new(#value) }, + Layer::Option => quote! { Some(#value) }, + Layer::Rc => quote! { Rc::new(#value) }, + Layer::Map(name) + | Layer::Set(name) + | Layer::Vec(name) + | Layer::Unknown(name) => { + let collection = format_ident!("{name}"); + + quote! { #collection::default() } + } + }; + } } - Some(quote! { - Some(#value) - }) + if wrap_with_some { + value = quote! { Some(#value) }; + } + + res.value = value; + res } } fn extract_type_information( ty: &Type, layers: &mut Vec, - nested_ident: bool, -) -> Option { + mut on_last: impl FnMut(&Type, &PathSegment), +) { // We don't need to traverse other types, just paths let Type::Path(ty_path) = ty else { - return None; + return; }; // Extract the last segment of the path, for example `Option`, @@ -162,9 +197,7 @@ fn extract_type_information( match &last_segment.arguments { // We've reached the final segment PathArguments::None => { - if nested_ident { - return Some(last_segment.ident.clone()); - } + on_last(ty, last_segment); } // Attempt to drill deeper down @@ -172,15 +205,14 @@ fn extract_type_information( extract_layer(last_segment, layers); if let Some(GenericArgument::Type(inner_ty)) = args.args.last() { - return extract_type_information(inner_ty, layers, nested_ident); + extract_type_information(inner_ty, layers, on_last); + return; } } // What to do here, anything? PathArguments::Parenthesized(_) => {} }; - - None } fn extract_layer(last_segment: &PathSegment, layers: &mut Vec) { diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index c89534a5..b2bc1521 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -38,10 +38,34 @@ pub fn to_type_string(ts: TokenStream) -> String { .replace(" >", ">") } -pub fn impl_struct_default(show: bool) -> TokenStream { - if show { - quote! { ..Default::default() } - } else { - quote! {} +#[derive(Default)] +pub struct ImplResult { + pub requires_internal: bool, + pub no_value: bool, + pub value: TokenStream, +} + +impl ImplResult { + pub fn skipped() -> Self { + Self { + no_value: true, + ..Default::default() + } + } + + pub fn impl_struct_default(show: bool) -> TokenStream { + if show { + quote! { ..Default::default() } + } else { + quote! {} + } + } + + pub fn impl_use_internal(show: bool) -> TokenStream { + if show { + quote! { use schematic::internal::*; } + } else { + quote! {} + } } } diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index ab806629..beeb53f2 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -1,13 +1,26 @@ -use crate::args::{SerdeContainerArgs, SerdeFieldArgs}; +use crate::args::{SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; use crate::container::ContainerArgs; +use crate::utils::ImplResult; use darling::FromAttributes; +use quote::quote; use std::rc::Rc; use syn::{Attribute, Fields, Ident, Variant as NativeVariant}; // #[setting()], #[schema()] #[derive(Debug, Default, FromAttributes)] #[darling(default, attributes(setting, schema))] -pub struct VariantArgs {} +pub struct VariantArgs { + pub default: bool, + + // serde + #[darling(multiple)] + pub alias: Vec, + pub rename: Option, + pub skip: bool, + pub skip_deserializing: bool, + pub skip_serializing: bool, + pub untagged: bool, +} #[derive(Debug)] pub struct Variant { @@ -29,8 +42,8 @@ impl Variant { container_args: Rc, serde_container_args: Rc, ) -> Variant { - let args = VariantArgs::from_attributes(&variant.attrs).unwrap_or_default(); - let serde_args = SerdeFieldArgs::from_attributes(&variant.attrs).unwrap_or_default(); + let args = VariantArgs::from_attributes(&variant.attrs).unwrap(); + let serde_args = SerdeFieldArgs::from_attributes(&variant.attrs).unwrap(); Variant { args, @@ -42,4 +55,31 @@ impl Variant { value: variant.fields, } } + + pub fn is_default(&self) -> bool { + self.args.default + } + + pub fn impl_partial_default_value(&self) -> ImplResult { + let mut res = ImplResult::default(); + let name = &self.ident; + + res.value = match &self.value { + Fields::Named(_) => panic!("Enums with named fields are not supported!"), + Fields::Unnamed(fields) => { + let fields = fields + .unnamed + .iter() + .map(|_| { + quote! { Default::default() } + }) + .collect::>(); + + quote! { #name(#(#fields),*) } + } + Fields::Unit => quote! { #name }, + }; + + res + } } diff --git a/crates/core/tests/field_default_test.rs b/crates/core/tests/field_default_test.rs index 9460128a..9b752af0 100644 --- a/crates/core/tests/field_default_test.rs +++ b/crates/core/tests/field_default_test.rs @@ -2,113 +2,165 @@ mod utils; use schematic_core::container::Container; use starbase_sandbox::{assert_debug_snapshot, assert_snapshot}; +use std::collections::BTreeMap; use syn::parse_quote; use utils::pretty; mod field_default { use super::*; + #[test] + fn handles_collections() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: HashMap, + b: Vec, + c: BTreeSet, + d: CustomVec, + e: UnknownCollection, + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + + #[test] + fn handles_layers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: Option, + b: Arc, + c: Box, + d: Rc>, + e: Arc>>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + + #[test] + fn supports_handler_func() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = handler)] + a: String, + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + mod named_struct { use super::*; #[test] - fn supports_bool() { + fn supports_types() { let container = Container::from(parse_quote! { #[derive(Config)] struct Example { + no_default: bool, #[setting(default = true)] a: bool, + #[setting(default = 100)] + b: usize, + #[setting(default = "abc")] + c: String, + #[setting(default = ["a".into(), "b".into(), "c".into()])] + d: [String; 3], + #[setting(default = vec!["a", "b", "c"])] + e: Vec, + #[setting(default = (10, -10, 0))] + f: (usize, isize, u8), } }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.default.is_some()); - assert_debug_snapshot!(field.args.default.as_ref().unwrap()); assert_snapshot!(pretty(container.impl_partial_default_values())); - } - #[test] - fn supports_number() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(default = 100)] - a: usize, + for field in container.inner.get_fields() { + if field.ident.as_ref().is_some_and(|id| id != "no_default") { + assert!(field.args.default.is_some()); } - }); - let field = container.inner.get_fields()[0]; + } - assert!(field.args.default.is_some()); - assert_debug_snapshot!(field.args.default.as_ref().unwrap()); + let defaults = container + .inner + .get_fields() + .into_iter() + .map(|field| (field.ident.as_ref().unwrap(), field.args.default.as_ref())) + .collect::>(); - assert_snapshot!(pretty(container.impl_partial_default_values())); + assert_debug_snapshot!(defaults); } #[test] - fn supports_string() { + fn renders_nothing_if_all_option_wrapped() { let container = Container::from(parse_quote! { #[derive(Config)] struct Example { - #[setting(default = "abc")] - a: String, + a: Option, + b: Option>, } }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.default.is_some()); - assert_debug_snapshot!(field.args.default.as_ref().unwrap()); assert_snapshot!(pretty(container.impl_partial_default_values())); } + } + + mod unnamed_struct { + use super::*; #[test] - fn supports_array() { + fn supports_types() { let container = Container::from(parse_quote! { #[derive(Config)] - struct Example { + struct Example( + bool, + #[setting(default = true)] + bool, + #[setting(default = 100)] + usize, + #[setting(default = "abc")] + String, #[setting(default = ["a".into(), "b".into(), "c".into()])] - a: [String; 3], - } + [String; 3], + #[setting(default = vec!["a", "b", "c"])] + Vec, + #[setting(default = (10, -10, 0))] + (usize, isize, u8), + ); }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.default.is_some()); - assert_debug_snapshot!(field.args.default.as_ref().unwrap()); assert_snapshot!(pretty(container.impl_partial_default_values())); - } - #[test] - fn supports_vec() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(default = vec!["a", "b", "c"])] - a: Vec, + for field in container.inner.get_fields() { + if field.index != 0 { + assert!(field.args.default.is_some()); } - }); - let field = container.inner.get_fields()[0]; + } - assert!(field.args.default.is_some()); - assert_debug_snapshot!(field.args.default.as_ref().unwrap()); + let defaults = container + .inner + .get_fields() + .into_iter() + .map(|field| (field.index, field.args.default.as_ref())) + .collect::>(); - assert_snapshot!(pretty(container.impl_partial_default_values())); + assert_debug_snapshot!(defaults); } #[test] - fn supports_tuple() { + fn renders_nothing_if_all_option_wrapped() { let container = Container::from(parse_quote! { #[derive(Config)] - struct Example { - #[setting(default = (10, -10, 0))] - a: (usize, isize, u8), - } + struct Example( + Option, + Option>, + ); }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.default.is_some()); - assert_debug_snapshot!(field.args.default.as_ref().unwrap()); assert_snapshot!(pretty(container.impl_partial_default_values())); } diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_string-2.snap b/crates/core/tests/snapshots/field_default_test__field_default__handles_collections.snap similarity index 56% rename from crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_string-2.snap rename to crates/core/tests/snapshots/field_default_test__field_default__handles_collections.snap index 32d821aa..02660590 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_string-2.snap +++ b/crates/core/tests/snapshots/field_default_test__field_default__handles_collections.snap @@ -7,11 +7,11 @@ fn default_values( ) -> std::result::Result, schematic::ConfigError> { Ok( Some(Self { - a: Some( - schematic::internal::handle_default_result( - std::convert::TryFrom::try_from("abc"), - )?, - ), + a: Some(HashMap::default()), + b: Some(Vec::default()), + c: Some(BTreeSet::default()), + d: Some(CustomVec::default()), + e: Some(UnknownCollection::default()), }), ) } diff --git a/crates/core/tests/snapshots/field_default_test__field_default__handles_layers.snap b/crates/core/tests/snapshots/field_default_test__field_default__handles_layers.snap new file mode 100644 index 00000000..4c79aac5 --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__handles_layers.snap @@ -0,0 +1,17 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- +fn default_values( + context: &Self::Context, +) -> std::result::Result, schematic::ConfigError> { + Ok( + Some(Self { + b: Some(Arc::new(Default::default())), + c: Some(Box::new(Default::default())), + d: Some(Rc::new(Some(Default::default()))), + e: Some(Arc::new(Vec::default())), + ..Default::default() + }), + ) +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__renders_nothing_if_all_option_wrapped.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__renders_nothing_if_all_option_wrapped.snap new file mode 100644 index 00000000..c250669a --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__renders_nothing_if_all_option_wrapped.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- + diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_array.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_array.snap deleted file mode 100644 index 344510ec..00000000 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_array.snap +++ /dev/null @@ -1,60 +0,0 @@ ---- -source: crates/core/tests/field_default_test.rs -expression: field.args.default.as_ref().unwrap() ---- -Expr::Array { - attrs: [], - bracket_token: Bracket, - elems: [ - Expr::MethodCall { - attrs: [], - receiver: Expr::Lit { - attrs: [], - lit: Lit::Str { - token: "a", - }, - }, - dot_token: Dot, - method: Ident( - into, - ), - turbofish: None, - paren_token: Paren, - args: [], - }, - Comma, - Expr::MethodCall { - attrs: [], - receiver: Expr::Lit { - attrs: [], - lit: Lit::Str { - token: "b", - }, - }, - dot_token: Dot, - method: Ident( - into, - ), - turbofish: None, - paren_token: Paren, - args: [], - }, - Comma, - Expr::MethodCall { - attrs: [], - receiver: Expr::Lit { - attrs: [], - lit: Lit::Str { - token: "c", - }, - }, - dot_token: Dot, - method: Ident( - into, - ), - turbofish: None, - paren_token: Paren, - args: [], - }, - ], -} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool-2.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool-2.snap deleted file mode 100644 index 29dd2fdb..00000000 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool-2.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/core/tests/field_default_test.rs -expression: pretty(container.impl_partial_default_values()) ---- -fn default_values( - context: &Self::Context, -) -> std::result::Result, schematic::ConfigError> { - Ok(Some(Self { a: Some(true) })) -} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool.snap deleted file mode 100644 index 1c831e90..00000000 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_bool.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/core/tests/field_default_test.rs -expression: field.args.default.as_ref().unwrap() ---- -Expr::Lit { - attrs: [], - lit: Lit::Bool { - value: true, - }, -} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_number.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_number.snap deleted file mode 100644 index 6c7c4d49..00000000 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_number.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/core/tests/field_default_test.rs -expression: field.args.default.as_ref().unwrap() ---- -Expr::Lit { - attrs: [], - lit: Lit::Int { - token: 100, - }, -} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_string.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_string.snap deleted file mode 100644 index 351325cf..00000000 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_string.snap +++ /dev/null @@ -1,10 +0,0 @@ ---- -source: crates/core/tests/field_default_test.rs -expression: field.args.default.as_ref().unwrap() ---- -Expr::Lit { - attrs: [], - lit: Lit::Str { - token: "abc", - }, -} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple-2.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple-2.snap deleted file mode 100644 index 2c2e7405..00000000 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple-2.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/core/tests/field_default_test.rs -expression: pretty(container.impl_partial_default_values()) ---- -fn default_values( - context: &Self::Context, -) -> std::result::Result, schematic::ConfigError> { - Ok(Some(Self { a: Some((10, -10, 0)) })) -} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple.snap deleted file mode 100644 index b6734170..00000000 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_tuple.snap +++ /dev/null @@ -1,36 +0,0 @@ ---- -source: crates/core/tests/field_default_test.rs -expression: field.args.default.as_ref().unwrap() ---- -Expr::Tuple { - attrs: [], - paren_token: Paren, - elems: [ - Expr::Lit { - attrs: [], - lit: Lit::Int { - token: 10, - }, - }, - Comma, - Expr::Unary { - attrs: [], - op: UnOp::Neg( - Minus, - ), - expr: Expr::Lit { - attrs: [], - lit: Lit::Int { - token: 10, - }, - }, - }, - Comma, - Expr::Lit { - attrs: [], - lit: Lit::Int { - token: 0, - }, - }, - ], -} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types-2.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types-2.snap new file mode 100644 index 00000000..3c6dceef --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types-2.snap @@ -0,0 +1,178 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: defaults +--- +{ + Ident( + a, + ): Some( + Expr::Lit { + attrs: [], + lit: Lit::Bool { + value: true, + }, + }, + ), + Ident( + b, + ): Some( + Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 100, + }, + }, + ), + Ident( + c, + ): Some( + Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "abc", + }, + }, + ), + Ident( + d, + ): Some( + Expr::Array { + attrs: [], + bracket_token: Bracket, + elems: [ + Expr::MethodCall { + attrs: [], + receiver: Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "a", + }, + }, + dot_token: Dot, + method: Ident( + into, + ), + turbofish: None, + paren_token: Paren, + args: [], + }, + Comma, + Expr::MethodCall { + attrs: [], + receiver: Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "b", + }, + }, + dot_token: Dot, + method: Ident( + into, + ), + turbofish: None, + paren_token: Paren, + args: [], + }, + Comma, + Expr::MethodCall { + attrs: [], + receiver: Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "c", + }, + }, + dot_token: Dot, + method: Ident( + into, + ), + turbofish: None, + paren_token: Paren, + args: [], + }, + ], + }, + ), + Ident( + e, + ): Some( + Expr::Macro { + attrs: [], + mac: Macro { + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident( + vec, + ), + arguments: PathArguments::None, + }, + ], + }, + bang_token: Not, + delimiter: MacroDelimiter::Bracket( + Bracket, + ), + tokens: TokenStream [ + Literal { + lit: "a", + }, + Punct { + char: ',', + spacing: Alone, + }, + Literal { + lit: "b", + }, + Punct { + char: ',', + spacing: Alone, + }, + Literal { + lit: "c", + }, + ], + }, + }, + ), + Ident( + f, + ): Some( + Expr::Tuple { + attrs: [], + paren_token: Paren, + elems: [ + Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 10, + }, + }, + Comma, + Expr::Unary { + attrs: [], + op: UnOp::Neg( + Minus, + ), + expr: Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 10, + }, + }, + }, + Comma, + Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 0, + }, + }, + ], + }, + ), + Ident( + no_default, + ): None, +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types.snap new file mode 100644 index 00000000..6b9654d6 --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- +fn default_values( + context: &Self::Context, +) -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + Ok( + Some(Self { + no_default: Some(Default::default()), + a: Some(true), + b: Some(100), + c: handle_default_result(String::try_from("abc"))?, + d: Some(["a".into(), "b".into(), "c".into()]), + e: Some(Vec::default()), + f: Some((10, -10, 0)), + }), + ) +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_vec.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_vec.snap deleted file mode 100644 index 07fd458d..00000000 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_vec.snap +++ /dev/null @@ -1,43 +0,0 @@ ---- -source: crates/core/tests/field_default_test.rs -expression: field.args.default.as_ref().unwrap() ---- -Expr::Macro { - attrs: [], - mac: Macro { - path: Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident( - vec, - ), - arguments: PathArguments::None, - }, - ], - }, - bang_token: Not, - delimiter: MacroDelimiter::Bracket( - Bracket, - ), - tokens: TokenStream [ - Literal { - lit: "a", - }, - Punct { - char: ',', - spacing: Alone, - }, - Literal { - lit: "b", - }, - Punct { - char: ',', - spacing: Alone, - }, - Literal { - lit: "c", - }, - ], - }, -} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_array-2.snap b/crates/core/tests/snapshots/field_default_test__field_default__supports_handler_func.snap similarity index 75% rename from crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_array-2.snap rename to crates/core/tests/snapshots/field_default_test__field_default__supports_handler_func.snap index 13190c3a..bfbb22a3 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_array-2.snap +++ b/crates/core/tests/snapshots/field_default_test__field_default__supports_handler_func.snap @@ -5,9 +5,10 @@ expression: pretty(container.impl_partial_default_values()) fn default_values( context: &Self::Context, ) -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; Ok( Some(Self { - a: Some(["a".into(), "b".into(), "c".into()]), + a: handle_default_result(handler(context))?, }), ) } diff --git a/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap b/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap new file mode 100644 index 00000000..c250669a --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- + diff --git a/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types-2.snap b/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types-2.snap new file mode 100644 index 00000000..6eb42f87 --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types-2.snap @@ -0,0 +1,164 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: defaults +--- +{ + 0: None, + 1: Some( + Expr::Lit { + attrs: [], + lit: Lit::Bool { + value: true, + }, + }, + ), + 2: Some( + Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 100, + }, + }, + ), + 3: Some( + Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "abc", + }, + }, + ), + 4: Some( + Expr::Array { + attrs: [], + bracket_token: Bracket, + elems: [ + Expr::MethodCall { + attrs: [], + receiver: Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "a", + }, + }, + dot_token: Dot, + method: Ident( + into, + ), + turbofish: None, + paren_token: Paren, + args: [], + }, + Comma, + Expr::MethodCall { + attrs: [], + receiver: Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "b", + }, + }, + dot_token: Dot, + method: Ident( + into, + ), + turbofish: None, + paren_token: Paren, + args: [], + }, + Comma, + Expr::MethodCall { + attrs: [], + receiver: Expr::Lit { + attrs: [], + lit: Lit::Str { + token: "c", + }, + }, + dot_token: Dot, + method: Ident( + into, + ), + turbofish: None, + paren_token: Paren, + args: [], + }, + ], + }, + ), + 5: Some( + Expr::Macro { + attrs: [], + mac: Macro { + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident( + vec, + ), + arguments: PathArguments::None, + }, + ], + }, + bang_token: Not, + delimiter: MacroDelimiter::Bracket( + Bracket, + ), + tokens: TokenStream [ + Literal { + lit: "a", + }, + Punct { + char: ',', + spacing: Alone, + }, + Literal { + lit: "b", + }, + Punct { + char: ',', + spacing: Alone, + }, + Literal { + lit: "c", + }, + ], + }, + }, + ), + 6: Some( + Expr::Tuple { + attrs: [], + paren_token: Paren, + elems: [ + Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 10, + }, + }, + Comma, + Expr::Unary { + attrs: [], + op: UnOp::Neg( + Minus, + ), + expr: Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 10, + }, + }, + }, + Comma, + Expr::Lit { + attrs: [], + lit: Lit::Int { + token: 0, + }, + }, + ], + }, + ), +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types.snap b/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types.snap new file mode 100644 index 00000000..6b8bb741 --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types.snap @@ -0,0 +1,22 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- +fn default_values( + context: &Self::Context, +) -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + Ok( + Some( + Self( + Some(Default::default()), + Some(true), + Some(100), + handle_default_result(String::try_from("abc"))?, + Some(["a".into(), "b".into(), "c".into()]), + Some(Vec::default()), + Some((10, -10, 0)), + ), + ), + ) +} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_number-2.snap b/crates/core/tests/snapshots/variant_default_test__variant_default__unit_enum.snap similarity index 68% rename from crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_number-2.snap rename to crates/core/tests/snapshots/variant_default_test__variant_default__unit_enum.snap index 8cf68d77..012ecdc2 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_number-2.snap +++ b/crates/core/tests/snapshots/variant_default_test__variant_default__unit_enum.snap @@ -1,9 +1,9 @@ --- -source: crates/core/tests/field_default_test.rs +source: crates/core/tests/variant_default_test.rs expression: pretty(container.impl_partial_default_values()) --- fn default_values( context: &Self::Context, ) -> std::result::Result, schematic::ConfigError> { - Ok(Some(Self { a: Some(100) })) + Ok(Some(Self::Bar)) } diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_vec-2.snap b/crates/core/tests/snapshots/variant_default_test__variant_default__unnamed_enum.snap similarity index 61% rename from crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_vec-2.snap rename to crates/core/tests/snapshots/variant_default_test__variant_default__unnamed_enum.snap index 73bdc075..da1c1b61 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_vec-2.snap +++ b/crates/core/tests/snapshots/variant_default_test__variant_default__unnamed_enum.snap @@ -1,9 +1,9 @@ --- -source: crates/core/tests/field_default_test.rs +source: crates/core/tests/variant_default_test.rs expression: pretty(container.impl_partial_default_values()) --- fn default_values( context: &Self::Context, ) -> std::result::Result, schematic::ConfigError> { - Ok(Some(Self { a: Some(Vec::default()) })) + Ok(Some(Self::Bar(Default::default(), Default::default()))) } diff --git a/crates/core/tests/utils.rs b/crates/core/tests/utils.rs index aa8e023b..abfa7cfc 100644 --- a/crates/core/tests/utils.rs +++ b/crates/core/tests/utils.rs @@ -1,5 +1,16 @@ use proc_macro2::TokenStream; +use schematic_core::container::Container; +use schematic_core::field::Field; pub fn pretty(tokens: TokenStream) -> String { prettyplease::unparse(&syn::parse_file(&tokens.to_string()).unwrap()) } + +pub fn get_field<'a>(container: &'a Container, key: &str) -> &'a Field { + container + .inner + .get_fields() + .into_iter() + .find(|field| field.ident.as_ref().is_some_and(|id| id == key)) + .unwrap() +} diff --git a/crates/core/tests/variant_default_test.rs b/crates/core/tests/variant_default_test.rs new file mode 100644 index 00000000..663ab69c --- /dev/null +++ b/crates/core/tests/variant_default_test.rs @@ -0,0 +1,80 @@ +mod utils; + +use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; +use utils::pretty; + +mod variant_default { + use super::*; + + #[test] + #[should_panic(expected = "Only 1 variant may be marked as default.")] + fn errors_if_multiple_defaults() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(default)] + Foo, + #[setting(default)] + Bar, + #[setting(default)] + Baz, + } + }) + .impl_partial_default_values(); + } + + #[test] + #[should_panic(expected = "Enums with named fields are not supported!")] + fn errors_for_named_enum() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + Foo {}, + #[setting(default)] + Bar {}, + Baz {}, + } + }) + .impl_partial_default_values(); + } + + #[test] + fn unit_enum() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + Foo, + #[setting(default)] + Bar, + Baz, + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + + let variants = container.inner.get_variants(); + + assert!(variants[1].args.default); + } + + #[test] + fn unnamed_enum() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + Foo(usize), + #[setting(default)] + Bar(String, u8), + Baz(bool, String, isize), + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + + let variants = container.inner.get_variants(); + + assert!(variants[1].args.default); + } +} diff --git a/crates/schematic/src/internal.rs b/crates/schematic/src/internal.rs index 9bbecef1..7c44a999 100644 --- a/crates/schematic/src/internal.rs +++ b/crates/schematic/src/internal.rs @@ -2,7 +2,6 @@ use crate::config::{ConfigError, HandlerError, MergeError, MergeResult, PartialC use schematic_types::Schema; use std::str::FromStr; -// Handles T and Option values pub fn handle_default_result( result: Result, ) -> Result { From 09f6834fdee0486c762b4ff0f209b41fe8c462f6 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Mon, 30 Jun 2025 13:51:43 -0700 Subject: [PATCH 15/33] Start testing env. --- crates/core/src/args.rs | 12 +- crates/core/src/container.rs | 60 ++++++- crates/core/src/field.rs | 138 ++++++++++++--- crates/core/src/field_value.rs | 47 ++++- crates/core/tests/field_default_test.rs | 15 ++ crates/core/tests/field_env_test.rs | 160 ++++++++++++++++-- ...efault__named_struct__supports_nested.snap | 14 ++ ...amed_struct__supports_different_types.snap | 14 ++ ...ld_env__named_struct__supports_nested.snap | 12 ++ ...amed_struct__supports_different_types.snap | 14 ++ ..._env__unnamed_struct__supports_nested.snap | 12 ++ crates/macros/src/config/field_value.rs | 1 + crates/schematic/src/internal.rs | 56 ++++++ 13 files changed, 507 insertions(+), 48 deletions(-) create mode 100644 crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_different_types.snap create mode 100644 crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_different_types.snap create mode 100644 crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_nested.snap diff --git a/crates/core/src/args.rs b/crates/core/src/args.rs index 0097fe8f..c893fd64 100644 --- a/crates/core/src/args.rs +++ b/crates/core/src/args.rs @@ -57,12 +57,12 @@ impl FromMeta for SerdeRenameArg { } impl SerdeRenameArg { - // pub fn get_name(&self, dir: SerdeIoDirection) -> Option<&str> { - // match dir { - // SerdeIoDirection::From => self.deserialize.as_deref(), - // SerdeIoDirection::To => self.serialize.as_deref(), - // } - // } + pub fn get_name(&self, dir: SerdeIoDirection) -> Option<&str> { + match dir { + SerdeIoDirection::From => self.deserialize.as_deref(), + SerdeIoDirection::To => self.serialize.as_deref(), + } + } pub fn get_meta(&self, key: &str) -> TokenStream { match (self.deserialize.as_deref(), self.serialize.as_deref()) { diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index f381b149..fed9c913 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -178,6 +178,7 @@ impl Container { }; let default_values_method = self.impl_partial_default_values(); + let env_values_method = self.impl_partial_env_values(); quote! { #[automatically_derived] @@ -185,6 +186,7 @@ impl Container { type Context = #context; #default_values_method + #env_values_method } #[automatically_derived] @@ -214,11 +216,11 @@ impl Container { let res = field.impl_partial_default_value(); if !res.no_value { - let name = field.ident.as_ref().unwrap(); + let key = field.get_key(); let value = res.value; rows.push(quote! { - #name: #value, + #key: #value, }); } @@ -227,6 +229,7 @@ impl Container { } } + // Do not implement method if rows.is_empty() { return quote! {}; } @@ -263,6 +266,7 @@ impl Container { } } + // Do not implement method if all_none { return quote! {}; } @@ -319,6 +323,58 @@ impl Container { } } } + + pub fn impl_partial_env_values(&self) -> TokenStream { + let inner = match &self.inner { + ContainerInner::NamedStruct { fields } | ContainerInner::UnnamedStruct { fields } => { + let mut rows = vec![]; + + for field in fields { + let res = field.impl_partial_env_value(); + + if !res.no_value { + let key = field.get_key(); + let value = res.value; + + rows.push(quote! { + partial.#key = #value; + }); + } + } + + // Do not implement method + if rows.is_empty() { + return quote! {}; + } + + quote! { + #(#rows)* + } + } + + // Enums don't support env vars + _ => return quote! {}, + }; + + let internal = ImplResult::impl_use_internal(true); + + quote! { + fn env_values() -> std::result::Result, schematic::ConfigError> { + #internal + + let mut env = EnvManager::default(); + let mut partial = Self::default(); + + #inner + + Ok(if env.is_empty() { + None + } else { + Some(partial) + }) + } + } + } } impl ToTokens for Container { diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 16566c38..72757247 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -1,9 +1,12 @@ -use crate::args::{PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; +use crate::args::{ + PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeIoDirection, SerdeRenameArg, +}; use crate::container::ContainerArgs; use crate::field_value::FieldValue; use crate::utils::{ImplResult, preserve_str_literal, to_type_string}; use darling::{FromAttributes, FromMeta}; -use quote::ToTokens; +use proc_macro2::{Literal, TokenStream}; +use quote::{ToTokens, TokenStreamExt, quote}; use std::ops::Deref; use std::rc::Rc; use syn::{Attribute, Expr, ExprPath, Field as NativeField, FieldMutability, Ident, Visibility}; @@ -154,37 +157,13 @@ impl Field { fn validate_args(&self) { #[cfg(feature = "env")] { - // env - if self.args.env.as_ref().is_some_and(|key| key.is_empty()) { - panic!("Attribute `env` cannot be empty."); - } - - if self.args.env.is_some() && self.args.env_prefix.is_some() { - panic!("Cannot use `env` and `env_prefix` together."); - } - // env_prefix - if self - .args - .env_prefix - .as_ref() - .is_some_and(|key| key.is_empty()) - { - panic!("Attribute `env_prefix` cannot be empty."); - } - if self.args.env_prefix.is_some() && self.args.nested.is_none() { panic!("Cannot use `env_prefix` without `nested`."); } } // nested - if self.args.nested.is_some() { - #[cfg(feature = "env")] - if self.args.env.is_some() { - panic!("Cannot use `env` with `nested`, use `env_prefix` instead?"); - } - } #[cfg(feature = "env")] { @@ -194,6 +173,83 @@ impl Field { } } } + + #[cfg(not(feature = "env"))] + pub fn get_env_var(&self) -> Option { + None + } + + #[cfg(feature = "env")] + pub fn get_env_var(&self) -> Option { + if self.args.env.is_some() && self.args.env_prefix.is_some() { + panic!("Cannot use `env` and `env_prefix` together."); + } + + if let Some(env_key) = &self.args.env { + if env_key.is_empty() { + panic!("Attribute `env` cannot be empty."); + } + + if self.is_nested() { + panic!("Cannot use `env` with `nested`, use `env_prefix` instead?"); + } + + return Some(env_key.to_owned()); + } + + if let Some(env_prefix) = &self.container_args.env_prefix { + if env_prefix.is_empty() { + panic!("Attribute `env_prefix` cannot be empty."); + } + + return Some(format!("{env_prefix}{}", self.get_name()).to_uppercase()); + } + + None + } + + pub fn get_key(&self) -> TokenStream { + self.ident + .as_ref() + .map(|name| quote! { #name }) + .unwrap_or_else(|| { + let index = Index(self.index); + + quote! { #index } + }) + } + + pub fn get_name(&self) -> String { + let dir = SerdeIoDirection::From; + + if let Some(name) = self.args.rename.as_ref().and_then(|rn| rn.get_name(dir)) { + return name.into(); + } + + if let Some(name) = self + .serde_args + .rename + .as_ref() + .and_then(|rn| rn.get_name(dir)) + { + return name.into(); + } + + self.ident + .as_ref() + .expect("Name only usable on named fields!") + .to_string() + } + + pub fn is_nested(&self) -> bool { + self.args + .nested + .as_ref() + .is_some_and(|nested| match nested { + FieldNestedArg::Detect(inner) => *inner, + FieldNestedArg::Ident(_) => true, + }) + } } // impl ToTokens for Field { @@ -223,4 +279,34 @@ impl Field { pub fn impl_partial_default_value(&self) -> ImplResult { self.value.impl_partial_default_value(&self.args) } + + #[cfg(not(feature = "env"))] + pub fn impl_partial_env_value(&self) -> ImplResult { + ImplResult::skipped() + } + + #[cfg(feature = "env")] + pub fn impl_partial_env_value(&self) -> ImplResult { + if self.is_nested() { + return self.value.impl_partial_env_value(&self.args, ""); + } + + let Some(env_key) = self.get_env_var() else { + if self.args.parse_env.is_some() { + panic!("Cannot use `parse_env` without `env` or a parent `env_prefix`."); + } + + return ImplResult::skipped(); + }; + + self.value.impl_partial_env_value(&self.args, &env_key) + } +} + +struct Index(usize); + +impl ToTokens for Index { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.append(Literal::usize_unsuffixed(self.0)); + } } diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index f322ab10..430bf954 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -84,10 +84,19 @@ impl FieldValue { self.inner_ty.as_ref().unwrap_or(&self.ty) } + pub fn is_collection(&self) -> bool { + self.layers.iter().any(|layer| { + matches!( + layer, + Layer::Map(_) | Layer::Set(_) | Layer::Vec(_) | Layer::Unknown(_) + ) + }) + } + pub fn is_outer_option_wrapped(&self) -> bool { self.layers .first() - .is_some_and(|wrapper| *wrapper == Layer::Option) + .is_some_and(|layer| *layer == Layer::Option) } pub fn impl_partial_default_value(&self, field_args: &FieldArgs) -> ImplResult { @@ -104,8 +113,14 @@ impl FieldValue { panic!("Cannot use `default` with `nested`."); } + let ident = format_ident!("Partial{}", nested_ident); + + // quote! { + // <#nested_ident as schematic::PartialConfig>::default_values(context)? + // } + quote! { - <#nested_ident as schematic::PartialConfig>::default_values(content)? + #ident::default_values(context)? } } else if let Some(expr) = &field_args.default { let ty = self.get_inner_type(); @@ -178,6 +193,34 @@ impl FieldValue { res.value = value; res } + + pub fn impl_partial_env_value(&self, field_args: &FieldArgs, env_key: &str) -> ImplResult { + let mut res = ImplResult::default(); + + if self.is_collection() { + panic!("Collection types cannot be used with `env`."); + } else if !self.layers.is_empty() { + panic!("Wrapper types cannot be used with `env`."); + } + + res.value = if let Some(nested_ident) = &self.nested_ident { + let ident = format_ident!("Partial{}", nested_ident); + + quote! { + env.nested(#ident::env_values()?)? + } + } else if let Some(parse_env) = &field_args.parse_env { + quote! { + env.get_and_parse(#env_key, #parse_env)? + } + } else { + quote! { + env.get(#env_key)? + } + }; + + res + } } fn extract_type_information( diff --git a/crates/core/tests/field_default_test.rs b/crates/core/tests/field_default_test.rs index 9b752af0..22946203 100644 --- a/crates/core/tests/field_default_test.rs +++ b/crates/core/tests/field_default_test.rs @@ -96,6 +96,21 @@ mod field_default { assert_debug_snapshot!(defaults); } + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: NestedConfig, + #[setting(nested = CustomConfig)] + b: CustomConfig, + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + #[test] fn renders_nothing_if_all_option_wrapped() { let container = Container::from(parse_quote! { diff --git a/crates/core/tests/field_env_test.rs b/crates/core/tests/field_env_test.rs index c1ea7703..694d9b1a 100644 --- a/crates/core/tests/field_env_test.rs +++ b/crates/core/tests/field_env_test.rs @@ -1,33 +1,169 @@ +mod utils; + use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; use syn::parse_quote; +use utils::pretty; mod field_env { use super::*; #[test] - fn accepts_string() { - let container = Container::from(parse_quote! { + #[should_panic(expected = "Wrapper types cannot be used with `env`.")] + fn errors_if_using_wrappers() { + Container::from(parse_quote! { #[derive(Config)] struct Example { #[setting(env = "KEY")] - a: String, + a: Arc, } - }); - let field = container.inner.get_fields()[0]; - - assert_eq!(field.args.env.as_ref().unwrap(), "KEY"); + }) + .impl_partial_env_values(); } #[test] - #[should_panic(expected = "Attribute `env` cannot be empty.")] - fn errors_if_empty() { + #[should_panic(expected = "Collection types cannot be used with `env`.")] + fn errors_if_using_collections() { Container::from(parse_quote! { #[derive(Config)] struct Example { - #[setting(env = "")] - a: String, + #[setting(env = "KEY")] + a: Vec, } - }); + }) + .impl_partial_env_values(); + } + + mod named_struct { + use super::*; + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env = "KEY")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert_eq!(field.args.env.as_ref().unwrap(), "KEY"); + } + + #[test] + #[should_panic(expected = "Attribute `env` cannot be empty.")] + fn errors_if_empty() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env = "")] + a: String, + } + }) + .impl_partial_env_values(); + } + + #[test] + fn supports_different_types() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + no_env: String, + #[setting(env = "A")] + a: String, + #[setting(env = "B")] + b: usize, + #[setting(env = "C")] + c: bool, + #[setting(env = "D")] + d: f32, + } + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: NestedConfig, + #[setting(nested = CustomConfig)] + b: CustomConfig, + } + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(env = "KEY")] + String, + ); + }); + let field = container.inner.get_fields()[0]; + + assert_eq!(field.args.env.as_ref().unwrap(), "KEY"); + } + + #[test] + #[should_panic(expected = "Attribute `env` cannot be empty.")] + fn errors_if_empty() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(env = "")] + String, + ); + }) + .impl_partial_env_values(); + } + + #[test] + fn supports_different_types() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + String, + #[setting(env = "A")] + String, + #[setting(env = "B")] + usize, + #[setting(env = "C")] + bool, + #[setting(env = "D")] + f32, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested)] + NestedConfig, + #[setting(nested = CustomConfig)] + CustomConfig, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } } } diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_nested.snap b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_nested.snap new file mode 100644 index 00000000..e6f5c435 --- /dev/null +++ b/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_nested.snap @@ -0,0 +1,14 @@ +--- +source: crates/core/tests/field_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- +fn default_values( + context: &Self::Context, +) -> std::result::Result, schematic::ConfigError> { + Ok( + Some(Self { + a: PartialNestedConfig::default_values(context)?, + b: PartialCustomConfig::default_values(context)?, + }), + ) +} diff --git a/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_different_types.snap b/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_different_types.snap new file mode 100644 index 00000000..9c7aa8b4 --- /dev/null +++ b/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_different_types.snap @@ -0,0 +1,14 @@ +--- +source: crates/core/tests/field_env_test.rs +expression: pretty(container.impl_partial_env_values()) +--- +fn env_values() -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + let mut env = EnvManager::default(); + let mut partial = Self::default(); + partial.a = env.get("A")?; + partial.b = env.get("B")?; + partial.c = env.get("C")?; + partial.d = env.get("D")?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_nested.snap b/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_nested.snap new file mode 100644 index 00000000..d82a0342 --- /dev/null +++ b/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_nested.snap @@ -0,0 +1,12 @@ +--- +source: crates/core/tests/field_env_test.rs +expression: pretty(container.impl_partial_env_values()) +--- +fn env_values() -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + let mut env = EnvManager::default(); + let mut partial = Self::default(); + partial.a = env.nested(PartialNestedConfig::env_values()?)?; + partial.b = env.nested(PartialCustomConfig::env_values()?)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_different_types.snap b/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_different_types.snap new file mode 100644 index 00000000..089f22de --- /dev/null +++ b/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_different_types.snap @@ -0,0 +1,14 @@ +--- +source: crates/core/tests/field_env_test.rs +expression: pretty(container.impl_partial_env_values()) +--- +fn env_values() -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + let mut env = EnvManager::default(); + let mut partial = Self::default(); + partial.1 = env.get("A")?; + partial.2 = env.get("B")?; + partial.3 = env.get("C")?; + partial.4 = env.get("D")?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_nested.snap new file mode 100644 index 00000000..f29cae7f --- /dev/null +++ b/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_nested.snap @@ -0,0 +1,12 @@ +--- +source: crates/core/tests/field_env_test.rs +expression: pretty(container.impl_partial_env_values()) +--- +fn env_values() -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + let mut env = EnvManager::default(); + let mut partial = Self::default(); + partial.0 = env.nested(PartialNestedConfig::env_values()?)?; + partial.1 = env.nested(PartialCustomConfig::env_values()?)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/macros/src/config/field_value.rs b/crates/macros/src/config/field_value.rs index 62301a0a..e681973c 100644 --- a/crates/macros/src/config/field_value.rs +++ b/crates/macros/src/config/field_value.rs @@ -77,6 +77,7 @@ impl FieldValue<'_> { _ => None, } } + pub fn get_finalize_value(&self) -> Option { match self { Self::NestedList { .. } | Self::NestedMap { .. } => { diff --git a/crates/schematic/src/internal.rs b/crates/schematic/src/internal.rs index 7c44a999..4f5481d4 100644 --- a/crates/schematic/src/internal.rs +++ b/crates/schematic/src/internal.rs @@ -2,12 +2,68 @@ use crate::config::{ConfigError, HandlerError, MergeError, MergeResult, PartialC use schematic_types::Schema; use std::str::FromStr; +// DEFAULT VALUES + pub fn handle_default_result( result: Result, ) -> Result { result.map_err(|error| ConfigError::InvalidDefaultValue(error.to_string())) } +// ENV VARS + +#[cfg(feature = "env")] +mod env { + use super::*; + use crate::config::ParseEnvResult; + + #[derive(Default)] + pub struct EnvManager { + count: u8, + } + + impl EnvManager { + pub fn is_empty(&self) -> bool { + self.count == 0 + } + + pub fn get(&mut self, key: &str) -> ParseEnvResult { + self.get_and_parse(key, |value| parse_value(value).map(|v| Some(v))) + } + + pub fn get_and_parse( + &mut self, + key: &str, + parser: impl Fn(String) -> ParseEnvResult, + ) -> ParseEnvResult { + if let Ok(value) = std::env::var(key) { + return parser(value) + .inspect(|inner| { + if inner.is_some() { + self.count += 1; + } + }) + .map_err(|error| { + HandlerError(format!("Invalid environment variable {key}: {error}")) + }); + } + + Ok(None) + } + + pub fn nested(&mut self, partial: Option) -> ParseEnvResult { + if partial.is_some() { + self.count += 1; + } + + Ok(partial) + } + } +} + +#[cfg(feature = "env")] +pub use env::*; + #[cfg(feature = "env")] pub fn track_env(value: Option, tracker: &mut std::collections::HashSet) -> Option { value.inspect(|_| { From 60e98c49dd06bd880df3995d8bde9c06cb9b9e77 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Tue, 8 Jul 2025 17:31:38 -0700 Subject: [PATCH 16/33] Test env prefix. --- crates/core/src/container.rs | 20 ++- crates/core/src/field.rs | 9 +- crates/core/src/field_value.rs | 14 +- crates/core/tests/container_env_test.rs | 57 ++++++ crates/core/tests/field_env_test.rs | 166 ++++++++++++++---- ...v_test__container_env__can_set_prefix.snap | 21 +++ ...env_test__container_env__can_set_vars.snap | 17 ++ ...amed_struct__supports_different_types.snap | 6 +- ...ld_env__named_struct__supports_nested.snap | 6 +- ...amed_struct__supports_different_types.snap | 6 +- ..._env__unnamed_struct__supports_nested.snap | 6 +- ...prefix__named_struct__supports_nested.snap | 16 ++ ...efix__unnamed_struct__supports_nested.snap | 16 ++ crates/schematic/src/config/configs.rs | 7 + crates/schematic/src/internal.rs | 15 +- 15 files changed, 330 insertions(+), 52 deletions(-) create mode 100644 crates/core/tests/container_env_test.rs create mode 100644 crates/core/tests/snapshots/container_env_test__container_env__can_set_prefix.snap create mode 100644 crates/core/tests/snapshots/container_env_test__container_env__can_set_vars.snap create mode 100644 crates/core/tests/snapshots/field_env_test__field_env_prefix__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/field_env_test__field_env_prefix__unnamed_struct__supports_nested.snap diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index fed9c913..8bd02a3d 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -324,6 +324,12 @@ impl Container { } } + #[cfg(not(feature = "env"))] + pub fn impl_partial_env_values(&self) -> TokenStream { + quote! {} + } + + #[cfg(feature = "env")] pub fn impl_partial_env_values(&self) -> TokenStream { let inner = match &self.inner { ContainerInner::NamedStruct { fields } | ContainerInner::UnnamedStruct { fields } => { @@ -358,11 +364,21 @@ impl Container { let internal = ImplResult::impl_use_internal(true); + let prefix_fallback = if let Some(env_prefix) = &self.args.env_prefix { + if env_prefix.is_empty() { + panic!("Attribute `env_prefix` cannot be empty."); + } + + quote! { prefix.or_else(Some(#env_prefix)) } + } else { + quote! { prefix } + }; + quote! { - fn env_values() -> std::result::Result, schematic::ConfigError> { + fn env_values_with_prefix(prefix: Option<&str>) -> std::result::Result, schematic::ConfigError> { #internal - let mut env = EnvManager::default(); + let mut env = EnvManager::new(#prefix_fallback); let mut partial = Self::default(); #inner diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 72757247..5fe5e02b 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -197,12 +197,9 @@ impl Field { return Some(env_key.to_owned()); } - if let Some(env_prefix) = &self.container_args.env_prefix { - if env_prefix.is_empty() { - panic!("Attribute `env_prefix` cannot be empty."); - } - - return Some(format!("{env_prefix}{}", self.get_name()).to_uppercase()); + // When the container has a prefix, we use the field name as a key + if self.container_args.env_prefix.is_some() { + return Some(self.get_name().to_uppercase()); } None diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index 430bf954..d7e4358f 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -206,8 +206,18 @@ impl FieldValue { res.value = if let Some(nested_ident) = &self.nested_ident { let ident = format_ident!("Partial{}", nested_ident); - quote! { - env.nested(#ident::env_values()?)? + if let Some(env_prefix) = &field_args.env_prefix { + if env_prefix.is_empty() { + panic!("Attribute `env_prefix` cannot be empty."); + } + + quote! { + env.nested(#ident::env_values_with_prefix(Some(#env_prefix))?)? + } + } else { + quote! { + env.nested(#ident::env_values()?)? + } } } else if let Some(parse_env) = &field_args.parse_env { quote! { diff --git a/crates/core/tests/container_env_test.rs b/crates/core/tests/container_env_test.rs new file mode 100644 index 00000000..30b179ab --- /dev/null +++ b/crates/core/tests/container_env_test.rs @@ -0,0 +1,57 @@ +mod utils; + +use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; +use utils::pretty; + +mod container_env { + use super::*; + + #[test] + fn can_set_vars() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + no_var: String, + no_var_nested: NestedConfig, + #[setting(env = "STR")] + a: String, + #[setting(env = "BOOL")] + b: bool, + #[setting(env = "INT")] + c: usize, + #[setting(nested)] + d: NestedConfig, + #[setting(nested = CustomConfig)] + e: CustomConfig, + } + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + + #[test] + fn can_set_prefix() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(env_prefix = "PREFIX_")] + struct Example { + #[setting(env = "OVERRIDE")] + a: String, + b: bool, + c: usize, + #[setting(nested)] + d: NestedConfig, + #[setting(nested = CustomConfig)] + e: CustomConfig, + #[setting(nested, env_prefix = "NESTED_")] + f: NestedConfig, + #[setting(nested = CustomConfig, env_prefix = "NESTED_")] + g: CustomConfig, + } + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } +} diff --git a/crates/core/tests/field_env_test.rs b/crates/core/tests/field_env_test.rs index 694d9b1a..36dbec69 100644 --- a/crates/core/tests/field_env_test.rs +++ b/crates/core/tests/field_env_test.rs @@ -170,42 +170,144 @@ mod field_env { mod field_env_prefix { use super::*; - #[test] - fn accepts_string() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(env_prefix = "KEY", nested)] - a: String, - } - }); - let field = container.inner.get_fields()[0]; + mod named_struct { + use super::*; - assert_eq!(field.args.env_prefix.as_ref().unwrap(), "KEY"); - } + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(env_prefix = "PRE_")] + struct Example { + #[setting(env = "A")] + a: String, + #[setting(env_prefix = "OVERRIDE_", nested)] + b: NestedConfig, + } + }); + let fields = container.inner.get_fields(); - #[test] - #[should_panic(expected = "Attribute `env_prefix` cannot be empty.")] - fn errors_if_empty() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(env_prefix = "", nested)] - a: String, - } - }); + assert_eq!(fields[0].args.env.as_ref().unwrap(), "A"); + assert!(fields[0].args.env_prefix.is_none()); + + assert!(fields[1].args.env.is_none()); + assert_eq!(fields[1].args.env_prefix.as_ref().unwrap(), "OVERRIDE_"); + } + + #[test] + #[should_panic(expected = "Attribute `env_prefix` cannot be empty.")] + fn errors_if_empty() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env_prefix = "", nested)] + a: String, + } + }) + .impl_partial_env_values(); + } + + #[test] + #[should_panic(expected = "Cannot use `env_prefix` without `nested`.")] + fn errors_if_not_nested() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env_prefix = "KEY")] + a: String, + } + }) + .impl_partial_env_values(); + } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: NestedConfig, + #[setting(nested = CustomConfig)] + b: CustomConfig, + #[setting(nested, env_prefix = "PRE_")] + c: NestedConfig, + #[setting(nested = CustomConfig, env_prefix = "PRE_")] + d: CustomConfig, + } + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } } - #[test] - #[should_panic(expected = "Cannot use `env_prefix` without `nested`.")] - fn errors_if_not_nested() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(env_prefix = "KEY")] - a: String, - } - }); + mod unnamed_struct { + use super::*; + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(env_prefix = "PRE_")] + struct Example( + #[setting(env = "A")] + String, + #[setting(env_prefix = "OVERRIDE_", nested)] + NestedConfig, + ); + }); + let fields = container.inner.get_fields(); + + assert_eq!(fields[0].args.env.as_ref().unwrap(), "A"); + assert!(fields[0].args.env_prefix.is_none()); + + assert!(fields[1].args.env.is_none()); + assert_eq!(fields[1].args.env_prefix.as_ref().unwrap(), "OVERRIDE_"); + } + + #[test] + #[should_panic(expected = "Attribute `env_prefix` cannot be empty.")] + fn errors_if_empty() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(env_prefix = "", nested)] + String, + ); + }) + .impl_partial_env_values(); + } + + #[test] + #[should_panic(expected = "Cannot use `env_prefix` without `nested`.")] + fn errors_if_not_nested() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(env_prefix = "KEY")] + String, + ); + }) + .impl_partial_env_values(); + } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested)] + NestedConfig, + #[setting(nested = CustomConfig)] + CustomConfig, + #[setting(nested, env_prefix = "PRE_")] + NestedConfig, + #[setting(nested = CustomConfig, env_prefix = "PRE_")] + CustomConfig, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } } } diff --git a/crates/core/tests/snapshots/container_env_test__container_env__can_set_prefix.snap b/crates/core/tests/snapshots/container_env_test__container_env__can_set_prefix.snap new file mode 100644 index 00000000..9f3c86e7 --- /dev/null +++ b/crates/core/tests/snapshots/container_env_test__container_env__can_set_prefix.snap @@ -0,0 +1,21 @@ +--- +source: crates/core/tests/container_env_test.rs +expression: pretty(container.impl_partial_env_values()) +--- +fn env_values_with_prefix( + prefix: Option<&str>, +) -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + let mut env = EnvManager::new(prefix.or_else(Some("PREFIX_"))); + let mut partial = Self::default(); + partial.a = env.get("OVERRIDE")?; + partial.b = env.get("B")?; + partial.c = env.get("C")?; + partial.d = env.nested(PartialNestedConfig::env_values()?)?; + partial.e = env.nested(PartialCustomConfig::env_values()?)?; + partial.f = env + .nested(PartialNestedConfig::env_values_with_prefix(Some("NESTED_"))?)?; + partial.g = env + .nested(PartialCustomConfig::env_values_with_prefix(Some("NESTED_"))?)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/container_env_test__container_env__can_set_vars.snap b/crates/core/tests/snapshots/container_env_test__container_env__can_set_vars.snap new file mode 100644 index 00000000..18b04f87 --- /dev/null +++ b/crates/core/tests/snapshots/container_env_test__container_env__can_set_vars.snap @@ -0,0 +1,17 @@ +--- +source: crates/core/tests/container_env_test.rs +expression: pretty(container.impl_partial_env_values()) +--- +fn env_values_with_prefix( + prefix: Option<&str>, +) -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + let mut env = EnvManager::new(prefix); + let mut partial = Self::default(); + partial.a = env.get("STR")?; + partial.b = env.get("BOOL")?; + partial.c = env.get("INT")?; + partial.d = env.nested(PartialNestedConfig::env_values()?)?; + partial.e = env.nested(PartialCustomConfig::env_values()?)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_different_types.snap b/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_different_types.snap index 9c7aa8b4..992077f8 100644 --- a/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_different_types.snap +++ b/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_different_types.snap @@ -2,9 +2,11 @@ source: crates/core/tests/field_env_test.rs expression: pretty(container.impl_partial_env_values()) --- -fn env_values() -> std::result::Result, schematic::ConfigError> { +fn env_values_with_prefix( + prefix: Option<&str>, +) -> std::result::Result, schematic::ConfigError> { use schematic::internal::*; - let mut env = EnvManager::default(); + let mut env = EnvManager::new(prefix); let mut partial = Self::default(); partial.a = env.get("A")?; partial.b = env.get("B")?; diff --git a/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_nested.snap b/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_nested.snap index d82a0342..a8589658 100644 --- a/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_nested.snap @@ -2,9 +2,11 @@ source: crates/core/tests/field_env_test.rs expression: pretty(container.impl_partial_env_values()) --- -fn env_values() -> std::result::Result, schematic::ConfigError> { +fn env_values_with_prefix( + prefix: Option<&str>, +) -> std::result::Result, schematic::ConfigError> { use schematic::internal::*; - let mut env = EnvManager::default(); + let mut env = EnvManager::new(prefix); let mut partial = Self::default(); partial.a = env.nested(PartialNestedConfig::env_values()?)?; partial.b = env.nested(PartialCustomConfig::env_values()?)?; diff --git a/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_different_types.snap b/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_different_types.snap index 089f22de..5cd475e8 100644 --- a/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_different_types.snap +++ b/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_different_types.snap @@ -2,9 +2,11 @@ source: crates/core/tests/field_env_test.rs expression: pretty(container.impl_partial_env_values()) --- -fn env_values() -> std::result::Result, schematic::ConfigError> { +fn env_values_with_prefix( + prefix: Option<&str>, +) -> std::result::Result, schematic::ConfigError> { use schematic::internal::*; - let mut env = EnvManager::default(); + let mut env = EnvManager::new(prefix); let mut partial = Self::default(); partial.1 = env.get("A")?; partial.2 = env.get("B")?; diff --git a/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_nested.snap index f29cae7f..7d76d6a6 100644 --- a/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_nested.snap @@ -2,9 +2,11 @@ source: crates/core/tests/field_env_test.rs expression: pretty(container.impl_partial_env_values()) --- -fn env_values() -> std::result::Result, schematic::ConfigError> { +fn env_values_with_prefix( + prefix: Option<&str>, +) -> std::result::Result, schematic::ConfigError> { use schematic::internal::*; - let mut env = EnvManager::default(); + let mut env = EnvManager::new(prefix); let mut partial = Self::default(); partial.0 = env.nested(PartialNestedConfig::env_values()?)?; partial.1 = env.nested(PartialCustomConfig::env_values()?)?; diff --git a/crates/core/tests/snapshots/field_env_test__field_env_prefix__named_struct__supports_nested.snap b/crates/core/tests/snapshots/field_env_test__field_env_prefix__named_struct__supports_nested.snap new file mode 100644 index 00000000..b04fc0f5 --- /dev/null +++ b/crates/core/tests/snapshots/field_env_test__field_env_prefix__named_struct__supports_nested.snap @@ -0,0 +1,16 @@ +--- +source: crates/core/tests/field_env_test.rs +expression: pretty(container.impl_partial_env_values()) +--- +fn env_values_with_prefix( + prefix: Option<&str>, +) -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + let mut env = EnvManager::new(prefix); + let mut partial = Self::default(); + partial.a = env.nested(PartialNestedConfig::env_values()?)?; + partial.b = env.nested(PartialCustomConfig::env_values()?)?; + partial.c = env.nested(PartialNestedConfig::env_values_with_prefix(Some("PRE_"))?)?; + partial.d = env.nested(PartialCustomConfig::env_values_with_prefix(Some("PRE_"))?)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/field_env_test__field_env_prefix__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/field_env_test__field_env_prefix__unnamed_struct__supports_nested.snap new file mode 100644 index 00000000..b31f1285 --- /dev/null +++ b/crates/core/tests/snapshots/field_env_test__field_env_prefix__unnamed_struct__supports_nested.snap @@ -0,0 +1,16 @@ +--- +source: crates/core/tests/field_env_test.rs +expression: pretty(container.impl_partial_env_values()) +--- +fn env_values_with_prefix( + prefix: Option<&str>, +) -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + let mut env = EnvManager::new(prefix); + let mut partial = Self::default(); + partial.0 = env.nested(PartialNestedConfig::env_values()?)?; + partial.1 = env.nested(PartialCustomConfig::env_values()?)?; + partial.2 = env.nested(PartialNestedConfig::env_values_with_prefix(Some("PRE_"))?)?; + partial.3 = env.nested(PartialCustomConfig::env_values_with_prefix(Some("PRE_"))?)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/schematic/src/config/configs.rs b/crates/schematic/src/config/configs.rs index 45d8b11c..202e1404 100644 --- a/crates/schematic/src/config/configs.rs +++ b/crates/schematic/src/config/configs.rs @@ -29,6 +29,13 @@ pub trait PartialConfig: /// the variable fails to parse or cast into the correct type, an error is returned. #[cfg(feature = "env")] fn env_values() -> Result, ConfigError> { + Self::env_values_with_prefix(None) + } + + /// Internal use only, use [`env_values`] instead. + #[cfg(feature = "env")] + #[doc(hidden)] + fn env_values_with_prefix(_prefix: Option<&str>) -> Result, ConfigError> { Ok(None) } diff --git a/crates/schematic/src/internal.rs b/crates/schematic/src/internal.rs index 4f5481d4..1aaca9cd 100644 --- a/crates/schematic/src/internal.rs +++ b/crates/schematic/src/internal.rs @@ -17,12 +17,21 @@ mod env { use super::*; use crate::config::ParseEnvResult; - #[derive(Default)] pub struct EnvManager { count: u8, + prefix: String, } impl EnvManager { + pub fn new>(prefix: Option) -> Self { + Self { + count: 0, + prefix: prefix + .map(|pre| pre.as_ref().to_string()) + .unwrap_or_default(), + } + } + pub fn is_empty(&self) -> bool { self.count == 0 } @@ -36,7 +45,9 @@ mod env { key: &str, parser: impl Fn(String) -> ParseEnvResult, ) -> ParseEnvResult { - if let Ok(value) = std::env::var(key) { + let key = format!("{}{key}", self.prefix); + + if let Ok(value) = std::env::var(&key) { return parser(value) .inspect(|inner| { if inner.is_some() { From 97f7ddfb03574fdb60f1b972469066142deed37b Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sat, 2 Aug 2025 13:56:33 -0700 Subject: [PATCH 17/33] Add extends. --- crates/core/src/container.rs | 50 +++++- crates/core/src/field.rs | 34 ++++- crates/core/src/field_value.rs | 48 ++++++ crates/core/tests/field_extend_test.rs | 143 ++++++++++++++++++ ...est__field_extend__can_set_opt_string.snap | 7 + ..._test__field_extend__can_set_opt_type.snap | 7 + ..._field_extend__can_set_opt_vec_string.snap | 7 + ...nd_test__field_extend__can_set_string.snap | 7 + ...tend_test__field_extend__can_set_type.snap | 7 + ...est__field_extend__can_set_vec_string.snap | 7 + crates/core/tests/utils.rs | 16 +- 11 files changed, 315 insertions(+), 18 deletions(-) create mode 100644 crates/core/tests/field_extend_test.rs create mode 100644 crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_string.snap create mode 100644 crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_type.snap create mode 100644 crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_vec_string.snap create mode 100644 crates/core/tests/snapshots/field_extend_test__field_extend__can_set_string.snap create mode 100644 crates/core/tests/snapshots/field_extend_test__field_extend__can_set_type.snap create mode 100644 crates/core/tests/snapshots/field_extend_test__field_extend__can_set_vec_string.snap diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 8bd02a3d..1dee5f02 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -89,16 +89,20 @@ impl Container { } }; - Self { + let container = Self { args, attrs: input.attrs, ident: input.ident, inner, serde_args, vis: input.vis, - } + }; + container.validate_args(); + container } + fn validate_args(&self) {} + pub fn get_partial_attributes(&self) -> Vec { let serde_args = self.get_partial_serde_attribute_args(); let mut attrs = vec![quote! { #[serde(#serde_args) ]}]; @@ -179,6 +183,7 @@ impl Container { let default_values_method = self.impl_partial_default_values(); let env_values_method = self.impl_partial_env_values(); + let extends_from_method = self.impl_partial_extends_from(); quote! { #[automatically_derived] @@ -187,6 +192,7 @@ impl Container { #default_values_method #env_values_method + #extends_from_method } #[automatically_derived] @@ -391,6 +397,46 @@ impl Container { } } } + + #[cfg(not(feature = "extends"))] + pub fn impl_partial_extends_from(&self) -> TokenStream { + quote! {} + } + + #[cfg(feature = "extends")] + pub fn impl_partial_extends_from(&self) -> TokenStream { + if let ContainerInner::NamedStruct { fields } = &self.inner { + let mut names = vec![]; + let mut inner = quote! { None }; + + for field in fields { + if field.is_extendable() { + names.push(field.get_name_original().to_string()); + + let res = field.impl_partial_extends_from(); + + if !res.no_value { + inner = res.value; + } + } + } + + if names.len() > 1 { + panic!( + "Only 1 setting may use `extend`, found: {}", + names.join(", ") + ); + } + + quote! { + fn extends_from(&self) -> Option { + #inner + } + } + } else { + panic!("Only named structs can use `extend` settings."); + } + } } impl ToTokens for Container { diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 5fe5e02b..56a7bc8a 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -91,7 +91,7 @@ pub struct FieldArgs { #[cfg(feature = "env")] pub parse_env: Option, pub partial: Option, - pub required: bool, + pub required: bool, // TODO pub transform: Option, #[cfg(feature = "validate")] pub validate: Option, @@ -232,10 +232,25 @@ impl Field { return name.into(); } + self.get_name_original().to_string() + } + + pub fn get_name_original(&self) -> &Ident { self.ident .as_ref() .expect("Name only usable on named fields!") - .to_string() + } + + pub fn is_extendable(&self) -> bool { + #[cfg(feature = "extends")] + { + self.args.extend + } + + #[cfg(not(feature = "extends"))] + { + false + } } pub fn is_nested(&self) -> bool { @@ -277,12 +292,6 @@ impl Field { self.value.impl_partial_default_value(&self.args) } - #[cfg(not(feature = "env"))] - pub fn impl_partial_env_value(&self) -> ImplResult { - ImplResult::skipped() - } - - #[cfg(feature = "env")] pub fn impl_partial_env_value(&self) -> ImplResult { if self.is_nested() { return self.value.impl_partial_env_value(&self.args, ""); @@ -298,6 +307,15 @@ impl Field { self.value.impl_partial_env_value(&self.args, &env_key) } + + pub fn impl_partial_extends_from(&self) -> ImplResult { + if self.is_extendable() { + self.value + .impl_partial_extends_from(self.get_name_original()) + } else { + ImplResult::skipped() + } + } } struct Index(usize); diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index d7e4358f..916793bf 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -194,6 +194,12 @@ impl FieldValue { res } + #[cfg(not(feature = "env"))] + pub fn impl_partial_env_value(&self, _field_args: &FieldArgs, _env_key: &str) -> ImplResult { + ImplResult::skipped() + } + + #[cfg(feature = "env")] pub fn impl_partial_env_value(&self, field_args: &FieldArgs, env_key: &str) -> ImplResult { let mut res = ImplResult::default(); @@ -231,6 +237,48 @@ impl FieldValue { res } + + #[cfg(not(feature = "extends"))] + pub fn impl_partial_extends_from(&self) -> ImplResult { + ImplResult::skipped() + } + + #[cfg(feature = "extends")] + pub fn impl_partial_extends_from(&self, field_name: &Ident) -> ImplResult { + let mut res = ImplResult::default(); + + res.value = match self.ty_string.as_str() { + "String" | "Option" => { + quote! { + self.#field_name + .as_ref() + .map(|inner| schematic::ExtendsFrom::String(inner.to_owned())) + } + } + "Vec" | "Option>" => { + quote! { + self.#field_name + .as_ref() + .map(|inner| schematic::ExtendsFrom::List(inner.to_owned())) + } + } + "ExtendsFrom" + | "schematic::ExtendsFrom" + | "Option" + | "Option" => { + quote! { + self.#field_name.clone() + } + } + inner => { + panic!( + "Only `String`, `Vec`, or `schematic::ExtendsFrom` are supported when using `extend` for {field_name}. Received `{inner}`." + ); + } + }; + + res + } } fn extract_type_information( diff --git a/crates/core/tests/field_extend_test.rs b/crates/core/tests/field_extend_test.rs new file mode 100644 index 00000000..6eba3635 --- /dev/null +++ b/crates/core/tests/field_extend_test.rs @@ -0,0 +1,143 @@ +mod utils; + +use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; +use utils::pretty; + +mod field_extend { + use super::*; + + #[test] + fn can_set_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.extend); + assert_snapshot!(pretty(container.impl_partial_extends_from())); + } + + #[test] + fn can_set_opt_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: Option, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.extend); + assert_snapshot!(pretty(container.impl_partial_extends_from())); + } + + #[test] + fn can_set_vec_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: Vec, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.extend); + assert_snapshot!(pretty(container.impl_partial_extends_from())); + } + + #[test] + fn can_set_opt_vec_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: Option>, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.extend); + assert_snapshot!(pretty(container.impl_partial_extends_from())); + } + + #[test] + fn can_set_type() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: ExtendsFrom, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.extend); + assert_snapshot!(pretty(container.impl_partial_extends_from())); + } + + #[test] + fn can_set_opt_type() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: Option, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.extend); + assert_snapshot!(pretty(container.impl_partial_extends_from())); + } + + #[test] + #[should_panic(expected = "Only 1 setting may use `extend`, found: a, b")] + fn errors_multiple_extends() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: String, + #[setting(extend)] + b: String, + } + }) + .impl_partial_extends_from(); + } + + #[test] + #[should_panic( + expected = "Only `String`, `Vec`, or `schematic::ExtendsFrom` are supported" + )] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: bool, + } + }) + .impl_partial_extends_from(); + } + + #[test] + #[should_panic(expected = "Only named structs can use `extend` settings.")] + fn errors_when_used_in_unnamed_struct() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(extend)] + String, + ); + }) + .impl_partial_extends_from(); + } +} diff --git a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_string.snap b/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_string.snap new file mode 100644 index 00000000..ff72180f --- /dev/null +++ b/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_string.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/field_extend_test.rs +expression: pretty(container.impl_partial_extends_from()) +--- +fn extends_from(&self) -> Option { + self.a.as_ref().map(|inner| schematic::ExtendsFrom::String(inner.to_owned())) +} diff --git a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_type.snap b/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_type.snap new file mode 100644 index 00000000..97e638dc --- /dev/null +++ b/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_type.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/field_extend_test.rs +expression: pretty(container.impl_partial_extends_from()) +--- +fn extends_from(&self) -> Option { + self.a.clone() +} diff --git a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_vec_string.snap b/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_vec_string.snap new file mode 100644 index 00000000..280d50ff --- /dev/null +++ b/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_vec_string.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/field_extend_test.rs +expression: pretty(container.impl_partial_extends_from()) +--- +fn extends_from(&self) -> Option { + self.a.as_ref().map(|inner| schematic::ExtendsFrom::List(inner.to_owned())) +} diff --git a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_string.snap b/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_string.snap new file mode 100644 index 00000000..ff72180f --- /dev/null +++ b/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_string.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/field_extend_test.rs +expression: pretty(container.impl_partial_extends_from()) +--- +fn extends_from(&self) -> Option { + self.a.as_ref().map(|inner| schematic::ExtendsFrom::String(inner.to_owned())) +} diff --git a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_type.snap b/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_type.snap new file mode 100644 index 00000000..97e638dc --- /dev/null +++ b/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_type.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/field_extend_test.rs +expression: pretty(container.impl_partial_extends_from()) +--- +fn extends_from(&self) -> Option { + self.a.clone() +} diff --git a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_vec_string.snap b/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_vec_string.snap new file mode 100644 index 00000000..280d50ff --- /dev/null +++ b/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_vec_string.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/field_extend_test.rs +expression: pretty(container.impl_partial_extends_from()) +--- +fn extends_from(&self) -> Option { + self.a.as_ref().map(|inner| schematic::ExtendsFrom::List(inner.to_owned())) +} diff --git a/crates/core/tests/utils.rs b/crates/core/tests/utils.rs index abfa7cfc..a5b40f77 100644 --- a/crates/core/tests/utils.rs +++ b/crates/core/tests/utils.rs @@ -6,11 +6,11 @@ pub fn pretty(tokens: TokenStream) -> String { prettyplease::unparse(&syn::parse_file(&tokens.to_string()).unwrap()) } -pub fn get_field<'a>(container: &'a Container, key: &str) -> &'a Field { - container - .inner - .get_fields() - .into_iter() - .find(|field| field.ident.as_ref().is_some_and(|id| id == key)) - .unwrap() -} +// pub fn get_field<'a>(container: &'a Container, key: &str) -> &'a Field { +// container +// .inner +// .get_fields() +// .into_iter() +// .find(|field| field.ident.as_ref().is_some_and(|id| id == key)) +// .unwrap() +// } From b4dca446318ee8d73c5e5c66e4a058ac628bceb6 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sat, 2 Aug 2025 14:11:33 -0700 Subject: [PATCH 18/33] Polish. --- CHANGELOG.md | 1 + crates/core/src/container.rs | 2 +- crates/core/src/field.rs | 17 +++--- crates/core/src/field_value.rs | 12 ++--- crates/core/src/utils.rs | 2 +- ...tial_test__container_partial__can_set.snap | 54 +++++++++++++++++++ ..._partial_test__field_partial__can_set.snap | 54 +++++++++++++++++++ crates/core/tests/utils.rs | 4 +- crates/schematic/src/config/configs.rs | 7 ++- 9 files changed, 130 insertions(+), 23 deletions(-) create mode 100644 crates/core/tests/snapshots/container_partial_test__container_partial__can_set.snap create mode 100644 crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 67aba1a6..b7021eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Added support for env prefixes at the field level when the field is also nested. This will override the env prefix defined on the nested container: `#[setting(nested, env_prefix = "OVERRIDE_")]`. +- Updated `#[setting(extend)]` settings to support `Option` wrapped values. - Updated the methods of `PartialConfig` to all have a default implementation. This helps to greatly reduce the amount of macro generated code. - Improved the parse, handling, and validation of container and field attributes. diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 1dee5f02..78d9e04a 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -293,7 +293,7 @@ impl Container { panic!("Only 1 variant may be marked as default."); } - match default_variants.get(0) { + match default_variants.first() { Some(default_variant) => { let res = default_variant.impl_partial_default_value(); diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 56a7bc8a..9503dd7e 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -202,6 +202,10 @@ impl Field { return Some(self.get_name().to_uppercase()); } + if self.args.parse_env.is_some() { + panic!("Cannot use `parse_env` without `env` or a parent `env_prefix`."); + } + None } @@ -297,15 +301,10 @@ impl Field { return self.value.impl_partial_env_value(&self.args, ""); } - let Some(env_key) = self.get_env_var() else { - if self.args.parse_env.is_some() { - panic!("Cannot use `parse_env` without `env` or a parent `env_prefix`."); - } - - return ImplResult::skipped(); - }; - - self.value.impl_partial_env_value(&self.args, &env_key) + match self.get_env_var() { + Some(env_key) => self.value.impl_partial_env_value(&self.args, &env_key), + None => ImplResult::skipped(), + } } pub fn impl_partial_extends_from(&self) -> ImplResult { diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index 916793bf..e5f9ca15 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -239,15 +239,13 @@ impl FieldValue { } #[cfg(not(feature = "extends"))] - pub fn impl_partial_extends_from(&self) -> ImplResult { + pub fn impl_partial_extends_from(&self, _field_name: &Ident) -> ImplResult { ImplResult::skipped() } #[cfg(feature = "extends")] pub fn impl_partial_extends_from(&self, field_name: &Ident) -> ImplResult { - let mut res = ImplResult::default(); - - res.value = match self.ty_string.as_str() { + let value = match self.ty_string.as_str() { "String" | "Option" => { quote! { self.#field_name @@ -277,7 +275,10 @@ impl FieldValue { } }; - res + ImplResult { + value, + ..Default::default() + } } } @@ -307,7 +308,6 @@ fn extract_type_information( if let Some(GenericArgument::Type(inner_ty)) = args.args.last() { extract_type_information(inner_ty, layers, on_last); - return; } } diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index b2bc1521..5bc2541c 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -27,7 +27,7 @@ pub fn is_inheritable_attribute(attr: &Attribute) -> bool { } pub fn to_type_string(ts: TokenStream) -> String { - format!("{}", ts) + format!("{ts}") .replace(" :: ", "::") .replace(" , ", ", ") .replace(" < ", "<") diff --git a/crates/core/tests/snapshots/container_partial_test__container_partial__can_set.snap b/crates/core/tests/snapshots/container_partial_test__container_partial__can_set.snap new file mode 100644 index 00000000..30e64c13 --- /dev/null +++ b/crates/core/tests/snapshots/container_partial_test__container_partial__can_set.snap @@ -0,0 +1,54 @@ +--- +source: crates/core/tests/container_partial_test.rs +expression: container.args.partial.as_ref().unwrap() +--- +PartialArg { + meta: [ + Meta( + Meta::List { + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident( + derive, + ), + arguments: PathArguments::None, + }, + ], + }, + delimiter: MacroDelimiter::Paren( + Paren, + ), + tokens: TokenStream [ + Ident { + sym: Other, + }, + ], + }, + ), + Meta( + Meta::List { + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident( + serde, + ), + arguments: PathArguments::None, + }, + ], + }, + delimiter: MacroDelimiter::Paren( + Paren, + ), + tokens: TokenStream [ + Ident { + sym: another, + }, + ], + }, + ), + ], +} diff --git a/crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap b/crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap new file mode 100644 index 00000000..35d03902 --- /dev/null +++ b/crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap @@ -0,0 +1,54 @@ +--- +source: crates/core/tests/field_partial_test.rs +expression: field.args.partial.as_ref().unwrap() +--- +PartialArg { + meta: [ + Meta( + Meta::List { + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident( + other, + ), + arguments: PathArguments::None, + }, + ], + }, + delimiter: MacroDelimiter::Paren( + Paren, + ), + tokens: TokenStream [ + Ident { + sym: attribute, + }, + ], + }, + ), + Meta( + Meta::List { + path: Path { + leading_colon: None, + segments: [ + PathSegment { + ident: Ident( + and, + ), + arguments: PathArguments::None, + }, + ], + }, + delimiter: MacroDelimiter::Paren( + Paren, + ), + tokens: TokenStream [ + Ident { + sym: another, + }, + ], + }, + ), + ], +} diff --git a/crates/core/tests/utils.rs b/crates/core/tests/utils.rs index a5b40f77..3a02b2f5 100644 --- a/crates/core/tests/utils.rs +++ b/crates/core/tests/utils.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; -use schematic_core::container::Container; -use schematic_core::field::Field; +// use schematic_core::container::Container; +// use schematic_core::field::Field; pub fn pretty(tokens: TokenStream) -> String { prettyplease::unparse(&syn::parse_file(&tokens.to_string()).unwrap()) diff --git a/crates/schematic/src/config/configs.rs b/crates/schematic/src/config/configs.rs index 202e1404..3eb93fbb 100644 --- a/crates/schematic/src/config/configs.rs +++ b/crates/schematic/src/config/configs.rs @@ -102,11 +102,10 @@ pub trait Config: Sized + Schematic { /// Return default values for the partial configuration. fn default_partial() -> Self::Partial { let context = <::Partial as PartialConfig>::Context::default(); - let defaults = <::Partial as PartialConfig>::default_values(&context) - .unwrap() - .unwrap_or_default(); - defaults + <::Partial as PartialConfig>::default_values(&context) + .unwrap() + .unwrap_or_default() } /// Convert a partial configuration into a full configuration, with all values populated. From 31b86b21ccbbebc8f953efac36a1aacdfba59c93 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 3 Aug 2025 08:56:17 -0700 Subject: [PATCH 19/33] Add merge. --- crates/core/src/container.rs | 67 ++++++ crates/core/src/field.rs | 6 +- crates/core/src/field_value.rs | 61 ++++- crates/core/src/variant.rs | 4 + crates/core/tests/field_merge_test.rs | 220 ++++++++++++++++++ ...ld_merge__named_struct__supports_func.snap | 20 ++ ..._merge__named_struct__supports_nested.snap | 17 ++ ...d_struct__supports_nested_collections.snap | 16 ++ ...erge__named_struct__supports_standard.snap | 20 ++ ..._merge__unnamed_struct__supports_func.snap | 20 ++ ...erge__unnamed_struct__supports_nested.snap | 17 ++ ...d_struct__supports_nested_collections.snap | 16 ++ ...ge__unnamed_struct__supports_standard.snap | 20 ++ crates/schematic/src/internal.rs | 172 ++++++++------ 14 files changed, 605 insertions(+), 71 deletions(-) create mode 100644 crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_func.snap create mode 100644 crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_standard.snap create mode 100644 crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_func.snap create mode 100644 crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_standard.snap diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 78d9e04a..67ae3bb6 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -184,6 +184,7 @@ impl Container { let default_values_method = self.impl_partial_default_values(); let env_values_method = self.impl_partial_env_values(); let extends_from_method = self.impl_partial_extends_from(); + let merge_method = self.impl_partial_merge(); quote! { #[automatically_derived] @@ -193,6 +194,7 @@ impl Container { #default_values_method #env_values_method #extends_from_method + #merge_method } #[automatically_derived] @@ -437,6 +439,71 @@ impl Container { panic!("Only named structs can use `extend` settings."); } } + + pub fn impl_partial_merge(&self) -> TokenStream { + let mut statements = vec![]; + + let inner = match &self.inner { + ContainerInner::NamedStruct { fields } | ContainerInner::UnnamedStruct { fields } => { + for field in fields { + let res = field.impl_partial_merge(); + + if !res.no_value { + statements.push(res.value); + } + } + + if statements.is_empty() { + return quote! {}; + } + + quote! { + #(#statements)* + } + } + ContainerInner::Enum { variants } | ContainerInner::UnitEnum { variants } => { + for variant in variants { + let res = variant.impl_partial_merge(); + + if !res.no_value { + statements.push(res.value); + } + } + + if statements.is_empty() { + quote! { + *self = next; + } + } else { + quote! { + match self { + #(#statements)* + _ => { + *self = next; + } + }; + } + } + } + }; + + let internal = ImplResult::impl_use_internal(true); + + quote! { + fn merge( + &mut self, + context: &Self::Context, + mut next: Self, + ) -> std::result::Result<(), schematic::ConfigError> { + #internal + + MergeManager::new(context) + #inner; + + Ok(()) + } + } + } } impl ToTokens for Container { diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 9503dd7e..7851333b 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -310,11 +310,15 @@ impl Field { pub fn impl_partial_extends_from(&self) -> ImplResult { if self.is_extendable() { self.value - .impl_partial_extends_from(self.get_name_original()) + .impl_partial_extends_from(&self.args, self.get_key()) } else { ImplResult::skipped() } } + + pub fn impl_partial_merge(&self) -> ImplResult { + self.value.impl_partial_merge(&self.args, self.get_key()) + } } struct Index(usize); diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index e5f9ca15..389d7f5b 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -1,5 +1,6 @@ use crate::field::{FieldArgs, FieldNestedArg}; use crate::utils::{ImplResult, to_type_string}; +use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote}; use syn::{Expr, GenericArgument, Ident, Lit, PathArguments, PathSegment, Type}; @@ -239,12 +240,20 @@ impl FieldValue { } #[cfg(not(feature = "extends"))] - pub fn impl_partial_extends_from(&self, _field_name: &Ident) -> ImplResult { + pub fn impl_partial_extends_from( + &self, + _field_args: &FieldArgs, + _field_name: TokenStream, + ) -> ImplResult { ImplResult::skipped() } #[cfg(feature = "extends")] - pub fn impl_partial_extends_from(&self, field_name: &Ident) -> ImplResult { + pub fn impl_partial_extends_from( + &self, + _field_args: &FieldArgs, + field_name: TokenStream, + ) -> ImplResult { let value = match self.ty_string.as_str() { "String" | "Option" => { quote! { @@ -280,6 +289,54 @@ impl FieldValue { ..Default::default() } } + + pub fn impl_partial_merge( + &self, + field_args: &FieldArgs, + field_name: TokenStream, + ) -> ImplResult { + let value = match field_args.merge.as_ref() { + Some(func) => { + if self.nested && !self.is_collection() { + panic!("Nested configs do not support `merge` unless wrapped in a collection."); + } + + quote! { + .apply_with( + &mut self.#field_name, + next.#field_name, + #func, + )? + } + } + _ => { + if self.nested { + if self.is_collection() { + panic!("Collections with nested configs must manually define `merge`."); + } + + quote! { + .nested( + &mut self.#field_name, + next.#field_name, + )? + } + } else { + quote! { + .apply( + &mut self.#field_name, + next.#field_name, + )? + } + } + } + }; + + ImplResult { + value, + ..Default::default() + } + } } fn extract_type_information( diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index beeb53f2..e969111a 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -82,4 +82,8 @@ impl Variant { res } + + pub fn impl_partial_merge(&self) -> ImplResult { + ImplResult::default() + } } diff --git a/crates/core/tests/field_merge_test.rs b/crates/core/tests/field_merge_test.rs index a8c10ef5..1a0aa829 100644 --- a/crates/core/tests/field_merge_test.rs +++ b/crates/core/tests/field_merge_test.rs @@ -1,5 +1,9 @@ +mod utils; + use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; use syn::parse_quote; +use utils::pretty; mod field_merge { use super::*; @@ -43,4 +47,220 @@ mod field_merge { } }); } + + mod named_struct { + use super::*; + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example{ + a: bool, + b: usize, + c: String, + d: i16, + e: Option, + f: Vec, + g: Option>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: NestedConfig, + #[setting(nested = CustomConfig)] + b: CustomConfig, + #[setting(nested)] + a: Option, + #[setting(nested = CustomConfig)] + b: Arc, + } + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + + #[test] + fn supports_nested_collections() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested, merge = append_vec)] + a: Vec, + #[setting(nested = CustomConfig, merge = merge_hashmap)] + b: HashMap, + #[setting(nested, merge = merge_btreeset)] + a: Option>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + + #[test] + fn supports_func() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example{ + a: bool, + b: usize, + #[setting(merge = discard)] + c: String, + #[setting(merge = "preserve")] + d: i16, + e: Option, + #[setting(merge = append_vec)] + f: Vec, + #[setting(merge = merge_hashmap)] + g: Option>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + + #[test] + #[should_panic( + expected = "Nested configs do not support `merge` unless wrapped in a collection." + )] + fn errors_if_nested_has_merge_attr() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested, merge = append_vec)] + a: NestedConfig, + } + }) + .impl_partial_merge(); + } + + #[test] + #[should_panic(expected = "Collections with nested configs must manually define `merge`.")] + fn errors_if_collection_doesnt_have_merge_attr() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: Vec, + } + }) + .impl_partial_merge(); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Exampl( + bool, + usize, + String, + i16, + Option, + Vec, + Option>, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested)] + NestedConfig, + #[setting(nested = CustomConfig)] + CustomConfig, + #[setting(nested)] + Option, + #[setting(nested = CustomConfig)] + Arc, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + + #[test] + fn supports_nested_collections() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested, merge = append_vec)] + Vec, + #[setting(nested = CustomConfig, merge = merge_hashmap)] + HashMap, + #[setting(nested, merge = merge_btreeset)] + Option>, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + + #[test] + fn supports_func() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example ( + bool, + usize, + #[setting(merge = discard)] + String, + #[setting(merge = "preserve")] + i16, + Option, + #[setting(merge = append_vec)] + Vec, + #[setting(merge = merge_hashmap)] + Option>, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + + #[test] + #[should_panic( + expected = "Nested configs do not support `merge` unless wrapped in a collection." + )] + fn errors_if_nested_has_merge_attr() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested, merge = append_vec)] + NestedConfig, + ); + }) + .impl_partial_merge(); + } + + #[test] + #[should_panic(expected = "Collections with nested configs must manually define `merge`.")] + fn errors_if_collection_doesnt_have_merge_attr() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested)] + Vec, + ); + }) + .impl_partial_merge(); + } + } } diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_func.snap b/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_func.snap new file mode 100644 index 00000000..eb07119f --- /dev/null +++ b/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_func.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/field_merge_test.rs +expression: pretty(container.impl_partial_merge()) +--- +fn merge( + &mut self, + context: &Self::Context, + mut next: Self, +) -> std::result::Result<(), schematic::ConfigError> { + use schematic::internal::*; + MergeManager::new(context) + .apply(&mut self.a, next.a)? + .apply(&mut self.b, next.b)? + .apply_with(&mut self.c, next.c, discard)? + .apply_with(&mut self.d, next.d, preserve)? + .apply(&mut self.e, next.e)? + .apply_with(&mut self.f, next.f, append_vec)? + .apply_with(&mut self.g, next.g, merge_hashmap)?; + Ok(()) +} diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested.snap b/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested.snap new file mode 100644 index 00000000..51cfac33 --- /dev/null +++ b/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested.snap @@ -0,0 +1,17 @@ +--- +source: crates/core/tests/field_merge_test.rs +expression: pretty(container.impl_partial_merge()) +--- +fn merge( + &mut self, + context: &Self::Context, + mut next: Self, +) -> std::result::Result<(), schematic::ConfigError> { + use schematic::internal::*; + MergeManager::new(context) + .nested(&mut self.a, next.a)? + .nested(&mut self.b, next.b)? + .nested(&mut self.a, next.a)? + .nested(&mut self.b, next.b)?; + Ok(()) +} diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested_collections.snap new file mode 100644 index 00000000..e96a6e01 --- /dev/null +++ b/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested_collections.snap @@ -0,0 +1,16 @@ +--- +source: crates/core/tests/field_merge_test.rs +expression: pretty(container.impl_partial_merge()) +--- +fn merge( + &mut self, + context: &Self::Context, + mut next: Self, +) -> std::result::Result<(), schematic::ConfigError> { + use schematic::internal::*; + MergeManager::new(context) + .apply_with(&mut self.a, next.a, append_vec)? + .apply_with(&mut self.b, next.b, merge_hashmap)? + .apply_with(&mut self.a, next.a, merge_btreeset)?; + Ok(()) +} diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_standard.snap b/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_standard.snap new file mode 100644 index 00000000..aa00a0fd --- /dev/null +++ b/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_standard.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/field_merge_test.rs +expression: pretty(container.impl_partial_merge()) +--- +fn merge( + &mut self, + context: &Self::Context, + mut next: Self, +) -> std::result::Result<(), schematic::ConfigError> { + use schematic::internal::*; + MergeManager::new(context) + .apply(&mut self.a, next.a)? + .apply(&mut self.b, next.b)? + .apply(&mut self.c, next.c)? + .apply(&mut self.d, next.d)? + .apply(&mut self.e, next.e)? + .apply(&mut self.f, next.f)? + .apply(&mut self.g, next.g)?; + Ok(()) +} diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_func.snap b/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_func.snap new file mode 100644 index 00000000..8a49a434 --- /dev/null +++ b/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_func.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/field_merge_test.rs +expression: pretty(container.impl_partial_merge()) +--- +fn merge( + &mut self, + context: &Self::Context, + mut next: Self, +) -> std::result::Result<(), schematic::ConfigError> { + use schematic::internal::*; + MergeManager::new(context) + .apply(&mut self.0, next.0)? + .apply(&mut self.1, next.1)? + .apply_with(&mut self.2, next.2, discard)? + .apply_with(&mut self.3, next.3, preserve)? + .apply(&mut self.4, next.4)? + .apply_with(&mut self.5, next.5, append_vec)? + .apply_with(&mut self.6, next.6, merge_hashmap)?; + Ok(()) +} diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested.snap new file mode 100644 index 00000000..177a038b --- /dev/null +++ b/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested.snap @@ -0,0 +1,17 @@ +--- +source: crates/core/tests/field_merge_test.rs +expression: pretty(container.impl_partial_merge()) +--- +fn merge( + &mut self, + context: &Self::Context, + mut next: Self, +) -> std::result::Result<(), schematic::ConfigError> { + use schematic::internal::*; + MergeManager::new(context) + .nested(&mut self.0, next.0)? + .nested(&mut self.1, next.1)? + .nested(&mut self.2, next.2)? + .nested(&mut self.3, next.3)?; + Ok(()) +} diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested_collections.snap new file mode 100644 index 00000000..14bc2749 --- /dev/null +++ b/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested_collections.snap @@ -0,0 +1,16 @@ +--- +source: crates/core/tests/field_merge_test.rs +expression: pretty(container.impl_partial_merge()) +--- +fn merge( + &mut self, + context: &Self::Context, + mut next: Self, +) -> std::result::Result<(), schematic::ConfigError> { + use schematic::internal::*; + MergeManager::new(context) + .apply_with(&mut self.0, next.0, append_vec)? + .apply_with(&mut self.1, next.1, merge_hashmap)? + .apply_with(&mut self.2, next.2, merge_btreeset)?; + Ok(()) +} diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_standard.snap b/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_standard.snap new file mode 100644 index 00000000..e697717d --- /dev/null +++ b/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_standard.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/field_merge_test.rs +expression: pretty(container.impl_partial_merge()) +--- +fn merge( + &mut self, + context: &Self::Context, + mut next: Self, +) -> std::result::Result<(), schematic::ConfigError> { + use schematic::internal::*; + MergeManager::new(context) + .apply(&mut self.0, next.0)? + .apply(&mut self.1, next.1)? + .apply(&mut self.2, next.2)? + .apply(&mut self.3, next.3)? + .apply(&mut self.4, next.4)? + .apply(&mut self.5, next.5)? + .apply(&mut self.6, next.6)?; + Ok(()) +} diff --git a/crates/schematic/src/internal.rs b/crates/schematic/src/internal.rs index 1aaca9cd..1eb2484d 100644 --- a/crates/schematic/src/internal.rs +++ b/crates/schematic/src/internal.rs @@ -1,4 +1,6 @@ -use crate::config::{ConfigError, HandlerError, MergeError, MergeResult, PartialConfig}; +use crate::config::{ + ConfigError, HandlerError, MergeError, MergeResult, ParseEnvResult, PartialConfig, +}; use schematic_types::Schema; use std::str::FromStr; @@ -12,68 +14,109 @@ pub fn handle_default_result( // ENV VARS -#[cfg(feature = "env")] -mod env { - use super::*; - use crate::config::ParseEnvResult; +pub struct EnvManager { + count: u8, + prefix: String, +} - pub struct EnvManager { - count: u8, - prefix: String, +impl EnvManager { + pub fn new>(prefix: Option) -> Self { + Self { + count: 0, + prefix: prefix + .map(|pre| pre.as_ref().to_string()) + .unwrap_or_default(), + } } - impl EnvManager { - pub fn new>(prefix: Option) -> Self { - Self { - count: 0, - prefix: prefix - .map(|pre| pre.as_ref().to_string()) - .unwrap_or_default(), - } - } + pub fn is_empty(&self) -> bool { + self.count == 0 + } - pub fn is_empty(&self) -> bool { - self.count == 0 - } + pub fn get(&mut self, key: &str) -> ParseEnvResult { + self.get_and_parse(key, |value| parse_value(value).map(|v| Some(v))) + } - pub fn get(&mut self, key: &str) -> ParseEnvResult { - self.get_and_parse(key, |value| parse_value(value).map(|v| Some(v))) + pub fn get_and_parse( + &mut self, + key: &str, + parser: impl Fn(String) -> ParseEnvResult, + ) -> ParseEnvResult { + let key = format!("{}{key}", self.prefix); + + if let Ok(value) = std::env::var(&key) { + return parser(value) + .inspect(|inner| { + if inner.is_some() { + self.count += 1; + } + }) + .map_err(|error| { + HandlerError(format!("Invalid environment variable {key}: {error}")) + }); } - pub fn get_and_parse( - &mut self, - key: &str, - parser: impl Fn(String) -> ParseEnvResult, - ) -> ParseEnvResult { - let key = format!("{}{key}", self.prefix); - - if let Ok(value) = std::env::var(&key) { - return parser(value) - .inspect(|inner| { - if inner.is_some() { - self.count += 1; - } - }) - .map_err(|error| { - HandlerError(format!("Invalid environment variable {key}: {error}")) - }); - } + Ok(None) + } - Ok(None) + pub fn nested(&mut self, partial: Option) -> ParseEnvResult { + if partial.is_some() { + self.count += 1; } - pub fn nested(&mut self, partial: Option) -> ParseEnvResult { - if partial.is_some() { - self.count += 1; - } + Ok(partial) + } +} - Ok(partial) +// MERGING + +pub struct MergeManager<'a, Ctx> { + context: &'a Ctx, +} + +impl<'a, Ctx> MergeManager<'a, Ctx> { + pub fn new(context: &'a Ctx) -> Self { + Self { context } + } + + pub fn apply(self, prev: &mut Option, next: Option) -> Result { + self.apply_with(prev, next, super::merge::replace) + } + + pub fn apply_with( + self, + prev: &mut Option, + next: Option, + merger: impl Fn(T, T, &Ctx) -> MergeResult, + ) -> Result { + let value = match (prev.take(), next) { + (Some(prev), Some(next)) => merger(prev, next, self.context)?, + (None, Some(next)) => Some(next), + (other, None) => other, + }; + + if let Some(value) = value { + prev.replace(value); } + + Ok(self) + } + + pub fn nested>( + self, + prev: &mut Option, + next: Option, + ) -> Result { + self.apply_with(prev, next, |mut p, n, ctx| { + p.merge(ctx, n) + .map_err(|error| MergeError(error.to_string()))?; + + Ok(Some(p)) + }) } } -#[cfg(feature = "env")] -pub use env::*; +// LEGACY #[cfg(feature = "env")] pub fn track_env(value: Option, tracker: &mut std::collections::HashSet) -> Option { @@ -110,40 +153,33 @@ pub fn parse_value>(value: V) -> Result( prev: Option, next: Option, context: &C, merger: impl Fn(T, T, &C) -> MergeResult, ) -> MergeResult { - if prev.is_some() && next.is_some() { - merger(prev.unwrap(), next.unwrap(), context) - } else if next.is_some() { - Ok(next) - } else { - Ok(prev) + match (prev, next) { + (Some(prev), Some(next)) => merger(prev, next, context), + (None, Some(next)) => Ok(Some(next)), + (other, _) => Ok(other), } } -#[allow(clippy::unnecessary_unwrap)] pub fn merge_nested_setting( prev: Option, next: Option, context: &T::Context, ) -> MergeResult { - if prev.is_some() && next.is_some() { - let mut nested = prev.unwrap(); - - nested - .merge(context, next.unwrap()) - .map_err(|error| MergeError(error.to_string()))?; - - Ok(Some(nested)) - } else if next.is_some() { - Ok(next) - } else { - Ok(prev) + match (prev, next) { + (Some(mut prev), Some(next)) => { + prev.merge(context, next) + .map_err(|error| MergeError(error.to_string()))?; + + Ok(Some(prev)) + } + (None, Some(next)) => Ok(Some(next)), + (other, _) => Ok(other), } } From 11784e0981b28ddbedcfee6610428be8e553dcdc Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 3 Aug 2025 09:42:01 -0700 Subject: [PATCH 20/33] Rename tests. --- crates/core/tests/field_nested_test.rs | 148 --------- crates/core/tests/field_partial_test.rs | 22 -- crates/core/tests/field_transform_test.rs | 46 --- crates/core/tests/field_validate_test.rs | 46 --- ...efault_test.rs => setting_default_test.rs} | 84 ++++- ...{field_env_test.rs => setting_env_test.rs} | 236 +++++++++----- ..._extend_test.rs => setting_extend_test.rs} | 2 +- ...ld_merge_test.rs => setting_merge_test.rs} | 128 +++++--- crates/core/tests/setting_nested_test.rs | 297 ++++++++++++++++++ crates/core/tests/setting_partial_test.rs | 24 ++ ...ld_serde_test.rs => setting_serde_test.rs} | 2 +- .../tests/{field_test.rs => setting_test.rs} | 2 +- crates/core/tests/setting_transform_test.rs | 94 ++++++ crates/core/tests/setting_validate_test.rs | 94 ++++++ ..._partial_test__field_partial__can_set.snap | 54 ---- ...setting_default__handles_collections.snap} | 2 +- ...est__setting_default__handles_layers.snap} | 2 +- ...enders_nothing_if_all_option_wrapped.snap} | 2 +- ...fault__named_struct__supports_nested.snap} | 2 +- ...ault__named_struct__supports_types-2.snap} | 2 +- ...efault__named_struct__supports_types.snap} | 2 +- ...tting_default__supports_handler_func.snap} | 2 +- ...setting_default__unit_enum__supports.snap} | 2 +- ...ting_default__unnamed_enum__supports.snap} | 2 +- ...enders_nothing_if_all_option_wrapped.snap} | 2 +- ...lt__unnamed_struct__supports_types-2.snap} | 2 +- ...ault__unnamed_struct__supports_types.snap} | 2 +- ...med_struct__supports_different_types.snap} | 2 +- ...g_env__named_struct__supports_nested.snap} | 2 +- ...med_struct__supports_different_types.snap} | 2 +- ...env__unnamed_struct__supports_nested.snap} | 2 +- ...refix__named_struct__supports_nested.snap} | 2 +- ...fix__unnamed_struct__supports_nested.snap} | 2 +- ...__setting_extend__can_set_opt_string.snap} | 2 +- ...st__setting_extend__can_set_opt_type.snap} | 2 +- ...tting_extend__can_set_opt_vec_string.snap} | 2 +- ...test__setting_extend__can_set_string.snap} | 2 +- ...d_test__setting_extend__can_set_type.snap} | 2 +- ...__setting_extend__can_set_vec_string.snap} | 2 +- ...g_merge__named_struct__supports_func.snap} | 2 +- ...merge__named_struct__supports_nested.snap} | 2 +- ..._struct__supports_nested_collections.snap} | 2 +- ...rge__named_struct__supports_standard.snap} | 2 +- ...merge__unnamed_struct__supports_func.snap} | 2 +- ...rge__unnamed_struct__supports_nested.snap} | 2 +- ..._struct__supports_nested_collections.snap} | 2 +- ...e__unnamed_struct__supports_standard.snap} | 2 +- crates/core/tests/variant_default_test.rs | 80 ----- 48 files changed, 872 insertions(+), 551 deletions(-) delete mode 100644 crates/core/tests/field_nested_test.rs delete mode 100644 crates/core/tests/field_partial_test.rs delete mode 100644 crates/core/tests/field_transform_test.rs delete mode 100644 crates/core/tests/field_validate_test.rs rename crates/core/tests/{field_default_test.rs => setting_default_test.rs} (71%) rename crates/core/tests/{field_env_test.rs => setting_env_test.rs} (63%) rename crates/core/tests/{field_extend_test.rs => setting_extend_test.rs} (99%) rename crates/core/tests/{field_merge_test.rs => setting_merge_test.rs} (74%) create mode 100644 crates/core/tests/setting_nested_test.rs create mode 100644 crates/core/tests/setting_partial_test.rs rename crates/core/tests/{field_serde_test.rs => setting_serde_test.rs} (99%) rename crates/core/tests/{field_test.rs => setting_test.rs} (99%) create mode 100644 crates/core/tests/setting_transform_test.rs create mode 100644 crates/core/tests/setting_validate_test.rs delete mode 100644 crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap rename crates/core/tests/snapshots/{field_default_test__field_default__handles_collections.snap => setting_default_test__setting_default__handles_collections.snap} (89%) rename crates/core/tests/snapshots/{field_default_test__field_default__handles_layers.snap => setting_default_test__setting_default__handles_layers.snap} (90%) rename crates/core/tests/snapshots/{field_default_test__field_default__named_struct__renders_nothing_if_all_option_wrapped.snap => setting_default_test__setting_default__named_struct__renders_nothing_if_all_option_wrapped.snap} (57%) rename crates/core/tests/snapshots/{field_default_test__field_default__named_struct__supports_nested.snap => setting_default_test__setting_default__named_struct__supports_nested.snap} (87%) rename crates/core/tests/snapshots/{field_default_test__field_default__named_struct__supports_types-2.snap => setting_default_test__setting_default__named_struct__supports_types-2.snap} (98%) rename crates/core/tests/snapshots/{field_default_test__field_default__named_struct__supports_types.snap => setting_default_test__setting_default__named_struct__supports_types.snap} (91%) rename crates/core/tests/snapshots/{field_default_test__field_default__supports_handler_func.snap => setting_default_test__setting_default__supports_handler_func.snap} (86%) rename crates/core/tests/snapshots/{variant_default_test__variant_default__unnamed_enum.snap => setting_default_test__setting_default__unit_enum__supports.snap} (83%) rename crates/core/tests/snapshots/{variant_default_test__variant_default__unit_enum.snap => setting_default_test__setting_default__unnamed_enum__supports.snap} (80%) rename crates/core/tests/snapshots/{field_default_test__field_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap => setting_default_test__setting_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap} (57%) rename crates/core/tests/snapshots/{field_default_test__field_default__unnamed_struct__supports_types-2.snap => setting_default_test__setting_default__unnamed_struct__supports_types-2.snap} (98%) rename crates/core/tests/snapshots/{field_default_test__field_default__unnamed_struct__supports_types.snap => setting_default_test__setting_default__unnamed_struct__supports_types.snap} (92%) rename crates/core/tests/snapshots/{field_env_test__field_env__named_struct__supports_different_types.snap => setting_env_test__setting_env__named_struct__supports_different_types.snap} (91%) rename crates/core/tests/snapshots/{field_env_test__field_env__named_struct__supports_nested.snap => setting_env_test__setting_env__named_struct__supports_nested.snap} (91%) rename crates/core/tests/snapshots/{field_env_test__field_env__unnamed_struct__supports_different_types.snap => setting_env_test__setting_env__unnamed_struct__supports_different_types.snap} (91%) rename crates/core/tests/snapshots/{field_env_test__field_env__unnamed_struct__supports_nested.snap => setting_env_test__setting_env__unnamed_struct__supports_nested.snap} (91%) rename crates/core/tests/snapshots/{field_env_test__field_env_prefix__named_struct__supports_nested.snap => setting_env_test__setting_env_prefix__named_struct__supports_nested.snap} (93%) rename crates/core/tests/snapshots/{field_env_test__field_env_prefix__unnamed_struct__supports_nested.snap => setting_env_test__setting_env_prefix__unnamed_struct__supports_nested.snap} (93%) rename crates/core/tests/snapshots/{field_extend_test__field_extend__can_set_opt_string.snap => setting_extend_test__setting_extend__can_set_opt_string.snap} (81%) rename crates/core/tests/snapshots/{field_extend_test__field_extend__can_set_opt_type.snap => setting_extend_test__setting_extend__can_set_opt_type.snap} (74%) rename crates/core/tests/snapshots/{field_extend_test__field_extend__can_set_vec_string.snap => setting_extend_test__setting_extend__can_set_opt_vec_string.snap} (80%) rename crates/core/tests/snapshots/{field_extend_test__field_extend__can_set_string.snap => setting_extend_test__setting_extend__can_set_string.snap} (81%) rename crates/core/tests/snapshots/{field_extend_test__field_extend__can_set_type.snap => setting_extend_test__setting_extend__can_set_type.snap} (74%) rename crates/core/tests/snapshots/{field_extend_test__field_extend__can_set_opt_vec_string.snap => setting_extend_test__setting_extend__can_set_vec_string.snap} (80%) rename crates/core/tests/snapshots/{field_merge_test__field_merge__named_struct__supports_func.snap => setting_merge_test__setting_merge__named_struct__supports_func.snap} (92%) rename crates/core/tests/snapshots/{field_merge_test__field_merge__named_struct__supports_nested.snap => setting_merge_test__setting_merge__named_struct__supports_nested.snap} (89%) rename crates/core/tests/snapshots/{field_merge_test__field_merge__named_struct__supports_nested_collections.snap => setting_merge_test__setting_merge__named_struct__supports_nested_collections.snap} (90%) rename crates/core/tests/snapshots/{field_merge_test__field_merge__named_struct__supports_standard.snap => setting_merge_test__setting_merge__named_struct__supports_standard.snap} (91%) rename crates/core/tests/snapshots/{field_merge_test__field_merge__unnamed_struct__supports_func.snap => setting_merge_test__setting_merge__unnamed_struct__supports_func.snap} (92%) rename crates/core/tests/snapshots/{field_merge_test__field_merge__unnamed_struct__supports_nested.snap => setting_merge_test__setting_merge__unnamed_struct__supports_nested.snap} (89%) rename crates/core/tests/snapshots/{field_merge_test__field_merge__unnamed_struct__supports_nested_collections.snap => setting_merge_test__setting_merge__unnamed_struct__supports_nested_collections.snap} (90%) rename crates/core/tests/snapshots/{field_merge_test__field_merge__unnamed_struct__supports_standard.snap => setting_merge_test__setting_merge__unnamed_struct__supports_standard.snap} (91%) delete mode 100644 crates/core/tests/variant_default_test.rs diff --git a/crates/core/tests/field_nested_test.rs b/crates/core/tests/field_nested_test.rs deleted file mode 100644 index 7cf4eee5..00000000 --- a/crates/core/tests/field_nested_test.rs +++ /dev/null @@ -1,148 +0,0 @@ -use quote::format_ident; -use schematic_core::container::Container; -use syn::parse_quote; - -mod field_nested { - use super::*; - - #[test] - fn word() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(nested)] - a: NestedConfig, - } - }); - let fields = container.inner.get_fields(); - - assert!(fields[0].value.nested); - assert_eq!( - fields[0].value.nested_ident.as_ref().unwrap(), - &format_ident!("NestedConfig") - ); - } - - #[test] - fn bool_true() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(nested = true)] - a: NestedConfig, - } - }); - let fields = container.inner.get_fields(); - - assert!(fields[0].value.nested); - assert_eq!( - fields[0].value.nested_ident.as_ref().unwrap(), - &format_ident!("NestedConfig") - ); - } - - #[test] - fn bool_false() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(nested = false)] - a: NestedConfig, - } - }); - let fields = container.inner.get_fields(); - - assert!(!fields[0].value.nested); - assert!(fields[0].value.nested_ident.is_none()); - } - - #[test] - fn explicit_ident() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(nested = NestedConfig)] - a: NestedConfig, - } - }); - let fields = container.inner.get_fields(); - - assert!(fields[0].value.nested); - assert_eq!( - fields[0].value.nested_ident.as_ref().unwrap(), - &format_ident!("NestedConfig") - ); - } - - #[test] - fn detect_in_vec() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(nested)] - a: Vec, - } - }); - let fields = container.inner.get_fields(); - - assert!(fields[0].value.nested); - assert_eq!( - fields[0].value.nested_ident.as_ref().unwrap(), - &format_ident!("NestedConfig") - ); - } - - #[test] - #[should_panic(expected = "UnexpectedType(\"paren\")")] - fn panics_invalid_expr() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(nested = (1 + 1))] - a: NestedConfig, - } - }); - } - - // #[test] - // #[should_panic( - // expected = "Unable to extract the nested configuration identifier from `Vec>>`. Try explicitly passing the identifier with `nested = ConfigName`." - // )] - // fn panics_cant_find_ident() { - // Container::from(parse_quote! { - // #[derive(Config)] - // struct Example { - // #[setting(nested)] - // a: Vec>>, - // } - // }); - // } - - #[test] - #[should_panic( - expected = "Too many segments for `sub::NestedConfig`, only a single identifier is allowed." - )] - fn panics_too_many_segments() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(nested = sub::NestedConfig)] - a: NestedConfig, - } - }); - } - - #[test] - #[should_panic( - expected = "Nested configuration identifier `OtherConfig` does not exist within `Vec`." - )] - fn panics_cant_find_custom_ident() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(nested = OtherConfig)] - a: Vec, - } - }); - } -} diff --git a/crates/core/tests/field_partial_test.rs b/crates/core/tests/field_partial_test.rs deleted file mode 100644 index 8530c202..00000000 --- a/crates/core/tests/field_partial_test.rs +++ /dev/null @@ -1,22 +0,0 @@ -use schematic_core::container::Container; -use starbase_sandbox::assert_debug_snapshot; -use syn::parse_quote; - -mod field_partial { - use super::*; - - #[test] - fn can_set() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(partial(other(attribute), and(another)))] - a: bool, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.partial.is_some()); - assert_debug_snapshot!(field.args.partial.as_ref().unwrap()); - } -} diff --git a/crates/core/tests/field_transform_test.rs b/crates/core/tests/field_transform_test.rs deleted file mode 100644 index b83d14ff..00000000 --- a/crates/core/tests/field_transform_test.rs +++ /dev/null @@ -1,46 +0,0 @@ -use schematic_core::container::Container; -use syn::parse_quote; - -mod field_transform { - use super::*; - - #[test] - fn accepts_func_ref() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(transform = func_ref)] - a: String, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.transform.is_some()); - } - - #[test] - fn accepts_string() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(transform = "func_ref")] - a: String, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.transform.is_some()); - } - - #[test] - #[should_panic(expected = "UnexpectedType")] - fn errors_invalid_type() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(transform = 123)] - a: String, - } - }); - } -} diff --git a/crates/core/tests/field_validate_test.rs b/crates/core/tests/field_validate_test.rs deleted file mode 100644 index 29c45e40..00000000 --- a/crates/core/tests/field_validate_test.rs +++ /dev/null @@ -1,46 +0,0 @@ -use schematic_core::container::Container; -use syn::parse_quote; - -mod field_validate { - use super::*; - - #[test] - fn accepts_func_ref() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(validate = func_ref)] - a: String, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.validate.is_some()); - } - - #[test] - fn accepts_curried_func() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(validate = func_call())] - a: String, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.validate.is_some()); - } - - #[test] - #[should_panic(expected = "UnexpectedType")] - fn errors_invalid_type() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(validate = 123)] - a: String, - } - }); - } -} diff --git a/crates/core/tests/field_default_test.rs b/crates/core/tests/setting_default_test.rs similarity index 71% rename from crates/core/tests/field_default_test.rs rename to crates/core/tests/setting_default_test.rs index 22946203..bacb80ec 100644 --- a/crates/core/tests/field_default_test.rs +++ b/crates/core/tests/setting_default_test.rs @@ -6,7 +6,7 @@ use std::collections::BTreeMap; use syn::parse_quote; use utils::pretty; -mod field_default { +mod setting_default { use super::*; #[test] @@ -180,4 +180,86 @@ mod field_default { assert_snapshot!(pretty(container.impl_partial_default_values())); } } + + mod named_enum { + use super::*; + + #[test] + #[should_panic(expected = "Enums with named fields are not supported!")] + fn errors_for_named_enum() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + Foo {}, + #[setting(default)] + Bar {}, + Baz {}, + } + }) + .impl_partial_default_values(); + } + } + + mod unnamed_enum { + use super::*; + + #[test] + #[should_panic(expected = "Only 1 variant may be marked as default.")] + fn errors_if_multiple_defaults() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(default)] + Foo, + #[setting(default)] + Bar, + #[setting(default)] + Baz, + } + }) + .impl_partial_default_values(); + } + + #[test] + fn supports() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + Foo, + #[setting(default)] + Bar, + Baz, + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + + let variants = container.inner.get_variants(); + + assert!(variants[1].args.default); + } + } + + mod unit_enum { + use super::*; + + #[test] + fn supports() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + Foo(usize), + #[setting(default)] + Bar(String, u8), + Baz(bool, String, isize), + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + + let variants = container.inner.get_variants(); + + assert!(variants[1].args.default); + } + } } diff --git a/crates/core/tests/field_env_test.rs b/crates/core/tests/setting_env_test.rs similarity index 63% rename from crates/core/tests/field_env_test.rs rename to crates/core/tests/setting_env_test.rs index 36dbec69..6355fd96 100644 --- a/crates/core/tests/field_env_test.rs +++ b/crates/core/tests/setting_env_test.rs @@ -5,38 +5,38 @@ use starbase_sandbox::assert_snapshot; use syn::parse_quote; use utils::pretty; -mod field_env { +mod setting_env { use super::*; - #[test] - #[should_panic(expected = "Wrapper types cannot be used with `env`.")] - fn errors_if_using_wrappers() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(env = "KEY")] - a: Arc, - } - }) - .impl_partial_env_values(); - } - - #[test] - #[should_panic(expected = "Collection types cannot be used with `env`.")] - fn errors_if_using_collections() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(env = "KEY")] - a: Vec, - } - }) - .impl_partial_env_values(); - } - mod named_struct { use super::*; + #[test] + #[should_panic(expected = "Wrapper types cannot be used with `env`.")] + fn errors_if_using_wrappers() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env = "KEY")] + a: Arc, + } + }) + .impl_partial_env_values(); + } + + #[test] + #[should_panic(expected = "Collection types cannot be used with `env`.")] + fn errors_if_using_collections() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env = "KEY")] + a: Vec, + } + }) + .impl_partial_env_values(); + } + #[test] fn accepts_string() { let container = Container::from(parse_quote! { @@ -103,6 +103,32 @@ mod field_env { mod unnamed_struct { use super::*; + #[test] + #[should_panic(expected = "Wrapper types cannot be used with `env`.")] + fn errors_if_using_wrappers() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(env = "KEY")] + Arc, + ); + }) + .impl_partial_env_values(); + } + + #[test] + #[should_panic(expected = "Collection types cannot be used with `env`.")] + fn errors_if_using_collections() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(env = "KEY")] + Vec, + ); + }) + .impl_partial_env_values(); + } + #[test] fn accepts_string() { let container = Container::from(parse_quote! { @@ -167,7 +193,7 @@ mod field_env { } } -mod field_env_prefix { +mod setting_env_prefix { use super::*; mod named_struct { @@ -311,58 +337,118 @@ mod field_env_prefix { } } -mod field_parse_env { +mod setting_parse_env { use super::*; - #[test] - fn accepts_func_ref() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(env = "KEY", parse_env = func_ref)] - a: String, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.parse_env.is_some()); - } + mod named_struct { + use super::*; - #[test] - fn accepts_string() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(env = "KEY", parse_env = "func_ref")] - a: String, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.parse_env.is_some()); - } + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env = "KEY", parse_env = func_ref)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.parse_env.is_some()); + } + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env = "KEY", parse_env = "func_ref")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.parse_env.is_some()); + } - #[test] - #[should_panic(expected = "UnexpectedType")] - fn errors_invalid_type() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(env = "KEY", parse_env = 123)] - a: String, - } - }); + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env = "KEY", parse_env = 123)] + a: String, + } + }); + } + + #[test] + #[should_panic(expected = "Cannot use `parse_env` without `env`.")] + fn errors_without_env() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(parse_env = func_ref)] + a: String, + } + }); + } } - #[test] - #[should_panic(expected = "Cannot use `parse_env` without `env`.")] - fn errors_without_env() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(parse_env = func_ref)] - a: String, - } - }); + mod unnamed_struct { + use super::*; + + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(env = "KEY", parse_env = func_ref)] + String, + ); + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.parse_env.is_some()); + } + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(env = "KEY", parse_env = "func_ref")] + String, + ); + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.parse_env.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(env = "KEY", parse_env = 123)] + String, + ); + }); + } + + #[test] + #[should_panic(expected = "Cannot use `parse_env` without `env`.")] + fn errors_without_env() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(parse_env = func_ref)] + String, + ); + }); + } } } diff --git a/crates/core/tests/field_extend_test.rs b/crates/core/tests/setting_extend_test.rs similarity index 99% rename from crates/core/tests/field_extend_test.rs rename to crates/core/tests/setting_extend_test.rs index 6eba3635..015dd002 100644 --- a/crates/core/tests/field_extend_test.rs +++ b/crates/core/tests/setting_extend_test.rs @@ -5,7 +5,7 @@ use starbase_sandbox::assert_snapshot; use syn::parse_quote; use utils::pretty; -mod field_extend { +mod setting_extend { use super::*; #[test] diff --git a/crates/core/tests/field_merge_test.rs b/crates/core/tests/setting_merge_test.rs similarity index 74% rename from crates/core/tests/field_merge_test.rs rename to crates/core/tests/setting_merge_test.rs index 1a0aa829..a2f08aa1 100644 --- a/crates/core/tests/field_merge_test.rs +++ b/crates/core/tests/setting_merge_test.rs @@ -5,57 +5,57 @@ use starbase_sandbox::assert_snapshot; use syn::parse_quote; use utils::pretty; -mod field_merge { +mod setting_merge { use super::*; - #[test] - fn accepts_func_ref() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(merge = func_ref)] - a: String, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.merge.is_some()); - } + mod named_struct { + use super::*; - #[test] - fn accepts_string() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(merge = "func_ref")] - a: String, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.merge.is_some()); - } + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(merge = func_ref)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; - #[test] - #[should_panic(expected = "UnexpectedType")] - fn errors_invalid_type() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(merge = 123)] - a: String, - } - }); - } + assert!(field.args.merge.is_some()); + } - mod named_struct { - use super::*; + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(merge = "func_ref")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.merge.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(merge = 123)] + a: String, + } + }); + } #[test] fn supports_standard() { let container = Container::from(parse_quote! { #[derive(Config)] - struct Example{ + struct Example { a: bool, b: usize, c: String, @@ -109,7 +109,7 @@ mod field_merge { fn supports_func() { let container = Container::from(parse_quote! { #[derive(Config)] - struct Example{ + struct Example { a: bool, b: usize, #[setting(merge = discard)] @@ -159,11 +159,51 @@ mod field_merge { mod unnamed_struct { use super::*; + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(merge = func_ref)] + String, + ); + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.merge.is_some()); + } + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(merge = "func_ref")] + String, + ); + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.merge.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(merge = 123)] + String, + ); + }); + } + #[test] fn supports_standard() { let container = Container::from(parse_quote! { #[derive(Config)] - struct Exampl( + struct Example( bool, usize, String, @@ -217,7 +257,7 @@ mod field_merge { fn supports_func() { let container = Container::from(parse_quote! { #[derive(Config)] - struct Example ( + struct Example( bool, usize, #[setting(merge = discard)] diff --git a/crates/core/tests/setting_nested_test.rs b/crates/core/tests/setting_nested_test.rs new file mode 100644 index 00000000..9f2f0140 --- /dev/null +++ b/crates/core/tests/setting_nested_test.rs @@ -0,0 +1,297 @@ +use quote::format_ident; +use schematic_core::container::Container; +use syn::parse_quote; + +mod setting_nested { + use super::*; + + mod named_struct { + use super::*; + + #[test] + fn word() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: NestedConfig, + } + }); + let fields = container.inner.get_fields(); + + assert!(fields[0].value.nested); + assert_eq!( + fields[0].value.nested_ident.as_ref().unwrap(), + &format_ident!("NestedConfig") + ); + } + + #[test] + fn bool_true() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested = true)] + a: NestedConfig, + } + }); + let fields = container.inner.get_fields(); + + assert!(fields[0].value.nested); + assert_eq!( + fields[0].value.nested_ident.as_ref().unwrap(), + &format_ident!("NestedConfig") + ); + } + + #[test] + fn bool_false() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested = false)] + a: NestedConfig, + } + }); + let fields = container.inner.get_fields(); + + assert!(!fields[0].value.nested); + assert!(fields[0].value.nested_ident.is_none()); + } + + #[test] + fn explicit_ident() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested = NestedConfig)] + a: NestedConfig, + } + }); + let fields = container.inner.get_fields(); + + assert!(fields[0].value.nested); + assert_eq!( + fields[0].value.nested_ident.as_ref().unwrap(), + &format_ident!("NestedConfig") + ); + } + + #[test] + fn detect_in_vec() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: Vec, + } + }); + let fields = container.inner.get_fields(); + + assert!(fields[0].value.nested); + assert_eq!( + fields[0].value.nested_ident.as_ref().unwrap(), + &format_ident!("NestedConfig") + ); + } + + #[test] + #[should_panic(expected = "UnexpectedType(\"paren\")")] + fn panics_invalid_expr() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested = (1 + 1))] + a: NestedConfig, + } + }); + } + + // #[test] + // #[should_panic( + // expected = "Unable to extract the nested configuration identifier from `Vec>>`. Try explicitly passing the identifier with `nested = ConfigName`." + // )] + // fn panics_cant_find_ident() { + // Container::from(parse_quote! { + // #[derive(Config)] + // struct Example { + // #[setting(nested)] + // a: Vec>>, + // } + // }); + // } + + #[test] + #[should_panic( + expected = "Too many segments for `sub::NestedConfig`, only a single identifier is allowed." + )] + fn panics_too_many_segments() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested = sub::NestedConfig)] + a: NestedConfig, + } + }); + } + + #[test] + #[should_panic( + expected = "Nested configuration identifier `OtherConfig` does not exist within `Vec`." + )] + fn panics_cant_find_custom_ident() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested = OtherConfig)] + a: Vec, + } + }); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn word() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested)] + NestedConfig, + ); + }); + let fields = container.inner.get_fields(); + + assert!(fields[0].value.nested); + assert_eq!( + fields[0].value.nested_ident.as_ref().unwrap(), + &format_ident!("NestedConfig") + ); + } + + #[test] + fn bool_true() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested = true)] + NestedConfig, + ); + }); + let fields = container.inner.get_fields(); + + assert!(fields[0].value.nested); + assert_eq!( + fields[0].value.nested_ident.as_ref().unwrap(), + &format_ident!("NestedConfig") + ); + } + + #[test] + fn bool_false() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested = false)] + NestedConfig, + ); + }); + let fields = container.inner.get_fields(); + + assert!(!fields[0].value.nested); + assert!(fields[0].value.nested_ident.is_none()); + } + + #[test] + fn explicit_ident() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested = NestedConfig)] + NestedConfig, + ); + }); + let fields = container.inner.get_fields(); + + assert!(fields[0].value.nested); + assert_eq!( + fields[0].value.nested_ident.as_ref().unwrap(), + &format_ident!("NestedConfig") + ); + } + + #[test] + fn detect_in_vec() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested)] + Vec, + ); + }); + let fields = container.inner.get_fields(); + + assert!(fields[0].value.nested); + assert_eq!( + fields[0].value.nested_ident.as_ref().unwrap(), + &format_ident!("NestedConfig") + ); + } + + #[test] + #[should_panic(expected = "UnexpectedType(\"paren\")")] + fn panics_invalid_expr() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested = (1 + 1))] + NestedConfig, + ); + }); + } + + // #[test] + // #[should_panic( + // expected = "Unable to extract the nested configuration identifier from `Vec>>`. Try explicitly passing the identifier with `nested = ConfigName`." + // )] + // fn panics_cant_find_ident() { + // Container::from(parse_quote! { + // #[derive(Config)] + // struct Example( + // #[setting(nested)] + // Vec>>, + // ); + // }); + // } + + #[test] + #[should_panic( + expected = "Too many segments for `sub::NestedConfig`, only a single identifier is allowed." + )] + fn panics_too_many_segments() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested = sub::NestedConfig)] + NestedConfig, + ); + }); + } + + #[test] + #[should_panic( + expected = "Nested configuration identifier `OtherConfig` does not exist within `Vec`." + )] + fn panics_cant_find_custom_ident() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested = OtherConfig)] + Vec, + ); + }); + } + } +} diff --git a/crates/core/tests/setting_partial_test.rs b/crates/core/tests/setting_partial_test.rs new file mode 100644 index 00000000..5c1d5979 --- /dev/null +++ b/crates/core/tests/setting_partial_test.rs @@ -0,0 +1,24 @@ +use schematic_core::container::Container; +use syn::parse_quote; + +mod setting_partial { + use super::*; + + mod named_struct { + use super::*; + + #[test] + fn can_set() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(partial(other(attribute), and(another)))] + a: bool, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.partial.is_some()); + } + } +} diff --git a/crates/core/tests/field_serde_test.rs b/crates/core/tests/setting_serde_test.rs similarity index 99% rename from crates/core/tests/field_serde_test.rs rename to crates/core/tests/setting_serde_test.rs index cd64429b..cba8d326 100644 --- a/crates/core/tests/field_serde_test.rs +++ b/crates/core/tests/setting_serde_test.rs @@ -2,7 +2,7 @@ use schematic_core::args::SerdeRenameArg; use schematic_core::container::Container; use syn::parse_quote; -mod field_serde { +mod setting_serde { use super::*; mod native { diff --git a/crates/core/tests/field_test.rs b/crates/core/tests/setting_test.rs similarity index 99% rename from crates/core/tests/field_test.rs rename to crates/core/tests/setting_test.rs index 460a041f..6accb4b8 100644 --- a/crates/core/tests/field_test.rs +++ b/crates/core/tests/setting_test.rs @@ -14,7 +14,7 @@ fn get_field_nested_ident(field: &Field) -> &Ident { field.value.nested_ident.as_ref().unwrap() } -mod field { +mod setting_field { use super::*; #[test] diff --git a/crates/core/tests/setting_transform_test.rs b/crates/core/tests/setting_transform_test.rs new file mode 100644 index 00000000..537436f5 --- /dev/null +++ b/crates/core/tests/setting_transform_test.rs @@ -0,0 +1,94 @@ +use schematic_core::container::Container; +use syn::parse_quote; + +mod setting_transform { + use super::*; + + mod named_struct { + use super::*; + + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(transform = func_ref)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.transform.is_some()); + } + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(transform = "func_ref")] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.transform.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(transform = 123)] + a: String, + } + }); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(transform = func_ref)] + String, + ); + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.transform.is_some()); + } + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(transform = "func_ref")] + String, + ); + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.transform.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(transform = 123)] + String, + ); + }); + } + } +} diff --git a/crates/core/tests/setting_validate_test.rs b/crates/core/tests/setting_validate_test.rs new file mode 100644 index 00000000..d911f6b6 --- /dev/null +++ b/crates/core/tests/setting_validate_test.rs @@ -0,0 +1,94 @@ +use schematic_core::container::Container; +use syn::parse_quote; + +mod setting_validate { + use super::*; + + mod named_struct { + use super::*; + + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(validate = func_ref)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.validate.is_some()); + } + + #[test] + fn accepts_curried_func() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(validate = func_call())] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.validate.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(validate = 123)] + a: String, + } + }); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(validate = func_ref)] + String, + ); + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.validate.is_some()); + } + + #[test] + fn accepts_curried_func() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(validate = func_call())] + String, + ); + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.validate.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(validate = 123)] + String, + ); + }); + } + } +} diff --git a/crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap b/crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap deleted file mode 100644 index 35d03902..00000000 --- a/crates/core/tests/snapshots/field_partial_test__field_partial__can_set.snap +++ /dev/null @@ -1,54 +0,0 @@ ---- -source: crates/core/tests/field_partial_test.rs -expression: field.args.partial.as_ref().unwrap() ---- -PartialArg { - meta: [ - Meta( - Meta::List { - path: Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident( - other, - ), - arguments: PathArguments::None, - }, - ], - }, - delimiter: MacroDelimiter::Paren( - Paren, - ), - tokens: TokenStream [ - Ident { - sym: attribute, - }, - ], - }, - ), - Meta( - Meta::List { - path: Path { - leading_colon: None, - segments: [ - PathSegment { - ident: Ident( - and, - ), - arguments: PathArguments::None, - }, - ], - }, - delimiter: MacroDelimiter::Paren( - Paren, - ), - tokens: TokenStream [ - Ident { - sym: another, - }, - ], - }, - ), - ], -} diff --git a/crates/core/tests/snapshots/field_default_test__field_default__handles_collections.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_collections.snap similarity index 89% rename from crates/core/tests/snapshots/field_default_test__field_default__handles_collections.snap rename to crates/core/tests/snapshots/setting_default_test__setting_default__handles_collections.snap index 02660590..1849aa80 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__handles_collections.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_collections.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_default_test.rs +source: crates/core/tests/setting_default_test.rs expression: pretty(container.impl_partial_default_values()) --- fn default_values( diff --git a/crates/core/tests/snapshots/field_default_test__field_default__handles_layers.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers.snap similarity index 90% rename from crates/core/tests/snapshots/field_default_test__field_default__handles_layers.snap rename to crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers.snap index 4c79aac5..f72afa24 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__handles_layers.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_default_test.rs +source: crates/core/tests/setting_default_test.rs expression: pretty(container.impl_partial_default_values()) --- fn default_values( diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__renders_nothing_if_all_option_wrapped.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__renders_nothing_if_all_option_wrapped.snap similarity index 57% rename from crates/core/tests/snapshots/field_default_test__field_default__named_struct__renders_nothing_if_all_option_wrapped.snap rename to crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__renders_nothing_if_all_option_wrapped.snap index c250669a..7ba81778 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__renders_nothing_if_all_option_wrapped.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__renders_nothing_if_all_option_wrapped.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_default_test.rs +source: crates/core/tests/setting_default_test.rs expression: pretty(container.impl_partial_default_values()) --- diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_nested.snap similarity index 87% rename from crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_nested.snap rename to crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_nested.snap index e6f5c435..232d8900 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_nested.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_default_test.rs +source: crates/core/tests/setting_default_test.rs expression: pretty(container.impl_partial_default_values()) --- fn default_values( diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types-2.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_types-2.snap similarity index 98% rename from crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types-2.snap rename to crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_types-2.snap index 3c6dceef..e5db7318 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types-2.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_types-2.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_default_test.rs +source: crates/core/tests/setting_default_test.rs expression: defaults --- { diff --git a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_types.snap similarity index 91% rename from crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types.snap rename to crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_types.snap index 6b9654d6..c60ce1c7 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__named_struct__supports_types.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_types.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_default_test.rs +source: crates/core/tests/setting_default_test.rs expression: pretty(container.impl_partial_default_values()) --- fn default_values( diff --git a/crates/core/tests/snapshots/field_default_test__field_default__supports_handler_func.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__supports_handler_func.snap similarity index 86% rename from crates/core/tests/snapshots/field_default_test__field_default__supports_handler_func.snap rename to crates/core/tests/snapshots/setting_default_test__setting_default__supports_handler_func.snap index bfbb22a3..987714e9 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__supports_handler_func.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__supports_handler_func.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_default_test.rs +source: crates/core/tests/setting_default_test.rs expression: pretty(container.impl_partial_default_values()) --- fn default_values( diff --git a/crates/core/tests/snapshots/variant_default_test__variant_default__unnamed_enum.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__unit_enum__supports.snap similarity index 83% rename from crates/core/tests/snapshots/variant_default_test__variant_default__unnamed_enum.snap rename to crates/core/tests/snapshots/setting_default_test__setting_default__unit_enum__supports.snap index da1c1b61..e5949dc3 100644 --- a/crates/core/tests/snapshots/variant_default_test__variant_default__unnamed_enum.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__unit_enum__supports.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/variant_default_test.rs +source: crates/core/tests/setting_default_test.rs expression: pretty(container.impl_partial_default_values()) --- fn default_values( diff --git a/crates/core/tests/snapshots/variant_default_test__variant_default__unit_enum.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_enum__supports.snap similarity index 80% rename from crates/core/tests/snapshots/variant_default_test__variant_default__unit_enum.snap rename to crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_enum__supports.snap index 012ecdc2..870132bb 100644 --- a/crates/core/tests/snapshots/variant_default_test__variant_default__unit_enum.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_enum__supports.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/variant_default_test.rs +source: crates/core/tests/setting_default_test.rs expression: pretty(container.impl_partial_default_values()) --- fn default_values( diff --git a/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap similarity index 57% rename from crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap rename to crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap index c250669a..7ba81778 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_default_test.rs +source: crates/core/tests/setting_default_test.rs expression: pretty(container.impl_partial_default_values()) --- diff --git a/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types-2.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__supports_types-2.snap similarity index 98% rename from crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types-2.snap rename to crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__supports_types-2.snap index 6eb42f87..6c02fe81 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types-2.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__supports_types-2.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_default_test.rs +source: crates/core/tests/setting_default_test.rs expression: defaults --- { diff --git a/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__supports_types.snap similarity index 92% rename from crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types.snap rename to crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__supports_types.snap index 6b8bb741..4f9f2be2 100644 --- a/crates/core/tests/snapshots/field_default_test__field_default__unnamed_struct__supports_types.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__supports_types.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_default_test.rs +source: crates/core/tests/setting_default_test.rs expression: pretty(container.impl_partial_default_values()) --- fn default_values( diff --git a/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_different_types.snap b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_different_types.snap similarity index 91% rename from crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_different_types.snap rename to crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_different_types.snap index 992077f8..4c2e5217 100644 --- a/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_different_types.snap +++ b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_different_types.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_env_test.rs +source: crates/core/tests/setting_env_test.rs expression: pretty(container.impl_partial_env_values()) --- fn env_values_with_prefix( diff --git a/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_nested.snap similarity index 91% rename from crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_nested.snap rename to crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_nested.snap index a8589658..7fd42d6b 100644 --- a/crates/core/tests/snapshots/field_env_test__field_env__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_nested.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_env_test.rs +source: crates/core/tests/setting_env_test.rs expression: pretty(container.impl_partial_env_values()) --- fn env_values_with_prefix( diff --git a/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_different_types.snap b/crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_different_types.snap similarity index 91% rename from crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_different_types.snap rename to crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_different_types.snap index 5cd475e8..a34635f9 100644 --- a/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_different_types.snap +++ b/crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_different_types.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_env_test.rs +source: crates/core/tests/setting_env_test.rs expression: pretty(container.impl_partial_env_values()) --- fn env_values_with_prefix( diff --git a/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_nested.snap similarity index 91% rename from crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_nested.snap rename to crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_nested.snap index 7d76d6a6..7c96bb2c 100644 --- a/crates/core/tests/snapshots/field_env_test__field_env__unnamed_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_nested.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_env_test.rs +source: crates/core/tests/setting_env_test.rs expression: pretty(container.impl_partial_env_values()) --- fn env_values_with_prefix( diff --git a/crates/core/tests/snapshots/field_env_test__field_env_prefix__named_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_env_test__setting_env_prefix__named_struct__supports_nested.snap similarity index 93% rename from crates/core/tests/snapshots/field_env_test__field_env_prefix__named_struct__supports_nested.snap rename to crates/core/tests/snapshots/setting_env_test__setting_env_prefix__named_struct__supports_nested.snap index b04fc0f5..46f30cc9 100644 --- a/crates/core/tests/snapshots/field_env_test__field_env_prefix__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_env_test__setting_env_prefix__named_struct__supports_nested.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_env_test.rs +source: crates/core/tests/setting_env_test.rs expression: pretty(container.impl_partial_env_values()) --- fn env_values_with_prefix( diff --git a/crates/core/tests/snapshots/field_env_test__field_env_prefix__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_env_test__setting_env_prefix__unnamed_struct__supports_nested.snap similarity index 93% rename from crates/core/tests/snapshots/field_env_test__field_env_prefix__unnamed_struct__supports_nested.snap rename to crates/core/tests/snapshots/setting_env_test__setting_env_prefix__unnamed_struct__supports_nested.snap index b31f1285..45dabc85 100644 --- a/crates/core/tests/snapshots/field_env_test__field_env_prefix__unnamed_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_env_test__setting_env_prefix__unnamed_struct__supports_nested.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_env_test.rs +source: crates/core/tests/setting_env_test.rs expression: pretty(container.impl_partial_env_values()) --- fn env_values_with_prefix( diff --git a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_string.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_string.snap similarity index 81% rename from crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_string.snap rename to crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_string.snap index ff72180f..bd38cb48 100644 --- a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_string.snap +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_string.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_extend_test.rs +source: crates/core/tests/setting_extend_test.rs expression: pretty(container.impl_partial_extends_from()) --- fn extends_from(&self) -> Option { diff --git a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_type.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_type.snap similarity index 74% rename from crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_type.snap rename to crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_type.snap index 97e638dc..20f23ba9 100644 --- a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_type.snap +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_type.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_extend_test.rs +source: crates/core/tests/setting_extend_test.rs expression: pretty(container.impl_partial_extends_from()) --- fn extends_from(&self) -> Option { diff --git a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_vec_string.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_vec_string.snap similarity index 80% rename from crates/core/tests/snapshots/field_extend_test__field_extend__can_set_vec_string.snap rename to crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_vec_string.snap index 280d50ff..5374d8c9 100644 --- a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_vec_string.snap +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_vec_string.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_extend_test.rs +source: crates/core/tests/setting_extend_test.rs expression: pretty(container.impl_partial_extends_from()) --- fn extends_from(&self) -> Option { diff --git a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_string.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_string.snap similarity index 81% rename from crates/core/tests/snapshots/field_extend_test__field_extend__can_set_string.snap rename to crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_string.snap index ff72180f..bd38cb48 100644 --- a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_string.snap +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_string.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_extend_test.rs +source: crates/core/tests/setting_extend_test.rs expression: pretty(container.impl_partial_extends_from()) --- fn extends_from(&self) -> Option { diff --git a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_type.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_type.snap similarity index 74% rename from crates/core/tests/snapshots/field_extend_test__field_extend__can_set_type.snap rename to crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_type.snap index 97e638dc..20f23ba9 100644 --- a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_type.snap +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_type.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_extend_test.rs +source: crates/core/tests/setting_extend_test.rs expression: pretty(container.impl_partial_extends_from()) --- fn extends_from(&self) -> Option { diff --git a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_vec_string.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_vec_string.snap similarity index 80% rename from crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_vec_string.snap rename to crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_vec_string.snap index 280d50ff..5374d8c9 100644 --- a/crates/core/tests/snapshots/field_extend_test__field_extend__can_set_opt_vec_string.snap +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_vec_string.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_extend_test.rs +source: crates/core/tests/setting_extend_test.rs expression: pretty(container.impl_partial_extends_from()) --- fn extends_from(&self) -> Option { diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_func.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_func.snap similarity index 92% rename from crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_func.snap rename to crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_func.snap index eb07119f..53c23a3b 100644 --- a/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_func.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_func.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_merge_test.rs +source: crates/core/tests/setting_merge_test.rs expression: pretty(container.impl_partial_merge()) --- fn merge( diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap similarity index 89% rename from crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested.snap rename to crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap index 51cfac33..c6b1424e 100644 --- a/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_merge_test.rs +source: crates/core/tests/setting_merge_test.rs expression: pretty(container.impl_partial_merge()) --- fn merge( diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_collections.snap similarity index 90% rename from crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested_collections.snap rename to crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_collections.snap index e96a6e01..a9c39c66 100644 --- a/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_nested_collections.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_collections.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_merge_test.rs +source: crates/core/tests/setting_merge_test.rs expression: pretty(container.impl_partial_merge()) --- fn merge( diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_standard.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_standard.snap similarity index 91% rename from crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_standard.snap rename to crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_standard.snap index aa00a0fd..d6092d21 100644 --- a/crates/core/tests/snapshots/field_merge_test__field_merge__named_struct__supports_standard.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_standard.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_merge_test.rs +source: crates/core/tests/setting_merge_test.rs expression: pretty(container.impl_partial_merge()) --- fn merge( diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_func.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_func.snap similarity index 92% rename from crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_func.snap rename to crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_func.snap index 8a49a434..543b0d4b 100644 --- a/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_func.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_func.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_merge_test.rs +source: crates/core/tests/setting_merge_test.rs expression: pretty(container.impl_partial_merge()) --- fn merge( diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_nested.snap similarity index 89% rename from crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested.snap rename to crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_nested.snap index 177a038b..c1deb807 100644 --- a/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_nested.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_merge_test.rs +source: crates/core/tests/setting_merge_test.rs expression: pretty(container.impl_partial_merge()) --- fn merge( diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_nested_collections.snap similarity index 90% rename from crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested_collections.snap rename to crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_nested_collections.snap index 14bc2749..188e66cf 100644 --- a/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_nested_collections.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_nested_collections.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_merge_test.rs +source: crates/core/tests/setting_merge_test.rs expression: pretty(container.impl_partial_merge()) --- fn merge( diff --git a/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_standard.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_standard.snap similarity index 91% rename from crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_standard.snap rename to crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_standard.snap index e697717d..ddbdad98 100644 --- a/crates/core/tests/snapshots/field_merge_test__field_merge__unnamed_struct__supports_standard.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_standard.snap @@ -1,5 +1,5 @@ --- -source: crates/core/tests/field_merge_test.rs +source: crates/core/tests/setting_merge_test.rs expression: pretty(container.impl_partial_merge()) --- fn merge( diff --git a/crates/core/tests/variant_default_test.rs b/crates/core/tests/variant_default_test.rs deleted file mode 100644 index 663ab69c..00000000 --- a/crates/core/tests/variant_default_test.rs +++ /dev/null @@ -1,80 +0,0 @@ -mod utils; - -use schematic_core::container::Container; -use starbase_sandbox::assert_snapshot; -use syn::parse_quote; -use utils::pretty; - -mod variant_default { - use super::*; - - #[test] - #[should_panic(expected = "Only 1 variant may be marked as default.")] - fn errors_if_multiple_defaults() { - Container::from(parse_quote! { - #[derive(Config)] - enum Example { - #[setting(default)] - Foo, - #[setting(default)] - Bar, - #[setting(default)] - Baz, - } - }) - .impl_partial_default_values(); - } - - #[test] - #[should_panic(expected = "Enums with named fields are not supported!")] - fn errors_for_named_enum() { - Container::from(parse_quote! { - #[derive(Config)] - enum Example { - Foo {}, - #[setting(default)] - Bar {}, - Baz {}, - } - }) - .impl_partial_default_values(); - } - - #[test] - fn unit_enum() { - let container = Container::from(parse_quote! { - #[derive(Config)] - enum Example { - Foo, - #[setting(default)] - Bar, - Baz, - } - }); - - assert_snapshot!(pretty(container.impl_partial_default_values())); - - let variants = container.inner.get_variants(); - - assert!(variants[1].args.default); - } - - #[test] - fn unnamed_enum() { - let container = Container::from(parse_quote! { - #[derive(Config)] - enum Example { - Foo(usize), - #[setting(default)] - Bar(String, u8), - Baz(bool, String, isize), - } - }); - - assert_snapshot!(pretty(container.impl_partial_default_values())); - - let variants = container.inner.get_variants(); - - assert!(variants[1].args.default); - } -} From 02011e1b9503a8d06b7141d4d1795df909dfa04b Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 3 Aug 2025 13:25:23 -0700 Subject: [PATCH 21/33] Start on enum merge. --- crates/core/src/args.rs | 95 +++++++++--- crates/core/src/container.rs | 53 ++++--- crates/core/src/field.rs | 78 +--------- crates/core/src/field_value.rs | 9 +- crates/core/src/variant.rs | 140 +++++++++++++++++- crates/core/tests/setting_merge_test.rs | 89 +++++++++++ ...ng_merge__unnamed_enum__supports_func.snap | 67 +++++++++ ...erge__unnamed_enum__supports_standard.snap | 12 ++ 8 files changed, 420 insertions(+), 123 deletions(-) create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_func.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_standard.snap diff --git a/crates/core/src/args.rs b/crates/core/src/args.rs index c893fd64..714e31d9 100644 --- a/crates/core/src/args.rs +++ b/crates/core/src/args.rs @@ -1,7 +1,10 @@ +use crate::utils::to_type_string; use darling::ast::NestedMeta; use darling::{FromAttributes, FromDeriveInput, FromMeta}; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; +use std::ops::Deref; +use syn::{Expr, Ident}; #[derive(Clone, Copy, Debug)] pub enum SerdeIoDirection { @@ -119,6 +122,7 @@ pub struct SerdeFieldArgs { pub untagged: bool, } +// #[setting(partial)] #[derive(Debug, Default)] pub struct PartialArg { meta: Vec, @@ -142,24 +146,73 @@ impl FromMeta for PartialArg { } } -// // #[config()], #[schematic()] -// #[derive(FromDeriveInput, Default)] -// #[darling( -// default, -// attributes(config, schematic), -// supports(struct_named, enum_any) -// )] -// pub struct MacroArgs { -// // config -// pub allow_unknown_fields: bool, -// pub context: Option, -// pub partial: PartialAttr, -// #[cfg(feature = "env")] -// pub env_prefix: Option, - -// // serde -// pub rename: Option, -// pub rename_all: Option, -// pub rename_all_fields: Option, -// pub serde: SerdeMeta, -// } +// #[setting(nested)] +#[derive(Debug)] +pub enum NestedArg { + Detect(bool), + Ident(Ident), +} + +impl NestedArg { + pub fn is_nested(&self) -> bool { + match self { + NestedArg::Detect(inner) => *inner, + NestedArg::Ident(_) => true, + } + } +} + +impl FromMeta for NestedArg { + // #[setting(nested)] + fn from_word() -> darling::Result { + Ok(Self::Detect(true)) + } + + // #[setting(nested = true)] + fn from_bool(value: bool) -> darling::Result { + Ok(Self::Detect(value)) + } + + // #[setting(nested = NestedConfig)] + fn from_expr(expr: &Expr) -> darling::Result { + match expr { + Expr::Lit(lit) => Self::from_value(&lit.lit), + Expr::Path(path) => { + if path.path.segments.len() > 1 { + Err(darling::Error::custom(format!( + "Too many segments for `{}`, only a single identifier is allowed.", + to_type_string(path.to_token_stream()) + ))) + } else { + Ok(Self::Ident( + path.path.segments.last().unwrap().ident.to_owned(), + )) + } + } + _ => Err(darling::Error::unexpected_expr_type(expr)), + } + .map_err(|e| e.with_span(expr)) + } +} + +// #[setting(validate)] +#[derive(Debug)] +pub struct ValidateArg(Expr); + +impl FromMeta for ValidateArg { + fn from_expr(expr: &Expr) -> darling::Result { + match expr { + Expr::Call(_) | Expr::Path(_) => Ok(Self(expr.to_owned())), + _ => Err(darling::Error::unexpected_expr_type(expr)), + } + .map_err(|e| e.with_span(expr)) + } +} + +impl Deref for ValidateArg { + type Target = Expr; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 67ae3bb6..6320f6cb 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -441,10 +441,10 @@ impl Container { } pub fn impl_partial_merge(&self) -> TokenStream { - let mut statements = vec![]; - - let inner = match &self.inner { + match &self.inner { ContainerInner::NamedStruct { fields } | ContainerInner::UnnamedStruct { fields } => { + let mut statements = vec![]; + for field in fields { let res = field.impl_partial_merge(); @@ -457,11 +457,26 @@ impl Container { return quote! {}; } + let internal = ImplResult::impl_use_internal(true); + quote! { - #(#statements)* + fn merge( + &mut self, + context: &Self::Context, + mut next: Self, + ) -> std::result::Result<(), schematic::ConfigError> { + #internal + + MergeManager::new(context) + #(#statements)*; + + Ok(()) + } } } ContainerInner::Enum { variants } | ContainerInner::UnitEnum { variants } => { + let mut statements = vec![]; + for variant in variants { let res = variant.impl_partial_merge(); @@ -470,7 +485,7 @@ impl Container { } } - if statements.is_empty() { + let inner = if statements.is_empty() { quote! { *self = next; } @@ -483,24 +498,18 @@ impl Container { } }; } - } - } - }; - - let internal = ImplResult::impl_use_internal(true); + }; - quote! { - fn merge( - &mut self, - context: &Self::Context, - mut next: Self, - ) -> std::result::Result<(), schematic::ConfigError> { - #internal - - MergeManager::new(context) - #inner; - - Ok(()) + quote! { + fn merge( + &mut self, + context: &Self::Context, + mut next: Self, + ) -> std::result::Result<(), schematic::ConfigError> { + #inner + Ok(()) + } + } } } } diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 7851333b..efb6f020 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -1,78 +1,15 @@ use crate::args::{ - PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeIoDirection, SerdeRenameArg, + NestedArg, PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeIoDirection, SerdeRenameArg, }; use crate::container::ContainerArgs; use crate::field_value::FieldValue; -use crate::utils::{ImplResult, preserve_str_literal, to_type_string}; -use darling::{FromAttributes, FromMeta}; +use crate::utils::{ImplResult, preserve_str_literal}; +use darling::FromAttributes; use proc_macro2::{Literal, TokenStream}; use quote::{ToTokens, TokenStreamExt, quote}; -use std::ops::Deref; use std::rc::Rc; use syn::{Attribute, Expr, ExprPath, Field as NativeField, FieldMutability, Ident, Visibility}; -// #[setting(nested)] -#[derive(Debug)] -pub enum FieldNestedArg { - Detect(bool), - Ident(Ident), -} - -impl FromMeta for FieldNestedArg { - // #[setting(nested)] - fn from_word() -> darling::Result { - Ok(Self::Detect(true)) - } - - // #[setting(nested = true)] - fn from_bool(value: bool) -> darling::Result { - Ok(Self::Detect(value)) - } - - // #[setting(nested = NestedConfig)] - fn from_expr(expr: &Expr) -> darling::Result { - match expr { - Expr::Lit(lit) => Self::from_value(&lit.lit), - Expr::Path(path) => { - if path.path.segments.len() > 1 { - Err(darling::Error::custom(format!( - "Too many segments for `{}`, only a single identifier is allowed.", - to_type_string(path.to_token_stream()) - ))) - } else { - Ok(Self::Ident( - path.path.segments.last().unwrap().ident.to_owned(), - )) - } - } - _ => Err(darling::Error::unexpected_expr_type(expr)), - } - .map_err(|e| e.with_span(expr)) - } -} - -// #[setting(validate)] -#[derive(Debug)] -pub struct FieldValidateArg(Expr); - -impl FromMeta for FieldValidateArg { - fn from_expr(expr: &Expr) -> darling::Result { - match expr { - Expr::Call(_) | Expr::Path(_) => Ok(Self(expr.to_owned())), - _ => Err(darling::Error::unexpected_expr_type(expr)), - } - .map_err(|e| e.with_span(expr)) - } -} - -impl Deref for FieldValidateArg { - type Target = Expr; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - // #[schema()], #[setting()] #[derive(Debug, FromAttributes, Default)] #[darling(default, attributes(schema, setting))] @@ -87,14 +24,14 @@ pub struct FieldArgs { #[cfg(feature = "extends")] pub extend: bool, pub merge: Option, - pub nested: Option, + pub nested: Option, #[cfg(feature = "env")] pub parse_env: Option, pub partial: Option, pub required: bool, // TODO pub transform: Option, #[cfg(feature = "validate")] - pub validate: Option, + pub validate: Option, // serde #[darling(multiple)] @@ -261,10 +198,7 @@ impl Field { self.args .nested .as_ref() - .is_some_and(|nested| match nested { - FieldNestedArg::Detect(inner) => *inner, - FieldNestedArg::Ident(_) => true, - }) + .is_some_and(|nested| nested.is_nested()) } } diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index 389d7f5b..6596a9cd 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -1,4 +1,5 @@ -use crate::field::{FieldArgs, FieldNestedArg}; +use crate::args::NestedArg; +use crate::field::FieldArgs; use crate::utils::{ImplResult, to_type_string}; use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote}; @@ -28,7 +29,7 @@ pub struct FieldValue { } impl FieldValue { - pub fn new(ty: Type, nested_arg: Option<&FieldNestedArg>) -> Self { + pub fn new(ty: Type, nested_arg: Option<&NestedArg>) -> Self { let mut nested = false; let mut nested_ident = None; let ty_string = to_type_string(ty.to_token_stream()); @@ -36,10 +37,10 @@ impl FieldValue { // Determine nested state if let Some(nested_arg) = nested_arg { match nested_arg { - FieldNestedArg::Detect(state) => { + NestedArg::Detect(state) => { nested = *state; } - FieldNestedArg::Ident(ident) => { + NestedArg::Ident(ident) => { nested = true; nested_ident = Some(ident.to_owned()); diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index e969111a..41ca28e1 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -1,16 +1,19 @@ -use crate::args::{SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; +use crate::args::{NestedArg, SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; use crate::container::ContainerArgs; use crate::utils::ImplResult; use darling::FromAttributes; -use quote::quote; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; use std::rc::Rc; -use syn::{Attribute, Fields, Ident, Variant as NativeVariant}; +use syn::{Attribute, ExprPath, Fields, FieldsUnnamed, Ident, Variant as NativeVariant}; // #[setting()], #[schema()] #[derive(Debug, Default, FromAttributes)] #[darling(default, attributes(setting, schema))] pub struct VariantArgs { pub default: bool, + pub merge: Option, + pub nested: Option, // serde #[darling(multiple)] @@ -60,6 +63,13 @@ impl Variant { self.args.default } + pub fn is_nested(&self) -> bool { + self.args + .nested + .as_ref() + .is_some_and(|nested| nested.is_nested()) + } + pub fn impl_partial_default_value(&self) -> ImplResult { let mut res = ImplResult::default(); let name = &self.ident; @@ -84,6 +94,128 @@ impl Variant { } pub fn impl_partial_merge(&self) -> ImplResult { - ImplResult::default() + let mut res = ImplResult::default(); + + match &self.value { + Fields::Named(_) => { + res.no_value = true; + } + Fields::Unnamed(fields) => { + let name = &self.ident; + + if self.is_nested() { + if self.args.merge.is_some() { + panic!("Nested variants do not support `merge`."); + } + + res.value = + self.map_unnamed_match(&self.ident, fields, |outer_names, inner_names| { + let statements = outer_names + .iter() + .enumerate() + .map(|(index, o)| { + let i = &inner_names[index]; + quote! { #o.merge(context, #i)?; } + }) + .collect::>(); + + quote! { + if let Self::#name(#(#inner_names),*) = next { + #(#statements)* + } else { + *self = next; + } + } + }); + } else if let Some(func) = &self.args.merge { + res.value = + self.map_unnamed_match(&self.ident, fields, |outer_names, inner_names| { + if outer_names.len() == 1 { + quote! { + if let Self::#name(ai) = next { + *self = Self::#name( + #func(ao.to_owned(), ai, context)?.unwrap_or_default(), + ); + } else { + *self = next; + } + } + } else { + let defaults = outer_names + .iter() + .map(|_| { + quote! { Default::default() } + }) + .collect::>(); + + quote! { + if let Self::#name(#(#inner_names),*) = next { + if let Some((#(#outer_names),*)) = #func( + (#(#outer_names.to_owned()),*), + (#(#inner_names),*), + context, + )? { + *self = Self::#name(#(#outer_names),*); + } else { + *self = Self::#name(#(#defaults),*); + } + } else { + *self = next; + } + } + } + }); + } else { + res.no_value = true; + } + } + Fields::Unit => { + res.no_value = true; + } + }; + + res + } + + fn map_unnamed_match(&self, name: &Ident, fields: &FieldsUnnamed, factory: F) -> TokenStream + where + F: FnOnce(&[Ident], &[Ident]) -> TokenStream, + { + let self_name = format_ident!("Self"); + + self.map_unnamed_match_custom(name, &self_name, fields, factory) + } + + fn map_unnamed_match_custom( + &self, + name: &Ident, + self_name: &Ident, + fields: &FieldsUnnamed, + factory: F, + ) -> TokenStream + where + F: FnOnce(&[Ident], &[Ident]) -> TokenStream, + { + let mut count: u8 = 97; // a + let mut outer_names = vec![]; + let mut inner_names = vec![]; + + for _ in &fields.unnamed { + let outer_name = format_ident!("{}o", count as char); + let inner_name = format_ident!("{}i", count as char); + + outer_names.push(outer_name); + inner_names.push(inner_name); + + count += 1; + } + + let inner = factory(&outer_names, &inner_names); + + quote! { + #self_name::#name(#(#outer_names),*) => { + #inner + }, + } } } diff --git a/crates/core/tests/setting_merge_test.rs b/crates/core/tests/setting_merge_test.rs index a2f08aa1..0c49d824 100644 --- a/crates/core/tests/setting_merge_test.rs +++ b/crates/core/tests/setting_merge_test.rs @@ -303,4 +303,93 @@ mod setting_merge { .impl_partial_merge(); } } + + mod named_enum { + // N/A + } + + mod unnamed_enum { + use super::*; + + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(merge = func_ref)] + A(String), + } + }); + let variant = container.inner.get_variants()[0]; + + assert!(variant.args.merge.is_some()); + } + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(merge = "func_ref")] + A(String), + } + }); + let variant = container.inner.get_variants()[0]; + + assert!(variant.args.merge.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(merge = 123)] + A(String), + } + }); + } + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A(bool), + B(usize), + C(String, i16), + D(Option, Vec), + E(Option>), + } + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + + #[test] + fn supports_func() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(merge = a)] + A(bool), + #[setting(merge = b)] + B(usize), + #[setting(merge = c)] + C(String, i16), + #[setting(merge = d)] + D(Option, Vec), + #[setting(merge = e)] + E(Option>), + } + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + } + + mod unit_enum { + // N/A + } } diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_func.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_func.snap new file mode 100644 index 00000000..442d7f4d --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_func.snap @@ -0,0 +1,67 @@ +--- +source: crates/core/tests/setting_merge_test.rs +expression: pretty(container.impl_partial_merge()) +--- +fn merge( + &mut self, + context: &Self::Context, + mut next: Self, +) -> std::result::Result<(), schematic::ConfigError> { + match self { + Self::A(ao) => { + if let Self::A(ai) = next { + *self = Self::A(a(ao.to_owned(), ai, context)?.unwrap_or_default()); + } else { + *self = next; + } + } + Self::B(ao) => { + if let Self::B(ai) = next { + *self = Self::B(b(ao.to_owned(), ai, context)?.unwrap_or_default()); + } else { + *self = next; + } + } + Self::C(ao, bo) => { + if let Self::C(ai, bi) = next { + if let Some((ao, bo)) = c( + (ao.to_owned(), bo.to_owned()), + (ai, bi), + context, + )? { + *self = Self::C(ao, bo); + } else { + *self = Self::C(Default::default(), Default::default()); + } + } else { + *self = next; + } + } + Self::D(ao, bo) => { + if let Self::D(ai, bi) = next { + if let Some((ao, bo)) = d( + (ao.to_owned(), bo.to_owned()), + (ai, bi), + context, + )? { + *self = Self::D(ao, bo); + } else { + *self = Self::D(Default::default(), Default::default()); + } + } else { + *self = next; + } + } + Self::E(ao) => { + if let Self::E(ai) = next { + *self = Self::E(e(ao.to_owned(), ai, context)?.unwrap_or_default()); + } else { + *self = next; + } + } + _ => { + *self = next; + } + }; + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_standard.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_standard.snap new file mode 100644 index 00000000..7c1f9979 --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_standard.snap @@ -0,0 +1,12 @@ +--- +source: crates/core/tests/setting_merge_test.rs +expression: pretty(container.impl_partial_merge()) +--- +fn merge( + &mut self, + context: &Self::Context, + mut next: Self, +) -> std::result::Result<(), schematic::ConfigError> { + *self = next; + Ok(()) +} From 521b4a807e27875c79db6786e03e1adb64a4e48b Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 3 Aug 2025 13:30:49 -0700 Subject: [PATCH 22/33] Use value composition. --- crates/core/src/field_value.rs | 166 +++--------------------------- crates/core/src/lib.rs | 1 + crates/core/src/value.rs | 161 +++++++++++++++++++++++++++++ crates/core/tests/setting_test.rs | 2 +- 4 files changed, 177 insertions(+), 153 deletions(-) create mode 100644 crates/core/src/value.rs diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index 6596a9cd..0afcca82 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -1,104 +1,26 @@ use crate::args::NestedArg; use crate::field::FieldArgs; -use crate::utils::{ImplResult, to_type_string}; +use crate::utils::ImplResult; +use crate::value::{Layer, Value}; use proc_macro2::TokenStream; -use quote::{ToTokens, format_ident, quote}; -use syn::{Expr, GenericArgument, Ident, Lit, PathArguments, PathSegment, Type}; - -#[derive(Debug, PartialEq)] -pub enum Layer { - Arc, - Box, - Option, - Rc, - // Collections - Map(String), - Set(String), - Vec(String), - Unknown(String), -} +use quote::{format_ident, quote}; +use std::ops::Deref; +use syn::{Expr, Lit, Type}; #[derive(Debug)] -pub struct FieldValue { - pub inner_ty: Option, - pub layers: Vec, - pub nested: bool, - pub nested_ident: Option, - pub ty: Type, - pub ty_string: String, -} - -impl FieldValue { - pub fn new(ty: Type, nested_arg: Option<&NestedArg>) -> Self { - let mut nested = false; - let mut nested_ident = None; - let ty_string = to_type_string(ty.to_token_stream()); - - // Determine nested state - if let Some(nested_arg) = nested_arg { - match nested_arg { - NestedArg::Detect(state) => { - nested = *state; - } - NestedArg::Ident(ident) => { - nested = true; - nested_ident = Some(ident.to_owned()); - - if !ty_string.contains(&ident.to_string()) { - panic!( - "Nested configuration identifier `{ident}` does not exist within `{ty_string}`." - ) - } - } - }; - } +pub struct FieldValue(Value); - let mut value = FieldValue { - inner_ty: None, - nested, - nested_ident, - layers: vec![], - ty_string, - ty, - }; - value.extract_type_information(); - value - } - - pub fn extract_type_information(&mut self) { - extract_type_information(&self.ty, &mut self.layers, |ty, segment| { - self.inner_ty = Some(ty.to_owned()); - - if self.nested && self.nested_ident.is_none() { - self.nested_ident = Some(segment.ident.clone()); - } - }); +impl Deref for FieldValue { + type Target = Value; - if self.nested && self.nested_ident.is_none() { - panic!( - "Unable to extract the nested configuration identifier from `{}`. Try explicitly passing the identifier with `nested = ConfigName`.", - self.ty_string - ) - } - } - - pub fn get_inner_type(&self) -> &Type { - self.inner_ty.as_ref().unwrap_or(&self.ty) - } - - pub fn is_collection(&self) -> bool { - self.layers.iter().any(|layer| { - matches!( - layer, - Layer::Map(_) | Layer::Set(_) | Layer::Vec(_) | Layer::Unknown(_) - ) - }) + fn deref(&self) -> &Self::Target { + &self.0 } +} - pub fn is_outer_option_wrapped(&self) -> bool { - self.layers - .first() - .is_some_and(|layer| *layer == Layer::Option) +impl FieldValue { + pub fn new(ty: Type, nested_arg: Option<&NestedArg>) -> Self { + FieldValue(Value::new(ty, nested_arg)) } pub fn impl_partial_default_value(&self, field_args: &FieldArgs) -> ImplResult { @@ -339,63 +261,3 @@ impl FieldValue { } } } - -fn extract_type_information( - ty: &Type, - layers: &mut Vec, - mut on_last: impl FnMut(&Type, &PathSegment), -) { - // We don't need to traverse other types, just paths - let Type::Path(ty_path) = ty else { - return; - }; - - // Extract the last segment of the path, for example `Option`, - // instead of the full path `std::option::Option` - let last_segment = ty_path.path.segments.last().unwrap(); - - match &last_segment.arguments { - // We've reached the final segment - PathArguments::None => { - on_last(ty, last_segment); - } - - // Attempt to drill deeper down - PathArguments::AngleBracketed(args) => { - extract_layer(last_segment, layers); - - if let Some(GenericArgument::Type(inner_ty)) = args.args.last() { - extract_type_information(inner_ty, layers, on_last); - } - } - - // What to do here, anything? - PathArguments::Parenthesized(_) => {} - }; -} - -fn extract_layer(last_segment: &PathSegment, layers: &mut Vec) { - let layer = if last_segment.ident == "Option" { - Layer::Option - } else if last_segment.ident == "Arc" { - Layer::Arc - } else if last_segment.ident == "Box" { - Layer::Box - } else if last_segment.ident == "Rc" { - Layer::Rc - } else { - let ident = last_segment.ident.to_string(); - - if ident.ends_with("Vec") { - Layer::Vec(ident) - } else if ident.ends_with("Set") { - Layer::Set(ident) - } else if ident.ends_with("Map") { - Layer::Map(ident) - } else { - Layer::Unknown(ident) - } - }; - - layers.push(layer); -} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index c153d921..adf0ec93 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -3,6 +3,7 @@ pub mod container; pub mod field; pub mod field_value; pub mod utils; +pub mod value; pub mod variant; // #[cfg(feature = "config")] diff --git a/crates/core/src/value.rs b/crates/core/src/value.rs new file mode 100644 index 00000000..0a244d32 --- /dev/null +++ b/crates/core/src/value.rs @@ -0,0 +1,161 @@ +use crate::args::NestedArg; +use crate::utils::to_type_string; +use quote::ToTokens; +use syn::{GenericArgument, Ident, PathArguments, PathSegment, Type}; + +#[derive(Debug, PartialEq)] +pub enum Layer { + Arc, + Box, + Option, + Rc, + // Collections + Map(String), + Set(String), + Vec(String), + Unknown(String), +} + +#[derive(Debug)] +pub struct Value { + pub inner_ty: Option, + pub layers: Vec, + pub nested: bool, + pub nested_ident: Option, + pub ty: Type, + pub ty_string: String, +} + +impl Value { + pub fn new(ty: Type, nested_arg: Option<&NestedArg>) -> Self { + let mut nested = false; + let mut nested_ident = None; + let ty_string = to_type_string(ty.to_token_stream()); + + // Determine nested state + if let Some(nested_arg) = nested_arg { + match nested_arg { + NestedArg::Detect(state) => { + nested = *state; + } + NestedArg::Ident(ident) => { + nested = true; + nested_ident = Some(ident.to_owned()); + + if !ty_string.contains(&ident.to_string()) { + panic!( + "Nested configuration identifier `{ident}` does not exist within `{ty_string}`." + ) + } + } + }; + } + + let mut value = Value { + inner_ty: None, + nested, + nested_ident, + layers: vec![], + ty_string, + ty, + }; + value.extract_type_information(); + value + } + + pub fn extract_type_information(&mut self) { + extract_type_information(&self.ty, &mut self.layers, |ty, segment| { + self.inner_ty = Some(ty.to_owned()); + + if self.nested && self.nested_ident.is_none() { + self.nested_ident = Some(segment.ident.clone()); + } + }); + + if self.nested && self.nested_ident.is_none() { + panic!( + "Unable to extract the nested configuration identifier from `{}`. Try explicitly passing the identifier with `nested = ConfigName`.", + self.ty_string + ) + } + } + + pub fn get_inner_type(&self) -> &Type { + self.inner_ty.as_ref().unwrap_or(&self.ty) + } + + pub fn is_collection(&self) -> bool { + self.layers.iter().any(|layer| { + matches!( + layer, + Layer::Map(_) | Layer::Set(_) | Layer::Vec(_) | Layer::Unknown(_) + ) + }) + } + + pub fn is_outer_option_wrapped(&self) -> bool { + self.layers + .first() + .is_some_and(|layer| *layer == Layer::Option) + } +} + +fn extract_type_information( + ty: &Type, + layers: &mut Vec, + mut on_last: impl FnMut(&Type, &PathSegment), +) { + // We don't need to traverse other types, just paths + let Type::Path(ty_path) = ty else { + return; + }; + + // Extract the last segment of the path, for example `Option`, + // instead of the full path `std::option::Option` + let last_segment = ty_path.path.segments.last().unwrap(); + + match &last_segment.arguments { + // We've reached the final segment + PathArguments::None => { + on_last(ty, last_segment); + } + + // Attempt to drill deeper down + PathArguments::AngleBracketed(args) => { + extract_layer(last_segment, layers); + + if let Some(GenericArgument::Type(inner_ty)) = args.args.last() { + extract_type_information(inner_ty, layers, on_last); + } + } + + // What to do here, anything? + PathArguments::Parenthesized(_) => {} + }; +} + +fn extract_layer(last_segment: &PathSegment, layers: &mut Vec) { + let layer = if last_segment.ident == "Option" { + Layer::Option + } else if last_segment.ident == "Arc" { + Layer::Arc + } else if last_segment.ident == "Box" { + Layer::Box + } else if last_segment.ident == "Rc" { + Layer::Rc + } else { + let ident = last_segment.ident.to_string(); + + if ident.ends_with("Vec") { + Layer::Vec(ident) + } else if ident.ends_with("Set") { + Layer::Set(ident) + } else if ident.ends_with("Map") { + Layer::Map(ident) + } else { + Layer::Unknown(ident) + } + }; + + layers.push(layer); +} diff --git a/crates/core/tests/setting_test.rs b/crates/core/tests/setting_test.rs index 6accb4b8..7341933b 100644 --- a/crates/core/tests/setting_test.rs +++ b/crates/core/tests/setting_test.rs @@ -1,6 +1,6 @@ use schematic_core::container::Container; use schematic_core::field::Field; -use schematic_core::field_value::Layer; +use schematic_core::value::Layer; use syn::{Ident, parse_quote}; fn get_field<'a>(fields: &'a [&'a Field], key: &str) -> &'a Field { From d2251f0b5983ab8790ea1b01ee4b7511924e43e6 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Mon, 4 Aug 2025 15:35:03 -0700 Subject: [PATCH 23/33] Polish. --- crates/core/src/lib.rs | 1 + crates/core/src/variant.rs | 126 ++++++++++++------ crates/core/src/variant_value.rs | 21 +++ crates/core/tests/setting_merge_test.rs | 64 +++++++++ crates/core/tests/setting_nested_test.rs | 125 +++++++++++++++++ ...ng_merge__unnamed_enum__supports_func.snap | 42 +++--- ..._merge__unnamed_enum__supports_nested.snap | 44 ++++++ ...med_enum__supports_nested_collections.snap | 43 ++++++ 8 files changed, 406 insertions(+), 60 deletions(-) create mode 100644 crates/core/src/variant_value.rs create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested_collections.snap diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index adf0ec93..9c0453f9 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -5,6 +5,7 @@ pub mod field_value; pub mod utils; pub mod value; pub mod variant; +pub mod variant_value; // #[cfg(feature = "config")] // pub mod config; diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index 41ca28e1..f444a75b 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -1,6 +1,7 @@ use crate::args::{NestedArg, SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; use crate::container::ContainerArgs; use crate::utils::ImplResult; +use crate::variant_value::VariantValue; use darling::FromAttributes; use proc_macro2::TokenStream; use quote::{format_ident, quote}; @@ -27,6 +28,8 @@ pub struct VariantArgs { #[derive(Debug)] pub struct Variant { + pub values: Vec, + // args pub args: VariantArgs, pub container_args: Rc, @@ -36,7 +39,7 @@ pub struct Variant { // inherited pub attrs: Vec, pub ident: Ident, - pub value: Fields, + pub fields: Fields, } impl Variant { @@ -48,14 +51,35 @@ impl Variant { let args = VariantArgs::from_attributes(&variant.attrs).unwrap(); let serde_args = SerdeFieldArgs::from_attributes(&variant.attrs).unwrap(); - Variant { - args, + let variant = Self { attrs: variant.attrs, container_args, ident: variant.ident, serde_args, serde_container_args, - value: variant.fields, + values: match &variant.fields { + Fields::Named(fields) => fields + .named + .iter() + .map(|field| VariantValue::new(field.ty.clone(), args.nested.as_ref())) + .collect(), + Fields::Unnamed(fields) => fields + .unnamed + .iter() + .map(|field| VariantValue::new(field.ty.clone(), args.nested.as_ref())) + .collect(), + Fields::Unit => vec![], + }, + fields: variant.fields, + args, + }; + variant.validate_args(); + variant + } + + fn validate_args(&self) { + if self.is_nested() && self.values.len() > 1 { + panic!("Only 1 item is supported when using `nested` in a tuple variant.") } } @@ -74,7 +98,7 @@ impl Variant { let mut res = ImplResult::default(); let name = &self.ident; - res.value = match &self.value { + res.value = match &self.fields { Fields::Named(_) => panic!("Enums with named fields are not supported!"), Fields::Unnamed(fields) => { let fields = fields @@ -96,45 +120,32 @@ impl Variant { pub fn impl_partial_merge(&self) -> ImplResult { let mut res = ImplResult::default(); - match &self.value { + match &self.fields { Fields::Named(_) => { res.no_value = true; } Fields::Unnamed(fields) => { let name = &self.ident; - if self.is_nested() { - if self.args.merge.is_some() { - panic!("Nested variants do not support `merge`."); - } + match &self.args.merge { + Some(func) => { + if self.is_nested() + && self + .values + .first() + .is_none_or(|value| !value.is_collection()) + { + panic!( + "Nested configs do not support `merge` unless wrapped in a collection." + ); + } - res.value = - self.map_unnamed_match(&self.ident, fields, |outer_names, inner_names| { - let statements = outer_names - .iter() - .enumerate() - .map(|(index, o)| { - let i = &inner_names[index]; - quote! { #o.merge(context, #i)?; } - }) - .collect::>(); - - quote! { - if let Self::#name(#(#inner_names),*) = next { - #(#statements)* - } else { - *self = next; - } - } - }); - } else if let Some(func) = &self.args.merge { - res.value = - self.map_unnamed_match(&self.ident, fields, |outer_names, inner_names| { + res.value = self.map_unnamed_match(&self.ident, fields, |outer_names, inner_names| { if outer_names.len() == 1 { quote! { - if let Self::#name(ai) = next { + if let Self::#name(na) = next { *self = Self::#name( - #func(ao.to_owned(), ai, context)?.unwrap_or_default(), + #func(pa.to_owned(), na, context)?.unwrap_or_default(), ); } else { *self = next; @@ -165,9 +176,46 @@ impl Variant { } } }); - } else { - res.no_value = true; - } + } + None => { + if self.is_nested() { + if self + .values + .first() + .is_some_and(|value| value.is_collection()) + { + panic!( + "Collections with nested configs must manually define `merge`." + ); + } + + res.value = self.map_unnamed_match( + &self.ident, + fields, + |outer_names, inner_names| { + let statements = outer_names + .iter() + .enumerate() + .map(|(index, o)| { + let i = &inner_names[index]; + quote! { #o.merge(context, #i)?; } + }) + .collect::>(); + + quote! { + if let Self::#name(#(#inner_names),*) = next { + #(#statements)* + } else { + *self = next; + } + } + }, + ); + } else { + res.no_value = true; + } + } + }; } Fields::Unit => { res.no_value = true; @@ -201,8 +249,8 @@ impl Variant { let mut inner_names = vec![]; for _ in &fields.unnamed { - let outer_name = format_ident!("{}o", count as char); - let inner_name = format_ident!("{}i", count as char); + let outer_name = format_ident!("p{}", count as char); + let inner_name = format_ident!("n{}", count as char); outer_names.push(outer_name); inner_names.push(inner_name); diff --git a/crates/core/src/variant_value.rs b/crates/core/src/variant_value.rs new file mode 100644 index 00000000..4fd69b78 --- /dev/null +++ b/crates/core/src/variant_value.rs @@ -0,0 +1,21 @@ +use crate::args::NestedArg; +use crate::value::Value; +use std::ops::Deref; +use syn::Type; + +#[derive(Debug)] +pub struct VariantValue(Value); + +impl Deref for VariantValue { + type Target = Value; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl VariantValue { + pub fn new(ty: Type, nested_arg: Option<&NestedArg>) -> Self { + VariantValue(Value::new(ty, nested_arg)) + } +} diff --git a/crates/core/tests/setting_merge_test.rs b/crates/core/tests/setting_merge_test.rs index 0c49d824..a3982b72 100644 --- a/crates/core/tests/setting_merge_test.rs +++ b/crates/core/tests/setting_merge_test.rs @@ -387,6 +387,70 @@ mod setting_merge { assert_snapshot!(pretty(container.impl_partial_merge())); } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A(NestedConfig), + #[setting(nested = CustomConfig)] + B(CustomConfig), + #[setting(nested)] + C(Option), + #[setting(nested = CustomConfig)] + D(Arc), + } + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + + #[test] + fn supports_nested_collections() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested, merge = append_vec)] + A(Vec), + #[setting(nested = CustomConfig, merge = merge_hashmap)] + B(HashMap), + #[setting(nested, merge = merge_btreeset)] + C(Option>), + } + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + + #[test] + #[should_panic( + expected = "Nested configs do not support `merge` unless wrapped in a collection." + )] + fn errors_if_nested_has_merge_attr() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested, merge = append_vec)] + A(NestedConfig), + } + }) + .impl_partial_merge(); + } + + #[test] + #[should_panic(expected = "Collections with nested configs must manually define `merge`.")] + fn errors_if_collection_doesnt_have_merge_attr() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A(Vec), + } + }) + .impl_partial_merge(); + } } mod unit_enum { diff --git a/crates/core/tests/setting_nested_test.rs b/crates/core/tests/setting_nested_test.rs index 9f2f0140..7b91b143 100644 --- a/crates/core/tests/setting_nested_test.rs +++ b/crates/core/tests/setting_nested_test.rs @@ -294,4 +294,129 @@ mod setting_nested { }); } } + + mod named_enum { + // TODO + } + + mod unnamed_enum { + use super::*; + + #[test] + fn word() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A(NestedConfig), + } + }); + let variants = container.inner.get_variants(); + + assert!(variants[0].values[0].nested); + assert_eq!( + variants[0].values[0].nested_ident.as_ref().unwrap(), + &format_ident!("NestedConfig") + ); + } + + #[test] + fn bool_true() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested = true)] + A(NestedConfig), + } + }); + let variants = container.inner.get_variants(); + + assert!(variants[0].values[0].nested); + assert_eq!( + variants[0].values[0].nested_ident.as_ref().unwrap(), + &format_ident!("NestedConfig") + ); + } + + #[test] + fn bool_false() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested = false)] + A(NestedConfig), + } + }); + let variants = container.inner.get_variants(); + + assert!(!variants[0].values[0].nested); + assert!(variants[0].values[0].nested_ident.is_none()); + } + + #[test] + fn explicit_ident() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested = NestedConfig)] + A(NestedConfig), + } + }); + let variants = container.inner.get_variants(); + + assert!(variants[0].values[0].nested); + assert_eq!( + variants[0].values[0].nested_ident.as_ref().unwrap(), + &format_ident!("NestedConfig") + ); + } + + #[test] + fn detect_in_vec() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A(Vec), + } + }); + let variants = container.inner.get_variants(); + + assert!(variants[0].values[0].nested); + assert_eq!( + variants[0].values[0].nested_ident.as_ref().unwrap(), + &format_ident!("NestedConfig") + ); + } + + #[test] + #[should_panic(expected = "UnexpectedType(\"paren\")")] + fn panics_invalid_expr() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested = (1 + 1))] + A(NestedConfig), + } + }); + } + + #[test] + #[should_panic( + expected = "Only 1 item is supported when using `nested` in a tuple variant." + )] + fn panics_multiple_items() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A(NestedConfig, bool), + } + }); + } + } + + mod unit_enum { + // N/A + } } diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_func.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_func.snap index 442d7f4d..538ccba1 100644 --- a/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_func.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_func.snap @@ -8,28 +8,28 @@ fn merge( mut next: Self, ) -> std::result::Result<(), schematic::ConfigError> { match self { - Self::A(ao) => { - if let Self::A(ai) = next { - *self = Self::A(a(ao.to_owned(), ai, context)?.unwrap_or_default()); + Self::A(pa) => { + if let Self::A(na) = next { + *self = Self::A(a(pa.to_owned(), na, context)?.unwrap_or_default()); } else { *self = next; } } - Self::B(ao) => { - if let Self::B(ai) = next { - *self = Self::B(b(ao.to_owned(), ai, context)?.unwrap_or_default()); + Self::B(pa) => { + if let Self::B(na) = next { + *self = Self::B(b(pa.to_owned(), na, context)?.unwrap_or_default()); } else { *self = next; } } - Self::C(ao, bo) => { - if let Self::C(ai, bi) = next { - if let Some((ao, bo)) = c( - (ao.to_owned(), bo.to_owned()), - (ai, bi), + Self::C(pa, pb) => { + if let Self::C(na, nb) = next { + if let Some((pa, pb)) = c( + (pa.to_owned(), pb.to_owned()), + (na, nb), context, )? { - *self = Self::C(ao, bo); + *self = Self::C(pa, pb); } else { *self = Self::C(Default::default(), Default::default()); } @@ -37,14 +37,14 @@ fn merge( *self = next; } } - Self::D(ao, bo) => { - if let Self::D(ai, bi) = next { - if let Some((ao, bo)) = d( - (ao.to_owned(), bo.to_owned()), - (ai, bi), + Self::D(pa, pb) => { + if let Self::D(na, nb) = next { + if let Some((pa, pb)) = d( + (pa.to_owned(), pb.to_owned()), + (na, nb), context, )? { - *self = Self::D(ao, bo); + *self = Self::D(pa, pb); } else { *self = Self::D(Default::default(), Default::default()); } @@ -52,9 +52,9 @@ fn merge( *self = next; } } - Self::E(ao) => { - if let Self::E(ai) = next { - *self = Self::E(e(ao.to_owned(), ai, context)?.unwrap_or_default()); + Self::E(pa) => { + if let Self::E(na) = next { + *self = Self::E(e(pa.to_owned(), na, context)?.unwrap_or_default()); } else { *self = next; } diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested.snap new file mode 100644 index 00000000..6c3ed314 --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested.snap @@ -0,0 +1,44 @@ +--- +source: crates/core/tests/setting_merge_test.rs +expression: pretty(container.impl_partial_merge()) +--- +fn merge( + &mut self, + context: &Self::Context, + mut next: Self, +) -> std::result::Result<(), schematic::ConfigError> { + match self { + Self::A(pa) => { + if let Self::A(na) = next { + pa.merge(context, na)?; + } else { + *self = next; + } + } + Self::B(pa) => { + if let Self::B(na) = next { + pa.merge(context, na)?; + } else { + *self = next; + } + } + Self::C(pa) => { + if let Self::C(na) = next { + pa.merge(context, na)?; + } else { + *self = next; + } + } + Self::D(pa) => { + if let Self::D(na) = next { + pa.merge(context, na)?; + } else { + *self = next; + } + } + _ => { + *self = next; + } + }; + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested_collections.snap new file mode 100644 index 00000000..794ac11b --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested_collections.snap @@ -0,0 +1,43 @@ +--- +source: crates/core/tests/setting_merge_test.rs +expression: pretty(container.impl_partial_merge()) +--- +fn merge( + &mut self, + context: &Self::Context, + mut next: Self, +) -> std::result::Result<(), schematic::ConfigError> { + match self { + Self::A(pa) => { + if let Self::A(na) = next { + *self = Self::A( + append_vec(pa.to_owned(), na, context)?.unwrap_or_default(), + ); + } else { + *self = next; + } + } + Self::B(pa) => { + if let Self::B(na) = next { + *self = Self::B( + merge_hashmap(pa.to_owned(), na, context)?.unwrap_or_default(), + ); + } else { + *self = next; + } + } + Self::C(pa) => { + if let Self::C(na) = next { + *self = Self::C( + merge_btreeset(pa.to_owned(), na, context)?.unwrap_or_default(), + ); + } else { + *self = next; + } + } + _ => { + *self = next; + } + }; + Ok(()) +} From 7158f0f3829252a2665c1ce27bda9cab530eb935 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Thu, 9 Oct 2025 15:09:21 -0700 Subject: [PATCH 24/33] Add notes. --- crates/core/src/field.rs | 8 +++----- crates/core/src/variant.rs | 2 ++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index efb6f020..b6864fa6 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -20,13 +20,16 @@ pub struct FieldArgs { pub env: Option, #[cfg(feature = "env")] pub env_prefix: Option, + // TODO test pub exclude: bool, #[cfg(feature = "extends")] pub extend: bool, pub merge: Option, + // TODO test pub nested: Option, #[cfg(feature = "env")] pub parse_env: Option, + // TODO test pub partial: Option, pub required: bool, // TODO pub transform: Option, @@ -98,12 +101,7 @@ impl Field { if self.args.env_prefix.is_some() && self.args.nested.is_none() { panic!("Cannot use `env_prefix` without `nested`."); } - } - - // nested - #[cfg(feature = "env")] - { // parse_env if self.args.parse_env.is_some() && self.args.env.is_none() { panic!("Cannot use `parse_env` without `env`."); diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index f444a75b..e18a961b 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -8,6 +8,8 @@ use quote::{format_ident, quote}; use std::rc::Rc; use syn::{Attribute, ExprPath, Fields, FieldsUnnamed, Ident, Variant as NativeVariant}; +// TODO test all + // #[setting()], #[schema()] #[derive(Debug, Default, FromAttributes)] #[darling(default, attributes(setting, schema))] From 3f917d39c40ae2ce815d0d4d1094d89aeaee9604 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 2 Nov 2025 14:06:07 -0800 Subject: [PATCH 25/33] More tests. --- Cargo.lock | 4 +- crates/core/Cargo.toml | 2 +- crates/core/src/field.rs | 14 +- crates/core/src/variant.rs | 45 ++- crates/core/tests/setting_env_test.rs | 24 ++ crates/core/tests/setting_extend_test.rs | 266 ++++++++++-------- crates/core/tests/setting_merge_test.rs | 14 +- crates/core/tests/setting_nested_test.rs | 14 +- crates/core/tests/setting_partial_test.rs | 74 +++++ crates/core/tests/setting_required_test.rs | 151 ++++++++++ crates/core/tests/setting_test.rs | 12 +- crates/core/tests/setting_transform_test.rs | 12 + crates/core/tests/setting_validate_test.rs | 64 +++++ ...end__named_struct__can_set_opt_string.snap | 7 + ...xtend__named_struct__can_set_opt_type.snap | 7 + ..._named_struct__can_set_opt_vec_string.snap | 7 + ..._extend__named_struct__can_set_string.snap | 7 + ...ng_extend__named_struct__can_set_type.snap | 7 + ...end__named_struct__can_set_vec_string.snap | 7 + 19 files changed, 599 insertions(+), 139 deletions(-) create mode 100644 crates/core/tests/setting_required_test.rs create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_string.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_type.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_vec_string.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_string.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_type.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_vec_string.snap diff --git a/Cargo.lock b/Cargo.lock index 5d6104b8..a7b23f92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1343,9 +1343,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.35" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn 2.0.114", diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index c5d19b6a..326f2a03 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -26,7 +26,7 @@ schematic_core = { path = ".", features = [ "tracing", "validate", ] } -prettyplease = "0.2.35" +prettyplease = "0.2.37" syn = { workspace = true, features = ["full", "extra-traits"] } starbase_sandbox = { workspace = true } diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index b6864fa6..4d8df293 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -10,6 +10,8 @@ use quote::{ToTokens, TokenStreamExt, quote}; use std::rc::Rc; use syn::{Attribute, Expr, ExprPath, Field as NativeField, FieldMutability, Ident, Visibility}; +// TODO: finalize, validate + // #[schema()], #[setting()] #[derive(Debug, FromAttributes, Default)] #[darling(default, attributes(schema, setting))] @@ -25,13 +27,11 @@ pub struct FieldArgs { #[cfg(feature = "extends")] pub extend: bool, pub merge: Option, - // TODO test pub nested: Option, #[cfg(feature = "env")] pub parse_env: Option, - // TODO test pub partial: Option, - pub required: bool, // TODO + pub required: bool, pub transform: Option, #[cfg(feature = "validate")] pub validate: Option, @@ -107,6 +107,10 @@ impl Field { panic!("Cannot use `parse_env` without `env`."); } } + + if self.is_required() && !self.value.is_outer_option_wrapped() { + panic!("Cannot use `required` with non-optional settings."); + } } #[cfg(not(feature = "env"))] @@ -198,6 +202,10 @@ impl Field { .as_ref() .is_some_and(|nested| nested.is_nested()) } + + pub fn is_required(&self) -> bool { + self.args.required + } } // impl ToTokens for Field { diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index e18a961b..695e1431 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -1,4 +1,4 @@ -use crate::args::{NestedArg, SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; +use crate::args::{NestedArg, PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; use crate::container::ContainerArgs; use crate::utils::ImplResult; use crate::variant_value::VariantValue; @@ -8,8 +8,6 @@ use quote::{format_ident, quote}; use std::rc::Rc; use syn::{Attribute, ExprPath, Fields, FieldsUnnamed, Ident, Variant as NativeVariant}; -// TODO test all - // #[setting()], #[schema()] #[derive(Debug, Default, FromAttributes)] #[darling(default, attributes(setting, schema))] @@ -17,6 +15,11 @@ pub struct VariantArgs { pub default: bool, pub merge: Option, pub nested: Option, + pub partial: Option, + pub required: bool, + #[cfg(feature = "validate")] + pub validate: Option, + // TODO exclude, null // serde #[darling(multiple)] @@ -83,6 +86,34 @@ impl Variant { if self.is_nested() && self.values.len() > 1 { panic!("Only 1 item is supported when using `nested` in a tuple variant.") } + + if self.is_required() + && self + .values + .iter() + .any(|value| !value.is_outer_option_wrapped()) + { + panic!("Cannot use `required` with non-optional settings."); + } + + if self.is_unit_variant() { + if self.args.merge.is_some() { + panic!("Cannot use `merge` with unit variants."); + } + + if self.args.nested.is_some() { + panic!("Cannot use `nested` with unit variants."); + } + + if self.args.required { + panic!("Cannot use `required` with unit variants."); + } + + #[cfg(feature = "validate")] + if self.args.validate.is_some() { + panic!("Cannot use `validate` with unit variants."); + } + } } pub fn is_default(&self) -> bool { @@ -96,6 +127,14 @@ impl Variant { .is_some_and(|nested| nested.is_nested()) } + pub fn is_required(&self) -> bool { + self.args.required + } + + pub fn is_unit_variant(&self) -> bool { + self.values.is_empty() + } + pub fn impl_partial_default_value(&self) -> ImplResult { let mut res = ImplResult::default(); let name = &self.ident; diff --git a/crates/core/tests/setting_env_test.rs b/crates/core/tests/setting_env_test.rs index 6355fd96..2c17bdda 100644 --- a/crates/core/tests/setting_env_test.rs +++ b/crates/core/tests/setting_env_test.rs @@ -191,6 +191,18 @@ mod setting_env { assert_snapshot!(pretty(container.impl_partial_env_values())); } } + + mod named_enum { + // N/A + } + + mod unnamed_enum { + // N/A + } + + mod unit_enum { + // N/A + } } mod setting_env_prefix { @@ -451,4 +463,16 @@ mod setting_parse_env { }); } } + + mod named_enum { + // N/A + } + + mod unnamed_enum { + // N/A + } + + mod unit_enum { + // N/A + } } diff --git a/crates/core/tests/setting_extend_test.rs b/crates/core/tests/setting_extend_test.rs index 015dd002..8c795818 100644 --- a/crates/core/tests/setting_extend_test.rs +++ b/crates/core/tests/setting_extend_test.rs @@ -8,136 +8,156 @@ use utils::pretty; mod setting_extend { use super::*; - #[test] - fn can_set_string() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(extend)] - a: String, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.extend); - assert_snapshot!(pretty(container.impl_partial_extends_from())); + mod named_struct { + use super::*; + + #[test] + fn can_set_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.extend); + assert_snapshot!(pretty(container.impl_partial_extends_from())); + } + + #[test] + fn can_set_opt_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: Option, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.extend); + assert_snapshot!(pretty(container.impl_partial_extends_from())); + } + + #[test] + fn can_set_vec_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: Vec, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.extend); + assert_snapshot!(pretty(container.impl_partial_extends_from())); + } + + #[test] + fn can_set_opt_vec_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: Option>, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.extend); + assert_snapshot!(pretty(container.impl_partial_extends_from())); + } + + #[test] + fn can_set_type() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: ExtendsFrom, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.extend); + assert_snapshot!(pretty(container.impl_partial_extends_from())); + } + + #[test] + fn can_set_opt_type() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: Option, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.extend); + assert_snapshot!(pretty(container.impl_partial_extends_from())); + } + + #[test] + #[should_panic(expected = "Only 1 setting may use `extend`, found: a, b")] + fn errors_multiple_extends() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: String, + #[setting(extend)] + b: String, + } + }) + .impl_partial_extends_from(); + } + + #[test] + #[should_panic( + expected = "Only `String`, `Vec`, or `schematic::ExtendsFrom` are supported" + )] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: bool, + } + }) + .impl_partial_extends_from(); + } } - #[test] - fn can_set_opt_string() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(extend)] - a: Option, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.extend); - assert_snapshot!(pretty(container.impl_partial_extends_from())); + mod unnamed_struct { + use super::*; + + #[test] + #[should_panic(expected = "Only named structs can use `extend` settings.")] + fn errors_when_used_in_unnamed_struct() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(extend)] + String, + ); + }) + .impl_partial_extends_from(); + } } - #[test] - fn can_set_vec_string() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(extend)] - a: Vec, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.extend); - assert_snapshot!(pretty(container.impl_partial_extends_from())); + mod named_enum { + // N/A } - #[test] - fn can_set_opt_vec_string() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(extend)] - a: Option>, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.extend); - assert_snapshot!(pretty(container.impl_partial_extends_from())); + mod unnamed_enum { + // N/A } - #[test] - fn can_set_type() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(extend)] - a: ExtendsFrom, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.extend); - assert_snapshot!(pretty(container.impl_partial_extends_from())); - } - - #[test] - fn can_set_opt_type() { - let container = Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(extend)] - a: Option, - } - }); - let field = container.inner.get_fields()[0]; - - assert!(field.args.extend); - assert_snapshot!(pretty(container.impl_partial_extends_from())); - } - - #[test] - #[should_panic(expected = "Only 1 setting may use `extend`, found: a, b")] - fn errors_multiple_extends() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(extend)] - a: String, - #[setting(extend)] - b: String, - } - }) - .impl_partial_extends_from(); - } - - #[test] - #[should_panic( - expected = "Only `String`, `Vec`, or `schematic::ExtendsFrom` are supported" - )] - fn errors_invalid_type() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example { - #[setting(extend)] - a: bool, - } - }) - .impl_partial_extends_from(); - } - - #[test] - #[should_panic(expected = "Only named structs can use `extend` settings.")] - fn errors_when_used_in_unnamed_struct() { - Container::from(parse_quote! { - #[derive(Config)] - struct Example( - #[setting(extend)] - String, - ); - }) - .impl_partial_extends_from(); + mod unit_enum { + // N/A } } diff --git a/crates/core/tests/setting_merge_test.rs b/crates/core/tests/setting_merge_test.rs index a3982b72..a436422b 100644 --- a/crates/core/tests/setting_merge_test.rs +++ b/crates/core/tests/setting_merge_test.rs @@ -454,6 +454,18 @@ mod setting_merge { } mod unit_enum { - // N/A + use super::*; + + #[test] + #[should_panic(expected = "Cannot use `merge` with unit variants.")] + fn errors_for_unit() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(merge = func_ref)] + A, + } + }); + } } } diff --git a/crates/core/tests/setting_nested_test.rs b/crates/core/tests/setting_nested_test.rs index 7b91b143..a7b113f6 100644 --- a/crates/core/tests/setting_nested_test.rs +++ b/crates/core/tests/setting_nested_test.rs @@ -417,6 +417,18 @@ mod setting_nested { } mod unit_enum { - // N/A + use super::*; + + #[test] + #[should_panic(expected = "Cannot use `nested` with unit variants.")] + fn errors_for_unit() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A, + } + }); + } } } diff --git a/crates/core/tests/setting_partial_test.rs b/crates/core/tests/setting_partial_test.rs index 5c1d5979..ced66344 100644 --- a/crates/core/tests/setting_partial_test.rs +++ b/crates/core/tests/setting_partial_test.rs @@ -21,4 +21,78 @@ mod setting_partial { assert!(field.args.partial.is_some()); } } + + mod unnamed_struct { + use super::*; + + #[test] + fn can_set() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(partial(other(attribute), and(another)))] + bool, + ); + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.partial.is_some()); + } + } + + mod named_enum { + use super::*; + + #[test] + fn can_set() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(partial(other(attribute), and(another)))] + A { + field: bool + } + } + }); + let field = container.inner.get_variants()[0]; + + assert!(field.args.partial.is_some()); + } + } + + mod unnamed_enum { + use super::*; + + #[test] + fn can_set() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(partial(other(attribute), and(another)))] + A(bool), + } + }); + let field = container.inner.get_variants()[0]; + + assert!(field.args.partial.is_some()); + } + } + + mod unit_enum { + use super::*; + + #[test] + fn can_set() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(partial(other(attribute), and(another)))] + A, + } + }); + let field = container.inner.get_variants()[0]; + + assert!(field.args.partial.is_some()); + } + } } diff --git a/crates/core/tests/setting_required_test.rs b/crates/core/tests/setting_required_test.rs new file mode 100644 index 00000000..c08aad90 --- /dev/null +++ b/crates/core/tests/setting_required_test.rs @@ -0,0 +1,151 @@ +use schematic_core::container::Container; +use syn::parse_quote; + +mod setting_required { + use super::*; + + mod named_struct { + use super::*; + + #[test] + fn accepts_bool() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(required)] + a: Option, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.required); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(required = 123)] + a: Option, + } + }); + } + + #[test] + #[should_panic(expected = "Cannot use `required` with non-optional settings.")] + fn errors_no_option() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(required)] + a: String, + } + }); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn accepts_bool() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(required)] + Option, + ); + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.required); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(required = 123)] + Option, + ); + }); + } + + #[test] + #[should_panic(expected = "Cannot use `required` with non-optional settings.")] + fn errors_no_option() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(required)] + String, + ); + }); + } + } + + mod named_enum { + // N/A + } + + mod unnamed_enum { + use super::*; + + #[test] + fn accepts_bool() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(required)] + A(Option), + } + }); + let field = container.inner.get_variants()[0]; + + assert!(field.args.required); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(required = 123)] + A(Option), + } + }); + } + + #[test] + #[should_panic(expected = "Cannot use `required` with non-optional settings.")] + fn errors_no_option() { + Container::from(parse_quote! { + enum Example { + #[setting(required)] + A(String), + } + }); + } + } + + mod unit_enum { + use super::*; + + #[test] + #[should_panic(expected = "Cannot use `required` with unit variants.")] + fn errors_for_unit() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(required)] + A, + } + }); + } + } +} diff --git a/crates/core/tests/setting_test.rs b/crates/core/tests/setting_test.rs index 7341933b..2c6d824d 100644 --- a/crates/core/tests/setting_test.rs +++ b/crates/core/tests/setting_test.rs @@ -22,15 +22,17 @@ mod setting_field { let container = Container::from(parse_quote! { #[derive(Config)] struct Example { - #[setting(exclude, extend, required)] + #[setting(exclude, extend)] a: String, + #[setting(required)] + b: Option, } }); - let field = container.inner.get_fields()[0]; + let fields = container.inner.get_fields(); - assert!(field.args.exclude); - assert!(field.args.extend); - assert!(field.args.required); + assert!(fields[0].args.exclude); + assert!(fields[0].args.extend); + assert!(fields[1].args.required); } #[test] diff --git a/crates/core/tests/setting_transform_test.rs b/crates/core/tests/setting_transform_test.rs index 537436f5..4ef8823b 100644 --- a/crates/core/tests/setting_transform_test.rs +++ b/crates/core/tests/setting_transform_test.rs @@ -91,4 +91,16 @@ mod setting_transform { }); } } + + mod named_enum { + // N/A + } + + mod unnamed_enum { + // N/A + } + + mod unit_enum { + // N/A + } } diff --git a/crates/core/tests/setting_validate_test.rs b/crates/core/tests/setting_validate_test.rs index d911f6b6..89c71cd7 100644 --- a/crates/core/tests/setting_validate_test.rs +++ b/crates/core/tests/setting_validate_test.rs @@ -91,4 +91,68 @@ mod setting_validate { }); } } + + mod named_enum { + // N/A + } + + mod unnamed_enum { + use super::*; + + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(validate = func_ref)] + A(String), + } + }); + let field = container.inner.get_variants()[0]; + + assert!(field.args.validate.is_some()); + } + + #[test] + fn accepts_curried_func() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(validate = func_call())] + A(String), + } + }); + let field = container.inner.get_variants()[0]; + + assert!(field.args.validate.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(validate = 123)] + A(String), + } + }); + } + } + + mod unit_enum { + use super::*; + + #[test] + #[should_panic(expected = "Cannot use `validate` with unit variants.")] + fn errors_for_unit() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(validate = func_ref)] + A, + } + }); + } + } } diff --git a/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_string.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_string.snap new file mode 100644 index 00000000..bd38cb48 --- /dev/null +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_string.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/setting_extend_test.rs +expression: pretty(container.impl_partial_extends_from()) +--- +fn extends_from(&self) -> Option { + self.a.as_ref().map(|inner| schematic::ExtendsFrom::String(inner.to_owned())) +} diff --git a/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_type.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_type.snap new file mode 100644 index 00000000..20f23ba9 --- /dev/null +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_type.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/setting_extend_test.rs +expression: pretty(container.impl_partial_extends_from()) +--- +fn extends_from(&self) -> Option { + self.a.clone() +} diff --git a/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_vec_string.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_vec_string.snap new file mode 100644 index 00000000..5374d8c9 --- /dev/null +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_vec_string.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/setting_extend_test.rs +expression: pretty(container.impl_partial_extends_from()) +--- +fn extends_from(&self) -> Option { + self.a.as_ref().map(|inner| schematic::ExtendsFrom::List(inner.to_owned())) +} diff --git a/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_string.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_string.snap new file mode 100644 index 00000000..bd38cb48 --- /dev/null +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_string.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/setting_extend_test.rs +expression: pretty(container.impl_partial_extends_from()) +--- +fn extends_from(&self) -> Option { + self.a.as_ref().map(|inner| schematic::ExtendsFrom::String(inner.to_owned())) +} diff --git a/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_type.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_type.snap new file mode 100644 index 00000000..20f23ba9 --- /dev/null +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_type.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/setting_extend_test.rs +expression: pretty(container.impl_partial_extends_from()) +--- +fn extends_from(&self) -> Option { + self.a.clone() +} diff --git a/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_vec_string.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_vec_string.snap new file mode 100644 index 00000000..5374d8c9 --- /dev/null +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_vec_string.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/setting_extend_test.rs +expression: pretty(container.impl_partial_extends_from()) +--- +fn extends_from(&self) -> Option { + self.a.as_ref().map(|inner| schematic::ExtendsFrom::List(inner.to_owned())) +} From 69bdbc9d344bf45f9b7c533481ac27272b555f0a Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 2 Nov 2025 15:47:06 -0800 Subject: [PATCH 26/33] Add exclude/null. --- crates/core/src/field.rs | 16 +++- crates/core/src/variant.rs | 20 ++++- crates/core/tests/setting_exclude_test.rs | 98 +++++++++++++++++++++++ crates/core/tests/setting_null_test.rs | 57 +++++++++++++ 4 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 crates/core/tests/setting_exclude_test.rs create mode 100644 crates/core/tests/setting_null_test.rs diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 4d8df293..c361a558 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -22,7 +22,7 @@ pub struct FieldArgs { pub env: Option, #[cfg(feature = "env")] pub env_prefix: Option, - // TODO test + #[cfg(feature = "schema")] pub exclude: bool, #[cfg(feature = "extends")] pub extend: bool, @@ -97,12 +97,10 @@ impl Field { fn validate_args(&self) { #[cfg(feature = "env")] { - // env_prefix if self.args.env_prefix.is_some() && self.args.nested.is_none() { panic!("Cannot use `env_prefix` without `nested`."); } - // parse_env if self.args.parse_env.is_some() && self.args.env.is_none() { panic!("Cannot use `parse_env` without `env`."); } @@ -184,6 +182,18 @@ impl Field { .expect("Name only usable on named fields!") } + pub fn is_excluded(&self) -> bool { + #[cfg(feature = "schema")] + { + self.args.exclude + } + + #[cfg(not(feature = "schema"))] + { + false + } + } + pub fn is_extendable(&self) -> bool { #[cfg(feature = "extends")] { diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index 695e1431..eba88bec 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -13,13 +13,15 @@ use syn::{Attribute, ExprPath, Fields, FieldsUnnamed, Ident, Variant as NativeVa #[darling(default, attributes(setting, schema))] pub struct VariantArgs { pub default: bool, + #[cfg(feature = "schema")] + pub exclude: bool, pub merge: Option, pub nested: Option, + pub null: bool, pub partial: Option, pub required: bool, #[cfg(feature = "validate")] pub validate: Option, - // TODO exclude, null // serde #[darling(multiple)] @@ -113,6 +115,10 @@ impl Variant { if self.args.validate.is_some() { panic!("Cannot use `validate` with unit variants."); } + } else { + if self.args.null { + panic!("Can only use `null` with unit variants."); + } } } @@ -120,6 +126,18 @@ impl Variant { self.args.default } + pub fn is_excluded(&self) -> bool { + #[cfg(feature = "schema")] + { + self.args.exclude + } + + #[cfg(not(feature = "schema"))] + { + false + } + } + pub fn is_nested(&self) -> bool { self.args .nested diff --git a/crates/core/tests/setting_exclude_test.rs b/crates/core/tests/setting_exclude_test.rs new file mode 100644 index 00000000..df2e2bcd --- /dev/null +++ b/crates/core/tests/setting_exclude_test.rs @@ -0,0 +1,98 @@ +use schematic_core::container::Container; +use syn::parse_quote; + +mod setting_exclude { + use super::*; + + mod named_struct { + use super::*; + + #[test] + fn can_set() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(exclude)] + a: bool, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.exclude); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn can_set() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(exclude)] + bool, + ); + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.exclude); + } + } + + mod named_enum { + use super::*; + + #[test] + fn can_set() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(exclude)] + A { + field: bool + } + } + }); + let field = container.inner.get_variants()[0]; + + assert!(field.args.exclude); + } + } + + mod unnamed_enum { + use super::*; + + #[test] + fn can_set() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(exclude)] + A(bool), + } + }); + let field = container.inner.get_variants()[0]; + + assert!(field.args.exclude); + } + } + + mod unit_enum { + use super::*; + + #[test] + fn can_set() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(exclude)] + A, + } + }); + let field = container.inner.get_variants()[0]; + + assert!(field.args.exclude); + } + } +} diff --git a/crates/core/tests/setting_null_test.rs b/crates/core/tests/setting_null_test.rs new file mode 100644 index 00000000..983a6648 --- /dev/null +++ b/crates/core/tests/setting_null_test.rs @@ -0,0 +1,57 @@ +use schematic_core::container::Container; +use syn::parse_quote; + +// Only applies to unit enums! +mod setting_null { + use super::*; + + // mod named_enum { + // use super::*; + + // #[test] + // #[should_panic(expected = "Can only use `null` with unit variants.")] + // fn errors_for_named() { + // Container::from(parse_quote! { + // #[derive(Config)] + // enum Example { + // #[setting(null)] + // A {} + // } + // }); + // } + // } + + mod unnamed_enum { + use super::*; + + #[test] + #[should_panic(expected = "Can only use `null` with unit variants.")] + fn errors_for_named() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(null)] + A(String) + } + }); + } + } + + mod unit_enum { + use super::*; + + #[test] + fn can_set() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(null)] + A, + } + }); + let field = container.inner.get_variants()[0]; + + assert!(field.args.null); + } + } +} From d6e30cb5868068aa9691b4203e6c82e4de556a1e Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Mon, 3 Nov 2025 09:32:41 -0800 Subject: [PATCH 27/33] Add struct validate. --- crates/core/src/container.rs | 60 ++++++++ crates/core/src/field.rs | 54 ++++++- crates/core/src/field_value.rs | 93 ++++++++++- crates/core/src/variant.rs | 5 + crates/core/tests/setting_env_test.rs | 4 + crates/core/tests/setting_merge_test.rs | 6 +- crates/core/tests/setting_validate_test.rs | 144 ++++++++++++++++++ ...e_env__named_struct__accepts_func_ref.snap | 13 ++ ...rse_env__named_struct__accepts_string.snap | 13 ++ ...env__unnamed_struct__accepts_func_ref.snap | 13 ++ ...e_env__unnamed_struct__accepts_string.snap | 13 ++ ..._merge__named_struct__supports_nested.snap | 4 +- ...d_struct__supports_nested_collections.snap | 2 +- ...e__named_struct__accepts_curried_func.snap | 20 +++ ...idate__named_struct__accepts_func_ref.snap | 20 +++ ...lidate__named_struct__supports_nested.snap | 39 +++++ ...d_struct__supports_nested_collections.snap | 38 +++++ ...date__named_struct__supports_standard.snap | 32 ++++ ...e__unnamed_enum__accepts_curried_func.snap | 5 + ...idate__unnamed_enum__accepts_func_ref.snap | 5 + ..._unnamed_struct__accepts_curried_func.snap | 20 +++ ...ate__unnamed_struct__accepts_func_ref.snap | 20 +++ ...date__unnamed_struct__supports_nested.snap | 39 +++++ ...d_struct__supports_nested_collections.snap | 38 +++++ ...te__unnamed_struct__supports_standard.snap | 32 ++++ crates/schematic/src/internal/env.rs | 57 +++++++ crates/schematic/src/internal/merge.rs | 47 ++++++ .../src/{internal.rs => internal/mod.rs} | 118 ++------------ crates/schematic/src/internal/validate.rs | 71 +++++++++ 29 files changed, 907 insertions(+), 118 deletions(-) create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_parse_env__named_struct__accepts_func_ref.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_parse_env__named_struct__accepts_string.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_parse_env__unnamed_struct__accepts_func_ref.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_parse_env__unnamed_struct__accepts_string.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_curried_func.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_curried_func.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap create mode 100644 crates/schematic/src/internal/env.rs create mode 100644 crates/schematic/src/internal/merge.rs rename crates/schematic/src/{internal.rs => internal/mod.rs} (58%) create mode 100644 crates/schematic/src/internal/validate.rs diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 6320f6cb..24612081 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -185,6 +185,7 @@ impl Container { let env_values_method = self.impl_partial_env_values(); let extends_from_method = self.impl_partial_extends_from(); let merge_method = self.impl_partial_merge(); + let validate_method = self.impl_partial_validate(); quote! { #[automatically_derived] @@ -195,6 +196,7 @@ impl Container { #env_values_method #extends_from_method #merge_method + #validate_method } #[automatically_derived] @@ -513,6 +515,64 @@ impl Container { } } } + + #[cfg(not(feature = "validate"))] + pub fn impl_partial_validate(&self) -> TokenStream { + quote! {} + } + + #[cfg(feature = "validate")] + pub fn impl_partial_validate(&self) -> TokenStream { + let mut statements = vec![]; + + match &self.inner { + ContainerInner::NamedStruct { fields } | ContainerInner::UnnamedStruct { fields } => { + for field in fields { + let res = field.impl_partial_validate(); + + if !res.no_value { + statements.push(res.value); + } + } + } + ContainerInner::Enum { variants } => { + for variant in variants { + let res = variant.impl_partial_validate(); + + if !res.no_value { + statements.push(res.value); + } + } + } + ContainerInner::UnitEnum { .. } => {} + }; + + if statements.is_empty() { + return quote! {}; + } + + let internal = ImplResult::impl_use_internal(true); + + quote! { + fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path + ) -> std::result::Result<(), Vec> { + #internal + + let mut validate = ValidateManager::new(context, finalize, path); + #(#statements)* + + if !validate.errors.is_empty() { + return Err(validate.errors); + } + + Ok(()) + } + } + } } impl ToTokens for Container { diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index c361a558..70e0db52 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -260,14 +260,64 @@ impl Field { pub fn impl_partial_extends_from(&self) -> ImplResult { if self.is_extendable() { self.value - .impl_partial_extends_from(&self.args, self.get_key()) + .impl_partial_extends_from(&self.args, &self.get_key()) } else { ImplResult::skipped() } } pub fn impl_partial_merge(&self) -> ImplResult { - self.value.impl_partial_merge(&self.args, self.get_key()) + self.value.impl_partial_merge(&self.args, &self.get_key()) + } + + pub fn impl_partial_validate(&self) -> ImplResult { + let key = self.get_key(); + let res = self.value.impl_partial_validate(&self.args, &key); + let mut inner = res.value; + let mut has_inner = !res.no_value; + + if self.is_nested() { + let nested_value = self.value.impl_partial_validate_nested(&key).value; + + has_inner = true; + inner = quote! { + #inner + #nested_value + }; + } + + let mut has_outer = has_inner; + let mut outer = if has_inner { + quote! { + if let Some(setting) = &self.#key { + #inner + } + } + } else { + quote! {} + }; + + if self.is_required() { + has_outer = true; + outer = quote! { + #outer + + if finalize && self.#key.is_none() { + errors.push(schematic::ValidateError::required().prepend_path( + path.join_key(#key) + )); + } + }; + } + + if has_outer { + ImplResult { + value: outer, + ..Default::default() + } + } else { + ImplResult::skipped() + } } } diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index 0afcca82..7f6e38f7 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -166,7 +166,7 @@ impl FieldValue { pub fn impl_partial_extends_from( &self, _field_args: &FieldArgs, - _field_name: TokenStream, + _field_name: &TokenStream, ) -> ImplResult { ImplResult::skipped() } @@ -175,7 +175,7 @@ impl FieldValue { pub fn impl_partial_extends_from( &self, _field_args: &FieldArgs, - field_name: TokenStream, + field_name: &TokenStream, ) -> ImplResult { let value = match self.ty_string.as_str() { "String" | "Option" => { @@ -216,7 +216,7 @@ impl FieldValue { pub fn impl_partial_merge( &self, field_args: &FieldArgs, - field_name: TokenStream, + field_name: &TokenStream, ) -> ImplResult { let value = match field_args.merge.as_ref() { Some(func) => { @@ -260,4 +260,91 @@ impl FieldValue { ..Default::default() } } + + #[cfg(not(feature = "validate"))] + pub fn impl_partial_validate( + &self, + _field_args: &FieldArgs, + _field_name: &TokenStream, + ) -> ImplResult { + ImplResult::skipped() + } + + #[cfg(feature = "validate")] + pub fn impl_partial_validate( + &self, + field_args: &FieldArgs, + field_name: &TokenStream, + ) -> ImplResult { + let mut res = ImplResult::default(); + + if let Some(expr) = field_args.validate.as_deref() { + let field_name_string = field_name.to_string(); + let func = match expr { + // func(arg)() + Expr::Call(func) => quote! { #func }, + // func() + Expr::Path(func) => quote! { #func }, + _ => { + panic!("Unsupported `validate` syntax."); + } + }; + + res.value = quote! { + validate.check(#field_name_string, setting, self, #func); + }; + } else { + res.no_value = true; + } + + res + } + + #[cfg(not(feature = "validate"))] + pub fn impl_partial_validate_nested(&self, _field_name: &TokenStream) -> ImplResult { + ImplResult::skipped() + } + + #[cfg(feature = "validate")] + pub fn impl_partial_validate_nested(&self, field_name: &TokenStream) -> ImplResult { + if self.layers.len() >= 2 + && self + .layers + .get(1) + .is_some_and(|layer| matches!(layer, Layer::Option)) + { + return ImplResult::skipped(); + } + + let field_name_string = field_name.to_string(); + let mut value = quote! { + validate.nested(#field_name_string, setting); + }; + + for layer in self.layers.iter().rev() { + match layer { + Layer::Arc | Layer::Box | Layer::Option | Layer::Rc => { + // Nothing? + } + Layer::Map(_) => { + value = quote! { + validate.nested_map(#field_name_string, setting.iter()); + }; + } + Layer::Set(_) | Layer::Vec(_) => { + value = quote! { + validate.nested_list(#field_name_string, setting.iter()); + }; + } + Layer::Unknown(_) => { + return ImplResult::skipped(); + } + }; + } + + ImplResult { + value, + ..Default::default() + } + } } diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index eba88bec..a4c91319 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -284,6 +284,11 @@ impl Variant { res } + pub fn impl_partial_validate(&self) -> ImplResult { + // TODO + ImplResult::skipped() + } + fn map_unnamed_match(&self, name: &Ident, fields: &FieldsUnnamed, factory: F) -> TokenStream where F: FnOnce(&[Ident], &[Ident]) -> TokenStream, diff --git a/crates/core/tests/setting_env_test.rs b/crates/core/tests/setting_env_test.rs index 2c17bdda..e1f41e73 100644 --- a/crates/core/tests/setting_env_test.rs +++ b/crates/core/tests/setting_env_test.rs @@ -367,6 +367,7 @@ mod setting_parse_env { let field = container.inner.get_fields()[0]; assert!(field.args.parse_env.is_some()); + assert_snapshot!(pretty(container.impl_partial_env_values())); } #[test] @@ -381,6 +382,7 @@ mod setting_parse_env { let field = container.inner.get_fields()[0]; assert!(field.args.parse_env.is_some()); + assert_snapshot!(pretty(container.impl_partial_env_values())); } #[test] @@ -423,6 +425,7 @@ mod setting_parse_env { let field = container.inner.get_fields()[0]; assert!(field.args.parse_env.is_some()); + assert_snapshot!(pretty(container.impl_partial_env_values())); } #[test] @@ -437,6 +440,7 @@ mod setting_parse_env { let field = container.inner.get_fields()[0]; assert!(field.args.parse_env.is_some()); + assert_snapshot!(pretty(container.impl_partial_env_values())); } #[test] diff --git a/crates/core/tests/setting_merge_test.rs b/crates/core/tests/setting_merge_test.rs index a436422b..082a444c 100644 --- a/crates/core/tests/setting_merge_test.rs +++ b/crates/core/tests/setting_merge_test.rs @@ -79,9 +79,9 @@ mod setting_merge { #[setting(nested = CustomConfig)] b: CustomConfig, #[setting(nested)] - a: Option, + c: Option, #[setting(nested = CustomConfig)] - b: Arc, + d: Arc, } }); @@ -98,7 +98,7 @@ mod setting_merge { #[setting(nested = CustomConfig, merge = merge_hashmap)] b: HashMap, #[setting(nested, merge = merge_btreeset)] - a: Option>, + c: Option>, } }); diff --git a/crates/core/tests/setting_validate_test.rs b/crates/core/tests/setting_validate_test.rs index 89c71cd7..a73e3c5f 100644 --- a/crates/core/tests/setting_validate_test.rs +++ b/crates/core/tests/setting_validate_test.rs @@ -1,5 +1,9 @@ +mod utils; + use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; use syn::parse_quote; +use utils::pretty; mod setting_validate { use super::*; @@ -19,6 +23,7 @@ mod setting_validate { let field = container.inner.get_fields()[0]; assert!(field.args.validate.is_some()); + assert_snapshot!(pretty(container.impl_partial_validate())); } #[test] @@ -33,6 +38,7 @@ mod setting_validate { let field = container.inner.get_fields()[0]; assert!(field.args.validate.is_some()); + assert_snapshot!(pretty(container.impl_partial_validate())); } #[test] @@ -46,6 +52,73 @@ mod setting_validate { } }); } + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(validate = func_ref)] + a: bool, + #[setting(validate = func_ref)] + b: Option, + #[setting(validate = func_ref)] + c: Vec, + #[setting(validate = func_ref)] + d: Vec>, + #[setting(validate = func_ref)] + e: Option>>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested, validate = func_ref)] + a: NestedConfig, + #[setting(nested = CustomConfig, validate = func_ref)] + b: CustomConfig, + #[setting(nested, validate = func_ref)] + c: Option, + #[setting(nested = CustomConfig, validate = func_ref)] + d: Arc, + #[setting(nested)] + e: NestedConfig, + #[setting(nested = CustomConfig)] + f: CustomConfig, + } + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } + + #[test] + fn supports_nested_collections() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested, validate = func_ref)] + a: Vec, + #[setting(nested = CustomConfig, validate = func_ref)] + b: HashMap, + #[setting(nested, validate = func_ref)] + c: Option>, + #[setting(nested)] + d: Vec, + #[setting(nested = CustomConfig)] + e: HashMap, + #[setting(nested)] + f: Option>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } } mod unnamed_struct { @@ -63,6 +136,7 @@ mod setting_validate { let field = container.inner.get_fields()[0]; assert!(field.args.validate.is_some()); + assert_snapshot!(pretty(container.impl_partial_validate())); } #[test] @@ -77,6 +151,7 @@ mod setting_validate { let field = container.inner.get_fields()[0]; assert!(field.args.validate.is_some()); + assert_snapshot!(pretty(container.impl_partial_validate())); } #[test] @@ -90,6 +165,73 @@ mod setting_validate { ); }); } + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(validate = func_ref)] + bool, + #[setting(validate = func_ref)] + Option, + #[setting(validate = func_ref)] + Vec, + #[setting(validate = func_ref)] + Vec>, + #[setting(validate = func_ref)] + Option>>, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested, validate = func_ref)] + NestedConfig, + #[setting(nested = CustomConfig, validate = func_ref)] + CustomConfig, + #[setting(nested, validate = func_ref)] + Option, + #[setting(nested = CustomConfig, validate = func_ref)] + Arc, + #[setting(nested)] + NestedConfig, + #[setting(nested = CustomConfig)] + CustomConfig, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } + + #[test] + fn supports_nested_collections() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested, validate = func_ref)] + Vec, + #[setting(nested = CustomConfig, validate = func_ref)] + HashMap, + #[setting(nested, validate = func_ref)] + Option>, + #[setting(nested)] + Vec, + #[setting(nested = CustomConfig)] + HashMap, + #[setting(nested)] + Option>, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } } mod named_enum { @@ -111,6 +253,7 @@ mod setting_validate { let field = container.inner.get_variants()[0]; assert!(field.args.validate.is_some()); + assert_snapshot!(pretty(container.impl_partial_validate())); } #[test] @@ -125,6 +268,7 @@ mod setting_validate { let field = container.inner.get_variants()[0]; assert!(field.args.validate.is_some()); + assert_snapshot!(pretty(container.impl_partial_validate())); } #[test] diff --git a/crates/core/tests/snapshots/setting_env_test__setting_parse_env__named_struct__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_env_test__setting_parse_env__named_struct__accepts_func_ref.snap new file mode 100644 index 00000000..bd801f20 --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_parse_env__named_struct__accepts_func_ref.snap @@ -0,0 +1,13 @@ +--- +source: crates/core/tests/setting_env_test.rs +expression: pretty(container.impl_partial_env_values()) +--- +fn env_values_with_prefix( + prefix: Option<&str>, +) -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + let mut env = EnvManager::new(prefix); + let mut partial = Self::default(); + partial.a = env.get_and_parse("KEY", func_ref)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/setting_env_test__setting_parse_env__named_struct__accepts_string.snap b/crates/core/tests/snapshots/setting_env_test__setting_parse_env__named_struct__accepts_string.snap new file mode 100644 index 00000000..bd801f20 --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_parse_env__named_struct__accepts_string.snap @@ -0,0 +1,13 @@ +--- +source: crates/core/tests/setting_env_test.rs +expression: pretty(container.impl_partial_env_values()) +--- +fn env_values_with_prefix( + prefix: Option<&str>, +) -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + let mut env = EnvManager::new(prefix); + let mut partial = Self::default(); + partial.a = env.get_and_parse("KEY", func_ref)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/setting_env_test__setting_parse_env__unnamed_struct__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_env_test__setting_parse_env__unnamed_struct__accepts_func_ref.snap new file mode 100644 index 00000000..b91e7d90 --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_parse_env__unnamed_struct__accepts_func_ref.snap @@ -0,0 +1,13 @@ +--- +source: crates/core/tests/setting_env_test.rs +expression: pretty(container.impl_partial_env_values()) +--- +fn env_values_with_prefix( + prefix: Option<&str>, +) -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + let mut env = EnvManager::new(prefix); + let mut partial = Self::default(); + partial.0 = env.get_and_parse("KEY", func_ref)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/setting_env_test__setting_parse_env__unnamed_struct__accepts_string.snap b/crates/core/tests/snapshots/setting_env_test__setting_parse_env__unnamed_struct__accepts_string.snap new file mode 100644 index 00000000..b91e7d90 --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_parse_env__unnamed_struct__accepts_string.snap @@ -0,0 +1,13 @@ +--- +source: crates/core/tests/setting_env_test.rs +expression: pretty(container.impl_partial_env_values()) +--- +fn env_values_with_prefix( + prefix: Option<&str>, +) -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + let mut env = EnvManager::new(prefix); + let mut partial = Self::default(); + partial.0 = env.get_and_parse("KEY", func_ref)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap index c6b1424e..4bfeb116 100644 --- a/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap @@ -11,7 +11,7 @@ fn merge( MergeManager::new(context) .nested(&mut self.a, next.a)? .nested(&mut self.b, next.b)? - .nested(&mut self.a, next.a)? - .nested(&mut self.b, next.b)?; + .nested(&mut self.c, next.c)? + .nested(&mut self.d, next.d)?; Ok(()) } diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_collections.snap index a9c39c66..95bd0a41 100644 --- a/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_collections.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_collections.snap @@ -11,6 +11,6 @@ fn merge( MergeManager::new(context) .apply_with(&mut self.a, next.a, append_vec)? .apply_with(&mut self.b, next.b, merge_hashmap)? - .apply_with(&mut self.a, next.a, merge_btreeset)?; + .apply_with(&mut self.c, next.c, merge_btreeset)?; Ok(()) } diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_curried_func.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_curried_func.snap new file mode 100644 index 00000000..cabfd6d3 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_curried_func.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + if let Some(setting) = &self.a { + validate.check("a", setting, self, func_call()); + } + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap new file mode 100644 index 00000000..235fc7e5 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + if let Some(setting) = &self.a { + validate.check("a", setting, self, func_ref); + } + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap new file mode 100644 index 00000000..3792ba2d --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap @@ -0,0 +1,39 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + if let Some(setting) = &self.a { + validate.check("a", setting, self, func_ref); + validate.nested("a", setting); + } + if let Some(setting) = &self.b { + validate.check("b", setting, self, func_ref); + validate.nested("b", setting); + } + if let Some(setting) = &self.c { + validate.check("c", setting, self, func_ref); + validate.nested("c", setting); + } + if let Some(setting) = &self.d { + validate.check("d", setting, self, func_ref); + validate.nested("d", setting); + } + if let Some(setting) = &self.e { + validate.nested("e", setting); + } + if let Some(setting) = &self.f { + validate.nested("f", setting); + } + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap new file mode 100644 index 00000000..1e47af19 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap @@ -0,0 +1,38 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + if let Some(setting) = &self.a { + validate.check("a", setting, self, func_ref); + validate.nested_list("a", setting.iter()); + } + if let Some(setting) = &self.b { + validate.check("b", setting, self, func_ref); + validate.nested_map("b", setting.iter()); + } + if let Some(setting) = &self.c { + validate.check("c", setting, self, func_ref); + validate.nested_list("c", setting.iter()); + } + if let Some(setting) = &self.d { + validate.nested_list("d", setting.iter()); + } + if let Some(setting) = &self.e { + validate.nested_map("e", setting.iter()); + } + if let Some(setting) = &self.f { + validate.nested_list("f", setting.iter()); + } + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap new file mode 100644 index 00000000..d93662c7 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap @@ -0,0 +1,32 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + if let Some(setting) = &self.a { + validate.check("a", setting, self, func_ref); + } + if let Some(setting) = &self.b { + validate.check("b", setting, self, func_ref); + } + if let Some(setting) = &self.c { + validate.check("c", setting, self, func_ref); + } + if let Some(setting) = &self.d { + validate.check("d", setting, self, func_ref); + } + if let Some(setting) = &self.e { + validate.check("e", setting, self, func_ref); + } + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap new file mode 100644 index 00000000..e813a8ae --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- + diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap new file mode 100644 index 00000000..e813a8ae --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- + diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_curried_func.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_curried_func.snap new file mode 100644 index 00000000..77a2f716 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_curried_func.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + if let Some(setting) = &self.0 { + validate.check("0", setting, self, func_call()); + } + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap new file mode 100644 index 00000000..f0f7f22d --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + if let Some(setting) = &self.0 { + validate.check("0", setting, self, func_ref); + } + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap new file mode 100644 index 00000000..42370059 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap @@ -0,0 +1,39 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + if let Some(setting) = &self.0 { + validate.check("0", setting, self, func_ref); + validate.nested("0", setting); + } + if let Some(setting) = &self.1 { + validate.check("1", setting, self, func_ref); + validate.nested("1", setting); + } + if let Some(setting) = &self.2 { + validate.check("2", setting, self, func_ref); + validate.nested("2", setting); + } + if let Some(setting) = &self.3 { + validate.check("3", setting, self, func_ref); + validate.nested("3", setting); + } + if let Some(setting) = &self.4 { + validate.nested("4", setting); + } + if let Some(setting) = &self.5 { + validate.nested("5", setting); + } + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap new file mode 100644 index 00000000..6c6f7e62 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap @@ -0,0 +1,38 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + if let Some(setting) = &self.0 { + validate.check("0", setting, self, func_ref); + validate.nested_list("0", setting.iter()); + } + if let Some(setting) = &self.1 { + validate.check("1", setting, self, func_ref); + validate.nested_map("1", setting.iter()); + } + if let Some(setting) = &self.2 { + validate.check("2", setting, self, func_ref); + validate.nested_list("2", setting.iter()); + } + if let Some(setting) = &self.3 { + validate.nested_list("3", setting.iter()); + } + if let Some(setting) = &self.4 { + validate.nested_map("4", setting.iter()); + } + if let Some(setting) = &self.5 { + validate.nested_list("5", setting.iter()); + } + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap new file mode 100644 index 00000000..7e53810c --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap @@ -0,0 +1,32 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + if let Some(setting) = &self.0 { + validate.check("0", setting, self, func_ref); + } + if let Some(setting) = &self.1 { + validate.check("1", setting, self, func_ref); + } + if let Some(setting) = &self.2 { + validate.check("2", setting, self, func_ref); + } + if let Some(setting) = &self.3 { + validate.check("3", setting, self, func_ref); + } + if let Some(setting) = &self.4 { + validate.check("4", setting, self, func_ref); + } + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/schematic/src/internal/env.rs b/crates/schematic/src/internal/env.rs new file mode 100644 index 00000000..6ab79f6b --- /dev/null +++ b/crates/schematic/src/internal/env.rs @@ -0,0 +1,57 @@ +use super::parse_value; +use crate::config::{HandlerError, ParseEnvResult}; +use std::str::FromStr; + +pub struct EnvManager { + count: u8, + prefix: String, +} + +impl EnvManager { + pub fn new>(prefix: Option) -> Self { + Self { + count: 0, + prefix: prefix + .map(|pre| pre.as_ref().to_string()) + .unwrap_or_default(), + } + } + + pub fn is_empty(&self) -> bool { + self.count == 0 + } + + pub fn get(&mut self, key: &str) -> ParseEnvResult { + self.get_and_parse(key, |value| parse_value(value).map(|v| Some(v))) + } + + pub fn get_and_parse( + &mut self, + key: &str, + parser: impl Fn(String) -> ParseEnvResult, + ) -> ParseEnvResult { + let key = format!("{}{key}", self.prefix); + + if let Ok(value) = std::env::var(&key) { + return parser(value) + .inspect(|inner| { + if inner.is_some() { + self.count += 1; + } + }) + .map_err(|error| { + HandlerError(format!("Invalid environment variable {key}: {error}")) + }); + } + + Ok(None) + } + + pub fn nested(&mut self, partial: Option) -> ParseEnvResult { + if partial.is_some() { + self.count += 1; + } + + Ok(partial) + } +} diff --git a/crates/schematic/src/internal/merge.rs b/crates/schematic/src/internal/merge.rs new file mode 100644 index 00000000..3d8f69aa --- /dev/null +++ b/crates/schematic/src/internal/merge.rs @@ -0,0 +1,47 @@ +use crate::config::{MergeError, MergeResult, PartialConfig}; + +pub struct MergeManager<'a, Ctx> { + context: &'a Ctx, +} + +impl<'a, Ctx> MergeManager<'a, Ctx> { + pub fn new(context: &'a Ctx) -> Self { + Self { context } + } + + pub fn apply(self, prev: &mut Option, next: Option) -> Result { + self.apply_with(prev, next, crate::merge::replace) + } + + pub fn apply_with( + self, + prev: &mut Option, + next: Option, + merger: impl Fn(T, T, &Ctx) -> MergeResult, + ) -> Result { + let value = match (prev.take(), next) { + (Some(prev), Some(next)) => merger(prev, next, self.context)?, + (None, Some(next)) => Some(next), + (other, None) => other, + }; + + if let Some(value) = value { + prev.replace(value); + } + + Ok(self) + } + + pub fn nested>( + self, + prev: &mut Option, + next: Option, + ) -> Result { + self.apply_with(prev, next, |mut p, n, ctx| { + p.merge(ctx, n) + .map_err(|error| MergeError(error.to_string()))?; + + Ok(Some(p)) + }) + } +} diff --git a/crates/schematic/src/internal.rs b/crates/schematic/src/internal/mod.rs similarity index 58% rename from crates/schematic/src/internal.rs rename to crates/schematic/src/internal/mod.rs index 1eb2484d..c24031a9 100644 --- a/crates/schematic/src/internal.rs +++ b/crates/schematic/src/internal/mod.rs @@ -1,6 +1,14 @@ -use crate::config::{ - ConfigError, HandlerError, MergeError, MergeResult, ParseEnvResult, PartialConfig, -}; +mod env; +mod merge; +#[cfg(feature = "validate")] +mod validate; + +pub use env::*; +pub use merge::*; +#[cfg(feature = "validate")] +pub use validate::*; + +use crate::config::{ConfigError, HandlerError, MergeError, MergeResult, PartialConfig}; use schematic_types::Schema; use std::str::FromStr; @@ -12,110 +20,6 @@ pub fn handle_default_result( result.map_err(|error| ConfigError::InvalidDefaultValue(error.to_string())) } -// ENV VARS - -pub struct EnvManager { - count: u8, - prefix: String, -} - -impl EnvManager { - pub fn new>(prefix: Option) -> Self { - Self { - count: 0, - prefix: prefix - .map(|pre| pre.as_ref().to_string()) - .unwrap_or_default(), - } - } - - pub fn is_empty(&self) -> bool { - self.count == 0 - } - - pub fn get(&mut self, key: &str) -> ParseEnvResult { - self.get_and_parse(key, |value| parse_value(value).map(|v| Some(v))) - } - - pub fn get_and_parse( - &mut self, - key: &str, - parser: impl Fn(String) -> ParseEnvResult, - ) -> ParseEnvResult { - let key = format!("{}{key}", self.prefix); - - if let Ok(value) = std::env::var(&key) { - return parser(value) - .inspect(|inner| { - if inner.is_some() { - self.count += 1; - } - }) - .map_err(|error| { - HandlerError(format!("Invalid environment variable {key}: {error}")) - }); - } - - Ok(None) - } - - pub fn nested(&mut self, partial: Option) -> ParseEnvResult { - if partial.is_some() { - self.count += 1; - } - - Ok(partial) - } -} - -// MERGING - -pub struct MergeManager<'a, Ctx> { - context: &'a Ctx, -} - -impl<'a, Ctx> MergeManager<'a, Ctx> { - pub fn new(context: &'a Ctx) -> Self { - Self { context } - } - - pub fn apply(self, prev: &mut Option, next: Option) -> Result { - self.apply_with(prev, next, super::merge::replace) - } - - pub fn apply_with( - self, - prev: &mut Option, - next: Option, - merger: impl Fn(T, T, &Ctx) -> MergeResult, - ) -> Result { - let value = match (prev.take(), next) { - (Some(prev), Some(next)) => merger(prev, next, self.context)?, - (None, Some(next)) => Some(next), - (other, None) => other, - }; - - if let Some(value) = value { - prev.replace(value); - } - - Ok(self) - } - - pub fn nested>( - self, - prev: &mut Option, - next: Option, - ) -> Result { - self.apply_with(prev, next, |mut p, n, ctx| { - p.merge(ctx, n) - .map_err(|error| MergeError(error.to_string()))?; - - Ok(Some(p)) - }) - } -} - // LEGACY #[cfg(feature = "env")] diff --git a/crates/schematic/src/internal/validate.rs b/crates/schematic/src/internal/validate.rs new file mode 100644 index 00000000..43252c78 --- /dev/null +++ b/crates/schematic/src/internal/validate.rs @@ -0,0 +1,71 @@ +use crate::config::{PartialConfig, Path, ValidateError, Validator}; + +pub struct ValidateManager<'a, Ctx> { + context: &'a Ctx, + finalize: bool, + path: Path, + + pub errors: Vec, +} + +impl<'a, Ctx> ValidateManager<'a, Ctx> { + pub fn new(context: &'a Ctx, finalize: bool, path: Path) -> Self { + Self { + context, + errors: vec![], + finalize, + path, + } + } + + pub fn check(&mut self, key: &str, value: &V, data: &D, validator: Validator) { + if let Err(error) = validator(value, data, self.context, self.finalize) { + self.errors + .push(error.prepend_path(self.path.join_key(key))); + } + } + + pub fn nested>(&mut self, key: &str, value: &S) { + if let Err(errors) = + value.validate_with_path(self.context, self.finalize, self.path.join_key(key)) + { + self.errors.extend(errors); + } + } + + pub fn nested_list<'v, I: IntoIterator, S: PartialConfig + 'v>( + &mut self, + key: &str, + list: I, + ) { + for (i, item) in list.into_iter().enumerate() { + if let Err(errors) = item.validate_with_path( + self.context, + self.finalize, + self.path.join_key(key).join_index(i), + ) { + self.errors.extend(errors); + } + } + } + + pub fn nested_map< + 'v, + I: IntoIterator, + S: PartialConfig + 'v, + >( + &mut self, + key: &str, + map: I, + ) { + for (sub_key, value) in map.into_iter() { + if let Err(errors) = value.validate_with_path( + self.context, + self.finalize, + self.path.join_key(key).join_key(sub_key), + ) { + self.errors.extend(errors); + } + } + } +} From aeccfba68ac1b96d0f21cdb31575fb544ade2b11 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Mon, 3 Nov 2025 11:54:22 -0800 Subject: [PATCH 28/33] Flesh out validate. --- crates/core/src/container.rs | 54 ++++++++---- crates/core/src/field.rs | 15 ++-- crates/core/src/field_value.rs | 52 ----------- crates/core/src/value.rs | 55 +++++++++++- crates/core/src/variant.rs | 62 ++++++++++++- crates/core/tests/setting_validate_test.rs | 88 +++++++++++++++++++ ...e__unnamed_enum__accepts_curried_func.snap | 20 ++++- ...idate__unnamed_enum__accepts_func_ref.snap | 20 ++++- ...e__unnamed_enum__supports_many_values.snap | 41 +++++++++ ...lidate__unnamed_enum__supports_nested.snap | 42 +++++++++ ...med_enum__supports_nested_collections.snap | 41 +++++++++ ...date__unnamed_enum__supports_standard.snap | 35 ++++++++ crates/schematic/src/internal/validate.rs | 7 ++ 13 files changed, 451 insertions(+), 81 deletions(-) create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 24612081..69b16327 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -81,7 +81,7 @@ impl Container { if all_unit { ContainerInner::UnitEnum { variants } } else { - ContainerInner::Enum { variants } + ContainerInner::UnnamedEnum { variants } } } Data::Union(_) => { @@ -134,7 +134,7 @@ impl Container { ContainerInner::UnnamedStruct { .. } => { meta.push(quote! { default }); } - ContainerInner::Enum { .. } => { + ContainerInner::UnnamedEnum { .. } => { if let Some(tag) = &self.serde_args.tag { meta.push(quote! { tag = #tag }); } @@ -287,7 +287,7 @@ impl Container { ))) } } - ContainerInner::Enum { variants } | ContainerInner::UnitEnum { variants } => { + ContainerInner::UnnamedEnum { variants } | ContainerInner::UnitEnum { variants } => { let default_variants = variants .iter() .filter(|v| v.is_default()) @@ -476,7 +476,7 @@ impl Container { } } } - ContainerInner::Enum { variants } | ContainerInner::UnitEnum { variants } => { + ContainerInner::UnnamedEnum { variants } | ContainerInner::UnitEnum { variants } => { let mut statements = vec![]; for variant in variants { @@ -523,10 +523,10 @@ impl Container { #[cfg(feature = "validate")] pub fn impl_partial_validate(&self) -> TokenStream { - let mut statements = vec![]; - - match &self.inner { + let inner = match &self.inner { ContainerInner::NamedStruct { fields } | ContainerInner::UnnamedStruct { fields } => { + let mut statements = vec![]; + for field in fields { let res = field.impl_partial_validate(); @@ -534,8 +534,18 @@ impl Container { statements.push(res.value); } } + + if statements.is_empty() { + return quote! {}; + } + + quote! { + #(#statements)* + } } - ContainerInner::Enum { variants } => { + ContainerInner::UnnamedEnum { variants } => { + let mut statements = vec![]; + for variant in variants { let res = variant.impl_partial_validate(); @@ -543,14 +553,23 @@ impl Container { statements.push(res.value); } } + + if statements.is_empty() { + return quote! {}; + } + + quote! { + match self { + #(#statements)* + _ => {} + }; + } + } + ContainerInner::UnitEnum { .. } => { + return quote! {}; } - ContainerInner::UnitEnum { .. } => {} }; - if statements.is_empty() { - return quote! {}; - } - let internal = ImplResult::impl_use_internal(true); quote! { @@ -563,7 +582,7 @@ impl Container { #internal let mut validate = ValidateManager::new(context, finalize, path); - #(#statements)* + #inner if !validate.errors.is_empty() { return Err(validate.errors); @@ -585,7 +604,8 @@ impl ToTokens for Container { pub enum ContainerInner { NamedStruct { fields: Vec }, UnnamedStruct { fields: Vec }, - Enum { variants: Vec }, + // TODO: NamedEnum + UnnamedEnum { variants: Vec }, UnitEnum { variants: Vec }, } @@ -601,7 +621,9 @@ impl ContainerInner { pub fn get_variants(&self) -> Vec<&Variant> { match self { - Self::Enum { variants } | Self::UnitEnum { variants } => variants.iter().collect(), + Self::UnnamedEnum { variants } | Self::UnitEnum { variants } => { + variants.iter().collect() + } _ => vec![], } } diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 70e0db52..12db52d9 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -6,7 +6,7 @@ use crate::field_value::FieldValue; use crate::utils::{ImplResult, preserve_str_literal}; use darling::FromAttributes; use proc_macro2::{Literal, TokenStream}; -use quote::{ToTokens, TokenStreamExt, quote}; +use quote::{ToTokens, TokenStreamExt, format_ident, quote}; use std::rc::Rc; use syn::{Attribute, Expr, ExprPath, Field as NativeField, FieldMutability, Ident, Visibility}; @@ -272,12 +272,17 @@ impl Field { pub fn impl_partial_validate(&self) -> ImplResult { let key = self.get_key(); + let key_string = key.to_string(); let res = self.value.impl_partial_validate(&self.args, &key); let mut inner = res.value; let mut has_inner = !res.no_value; if self.is_nested() { - let nested_value = self.value.impl_partial_validate_nested(&key).value; + let setting_var = format_ident!("setting"); + let nested_value = self + .value + .impl_partial_validate_nested(&key_string, &setting_var) + .value; has_inner = true; inner = quote! { @@ -302,10 +307,8 @@ impl Field { outer = quote! { #outer - if finalize && self.#key.is_none() { - errors.push(schematic::ValidateError::required().prepend_path( - path.join_key(#key) - )); + if self.#key.is_none() { + validate.required(#key); } }; } diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index 7f6e38f7..3e2bd1b4 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -39,10 +39,6 @@ impl FieldValue { let ident = format_ident!("Partial{}", nested_ident); - // quote! { - // <#nested_ident as schematic::PartialConfig>::default_values(context)? - // } - quote! { #ident::default_values(context)? } @@ -299,52 +295,4 @@ impl FieldValue { res } - - #[cfg(not(feature = "validate"))] - pub fn impl_partial_validate_nested(&self, _field_name: &TokenStream) -> ImplResult { - ImplResult::skipped() - } - - #[cfg(feature = "validate")] - pub fn impl_partial_validate_nested(&self, field_name: &TokenStream) -> ImplResult { - if self.layers.len() >= 2 - && self - .layers - .get(1) - .is_some_and(|layer| matches!(layer, Layer::Option)) - { - return ImplResult::skipped(); - } - - let field_name_string = field_name.to_string(); - let mut value = quote! { - validate.nested(#field_name_string, setting); - }; - - for layer in self.layers.iter().rev() { - match layer { - Layer::Arc | Layer::Box | Layer::Option | Layer::Rc => { - // Nothing? - } - Layer::Map(_) => { - value = quote! { - validate.nested_map(#field_name_string, setting.iter()); - }; - } - Layer::Set(_) | Layer::Vec(_) => { - value = quote! { - validate.nested_list(#field_name_string, setting.iter()); - }; - } - Layer::Unknown(_) => { - return ImplResult::skipped(); - } - }; - } - - ImplResult { - value, - ..Default::default() - } - } } diff --git a/crates/core/src/value.rs b/crates/core/src/value.rs index 0a244d32..ba875400 100644 --- a/crates/core/src/value.rs +++ b/crates/core/src/value.rs @@ -1,6 +1,6 @@ use crate::args::NestedArg; -use crate::utils::to_type_string; -use quote::ToTokens; +use crate::utils::{ImplResult, to_type_string}; +use quote::{ToTokens, quote}; use syn::{GenericArgument, Ident, PathArguments, PathSegment, Type}; #[derive(Debug, PartialEq)] @@ -98,6 +98,57 @@ impl Value { .first() .is_some_and(|layer| *layer == Layer::Option) } + + #[cfg(not(feature = "validate"))] + pub fn impl_partial_validate_nested( + &self, + _path_key: &str, + _setting_var: &Ident, + ) -> ImplResult { + ImplResult::skipped() + } + + #[cfg(feature = "validate")] + pub fn impl_partial_validate_nested(&self, path_key: &str, setting_var: &Ident) -> ImplResult { + if self.layers.len() >= 2 + && self + .layers + .get(1) + .is_some_and(|layer| matches!(layer, Layer::Option)) + { + return ImplResult::skipped(); + } + + let mut value = quote! { + validate.nested(#path_key, #setting_var); + }; + + for layer in self.layers.iter().rev() { + match layer { + Layer::Arc | Layer::Box | Layer::Option | Layer::Rc => { + // Nothing? + } + Layer::Map(_) => { + value = quote! { + validate.nested_map(#path_key, #setting_var.iter()); + }; + } + Layer::Set(_) | Layer::Vec(_) => { + value = quote! { + validate.nested_list(#path_key, #setting_var.iter()); + }; + } + Layer::Unknown(_) => { + return ImplResult::skipped(); + } + }; + } + + ImplResult { + value, + ..Default::default() + } + } } fn extract_type_information( diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index a4c91319..ec7f1c39 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -6,7 +6,7 @@ use darling::FromAttributes; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use std::rc::Rc; -use syn::{Attribute, ExprPath, Fields, FieldsUnnamed, Ident, Variant as NativeVariant}; +use syn::{Attribute, Expr, ExprPath, Fields, FieldsUnnamed, Ident, Variant as NativeVariant}; // #[setting()], #[schema()] #[derive(Debug, Default, FromAttributes)] @@ -285,8 +285,64 @@ impl Variant { } pub fn impl_partial_validate(&self) -> ImplResult { - // TODO - ImplResult::skipped() + let Fields::Unnamed(fields) = &self.fields else { + return ImplResult::skipped(); + }; + + let value = self.map_unnamed_match(&self.ident, fields, |outer_names, _| { + let mut statements = vec![]; + let name_string = self.ident.to_string(); + + #[cfg(feature = "validate")] + if let Some(expr) = self.args.validate.as_deref() { + let func = match expr { + // func(arg)() + Expr::Call(func) => quote! { #func }, + // func() + Expr::Path(func) => quote! { #func }, + _ => { + panic!("Unsupported `validate` syntax."); + } + }; + + statements.push(quote! { + validate.check(#name_string, (#(#outer_names),*), self, #func); + }); + } + + if self.is_required() { + statements.push(quote! { + if [#(#outer_names),*].iter().any(|v| v.is_none()) { + validate.required(#name_string); + } + }); + } + + if self.is_nested() { + statements.extend( + outer_names + .iter() + .enumerate() + .map(|(index, o)| { + let name_index = format!("{name_string}.{index}"); + + self.values[index] + .impl_partial_validate_nested(&name_index, o) + .value + }) + .collect::>(), + ); + } + + quote! { + #(#statements)* + } + }); + + ImplResult { + value, + ..Default::default() + } } fn map_unnamed_match(&self, name: &Ident, fields: &FieldsUnnamed, factory: F) -> TokenStream diff --git a/crates/core/tests/setting_validate_test.rs b/crates/core/tests/setting_validate_test.rs index a73e3c5f..44fdaff0 100644 --- a/crates/core/tests/setting_validate_test.rs +++ b/crates/core/tests/setting_validate_test.rs @@ -282,6 +282,94 @@ mod setting_validate { } }); } + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(validate = func_ref)] + A(bool), + #[setting(validate = func_ref)] + B(Option), + #[setting(validate = func_ref)] + C(Vec), + #[setting(validate = func_ref)] + D(Vec>), + #[setting(validate = func_ref)] + E(Option>>), + } + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested, validate = func_ref)] + A(NestedConfig), + #[setting(nested = CustomConfig, validate = func_ref)] + B(CustomConfig), + #[setting(nested, validate = func_ref)] + C(Option), + #[setting(nested = CustomConfig, validate = func_ref)] + D(Arc), + #[setting(nested)] + E(NestedConfig), + #[setting(nested = CustomConfig)] + F(CustomConfig), + } + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } + + #[test] + fn supports_nested_collections() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested, validate = func_ref)] + A(Vec), + #[setting(nested = CustomConfig, validate = func_ref)] + B(HashMap), + #[setting(nested, validate = func_ref)] + C(Option>), + #[setting(nested)] + D(Vec), + #[setting(nested = CustomConfig)] + E(HashMap), + #[setting(nested)] + F(Option>), + } + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } + + #[test] + fn supports_many_values() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(validate = func_ref)] + A(bool), + #[setting(validate = func_ref)] + B(bool, usize), + #[setting(validate = func_ref)] + C(bool, usize, String), + #[setting(required, validate = func_ref)] + A(Option), + #[setting(required, validate = func_ref)] + B(Option, Option), + } + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } } mod unit_enum { diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap index e813a8ae..0c9704a8 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap @@ -2,4 +2,22 @@ source: crates/core/tests/setting_validate_test.rs expression: pretty(container.impl_partial_validate()) --- - +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + match self { + Self::A(pa) => { + validate.check("A", (pa), self, func_call()); + } + _ => {} + }; + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap index e813a8ae..5477207f 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap @@ -2,4 +2,22 @@ source: crates/core/tests/setting_validate_test.rs expression: pretty(container.impl_partial_validate()) --- - +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + match self { + Self::A(pa) => { + validate.check("A", (pa), self, func_ref); + } + _ => {} + }; + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap new file mode 100644 index 00000000..8dfff373 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap @@ -0,0 +1,41 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + match self { + Self::A(pa) => { + validate.check("A", (pa), self, func_ref); + } + Self::B(pa, pb) => { + validate.check("B", (pa, pb), self, func_ref); + } + Self::C(pa, pb, pc) => { + validate.check("C", (pa, pb, pc), self, func_ref); + } + Self::A(pa) => { + validate.check("A", (pa), self, func_ref); + if [pa].iter().any(|v| v.is_none()) { + validate.required("A"); + } + } + Self::B(pa, pb) => { + validate.check("B", (pa, pb), self, func_ref); + if [pa, pb].iter().any(|v| v.is_none()) { + validate.required("B"); + } + } + _ => {} + }; + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap new file mode 100644 index 00000000..65fa06f0 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap @@ -0,0 +1,42 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + match self { + Self::A(pa) => { + validate.check("A", (pa), self, func_ref); + validate.nested("A.0", pa); + } + Self::B(pa) => { + validate.check("B", (pa), self, func_ref); + validate.nested("B.0", pa); + } + Self::C(pa) => { + validate.check("C", (pa), self, func_ref); + validate.nested("C.0", pa); + } + Self::D(pa) => { + validate.check("D", (pa), self, func_ref); + validate.nested("D.0", pa); + } + Self::E(pa) => { + validate.nested("E.0", pa); + } + Self::F(pa) => { + validate.nested("F.0", pa); + } + _ => {} + }; + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap new file mode 100644 index 00000000..39689c24 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap @@ -0,0 +1,41 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + match self { + Self::A(pa) => { + validate.check("A", (pa), self, func_ref); + validate.nested_list("A.0", pa.iter()); + } + Self::B(pa) => { + validate.check("B", (pa), self, func_ref); + validate.nested_map("B.0", pa.iter()); + } + Self::C(pa) => { + validate.check("C", (pa), self, func_ref); + validate.nested_list("C.0", pa.iter()); + } + Self::D(pa) => { + validate.nested_list("D.0", pa.iter()); + } + Self::E(pa) => { + validate.nested_map("E.0", pa.iter()); + } + Self::F(pa) => { + validate.nested_list("F.0", pa.iter()); + } + _ => {} + }; + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap new file mode 100644 index 00000000..a3ccbdc1 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap @@ -0,0 +1,35 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalize: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalize, path); + match self { + Self::A(pa) => { + validate.check("A", (pa), self, func_ref); + } + Self::B(pa) => { + validate.check("B", (pa), self, func_ref); + } + Self::C(pa) => { + validate.check("C", (pa), self, func_ref); + } + Self::D(pa) => { + validate.check("D", (pa), self, func_ref); + } + Self::E(pa) => { + validate.check("E", (pa), self, func_ref); + } + _ => {} + }; + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} diff --git a/crates/schematic/src/internal/validate.rs b/crates/schematic/src/internal/validate.rs index 43252c78..1cb55d7a 100644 --- a/crates/schematic/src/internal/validate.rs +++ b/crates/schematic/src/internal/validate.rs @@ -25,6 +25,13 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { } } + pub fn required(&mut self, key: &str) { + if self.finalize { + self.errors + .push(ValidateError::required().prepend_path(self.path.join_key(key))); + } + } + pub fn nested>(&mut self, key: &str, value: &S) { if let Err(errors) = value.validate_with_path(self.context, self.finalize, self.path.join_key(key)) From efda7ab798e25584fe0aeeb14d1df341a1337adc Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sat, 22 Nov 2025 12:06:32 -0800 Subject: [PATCH 29/33] Rename param. --- crates/core/src/container.rs | 4 ++-- crates/core/src/field.rs | 2 +- crates/core/tests/setting_nested_test.rs | 2 +- ...date__named_struct__accepts_curried_func.snap | 4 ++-- ...validate__named_struct__accepts_func_ref.snap | 4 ++-- ..._validate__named_struct__supports_nested.snap | 4 ++-- ...amed_struct__supports_nested_collections.snap | 4 ++-- ...alidate__named_struct__supports_standard.snap | 4 ++-- ...date__unnamed_enum__accepts_curried_func.snap | 4 ++-- ...validate__unnamed_enum__accepts_func_ref.snap | 4 ++-- ...date__unnamed_enum__supports_many_values.snap | 4 ++-- ..._validate__unnamed_enum__supports_nested.snap | 4 ++-- ...nnamed_enum__supports_nested_collections.snap | 4 ++-- ...alidate__unnamed_enum__supports_standard.snap | 4 ++-- ...te__unnamed_struct__accepts_curried_func.snap | 4 ++-- ...lidate__unnamed_struct__accepts_func_ref.snap | 4 ++-- ...alidate__unnamed_struct__supports_nested.snap | 4 ++-- ...amed_struct__supports_nested_collections.snap | 4 ++-- ...idate__unnamed_struct__supports_standard.snap | 4 ++-- crates/schematic/src/config/configs.rs | 6 +++--- crates/schematic/src/internal/validate.rs | 16 ++++++++-------- 21 files changed, 47 insertions(+), 47 deletions(-) diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 69b16327..99a09ed1 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -576,12 +576,12 @@ impl Container { fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path ) -> std::result::Result<(), Vec> { #internal - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); #inner if !validate.errors.is_empty() { diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 12db52d9..343ee954 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -10,7 +10,7 @@ use quote::{ToTokens, TokenStreamExt, format_ident, quote}; use std::rc::Rc; use syn::{Attribute, Expr, ExprPath, Field as NativeField, FieldMutability, Ident, Visibility}; -// TODO: finalize, validate +// TODO: finalize // #[schema()], #[setting()] #[derive(Debug, FromAttributes, Default)] diff --git a/crates/core/tests/setting_nested_test.rs b/crates/core/tests/setting_nested_test.rs index a7b113f6..5b745a62 100644 --- a/crates/core/tests/setting_nested_test.rs +++ b/crates/core/tests/setting_nested_test.rs @@ -296,7 +296,7 @@ mod setting_nested { } mod named_enum { - // TODO + // N/A } mod unnamed_enum { diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_curried_func.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_curried_func.snap index cabfd6d3..59eba0d9 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_curried_func.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_curried_func.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.a { validate.check("a", setting, self, func_call()); } diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap index 235fc7e5..6f8d0a01 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.a { validate.check("a", setting, self, func_ref); } diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap index 3792ba2d..9e94e211 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.a { validate.check("a", setting, self, func_ref); validate.nested("a", setting); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap index 1e47af19..73fefeb3 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.a { validate.check("a", setting, self, func_ref); validate.nested_list("a", setting.iter()); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap index d93662c7..66f92821 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.a { validate.check("a", setting, self, func_ref); } diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap index 0c9704a8..b42e0daa 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { validate.check("A", (pa), self, func_call()); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap index 5477207f..6cfbce8c 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { validate.check("A", (pa), self, func_ref); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap index 8dfff373..b4465be0 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { validate.check("A", (pa), self, func_ref); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap index 65fa06f0..720b8137 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { validate.check("A", (pa), self, func_ref); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap index 39689c24..f1a42013 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { validate.check("A", (pa), self, func_ref); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap index a3ccbdc1..f3d1822b 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { validate.check("A", (pa), self, func_ref); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_curried_func.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_curried_func.snap index 77a2f716..3f32edc4 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_curried_func.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_curried_func.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.0 { validate.check("0", setting, self, func_call()); } diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap index f0f7f22d..d19eb7c1 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.0 { validate.check("0", setting, self, func_ref); } diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap index 42370059..600c4718 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.0 { validate.check("0", setting, self, func_ref); validate.nested("0", setting); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap index 6c6f7e62..84243014 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.0 { validate.check("0", setting, self, func_ref); validate.nested_list("0", setting.iter()); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap index 7e53810c..409beb19 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap @@ -5,11 +5,11 @@ expression: pretty(container.impl_partial_validate()) fn validate_with_path( &self, context: &Self::Context, - finalize: bool, + finalizing: bool, path: schematic::Path, ) -> std::result::Result<(), Vec> { use schematic::internal::*; - let mut validate = ValidateManager::new(context, finalize, path); + let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.0 { validate.check("0", setting, self, func_ref); } diff --git a/crates/schematic/src/config/configs.rs b/crates/schematic/src/config/configs.rs index 3eb93fbb..3e54c4d6 100644 --- a/crates/schematic/src/config/configs.rs +++ b/crates/schematic/src/config/configs.rs @@ -68,9 +68,9 @@ pub trait PartialConfig: /// Recursively validate the configuration with the provided context. /// Validation should be done on the final state, after merging partials. #[cfg(feature = "validate")] - fn validate(&self, context: &Self::Context, finalize: bool) -> Result<(), ConfigError> { + fn validate(&self, context: &Self::Context, finalizing: bool) -> Result<(), ConfigError> { if let Err(errors) = - self.validate_with_path(context, finalize, super::path::Path::default()) + self.validate_with_path(context, finalizing, super::path::Path::default()) { return Err(ConfigError::Validator { location: String::new(), @@ -88,7 +88,7 @@ pub trait PartialConfig: fn validate_with_path( &self, _context: &Self::Context, - _finalize: bool, + _finalizing: bool, _path: super::path::Path, ) -> Result<(), Vec> { Ok(()) diff --git a/crates/schematic/src/internal/validate.rs b/crates/schematic/src/internal/validate.rs index 1cb55d7a..bc1774c7 100644 --- a/crates/schematic/src/internal/validate.rs +++ b/crates/schematic/src/internal/validate.rs @@ -2,31 +2,31 @@ use crate::config::{PartialConfig, Path, ValidateError, Validator}; pub struct ValidateManager<'a, Ctx> { context: &'a Ctx, - finalize: bool, + finalizing: bool, path: Path, pub errors: Vec, } impl<'a, Ctx> ValidateManager<'a, Ctx> { - pub fn new(context: &'a Ctx, finalize: bool, path: Path) -> Self { + pub fn new(context: &'a Ctx, finalizing: bool, path: Path) -> Self { Self { context, errors: vec![], - finalize, + finalizing, path, } } pub fn check(&mut self, key: &str, value: &V, data: &D, validator: Validator) { - if let Err(error) = validator(value, data, self.context, self.finalize) { + if let Err(error) = validator(value, data, self.context, self.finalizing) { self.errors .push(error.prepend_path(self.path.join_key(key))); } } pub fn required(&mut self, key: &str) { - if self.finalize { + if self.finalizing { self.errors .push(ValidateError::required().prepend_path(self.path.join_key(key))); } @@ -34,7 +34,7 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { pub fn nested>(&mut self, key: &str, value: &S) { if let Err(errors) = - value.validate_with_path(self.context, self.finalize, self.path.join_key(key)) + value.validate_with_path(self.context, self.finalizing, self.path.join_key(key)) { self.errors.extend(errors); } @@ -48,7 +48,7 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { for (i, item) in list.into_iter().enumerate() { if let Err(errors) = item.validate_with_path( self.context, - self.finalize, + self.finalizing, self.path.join_key(key).join_index(i), ) { self.errors.extend(errors); @@ -68,7 +68,7 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { for (sub_key, value) in map.into_iter() { if let Err(errors) = value.validate_with_path( self.context, - self.finalize, + self.finalizing, self.path.join_key(key).join_key(sub_key), ) { self.errors.extend(errors); From 7cf5004afb8b65282700fc7da8725e538842bfba Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sat, 22 Nov 2025 13:34:20 -0800 Subject: [PATCH 30/33] Add finalize. --- CHANGELOG.md | 7 +- crates/core/src/container.rs | 74 +++++++ crates/core/src/field.rs | 31 ++- crates/core/src/value.rs | 71 ++++++- crates/core/src/variant.rs | 44 ++++ crates/core/tests/container_finalize_test.rs | 198 ++++++++++++++++++ ...nalize__named_struct__supports_nested.snap | 35 ++++ ...d_struct__supports_nested_collections.snap | 58 +++++ ...lize__named_struct__supports_standard.snap | 24 +++ ...nalize__unnamed_enum__supports_nested.snap | 27 +++ ...med_enum__supports_nested_collections.snap | 54 +++++ ...lize__unnamed_enum__supports_standard.snap | 16 ++ ...lize__unnamed_struct__supports_nested.snap | 35 ++++ ...d_struct__supports_nested_collections.snap | 58 +++++ ...ze__unnamed_struct__supports_standard.snap | 24 +++ 15 files changed, 750 insertions(+), 6 deletions(-) create mode 100644 crates/core/tests/container_finalize_test.rs create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_standard.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_nested.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_standard.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_standard.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index b7021eb3..3a6f777c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,10 @@ - Added support for unnamed tuple and newtype structs. Unnamed fields within the struct support `#[setting]`. -- Added support for `#[setting(nested = NestedConfig)]` on fields, where the nested config name can - be explicitly defined if we fail to detect it. This is useful for extremely complex/composed - types. +- Added support for `#[setting(nested = NestedConfig)]` on struct fields and enum variants, where + the nested config name can be explicitly defined if we fail to detect it. This is useful for + extremely complex/composed types. +- Added support for `#[setting(transform)]` on enum variants. - Added support for env prefixes at the field level when the field is also nested. This will override the env prefix defined on the nested container: `#[setting(nested, env_prefix = "OVERRIDE_")]`. diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 99a09ed1..6748a2bf 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -184,6 +184,7 @@ impl Container { let default_values_method = self.impl_partial_default_values(); let env_values_method = self.impl_partial_env_values(); let extends_from_method = self.impl_partial_extends_from(); + let finalize_method = self.impl_partial_finalize(); let merge_method = self.impl_partial_merge(); let validate_method = self.impl_partial_validate(); @@ -195,6 +196,7 @@ impl Container { #default_values_method #env_values_method #extends_from_method + #finalize_method #merge_method #validate_method } @@ -442,6 +444,78 @@ impl Container { } } + pub fn impl_partial_finalize(&self) -> TokenStream { + let inner = match &self.inner { + ContainerInner::NamedStruct { fields } | ContainerInner::UnnamedStruct { fields } => { + let mut statements = vec![]; + + #[cfg(feature = "env")] + { + statements.push(quote! { + if let Some(layer) = Self::env_values()? { + partial.merge(context, layer)?; + } + }); + } + + for field in fields { + let res = field.impl_partial_finalize(); + + if !res.no_value { + statements.push(res.value); + } + } + + quote! { + let mut partial = Self::default(); + + if let Some(layer) = Self::default_values(context)? { + partial.merge(context, layer)?; + } + + partial.merge(context, self)?; + + #(#statements)* + + Ok(partial) + } + } + ContainerInner::UnnamedEnum { variants } => { + let mut statements = vec![]; + + for variant in variants { + let res = variant.impl_partial_finalize(); + + if !res.no_value { + statements.push(res.value); + } + } + + if statements.is_empty() { + quote! { + Ok(self) + } + } else { + quote! { + Ok(match self { + #(#statements)* + _ => self + }) + } + } + } + ContainerInner::UnitEnum { .. } => { + return quote! {}; + } + }; + + quote! { + fn finalize(self, context: &Self::Context) -> std::result::Result { + #inner + } + } + } + pub fn impl_partial_merge(&self) -> TokenStream { match &self.inner { ContainerInner::NamedStruct { fields } | ContainerInner::UnnamedStruct { fields } => { diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 343ee954..a77fc522 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -10,8 +10,6 @@ use quote::{ToTokens, TokenStreamExt, format_ident, quote}; use std::rc::Rc; use syn::{Attribute, Expr, ExprPath, Field as NativeField, FieldMutability, Ident, Visibility}; -// TODO: finalize - // #[schema()], #[setting()] #[derive(Debug, FromAttributes, Default)] #[darling(default, attributes(schema, setting))] @@ -266,6 +264,35 @@ impl Field { } } + pub fn impl_partial_finalize(&self) -> ImplResult { + if !self.is_nested() && self.args.transform.is_none() { + return ImplResult::skipped(); + } + + let key = self.get_key(); + + let mut value = if self.is_nested() { + self.value + .impl_partial_finalize_nested(&format_ident!("layer")) + .value + } else { + quote! { layer } + }; + + if let Some(func) = &self.args.transform { + value = quote! { #func(#value, context)? }; + }; + + ImplResult { + value: quote! { + if let Some(layer) = partial.#key { + partial.#key = Some(#value); + } + }, + ..Default::default() + } + } + pub fn impl_partial_merge(&self) -> ImplResult { self.value.impl_partial_merge(&self.args, &self.get_key()) } diff --git a/crates/core/src/value.rs b/crates/core/src/value.rs index ba875400..2019ce3c 100644 --- a/crates/core/src/value.rs +++ b/crates/core/src/value.rs @@ -1,6 +1,6 @@ use crate::args::NestedArg; use crate::utils::{ImplResult, to_type_string}; -use quote::{ToTokens, quote}; +use quote::{ToTokens, format_ident, quote}; use syn::{GenericArgument, Ident, PathArguments, PathSegment, Type}; #[derive(Debug, PartialEq)] @@ -99,6 +99,75 @@ impl Value { .is_some_and(|layer| *layer == Layer::Option) } + pub fn impl_partial_finalize_nested(&self, layer_var: &Ident) -> ImplResult { + let mut res = ImplResult::default(); + let mut value = quote! { #layer_var.finalize(context)? }; + + // Then wrap with each layer + if !self.layers.is_empty() { + for layer in self.layers.iter().rev() { + value = match layer { + Layer::Arc => quote! { Arc::new(#value) }, + Layer::Rc => quote! { Rc::new(#value) }, + Layer::Box => quote! { Box::new(#value) }, + Layer::Option => quote! { + match #layer_var { + Some(#layer_var) => #value, + None => None + } + }, + Layer::Map(name) => { + let collection = format_ident!("{name}"); + + quote! { + { + let mut map = #collection::default(); + for (key, value) in #layer_var { + map.insert(key, value.finalize(context)?); + } + map + } + } + } + Layer::Set(name) => { + let collection = format_ident!("{name}"); + + quote! { + { + let mut set = #collection::default(); + for item in #layer_var { + set.insert(item.finalize(context)?); + } + set + } + } + } + Layer::Vec(name) => { + let collection = format_ident!("{name}"); + + quote! { + { + let mut list = #collection::default(); + for item in #layer_var { + list.push(item.finalize(context)?); + } + list + } + } + } + Layer::Unknown(name) => { + let collection = format_ident!("{name}"); + + quote! { #collection::default() } + } + }; + } + } + + res.value = value; + res + } + #[cfg(not(feature = "validate"))] pub fn impl_partial_validate_nested( &self, diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index ec7f1c39..fee73109 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -20,6 +20,7 @@ pub struct VariantArgs { pub null: bool, pub partial: Option, pub required: bool, + pub transform: Option, #[cfg(feature = "validate")] pub validate: Option, @@ -176,6 +177,49 @@ impl Variant { res } + pub fn impl_partial_finalize(&self) -> ImplResult { + let mut res = ImplResult::default(); + + match &self.fields { + Fields::Named(_) | Fields::Unit => { + res.no_value = true; + } + Fields::Unnamed(fields) => { + if !self.is_nested() && self.args.transform.is_none() { + res.no_value = true; + } else { + let name = &self.ident; + + res.value = self.map_unnamed_match(name, fields, |outer_names, _| { + let items = outer_names + .iter() + .enumerate() + .map(|(i, o)| { + let value = if self.is_nested() { + self.values[i].impl_partial_finalize_nested(o).value + } else { + quote! { #o } + }; + + if let Some(func) = &self.args.transform { + quote! { #func(#value, context)? } + } else { + value + } + }) + .collect::>(); + + quote! { + Self::#name(#(#items),*) + } + }); + } + } + } + + res + } + pub fn impl_partial_merge(&self) -> ImplResult { let mut res = ImplResult::default(); diff --git a/crates/core/tests/container_finalize_test.rs b/crates/core/tests/container_finalize_test.rs new file mode 100644 index 00000000..a2994db3 --- /dev/null +++ b/crates/core/tests/container_finalize_test.rs @@ -0,0 +1,198 @@ +mod utils; + +use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; +use utils::pretty; + +mod container_finalize { + use super::*; + + mod named_struct { + use super::*; + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: bool, + b: usize, + #[setting(transform = transform_string)] + c: String, + d: i16, + e: Option, + #[setting(transform = "transform_vec")] + f: Vec, + g: Option>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_finalize())); + } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: NestedConfig, + #[setting(nested = CustomConfig, transform = transform_config)] + b: CustomConfig, + #[setting(nested)] + c: Option, + #[setting(nested = CustomConfig, transform = "transform_config")] + d: Arc, + } + }); + + assert_snapshot!(pretty(container.impl_partial_finalize())); + } + + #[test] + fn supports_nested_collections() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: Vec, + #[setting(nested = CustomConfig, transform = transform_config)] + b: HashMap, + #[setting(nested, transform = transform_config)] + c: Option>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_finalize())); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + bool, + usize, + #[setting(transform = transform_string)] + String, + i16, + Option, + #[setting(transform = "transform_vec")] + Vec, + Option>, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_finalize())); + } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested)] + NestedConfig, + #[setting(nested = CustomConfig, transform = transform_config)] + CustomConfig, + #[setting(nested)] + Option, + #[setting(nested = CustomConfig, transform = "transform_config")] + Arc, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_finalize())); + } + + #[test] + fn supports_nested_collections() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested)] + Vec, + #[setting(nested = CustomConfig, transform = transform_config)] + HashMap, + #[setting(nested, transform = transform_config)] + Option>, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_finalize())); + } + } + + mod named_enum { + // N/A + } + + mod unnamed_enum { + use super::*; + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A(bool), + B(usize), + #[setting(transform = transform_string)] + C(String), + D(i16), + E(Option), + #[setting(transform = "transform_vec")] + F(Vec), + G(Option>), + } + }); + + assert_snapshot!(pretty(container.impl_partial_finalize())); + } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A(NestedConfig), + #[setting(nested = CustomConfig, transform = transform_config)] + B(CustomConfig), + #[setting(nested)] + C(Option), + #[setting(nested = CustomConfig, transform = "transform_config")] + D(Arc), + } + }); + + assert_snapshot!(pretty(container.impl_partial_finalize())); + } + + #[test] + fn supports_nested_collections() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A(Vec), + #[setting(nested = CustomConfig, transform = transform_config)] + B(HashMap), + #[setting(nested, transform = transform_config)] + C(Option>), + } + }); + + assert_snapshot!(pretty(container.impl_partial_finalize())); + } + } + + mod unit_enum { + // N/A + } +} diff --git a/crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_nested.snap b/crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_nested.snap new file mode 100644 index 00000000..b6187dc8 --- /dev/null +++ b/crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_nested.snap @@ -0,0 +1,35 @@ +--- +source: crates/core/tests/container_finalize_test.rs +expression: pretty(container.impl_partial_finalize()) +--- +fn finalize( + self, + context: &Self::Context, +) -> std::result::Result { + let mut partial = Self::default(); + if let Some(layer) = Self::default_values(context)? { + partial.merge(context, layer)?; + } + partial.merge(context, self)?; + if let Some(layer) = Self::env_values()? { + partial.merge(context, layer)?; + } + if let Some(layer) = partial.a { + partial.a = Some(layer.finalize(context)?); + } + if let Some(layer) = partial.b { + partial.b = Some(transform_config(layer.finalize(context)?, context)?); + } + if let Some(layer) = partial.c { + partial.c = Some( + match layer { + Some(layer) => layer.finalize(context)?, + None => None, + }, + ); + } + if let Some(layer) = partial.d { + partial.d = Some(transform_config(Arc::new(layer.finalize(context)?), context)?); + } + Ok(partial) +} diff --git a/crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_nested_collections.snap new file mode 100644 index 00000000..8716e63e --- /dev/null +++ b/crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_nested_collections.snap @@ -0,0 +1,58 @@ +--- +source: crates/core/tests/container_finalize_test.rs +expression: pretty(container.impl_partial_finalize()) +--- +fn finalize( + self, + context: &Self::Context, +) -> std::result::Result { + let mut partial = Self::default(); + if let Some(layer) = Self::default_values(context)? { + partial.merge(context, layer)?; + } + partial.merge(context, self)?; + if let Some(layer) = Self::env_values()? { + partial.merge(context, layer)?; + } + if let Some(layer) = partial.a { + partial.a = Some({ + let mut list = Vec::default(); + for item in layer { + list.push(item.finalize(context)?); + } + list + }); + } + if let Some(layer) = partial.b { + partial.b = Some( + transform_config( + { + let mut map = HashMap::default(); + for (key, value) in layer { + map.insert(key, value.finalize(context)?); + } + map + }, + context, + )?, + ); + } + if let Some(layer) = partial.c { + partial.c = Some( + transform_config( + match layer { + Some(layer) => { + let mut set = BTreeSet::default(); + for item in layer { + set.insert(item.finalize(context)?); + } + set + } + None => None, + }, + context, + )?, + ); + } + Ok(partial) +} diff --git a/crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_standard.snap b/crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_standard.snap new file mode 100644 index 00000000..60492ed3 --- /dev/null +++ b/crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_standard.snap @@ -0,0 +1,24 @@ +--- +source: crates/core/tests/container_finalize_test.rs +expression: pretty(container.impl_partial_finalize()) +--- +fn finalize( + self, + context: &Self::Context, +) -> std::result::Result { + let mut partial = Self::default(); + if let Some(layer) = Self::default_values(context)? { + partial.merge(context, layer)?; + } + partial.merge(context, self)?; + if let Some(layer) = Self::env_values()? { + partial.merge(context, layer)?; + } + if let Some(layer) = partial.c { + partial.c = Some(transform_string(layer, context)?); + } + if let Some(layer) = partial.f { + partial.f = Some(transform_vec(layer, context)?); + } + Ok(partial) +} diff --git a/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_nested.snap b/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_nested.snap new file mode 100644 index 00000000..91411930 --- /dev/null +++ b/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_nested.snap @@ -0,0 +1,27 @@ +--- +source: crates/core/tests/container_finalize_test.rs +expression: pretty(container.impl_partial_finalize()) +--- +fn finalize( + self, + context: &Self::Context, +) -> std::result::Result { + Ok( + match self { + Self::A(pa) => Self::A(pa.finalize(context)?), + Self::B(pa) => Self::B(transform_config(pa.finalize(context)?, context)?), + Self::C(pa) => { + Self::C( + match pa { + Some(pa) => pa.finalize(context)?, + None => None, + }, + ) + } + Self::D(pa) => { + Self::D(transform_config(Arc::new(pa.finalize(context)?), context)?) + } + _ => self, + }, + ) +} diff --git a/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_nested_collections.snap b/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_nested_collections.snap new file mode 100644 index 00000000..75fc0b7e --- /dev/null +++ b/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_nested_collections.snap @@ -0,0 +1,54 @@ +--- +source: crates/core/tests/container_finalize_test.rs +expression: pretty(container.impl_partial_finalize()) +--- +fn finalize( + self, + context: &Self::Context, +) -> std::result::Result { + Ok( + match self { + Self::A(pa) => { + Self::A({ + let mut list = Vec::default(); + for item in pa { + list.push(item.finalize(context)?); + } + list + }) + } + Self::B(pa) => { + Self::B( + transform_config( + { + let mut map = HashMap::default(); + for (key, value) in pa { + map.insert(key, value.finalize(context)?); + } + map + }, + context, + )?, + ) + } + Self::C(pa) => { + Self::C( + transform_config( + match pa { + Some(pa) => { + let mut set = BTreeSet::default(); + for item in pa { + set.insert(item.finalize(context)?); + } + set + } + None => None, + }, + context, + )?, + ) + } + _ => self, + }, + ) +} diff --git a/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_standard.snap b/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_standard.snap new file mode 100644 index 00000000..adf03be2 --- /dev/null +++ b/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_standard.snap @@ -0,0 +1,16 @@ +--- +source: crates/core/tests/container_finalize_test.rs +expression: pretty(container.impl_partial_finalize()) +--- +fn finalize( + self, + context: &Self::Context, +) -> std::result::Result { + Ok( + match self { + Self::C(pa) => Self::C(transform_string(pa, context)?), + Self::F(pa) => Self::F(transform_vec(pa, context)?), + _ => self, + }, + ) +} diff --git a/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_nested.snap new file mode 100644 index 00000000..a4864557 --- /dev/null +++ b/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_nested.snap @@ -0,0 +1,35 @@ +--- +source: crates/core/tests/container_finalize_test.rs +expression: pretty(container.impl_partial_finalize()) +--- +fn finalize( + self, + context: &Self::Context, +) -> std::result::Result { + let mut partial = Self::default(); + if let Some(layer) = Self::default_values(context)? { + partial.merge(context, layer)?; + } + partial.merge(context, self)?; + if let Some(layer) = Self::env_values()? { + partial.merge(context, layer)?; + } + if let Some(layer) = partial.0 { + partial.0 = Some(layer.finalize(context)?); + } + if let Some(layer) = partial.1 { + partial.1 = Some(transform_config(layer.finalize(context)?, context)?); + } + if let Some(layer) = partial.2 { + partial.2 = Some( + match layer { + Some(layer) => layer.finalize(context)?, + None => None, + }, + ); + } + if let Some(layer) = partial.3 { + partial.3 = Some(transform_config(Arc::new(layer.finalize(context)?), context)?); + } + Ok(partial) +} diff --git a/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_nested_collections.snap new file mode 100644 index 00000000..117adfb0 --- /dev/null +++ b/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_nested_collections.snap @@ -0,0 +1,58 @@ +--- +source: crates/core/tests/container_finalize_test.rs +expression: pretty(container.impl_partial_finalize()) +--- +fn finalize( + self, + context: &Self::Context, +) -> std::result::Result { + let mut partial = Self::default(); + if let Some(layer) = Self::default_values(context)? { + partial.merge(context, layer)?; + } + partial.merge(context, self)?; + if let Some(layer) = Self::env_values()? { + partial.merge(context, layer)?; + } + if let Some(layer) = partial.0 { + partial.0 = Some({ + let mut list = Vec::default(); + for item in layer { + list.push(item.finalize(context)?); + } + list + }); + } + if let Some(layer) = partial.1 { + partial.1 = Some( + transform_config( + { + let mut map = HashMap::default(); + for (key, value) in layer { + map.insert(key, value.finalize(context)?); + } + map + }, + context, + )?, + ); + } + if let Some(layer) = partial.2 { + partial.2 = Some( + transform_config( + match layer { + Some(layer) => { + let mut set = BTreeSet::default(); + for item in layer { + set.insert(item.finalize(context)?); + } + set + } + None => None, + }, + context, + )?, + ); + } + Ok(partial) +} diff --git a/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_standard.snap b/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_standard.snap new file mode 100644 index 00000000..6af5b957 --- /dev/null +++ b/crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_standard.snap @@ -0,0 +1,24 @@ +--- +source: crates/core/tests/container_finalize_test.rs +expression: pretty(container.impl_partial_finalize()) +--- +fn finalize( + self, + context: &Self::Context, +) -> std::result::Result { + let mut partial = Self::default(); + if let Some(layer) = Self::default_values(context)? { + partial.merge(context, layer)?; + } + partial.merge(context, self)?; + if let Some(layer) = Self::env_values()? { + partial.merge(context, layer)?; + } + if let Some(layer) = partial.2 { + partial.2 = Some(transform_string(layer, context)?); + } + if let Some(layer) = partial.5 { + partial.5 = Some(transform_vec(layer, context)?); + } + Ok(partial) +} From bddc68ab641db1e19774fc379718065957519fef Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Wed, 26 Nov 2025 15:23:56 -0800 Subject: [PATCH 31/33] Update deps. --- Cargo.toml | 4 ++-- crates/core/src/variant.rs | 1 + crates/macros/Cargo.toml | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aef46406..3c37302a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,8 @@ convert_case = "0.10.0" darling = "0.23.0" indexmap = "2.13.0" miette = "7.6.0" -proc-macro2 = "1.0.95" -quote = "1.0.40" +proc-macro2 = "1.0.103" +quote = "1.0.42" regex = "1.12.2" relative-path = "2.0.1" reqwest = { version = "0.13.1", default-features = false } diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index fee73109..b5c964bf 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -99,6 +99,7 @@ impl Variant { panic!("Cannot use `required` with non-optional settings."); } + #[allow(clippy::collapsible_else_if)] if self.is_unit_variant() { if self.args.merge.is_some() { panic!("Cannot use `merge` with unit variants."); diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index cc9de613..4cc2fd35 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -15,10 +15,10 @@ proc-macro = true [dependencies] convert_case = { workspace = true } -darling = "0.23.0" -proc-macro2 = "1.0.105" -quote = "1.0.43" -syn = { version = "2.0.114", features = ["full"] } +darling = { workspace = true } +proc-macro2 = { workspace = true } +quote = { workspace = true } +syn = { workspace = true, features = ["full"] } [features] default = [] From 0587ca3c9853393321613f8d4f0a53b5e940b90b Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 11 Jan 2026 20:05:31 -0800 Subject: [PATCH 32/33] Start on full. --- crates/core/src/container.rs | 78 +++++++++++++++++++++----- crates/schematic/src/config/configs.rs | 2 +- 2 files changed, 65 insertions(+), 15 deletions(-) diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 6748a2bf..3860de9c 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -173,6 +173,70 @@ impl Container { } } + pub fn impl_full(&self) -> TokenStream { + let base_name = &self.ident; + let partial_name = format_ident!("Partial{base_name}"); + + let from_partial_method = self.impl_full_from_partial(); + let settings_method = self.impl_full_settings(); + + quote! { + #[automatically_derived] + impl schematic::Config for #base_name { + type Partial = #partial_name; + + #from_partial_method + #settings_method + } + + #[automatically_derived] + impl Default for #base_name { + fn default() -> Self { + ::from_partial( + ::default_partial() + ) + } + } + } + } + + // TODO + pub fn impl_full_from_partial(&self) -> TokenStream { + let inner = match &self.inner { + ContainerInner::NamedStruct { fields } => { + let mut statements = vec![]; + + for field in fields { + let res = field.impl_partial_merge(); + + if !res.no_value { + statements.push(res.value); + } + } + + quote! { + Self { + #(#setting_names: #from_partial_values),* + } + } + } + ContainerInner::UnnamedStruct { fields } => {} + ContainerInner::UnnamedEnum { variants } => {} + ContainerInner::UnitEnum { variants } => todo!(), + }; + + quote! { + fn from_partial(partial: Self::Partial) -> Self {} + } + } + + // TODO + pub fn impl_full_settings(&self) -> TokenStream { + quote! { + fn settings() -> schematic::ConfigSettingMap {} + } + } + pub fn impl_partial(&self) -> TokenStream { let base_name = &self.ident; let partial_name = format_ident!("Partial{base_name}"); @@ -200,20 +264,6 @@ impl Container { #merge_method #validate_method } - - #[automatically_derived] - impl schematic::Config for #base_name { - // TODO - } - - #[automatically_derived] - impl Default for #base_name { - fn default() -> Self { - ::from_partial( - ::default_partial() - ) - } - } } } diff --git a/crates/schematic/src/config/configs.rs b/crates/schematic/src/config/configs.rs index 3e54c4d6..60a9ca41 100644 --- a/crates/schematic/src/config/configs.rs +++ b/crates/schematic/src/config/configs.rs @@ -104,7 +104,7 @@ pub trait Config: Sized + Schematic { let context = <::Partial as PartialConfig>::Context::default(); <::Partial as PartialConfig>::default_values(&context) - .unwrap() + .unwrap_or_default() .unwrap_or_default() } From ed33b08eda1698c6a9fb38a0ce1dee2636be7485 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sat, 16 May 2026 17:00:21 -0700 Subject: [PATCH 33/33] Bump rust. --- crates/core/src/container.rs | 40 ++++++++++++++++-------------------- rust-toolchain.toml | 2 +- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 3860de9c..50ec0c6f 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -202,28 +202,24 @@ impl Container { // TODO pub fn impl_full_from_partial(&self) -> TokenStream { - let inner = match &self.inner { - ContainerInner::NamedStruct { fields } => { - let mut statements = vec![]; - - for field in fields { - let res = field.impl_partial_merge(); - - if !res.no_value { - statements.push(res.value); - } - } - - quote! { - Self { - #(#setting_names: #from_partial_values),* - } - } - } - ContainerInner::UnnamedStruct { fields } => {} - ContainerInner::UnnamedEnum { variants } => {} - ContainerInner::UnitEnum { variants } => todo!(), - }; + // let inner = match &self.inner { + // ContainerInner::NamedStruct { fields } => { + // let mut statements = vec![]; + + // for field in fields { + // let res = field.impl_partial_merge(); + + // if !res.no_value { + // statements.push(res.value); + // } + // } + + // todo!(); + // } + // ContainerInner::UnnamedStruct { fields } => {} + // ContainerInner::UnnamedEnum { variants } => {} + // ContainerInner::UnitEnum { variants } => todo!(), + // }; quote! { fn from_partial(partial: Self::Partial) -> Self {} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 799d5e76..0b149c09 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] profile = "default" -channel = "1.91.0" +channel = "1.95.0"