diff --git a/AGENTS.md b/AGENTS.md index 9b20db2a..b0ed2c11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,11 +62,24 @@ is set, `Field::get_name` and `Variant::get_name` apply it via `format_case`, so `settings()`, and validation paths agree with what serde accepts. Env keys deliberately skip the casing and stay derived from the Rust name (or an explicit `rename`), matching production. -Generics are supported by `Schematic` but not `Config` (the partial type isn't generic), which -matches production. As in production, `#[derive(Schematic)]` on a generic type does not add a -`T: Schematic` bound for you — write it yourself. Unlike production, a generic type's `schema_name` -appends each type argument (`Wrapper` becomes `WrapperString`), because schemas are keyed by -name alone and every instantiation would otherwise claim the same one. +**All three derives support generics**, which is a divergence from production (where only +`Schematic` does). `Container` carries `input.generics`, and every emitted item threads them +through: the partial declaration, its `Default`/`Deserialize`, `PartialConfig`, `Config`, and both +`Schematic` impls. Three things fall out of that: + +- No bounds are added for you. `PartialConfig` requires `Clone + Default + DeserializeOwned + + Schematic + Serialize`, so a generic `Config` needs those on its own type parameters, and a + generic `Schematic` needs `T: Schematic`. An under-bounded type fails to compile at the impl. +- The partial gets an explicit `#[serde(bound(deserialize = "T: DeserializeOwned"))]`. Without it + serde infers `T: Deserialize<'de>`, which is ambiguous against the `DeserializeOwned` the where + clause carries. `#[config(partial(serde(bound(...))))]` suppresses the generated one. +- A generic type's `schema_name` appends each type argument (`Wrapper` becomes + `WrapperString`, its partial `PartialWrapperString`), because schemas are keyed by name alone and + every instantiation would otherwise claim the same one. + +A generic `ConfigEnum` only makes sense with a `fallback`, since unit variants carry no data. Its +`Display` writes each arm for itself, so the fallback goes through `T: Display` instead of having to +be a `&str`. When implementing something in `core`, the old implementation in `crates/macros` is the reference. It is _not_ always correct — this session found many bugs in it — but it tells you the intended diff --git a/CHANGELOG.md b/CHANGELOG.md index 9583a3d9..cc0a194f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,21 @@ ## Next +This is a major release that has been in development for over a year. The macro layer has been +rewritten from the ground up, utilizing new patterns to improve maintainability and extendability, +while the schema types have been updated to be more flexible and composable. + #### 💥 Breaking ##### Config - Structs will no longer default to `camelCase` field name casing. - Enums will no longer default to `kebab-case` variant name casing. -- Removed `#[config(serde(...))]` on containers. Use `#[serde(...)]` instead. +- Removed `#[config(serde(...))]` on containers. Use `#[serde(...)]` directly instead. - Removed `#[variant(value)]` on enum variants. Use `#[variant(rename)]` instead, which does the same thing. -- Removed the `tracing` Cargo feature, which wrapped generated code in `#[tracing::instrument]`. - The loader is still instrumented; only the derive output no longer is. +- Removed the `tracing` Cargo feature, which wrapped generated code in `#[tracing::instrument]`. The + loader is still instrumented; only the derive output no longer is. ##### Schema @@ -64,6 +68,13 @@ - 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. +- Added generics support to `#[derive(Config)]` and `#[derive(ConfigEnum)]`, which previously only + worked on `#[derive(Schematic)]`. Type arguments are carried into the partial type and every + generated implementation, and a generic type's schema name appends them, so `Wrapper` and + `Wrapper` no longer collide. + - Bounds are not inferred. A generic `Config` needs + `Clone + Default + DeserializeOwned + Schematic + Serialize` on its type parameters, since + `PartialConfig` requires them. - Improved the parse, handling, and validation of container and field attributes. - Updated `#[config(before_parse)]` on `ConfigEnum` to accept every case that `rename_all` does, instead of only `lowercase` and `UPPERCASE`. Incoming values are normalized before being matched, diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 747acd2e..5de581cc 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -141,6 +141,29 @@ impl Container { self.ident.to_string() } + /// Generics for a type *declaration*, which keeps bounds and defaults. + /// The where clause is returned separately, as its position differs + /// between named and tuple shapes. + pub fn get_declaration_generics(&self) -> (&Generics, Option<&syn::WhereClause>) { + (&self.generics, self.generics.where_clause.as_ref()) + } + + /// The same generics with a `'de` lifetime prepended, for a hand written + /// `Deserialize` implementation. + pub fn get_deserialize_generics(&self) -> Generics { + let mut generics = self.generics.clone(); + + generics.params.insert( + 0, + syn::GenericParam::Lifetime(syn::LifetimeParam::new(syn::Lifetime::new( + "'de", + proc_macro2::Span::call_site(), + ))), + ); + + generics + } + pub fn is_config_enum(&self) -> bool { matches!(self.macro_type, ContainerMacro::ConfigUnitEnum) } @@ -200,6 +223,36 @@ impl Container { attrs } + /// `PartialConfig` requires the partial to be `DeserializeOwned`, which + /// means every type argument must be too. Serde would otherwise infer a + /// `T: Deserialize<'de>` bound of its own, and the two are ambiguous when + /// both are in scope, so the bound is stated outright. + /// + /// Returns `None` when there is nothing generic to bound, or when the + /// user supplied their own bound through `partial(serde(...))`. + fn get_partial_deserialize_bound(&self) -> Option { + let params = self + .generics + .type_params() + .map(|param| format!("{}: serde::de::DeserializeOwned", param.ident)) + .collect::>(); + + if params.is_empty() || self.has_partial_serde_bound() { + return None; + } + + Some(params.join(", ")) + } + + fn has_partial_serde_bound(&self) -> bool { + self.args.partial.as_ref().is_some_and(|partial| { + partial + .get_attributes() + .iter() + .any(|attr| attr.to_string().contains("bound")) + }) + } + pub fn get_partial_serde_attribute_args(&self) -> TokenStream { let mut meta = vec![]; @@ -235,6 +288,10 @@ impl Container { meta.push(quote! { expecting = #expecting }); } + if let Some(bound) = self.get_partial_deserialize_bound() { + meta.push(quote! { bound(deserialize = #bound) }); + } + // Config attributes take precedence over serde attributes let renames = [ ("rename", &self.args.rename, &self.serde_args.rename), @@ -271,18 +328,19 @@ impl Container { let from_partial_method = self.impl_full_from_partial(); let settings_method = self.impl_full_settings(); + let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); quote! { #[automatically_derived] - impl schematic::Config for #base_name { - type Partial = #partial_name; + impl #impl_generics schematic::Config for #base_name #ty_generics #where_clause { + type Partial = #partial_name #ty_generics; #from_partial_method #settings_method } #[automatically_derived] - impl Default for #base_name { + impl #impl_generics Default for #base_name #ty_generics #where_clause { fn default() -> Self { ::from_partial( ::default_partial() @@ -423,21 +481,23 @@ impl Container { let partial_name = self.get_partial_ident(); let attrs = self.get_partial_attributes(); let vis = &self.vis; + let (generics, where_clause) = self.get_declaration_generics(); match &self.inner { ContainerInner::NamedStruct { fields } => quote! { #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] #(#attrs)* - #vis struct #partial_name { + #vis struct #partial_name #generics #where_clause { #(#fields)* } }, + // A tuple struct takes its where clause after the fields ContainerInner::UnnamedStruct { fields } => quote! { #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] #(#attrs)* - #vis struct #partial_name( + #vis struct #partial_name #generics ( #(#fields)* - ); + ) #where_clause; }, ContainerInner::UnnamedEnum { variants } | ContainerInner::UnitEnum { variants } => { // Untagged enums implement `Deserialize` manually, @@ -451,7 +511,7 @@ impl Container { quote! { #[derive(#derives)] #(#attrs)* - #vis enum #partial_name { + #vis enum #partial_name #generics #where_clause { #(#variants)* } } @@ -480,9 +540,11 @@ impl Container { let partial_name = self.get_partial_ident(); let value = default_variant.impl_partial_default_value().value; + let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); + quote! { #[automatically_derived] - impl Default for #partial_name { + impl #impl_generics Default for #partial_name #ty_generics #where_clause { fn default() -> Self { Self::#value } @@ -509,9 +571,13 @@ impl Container { } } + let de_generics = self.get_deserialize_generics(); + let (de_impl_generics, _, _) = de_generics.split_for_impl(); + let (_, ty_generics, where_clause) = self.generics.split_for_impl(); + quote! { #[automatically_derived] - impl<'de> serde::Deserialize<'de> for #partial_name { + impl #de_impl_generics serde::Deserialize<'de> for #partial_name #ty_generics #where_clause { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, @@ -652,9 +718,9 @@ impl Container { #[automatically_derived] impl #impl_generics std::fmt::Display for #name #ty_generics #where_clause { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", match self { + match self { #(#display_arms)* - }) + } } } } @@ -695,7 +761,11 @@ impl Container { /// every instantiation would otherwise claim the same one. #[cfg(feature = "schema")] pub fn impl_schematic_name(&self) -> TokenStream { - let base_name_string = self.get_name(); + self.impl_schematic_name_for(self.get_name()) + } + + #[cfg(feature = "schema")] + fn impl_schematic_name_for(&self, base_name_string: String) -> TokenStream { let params = self .generics .type_params() @@ -748,10 +818,11 @@ impl Container { #[cfg(not(feature = "schema"))] pub fn impl_schematic_partial(&self) -> TokenStream { let partial_name = self.get_partial_ident(); + let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); quote! { #[automatically_derived] - impl schematic::Schematic for #partial_name {} + impl #impl_generics schematic::Schematic for #partial_name #ty_generics #where_clause {} } } @@ -761,17 +832,18 @@ impl Container { pub fn impl_schematic_partial(&self) -> TokenStream { let base_name = &self.ident; let partial_name = self.get_partial_ident(); - let partial_name_string = partial_name.to_string(); + let partial_schema_name = self.impl_schematic_name_for(partial_name.to_string()); + let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); quote! { #[automatically_derived] - impl schematic::Schematic for #partial_name { + impl #impl_generics schematic::Schematic for #partial_name #ty_generics #where_clause { fn schema_name() -> Option { - Some(#partial_name_string.into()) + #partial_schema_name } fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { - let mut schema = #base_name::build_schema(schema); + let mut schema = <#base_name #ty_generics as schematic::Schematic>::build_schema(schema); schematic::internal::partialize_schema(&mut schema, true); schema } @@ -891,10 +963,11 @@ impl Container { let finalize_method = self.impl_partial_finalize(); let merge_method = self.impl_partial_merge(); let validate_method = self.impl_partial_validate(); + let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); quote! { #[automatically_derived] - impl schematic::PartialConfig for #partial_name { + impl #impl_generics schematic::PartialConfig for #partial_name #ty_generics #where_clause { type Context = #context; #default_values_method diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index 779845f8..ed9e9a6c 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -205,16 +205,18 @@ impl Variant { } /// A match arm that formats this variant back into its string value. + /// Each arm writes for itself, so that a fallback holding a generic type + /// can go through `Display` rather than having to be a `&str`. pub fn impl_config_enum_display(&self) -> TokenStream { let name = &self.ident; if self.is_fallback() { - return quote! { Self::#name(fallback) => fallback, }; + return quote! { Self::#name(fallback) => write!(f, "{fallback}"), }; } let value = self.get_name(); - quote! { Self::#name => #value, } + quote! { Self::#name => f.write_str(#value), } } /// A match arm that parses a string into this variant. A fallback absorbs diff --git a/crates/core/tests/container_type_test.rs b/crates/core/tests/container_type_test.rs index 3c33b56c..42293039 100644 --- a/crates/core/tests/container_type_test.rs +++ b/crates/core/tests/container_type_test.rs @@ -494,3 +494,77 @@ mod to_tokens { assert_snapshot!(pretty(container.to_token_stream())); } } + +// Every emitted item has to carry the type arguments through: the partial +// declaration, its `Default`/`Deserialize`, `PartialConfig`, `Config`, and +// both `Schematic` impls. +mod generics { + use super::*; + + #[test] + fn named_struct() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + inner: T, + label: String, + } + }); + + assert_snapshot!(pretty(container.to_token_stream())); + } + + #[test] + fn unnamed_struct() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example(T, U); + }); + + assert_snapshot!(pretty(container.to_token_stream())); + } + + #[test] + fn unnamed_enum() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + Value(T), + #[setting(default)] + Nothing, + } + }); + + assert_snapshot!(pretty(container.to_token_stream())); + } + + #[test] + fn supports_bounds_and_where_clauses() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example + where + T: Default, + { + inner: T, + } + }); + + assert_snapshot!(pretty(container.to_token_stream())); + } + + #[test] + fn untagged_enum_deserialize_carries_generics() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(untagged)] + enum Example { + Value(T), + #[setting(default)] + Nothing, + } + }); + + assert_snapshot!(pretty(container.impl_partial_type_deserialize())); + } +} diff --git a/crates/core/tests/derive_config_enum_test.rs b/crates/core/tests/derive_config_enum_test.rs index 5249aee3..2f29bebc 100644 --- a/crates/core/tests/derive_config_enum_test.rs +++ b/crates/core/tests/derive_config_enum_test.rs @@ -172,3 +172,21 @@ mod validation { }); } } + +mod generics { + use super::*; + + #[test] + fn generic_fallback() { + let output = config_enum(parse_quote! { + #[derive(ConfigEnum)] + enum Example { + Known, + #[variant(fallback)] + Other(T), + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } +} diff --git a/crates/core/tests/snapshots/container_schema_test__schematic__named_struct.snap b/crates/core/tests/snapshots/container_schema_test__schematic__named_struct.snap index 7684859b..b66af01c 100644 --- a/crates/core/tests/snapshots/container_schema_test__schematic__named_struct.snap +++ b/crates/core/tests/snapshots/container_schema_test__schematic__named_struct.snap @@ -23,7 +23,7 @@ impl schematic::Schematic for PartialExample { Some("PartialExample".into()) } fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { - let mut schema = Example::build_schema(schema); + let mut schema = ::build_schema(schema); schematic::internal::partialize_schema(&mut schema, true); schema } diff --git a/crates/core/tests/snapshots/container_schema_test__schematic__unit_enum.snap b/crates/core/tests/snapshots/container_schema_test__schematic__unit_enum.snap index 74e32506..4b4d5e82 100644 --- a/crates/core/tests/snapshots/container_schema_test__schematic__unit_enum.snap +++ b/crates/core/tests/snapshots/container_schema_test__schematic__unit_enum.snap @@ -37,7 +37,7 @@ impl schematic::Schematic for PartialExample { Some("PartialExample".into()) } fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { - let mut schema = Example::build_schema(schema); + let mut schema = ::build_schema(schema); schematic::internal::partialize_schema(&mut schema, true); schema } diff --git a/crates/core/tests/snapshots/container_schema_test__schematic__unnamed_enum.snap b/crates/core/tests/snapshots/container_schema_test__schematic__unnamed_enum.snap index d7b64a51..fc2d5c70 100644 --- a/crates/core/tests/snapshots/container_schema_test__schematic__unnamed_enum.snap +++ b/crates/core/tests/snapshots/container_schema_test__schematic__unnamed_enum.snap @@ -28,7 +28,7 @@ impl schematic::Schematic for PartialExample { Some("PartialExample".into()) } fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { - let mut schema = Example::build_schema(schema); + let mut schema = ::build_schema(schema); schematic::internal::partialize_schema(&mut schema, true); schema } diff --git a/crates/core/tests/snapshots/container_schema_test__schematic__unnamed_struct.snap b/crates/core/tests/snapshots/container_schema_test__schematic__unnamed_struct.snap index adf61f56..1b3ebabd 100644 --- a/crates/core/tests/snapshots/container_schema_test__schematic__unnamed_struct.snap +++ b/crates/core/tests/snapshots/container_schema_test__schematic__unnamed_struct.snap @@ -18,7 +18,7 @@ impl schematic::Schematic for PartialExample { Some("PartialExample".into()) } fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { - let mut schema = Example::build_schema(schema); + let mut schema = ::build_schema(schema); schematic::internal::partialize_schema(&mut schema, true); schema } diff --git a/crates/core/tests/snapshots/container_type_test__generics__named_struct.snap b/crates/core/tests/snapshots/container_type_test__generics__named_struct.snap new file mode 100644 index 00000000..6f56da68 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__generics__named_struct.snap @@ -0,0 +1,111 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.to_token_stream()) +--- +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde( + default, + deny_unknown_fields, + bound(deserialize = "T: serde::de::DeserializeOwned") +)] +struct PartialExample { + #[serde(skip_serializing_if = "Option::is_none")] + inner: Option, + #[serde(skip_serializing_if = "Option::is_none")] + label: Option, +} +#[automatically_derived] +impl schematic::PartialConfig for PartialExample { + type Context = (); + fn default_values( + context: &Self::Context, + ) -> std::result::Result, schematic::ConfigError> { + Ok( + Some(Self { + inner: Some(Default::default()), + label: Some(Default::default()), + }), + ) + } + 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)?; + } + Ok(partial) + } + fn merge( + &mut self, + context: &Self::Context, + mut next: Self, + ) -> std::result::Result<(), schematic::ConfigError> { + use schematic::internal::*; + MergeManager::new(context) + .apply(&mut self.inner, next.inner)? + .apply(&mut self.label, next.label)?; + Ok(()) + } +} +#[automatically_derived] +impl schematic::Config for Example { + type Partial = PartialExample; + fn from_partial(partial: Self::Partial) -> Self { + Self { + inner: partial.inner.unwrap_or_default(), + label: partial.label.unwrap_or_default(), + } + } + fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + std::collections::BTreeMap::from_iter([ + ("inner".into(), ConfigSetting::new("T")), + ("label".into(), ConfigSetting::new("String")), + ]) + } +} +#[automatically_derived] +impl Default for Example { + fn default() -> Self { + ::from_partial( + ::default_partial(), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + let mut name = String::from("Example"); + name.push_str(&schematic::schema::schema_name_of::()); + Some(name) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .structure( + StructType::new([ + ("inner".into(), SchemaField::new(schema.infer::())), + ("label".into(), SchemaField::new(schema.infer::())), + ]), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for PartialExample { + fn schema_name() -> Option { + let mut name = String::from("PartialExample"); + name.push_str(&schematic::schema::schema_name_of::()); + Some(name) + } + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = as schematic::Schematic>::build_schema(schema); + schematic::internal::partialize_schema(&mut schema, true); + schema + } +} diff --git a/crates/core/tests/snapshots/container_type_test__generics__supports_bounds_and_where_clauses.snap b/crates/core/tests/snapshots/container_type_test__generics__supports_bounds_and_where_clauses.snap new file mode 100644 index 00000000..58f00f9a --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__generics__supports_bounds_and_where_clauses.snap @@ -0,0 +1,121 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.to_token_stream()) +--- +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde( + default, + deny_unknown_fields, + bound(deserialize = "T: serde::de::DeserializeOwned") +)] +struct PartialExample +where + T: Default, +{ + #[serde(skip_serializing_if = "Option::is_none")] + inner: Option, +} +#[automatically_derived] +impl schematic::PartialConfig for PartialExample +where + T: Default, +{ + type Context = (); + fn default_values( + context: &Self::Context, + ) -> std::result::Result, schematic::ConfigError> { + Ok( + Some(Self { + inner: Some(Default::default()), + }), + ) + } + 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)?; + } + Ok(partial) + } + fn merge( + &mut self, + context: &Self::Context, + mut next: Self, + ) -> std::result::Result<(), schematic::ConfigError> { + use schematic::internal::*; + MergeManager::new(context).apply(&mut self.inner, next.inner)?; + Ok(()) + } +} +#[automatically_derived] +impl schematic::Config for Example +where + T: Default, +{ + type Partial = PartialExample; + fn from_partial(partial: Self::Partial) -> Self { + Self { + inner: partial.inner.unwrap_or_default(), + } + } + fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + std::collections::BTreeMap::from_iter([ + ("inner".into(), ConfigSetting::new("T")), + ]) + } +} +#[automatically_derived] +impl Default for Example +where + T: Default, +{ + fn default() -> Self { + ::from_partial( + ::default_partial(), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for Example +where + T: Default, +{ + fn schema_name() -> Option { + let mut name = String::from("Example"); + name.push_str(&schematic::schema::schema_name_of::()); + Some(name) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .structure( + StructType::new([ + ("inner".into(), SchemaField::new(schema.infer::())), + ]), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for PartialExample +where + T: Default, +{ + fn schema_name() -> Option { + let mut name = String::from("PartialExample"); + name.push_str(&schematic::schema::schema_name_of::()); + Some(name) + } + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = as schematic::Schematic>::build_schema(schema); + schematic::internal::partialize_schema(&mut schema, true); + schema + } +} diff --git a/crates/core/tests/snapshots/container_type_test__generics__unnamed_enum.snap b/crates/core/tests/snapshots/container_type_test__generics__unnamed_enum.snap new file mode 100644 index 00000000..2de0bc81 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__generics__unnamed_enum.snap @@ -0,0 +1,117 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.to_token_stream()) +--- +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(bound(deserialize = "T: serde::de::DeserializeOwned"))] +enum PartialExample { + Value(T), + Nothing, +} +#[automatically_derived] +impl Default for PartialExample { + fn default() -> Self { + Self::Nothing + } +} +#[automatically_derived] +impl schematic::PartialConfig for PartialExample { + type Context = (); + fn default_values( + context: &Self::Context, + ) -> std::result::Result, schematic::ConfigError> { + Ok(Some(Self::Nothing)) + } + fn finalize( + self, + context: &Self::Context, + ) -> std::result::Result { + Ok(self) + } + fn merge( + &mut self, + context: &Self::Context, + mut next: Self, + ) -> std::result::Result<(), schematic::ConfigError> { + *self = next; + Ok(()) + } + 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::Value(pa) => {} + _ => {} + }; + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) + } +} +#[automatically_derived] +impl schematic::Config for Example { + type Partial = PartialExample; + fn from_partial(partial: Self::Partial) -> Self { + match partial { + PartialExample::Value(pa) => Self::Value(pa), + PartialExample::Nothing => Self::Nothing, + } + } + fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + std::collections::BTreeMap::from_iter([ + ("Value".into(), ConfigSetting::new("T")), + ("Nothing".into(), ConfigSetting::new("")), + ]) + } +} +#[automatically_derived] +impl Default for Example { + fn default() -> Self { + ::from_partial( + ::default_partial(), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + let mut name = String::from("Example"); + name.push_str(&schematic::schema::schema_name_of::()); + Some(name) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .union( + UnionType::from_schemas( + [ + Schema::structure( + StructType::new([("Value".into(), schema.infer::())]), + ), + Schema::literal_value(LiteralValue::String("Nothing".into())), + ], + Some(1usize), + ), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for PartialExample { + fn schema_name() -> Option { + let mut name = String::from("PartialExample"); + name.push_str(&schematic::schema::schema_name_of::()); + Some(name) + } + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = as schematic::Schematic>::build_schema(schema); + schematic::internal::partialize_schema(&mut schema, true); + schema + } +} diff --git a/crates/core/tests/snapshots/container_type_test__generics__unnamed_struct.snap b/crates/core/tests/snapshots/container_type_test__generics__unnamed_struct.snap new file mode 100644 index 00000000..ebf508a6 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__generics__unnamed_struct.snap @@ -0,0 +1,98 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.to_token_stream()) +--- +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde( + default, + bound(deserialize = "T: serde::de::DeserializeOwned, U: serde::de::DeserializeOwned") +)] +struct PartialExample( + #[serde(skip_serializing_if = "Option::is_none")] + Option, + #[serde(skip_serializing_if = "Option::is_none")] + Option, +); +#[automatically_derived] +impl schematic::PartialConfig for PartialExample { + type Context = (); + fn default_values( + context: &Self::Context, + ) -> std::result::Result, schematic::ConfigError> { + Ok(Some(Self(Some(Default::default()), Some(Default::default())))) + } + 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)?; + } + Ok(partial) + } + 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)?; + Ok(()) + } +} +#[automatically_derived] +impl schematic::Config for Example { + type Partial = PartialExample; + fn from_partial(partial: Self::Partial) -> Self { + Self(partial.0.unwrap_or_default(), partial.1.unwrap_or_default()) + } + fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + std::collections::BTreeMap::from_iter([ + ("0".into(), ConfigSetting::new("T")), + ("1".into(), ConfigSetting::new("U")), + ]) + } +} +#[automatically_derived] +impl Default for Example { + fn default() -> Self { + ::from_partial( + ::default_partial(), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + let mut name = String::from("Example"); + name.push_str(&schematic::schema::schema_name_of::()); + name.push_str(&schematic::schema::schema_name_of::()); + Some(name) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema.tuple(TupleType::new([schema.infer::(), schema.infer::()])) + } +} +#[automatically_derived] +impl schematic::Schematic for PartialExample { + fn schema_name() -> Option { + let mut name = String::from("PartialExample"); + name.push_str(&schematic::schema::schema_name_of::()); + name.push_str(&schematic::schema::schema_name_of::()); + Some(name) + } + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = as schematic::Schematic>::build_schema(schema); + schematic::internal::partialize_schema(&mut schema, true); + schema + } +} diff --git a/crates/core/tests/snapshots/container_type_test__generics__untagged_enum_deserialize_carries_generics.snap b/crates/core/tests/snapshots/container_type_test__generics__untagged_enum_deserialize_carries_generics.snap new file mode 100644 index 00000000..f3817d2f --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__generics__untagged_enum_deserialize_carries_generics.snap @@ -0,0 +1,39 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type_deserialize()) +--- +#[automatically_derived] +impl<'de, T> serde::Deserialize<'de> for PartialExample { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + let content = deserializer + .deserialize_any(schematic::serde_content::ValueVisitor)?; + let mut errors: Vec<(&str, String)> = Vec::new(); + match ::deserialize( + schematic::serde_content::Deserializer::new(content.clone()) + .coerce_numbers() + .human_readable(), + ) { + Ok(value) => return Ok(PartialExample::Value(value)), + Err(error) => errors.push(("Value", error.to_string())), + } + match <() as serde::Deserialize>::deserialize( + schematic::serde_content::Deserializer::new(content.clone()) + .coerce_numbers() + .human_readable(), + ) { + Ok(_) => return Ok(PartialExample::Nothing), + Err(error) => errors.push(("Nothing", error.to_string())), + } + let mut message = format!( + "failed to parse as any variant of {}:", stringify!(PartialExample) + ); + for (variant, error) in &errors { + message.push_str(&format!("\n- {variant}: {error}")); + } + Err(D::Error::custom(message)) + } +} diff --git a/crates/core/tests/snapshots/container_type_test__schematic__implements_both_types.snap b/crates/core/tests/snapshots/container_type_test__schematic__implements_both_types.snap index 0d1a1daf..38f6005b 100644 --- a/crates/core/tests/snapshots/container_type_test__schematic__implements_both_types.snap +++ b/crates/core/tests/snapshots/container_type_test__schematic__implements_both_types.snap @@ -21,7 +21,7 @@ impl schematic::Schematic for PartialExample { Some("PartialExample".into()) } fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { - let mut schema = Example::build_schema(schema); + let mut schema = ::build_schema(schema); schematic::internal::partialize_schema(&mut schema, true); schema } diff --git a/crates/core/tests/snapshots/container_type_test__to_tokens__named_struct.snap b/crates/core/tests/snapshots/container_type_test__to_tokens__named_struct.snap index cbf42529..a54f6b15 100644 --- a/crates/core/tests/snapshots/container_type_test__to_tokens__named_struct.snap +++ b/crates/core/tests/snapshots/container_type_test__to_tokens__named_struct.snap @@ -247,7 +247,7 @@ impl schematic::Schematic for PartialExample { Some("PartialExample".into()) } fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { - let mut schema = Example::build_schema(schema); + let mut schema = ::build_schema(schema); schematic::internal::partialize_schema(&mut schema, true); schema } diff --git a/crates/core/tests/snapshots/container_type_test__to_tokens__unit_enum.snap b/crates/core/tests/snapshots/container_type_test__to_tokens__unit_enum.snap index e9c8fddd..a6577611 100644 --- a/crates/core/tests/snapshots/container_type_test__to_tokens__unit_enum.snap +++ b/crates/core/tests/snapshots/container_type_test__to_tokens__unit_enum.snap @@ -100,7 +100,7 @@ impl schematic::Schematic for PartialExample { Some("PartialExample".into()) } fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { - let mut schema = Example::build_schema(schema); + let mut schema = ::build_schema(schema); schematic::internal::partialize_schema(&mut schema, true); schema } diff --git a/crates/core/tests/snapshots/container_type_test__to_tokens__unnamed_enum.snap b/crates/core/tests/snapshots/container_type_test__to_tokens__unnamed_enum.snap index 4364c995..74db6790 100644 --- a/crates/core/tests/snapshots/container_type_test__to_tokens__unnamed_enum.snap +++ b/crates/core/tests/snapshots/container_type_test__to_tokens__unnamed_enum.snap @@ -220,7 +220,7 @@ impl schematic::Schematic for PartialExample { Some("PartialExample".into()) } fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { - let mut schema = Example::build_schema(schema); + let mut schema = ::build_schema(schema); schematic::internal::partialize_schema(&mut schema, true); schema } diff --git a/crates/core/tests/snapshots/container_type_test__to_tokens__unnamed_struct.snap b/crates/core/tests/snapshots/container_type_test__to_tokens__unnamed_struct.snap index 6cd0d463..eee30c0b 100644 --- a/crates/core/tests/snapshots/container_type_test__to_tokens__unnamed_struct.snap +++ b/crates/core/tests/snapshots/container_type_test__to_tokens__unnamed_struct.snap @@ -129,7 +129,7 @@ impl schematic::Schematic for PartialExample { Some("PartialExample".into()) } fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { - let mut schema = Example::build_schema(schema); + let mut schema = ::build_schema(schema); schematic::internal::partialize_schema(&mut schema, true); schema } diff --git a/crates/core/tests/snapshots/derive_config_enum_test__generics__generic_fallback.snap b/crates/core/tests/snapshots/derive_config_enum_test__generics__generic_fallback.snap new file mode 100644 index 00000000..122bda58 --- /dev/null +++ b/crates/core/tests/snapshots/derive_config_enum_test__generics__generic_fallback.snap @@ -0,0 +1,94 @@ +--- +source: crates/core/tests/derive_config_enum_test.rs +expression: pretty(output.to_token_stream()) +--- +#[automatically_derived] +impl schematic::ConfigEnum for Example { + fn variants() -> Vec> { + vec![Self::Known, Self::Other(Default::default())] + } +} +#[automatically_derived] +impl std::str::FromStr for Example { + type Err = schematic::ConfigError; + fn from_str(value: &str) -> std::result::Result { + Ok( + match value { + "Known" => Self::Known, + fallback => { + Self::Other( + fallback + .try_into() + .map_err(|_| { + schematic::ConfigError::EnumInvalidFallback( + fallback.to_string(), + ) + })?, + ) + } + }, + ) + } +} +#[automatically_derived] +impl std::convert::TryFrom for Example { + type Error = schematic::ConfigError; + fn try_from(value: String) -> std::result::Result { + std::str::FromStr::from_str(&value) + } +} +#[automatically_derived] +impl std::convert::TryFrom<&String> for Example { + type Error = schematic::ConfigError; + fn try_from(value: &String) -> std::result::Result { + std::str::FromStr::from_str(value) + } +} +#[automatically_derived] +impl std::convert::TryFrom<&str> for Example { + type Error = schematic::ConfigError; + fn try_from(value: &str) -> std::result::Result { + std::str::FromStr::from_str(value) + } +} +#[automatically_derived] +impl std::fmt::Display for Example { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Known => f.write_str("Known"), + Self::Other(fallback) => write!(f, "{fallback}"), + } + } +} +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + let mut name = String::from("Example"); + name.push_str(&schematic::schema::schema_name_of::()); + Some(name) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .enumerable( + EnumType::from_schemas( + [ + Schema { + name: Some("Known".into()), + ty: Schema::literal_value( + LiteralValue::String("Known".into()), + ) + .ty, + ..Default::default() + }, + Schema { + name: Some("Other".into()), + ty: schema.infer::().ty, + ..Default::default() + }, + ], + None, + ), + ) + } +} diff --git a/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_a_default.snap b/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_a_default.snap index 6ebb76ac..67d2fdbc 100644 --- a/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_a_default.snap +++ b/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_a_default.snap @@ -49,7 +49,10 @@ impl std::convert::TryFrom<&str> for Example { #[automatically_derived] impl std::fmt::Display for Example { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", match self { Self::Info => "Info", Self::Error => "Error", }) + match self { + Self::Info => f.write_str("Info"), + Self::Error => f.write_str("Error"), + } } } #[automatically_derived] diff --git a/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_a_fallback.snap b/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_a_fallback.snap index 85be7a53..ee6447ff 100644 --- a/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_a_fallback.snap +++ b/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_a_fallback.snap @@ -54,10 +54,10 @@ impl std::convert::TryFrom<&str> for Example { #[automatically_derived] impl std::fmt::Display for Example { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, "{}", match self { Self::Known => "Known", Self::Other(fallback) => - fallback, } - ) + match self { + Self::Known => f.write_str("Known"), + Self::Other(fallback) => write!(f, "{fallback}"), + } } } #[automatically_derived] diff --git a/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_before_parse.snap b/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_before_parse.snap index 76d29b0f..542c4261 100644 --- a/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_before_parse.snap +++ b/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_before_parse.snap @@ -50,7 +50,9 @@ impl std::convert::TryFrom<&str> for Example { #[automatically_derived] impl std::fmt::Display for Example { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", match self { Self::Info => "Info", }) + match self { + Self::Info => f.write_str("Info"), + } } } #[automatically_derived] diff --git a/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_renames_and_aliases.snap b/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_renames_and_aliases.snap index 8b6c7db2..cc925f3e 100644 --- a/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_renames_and_aliases.snap +++ b/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_renames_and_aliases.snap @@ -50,10 +50,11 @@ impl std::convert::TryFrom<&str> for Example { #[automatically_derived] impl std::fmt::Display for Example { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, "{}", match self { Self::VeryHigh => "very-high", Self::Explicit => - "custom", Self::Aliased => "aliased", } - ) + match self { + Self::VeryHigh => f.write_str("very-high"), + Self::Explicit => f.write_str("custom"), + Self::Aliased => f.write_str("aliased"), + } } } #[automatically_derived] diff --git a/crates/core/tests/snapshots/derive_config_enum_test__rendering__unit_enum.snap b/crates/core/tests/snapshots/derive_config_enum_test__rendering__unit_enum.snap index d8d7459e..85ff1cee 100644 --- a/crates/core/tests/snapshots/derive_config_enum_test__rendering__unit_enum.snap +++ b/crates/core/tests/snapshots/derive_config_enum_test__rendering__unit_enum.snap @@ -50,10 +50,11 @@ impl std::convert::TryFrom<&str> for Example { #[automatically_derived] impl std::fmt::Display for Example { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, "{}", match self { Self::Info => "Info", Self::Error => "Error", Self::Off - => "Off", } - ) + match self { + Self::Info => f.write_str("Info"), + Self::Error => f.write_str("Error"), + Self::Off => f.write_str("Off"), + } } } #[automatically_derived] diff --git a/crates/macros-next/tests/config_enum_test.rs b/crates/macros-next/tests/config_enum_test.rs index 920d3a34..3a4255fd 100644 --- a/crates/macros-next/tests/config_enum_test.rs +++ b/crates/macros-next/tests/config_enum_test.rs @@ -374,3 +374,79 @@ mod derive_enum_helper { ); } } + +// A generic `ConfigEnum` only makes sense with a fallback, since unit +// variants carry no data. The type argument has to reach every impl, and +// `Display` writes the fallback through its own `Display` rather than +// requiring it to be a `&str`. +mod generics { + use super::*; + + #[derive(Clone, Debug, PartialEq, ConfigEnum)] + #[config(rename_all = "kebab-case")] + enum Value + where + T: Clone + Default + std::fmt::Display + for<'a> TryFrom<&'a str> + schematic::Schematic, + { + Known, + Other, + #[variant(fallback)] + Custom(T), + } + + #[test] + fn parses_named_variants() { + assert_eq!(Value::::from_str("known").unwrap(), Value::Known); + assert_eq!(Value::::from_str("other").unwrap(), Value::Other); + } + + #[test] + fn parses_through_a_generic_fallback() { + assert_eq!( + Value::::from_str("anything").unwrap(), + Value::Custom("anything".into()) + ); + } + + #[test] + fn formats_a_generic_fallback() { + assert_eq!(Value::::Custom("abc".into()).to_string(), "abc"); + assert_eq!(Value::::Known.to_string(), "known"); + } + + #[test] + fn lists_variants() { + assert_eq!( + Value::::variants(), + vec![Value::Known, Value::Other, Value::Custom(String::new())] + ); + } + + // The schema name carries the type argument, so instantiations don't + // collide in a generator + #[test] + fn names_schemas_per_instantiation() { + assert_eq!( + SchemaBuilder::build_root::>().name.as_deref(), + Some("ValueString") + ); + } + + #[test] + fn builds_a_generic_schema() { + let schema = SchemaBuilder::build_root::>(); + let SchemaType::Enum(inner) = &schema.ty else { + panic!("expected an enum, got {:?}", schema.ty); + }; + let variants = inner.variants.as_ref().unwrap(); + + assert_eq!( + variants.keys().collect::>(), + vec!["known", "other", "custom"] + ); + assert!(matches!( + variants["custom"].schema.ty, + SchemaType::String(_) + )); + } +} diff --git a/crates/macros-next/tests/config_test.rs b/crates/macros-next/tests/config_test.rs index 7a4f67b5..91ecd25b 100644 --- a/crates/macros-next/tests/config_test.rs +++ b/crates/macros-next/tests/config_test.rs @@ -473,3 +473,176 @@ mod casing { assert_eq!(field_names::(), vec!["SOME_FIELD_NAME"]); } } + +// A generic config needs its type arguments carried into the partial type +// and every impl. `PartialConfig` requires `DeserializeOwned`, so the derive +// states that bound outright rather than letting serde infer a conflicting +// `Deserialize<'de>` one. +mod generics { + use super::*; + use schematic::{Schema, SchemaBuilder, SchemaType}; + use serde::de::DeserializeOwned; + + pub trait Setting: + Clone + + std::fmt::Debug + + Default + + PartialEq + + Serialize + + DeserializeOwned + + schematic::Schematic + { + } + + impl Setting for T where + T: Clone + + std::fmt::Debug + + Default + + PartialEq + + Serialize + + DeserializeOwned + + schematic::Schematic + { + } + + #[derive(Debug, Config)] + pub struct Wrapper { + inner: T, + label: String, + } + + #[test] + fn builds_a_generic_partial() { + let partial: PartialWrapper = + serde_json::from_str(r#"{"inner": 5, "label": "a"}"#).unwrap(); + + assert_eq!(partial.inner, Some(5)); + assert_eq!(partial.label, Some("a".into())); + } + + #[test] + fn constructs_the_full_type() { + let config = Wrapper::::from_partial(PartialWrapper { + inner: Some(5), + label: Some("a".into()), + }); + + assert_eq!(config.inner, 5); + assert_eq!(config.label, "a"); + } + + #[test] + fn loads_through_the_loader() { + let result = ConfigLoader::>::new().load().unwrap(); + + assert_eq!(result.config.inner, 0); + assert_eq!(result.config.label, ""); + } + + #[test] + fn merges_a_generic_partial() { + let mut base = PartialWrapper:: { + inner: Some(1), + label: None, + }; + + base.merge( + &(), + PartialWrapper { + inner: Some(2), + label: Some("b".into()), + }, + ) + .unwrap(); + + assert_eq!(base.inner, Some(2)); + assert_eq!(base.label, Some("b".into())); + } + + #[test] + fn implements_default() { + assert_eq!(Wrapper::::default().inner, 0); + } + + // Each instantiation resolves to a distinct schema name, for both the + // full type and its partial + #[test] + fn names_schemas_per_instantiation() { + assert_eq!( + SchemaBuilder::build_root::>() + .name + .as_deref(), + Some("WrapperUsize") + ); + assert_eq!( + SchemaBuilder::build_root::>() + .name + .as_deref(), + Some("WrapperString") + ); + assert_eq!( + SchemaBuilder::build_root::>() + .name + .as_deref(), + Some("PartialWrapperUsize") + ); + } + + #[test] + fn builds_a_generic_schema() { + let schema: Schema = SchemaBuilder::build_root::>(); + let SchemaType::Struct(inner) = &schema.ty else { + panic!("expected a struct"); + }; + + assert_eq!( + inner.fields["inner"].schema.ty, + SchemaType::Boolean(Box::default()) + ); + } + + #[derive(Debug, Config)] + pub struct Pair(T, U); + + #[test] + fn supports_multiple_parameters_on_a_tuple_struct() { + let config = Pair::::from_partial(PartialPair(Some(1), Some("a".into()))); + + assert_eq!(config.0, 1); + assert_eq!(config.1, "a"); + } + + #[derive(Debug, Config)] + pub enum Either { + Value(T), + #[setting(default)] + Nothing, + } + + #[test] + fn supports_generic_enums() { + assert!(matches!( + PartialEither::::default(), + PartialEither::Nothing + )); + + let config = Either::::from_partial(PartialEither::Value(3)); + + assert!(matches!(config, Either::Value(3))); + } + + #[derive(Debug, Config)] + pub struct Bounded + where + T: Setting, + { + value: T, + } + + #[test] + fn supports_where_clauses() { + let config = Bounded::::from_partial(PartialBounded { value: Some(true) }); + + assert!(config.value); + } +}