diff --git a/CHANGELOG.md b/CHANGELOG.md index b801f07d..3a6f777c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ # Changelog +## Next + +#### 💥 Breaking + +- Removed `#[config(serde(...))]` on containers. Use `#[serde(...)]` instead. + +#### 🚀 Updates + +##### Config + +- Added support for unnamed tuple and newtype structs. Unnamed fields within the struct support + `#[setting]`. +- 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_")]`. +- 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. + +##### Serde + +- Added support for explicit deserialize and serialize renaming on containers and fields: + `#[serde(rename(deserialize = "de_name", serialize = "ser_name"))]`. +- 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 #### ⚙️ Internal diff --git a/Cargo.lock b/Cargo.lock index d3a6833f..a7b23f92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1341,6 +1341,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.114", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -1931,6 +1941,19 @@ dependencies = [ "uuid", ] +[[package]] +name = "schematic_core" +version = "0.18.7" +dependencies = [ + "darling", + "prettyplease", + "proc-macro2", + "quote", + "schematic_core", + "starbase_sandbox", + "syn 2.0.114", +] + [[package]] name = "schematic_macros" version = "0.19.4" @@ -1942,6 +1965,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "schematic_macros_next" +version = "0.18.7" +dependencies = [ + "quote", + "schematic_core", + "syn 2.0.114", +] + [[package]] name = "schematic_types" version = "0.11.5" diff --git a/Cargo.toml b/Cargo.toml index 2b8fc253..3c37302a 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.103" +quote = "1.0.42" 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.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 new file mode 100644 index 00000000..326f2a03 --- /dev/null +++ b/crates/core/Cargo.toml @@ -0,0 +1,40 @@ +[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", +] } +prettyplease = "0.2.37" +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..714e31d9 --- /dev/null +++ b/crates/core/src/args.rs @@ -0,0 +1,218 @@ +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 { + 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, +} + +// #[setting(partial)] +#[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(), + }) + } +} + +// #[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 new file mode 100644 index 00000000..50ec0c6f --- /dev/null +++ b/crates/core/src/container.rs @@ -0,0 +1,750 @@ +use crate::args::{PartialArg, SerdeContainerArgs, SerdeRenameArg}; +use crate::field::Field; +use crate::utils::{ImplResult, is_inheritable_attribute}; +use crate::variant::Variant; +use darling::FromDeriveInput; +use proc_macro2::TokenStream; +use quote::{ToTokens, format_ident, quote}; +use std::rc::Rc; +use syn::{Attribute, Data, DeriveInput, ExprPath, Fields, Ident, Visibility}; + +// #[config()], #[schematic()] +#[derive(Debug, Default, FromDeriveInput)] +#[darling(default, attributes(config, schematic), supports(struct_any, enum_any))] +pub struct ContainerArgs { + // config + pub allow_unknown_fields: bool, + pub context: Option, + #[cfg(feature = "env")] + pub env_prefix: Option, + pub partial: Option, + + // serde + pub rename: Option, + pub rename_all: Option, + pub rename_all_fields: Option, +} + +#[derive(Debug)] +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()); + let serde_args = Rc::new(SerdeContainerArgs::from_derive_input(&input).unwrap()); + + 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::UnnamedEnum { variants } + } + } + Data::Union(_) => { + panic!("Unions are not supported."); + } + }; + + 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) ]}]; + + 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 { + 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::UnnamedEnum { .. } => { + 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 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); + // } + // } + + // todo!(); + // } + // 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}"); + let context = match self.args.context.as_ref() { + Some(ctx) => quote! { #ctx }, + None => quote! { () }, + }; + + 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(); + + quote! { + #[automatically_derived] + impl schematic::PartialConfig for #partial_name { + type Context = #context; + + #default_values_method + #env_values_method + #extends_from_method + #finalize_method + #merge_method + #validate_method + } + } + } + + 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 { + let res = field.impl_partial_default_value(); + + if !res.no_value { + let key = field.get_key(); + let value = res.value; + + rows.push(quote! { + #key: #value, + }); + } + + if res.requires_internal { + requires_internal = true; + } + } + + // Do not implement method + if rows.is_empty() { + return quote! {}; + } + + let default_row = ImplResult::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 { + 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 + }); + } + + if res.requires_internal { + requires_internal = true; + } + } + + // Do not implement method + if all_none { + return quote! {}; + } + + quote! { + Ok(Some(Self( + #(#rows),* + ))) + } + } + ContainerInner::UnnamedEnum { 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.first() { + 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 + } + } + } + + #[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 } => { + 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); + + 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_with_prefix(prefix: Option<&str>) -> std::result::Result, schematic::ConfigError> { + #internal + + let mut env = EnvManager::new(#prefix_fallback); + let mut partial = Self::default(); + + #inner + + Ok(if env.is_empty() { + None + } else { + Some(partial) + }) + } + } + } + + #[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."); + } + } + + 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 } => { + let mut statements = vec![]; + + for field in fields { + let res = field.impl_partial_merge(); + + if !res.no_value { + statements.push(res.value); + } + } + + if statements.is_empty() { + return quote! {}; + } + + 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) + #(#statements)*; + + Ok(()) + } + } + } + ContainerInner::UnnamedEnum { variants } | ContainerInner::UnitEnum { variants } => { + let mut statements = vec![]; + + for variant in variants { + let res = variant.impl_partial_merge(); + + if !res.no_value { + statements.push(res.value); + } + } + + let inner = if statements.is_empty() { + quote! { + *self = next; + } + } else { + quote! { + match self { + #(#statements)* + _ => { + *self = next; + } + }; + } + }; + + quote! { + fn merge( + &mut self, + context: &Self::Context, + mut next: Self, + ) -> std::result::Result<(), schematic::ConfigError> { + #inner + Ok(()) + } + } + } + } + } + + #[cfg(not(feature = "validate"))] + pub fn impl_partial_validate(&self) -> TokenStream { + quote! {} + } + + #[cfg(feature = "validate")] + pub fn impl_partial_validate(&self) -> TokenStream { + 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(); + + if !res.no_value { + statements.push(res.value); + } + } + + if statements.is_empty() { + return quote! {}; + } + + quote! { + #(#statements)* + } + } + ContainerInner::UnnamedEnum { variants } => { + let mut statements = vec![]; + + for variant in variants { + let res = variant.impl_partial_validate(); + + if !res.no_value { + statements.push(res.value); + } + } + + if statements.is_empty() { + return quote! {}; + } + + quote! { + match self { + #(#statements)* + _ => {} + }; + } + } + ContainerInner::UnitEnum { .. } => { + return quote! {}; + } + }; + + let internal = ImplResult::impl_use_internal(true); + + quote! { + fn validate_with_path( + &self, + context: &Self::Context, + finalizing: bool, + path: schematic::Path + ) -> std::result::Result<(), Vec> { + #internal + + let mut validate = ValidateManager::new(context, finalizing, path); + #inner + + if !validate.errors.is_empty() { + return Err(validate.errors); + } + + Ok(()) + } + } + } +} + +impl ToTokens for Container { + fn to_tokens(&self, _tokens: &mut TokenStream) { + // TODO + } +} + +#[derive(Debug)] +pub enum ContainerInner { + NamedStruct { fields: Vec }, + UnnamedStruct { fields: Vec }, + // TODO: NamedEnum + UnnamedEnum { 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::UnnamedEnum { variants } | Self::UnitEnum { variants } => { + variants.iter().collect() + } + _ => vec![], + } + } +} diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs new file mode 100644 index 00000000..a77fc522 --- /dev/null +++ b/crates/core/src/field.rs @@ -0,0 +1,360 @@ +use crate::args::{ + NestedArg, PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeIoDirection, SerdeRenameArg, +}; +use crate::container::ContainerArgs; +use crate::field_value::FieldValue; +use crate::utils::{ImplResult, preserve_str_literal}; +use darling::FromAttributes; +use proc_macro2::{Literal, TokenStream}; +use quote::{ToTokens, TokenStreamExt, format_ident, quote}; +use std::rc::Rc; +use syn::{Attribute, Expr, ExprPath, Field as NativeField, FieldMutability, Ident, Visibility}; + +// #[schema()], #[setting()] +#[derive(Debug, FromAttributes, Default)] +#[darling(default, attributes(schema, setting))] +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, + #[cfg(feature = "schema")] + pub exclude: bool, + #[cfg(feature = "extends")] + pub extend: bool, + pub merge: Option, + pub nested: Option, + #[cfg(feature = "env")] + pub parse_env: Option, + pub partial: Option, + pub required: bool, + pub transform: Option, + #[cfg(feature = "validate")] + pub validate: 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)] +pub struct Field { + pub value: FieldValue, + + // 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 vis: Visibility, +} + +impl Field { + pub fn new( + field: NativeField, + container_args: Rc, + serde_container_args: Rc, + ) -> Self { + let args = FieldArgs::from_attributes(&field.attrs).unwrap(); + let serde_args = SerdeFieldArgs::from_attributes(&field.attrs).unwrap(); + + let field = Self { + attrs: field.attrs, + container_args, + ident: field.ident, + index: 0, + mutability: field.mutability, + serde_args, + serde_container_args, + vis: field.vis, + value: FieldValue::new(field.ty, args.nested.as_ref()), + args, + }; + + // dbg!(&field); + + field.validate_args(); + field + } + + fn validate_args(&self) { + #[cfg(feature = "env")] + { + if self.args.env_prefix.is_some() && self.args.nested.is_none() { + panic!("Cannot use `env_prefix` without `nested`."); + } + + if self.args.parse_env.is_some() && self.args.env.is_none() { + 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"))] + 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()); + } + + // 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()); + } + + if self.args.parse_env.is_some() { + panic!("Cannot use `parse_env` without `env` or a parent `env_prefix`."); + } + + 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.get_name_original().to_string() + } + + pub fn get_name_original(&self) -> &Ident { + self.ident + .as_ref() + .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")] + { + self.args.extend + } + + #[cfg(not(feature = "extends"))] + { + false + } + } + + pub fn is_nested(&self) -> bool { + self.args + .nested + .as_ref() + .is_some_and(|nested| nested.is_nested()) + } + + pub fn is_required(&self) -> bool { + self.args.required + } +} + +// 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(); + +// 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, +// }); +// } +// } + +impl Field { + pub fn impl_partial_default_value(&self) -> ImplResult { + self.value.impl_partial_default_value(&self.args) + } + + pub fn impl_partial_env_value(&self) -> ImplResult { + if self.is_nested() { + return self.value.impl_partial_env_value(&self.args, ""); + } + + 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 { + if self.is_extendable() { + self.value + .impl_partial_extends_from(&self.args, &self.get_key()) + } else { + ImplResult::skipped() + } + } + + 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()) + } + + 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 setting_var = format_ident!("setting"); + let nested_value = self + .value + .impl_partial_validate_nested(&key_string, &setting_var) + .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 self.#key.is_none() { + validate.required(#key); + } + }; + } + + if has_outer { + ImplResult { + value: outer, + ..Default::default() + } + } else { + ImplResult::skipped() + } + } +} + +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 new file mode 100644 index 00000000..3e2bd1b4 --- /dev/null +++ b/crates/core/src/field_value.rs @@ -0,0 +1,298 @@ +use crate::args::NestedArg; +use crate::field::FieldArgs; +use crate::utils::ImplResult; +use crate::value::{Layer, Value}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use std::ops::Deref; +use syn::{Expr, Lit, Type}; + +#[derive(Debug)] +pub struct FieldValue(Value); + +impl Deref for FieldValue { + type Target = Value; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +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 { + if self.is_outer_option_wrapped() { + 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() { + panic!("Cannot use `default` with `nested`."); + } + + let ident = format_ident!("Partial{}", nested_ident); + + quote! { + #ident::default_values(context)? + } + } 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) => { + res.requires_internal = true; + + quote! { handle_default_result(#func(context))? } + } + Expr::Lit(lit) => match &lit.lit { + Lit::Str(string) => { + res.requires_internal = true; + + quote! { + handle_default_result(#ty::try_from(#string))? + } + } + other => { + wrap_with_some = true; + + quote! { #other } + } + }, + invalid => { + panic!( + "Unsupported default value ({invalid:?}). May only provide literals, primitives, arrays, or tuples." + ); + } + } + } else { + wrap_with_some = true; + + quote! { + Default::default() + } + }; + + // Then wrap with each layer + 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() } + } + }; + } + } + + if wrap_with_some { + value = quote! { Some(#value) }; + } + + res.value = value; + 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(); + + 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); + + 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! { + env.get_and_parse(#env_key, #parse_env)? + } + } else { + quote! { + env.get(#env_key)? + } + }; + + res + } + + #[cfg(not(feature = "extends"))] + 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_args: &FieldArgs, + field_name: &TokenStream, + ) -> ImplResult { + let 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}`." + ); + } + }; + + ImplResult { + value, + ..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() + } + } + + #[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 + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs new file mode 100644 index 00000000..9c0453f9 --- /dev/null +++ b/crates/core/src/lib.rs @@ -0,0 +1,15 @@ +pub mod args; +pub mod container; +pub mod field; +pub mod field_value; +pub mod utils; +pub mod value; +pub mod variant; +pub mod variant_value; + +// #[cfg(feature = "config")] +// pub mod config; +// #[cfg(feature = "config")] +// pub mod config_enum; +#[cfg(feature = "schema")] +pub mod schematic; diff --git a/crates/core/src/schematic/mod.rs b/crates/core/src/schematic/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs new file mode 100644 index 00000000..5bc2541c --- /dev/null +++ b/crates/core/src/utils.rs @@ -0,0 +1,71 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Attribute, Expr, 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 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); + + ["allow", "default", "deprecated", "doc", "warn"] + .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("> ", ">") + .replace(" >", ">") +} + +#[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/value.rs b/crates/core/src/value.rs new file mode 100644 index 00000000..2019ce3c --- /dev/null +++ b/crates/core/src/value.rs @@ -0,0 +1,281 @@ +use crate::args::NestedArg; +use crate::utils::{ImplResult, to_type_string}; +use quote::{ToTokens, format_ident, quote}; +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) + } + + 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, + _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( + 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/variant.rs b/crates/core/src/variant.rs new file mode 100644 index 00000000..b5c964bf --- /dev/null +++ b/crates/core/src/variant.rs @@ -0,0 +1,434 @@ +use crate::args::{NestedArg, PartialArg, 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}; +use std::rc::Rc; +use syn::{Attribute, Expr, ExprPath, Fields, FieldsUnnamed, Ident, Variant as NativeVariant}; + +// #[setting()], #[schema()] +#[derive(Debug, Default, FromAttributes)] +#[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, + pub transform: Option, + #[cfg(feature = "validate")] + pub validate: Option, + + // 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 { + pub values: Vec, + + // 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 fields: Fields, +} + +impl Variant { + pub fn new( + variant: NativeVariant, + container_args: Rc, + serde_container_args: Rc, + ) -> Variant { + let args = VariantArgs::from_attributes(&variant.attrs).unwrap(); + let serde_args = SerdeFieldArgs::from_attributes(&variant.attrs).unwrap(); + + let variant = Self { + attrs: variant.attrs, + container_args, + ident: variant.ident, + serde_args, + serde_container_args, + 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.") + } + + if self.is_required() + && self + .values + .iter() + .any(|value| !value.is_outer_option_wrapped()) + { + 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."); + } + + 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."); + } + } else { + if self.args.null { + panic!("Can only use `null` with unit variants."); + } + } + } + + pub fn is_default(&self) -> bool { + 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 + .as_ref() + .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; + + res.value = match &self.fields { + 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 + } + + 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(); + + match &self.fields { + Fields::Named(_) => { + res.no_value = true; + } + Fields::Unnamed(fields) => { + let name = &self.ident; + + 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| { + if outer_names.len() == 1 { + quote! { + if let Self::#name(na) = next { + *self = Self::#name( + #func(pa.to_owned(), na, 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; + } + } + } + }); + } + 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; + } + }; + + res + } + + pub fn impl_partial_validate(&self) -> ImplResult { + 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 + 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!("p{}", count as char); + let inner_name = format_ident!("n{}", 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/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/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_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/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/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/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/core/tests/setting_default_test.rs b/crates/core/tests/setting_default_test.rs new file mode 100644 index 00000000..bacb80ec --- /dev/null +++ b/crates/core/tests/setting_default_test.rs @@ -0,0 +1,265 @@ +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 setting_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_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), + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + + 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 defaults = container + .inner + .get_fields() + .into_iter() + .map(|field| (field.ident.as_ref().unwrap(), field.args.default.as_ref())) + .collect::>(); + + 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! { + #[derive(Config)] + struct Example { + a: Option, + b: Option>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn supports_types() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + bool, + #[setting(default = true)] + bool, + #[setting(default = 100)] + usize, + #[setting(default = "abc")] + String, + #[setting(default = ["a".into(), "b".into(), "c".into()])] + [String; 3], + #[setting(default = vec!["a", "b", "c"])] + Vec, + #[setting(default = (10, -10, 0))] + (usize, isize, u8), + ); + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + + for field in container.inner.get_fields() { + if field.index != 0 { + assert!(field.args.default.is_some()); + } + } + + let defaults = container + .inner + .get_fields() + .into_iter() + .map(|field| (field.index, field.args.default.as_ref())) + .collect::>(); + + assert_debug_snapshot!(defaults); + } + + #[test] + fn renders_nothing_if_all_option_wrapped() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + Option, + Option>, + ); + }); + + 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/setting_env_test.rs b/crates/core/tests/setting_env_test.rs new file mode 100644 index 00000000..e1f41e73 --- /dev/null +++ b/crates/core/tests/setting_env_test.rs @@ -0,0 +1,482 @@ +mod utils; + +use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; +use utils::pretty; + +mod setting_env { + use super::*; + + 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! { + #[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] + #[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! { + #[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())); + } + } + + mod named_enum { + // N/A + } + + mod unnamed_enum { + // N/A + } + + mod unit_enum { + // N/A + } +} + +mod setting_env_prefix { + use super::*; + + mod named_struct { + use super::*; + + #[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(); + + 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())); + } + } + + 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())); + } + } +} + +mod setting_parse_env { + use super::*; + + mod named_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)] + a: String, + } + }); + let field = container.inner.get_fields()[0]; + + assert!(field.args.parse_env.is_some()); + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + + #[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()); + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + + #[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, + } + }); + } + } + + 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()); + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + + #[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()); + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + + #[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, + ); + }); + } + } + + mod named_enum { + // N/A + } + + mod unnamed_enum { + // N/A + } + + mod unit_enum { + // N/A + } +} 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_extend_test.rs b/crates/core/tests/setting_extend_test.rs new file mode 100644 index 00000000..8c795818 --- /dev/null +++ b/crates/core/tests/setting_extend_test.rs @@ -0,0 +1,163 @@ +mod utils; + +use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; +use utils::pretty; + +mod setting_extend { + use super::*; + + 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(); + } + } + + 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(); + } + } + + mod named_enum { + // N/A + } + + mod unnamed_enum { + // N/A + } + + mod unit_enum { + // N/A + } +} diff --git a/crates/core/tests/setting_merge_test.rs b/crates/core/tests/setting_merge_test.rs new file mode 100644 index 00000000..082a444c --- /dev/null +++ b/crates/core/tests/setting_merge_test.rs @@ -0,0 +1,471 @@ +mod utils; + +use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; +use utils::pretty; + +mod setting_merge { + use super::*; + + mod named_struct { + 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, + } + }); + } + + #[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)] + 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)] + struct 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] + 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 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 Example( + 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(); + } + } + + 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())); + } + + #[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 { + 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 new file mode 100644 index 00000000..5b745a62 --- /dev/null +++ b/crates/core/tests/setting_nested_test.rs @@ -0,0 +1,434 @@ +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, + ); + }); + } + } + + mod named_enum { + // N/A + } + + 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 { + 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_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); + } + } +} diff --git a/crates/core/tests/setting_partial_test.rs b/crates/core/tests/setting_partial_test.rs new file mode 100644 index 00000000..ced66344 --- /dev/null +++ b/crates/core/tests/setting_partial_test.rs @@ -0,0 +1,98 @@ +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()); + } + } + + 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_serde_test.rs b/crates/core/tests/setting_serde_test.rs new file mode 100644 index 00000000..cba8d326 --- /dev/null +++ b/crates/core/tests/setting_serde_test.rs @@ -0,0 +1,221 @@ +use schematic_core::args::SerdeRenameArg; +use schematic_core::container::Container; +use syn::parse_quote; + +mod setting_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"); + } + } +} diff --git a/crates/core/tests/setting_test.rs b/crates/core/tests/setting_test.rs new file mode 100644 index 00000000..2c6d824d --- /dev/null +++ b/crates/core/tests/setting_test.rs @@ -0,0 +1,482 @@ +use schematic_core::container::Container; +use schematic_core::field::Field; +use schematic_core::value::Layer; +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() +} + +mod setting_field { + use super::*; + + #[test] + fn basic_args() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(exclude, extend)] + a: String, + #[setting(required)] + b: Option, + } + }); + let fields = container.inner.get_fields(); + + assert!(fields[0].args.exclude); + assert!(fields[0].args.extend); + assert!(fields[1].args.required); + } + + #[test] + fn extracts_layers() { + 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.layers, vec![]); + + // b + let field = get_field(&fields, "b"); + assert_eq!(field.value.ty_string, "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.layers, vec![Layer::Arc]); + + // d + let field = get_field(&fields, "d"); + assert_eq!(field.value.ty_string, "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.layers, vec![Layer::Rc]); + + // f + let field = get_field(&fields, "f"); + assert_eq!(field.value.ty_string, "Option>"); + 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.layers, vec![Layer::Option, Layer::Box]); + + // h + let field = get_field(&fields, "h"); + assert_eq!(field.value.ty_string, "Option>"); + 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.layers, vec![Layer::Arc, Layer::Option]); + + // j + let field = get_field(&fields, "j"); + assert_eq!(field.value.ty_string, "Box>"); + 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.layers, vec![Layer::Rc, Layer::Option]); + + // l + let field = get_field(&fields, "l"); + assert_eq!(field.value.ty_string, "Option>>"); + assert_eq!( + field.value.layers, + vec![Layer::Option, Layer::Arc, Layer::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_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.layers, + vec![Layer::Vec("Vec".into()), Layer::Vec("SmallVec".into())] + ); + 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!(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" + ); + + // 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_eq!(get_field_nested_ident(field).to_string(), "NestedConfig"); + } +} diff --git a/crates/core/tests/setting_transform_test.rs b/crates/core/tests/setting_transform_test.rs new file mode 100644 index 00000000..4ef8823b --- /dev/null +++ b/crates/core/tests/setting_transform_test.rs @@ -0,0 +1,106 @@ +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, + ); + }); + } + } + + 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 new file mode 100644 index 00000000..44fdaff0 --- /dev/null +++ b/crates/core/tests/setting_validate_test.rs @@ -0,0 +1,390 @@ +mod utils; + +use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; +use utils::pretty; + +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()); + assert_snapshot!(pretty(container.impl_partial_validate())); + } + + #[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()); + assert_snapshot!(pretty(container.impl_partial_validate())); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(validate = 123)] + a: String, + } + }); + } + + #[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 { + 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()); + assert_snapshot!(pretty(container.impl_partial_validate())); + } + + #[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()); + assert_snapshot!(pretty(container.impl_partial_validate())); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(validate = 123)] + String, + ); + }); + } + + #[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 { + // 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()); + assert_snapshot!(pretty(container.impl_partial_validate())); + } + + #[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()); + assert_snapshot!(pretty(container.impl_partial_validate())); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(validate = 123)] + A(String), + } + }); + } + + #[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 { + 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/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/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) +} 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/setting_default_test__setting_default__handles_collections.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_collections.snap new file mode 100644 index 00000000..1849aa80 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_collections.snap @@ -0,0 +1,17 @@ +--- +source: crates/core/tests/setting_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(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/setting_default_test__setting_default__handles_layers.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers.snap new file mode 100644 index 00000000..f72afa24 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers.snap @@ -0,0 +1,17 @@ +--- +source: crates/core/tests/setting_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/setting_default_test__setting_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 new file mode 100644 index 00000000..7ba81778 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__renders_nothing_if_all_option_wrapped.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/setting_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- + diff --git a/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_nested.snap new file mode 100644 index 00000000..232d8900 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_nested.snap @@ -0,0 +1,14 @@ +--- +source: crates/core/tests/setting_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/setting_default_test__setting_default__named_struct__supports_types-2.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_types-2.snap new file mode 100644 index 00000000..e5db7318 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_types-2.snap @@ -0,0 +1,178 @@ +--- +source: crates/core/tests/setting_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/setting_default_test__setting_default__named_struct__supports_types.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_types.snap new file mode 100644 index 00000000..c60ce1c7 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_types.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/setting_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/setting_default_test__setting_default__supports_handler_func.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__supports_handler_func.snap new file mode 100644 index 00000000..987714e9 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__supports_handler_func.snap @@ -0,0 +1,14 @@ +--- +source: crates/core/tests/setting_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 { + a: handle_default_result(handler(context))?, + }), + ) +} diff --git a/crates/core/tests/snapshots/setting_default_test__setting_default__unit_enum__supports.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__unit_enum__supports.snap new file mode 100644 index 00000000..e5949dc3 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__unit_enum__supports.snap @@ -0,0 +1,9 @@ +--- +source: crates/core/tests/setting_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- +fn default_values( + context: &Self::Context, +) -> std::result::Result, schematic::ConfigError> { + Ok(Some(Self::Bar(Default::default(), Default::default()))) +} diff --git a/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_enum__supports.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_enum__supports.snap new file mode 100644 index 00000000..870132bb --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_enum__supports.snap @@ -0,0 +1,9 @@ +--- +source: crates/core/tests/setting_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- +fn default_values( + context: &Self::Context, +) -> std::result::Result, schematic::ConfigError> { + Ok(Some(Self::Bar)) +} diff --git a/crates/core/tests/snapshots/setting_default_test__setting_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 new file mode 100644 index 00000000..7ba81778 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/setting_default_test.rs +expression: pretty(container.impl_partial_default_values()) +--- + diff --git a/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__supports_types-2.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__supports_types-2.snap new file mode 100644 index 00000000..6c02fe81 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__supports_types-2.snap @@ -0,0 +1,164 @@ +--- +source: crates/core/tests/setting_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/setting_default_test__setting_default__unnamed_struct__supports_types.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__supports_types.snap new file mode 100644 index 00000000..4f9f2be2 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__supports_types.snap @@ -0,0 +1,22 @@ +--- +source: crates/core/tests/setting_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/setting_env_test__setting_env__named_struct__supports_different_types.snap b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_different_types.snap new file mode 100644 index 00000000..4c2e5217 --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_different_types.snap @@ -0,0 +1,16 @@ +--- +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("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/setting_env_test__setting_env__named_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_nested.snap new file mode 100644 index 00000000..7fd42d6b --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_nested.snap @@ -0,0 +1,14 @@ +--- +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.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/setting_env_test__setting_env__unnamed_struct__supports_different_types.snap b/crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_different_types.snap new file mode 100644 index 00000000..a34635f9 --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_different_types.snap @@ -0,0 +1,16 @@ +--- +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.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/setting_env_test__setting_env__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_nested.snap new file mode 100644 index 00000000..7c96bb2c --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_nested.snap @@ -0,0 +1,14 @@ +--- +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.nested(PartialNestedConfig::env_values()?)?; + partial.1 = env.nested(PartialCustomConfig::env_values()?)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/setting_env_test__setting_env_prefix__named_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_env_test__setting_env_prefix__named_struct__supports_nested.snap new file mode 100644 index 00000000..46f30cc9 --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_env_prefix__named_struct__supports_nested.snap @@ -0,0 +1,16 @@ +--- +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.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/setting_env_test__setting_env_prefix__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_env_test__setting_env_prefix__unnamed_struct__supports_nested.snap new file mode 100644 index 00000000..45dabc85 --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_env_prefix__unnamed_struct__supports_nested.snap @@ -0,0 +1,16 @@ +--- +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.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/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_extend_test__setting_extend__can_set_opt_string.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_string.snap new file mode 100644 index 00000000..bd38cb48 --- /dev/null +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__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__can_set_opt_type.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_type.snap new file mode 100644 index 00000000..20f23ba9 --- /dev/null +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__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__can_set_opt_vec_string.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__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__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__can_set_string.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_string.snap new file mode 100644 index 00000000..bd38cb48 --- /dev/null +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__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__can_set_type.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_type.snap new file mode 100644 index 00000000..20f23ba9 --- /dev/null +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__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__can_set_vec_string.snap b/crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_vec_string.snap new file mode 100644 index 00000000..5374d8c9 --- /dev/null +++ b/crates/core/tests/snapshots/setting_extend_test__setting_extend__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())) +} 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())) +} diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_func.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_func.snap new file mode 100644 index 00000000..53c23a3b --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_func.snap @@ -0,0 +1,20 @@ +--- +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> { + 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/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 new file mode 100644 index 00000000..4bfeb116 --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap @@ -0,0 +1,17 @@ +--- +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> { + use schematic::internal::*; + MergeManager::new(context) + .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 new file mode 100644 index 00000000..95bd0a41 --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_collections.snap @@ -0,0 +1,16 @@ +--- +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> { + 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.c, next.c, merge_btreeset)?; + Ok(()) +} diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_standard.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_standard.snap new file mode 100644 index 00000000..d6092d21 --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_standard.snap @@ -0,0 +1,20 @@ +--- +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> { + 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/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..538ccba1 --- /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(pa) => { + if let Self::A(na) = next { + *self = Self::A(a(pa.to_owned(), na, context)?.unwrap_or_default()); + } else { + *self = next; + } + } + 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(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(pa, pb); + } else { + *self = Self::C(Default::default(), Default::default()); + } + } else { + *self = next; + } + } + 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(pa, pb); + } else { + *self = Self::D(Default::default(), Default::default()); + } + } else { + *self = next; + } + } + Self::E(pa) => { + if let Self::E(na) = next { + *self = Self::E(e(pa.to_owned(), na, 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_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(()) +} 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(()) +} diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_func.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_func.snap new file mode 100644 index 00000000..543b0d4b --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_func.snap @@ -0,0 +1,20 @@ +--- +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> { + 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/setting_merge_test__setting_merge__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_nested.snap new file mode 100644 index 00000000..c1deb807 --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_nested.snap @@ -0,0 +1,17 @@ +--- +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> { + 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/setting_merge_test__setting_merge__unnamed_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_nested_collections.snap new file mode 100644 index 00000000..188e66cf --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_nested_collections.snap @@ -0,0 +1,16 @@ +--- +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> { + 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/setting_merge_test__setting_merge__unnamed_struct__supports_standard.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_standard.snap new file mode 100644 index 00000000..ddbdad98 --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_standard.snap @@ -0,0 +1,20 @@ +--- +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> { + 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/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..59eba0d9 --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalizing, 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..6f8d0a01 --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalizing, 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..9e94e211 --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + 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); + } + 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..73fefeb3 --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + 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()); + } + 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..66f92821 --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalizing, 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..b42e0daa --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap @@ -0,0 +1,23 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalizing, 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 new file mode 100644 index 00000000..6cfbce8c --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap @@ -0,0 +1,23 @@ +--- +source: crates/core/tests/setting_validate_test.rs +expression: pretty(container.impl_partial_validate()) +--- +fn validate_with_path( + &self, + context: &Self::Context, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalizing, 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..b4465be0 --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalizing, 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..720b8137 --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalizing, 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..f1a42013 --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalizing, 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..f3d1822b --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalizing, 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/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..3f32edc4 --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalizing, 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..d19eb7c1 --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalizing, 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..600c4718 --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + 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); + } + 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..84243014 --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + 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()); + } + 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..409beb19 --- /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, + finalizing: bool, + path: schematic::Path, +) -> std::result::Result<(), Vec> { + use schematic::internal::*; + let mut validate = ValidateManager::new(context, finalizing, 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/core/tests/utils.rs b/crates/core/tests/utils.rs new file mode 100644 index 00000000..3a02b2f5 --- /dev/null +++ b/crates/core/tests/utils.rs @@ -0,0 +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/macros-next/Cargo.toml b/crates/macros-next/Cargo.toml new file mode 100644 index 00000000..d283526e --- /dev/null +++ b/crates/macros-next/Cargo.toml @@ -0,0 +1,28 @@ +[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] +schematic_core = { version = "0.18.7", path = "../core" } +quote = { workspace = true } +syn = { workspace = true } + +[features] +default = ["schema"] +config = [] +env = [] +extends = [] +schema = [] +tracing = [] +validate = [] diff --git a/crates/macros-next/src/lib.rs b/crates/macros-next/src/lib.rs new file mode 100644 index 00000000..e15d4257 --- /dev/null +++ b/crates/macros-next/src/lib.rs @@ -0,0 +1,41 @@ +use schematic_core::container::Container; + +// #[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)); + let container = Container::from(input); + + quote! { #container }.into() +} 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 = [] 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/config/configs.rs b/crates/schematic/src/config/configs.rs index e7bcf85a..60a9ca41 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,32 @@ 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> { + 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) + } /// 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,14 +61,16 @@ 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. #[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(), @@ -71,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(()) @@ -82,6 +99,15 @@ 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(); + + <::Partial as PartialConfig>::default_values(&context) + .unwrap_or_default() + .unwrap_or_default() + } + /// Convert a partial configuration into a full configuration, with all values populated. fn from_partial(partial: Self::Partial) -> Self; 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 84% rename from crates/schematic/src/internal.rs rename to crates/schematic/src/internal/mod.rs index 9bbecef1..c24031a9 100644 --- a/crates/schematic/src/internal.rs +++ b/crates/schematic/src/internal/mod.rs @@ -1,14 +1,27 @@ +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; -// Handles T and Option values +// DEFAULT VALUES + pub fn handle_default_result( result: Result, ) -> Result { result.map_err(|error| ConfigError::InvalidDefaultValue(error.to_string())) } +// LEGACY + #[cfg(feature = "env")] pub fn track_env(value: Option, tracker: &mut std::collections::HashSet) -> Option { value.inspect(|_| { @@ -44,40 +57,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), } } diff --git a/crates/schematic/src/internal/validate.rs b/crates/schematic/src/internal/validate.rs new file mode 100644 index 00000000..bc1774c7 --- /dev/null +++ b/crates/schematic/src/internal/validate.rs @@ -0,0 +1,78 @@ +use crate::config::{PartialConfig, Path, ValidateError, Validator}; + +pub struct ValidateManager<'a, Ctx> { + context: &'a Ctx, + finalizing: bool, + path: Path, + + pub errors: Vec, +} + +impl<'a, Ctx> ValidateManager<'a, Ctx> { + pub fn new(context: &'a Ctx, finalizing: bool, path: Path) -> Self { + Self { + context, + errors: vec![], + 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.finalizing) { + self.errors + .push(error.prepend_path(self.path.join_key(key))); + } + } + + pub fn required(&mut self, key: &str) { + if self.finalizing { + 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.finalizing, 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.finalizing, + 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.finalizing, + self.path.join_key(key).join_key(sub_key), + ) { + self.errors.extend(errors); + } + } + } +} 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"