From 3809dce95187ec68b12aef9e7f42525ee247a43c Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 5 Jul 2026 14:21:46 -0700 Subject: [PATCH 01/18] Initial spike of next version rewrite (#160) --- CHANGELOG.md | 31 + Cargo.lock | 32 + Cargo.toml | 4 + crates/core/Cargo.toml | 40 + crates/core/src/args.rs | 218 +++++ crates/core/src/container.rs | 750 ++++++++++++++++++ crates/core/src/field.rs | 360 +++++++++ crates/core/src/field_value.rs | 298 +++++++ crates/core/src/lib.rs | 15 + crates/core/src/schematic/mod.rs | 1 + crates/core/src/utils.rs | 71 ++ crates/core/src/value.rs | 281 +++++++ crates/core/src/variant.rs | 434 ++++++++++ crates/core/src/variant_value.rs | 21 + crates/core/tests/args_test.rs | 153 ++++ crates/core/tests/container_env_test.rs | 57 ++ crates/core/tests/container_finalize_test.rs | 198 +++++ crates/core/tests/container_partial_test.rs | 19 + crates/core/tests/container_test.rs | 135 ++++ crates/core/tests/setting_default_test.rs | 265 +++++++ crates/core/tests/setting_env_test.rs | 482 +++++++++++ crates/core/tests/setting_exclude_test.rs | 98 +++ crates/core/tests/setting_extend_test.rs | 163 ++++ crates/core/tests/setting_merge_test.rs | 471 +++++++++++ crates/core/tests/setting_nested_test.rs | 434 ++++++++++ crates/core/tests/setting_null_test.rs | 57 ++ crates/core/tests/setting_partial_test.rs | 98 +++ crates/core/tests/setting_required_test.rs | 151 ++++ crates/core/tests/setting_serde_test.rs | 221 ++++++ crates/core/tests/setting_test.rs | 482 +++++++++++ crates/core/tests/setting_transform_test.rs | 106 +++ crates/core/tests/setting_validate_test.rs | 390 +++++++++ ...v_test__container_env__can_set_prefix.snap | 21 + ...env_test__container_env__can_set_vars.snap | 17 + ...nalize__named_struct__supports_nested.snap | 35 + ...d_struct__supports_nested_collections.snap | 58 ++ ...lize__named_struct__supports_standard.snap | 24 + ...nalize__unnamed_enum__supports_nested.snap | 27 + ...med_enum__supports_nested_collections.snap | 54 ++ ...lize__unnamed_enum__supports_standard.snap | 16 + ...lize__unnamed_struct__supports_nested.snap | 35 + ...d_struct__supports_nested_collections.snap | 58 ++ ...ze__unnamed_struct__supports_standard.snap | 24 + ...tial_test__container_partial__can_set.snap | 54 ++ ..._setting_default__handles_collections.snap | 17 + ...test__setting_default__handles_layers.snap | 17 + ...renders_nothing_if_all_option_wrapped.snap | 5 + ...efault__named_struct__supports_nested.snap | 14 + ...fault__named_struct__supports_types-2.snap | 178 +++++ ...default__named_struct__supports_types.snap | 20 + ...etting_default__supports_handler_func.snap | 14 + ..._setting_default__unit_enum__supports.snap | 9 + ...tting_default__unnamed_enum__supports.snap | 9 + ...renders_nothing_if_all_option_wrapped.snap | 5 + ...ult__unnamed_struct__supports_types-2.snap | 164 ++++ ...fault__unnamed_struct__supports_types.snap | 22 + ...amed_struct__supports_different_types.snap | 16 + ...ng_env__named_struct__supports_nested.snap | 14 + ...amed_struct__supports_different_types.snap | 16 + ..._env__unnamed_struct__supports_nested.snap | 14 + ...prefix__named_struct__supports_nested.snap | 16 + ...efix__unnamed_struct__supports_nested.snap | 16 + ...e_env__named_struct__accepts_func_ref.snap | 13 + ...rse_env__named_struct__accepts_string.snap | 13 + ...env__unnamed_struct__accepts_func_ref.snap | 13 + ...e_env__unnamed_struct__accepts_string.snap | 13 + ...t__setting_extend__can_set_opt_string.snap | 7 + ...est__setting_extend__can_set_opt_type.snap | 7 + ...etting_extend__can_set_opt_vec_string.snap | 7 + ..._test__setting_extend__can_set_string.snap | 7 + ...nd_test__setting_extend__can_set_type.snap | 7 + ...t__setting_extend__can_set_vec_string.snap | 7 + ...end__named_struct__can_set_opt_string.snap | 7 + ...xtend__named_struct__can_set_opt_type.snap | 7 + ..._named_struct__can_set_opt_vec_string.snap | 7 + ..._extend__named_struct__can_set_string.snap | 7 + ...ng_extend__named_struct__can_set_type.snap | 7 + ...end__named_struct__can_set_vec_string.snap | 7 + ...ng_merge__named_struct__supports_func.snap | 20 + ..._merge__named_struct__supports_nested.snap | 17 + ...d_struct__supports_nested_collections.snap | 16 + ...erge__named_struct__supports_standard.snap | 20 + ...ng_merge__unnamed_enum__supports_func.snap | 67 ++ ..._merge__unnamed_enum__supports_nested.snap | 44 + ...med_enum__supports_nested_collections.snap | 43 + ...erge__unnamed_enum__supports_standard.snap | 12 + ..._merge__unnamed_struct__supports_func.snap | 20 + ...erge__unnamed_struct__supports_nested.snap | 17 + ...d_struct__supports_nested_collections.snap | 16 + ...ge__unnamed_struct__supports_standard.snap | 20 + ...e__named_struct__accepts_curried_func.snap | 20 + ...idate__named_struct__accepts_func_ref.snap | 20 + ...lidate__named_struct__supports_nested.snap | 39 + ...d_struct__supports_nested_collections.snap | 38 + ...date__named_struct__supports_standard.snap | 32 + ...e__unnamed_enum__accepts_curried_func.snap | 23 + ...idate__unnamed_enum__accepts_func_ref.snap | 23 + ...e__unnamed_enum__supports_many_values.snap | 41 + ...lidate__unnamed_enum__supports_nested.snap | 42 + ...med_enum__supports_nested_collections.snap | 41 + ...date__unnamed_enum__supports_standard.snap | 35 + ..._unnamed_struct__accepts_curried_func.snap | 20 + ...ate__unnamed_struct__accepts_func_ref.snap | 20 + ...date__unnamed_struct__supports_nested.snap | 39 + ...d_struct__supports_nested_collections.snap | 38 + ...te__unnamed_struct__supports_standard.snap | 32 + crates/core/tests/utils.rs | 16 + crates/macros-next/Cargo.toml | 28 + crates/macros-next/src/lib.rs | 41 + crates/macros/Cargo.toml | 8 +- crates/macros/src/config/field_value.rs | 1 + crates/schematic/src/config/configs.rs | 42 +- crates/schematic/src/internal/env.rs | 57 ++ crates/schematic/src/internal/merge.rs | 47 ++ .../src/{internal.rs => internal/mod.rs} | 48 +- crates/schematic/src/internal/validate.rs | 78 ++ rust-toolchain.toml | 2 +- 117 files changed, 8810 insertions(+), 34 deletions(-) create mode 100644 crates/core/Cargo.toml create mode 100644 crates/core/src/args.rs create mode 100644 crates/core/src/container.rs create mode 100644 crates/core/src/field.rs create mode 100644 crates/core/src/field_value.rs create mode 100644 crates/core/src/lib.rs create mode 100644 crates/core/src/schematic/mod.rs create mode 100644 crates/core/src/utils.rs create mode 100644 crates/core/src/value.rs create mode 100644 crates/core/src/variant.rs create mode 100644 crates/core/src/variant_value.rs create mode 100644 crates/core/tests/args_test.rs create mode 100644 crates/core/tests/container_env_test.rs create mode 100644 crates/core/tests/container_finalize_test.rs create mode 100644 crates/core/tests/container_partial_test.rs create mode 100644 crates/core/tests/container_test.rs create mode 100644 crates/core/tests/setting_default_test.rs create mode 100644 crates/core/tests/setting_env_test.rs create mode 100644 crates/core/tests/setting_exclude_test.rs create mode 100644 crates/core/tests/setting_extend_test.rs create mode 100644 crates/core/tests/setting_merge_test.rs create mode 100644 crates/core/tests/setting_nested_test.rs create mode 100644 crates/core/tests/setting_null_test.rs create mode 100644 crates/core/tests/setting_partial_test.rs create mode 100644 crates/core/tests/setting_required_test.rs create mode 100644 crates/core/tests/setting_serde_test.rs create mode 100644 crates/core/tests/setting_test.rs create mode 100644 crates/core/tests/setting_transform_test.rs create mode 100644 crates/core/tests/setting_validate_test.rs create mode 100644 crates/core/tests/snapshots/container_env_test__container_env__can_set_prefix.snap create mode 100644 crates/core/tests/snapshots/container_env_test__container_env__can_set_vars.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__named_struct__supports_standard.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_nested.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_enum__supports_standard.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/container_finalize_test__container_finalize__unnamed_struct__supports_standard.snap create mode 100644 crates/core/tests/snapshots/container_partial_test__container_partial__can_set.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__handles_collections.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__renders_nothing_if_all_option_wrapped.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_types-2.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__named_struct__supports_types.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__supports_handler_func.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__unit_enum__supports.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_enum__supports.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__renders_nothing_if_all_option_wrapped.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__supports_types-2.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__supports_types.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_different_types.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_different_types.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_env_prefix__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_env_prefix__unnamed_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_parse_env__named_struct__accepts_func_ref.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_parse_env__named_struct__accepts_string.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_parse_env__unnamed_struct__accepts_func_ref.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_parse_env__unnamed_struct__accepts_string.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_string.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_type.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_opt_vec_string.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_string.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_type.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__can_set_vec_string.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_string.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_type.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_opt_vec_string.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_string.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_type.snap create mode 100644 crates/core/tests/snapshots/setting_extend_test__setting_extend__named_struct__can_set_vec_string.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_func.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_standard.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_func.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_standard.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_func.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__supports_standard.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_curried_func.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_curried_func.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap create mode 100644 crates/core/tests/utils.rs create mode 100644 crates/macros-next/Cargo.toml create mode 100644 crates/macros-next/src/lib.rs create mode 100644 crates/schematic/src/internal/env.rs create mode 100644 crates/schematic/src/internal/merge.rs rename crates/schematic/src/{internal.rs => internal/mod.rs} (84%) create mode 100644 crates/schematic/src/internal/validate.rs 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..8b137891 --- /dev/null +++ b/crates/core/src/schematic/mod.rs @@ -0,0 +1 @@ + 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" From 922b46450675ef0529e0a865364ad1c69c7a3582 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Tue, 7 Jul 2026 21:06:03 -0700 Subject: [PATCH 02/18] Add settings. --- crates/core/src/container.rs | 71 ++++++++++++- crates/core/src/variant.rs | 23 ++++- crates/core/tests/container_test.rs | 99 +++++++++++++++++++ crates/core/tests/setting_transform_test.rs | 42 +++++++- ...est__settings__named_struct__supports.snap | 19 ++++ ...r_test__settings__unit_enum__supports.snap | 12 +++ ...est__settings__unnamed_enum__supports.snap | 16 +++ ...t__settings__unnamed_struct__supports.snap | 19 ++++ crates/schematic/src/config/configs.rs | 20 ++++ crates/schematic/tests/variants_test.rs | 1 - 10 files changed, 316 insertions(+), 6 deletions(-) create mode 100644 crates/core/tests/snapshots/container_test__settings__named_struct__supports.snap create mode 100644 crates/core/tests/snapshots/container_test__settings__unit_enum__supports.snap create mode 100644 crates/core/tests/snapshots/container_test__settings__unnamed_enum__supports.snap create mode 100644 crates/core/tests/snapshots/container_test__settings__unnamed_struct__supports.snap diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 50ec0c6f..33c1861f 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -1,6 +1,6 @@ use crate::args::{PartialArg, SerdeContainerArgs, SerdeRenameArg}; use crate::field::Field; -use crate::utils::{ImplResult, is_inheritable_attribute}; +use crate::utils::{ImplResult, is_inheritable_attribute, to_type_string}; use crate::variant::Variant; use darling::FromDeriveInput; use proc_macro2::TokenStream; @@ -226,10 +226,75 @@ impl Container { } } - // TODO pub fn impl_full_settings(&self) -> TokenStream { + let mut settings = vec![]; + + match &self.inner { + ContainerInner::NamedStruct { fields } | ContainerInner::UnnamedStruct { fields } => { + for field in fields { + let name = if field.ident.is_some() { + field.get_name() + } else { + field.index.to_string() + }; + let env_key = if let Some(value) = field.get_env_var() { + quote! { .env(#value) } + } else { + quote! {} + }; + let nested = if field.is_nested() { + let value = field.value.get_inner_type(); + quote! { .nested(#value::settings()) } + } else { + quote! {} + }; + let type_alias = to_type_string(field.value.ty.to_token_stream()); + + settings.push(quote! { + (#name.into(), ConfigSetting::new(#type_alias) + #env_key + #nested + ), + }); + } + } + ContainerInner::UnnamedEnum { variants } => { + for variant in variants { + let name = variant.get_name(); + let type_alias = to_type_string( + variant + .values + .iter() + .map(|v| v.ty.to_token_stream()) + .collect::>() + .into_iter() + .collect::(), + ); + + settings.push(quote! { + (#name.into(), ConfigSetting::new(#type_alias)), + }); + } + } + ContainerInner::UnitEnum { variants } => { + for variant in variants { + let name = variant.get_name(); + + settings.push(quote! { + (#name.into(), ConfigSetting::new(#name)), + }); + } + } + }; + quote! { - fn settings() -> schematic::ConfigSettingMap {} + fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + + std::collections::BTreeMap::from_iter([ + #(#settings)* + ]) + } } } diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index b5c964bf..ae9acd5e 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -1,4 +1,6 @@ -use crate::args::{NestedArg, PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; +use crate::args::{ + NestedArg, PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeIoDirection, SerdeRenameArg, +}; use crate::container::ContainerArgs; use crate::utils::ImplResult; use crate::variant_value::VariantValue; @@ -124,6 +126,25 @@ impl Variant { } } + pub fn get_name(&self) -> String { + let dir = SerdeIoDirection::From; + + if let Some(name) = self.args.rename.as_ref().and_then(|rn| rn.get_name(dir)) { + return name.into(); + } + + if let Some(name) = self + .serde_args + .rename + .as_ref() + .and_then(|rn| rn.get_name(dir)) + { + return name.into(); + } + + self.ident.to_string() + } + pub fn is_default(&self) -> bool { self.args.default } diff --git a/crates/core/tests/container_test.rs b/crates/core/tests/container_test.rs index a3151482..8b132af3 100644 --- a/crates/core/tests/container_test.rs +++ b/crates/core/tests/container_test.rs @@ -1,6 +1,10 @@ +mod utils; + use schematic_core::args::*; use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; use syn::parse_quote; +use utils::pretty; mod container { use super::*; @@ -133,3 +137,98 @@ mod container { ); } } + +mod settings { + use super::*; + + mod named_struct { + use super::*; + + #[test] + fn supports() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: String, + b: i32, + #[setting(env = "C_VAR")] + c: bool, + d: Option, + e: Vec, + f: std::collections::HashMap, + #[setting(nested)] + g: NestedExample, + } + }); + + assert_snapshot!(pretty(container.impl_full_settings())); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn supports() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + String, + i32, + #[setting(env = "C_VAR")] + bool, + Option, + Vec, + std::collections::HashMap, + #[setting(nested)] + NestedExample, + ); + }); + + assert_snapshot!(pretty(container.impl_full_settings())); + } + } + + mod named_enum { + // N/A + } + + mod unnamed_enum { + use super::*; + + #[test] + fn supports() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A(String), + B(i32), + C(bool), + D(Option), + E(Vec), + F(std::collections::HashMap), + #[setting(nested)] + G(NestedExample), + } + }); + + assert_snapshot!(pretty(container.impl_full_settings())); + } + } + + mod unit_enum { + use super::*; + + #[test] + fn supports() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A, B, C + } + }); + + assert_snapshot!(pretty(container.impl_full_settings())); + } + } +} diff --git a/crates/core/tests/setting_transform_test.rs b/crates/core/tests/setting_transform_test.rs index 4ef8823b..1acd5ee3 100644 --- a/crates/core/tests/setting_transform_test.rs +++ b/crates/core/tests/setting_transform_test.rs @@ -97,7 +97,47 @@ mod setting_transform { } mod unnamed_enum { - // N/A + use super::*; + + #[test] + fn accepts_func_ref() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(transform = func_ref)] + A(String) + } + }); + let field = container.inner.get_variants()[0]; + + assert!(field.args.transform.is_some()); + } + + #[test] + fn accepts_string() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(transform = "func_ref")] + A(String) + } + }); + let field = container.inner.get_variants()[0]; + + assert!(field.args.transform.is_some()); + } + + #[test] + #[should_panic(expected = "UnexpectedType")] + fn errors_invalid_type() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(transform = 123)] + A(String) + } + }); + } } mod unit_enum { diff --git a/crates/core/tests/snapshots/container_test__settings__named_struct__supports.snap b/crates/core/tests/snapshots/container_test__settings__named_struct__supports.snap new file mode 100644 index 00000000..cb87ab78 --- /dev/null +++ b/crates/core/tests/snapshots/container_test__settings__named_struct__supports.snap @@ -0,0 +1,19 @@ +--- +source: crates/core/tests/container_test.rs +expression: pretty(container.impl_full_settings()) +--- +fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + std::collections::BTreeMap::from_iter([ + ("a".into(), ConfigSetting::new("String")), + ("b".into(), ConfigSetting::new("i32")), + ("c".into(), ConfigSetting::new("bool").env("C_VAR")), + ("d".into(), ConfigSetting::new("Option")), + ("e".into(), ConfigSetting::new("Vec")), + ("f".into(), ConfigSetting::new("std::collections::HashMap")), + ( + "g".into(), + ConfigSetting::new("NestedExample").nested(NestedExample::settings()), + ), + ]) +} diff --git a/crates/core/tests/snapshots/container_test__settings__unit_enum__supports.snap b/crates/core/tests/snapshots/container_test__settings__unit_enum__supports.snap new file mode 100644 index 00000000..af36d7d3 --- /dev/null +++ b/crates/core/tests/snapshots/container_test__settings__unit_enum__supports.snap @@ -0,0 +1,12 @@ +--- +source: crates/core/tests/container_test.rs +expression: pretty(container.impl_full_settings()) +--- +fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + std::collections::BTreeMap::from_iter([ + ("A".into(), ConfigSetting::new("A")), + ("B".into(), ConfigSetting::new("B")), + ("C".into(), ConfigSetting::new("C")), + ]) +} diff --git a/crates/core/tests/snapshots/container_test__settings__unnamed_enum__supports.snap b/crates/core/tests/snapshots/container_test__settings__unnamed_enum__supports.snap new file mode 100644 index 00000000..d2221b00 --- /dev/null +++ b/crates/core/tests/snapshots/container_test__settings__unnamed_enum__supports.snap @@ -0,0 +1,16 @@ +--- +source: crates/core/tests/container_test.rs +expression: pretty(container.impl_full_settings()) +--- +fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + std::collections::BTreeMap::from_iter([ + ("A".into(), ConfigSetting::new("String")), + ("B".into(), ConfigSetting::new("i32")), + ("C".into(), ConfigSetting::new("bool")), + ("D".into(), ConfigSetting::new("Option")), + ("E".into(), ConfigSetting::new("Vec")), + ("F".into(), ConfigSetting::new("std::collections::HashMap")), + ("G".into(), ConfigSetting::new("NestedExample")), + ]) +} diff --git a/crates/core/tests/snapshots/container_test__settings__unnamed_struct__supports.snap b/crates/core/tests/snapshots/container_test__settings__unnamed_struct__supports.snap new file mode 100644 index 00000000..ceffb73d --- /dev/null +++ b/crates/core/tests/snapshots/container_test__settings__unnamed_struct__supports.snap @@ -0,0 +1,19 @@ +--- +source: crates/core/tests/container_test.rs +expression: pretty(container.impl_full_settings()) +--- +fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + std::collections::BTreeMap::from_iter([ + ("0".into(), ConfigSetting::new("String")), + ("1".into(), ConfigSetting::new("i32")), + ("2".into(), ConfigSetting::new("bool").env("C_VAR")), + ("3".into(), ConfigSetting::new("Option")), + ("4".into(), ConfigSetting::new("Vec")), + ("5".into(), ConfigSetting::new("std::collections::HashMap")), + ( + "6".into(), + ConfigSetting::new("NestedExample").nested(NestedExample::settings()), + ), + ]) +} diff --git a/crates/schematic/src/config/configs.rs b/crates/schematic/src/config/configs.rs index 60a9ca41..34cc8d77 100644 --- a/crates/schematic/src/config/configs.rs +++ b/crates/schematic/src/config/configs.rs @@ -131,4 +131,24 @@ pub struct ConfigSetting { pub type_alias: String, } +impl ConfigSetting { + pub fn new(type_alias: impl AsRef) -> Self { + Self { + env_key: None, + nested: None, + type_alias: type_alias.as_ref().to_string(), + } + } + + pub fn env(mut self, env_key: impl AsRef) -> Self { + self.env_key = Some(env_key.as_ref().to_string()); + self + } + + pub fn nested(mut self, nested: ConfigSettingMap) -> Self { + self.nested = Some(nested); + self + } +} + pub type ConfigSettingMap = BTreeMap; diff --git a/crates/schematic/tests/variants_test.rs b/crates/schematic/tests/variants_test.rs index a4da82bc..3c677e98 100644 --- a/crates/schematic/tests/variants_test.rs +++ b/crates/schematic/tests/variants_test.rs @@ -1,7 +1,6 @@ use schematic::*; use serial_test::serial; use std::collections::HashMap; -use std::env; fn test_list(_: &[String], _: &T, context: &Context, _: bool) -> ValidateResult { if context.fail { From 6be334eef96b9b76d73ad643359387bba03b5ee0 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Fri, 14 Aug 2026 14:52:25 -0700 Subject: [PATCH 03/18] deps: Audit 08/14 (#190) --- CHANGELOG.md | 9 + Cargo.lock | 1187 ++++++++--------- Cargo.toml | 36 +- crates/core/Cargo.toml | 2 +- crates/core/src/field.rs | 6 +- crates/core/src/variant.rs | 4 +- ...fault__named_struct__supports_types-2.snap | 18 +- ...ult__unnamed_struct__supports_types-2.snap | 18 +- crates/schematic/Cargo.toml | 14 +- crates/schematic/src/config/formats/pkl.rs | 9 +- .../snapshots/partialize_test__enums.snap | 42 +- rust-toolchain.toml | 2 +- 12 files changed, 633 insertions(+), 714 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6f777c..5e09c469 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,15 @@ - Added support for `skip_deserializing_if` and `skip_serializing_if` on fields. - Updated `alias` to support multiple aliases: `#[serde(alias = "alias1", alias = "alias2")]` +#### ⚙️ Internal + +- Updated `syn` to v3. +- Updated `darling` to v0.24. +- Updated `pkl` to v0.8. +- Updated `garde` (validation) to v0.23. +- Updated Rust to v1.97. +- Updated dependencies. + ## 0.19.7 #### ⚙️ Internal diff --git a/Cargo.lock b/Cargo.lock index a7b23f92..a1b6a412 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,39 +30,39 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "assert_cmd" -version = "2.2.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a686bbee5efb88a82df0621b236e74d925f470e5445d3220a5648b892ec99c9" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" dependencies = [ "anstyle", "bstr", @@ -75,12 +75,11 @@ dependencies = [ [[package]] name = "assert_fs" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a652f6cb1f516886fcfee5e7a5c078b9ade62cfcb889524efe5a64d682dd27a9" +checksum = "6ecf5c70ca07b7f80220bce936f0556a960ca6fb00fc2bd4125b5e581b218137" dependencies = [ "anstyle", - "doc-comment", "globwalk", "predicates", "predicates-core", @@ -96,15 +95,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.15.2" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ "aws-lc-sys", "zeroize", @@ -112,14 +111,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.35.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" dependencies = [ "cc", "cmake", "dunce", "fs_extra", + "pkg-config", ] [[package]] @@ -154,18 +154,18 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" dependencies = [ "serde_core", ] [[package]] name = "bitvec" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" dependencies = [ "funty", "radium", @@ -175,43 +175,44 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +checksum = "d8f347189c62a579b8cd5f80714efa178f52e461dc2e6d701d264f5ff22e566c" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.119", ] [[package]] name = "bstr" -version = "1.12.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", "regex-automata", - "serde", + "serde_core", ] [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytecheck" @@ -237,9 +238,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "castaway" @@ -252,9 +253,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.52" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -262,12 +263,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -276,15 +271,26 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.1", +] [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -296,9 +302,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] @@ -315,9 +321,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.8.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -329,9 +335,9 @@ dependencies = [ [[package]] name = "console" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", @@ -340,9 +346,9 @@ dependencies = [ [[package]] name = "convert_case" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" dependencies = [ "unicode-segmentation", ] @@ -363,11 +369,20 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-deque" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -375,24 +390,24 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "darling" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" dependencies = [ "darling_core", "darling_macro", @@ -400,26 +415,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 3.0.3", ] [[package]] name = "darling_macro" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" dependencies = [ "darling_core", "quote", - "syn 2.0.114", + "syn 3.0.3", ] [[package]] @@ -440,7 +455,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.114", + "syn 2.0.119", ] [[package]] @@ -478,21 +493,15 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 3.0.3", ] -[[package]] -name = "doc-comment" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" - [[package]] name = "dunce" version = "1.0.5" @@ -529,15 +538,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.7" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "float-cmp" @@ -571,9 +580,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -581,15 +590,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -598,27 +607,27 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", "futures-io", @@ -626,15 +635,14 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] [[package]] name = "garde" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a74b56a4039a46e8c91cc9d84e8a7df4e1f8b24239ca57d1304b3263cb599b9" +checksum = "5d7f479d28d1daedf23970890723b8774d120aa627abcb33c1d7c93d0965e6c3" dependencies = [ "compact_str", "garde_derive", @@ -647,14 +655,14 @@ dependencies = [ [[package]] name = "garde_derive" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7224c08ec489e2840af29ed882b47f7f6ac8f4ce15c275d9fc0d6d1b94578ae6" +checksum = "7b0252cacdca8e6f30a900e6d436fbf294e23f674b9288ca4a5bd35976bef87c" dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.114", + "syn 2.0.119", ] [[package]] @@ -672,15 +680,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasip2", + "rand_core 0.10.1", "wasm-bindgen", ] @@ -692,9 +700,9 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "globset" -version = "0.4.18" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "07c34a9410465b45bd9787443bc7370f37735bad04b0f0cd57ff1a3186c98988" dependencies = [ "aho-corasick", "bstr", @@ -725,15 +733,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "http" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -741,9 +749,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http", @@ -751,9 +759,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -770,9 +778,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.8.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" dependencies = [ "atomic-waker", "bytes", @@ -783,7 +791,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -791,15 +798,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -807,14 +813,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -831,9 +836,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -855,12 +860,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -868,9 +874,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -881,9 +887,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -895,16 +901,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -915,15 +922,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "92a7ed671a6aad807a8651a2e1782a6598fda9ce5185dd8158549e95a91c6428" dependencies = [ "displaydoc", "icu_locale_core", @@ -953,9 +960,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -963,9 +970,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.25" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +checksum = "00b69833ed729dc5aa7d19541d96d6cf8e9137194207a04916d658e43168402f" dependencies = [ "crossbeam-deque", "globset", @@ -979,43 +986,33 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] [[package]] name = "insta" -version = "1.47.2" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +checksum = "86f0f8fee8c926415c58d6ae43a08523a26faccb2323f5e6b644fe7dd4ef6b82" dependencies = [ "console", "once_cell", - "similar", + "similar 2.7.0", "tempfile", ] [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "is_ci" @@ -1025,88 +1022,115 @@ checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", + "jni-macros", "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror", "walkdir", - "windows-sys 0.45.0", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", ] [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] [[package]] name = "jobserver" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ - "once_cell", + "cfg-if", + "futures-util", "wasm-bindgen", ] [[package]] name = "json-strip-comments" -version = "3.1.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25376d12b2f6ae53f986f86e2a808a56af03d72284ae24fc35a2e290d09ee3c3" +checksum = "01ed659c5f428031b9c1fb7de6729af2eb491654d4bcd416a5b5b704986b9658" dependencies = [ "memchr", ] [[package]] name = "libc" -version = "0.2.180" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" dependencies = [ - "bitflags", "libc", ] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" @@ -1119,9 +1143,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -1140,9 +1164,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "miette" @@ -1171,7 +1195,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.119", ] [[package]] @@ -1185,9 +1209,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", @@ -1220,15 +1244,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "openssl-probe" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "option-ext" @@ -1238,9 +1262,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "owo-colors" -version = "4.2.3" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "parking_lot" @@ -1273,21 +1297,21 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "pin-utils" -version = "0.1.0" +name = "pkg-config" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -1317,15 +1341,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" [[package]] name = "predicates-tree" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" dependencies = [ "predicates-core", "termtree", @@ -1343,28 +1367,28 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.37" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 3.0.3", ] [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -1391,9 +1415,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -1403,7 +1427,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "web-time", @@ -1411,21 +1435,22 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" dependencies = [ "aws-lc-rs", "bytes", - "getrandom 0.3.4", + "getrandom 0.4.3", "lru-slab", - "rand 0.9.2", + "rand 0.10.2", + "rand_pcg", "ring", "rustc-hash", "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror", "tinyvec", "tracing", "web-time", @@ -1433,32 +1458,32 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.43" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.3.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radium" @@ -1468,23 +1493,24 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.5" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", - "rand_chacha 0.3.1", + "rand_chacha", "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", ] [[package]] @@ -1497,16 +1523,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -1518,11 +1534,17 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "getrandom 0.3.4", + "rand_core 0.10.1", ] [[package]] @@ -1542,14 +1564,26 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror", +] + +[[package]] +name = "reflink-copy" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9dd7ab4af0363d5ccfd2838d782a28196cf32a5cc2e4fe3c5dc83f2be588b8b" +dependencies = [ + "cfg-if", + "libc", + "rustix", + "windows", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -1559,9 +1593,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -1570,9 +1604,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "relative-path" @@ -1594,9 +1628,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.1" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -1702,9 +1736,9 @@ dependencies = [ [[package]] name = "ron" -version = "0.12.0" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" dependencies = [ "bitflags", "once_cell", @@ -1716,9 +1750,9 @@ dependencies = [ [[package]] name = "rpkl" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c58732d31abf3a100d8f2cf0bb30ae66e009b7ddeb01888cff0607b6a02aac4" +checksum = "0a9bf78448f899ef84e3b12215af07cc91ae9a7f4ab649a118cb9c8d77da6608" dependencies = [ "dunce", "rmp-serde", @@ -1728,31 +1762,32 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.39.0" +version = "1.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a" dependencies = [ "arrayvec", "borsh", "bytes", "num-traits", - "rand 0.8.5", + "rand 0.8.7", "rkyv", "serde", "serde_json", + "wasm-bindgen", ] [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -1765,9 +1800,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -1778,9 +1813,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "once_cell", @@ -1792,9 +1827,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -1804,9 +1839,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -1814,9 +1849,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation", "core-foundation-sys", @@ -1841,9 +1876,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" dependencies = [ "aws-lc-rs", "ring", @@ -1853,15 +1888,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -1872,20 +1907,11 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -1930,11 +1956,11 @@ dependencies = [ "serde_norway", "serde_path_to_error", "serial_test", - "similar", + "similar 3.1.2", "starbase_sandbox", "starbase_styles", "strip-ansi-escapes", - "thiserror 2.0.18", + "thiserror", "toml", "tracing", "url", @@ -1951,7 +1977,7 @@ dependencies = [ "quote", "schematic_core", "starbase_sandbox", - "syn 2.0.114", + "syn 3.0.3", ] [[package]] @@ -1962,7 +1988,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.114", + "syn 3.0.3", ] [[package]] @@ -1971,7 +1997,7 @@ version = "0.18.7" dependencies = [ "quote", "schematic_core", - "syn 2.0.114", + "syn 3.0.3", ] [[package]] @@ -2003,12 +2029,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "seahash" version = "4.1.0" @@ -2017,9 +2037,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", "core-foundation", @@ -2030,9 +2050,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -2040,9 +2060,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -2050,9 +2070,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -2069,29 +2089,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 3.0.3", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "indexmap", "itoa", @@ -2149,35 +2169,44 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.3.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0b343e184fc3b7bb44dff0705fffcf4b3756ba6aff420dddd8b24ca145e555" +checksum = "a6df5ed973ad8d834e09f824f9e9f449af6b9a3745f78dec7cc752770bd3bf11" dependencies = [ "futures-executor", "futures-util", "log", "once_cell", "parking_lot", - "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "3.3.1" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f50427f258fb77356e4cd4aa0e87e2bd2c66dbcee41dc405282cae2bfc26c83" +checksum = "a22144e767da4ddd8416dbf383700542ffd8a5dc493dfecedfe1fe3ad03c98ae" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 3.0.3", ] [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] [[package]] name = "simdutf8" @@ -2191,26 +2220,35 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "similar" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ee016af5d736b69fc89e19254540fa4b5f5492853fb5503920f084011c78b6" +dependencies = [ + "bstr", +] + [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2221,9 +2259,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "starbase_sandbox" -version = "0.10.7" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfc48e15d92211d3e2b2ace7a1d8a8bd448f38128711d41550d7d32a0a7a4e23" +checksum = "2b0f10dd659949fc8d5e65f2bfbb9a24a0b49ce633d766d93efadc8e4966fcf7" dependencies = [ "assert_cmd", "assert_fs", @@ -2235,9 +2273,9 @@ dependencies = [ [[package]] name = "starbase_styles" -version = "0.6.7" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67078591b8bea68aa42e19c61b4a40978e8e3787155012c2aa26331efe07f5e0" +checksum = "fcbd7d71dc8abc949c98dd6d8abaeac06899a6791a2d87407dc1fe26554cdacd" dependencies = [ "dirs", "owo-colors", @@ -2246,13 +2284,14 @@ dependencies = [ [[package]] name = "starbase_utils" -version = "0.12.9" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e939a50874d4a6ffca941be181f73a75b4efbc31b6305f29ab27dd34863e1e2f" +checksum = "879e06a90157702a315c6859585349cd9c5219f2bdf0288e0ac85a3bb5c9234c" dependencies = [ "dirs", + "reflink-copy", "starbase_styles", - "thiserror 2.0.18", + "thiserror", "tracing", ] @@ -2317,9 +2356,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -2343,7 +2393,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.119", ] [[package]] @@ -2354,12 +2404,12 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -2367,12 +2417,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2407,49 +2457,29 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 3.0.3", ] [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -2457,9 +2487,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -2472,9 +2502,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -2496,26 +2526,17 @@ dependencies = [ [[package]] name = "toml" -version = "1.1.2+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 1.1.1+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", - "winnow 1.0.1", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", + "winnow", ] [[package]] @@ -2529,36 +2550,36 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", - "winnow 0.7.14", + "winnow", ] [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ - "winnow 1.0.1", + "winnow", ] [[package]] name = "toml_writer" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -2571,20 +2592,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -2618,7 +2639,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.119", ] [[package]] @@ -2650,9 +2671,9 @@ checksum = "70ba288e709927c043cbe476718d37be306be53fb1fafecd0dbe36d072be2580" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-linebreak" @@ -2662,9 +2683,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-width" @@ -2717,9 +2738,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.19.0" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "js-sys", "serde_core", @@ -2775,46 +2796,35 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", "rustversion", + "serde", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2822,31 +2832,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.119", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -2864,9 +2874,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.5" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a29fc0408b113f68cf32637857ab740edfafdf460c326cd2afaa2d84cc05dc" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] @@ -2880,6 +2890,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -2893,6 +2924,17 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + [[package]] name = "windows-implement" version = "0.60.2" @@ -2901,7 +2943,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.119", ] [[package]] @@ -2912,7 +2954,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.119", ] [[package]] @@ -2921,6 +2963,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" version = "0.4.1" @@ -2939,31 +2991,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -2975,218 +3009,93 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "windows-targets" -version = "0.53.5" +name = "windows-threading" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winnow" -version = "0.7.14" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] -[[package]] -name = "winnow" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "wyz" @@ -3205,9 +3114,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3216,68 +3125,68 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.119", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -3286,9 +3195,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8" dependencies = [ "yoke", "zerofrom", @@ -3297,17 +3206,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 3.0.3", ] [[package]] name = "zmij" -version = "1.0.13" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac93432f5b761b22864c774aac244fa5c0fd877678a4c37ebf6cf42208f9c9ec" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 3c37302a..b80db19c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,27 +3,27 @@ resolver = "2" members = ["crates/*"] [workspace.dependencies] -chrono = "0.4.42" -convert_case = "0.10.0" -darling = "0.23.0" -indexmap = "2.13.0" +chrono = "0.4.45" +convert_case = "0.11.0" +darling = "0.24.0" +indexmap = "2.14.0" miette = "7.6.0" -proc-macro2 = "1.0.103" -quote = "1.0.42" -regex = "1.12.2" +proc-macro2 = "1.0.107" +quote = "1.0.47" +regex = "1.13.1" relative-path = "2.0.1" -reqwest = { version = "0.13.1", default-features = false } -ron = "0.12.0" -rpkl = "0.7.0" -rust_decimal = "1.39.0" -semver = "1.0.27" -serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.149" +reqwest = { version = "0.13.4", default-features = false } +ron = "0.12.2" +rpkl = "0.8.0" +rust_decimal = "1.42.1" +semver = "1.0.28" +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" serde_yaml = "0.9.33" serde_norway = "0.9.42" -starbase_sandbox = "0.10.7" -syn = "2.0.111" -toml = "1.1.2" +starbase_sandbox = "0.12.0" +syn = "3.0.3" +toml = "1.1.4" tracing = "0.1.44" url = "2.5.8" -uuid = "1.19.0" +uuid = "1.24.0" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 326f2a03..8afcddd3 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -26,7 +26,7 @@ schematic_core = { path = ".", features = [ "tracing", "validate", ] } -prettyplease = "0.2.37" +prettyplease = "0.3.0" syn = { workspace = true, features = ["full", "extra-traits"] } starbase_sandbox = { workspace = true } diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index a77fc522..43371b37 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -8,7 +8,7 @@ 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}; +use syn::{Attribute, Expr, ExprPath, Field as NativeField, FieldModifiers, Ident, Visibility}; // #[schema()], #[setting()] #[derive(Debug, FromAttributes, Default)] @@ -60,7 +60,7 @@ pub struct Field { pub attrs: Vec, pub ident: Option, // Named pub index: usize, // Unnamed - pub mutability: FieldMutability, + pub modifiers: FieldModifiers, pub vis: Visibility, } @@ -78,7 +78,7 @@ impl Field { container_args, ident: field.ident, index: 0, - mutability: field.mutability, + modifiers: field.modifiers, serde_args, serde_container_args, vis: field.vis, diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index ae9acd5e..ab36dbf7 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -8,7 +8,7 @@ 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}; +use syn::{Attribute, ExprPath, Fields, FieldsUnnamed, Ident, Variant as NativeVariant}; // #[setting()], #[schema()] #[derive(Debug, Default, FromAttributes)] @@ -361,6 +361,8 @@ impl Variant { #[cfg(feature = "validate")] if let Some(expr) = self.args.validate.as_deref() { + use syn::Expr; + let func = match expr { // func(arg)() Expr::Call(func) => quote! { #func }, 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 index e5db7318..56569b53 100644 --- 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 @@ -48,7 +48,7 @@ expression: defaults token: "a", }, }, - dot_token: Dot, + dot_token: Token![.], method: Ident( into, ), @@ -56,7 +56,7 @@ expression: defaults paren_token: Paren, args: [], }, - Comma, + Token![,], Expr::MethodCall { attrs: [], receiver: Expr::Lit { @@ -65,7 +65,7 @@ expression: defaults token: "b", }, }, - dot_token: Dot, + dot_token: Token![.], method: Ident( into, ), @@ -73,7 +73,7 @@ expression: defaults paren_token: Paren, args: [], }, - Comma, + Token![,], Expr::MethodCall { attrs: [], receiver: Expr::Lit { @@ -82,7 +82,7 @@ expression: defaults token: "c", }, }, - dot_token: Dot, + dot_token: Token![.], method: Ident( into, ), @@ -110,7 +110,7 @@ expression: defaults }, ], }, - bang_token: Not, + bang_token: Token![!], delimiter: MacroDelimiter::Bracket( Bracket, ), @@ -149,11 +149,11 @@ expression: defaults token: 10, }, }, - Comma, + Token![,], Expr::Unary { attrs: [], op: UnOp::Neg( - Minus, + Token![-], ), expr: Expr::Lit { attrs: [], @@ -162,7 +162,7 @@ expression: defaults }, }, }, - Comma, + Token![,], Expr::Lit { attrs: [], lit: Lit::Int { 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 index 6c02fe81..a5b9e73a 100644 --- 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 @@ -41,7 +41,7 @@ expression: defaults token: "a", }, }, - dot_token: Dot, + dot_token: Token![.], method: Ident( into, ), @@ -49,7 +49,7 @@ expression: defaults paren_token: Paren, args: [], }, - Comma, + Token![,], Expr::MethodCall { attrs: [], receiver: Expr::Lit { @@ -58,7 +58,7 @@ expression: defaults token: "b", }, }, - dot_token: Dot, + dot_token: Token![.], method: Ident( into, ), @@ -66,7 +66,7 @@ expression: defaults paren_token: Paren, args: [], }, - Comma, + Token![,], Expr::MethodCall { attrs: [], receiver: Expr::Lit { @@ -75,7 +75,7 @@ expression: defaults token: "c", }, }, - dot_token: Dot, + dot_token: Token![.], method: Ident( into, ), @@ -101,7 +101,7 @@ expression: defaults }, ], }, - bang_token: Not, + bang_token: Token![!], delimiter: MacroDelimiter::Bracket( Bracket, ), @@ -138,11 +138,11 @@ expression: defaults token: 10, }, }, - Comma, + Token![,], Expr::Unary { attrs: [], op: UnOp::Neg( - Minus, + Token![-], ), expr: Expr::Lit { attrs: [], @@ -151,7 +151,7 @@ expression: defaults }, }, }, - Comma, + Token![,], Expr::Lit { attrs: [], lit: Lit::Int { diff --git a/crates/schematic/Cargo.toml b/crates/schematic/Cargo.toml index bca438b4..450f49e1 100644 --- a/crates/schematic/Cargo.toml +++ b/crates/schematic/Cargo.toml @@ -22,16 +22,16 @@ schematic_macros = { version = "0.19.4", path = "../macros" } schematic_types = { version = "0.11.5", path = "../types" } miette = { workspace = true } serde-content = "0.1.2" -thiserror = "2.0.17" +thiserror = "2.0.20" tracing = { workspace = true } # config -garde = { version = "0.22.1", default-features = false, optional = true, features = [ +garde = { version = "0.23.0", default-features = false, optional = true, features = [ "regex", ] } serde = { workspace = true } serde_path_to_error = { version = "0.1.20", optional = true } -starbase_styles = { version = "0.6.5", optional = true } +starbase_styles = { version = "0.6.9", optional = true } # settings regex = { workspace = true, optional = true } @@ -42,7 +42,7 @@ convert_case = { workspace = true, optional = true } indexmap = { workspace = true, optional = true, features = ["serde"] } # json -json-strip-comments = { version = "3.1.0", optional = true } +json-strip-comments = { version = "3.1.2", optional = true } serde_json = { workspace = true, optional = true, features = [ "preserve_order", ] } @@ -143,10 +143,10 @@ schematic = { path = ".", features = [ "yaml", ] } reqwest = { workspace = true, features = ["blocking", "rustls"] } -serial_test = "3.3.1" -similar = "2.7.0" +serial_test = "4.0.1" +similar = "3.1.2" starbase_sandbox = { workspace = true } -strip-ansi-escapes = "0.2" +strip-ansi-escapes = "0.2.1" derive_more = { version = "2.1.1", features = ["try_into", "as_ref"] } # Types diff --git a/crates/schematic/src/config/formats/pkl.rs b/crates/schematic/src/config/formats/pkl.rs index 261f96c6..80b1b250 100644 --- a/crates/schematic/src/config/formats/pkl.rs +++ b/crates/schematic/src/config/formats/pkl.rs @@ -2,7 +2,7 @@ use crate::config::error::ConfigError; use crate::config::parser::ParserError; use crate::config::source::*; use miette::NamedSource; -use rpkl::{EvaluatorOptions, pkl::PklSerialize}; +use rpkl::{EvaluatorOptions, pkl::IntoPklMap}; use serde::de::DeserializeOwned; use std::env; use std::path::Path; @@ -76,14 +76,13 @@ impl SourceFormat for PklFormat { }; // Based on `rpkl::from_config` - let ast = rpkl::api::Evaluator::new_from_options(self.options.clone()) + let map = rpkl::api::Evaluator::new_from_options(self.options.clone()) .map_err(handle_error)? .evaluate_module(file_path) .map_err(handle_error)? - .serialize_pkl_ast() - .map_err(handle_error)?; + .into_pkl_map(); - let mut de = rpkl::pkl::Deserializer::from_pkl_map(&ast); + let mut de = rpkl::pkl::Deserializer::from_pkl_map(&map); let result: T = serde_path_to_error::deserialize(&mut de).map_err(|error| ParserError { content: NamedSource::new(source.get_file_name(), content.to_owned()), diff --git a/crates/schematic/tests/snapshots/partialize_test__enums.snap b/crates/schematic/tests/snapshots/partialize_test__enums.snap index 0b818fb3..149b8d48 100644 --- a/crates/schematic/tests/snapshots/partialize_test__enums.snap +++ b/crates/schematic/tests/snapshots/partialize_test__enums.snap @@ -976,26 +976,7 @@ expression: "create_diff::()" 🟥 "a", 🟥 ), 🟥 }, -🟩 name: None, -🟩 nullable: true, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, -🟩 partial: false, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: Some( -🟩 "UnitEnum", -🟩 ), -🟩 nullable: false, -🟩 ty: Enum( -🟩 EnumType { -🟩 default_index: Some( -🟩 0, -⬛️ ), +🟥 ), 🟥 }, 🟥 deprecated: None, 🟥 env_var: None, @@ -1021,7 +1002,26 @@ expression: "create_diff::()" 🟥 "b", 🟥 ), 🟥 }, -🟥 ), +🟩 name: None, +🟩 nullable: true, +🟩 ty: Union( +🟩 UnionType { +🟩 default_index: None, +🟩 partial: false, +🟩 operator: AnyOf, +🟩 variants_types: [ +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: Some( +🟩 "UnitEnum", +🟩 ), +🟩 nullable: false, +🟩 ty: Enum( +🟩 EnumType { +🟩 default_index: Some( +🟩 0, +⬛️ ), 🟥 }, 🟥 deprecated: None, 🟥 env_var: None, diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 0b149c09..67586ade 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] profile = "default" -channel = "1.95.0" +channel = "1.97.0" From abb4142abd09fc77c03bb4e46481518a937a69cf Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Fri, 14 Aug 2026 19:08:39 -0700 Subject: [PATCH 04/18] internal: Implement `from_partial`. (#191) --- crates/core/src/container.rs | 123 ++++++---- crates/core/src/field.rs | 49 +++- crates/core/src/field_value.rs | 194 +++++++++------ crates/core/src/value.rs | 145 +++++++++-- crates/core/src/variant.rs | 37 ++- crates/core/tests/container_env_test.rs | 16 ++ .../core/tests/container_from_partial_test.rs | 225 ++++++++++++++++++ crates/core/tests/setting_default_test.rs | 38 +++ crates/core/tests/setting_env_test.rs | 45 ++++ crates/core/tests/setting_extend_test.rs | 41 +++- crates/core/tests/setting_required_test.rs | 17 ++ ...v_test__container_env__can_set_prefix.snap | 2 +- ...v__skips_unsupported_types_for_prefix.snap | 14 ++ ...nalize__named_struct__supports_nested.snap | 17 +- ...d_struct__supports_nested_collections.snap | 21 +- ...nalize__unnamed_enum__supports_nested.snap | 12 +- ...med_enum__supports_nested_collections.snap | 20 +- ...lize__unnamed_struct__supports_nested.snap | 17 +- ...d_struct__supports_nested_collections.snap | 21 +- ...artial__named_struct__resets_extended.snap | 10 + ...artial__named_struct__supports_nested.snap | 37 +++ ...d_struct__supports_nested_collections.snap | 43 ++++ ...tial__named_struct__supports_standard.snap | 16 ++ ...ner_from_partial__unit_enum__supports.snap | 11 + ...artial__unnamed_enum__supports_nested.snap | 24 ++ ...med_enum__supports_nested_collections.snap | 42 ++++ ...tial__unnamed_enum__supports_standard.snap | 16 ++ ...tial__unnamed_struct__supports_nested.snap | 37 +++ ...d_struct__supports_nested_collections.snap | 43 ++++ ...al__unnamed_struct__supports_standard.snap | 16 ++ ...default__handles_layers_with_defaults.snap | 18 ++ ...etting_default__handles_nested_layers.snap | 17 ++ ...default__named_struct__supports_types.snap | 4 +- ...fault__unnamed_struct__supports_types.snap | 4 +- ..._named_struct__supports_nested_layers.snap | 14 ++ ...named_struct__supports_optional_types.snap | 14 ++ ..._named_struct__renders_validate_check.snap | 20 ++ 37 files changed, 1255 insertions(+), 185 deletions(-) create mode 100644 crates/core/tests/container_from_partial_test.rs create mode 100644 crates/core/tests/snapshots/container_env_test__container_env__skips_unsupported_types_for_prefix.snap create mode 100644 crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__resets_extended.snap create mode 100644 crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_standard.snap create mode 100644 crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unit_enum__supports.snap create mode 100644 crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_nested.snap create mode 100644 crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_standard.snap create mode 100644 crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_nested_collections.snap create mode 100644 crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_standard.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers_with_defaults.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__handles_nested_layers.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_nested_layers.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_optional_types.snap create mode 100644 crates/core/tests/snapshots/setting_required_test__setting_required__named_struct__renders_validate_check.snap diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 33c1861f..3881e031 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -200,29 +200,59 @@ impl Container { } } - // TODO pub fn impl_full_from_partial(&self) -> TokenStream { - // let inner = match &self.inner { - // ContainerInner::NamedStruct { fields } => { - // let mut statements = vec![]; + let inner = match &self.inner { + ContainerInner::NamedStruct { fields } => { + let mut rows = vec![]; - // for field in fields { - // let res = field.impl_partial_merge(); + for field in fields { + let key = field.get_key(); + let value = field.impl_full_from_partial().value; + + rows.push(quote! { + #key: #value, + }); + } + + quote! { + Self { + #(#rows)* + } + } + } + ContainerInner::UnnamedStruct { fields } => { + let mut rows = vec![]; + + for field in fields { + rows.push(field.impl_full_from_partial().value); + } + + quote! { + Self( + #(#rows),* + ) + } + } + ContainerInner::UnnamedEnum { variants } | ContainerInner::UnitEnum { variants } => { + let partial_name = format_ident!("Partial{}", self.ident); + let mut arms = vec![]; - // if !res.no_value { - // statements.push(res.value); - // } - // } + for variant in variants { + arms.push(variant.impl_full_from_partial(&partial_name).value); + } - // todo!(); - // } - // ContainerInner::UnnamedStruct { fields } => {} - // ContainerInner::UnnamedEnum { variants } => {} - // ContainerInner::UnitEnum { variants } => todo!(), - // }; + quote! { + match partial { + #(#arms)* + } + } + } + }; quote! { - fn from_partial(partial: Self::Partial) -> Self {} + fn from_partial(partial: Self::Partial) -> Self { + #inner + } } } @@ -492,7 +522,7 @@ impl Container { panic!("Attribute `env_prefix` cannot be empty."); } - quote! { prefix.or_else(Some(#env_prefix)) } + quote! { prefix.or(Some(#env_prefix)) } } else { quote! { prefix } }; @@ -522,36 +552,45 @@ impl Container { #[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 }; + let extendable = self + .inner + .get_fields() + .into_iter() + .filter(|field| field.is_extendable()) + .collect::>(); + + // Do not implement method + if extendable.is_empty() { + return quote! {}; + } - for field in fields { - if field.is_extendable() { - names.push(field.get_name_original().to_string()); + if !matches!(self.inner, ContainerInner::NamedStruct { .. }) { + panic!("Only named structs can use `extend` settings."); + } - let res = field.impl_partial_extends_from(); + if extendable.len() > 1 { + let names = extendable + .iter() + .map(|field| field.get_name_original().to_string()) + .collect::>(); - if !res.no_value { - inner = res.value; - } - } - } + panic!( + "Only 1 setting may use `extend`, found: {}", + names.join(", ") + ); + } - if names.len() > 1 { - panic!( - "Only 1 setting may use `extend`, found: {}", - names.join(", ") - ); - } + let res = extendable[0].impl_partial_extends_from(); + let inner = if res.no_value { + quote! { None } + } else { + res.value + }; - quote! { - fn extends_from(&self) -> Option { - #inner - } + quote! { + fn extends_from(&self) -> Option { + #inner } - } else { - panic!("Only named structs can use `extend` settings."); } } diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 43371b37..e18cf54e 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -240,6 +240,49 @@ impl Field { // } impl Field { + pub fn impl_full_from_partial(&self) -> ImplResult { + let key = self.get_key(); + + // Reset extendable values since we don't have the entire resolved list + if self.is_extendable() { + return ImplResult { + value: quote! { Default::default() }, + ..Default::default() + }; + } + + let value = if self.is_nested() { + let data_var = format_ident!("data"); + let inner = self.value.impl_full_from_partial_nested(&data_var).value; + + // When option wrapped, the partial's `Option` is the first layer, + // so pass the value as-is and let the layer unwrap it + let data = if self.value.is_outer_option_wrapped() { + quote! { partial.#key } + } else { + quote! { partial.#key.unwrap_or_default() } + }; + + quote! { + { + let #data_var = #data; + #inner + } + } + } else if self.value.is_outer_option_wrapped() { + // Use optional values as-is as they're already wrapped in `Option` + quote! { partial.#key } + } else { + // Otherwise unwrap the resolved value or use the type default + quote! { partial.#key.unwrap_or_default() } + }; + + ImplResult { + value, + ..Default::default() + } + } + pub fn impl_partial_default_value(&self) -> ImplResult { self.value.impl_partial_default_value(&self.args) } @@ -272,8 +315,10 @@ impl Field { let key = self.get_key(); let mut value = if self.is_nested() { + // The `if let` below consumes the partial's `Option`, which is + // the first `Option` layer when the type is optional self.value - .impl_partial_finalize_nested(&format_ident!("layer")) + .impl_partial_finalize_nested(&format_ident!("layer"), true) .value } else { quote! { layer } @@ -335,7 +380,7 @@ impl Field { #outer if self.#key.is_none() { - validate.required(#key); + validate.required(#key_string); } }; } diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index 3e2bd1b4..bf92b922 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -10,6 +10,36 @@ use syn::{Expr, Lit, Type}; #[derive(Debug)] pub struct FieldValue(Value); +fn is_collection_layer(layer: &Layer) -> bool { + matches!( + layer, + Layer::Map(_) | Layer::Set(_) | Layer::Vec(_) | Layer::Unknown(_) + ) +} + +fn wrap_layer(layer: &Layer, value: TokenStream) -> TokenStream { + 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) }, + // Collections reset to empty, discarding the inner value + Layer::Map(name) | Layer::Set(name) | Layer::Vec(name) | Layer::Unknown(name) => { + let collection = format_ident!("{name}"); + + quote! { #collection::default() } + } + } +} + +fn wrap_default_layers(layers: &[Layer], mut value: TokenStream) -> TokenStream { + for layer in layers.iter().rev() { + value = wrap_layer(layer, value); + } + + value +} + impl Deref for FieldValue { type Target = Value; @@ -29,88 +59,88 @@ impl FieldValue { }; 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 { + // Nested configs source their defaults from the inner partial + 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(); + res.value = if self.is_collection() { + // Collections of nested configs start empty + let value = wrap_default_layers(&self.layers, quote! { Default::default() }); - match expr { - Expr::Array(_) | Expr::Call(_) | Expr::Macro(_) | Expr::Tuple(_) => { - wrap_with_some = true; + quote! { Some(#value) } + } else if self.layers.is_empty() { + quote! { #ident::default_values(context)? } + } else { + // Wrap the inner partial with each layer + let mut value = quote! { inner }; - quote! { #expr } + for layer in self.layers.iter().rev() { + value = wrap_layer(layer, value); } - 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! { #ident::default_values(context)?.map(|inner| #value) } + }; - quote! { - handle_default_result(#ty::try_from(#string))? + return res; + } + + match field_args.default.as_ref() { + // Handler functions return the entire value + Some(Expr::Path(func)) => { + res.requires_internal = true; + res.value = quote! { handle_default_result(#func(context))? }; + } + // Explicit defaults provide the value up to the outermost + // collection, so only wrap with the layers outside of it + Some(expr) => { + let mut value = match expr { + Expr::Array(_) | Expr::Call(_) | Expr::Macro(_) | Expr::Tuple(_) => { + quote! { #expr } + } + Expr::Lit(lit) => match &lit.lit { + Lit::Str(string) => { + let ty = self.get_inner_type(); + res.requires_internal = true; + + quote! { + handle_default_result(#ty::try_from(#string))? + } } + other => quote! { #other }, + }, + invalid => { + panic!( + "Unsupported default value ({invalid:?}). May only provide literals, primitives, arrays, or tuples." + ); } - other => { - wrap_with_some = true; + }; - quote! { #other } - } - }, - invalid => { - panic!( - "Unsupported default value ({invalid:?}). May only provide literals, primitives, arrays, or tuples." - ); + let wrappers = self + .layers + .iter() + .position(is_collection_layer) + .map(|index| &self.layers[..index]) + .unwrap_or(&self.layers[..]); + + for layer in wrappers.iter().rev() { + value = wrap_layer(layer, value); } - } - } else { - wrap_with_some = true; - quote! { - Default::default() + res.value = quote! { Some(#value) }; } - }; + // Otherwise fallback to the type default + None => { + let value = wrap_default_layers(&self.layers, 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() } - } - }; + res.value = quote! { Some(#value) }; } - } - - if wrap_with_some { - value = quote! { Some(#value) }; - } + }; - res.value = value; res } @@ -123,16 +153,24 @@ impl FieldValue { 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`."); - } + // Values can only be sourced from the environment when the type + // is bare or wrapped in a single `Option`, as other layers and + // collections cannot be represented by a variable + let supported = + self.layers.is_empty() || (self.layers.len() == 1 && self.is_outer_option_wrapped()); + + if let Some(nested_ident) = &self.nested_ident { + if !supported { + if field_args.env_prefix.is_some() { + panic!("Cannot use `env_prefix` with collections or wrapped types."); + } + + return ImplResult::skipped(); + } - 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 { + res.value = if let Some(env_prefix) = &field_args.env_prefix { if env_prefix.is_empty() { panic!("Attribute `env_prefix` cannot be empty."); } @@ -144,8 +182,26 @@ impl FieldValue { quote! { env.nested(#ident::env_values()?)? } + }; + + return res; + } + + if !supported { + // Only error for explicit `env` keys, and silently skip keys + // derived from the container-level `env_prefix` + if field_args.env.is_some() { + if self.is_collection() { + panic!("Collection types cannot be used with `env`."); + } else { + panic!("Wrapper types cannot be used with `env`."); + } } - } else if let Some(parse_env) = &field_args.parse_env { + + return ImplResult::skipped(); + } + + res.value = if let Some(parse_env) = &field_args.parse_env { quote! { env.get_and_parse(#env_key, #parse_env)? } diff --git a/crates/core/src/value.rs b/crates/core/src/value.rs index 2019ce3c..2805eeab 100644 --- a/crates/core/src/value.rs +++ b/crates/core/src/value.rs @@ -99,22 +99,137 @@ impl Value { .is_some_and(|layer| *layer == Layer::Option) } - pub fn impl_partial_finalize_nested(&self, layer_var: &Ident) -> ImplResult { + pub fn impl_full_from_partial_nested(&self, data_var: &Ident) -> ImplResult { + let Some(config) = &self.nested_ident else { + return ImplResult::skipped(); + }; + let mut res = ImplResult::default(); - let mut value = quote! { #layer_var.finalize(context)? }; + let mut value = quote! { #config::from_partial(#data_var) }; // 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::Arc => quote! { + { + let #data_var = Arc::unwrap_or_clone(#data_var); + Arc::new(#value) + } + }, + Layer::Rc => quote! { + { + let #data_var = Rc::unwrap_or_clone(#data_var); + Rc::new(#value) + } + }, + Layer::Box => quote! { + { + let #data_var = *#data_var; + Box::new(#value) + } + }, + Layer::Option => quote! { + match #data_var { + Some(#data_var) => Some(#value), + None => None + } + }, + Layer::Map(name) => { + let collection = format_ident!("{name}"); + + quote! { + { + let mut map = #collection::default(); + for (key, #data_var) in #data_var { + map.insert(key, #value); + } + map + } + } + } + Layer::Set(name) => { + let collection = format_ident!("{name}"); + + quote! { + { + let mut set = #collection::default(); + for #data_var in #data_var { + set.insert(#value); + } + set + } + } + } + Layer::Vec(name) => { + let collection = format_ident!("{name}"); + + quote! { + { + let mut list = #collection::default(); + for #data_var in #data_var { + list.push(#value); + } + list + } + } + } + Layer::Unknown(name) => { + let collection = format_ident!("{name}"); + + quote! { #collection::default() } + } + }; + } + } + + res.value = value; + res + } + + pub fn impl_partial_finalize_nested( + &self, + layer_var: &Ident, + skip_outer_option: bool, + ) -> ImplResult { + let mut res = ImplResult::default(); + let mut value = quote! { #layer_var.finalize(context)? }; + + // The first `Option` layer is represented by the partial's own + // `Option`, so skip it when the caller has already unwrapped it + let layers = if skip_outer_option && self.is_outer_option_wrapped() { + &self.layers[1..] + } else { + &self.layers[..] + }; + + // Then wrap with each layer + if !layers.is_empty() { + for layer in layers.iter().rev() { + value = match layer { + Layer::Arc => quote! { + { + let #layer_var = Arc::unwrap_or_clone(#layer_var); + Arc::new(#value) + } + }, + Layer::Rc => quote! { + { + let #layer_var = Rc::unwrap_or_clone(#layer_var); + Rc::new(#value) + } + }, + Layer::Box => quote! { + { + let #layer_var = *#layer_var; + Box::new(#value) + } + }, Layer::Option => quote! { - match #layer_var { - Some(#layer_var) => #value, - None => None - } + match #layer_var { + Some(#layer_var) => Some(#value), + None => None + } }, Layer::Map(name) => { let collection = format_ident!("{name}"); @@ -122,8 +237,8 @@ impl Value { quote! { { let mut map = #collection::default(); - for (key, value) in #layer_var { - map.insert(key, value.finalize(context)?); + for (key, #layer_var) in #layer_var { + map.insert(key, #value); } map } @@ -135,8 +250,8 @@ impl Value { quote! { { let mut set = #collection::default(); - for item in #layer_var { - set.insert(item.finalize(context)?); + for #layer_var in #layer_var { + set.insert(#value); } set } @@ -148,8 +263,8 @@ impl Value { quote! { { let mut list = #collection::default(); - for item in #layer_var { - list.push(item.finalize(context)?); + for #layer_var in #layer_var { + list.push(#value); } list } diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index ab36dbf7..afb960d4 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -176,6 +176,39 @@ impl Variant { self.values.is_empty() } + pub fn impl_full_from_partial(&self, partial_name: &Ident) -> 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) => { + self.map_unnamed_match_custom(name, partial_name, fields, |outer_names, _| { + let items = outer_names + .iter() + .enumerate() + .map(|(index, o)| { + if self.is_nested() { + self.values[index].impl_full_from_partial_nested(o).value + } else { + quote! { #o } + } + }) + .collect::>(); + + quote! { + Self::#name(#(#items),*) + } + }) + } + Fields::Unit => quote! { + #partial_name::#name => Self::#name, + }, + }; + + res + } + pub fn impl_partial_default_value(&self) -> ImplResult { let mut res = ImplResult::default(); let name = &self.ident; @@ -218,7 +251,9 @@ impl Variant { .enumerate() .map(|(i, o)| { let value = if self.is_nested() { - self.values[i].impl_partial_finalize_nested(o).value + // Variant values have no partial `Option` + // wrapper, so walk all layers + self.values[i].impl_partial_finalize_nested(o, false).value } else { quote! { #o } }; diff --git a/crates/core/tests/container_env_test.rs b/crates/core/tests/container_env_test.rs index 30b179ab..e33cf675 100644 --- a/crates/core/tests/container_env_test.rs +++ b/crates/core/tests/container_env_test.rs @@ -31,6 +31,22 @@ mod container_env { assert_snapshot!(pretty(container.impl_partial_env_values())); } + #[test] + fn skips_unsupported_types_for_prefix() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(env_prefix = "PREFIX_")] + struct Example { + a: String, + b: Vec, + c: Box, + d: Option, + } + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + #[test] fn can_set_prefix() { let container = Container::from(parse_quote! { diff --git a/crates/core/tests/container_from_partial_test.rs b/crates/core/tests/container_from_partial_test.rs new file mode 100644 index 00000000..435c22d4 --- /dev/null +++ b/crates/core/tests/container_from_partial_test.rs @@ -0,0 +1,225 @@ +mod utils; + +use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; +use utils::pretty; + +mod container_from_partial { + 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, + c: String, + d: i16, + e: Option, + f: Vec, + g: Option>, + h: Box, + } + }); + + assert_snapshot!(pretty(container.impl_full_from_partial())); + } + + #[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, + #[setting(nested)] + e: Box, + } + }); + + assert_snapshot!(pretty(container.impl_full_from_partial())); + } + + #[test] + fn supports_nested_collections() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: Vec, + #[setting(nested = CustomConfig)] + b: HashMap, + #[setting(nested)] + c: Option>, + } + }); + + assert_snapshot!(pretty(container.impl_full_from_partial())); + } + + #[test] + fn resets_extended() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(extend)] + a: Vec, + b: bool, + } + }); + + assert_snapshot!(pretty(container.impl_full_from_partial())); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + bool, + usize, + String, + i16, + Option, + Vec, + Option>, + Box, + ); + }); + + assert_snapshot!(pretty(container.impl_full_from_partial())); + } + + #[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, + #[setting(nested)] + Box, + ); + }); + + assert_snapshot!(pretty(container.impl_full_from_partial())); + } + + #[test] + fn supports_nested_collections() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(nested)] + Vec, + #[setting(nested = CustomConfig)] + HashMap, + #[setting(nested)] + Option>, + ); + }); + + assert_snapshot!(pretty(container.impl_full_from_partial())); + } + } + + 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), + C(String), + D(i16), + E(Option), + F(Vec), + G(Option>), + H(String, usize), + } + }); + + assert_snapshot!(pretty(container.impl_full_from_partial())); + } + + #[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_full_from_partial())); + } + + #[test] + fn supports_nested_collections() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A(Vec), + #[setting(nested = CustomConfig)] + B(HashMap), + #[setting(nested)] + C(Option>), + } + }); + + assert_snapshot!(pretty(container.impl_full_from_partial())); + } + } + + mod unit_enum { + use super::*; + + #[test] + fn supports() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A, B, C + } + }); + + assert_snapshot!(pretty(container.impl_full_from_partial())); + } + } +} diff --git a/crates/core/tests/setting_default_test.rs b/crates/core/tests/setting_default_test.rs index bacb80ec..db770766 100644 --- a/crates/core/tests/setting_default_test.rs +++ b/crates/core/tests/setting_default_test.rs @@ -41,6 +41,44 @@ mod setting_default { assert_snapshot!(pretty(container.impl_partial_default_values())); } + #[test] + fn handles_layers_with_defaults() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = 10)] + a: Box, + #[setting(default = 10)] + b: Arc>, + #[setting(default = vec![1, 2, 3])] + c: Vec, + #[setting(default = vec![1, 2, 3])] + d: Arc>, + #[setting(default = "abc")] + e: Box, + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + + #[test] + fn handles_nested_layers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: Box, + #[setting(nested = CustomConfig)] + b: Arc, + #[setting(nested)] + c: Vec, + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + #[test] fn supports_handler_func() { let container = Container::from(parse_quote! { diff --git a/crates/core/tests/setting_env_test.rs b/crates/core/tests/setting_env_test.rs index e1f41e73..685d4ea7 100644 --- a/crates/core/tests/setting_env_test.rs +++ b/crates/core/tests/setting_env_test.rs @@ -98,6 +98,51 @@ mod setting_env { assert_snapshot!(pretty(container.impl_partial_env_values())); } + + #[test] + fn supports_optional_types() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env = "A")] + a: Option, + #[setting(env = "B", parse_env = parse_func)] + b: Option, + } + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + + #[test] + fn supports_nested_layers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: NestedConfig, + #[setting(nested)] + b: Option, + #[setting(nested)] + c: Vec, + } + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + + #[test] + #[should_panic(expected = "Cannot use `env_prefix` with collections or wrapped types.")] + fn errors_if_prefix_on_nested_collection() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested, env_prefix = "X_")] + a: Vec, + } + }) + .impl_partial_env_values(); + } } mod unnamed_struct { diff --git a/crates/core/tests/setting_extend_test.rs b/crates/core/tests/setting_extend_test.rs index 8c795818..1cde9550 100644 --- a/crates/core/tests/setting_extend_test.rs +++ b/crates/core/tests/setting_extend_test.rs @@ -101,6 +101,18 @@ mod setting_extend { assert_snapshot!(pretty(container.impl_partial_extends_from())); } + #[test] + fn skips_when_unused() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: String, + } + }); + + assert!(container.impl_partial_extends_from().is_empty()); + } + #[test] #[should_panic(expected = "Only 1 setting may use `extend`, found: a, b")] fn errors_multiple_extends() { @@ -154,10 +166,35 @@ mod setting_extend { } mod unnamed_enum { - // N/A + use super::*; + + #[test] + fn skips_when_unused() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A(bool), + B(usize), + } + }); + + assert!(container.impl_partial_extends_from().is_empty()); + } } mod unit_enum { - // N/A + use super::*; + + #[test] + fn skips_when_unused() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A, B, C + } + }); + + assert!(container.impl_partial_extends_from().is_empty()); + } } } diff --git a/crates/core/tests/setting_required_test.rs b/crates/core/tests/setting_required_test.rs index c08aad90..fc262be9 100644 --- a/crates/core/tests/setting_required_test.rs +++ b/crates/core/tests/setting_required_test.rs @@ -1,5 +1,9 @@ +mod utils; + use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; use syn::parse_quote; +use utils::pretty; mod setting_required { use super::*; @@ -7,6 +11,19 @@ mod setting_required { mod named_struct { use super::*; + #[test] + fn renders_validate_check() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(required)] + a: Option, + } + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } + #[test] fn accepts_bool() { let container = Container::from(parse_quote! { 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 index 9f3c86e7..f52e1c72 100644 --- 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 @@ -6,7 +6,7 @@ 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 env = EnvManager::new(prefix.or(Some("PREFIX_"))); let mut partial = Self::default(); partial.a = env.get("OVERRIDE")?; partial.b = env.get("B")?; diff --git a/crates/core/tests/snapshots/container_env_test__container_env__skips_unsupported_types_for_prefix.snap b/crates/core/tests/snapshots/container_env_test__container_env__skips_unsupported_types_for_prefix.snap new file mode 100644 index 00000000..6bb9f6c6 --- /dev/null +++ b/crates/core/tests/snapshots/container_env_test__container_env__skips_unsupported_types_for_prefix.snap @@ -0,0 +1,14 @@ +--- +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(Some("PREFIX_"))); + let mut partial = Self::default(); + partial.a = env.get("A")?; + partial.d = env.get("D")?; + 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 index b6187dc8..e53b82fd 100644 --- 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 @@ -21,15 +21,18 @@ fn finalize( 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, - }, - ); + partial.c = Some(layer.finalize(context)?); } if let Some(layer) = partial.d { - partial.d = Some(transform_config(Arc::new(layer.finalize(context)?), context)?); + partial.d = Some( + transform_config( + { + let layer = Arc::unwrap_or_clone(layer); + 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 index 8716e63e..6739ef69 100644 --- 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 @@ -17,8 +17,8 @@ fn finalize( if let Some(layer) = partial.a { partial.a = Some({ let mut list = Vec::default(); - for item in layer { - list.push(item.finalize(context)?); + for layer in layer { + list.push(layer.finalize(context)?); } list }); @@ -28,8 +28,8 @@ fn finalize( transform_config( { let mut map = HashMap::default(); - for (key, value) in layer { - map.insert(key, value.finalize(context)?); + for (key, layer) in layer { + map.insert(key, layer.finalize(context)?); } map }, @@ -40,15 +40,12 @@ fn finalize( 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 + { + let mut set = BTreeSet::default(); + for layer in layer { + set.insert(layer.finalize(context)?); } - None => None, + set }, context, )?, 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 index 91411930..bd671ba5 100644 --- 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 @@ -13,13 +13,21 @@ fn finalize( Self::C(pa) => { Self::C( match pa { - Some(pa) => pa.finalize(context)?, + Some(pa) => Some(pa.finalize(context)?), None => None, }, ) } Self::D(pa) => { - Self::D(transform_config(Arc::new(pa.finalize(context)?), context)?) + Self::D( + transform_config( + { + let pa = Arc::unwrap_or_clone(pa); + 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 index 75fc0b7e..0d975462 100644 --- 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 @@ -11,8 +11,8 @@ fn finalize( Self::A(pa) => { Self::A({ let mut list = Vec::default(); - for item in pa { - list.push(item.finalize(context)?); + for pa in pa { + list.push(pa.finalize(context)?); } list }) @@ -22,8 +22,8 @@ fn finalize( transform_config( { let mut map = HashMap::default(); - for (key, value) in pa { - map.insert(key, value.finalize(context)?); + for (key, pa) in pa { + map.insert(key, pa.finalize(context)?); } map }, @@ -36,11 +36,13 @@ fn finalize( transform_config( match pa { Some(pa) => { - let mut set = BTreeSet::default(); - for item in pa { - set.insert(item.finalize(context)?); - } - set + Some({ + let mut set = BTreeSet::default(); + for pa in pa { + set.insert(pa.finalize(context)?); + } + set + }) } None => None, }, 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 index a4864557..5785c7a5 100644 --- 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 @@ -21,15 +21,18 @@ fn finalize( 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, - }, - ); + partial.2 = Some(layer.finalize(context)?); } if let Some(layer) = partial.3 { - partial.3 = Some(transform_config(Arc::new(layer.finalize(context)?), context)?); + partial.3 = Some( + transform_config( + { + let layer = Arc::unwrap_or_clone(layer); + 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 index 117adfb0..a86fa070 100644 --- 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 @@ -17,8 +17,8 @@ fn finalize( if let Some(layer) = partial.0 { partial.0 = Some({ let mut list = Vec::default(); - for item in layer { - list.push(item.finalize(context)?); + for layer in layer { + list.push(layer.finalize(context)?); } list }); @@ -28,8 +28,8 @@ fn finalize( transform_config( { let mut map = HashMap::default(); - for (key, value) in layer { - map.insert(key, value.finalize(context)?); + for (key, layer) in layer { + map.insert(key, layer.finalize(context)?); } map }, @@ -40,15 +40,12 @@ fn finalize( 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 + { + let mut set = BTreeSet::default(); + for layer in layer { + set.insert(layer.finalize(context)?); } - None => None, + set }, context, )?, diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__resets_extended.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__resets_extended.snap new file mode 100644 index 00000000..d3a8b0a9 --- /dev/null +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__resets_extended.snap @@ -0,0 +1,10 @@ +--- +source: crates/core/tests/container_from_partial_test.rs +expression: pretty(container.impl_full_from_partial()) +--- +fn from_partial(partial: Self::Partial) -> Self { + Self { + a: Default::default(), + b: partial.b.unwrap_or_default(), + } +} diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_nested.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_nested.snap new file mode 100644 index 00000000..4312fa9e --- /dev/null +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_nested.snap @@ -0,0 +1,37 @@ +--- +source: crates/core/tests/container_from_partial_test.rs +expression: pretty(container.impl_full_from_partial()) +--- +fn from_partial(partial: Self::Partial) -> Self { + Self { + a: { + let data = partial.a.unwrap_or_default(); + NestedConfig::from_partial(data) + }, + b: { + let data = partial.b.unwrap_or_default(); + CustomConfig::from_partial(data) + }, + c: { + let data = partial.c; + match data { + Some(data) => Some(NestedConfig::from_partial(data)), + None => None, + } + }, + d: { + let data = partial.d.unwrap_or_default(); + { + let data = Arc::unwrap_or_clone(data); + Arc::new(CustomConfig::from_partial(data)) + } + }, + e: { + let data = partial.e.unwrap_or_default(); + { + let data = *data; + Box::new(NestedConfig::from_partial(data)) + } + }, + } +} diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_nested_collections.snap new file mode 100644 index 00000000..ccacaf5e --- /dev/null +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_nested_collections.snap @@ -0,0 +1,43 @@ +--- +source: crates/core/tests/container_from_partial_test.rs +expression: pretty(container.impl_full_from_partial()) +--- +fn from_partial(partial: Self::Partial) -> Self { + Self { + a: { + let data = partial.a.unwrap_or_default(); + { + let mut list = Vec::default(); + for data in data { + list.push(NestedConfig::from_partial(data)); + } + list + } + }, + b: { + let data = partial.b.unwrap_or_default(); + { + let mut map = HashMap::default(); + for (key, data) in data { + map.insert(key, CustomConfig::from_partial(data)); + } + map + } + }, + c: { + let data = partial.c; + match data { + Some(data) => { + Some({ + let mut set = BTreeSet::default(); + for data in data { + set.insert(NestedConfig::from_partial(data)); + } + set + }) + } + None => None, + } + }, + } +} diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_standard.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_standard.snap new file mode 100644 index 00000000..6e14f0d9 --- /dev/null +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_standard.snap @@ -0,0 +1,16 @@ +--- +source: crates/core/tests/container_from_partial_test.rs +expression: pretty(container.impl_full_from_partial()) +--- +fn from_partial(partial: Self::Partial) -> Self { + Self { + a: partial.a.unwrap_or_default(), + b: partial.b.unwrap_or_default(), + c: partial.c.unwrap_or_default(), + d: partial.d.unwrap_or_default(), + e: partial.e, + f: partial.f.unwrap_or_default(), + g: partial.g, + h: partial.h.unwrap_or_default(), + } +} diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unit_enum__supports.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unit_enum__supports.snap new file mode 100644 index 00000000..6df63eaf --- /dev/null +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unit_enum__supports.snap @@ -0,0 +1,11 @@ +--- +source: crates/core/tests/container_from_partial_test.rs +expression: pretty(container.impl_full_from_partial()) +--- +fn from_partial(partial: Self::Partial) -> Self { + match partial { + PartialExample::A => Self::A, + PartialExample::B => Self::B, + PartialExample::C => Self::C, + } +} diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_nested.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_nested.snap new file mode 100644 index 00000000..2e576c4a --- /dev/null +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_nested.snap @@ -0,0 +1,24 @@ +--- +source: crates/core/tests/container_from_partial_test.rs +expression: pretty(container.impl_full_from_partial()) +--- +fn from_partial(partial: Self::Partial) -> Self { + match partial { + PartialExample::A(pa) => Self::A(NestedConfig::from_partial(pa)), + PartialExample::B(pa) => Self::B(CustomConfig::from_partial(pa)), + PartialExample::C(pa) => { + Self::C( + match pa { + Some(pa) => Some(NestedConfig::from_partial(pa)), + None => None, + }, + ) + } + PartialExample::D(pa) => { + Self::D({ + let pa = Arc::unwrap_or_clone(pa); + Arc::new(CustomConfig::from_partial(pa)) + }) + } + } +} diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_nested_collections.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_nested_collections.snap new file mode 100644 index 00000000..4842b40a --- /dev/null +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_nested_collections.snap @@ -0,0 +1,42 @@ +--- +source: crates/core/tests/container_from_partial_test.rs +expression: pretty(container.impl_full_from_partial()) +--- +fn from_partial(partial: Self::Partial) -> Self { + match partial { + PartialExample::A(pa) => { + Self::A({ + let mut list = Vec::default(); + for pa in pa { + list.push(NestedConfig::from_partial(pa)); + } + list + }) + } + PartialExample::B(pa) => { + Self::B({ + let mut map = HashMap::default(); + for (key, pa) in pa { + map.insert(key, CustomConfig::from_partial(pa)); + } + map + }) + } + PartialExample::C(pa) => { + Self::C( + match pa { + Some(pa) => { + Some({ + let mut set = BTreeSet::default(); + for pa in pa { + set.insert(NestedConfig::from_partial(pa)); + } + set + }) + } + None => None, + }, + ) + } + } +} diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_standard.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_standard.snap new file mode 100644 index 00000000..6ea56cf6 --- /dev/null +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_standard.snap @@ -0,0 +1,16 @@ +--- +source: crates/core/tests/container_from_partial_test.rs +expression: pretty(container.impl_full_from_partial()) +--- +fn from_partial(partial: Self::Partial) -> Self { + match partial { + PartialExample::A(pa) => Self::A(pa), + PartialExample::B(pa) => Self::B(pa), + PartialExample::C(pa) => Self::C(pa), + PartialExample::D(pa) => Self::D(pa), + PartialExample::E(pa) => Self::E(pa), + PartialExample::F(pa) => Self::F(pa), + PartialExample::G(pa) => Self::G(pa), + PartialExample::H(pa, pb) => Self::H(pa, pb), + } +} diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_nested.snap new file mode 100644 index 00000000..129fd764 --- /dev/null +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_nested.snap @@ -0,0 +1,37 @@ +--- +source: crates/core/tests/container_from_partial_test.rs +expression: pretty(container.impl_full_from_partial()) +--- +fn from_partial(partial: Self::Partial) -> Self { + Self( + { + let data = partial.0.unwrap_or_default(); + NestedConfig::from_partial(data) + }, + { + let data = partial.1.unwrap_or_default(); + CustomConfig::from_partial(data) + }, + { + let data = partial.2; + match data { + Some(data) => Some(NestedConfig::from_partial(data)), + None => None, + } + }, + { + let data = partial.3.unwrap_or_default(); + { + let data = Arc::unwrap_or_clone(data); + Arc::new(CustomConfig::from_partial(data)) + } + }, + { + let data = partial.4.unwrap_or_default(); + { + let data = *data; + Box::new(NestedConfig::from_partial(data)) + } + }, + ) +} diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_nested_collections.snap new file mode 100644 index 00000000..14eeca76 --- /dev/null +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_nested_collections.snap @@ -0,0 +1,43 @@ +--- +source: crates/core/tests/container_from_partial_test.rs +expression: pretty(container.impl_full_from_partial()) +--- +fn from_partial(partial: Self::Partial) -> Self { + Self( + { + let data = partial.0.unwrap_or_default(); + { + let mut list = Vec::default(); + for data in data { + list.push(NestedConfig::from_partial(data)); + } + list + } + }, + { + let data = partial.1.unwrap_or_default(); + { + let mut map = HashMap::default(); + for (key, data) in data { + map.insert(key, CustomConfig::from_partial(data)); + } + map + } + }, + { + let data = partial.2; + match data { + Some(data) => { + Some({ + let mut set = BTreeSet::default(); + for data in data { + set.insert(NestedConfig::from_partial(data)); + } + set + }) + } + None => None, + } + }, + ) +} diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_standard.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_standard.snap new file mode 100644 index 00000000..cba5d5d9 --- /dev/null +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_standard.snap @@ -0,0 +1,16 @@ +--- +source: crates/core/tests/container_from_partial_test.rs +expression: pretty(container.impl_full_from_partial()) +--- +fn from_partial(partial: Self::Partial) -> Self { + Self( + partial.0.unwrap_or_default(), + partial.1.unwrap_or_default(), + partial.2.unwrap_or_default(), + partial.3.unwrap_or_default(), + partial.4, + partial.5.unwrap_or_default(), + partial.6, + partial.7.unwrap_or_default(), + ) +} diff --git a/crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers_with_defaults.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers_with_defaults.snap new file mode 100644 index 00000000..022728a6 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers_with_defaults.snap @@ -0,0 +1,18 @@ +--- +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: Some(Box::new(10)), + b: Some(Arc::new(Some(10))), + c: Some(vec![1, 2, 3]), + d: Some(Arc::new(vec![1, 2, 3])), + e: Some(Box::new(handle_default_result(String::try_from("abc"))?)), + }), + ) +} diff --git a/crates/core/tests/snapshots/setting_default_test__setting_default__handles_nested_layers.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_nested_layers.snap new file mode 100644 index 00000000..c4bf12d5 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_nested_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 { + a: PartialNestedConfig::default_values(context)? + .map(|inner| Box::new(inner)), + b: PartialCustomConfig::default_values(context)? + .map(|inner| Arc::new(inner)), + c: Some(Vec::default()), + }), + ) +} 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 index c60ce1c7..51d3c950 100644 --- 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 @@ -11,9 +11,9 @@ fn default_values( no_default: Some(Default::default()), a: Some(true), b: Some(100), - c: handle_default_result(String::try_from("abc"))?, + c: Some(handle_default_result(String::try_from("abc"))?), d: Some(["a".into(), "b".into(), "c".into()]), - e: Some(Vec::default()), + e: Some(vec!["a", "b", "c"]), f: Some((10, -10, 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 index 4f9f2be2..a109ff93 100644 --- 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 @@ -12,9 +12,9 @@ fn default_values( Some(Default::default()), Some(true), Some(100), - handle_default_result(String::try_from("abc"))?, + Some(handle_default_result(String::try_from("abc"))?), Some(["a".into(), "b".into(), "c".into()]), - Some(Vec::default()), + Some(vec!["a", "b", "c"]), Some((10, -10, 0)), ), ), diff --git a/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_nested_layers.snap b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_nested_layers.snap new file mode 100644 index 00000000..41789d32 --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_nested_layers.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(PartialNestedConfig::env_values()?)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_optional_types.snap b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_optional_types.snap new file mode 100644 index 00000000..c59fb3ae --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_optional_types.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.get("A")?; + partial.b = env.get_and_parse("B", parse_func)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/setting_required_test__setting_required__named_struct__renders_validate_check.snap b/crates/core/tests/snapshots/setting_required_test__setting_required__named_struct__renders_validate_check.snap new file mode 100644 index 00000000..37578968 --- /dev/null +++ b/crates/core/tests/snapshots/setting_required_test__setting_required__named_struct__renders_validate_check.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/setting_required_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 self.a.is_none() { + validate.required("a"); + } + if !validate.errors.is_empty() { + return Err(validate.errors); + } + Ok(()) +} From cff9b5d6b9920032a75c6f148d78e177af7a7204 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sat, 15 Aug 2026 09:35:38 -0700 Subject: [PATCH 05/18] internal: Implement container attributes. --- crates/core/src/args.rs | 29 +- crates/core/src/container.rs | 47 ++- crates/core/src/utils.rs | 18 +- crates/core/tests/container_attrs_test.rs | 272 ++++++++++++++++++ ..._partial_attrs__appends_partial_attrs.snap | 7 + ...rtial_attrs__inherits_supported_attrs.snap | 13 + ...ers_serde_then_inherited_then_partial.snap | 6 + ..._attrs__renders_serde_only_by_default.snap | 5 + ...__named_struct__allows_unknown_fields.snap | 5 + ...d_struct__config_args_take_precedence.snap | 5 + ...al_serde_args__named_struct__defaults.snap | 5 + ...s__named_struct__inherits_config_args.snap | 5 + ...gs__named_struct__inherits_serde_args.snap | 5 + ...rde_denies_unknown_fields_over_config.snap | 5 + ..._named_struct__supports_split_renames.snap | 5 + ...rtial_serde_args__unit_enum__defaults.snap | 5 + ...e_args__unnamed_enum__supports_tagged.snap | 5 + ...args__unnamed_enum__supports_untagged.snap | 5 + ..._serde_args__unnamed_struct__defaults.snap | 5 + 19 files changed, 429 insertions(+), 23 deletions(-) create mode 100644 crates/core/tests/container_attrs_test.rs create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_attrs__appends_partial_attrs.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_attrs__inherits_supported_attrs.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_attrs__orders_serde_then_inherited_then_partial.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_attrs__renders_serde_only_by_default.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__allows_unknown_fields.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__config_args_take_precedence.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__defaults.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__inherits_config_args.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__inherits_serde_args.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__serde_denies_unknown_fields_over_config.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__supports_split_renames.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unit_enum__defaults.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unnamed_enum__supports_tagged.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unnamed_enum__supports_untagged.snap create mode 100644 crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unnamed_struct__defaults.snap diff --git a/crates/core/src/args.rs b/crates/core/src/args.rs index 714e31d9..6a7a4be5 100644 --- a/crates/core/src/args.rs +++ b/crates/core/src/args.rs @@ -2,7 +2,7 @@ use crate::utils::to_type_string; use darling::ast::NestedMeta; use darling::{FromAttributes, FromDeriveInput, FromMeta}; use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; +use quote::{ToTokens, format_ident, quote}; use std::ops::Deref; use syn::{Expr, Ident}; @@ -68,6 +68,9 @@ impl SerdeRenameArg { } pub fn get_meta(&self, key: &str) -> TokenStream { + // Format as an identifier, otherwise it'll be quoted as a string + let key = format_ident!("{key}"); + match (self.deserialize.as_deref(), self.serialize.as_deref()) { (Some(de), Some(ser)) => { if de == ser { @@ -81,6 +84,10 @@ impl SerdeRenameArg { _ => quote! {}, } } + + pub fn is_empty(&self) -> bool { + self.deserialize.is_none() && self.serialize.is_none() + } } // #[serde()] @@ -122,18 +129,28 @@ pub struct SerdeFieldArgs { pub untagged: bool, } -// #[setting(partial)] +// #[config(partial(derive(Other), serde(another)))] +// #[setting(partial(serde(another)))] #[derive(Debug, Default)] pub struct PartialArg { meta: Vec, } +impl PartialArg { + // Each meta item is emitted as its own attribute, + // for example `#[derive(Other)] #[serde(another)]` + pub fn get_attributes(&self) -> Vec { + self.meta + .iter() + .map(|meta| quote! { #[#meta] }) + .collect() + } +} + 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),*]}); + for attr in self.get_attributes() { + tokens.extend(attr); } } } diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 3881e031..11bd0155 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -104,18 +104,28 @@ impl Container { fn validate_args(&self) {} pub fn get_partial_attributes(&self) -> Vec { + let mut attrs = vec![]; + + // Serde attributes come first, so that they take precedence + // over any provided by the user via `partial(serde(...))` let serde_args = self.get_partial_serde_attribute_args(); - let mut attrs = vec![quote! { #[serde(#serde_args) ]}]; + if !serde_args.is_empty() { + attrs.push(quote! { #[serde(#serde_args)] }); + } + + // Inherit non-schematic attributes from the container, + // like `doc`, `allow`, and `deprecated` for attr in &self.attrs { if is_inheritable_attribute(attr) { attrs.push(quote! { #attr }); } } - // TODO - // let partial = &self.args.partial; - // attrs.push(quote! { #partial }); + // Then apply any user-provided partial attributes + if let Some(partial) = &self.args.partial { + attrs.extend(partial.get_attributes()); + } attrs } @@ -156,16 +166,25 @@ impl Container { 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")); + // Config attributes take precedence over serde attributes + let renames = [ + ("rename", &self.args.rename, &self.serde_args.rename), + ("rename_all", &self.args.rename_all, &self.serde_args.rename_all), + ( + "rename_all_fields", + &self.args.rename_all_fields, + &self.serde_args.rename_all_fields, + ), + ]; + + for (key, config_arg, serde_arg) in renames { + if let Some(rename) = config_arg + .as_ref() + .or(serde_arg.as_ref()) + .filter(|rename| !rename.is_empty()) + { + meta.push(rename.get_meta(key)); + } } quote! { diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index 5bc2541c..0dfa8e53 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -21,9 +21,21 @@ pub fn preserve_str_literal(meta: &Meta) -> darling::Result { 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)) + [ + // Lints + "allow", + "expect", + "warn", + // Docs + "deprecated", + "doc", + // Compilation + "cfg", + "default", + "non_exhaustive", + ] + .into_iter() + .any(|n| path.is_ident(n)) } pub fn to_type_string(ts: TokenStream) -> String { diff --git a/crates/core/tests/container_attrs_test.rs b/crates/core/tests/container_attrs_test.rs new file mode 100644 index 00000000..c68ad7d1 --- /dev/null +++ b/crates/core/tests/container_attrs_test.rs @@ -0,0 +1,272 @@ +use proc_macro2::TokenStream; +use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; + +// Render each attribute on its own line +fn pretty_attrs(attrs: Vec) -> String { + attrs + .into_iter() + .map(|attr| attr.to_string()) + .collect::>() + .join("\n") +} + +mod partial_serde_args { + use super::*; + + mod named_struct { + use super::*; + + #[test] + fn defaults() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: bool, + } + }); + + assert_snapshot!(container.get_partial_serde_attribute_args().to_string()); + } + + #[test] + fn allows_unknown_fields() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(allow_unknown_fields)] + struct Example { + a: bool, + } + }); + + assert_snapshot!(container.get_partial_serde_attribute_args().to_string()); + } + + #[test] + fn serde_denies_unknown_fields_over_config() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(allow_unknown_fields)] + #[serde(deny_unknown_fields)] + struct Example { + a: bool, + } + }); + + assert_snapshot!(container.get_partial_serde_attribute_args().to_string()); + } + + #[test] + fn inherits_serde_args() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(rename = "Renamed", rename_all = "kebab-case", expecting = "expected")] + struct Example { + a: bool, + } + }); + + assert_snapshot!(container.get_partial_serde_attribute_args().to_string()); + } + + #[test] + fn inherits_config_args() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(rename = "Renamed", rename_all = "kebab-case")] + struct Example { + a: bool, + } + }); + + assert_snapshot!(container.get_partial_serde_attribute_args().to_string()); + } + + #[test] + fn config_args_take_precedence() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(rename = "FromConfig")] + #[serde(rename = "FromSerde", rename_all = "kebab-case")] + struct Example { + a: bool, + } + }); + + assert_snapshot!(container.get_partial_serde_attribute_args().to_string()); + } + + #[test] + fn supports_split_renames() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde( + rename(deserialize = "De"), + rename_all(deserialize = "kebab-case", serialize = "camelCase"), + )] + struct Example { + a: bool, + } + }); + + assert_snapshot!(container.get_partial_serde_attribute_args().to_string()); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn defaults() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example(bool); + }); + + assert_snapshot!(container.get_partial_serde_attribute_args().to_string()); + } + } + + mod unnamed_enum { + use super::*; + + #[test] + fn defaults() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A(bool), + B(usize), + } + }); + + assert!(container.get_partial_serde_attribute_args().is_empty()); + } + + #[test] + fn supports_untagged() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(untagged)] + enum Example { + A(bool), + B(usize), + } + }); + + assert_snapshot!(container.get_partial_serde_attribute_args().to_string()); + } + + #[test] + fn supports_tagged() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(tag = "type", content = "value", rename_all_fields = "camelCase")] + enum Example { + A(bool), + B(usize), + } + }); + + assert_snapshot!(container.get_partial_serde_attribute_args().to_string()); + } + } + + mod unit_enum { + use super::*; + + #[test] + fn defaults() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(rename_all = "kebab-case")] + enum Example { + A, + B, + } + }); + + assert_snapshot!(container.get_partial_serde_attribute_args().to_string()); + } + } +} + +mod partial_attrs { + use super::*; + + #[test] + fn renders_serde_only_by_default() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: bool, + } + }); + + assert_snapshot!(pretty_attrs(container.get_partial_attributes())); + } + + #[test] + fn skips_serde_when_empty() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A(bool), + } + }); + + assert!(container.get_partial_attributes().is_empty()); + } + + #[test] + fn inherits_supported_attrs() { + let container = Container::from(parse_quote! { + /// Some docs. + /// More docs. + #[derive(Config)] + #[allow(dead_code)] + #[expect(missing_docs)] + #[warn(unused)] + #[deprecated] + #[cfg(feature = "example")] + #[non_exhaustive] + #[repr(C)] + #[cfg_attr(test, derive(Other))] + #[config(allow_unknown_fields)] + #[serde(rename = "Renamed")] + struct Example { + a: bool, + } + }); + + assert_snapshot!(pretty_attrs(container.get_partial_attributes())); + } + + #[test] + fn appends_partial_attrs() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(partial(derive(Other, Another), serde(rename_all = "kebab-case")))] + struct Example { + a: bool, + } + }); + + assert_snapshot!(pretty_attrs(container.get_partial_attributes())); + } + + #[test] + fn orders_serde_then_inherited_then_partial() { + let container = Container::from(parse_quote! { + /// Docs. + #[derive(Config)] + #[config(partial(derive(Other)))] + enum Example { + A(bool), + } + }); + + assert_snapshot!(pretty_attrs(container.get_partial_attributes())); + } +} diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_attrs__appends_partial_attrs.snap b/crates/core/tests/snapshots/container_attrs_test__partial_attrs__appends_partial_attrs.snap new file mode 100644 index 00000000..38a41c9d --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_attrs__appends_partial_attrs.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: pretty_attrs(container.get_partial_attributes()) +--- +# [serde (default , deny_unknown_fields)] +# [derive (Other , Another)] +# [serde (rename_all = "kebab-case")] diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_attrs__inherits_supported_attrs.snap b/crates/core/tests/snapshots/container_attrs_test__partial_attrs__inherits_supported_attrs.snap new file mode 100644 index 00000000..42f21daf --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_attrs__inherits_supported_attrs.snap @@ -0,0 +1,13 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: pretty_attrs(container.get_partial_attributes()) +--- +# [serde (default , rename = "Renamed")] +# [doc = r" Some docs."] +# [doc = r" More docs."] +# [allow (dead_code)] +# [expect (missing_docs)] +# [warn (unused)] +# [deprecated] +# [cfg (feature = "example")] +# [non_exhaustive] diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_attrs__orders_serde_then_inherited_then_partial.snap b/crates/core/tests/snapshots/container_attrs_test__partial_attrs__orders_serde_then_inherited_then_partial.snap new file mode 100644 index 00000000..8e08b597 --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_attrs__orders_serde_then_inherited_then_partial.snap @@ -0,0 +1,6 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: pretty_attrs(container.get_partial_attributes()) +--- +# [doc = r" Docs."] +# [derive (Other)] diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_attrs__renders_serde_only_by_default.snap b/crates/core/tests/snapshots/container_attrs_test__partial_attrs__renders_serde_only_by_default.snap new file mode 100644 index 00000000..a4d2e3b2 --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_attrs__renders_serde_only_by_default.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: pretty_attrs(container.get_partial_attributes()) +--- +# [serde (default , deny_unknown_fields)] diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__allows_unknown_fields.snap b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__allows_unknown_fields.snap new file mode 100644 index 00000000..467430b9 --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__allows_unknown_fields.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: container.get_partial_serde_attribute_args().to_string() +--- +default diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__config_args_take_precedence.snap b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__config_args_take_precedence.snap new file mode 100644 index 00000000..ad0f2f9b --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__config_args_take_precedence.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: container.get_partial_serde_attribute_args().to_string() +--- +default , deny_unknown_fields , rename = "FromConfig" , rename_all = "kebab-case" diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__defaults.snap b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__defaults.snap new file mode 100644 index 00000000..5129e3fd --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__defaults.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: container.get_partial_serde_attribute_args().to_string() +--- +default , deny_unknown_fields diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__inherits_config_args.snap b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__inherits_config_args.snap new file mode 100644 index 00000000..6b611a05 --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__inherits_config_args.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: container.get_partial_serde_attribute_args().to_string() +--- +default , deny_unknown_fields , rename = "Renamed" , rename_all = "kebab-case" diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__inherits_serde_args.snap b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__inherits_serde_args.snap new file mode 100644 index 00000000..2901766c --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__inherits_serde_args.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: container.get_partial_serde_attribute_args().to_string() +--- +default , deny_unknown_fields , expecting = "expected" , rename = "Renamed" , rename_all = "kebab-case" diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__serde_denies_unknown_fields_over_config.snap b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__serde_denies_unknown_fields_over_config.snap new file mode 100644 index 00000000..5129e3fd --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__serde_denies_unknown_fields_over_config.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: container.get_partial_serde_attribute_args().to_string() +--- +default , deny_unknown_fields diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__supports_split_renames.snap b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__supports_split_renames.snap new file mode 100644 index 00000000..dcea7664 --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__named_struct__supports_split_renames.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: container.get_partial_serde_attribute_args().to_string() +--- +default , deny_unknown_fields , rename (deserialize = "De") , rename_all (deserialize = "kebab-case" , serialize = "camelCase") diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unit_enum__defaults.snap b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unit_enum__defaults.snap new file mode 100644 index 00000000..c8745c3b --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unit_enum__defaults.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: container.get_partial_serde_attribute_args().to_string() +--- +untagged , rename_all = "kebab-case" diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unnamed_enum__supports_tagged.snap b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unnamed_enum__supports_tagged.snap new file mode 100644 index 00000000..a13bdab2 --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unnamed_enum__supports_tagged.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: container.get_partial_serde_attribute_args().to_string() +--- +tag = "type" , content = "value" , rename_all_fields = "camelCase" diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unnamed_enum__supports_untagged.snap b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unnamed_enum__supports_untagged.snap new file mode 100644 index 00000000..85c33dc5 --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unnamed_enum__supports_untagged.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: container.get_partial_serde_attribute_args().to_string() +--- +untagged diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unnamed_struct__defaults.snap b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unnamed_struct__defaults.snap new file mode 100644 index 00000000..467430b9 --- /dev/null +++ b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unnamed_struct__defaults.snap @@ -0,0 +1,5 @@ +--- +source: crates/core/tests/container_attrs_test.rs +expression: container.get_partial_serde_attribute_args().to_string() +--- +default From 22058a803fbf6eb2326f30a77215d4d48e09784a Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sat, 15 Aug 2026 15:16:59 -0700 Subject: [PATCH 06/18] internal: Implement core `ToTokens`. (#192) --- crates/core/src/args.rs | 5 +- crates/core/src/container.rs | 205 ++++++++- crates/core/src/field.rs | 182 ++++++-- crates/core/src/field_value.rs | 59 ++- crates/core/src/value.rs | 207 ++++++++- crates/core/src/variant.rs | 224 +++++++++- crates/core/tests/container_env_test.rs | 33 ++ crates/core/tests/container_test.rs | 17 + crates/core/tests/container_type_test.rs | 410 ++++++++++++++++++ crates/core/tests/setting_env_test.rs | 17 + crates/core/tests/setting_merge_test.rs | 51 +++ crates/core/tests/setting_validate_test.rs | 40 ++ ...rtial_serde_args__unit_enum__defaults.snap | 2 +- ...v_test__container_env__can_set_prefix.snap | 4 +- ...__skips_derived_keys_without_a_prefix.snap | 14 + ...nv__skips_unnamed_settings_for_prefix.snap | 13 + ...v__skips_unsupported_types_for_prefix.snap | 4 +- ...ed_struct__only_includes_explicit_env.snap | 15 + ...pe__named_struct__inherits_visibility.snap | 14 + ...pe__named_struct__supports_attributes.snap | 27 ++ ...l_type__named_struct__supports_nested.snap | 24 + ...type__named_struct__supports_standard.snap | 26 ++ ...al_type__unit_enum__supports_standard.snap | 13 + ...type__unnamed_enum__supports_standard.snap | 20 + ...l_type__unnamed_enum__supports_tagged.snap | 10 + ...type__unnamed_enum__supports_untagged.snap | 10 + ...pe__unnamed_struct__supports_standard.snap | 20 + ..._default__falls_back_to_first_variant.snap | 10 + ..._type_default__supports_unit_variants.snap | 10 + ...ial_type_default__uses_marked_variant.snap | 10 + ..._deserialize__supports_untagged_enums.snap | 68 +++ ...est__schematic__implements_both_types.snap | 8 + ...er_type_test__to_tokens__named_struct.snap | 185 ++++++++ ...ainer_type_test__to_tokens__unit_enum.snap | 65 +++ ...er_type_test__to_tokens__unnamed_enum.snap | 199 +++++++++ ..._type_test__to_tokens__unnamed_struct.snap | 113 +++++ ...ports_unparsable_types_with_parse_env.snap | 15 + ..._merge__named_struct__supports_nested.snap | 11 +- ...amed_struct__supports_nested_wrappers.snap | 53 +++ ..._merge__unnamed_enum__supports_nested.snap | 5 +- ...nnamed_enum__supports_nested_wrappers.snap | 55 +++ ...erge__unnamed_struct__supports_nested.snap | 11 +- ...idate__named_struct__accepts_func_ref.snap | 2 +- ...lidate__named_struct__supports_nested.snap | 10 +- ...d_struct__supports_nested_collections.snap | 6 +- ...amed_struct__supports_nested_wrappers.snap | 29 ++ ...date__named_struct__supports_standard.snap | 10 +- ...idate__unnamed_enum__accepts_func_ref.snap | 2 +- ...e__unnamed_enum__supports_many_values.snap | 10 +- ...lidate__unnamed_enum__supports_nested.snap | 14 +- ...med_enum__supports_nested_collections.snap | 14 +- ...nnamed_enum__supports_nested_wrappers.snap | 39 ++ ...date__unnamed_enum__supports_standard.snap | 10 +- ...ate__unnamed_struct__accepts_func_ref.snap | 2 +- ...date__unnamed_struct__supports_nested.snap | 10 +- ...d_struct__supports_nested_collections.snap | 6 +- ...te__unnamed_struct__supports_standard.snap | 10 +- crates/schematic/src/internal/env.rs | 36 +- 58 files changed, 2530 insertions(+), 164 deletions(-) create mode 100644 crates/core/tests/container_type_test.rs create mode 100644 crates/core/tests/snapshots/container_env_test__container_env__skips_derived_keys_without_a_prefix.snap create mode 100644 crates/core/tests/snapshots/container_env_test__container_env__skips_unnamed_settings_for_prefix.snap create mode 100644 crates/core/tests/snapshots/container_test__settings__named_struct__only_includes_explicit_env.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type__named_struct__inherits_visibility.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_attributes.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_standard.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type__unit_enum__supports_standard.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__supports_standard.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__supports_tagged.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__supports_untagged.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type__unnamed_struct__supports_standard.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type_default__falls_back_to_first_variant.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type_default__supports_unit_variants.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type_default__uses_marked_variant.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type_deserialize__supports_untagged_enums.snap create mode 100644 crates/core/tests/snapshots/container_type_test__schematic__implements_both_types.snap create mode 100644 crates/core/tests/snapshots/container_type_test__to_tokens__named_struct.snap create mode 100644 crates/core/tests/snapshots/container_type_test__to_tokens__unit_enum.snap create mode 100644 crates/core/tests/snapshots/container_type_test__to_tokens__unnamed_enum.snap create mode 100644 crates/core/tests/snapshots/container_type_test__to_tokens__unnamed_struct.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_unparsable_types_with_parse_env.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_wrappers.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested_wrappers.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_wrappers.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_wrappers.snap diff --git a/crates/core/src/args.rs b/crates/core/src/args.rs index 6a7a4be5..c2c42a94 100644 --- a/crates/core/src/args.rs +++ b/crates/core/src/args.rs @@ -140,10 +140,7 @@ impl PartialArg { // Each meta item is emitted as its own attribute, // for example `#[derive(Other)] #[serde(another)]` pub fn get_attributes(&self) -> Vec { - self.meta - .iter() - .map(|meta| quote! { #[#meta] }) - .collect() + self.meta.iter().map(|meta| quote! { #[#meta] }).collect() } } diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 11bd0155..080b7c8b 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -1,5 +1,5 @@ use crate::args::{PartialArg, SerdeContainerArgs, SerdeRenameArg}; -use crate::field::Field; +use crate::field::{EnvKey, Field}; use crate::utils::{ImplResult, is_inheritable_attribute, to_type_string}; use crate::variant::Variant; use darling::FromDeriveInput; @@ -103,6 +103,19 @@ impl Container { fn validate_args(&self) {} + pub fn get_partial_ident(&self) -> Ident { + format_ident!("Partial{}", self.ident) + } + + /// Whether the partial enum is untagged, and requires + /// each variant to be attempted in order when deserializing. + pub fn is_untagged(&self) -> bool { + matches!( + self.inner, + ContainerInner::UnnamedEnum { .. } | ContainerInner::UnitEnum { .. } + ) && self.serde_args.untagged + } + pub fn get_partial_attributes(&self) -> Vec { let mut attrs = vec![]; @@ -144,7 +157,9 @@ impl Container { ContainerInner::UnnamedStruct { .. } => { meta.push(quote! { default }); } - ContainerInner::UnnamedEnum { .. } => { + // Unit enums must remain externally tagged, otherwise + // variants can only be deserialized from `null` + ContainerInner::UnnamedEnum { .. } | ContainerInner::UnitEnum { .. } => { if let Some(tag) = &self.serde_args.tag { meta.push(quote! { tag = #tag }); } @@ -157,9 +172,6 @@ impl Container { meta.push(quote! { untagged }); } } - ContainerInner::UnitEnum { .. } => { - meta.push(quote! { untagged }); - } }; if let Some(expecting) = &self.serde_args.expecting { @@ -169,7 +181,11 @@ impl Container { // Config attributes take precedence over serde attributes let renames = [ ("rename", &self.args.rename, &self.serde_args.rename), - ("rename_all", &self.args.rename_all, &self.serde_args.rename_all), + ( + "rename_all", + &self.args.rename_all, + &self.serde_args.rename_all, + ), ( "rename_all_fields", &self.args.rename_all_fields, @@ -194,7 +210,7 @@ impl Container { pub fn impl_full(&self) -> TokenStream { let base_name = &self.ident; - let partial_name = format_ident!("Partial{base_name}"); + let partial_name = self.get_partial_ident(); let from_partial_method = self.impl_full_from_partial(); let settings_method = self.impl_full_settings(); @@ -253,7 +269,7 @@ impl Container { } } ContainerInner::UnnamedEnum { variants } | ContainerInner::UnitEnum { variants } => { - let partial_name = format_ident!("Partial{}", self.ident); + let partial_name = self.get_partial_ident(); let mut arms = vec![]; for variant in variants { @@ -286,10 +302,11 @@ impl Container { } else { field.index.to_string() }; - let env_key = if let Some(value) = field.get_env_var() { - quote! { .env(#value) } - } else { - quote! {} + // Only explicit keys are known statically, as derived + // keys depend on the prefix in effect at runtime + let env_key = match field.get_env_var() { + Some(EnvKey::Explicit(value)) => quote! { .env(#value) }, + _ => quote! {}, }; let nested = if field.is_nested() { let value = field.value.get_inner_type(); @@ -347,9 +364,147 @@ impl Container { } } - pub fn impl_partial(&self) -> TokenStream { + /// Generate the partial type declaration, in which every field + /// is optional and nested configurations are replaced by their partials. + pub fn impl_partial_type(&self) -> TokenStream { + let partial_name = self.get_partial_ident(); + let attrs = self.get_partial_attributes(); + let vis = &self.vis; + + match &self.inner { + ContainerInner::NamedStruct { fields } => quote! { + #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] + #(#attrs)* + #vis struct #partial_name { + #(#fields)* + } + }, + ContainerInner::UnnamedStruct { fields } => quote! { + #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] + #(#attrs)* + #vis struct #partial_name( + #(#fields)* + ); + }, + ContainerInner::UnnamedEnum { variants } | ContainerInner::UnitEnum { variants } => { + // Untagged enums implement `Deserialize` manually, + // and all enums implement `Default` manually + let derives = if self.is_untagged() { + quote! { Clone, Debug, PartialEq, serde::Serialize } + } else { + quote! { Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize } + }; + + quote! { + #[derive(#derives)] + #(#attrs)* + #vis enum #partial_name { + #(#variants)* + } + } + } + } + } + + /// Generate a `Default` implementation for partial enums, as the + /// derive is unable to determine the default variant. Partial structs + /// derive `Default` instead. + pub fn impl_partial_type_default(&self) -> TokenStream { + let variants = match &self.inner { + ContainerInner::UnnamedEnum { variants } | ContainerInner::UnitEnum { variants } => { + variants + } + _ => return quote! {}, + }; + + // Prefer the marked variant, otherwise fallback to the first + let default_variant = variants + .iter() + .find(|variant| variant.is_default()) + .or_else(|| variants.first()) + .unwrap_or_else(|| panic!("Enums must have at least 1 variant.")); + + let partial_name = self.get_partial_ident(); + let value = default_variant.impl_partial_default_value().value; + + quote! { + #[automatically_derived] + impl Default for #partial_name { + fn default() -> Self { + Self::#value + } + } + } + } + + /// Generate a `Deserialize` implementation for untagged partial enums, + /// that attempts each variant in order, and reports all errors on failure, + /// as the derived implementation loses this information. + pub fn impl_partial_type_deserialize(&self) -> TokenStream { + if !self.is_untagged() { + return quote! {}; + } + + let partial_name = self.get_partial_ident(); + let mut attempts = vec![]; + + for variant in self.inner.get_variants() { + let res = variant.impl_partial_type_deserialize(&partial_name); + + if !res.no_value { + attempts.push(res.value); + } + } + + quote! { + #[automatically_derived] + impl<'de> serde::Deserialize<'de> for #partial_name { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error as _; + + // Buffer the content so that we can attempt to deserialize it multiple times + let content = deserializer.deserialize_any(schematic::serde_content::ValueVisitor)?; + let mut errors: Vec<(&str, String)> = Vec::new(); + + #(#attempts)* + + // All variants failed, so combine the errors into a single message + let mut message = format!( + "failed to parse as any variant of {}:", + stringify!(#partial_name) + ); + + for (variant, error) in &errors { + message.push_str(&format!("\n- {variant}: {error}")); + } + + Err(D::Error::custom(message)) + } + } + } + } + + /// Generate `Schematic` implementations for both types, which are + /// required by the `Config` and `PartialConfig` traits. + pub fn impl_schematic(&self) -> TokenStream { let base_name = &self.ident; - let partial_name = format_ident!("Partial{base_name}"); + let partial_name = self.get_partial_ident(); + + // TODO: Generate schemas when the `schema` feature is enabled + quote! { + #[automatically_derived] + impl schematic::Schematic for #base_name {} + + #[automatically_derived] + impl schematic::Schematic for #partial_name {} + } + } + + pub fn impl_partial(&self) -> TokenStream { + let partial_name = self.get_partial_ident(); let context = match self.args.context.as_ref() { Some(ctx) => quote! { #ctx }, None => quote! { () }, @@ -721,6 +876,7 @@ impl Container { } ContainerInner::UnnamedEnum { variants } | ContainerInner::UnitEnum { variants } => { let mut statements = vec![]; + let mut requires_internal = false; for variant in variants { let res = variant.impl_partial_merge(); @@ -728,8 +884,13 @@ impl Container { if !res.no_value { statements.push(res.value); } + + if res.requires_internal { + requires_internal = true; + } } + let internal = ImplResult::impl_use_internal(requires_internal); let inner = if statements.is_empty() { quote! { *self = next; @@ -751,6 +912,7 @@ impl Container { context: &Self::Context, mut next: Self, ) -> std::result::Result<(), schematic::ConfigError> { + #internal #inner Ok(()) } @@ -837,9 +999,20 @@ impl Container { } } +// #[derive(Config)] impl ToTokens for Container { - fn to_tokens(&self, _tokens: &mut TokenStream) { - // TODO + fn to_tokens(&self, tokens: &mut TokenStream) { + // Partial type + tokens.extend(self.impl_partial_type()); + tokens.extend(self.impl_partial_type_default()); + tokens.extend(self.impl_partial_type_deserialize()); + tokens.extend(self.impl_partial()); + + // Full type + tokens.extend(self.impl_full()); + + // Both types + tokens.extend(self.impl_schematic()); } } diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index e18cf54e..f12cbda7 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -3,7 +3,7 @@ use crate::args::{ }; use crate::container::ContainerArgs; use crate::field_value::FieldValue; -use crate::utils::{ImplResult, preserve_str_literal}; +use crate::utils::{ImplResult, is_inheritable_attribute, preserve_str_literal}; use darling::FromAttributes; use proc_macro2::{Literal, TokenStream}; use quote::{ToTokens, TokenStreamExt, format_ident, quote}; @@ -46,6 +46,25 @@ pub struct FieldArgs { pub skip_serializing_if: Option, } +/// The environment variable key for a setting. +#[derive(Debug, PartialEq)] +pub enum EnvKey { + /// An explicit key from `#[setting(env)]`, that is used as-is + /// and takes precedence over any prefix. + Explicit(String), + /// A key derived from the setting name when using `env_prefix`, + /// that is prefixed at runtime. + Derived(String), +} + +impl EnvKey { + pub fn as_str(&self) -> &str { + match self { + Self::Explicit(key) | Self::Derived(key) => key, + } + } +} + #[derive(Debug)] pub struct Field { pub value: FieldValue, @@ -110,12 +129,12 @@ impl Field { } #[cfg(not(feature = "env"))] - pub fn get_env_var(&self) -> Option { + pub fn get_env_var(&self) -> Option { None } #[cfg(feature = "env")] - pub fn get_env_var(&self) -> Option { + 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."); } @@ -129,16 +148,17 @@ impl Field { panic!("Cannot use `env` with `nested`, use `env_prefix` instead?"); } - return Some(env_key.to_owned()); + return Some(EnvKey::Explicit(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`."); + // Otherwise derive a key from the setting name, but only when this + // container declares a prefix, as that's how a setting opts into + // being sourced from the environment. + // + // Unnamed settings have no name to derive from, so they may only + // be sourced with an explicit `env`. + if self.container_args.env_prefix.is_some() && self.ident.is_some() { + return Some(EnvKey::Derived(self.get_name().to_uppercase())); } None @@ -216,28 +236,125 @@ impl Field { } } -// impl ToTokens for Field { -// fn to_tokens(&self, tokens: &mut TokenStream) { -// let mut value = self.value.ty_string.clone(); +impl Field { + pub fn get_partial_attributes(&self) -> Vec { + let mut attrs = vec![]; + + // Serde attributes come first, so that they take precedence + // over any provided by the user via `partial(serde(...))` + let serde_args = self.get_partial_serde_attribute_args(); + + if !serde_args.is_empty() { + attrs.push(quote! { #[serde(#serde_args)] }); + } + + // Inherit non-schematic attributes from the field, + // like `doc`, `allow`, and `deprecated` + for attr in &self.attrs { + if is_inheritable_attribute(attr) { + attrs.push(quote! { #attr }); + } + } -// if let Some(nested_ident) = &self.value.nested_ident { -// let ident = nested_ident.to_string(); + // Then apply any user-provided partial attributes + if let Some(partial) = &self.args.partial { + attrs.extend(partial.get_attributes()); + } -// value = value.replace(&ident, &format!("<{ident} as schematic::Config>::Partial")); -// } + attrs + } -// if !self.value.is_outer_option_wrapped() { -// value = format!("Option<{value}>"); -// } + pub fn get_partial_serde_attribute_args(&self) -> TokenStream { + let mut meta = vec![]; -// let key = self.ident.as_ref().unwrap(); -// let value: TokenStream = parse_str(&value).unwrap(); + // Aliases can be provided by both, so combine them + let mut aliases: Vec<&String> = vec![]; -// tokens.extend(quote! { -// pub #key: #value, -// }); -// } -// } + for alias in self.args.alias.iter().chain(self.serde_args.alias.iter()) { + if !aliases.contains(&alias) { + aliases.push(alias); + } + } + + for alias in aliases { + meta.push(quote! { alias = #alias }); + } + + if self.args.flatten || self.serde_args.flatten { + meta.push(quote! { flatten }); + } + + // Setting attributes take precedence over serde attributes + if let Some(rename) = self + .args + .rename + .as_ref() + .or(self.serde_args.rename.as_ref()) + .filter(|rename| !rename.is_empty()) + { + meta.push(rename.get_meta("rename")); + } + + if self.args.skip || self.serde_args.skip { + meta.push(quote! { skip }); + } else { + if self.args.skip_serializing || self.serde_args.skip_serializing { + meta.push(quote! { skip_serializing }); + } else if let Some(func) = self + .args + .skip_serializing_if + .as_ref() + .or(self.serde_args.skip_serializing_if.as_ref()) + { + meta.push(quote! { skip_serializing_if = #func }); + } else { + // Partial values are always optional, so avoid serializing `None` + meta.push(quote! { skip_serializing_if = "Option::is_none" }); + } + + if self.args.skip_deserializing || self.serde_args.skip_deserializing { + meta.push(quote! { skip_deserializing }); + } + } + + quote! { + #(#meta),* + } + } + + /// Return the type to use within the partial, which is always + /// wrapped in an `Option`, unless the type is already optional. + pub fn get_partial_type(&self) -> TokenStream { + let ty = self.value.get_partial_type(); + + if self.value.is_outer_option_wrapped() { + quote! { #ty } + } else { + quote! { Option<#ty> } + } + } +} + +// Only used for partials! +impl ToTokens for Field { + fn to_tokens(&self, tokens: &mut TokenStream) { + let attrs = self.get_partial_attributes(); + let vis = &self.vis; + let ty = self.get_partial_type(); + + if let Some(name) = &self.ident { + tokens.extend(quote! { + #(#attrs)* + #vis #name: #ty, + }); + } else { + tokens.extend(quote! { + #(#attrs)* + #vis #ty, + }); + } + } +} impl Field { pub fn impl_full_from_partial(&self) -> ImplResult { @@ -289,11 +406,13 @@ impl Field { pub fn impl_partial_env_value(&self) -> ImplResult { if self.is_nested() { - return self.value.impl_partial_env_value(&self.args, ""); + return self.value.impl_partial_env_value(&self.args, None); } match self.get_env_var() { - Some(env_key) => self.value.impl_partial_env_value(&self.args, &env_key), + Some(env_key) => self + .value + .impl_partial_env_value(&self.args, Some(&env_key)), None => ImplResult::skipped(), } } @@ -351,9 +470,10 @@ impl Field { if self.is_nested() { let setting_var = format_ident!("setting"); + // The `if let` below consumes the partial's `Option` let nested_value = self .value - .impl_partial_validate_nested(&key_string, &setting_var) + .impl_partial_validate_nested(&key_string, &setting_var, true) .value; has_inner = true; diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index bf92b922..29b9b421 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -1,5 +1,5 @@ use crate::args::NestedArg; -use crate::field::FieldArgs; +use crate::field::{EnvKey, FieldArgs}; use crate::utils::ImplResult; use crate::value::{Layer, Value}; use proc_macro2::TokenStream; @@ -145,19 +145,29 @@ impl FieldValue { } #[cfg(not(feature = "env"))] - pub fn impl_partial_env_value(&self, _field_args: &FieldArgs, _env_key: &str) -> ImplResult { + pub fn impl_partial_env_value( + &self, + _field_args: &FieldArgs, + _env_key: Option<&EnvKey>, + ) -> ImplResult { ImplResult::skipped() } #[cfg(feature = "env")] - pub fn impl_partial_env_value(&self, field_args: &FieldArgs, env_key: &str) -> ImplResult { + pub fn impl_partial_env_value( + &self, + field_args: &FieldArgs, + env_key: Option<&EnvKey>, + ) -> ImplResult { let mut res = ImplResult::default(); // Values can only be sourced from the environment when the type // is bare or wrapped in a single `Option`, as other layers and - // collections cannot be represented by a variable - let supported = - self.layers.is_empty() || (self.layers.len() == 1 && self.is_outer_option_wrapped()); + // collections cannot be parsed from a string. Unless a `parse_env` + // function is provided, which handles the conversion itself. + let supported = field_args.parse_env.is_some() + || self.layers.is_empty() + || (self.layers.len() == 1 && self.is_outer_option_wrapped()); if let Some(nested_ident) = &self.nested_ident { if !supported { @@ -201,13 +211,28 @@ impl FieldValue { return ImplResult::skipped(); } + let Some(env_key) = env_key else { + return ImplResult::skipped(); + }; + + // Explicit keys are read as-is and take precedence over any prefix, + // while derived keys have the prefix applied at runtime + let (key, get, get_and_parse) = match env_key { + EnvKey::Explicit(key) => (key, quote! { get }, quote! { get_and_parse }), + EnvKey::Derived(key) => ( + key, + quote! { get_prefixed }, + quote! { get_and_parse_prefixed }, + ), + }; + res.value = if let Some(parse_env) = &field_args.parse_env { quote! { - env.get_and_parse(#env_key, #parse_env)? + env.#get_and_parse(#key, #parse_env)? } } else { quote! { - env.get(#env_key)? + env.#get(#key)? } }; @@ -290,12 +315,12 @@ impl FieldValue { panic!("Collections with nested configs must manually define `merge`."); } - quote! { - .nested( - &mut self.#field_name, - next.#field_name, - )? - } + // The partial field is always wrapped in an `Option` + return self.impl_partial_merge_nested( + "e! { &mut self.#field_name }, + "e! { next.#field_name }, + true, + ); } else { quote! { .apply( @@ -333,10 +358,10 @@ impl FieldValue { if let Some(expr) = field_args.validate.as_deref() { let field_name_string = field_name.to_string(); let func = match expr { - // func(arg)() + // func(arg)() - already returns a boxed validator Expr::Call(func) => quote! { #func }, - // func() - Expr::Path(func) => quote! { #func }, + // func() - must be boxed + Expr::Path(func) => quote! { Box::new(#func) }, _ => { panic!("Unsupported `validate` syntax."); } diff --git a/crates/core/src/value.rs b/crates/core/src/value.rs index 2805eeab..cfba1b11 100644 --- a/crates/core/src/value.rs +++ b/crates/core/src/value.rs @@ -1,7 +1,8 @@ use crate::args::NestedArg; use crate::utils::{ImplResult, to_type_string}; +use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote}; -use syn::{GenericArgument, Ident, PathArguments, PathSegment, Type}; +use syn::{GenericArgument, Ident, PathArguments, PathSegment, Type, parse_quote}; #[derive(Debug, PartialEq)] pub enum Layer { @@ -84,6 +85,17 @@ impl Value { self.inner_ty.as_ref().unwrap_or(&self.ty) } + /// Return the type to use within the partial, which replaces the nested + /// configuration type with its partial counterpart, while preserving all + /// wrapping layers. For example, `Vec` becomes + /// `Vec<::Partial>`. + pub fn get_partial_type(&self) -> Type { + match &self.nested_ident { + Some(nested_ident) => replace_nested_type(&self.ty, nested_ident), + None => self.ty.clone(), + } + } + pub fn is_collection(&self) -> bool { self.layers.iter().any(|layer| { matches!( @@ -283,17 +295,120 @@ impl Value { res } + /// Generate a merge for a nested configuration, unwrapping each layer of + /// `prev` and `next` until the inner partials can be merged. + /// + /// When `optional` is true, `prev` and `next` are already wrapped in an + /// `Option` by the partial, so a `MergeManager` call is generated instead + /// of a statement, and the outermost `Option` layer is skipped. + pub fn impl_partial_merge_nested( + &self, + prev: &TokenStream, + next: &TokenStream, + optional: bool, + ) -> ImplResult { + let mut res = ImplResult::default(); + let outer_option = self.is_outer_option_wrapped(); + + // The outermost `Option` is handled by the manager, + // while the remaining layers must be unwrapped manually + let manager = optional || outer_option; + let wrappers = if outer_option { + &self.layers[1..] + } else { + &self.layers[..] + }; + + // Nothing to unwrap, so merge directly + if wrappers.is_empty() { + res.requires_internal = manager; + res.value = if manager { + quote! { .nested(#prev, #next)? } + } else { + quote! { #prev.merge(context, #next)?; } + }; + + return res; + } + + // Then unwrap each layer, from the outermost to the innermost + let mut place = if manager { + quote! { prev } + } else { + quote! { *#prev } + }; + let mut value = if manager { + quote! { next } + } else { + quote! { #next } + }; + + for layer in wrappers { + match layer { + Layer::Box => { + place = quote! { *#place }; + value = quote! { *#value }; + } + Layer::Arc => { + place = quote! { *Arc::make_mut(&mut #place) }; + value = quote! { Arc::unwrap_or_clone(#value) }; + } + Layer::Rc => { + place = quote! { *Rc::make_mut(&mut #place) }; + value = quote! { Rc::unwrap_or_clone(#value) }; + } + Layer::Option => { + panic!( + "Nested configs may only be wrapped in an outermost `Option` when using `merge`." + ); + } + Layer::Map(_) | Layer::Set(_) | Layer::Vec(_) | Layer::Unknown(_) => { + panic!("Collections with nested configs must manually define `merge`."); + } + }; + } + + res.requires_internal = manager; + res.value = if manager { + quote! { + .apply_with(#prev, #next, |mut prev, next, context| { + (#place).merge(context, #value) + .map_err(|error| schematic::MergeError(error.to_string()))?; + + Ok(Some(prev)) + })? + } + } else { + quote! { + (#place).merge(context, #value)?; + } + }; + + res + } + #[cfg(not(feature = "validate"))] pub fn impl_partial_validate_nested( &self, _path_key: &str, _setting_var: &Ident, + _optional: bool, ) -> ImplResult { ImplResult::skipped() } + /// Generate a validation for a nested configuration, unwrapping each + /// layer of the setting until the inner partials can be validated. + /// + /// When `optional` is true, the setting has already been unwrapped + /// from the partial's `Option`, so the outermost `Option` layer is skipped. #[cfg(feature = "validate")] - pub fn impl_partial_validate_nested(&self, path_key: &str, setting_var: &Ident) -> ImplResult { + pub fn impl_partial_validate_nested( + &self, + path_key: &str, + setting_var: &Ident, + optional: bool, + ) -> ImplResult { if self.layers.len() >= 2 && self .layers @@ -303,31 +418,56 @@ impl Value { return ImplResult::skipped(); } - let mut value = quote! { - validate.nested(#path_key, #setting_var); + let outer_option = self.is_outer_option_wrapped(); + let layers = if outer_option { + &self.layers[1..] + } else { + &self.layers[..] }; - for layer in self.layers.iter().rev() { + // Then unwrap each layer, from the outermost to the innermost, + // stopping at the first collection + let mut setting = quote! { #setting_var }; + let mut value = None; + + for layer in layers { match layer { - Layer::Arc | Layer::Box | Layer::Option | Layer::Rc => { - // Nothing? + Layer::Arc | Layer::Box | Layer::Rc => { + setting = quote! { #setting.as_ref() }; } Layer::Map(_) => { - value = quote! { - validate.nested_map(#path_key, #setting_var.iter()); - }; + value = Some(quote! { + validate.nested_map(#path_key, #setting.iter()); + }); + break; } Layer::Set(_) | Layer::Vec(_) => { - value = quote! { - validate.nested_list(#path_key, #setting_var.iter()); - }; + value = Some(quote! { + validate.nested_list(#path_key, #setting.iter()); + }); + break; } - Layer::Unknown(_) => { + Layer::Option | Layer::Unknown(_) => { return ImplResult::skipped(); } }; } + let mut value = value.unwrap_or_else(|| { + quote! { + validate.nested(#path_key, #setting); + } + }); + + // The outermost `Option` is a real value, so unwrap it + if outer_option && !optional { + value = quote! { + if let Some(#setting_var) = #setting_var { + #value + } + }; + } + ImplResult { value, ..Default::default() @@ -335,6 +475,45 @@ impl Value { } } +fn replace_nested_type(ty: &Type, nested_ident: &Ident) -> Type { + // We don't need to traverse other types, just paths + let Type::Path(ty_path) = ty else { + return ty.clone(); + }; + + let last_segment = ty_path.path.segments.last().unwrap(); + + match &last_segment.arguments { + // We've reached the final segment, so replace it if it matches + PathArguments::None => { + if last_segment.ident == *nested_ident { + parse_quote! { <#ty_path as schematic::Config>::Partial } + } else { + ty.clone() + } + } + + // Attempt to drill deeper down + PathArguments::AngleBracketed(_) => { + let mut ty_path = ty_path.clone(); + let last_segment = ty_path.path.segments.last_mut().unwrap(); + + if let PathArguments::AngleBracketed(args) = &mut last_segment.arguments { + for arg in args.args.iter_mut() { + if let GenericArgument::Type(inner_ty) = arg { + *inner_ty = replace_nested_type(inner_ty, nested_ident); + } + } + } + + Type::Path(ty_path) + } + + // What to do here, anything? + PathArguments::Parenthesized(_) => ty.clone(), + } +} + fn extract_type_information( ty: &Type, layers: &mut Vec, diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index afb960d4..fc7cc87f 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -2,13 +2,13 @@ use crate::args::{ NestedArg, PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeIoDirection, SerdeRenameArg, }; use crate::container::ContainerArgs; -use crate::utils::ImplResult; +use crate::utils::{ImplResult, is_inheritable_attribute}; use crate::variant_value::VariantValue; use darling::FromAttributes; use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::{ToTokens, format_ident, quote}; use std::rc::Rc; -use syn::{Attribute, ExprPath, Fields, FieldsUnnamed, Ident, Variant as NativeVariant}; +use syn::{Attribute, ExprPath, Fields, FieldsUnnamed, Ident, Index, Variant as NativeVariant}; // #[setting()], #[schema()] #[derive(Debug, Default, FromAttributes)] @@ -176,6 +176,85 @@ impl Variant { self.values.is_empty() } + pub fn get_partial_attributes(&self) -> Vec { + let mut attrs = vec![]; + + // Serde attributes come first, so that they take precedence + // over any provided by the user via `partial(serde(...))` + let serde_args = self.get_partial_serde_attribute_args(); + + if !serde_args.is_empty() { + attrs.push(quote! { #[serde(#serde_args)] }); + } + + // Inherit non-schematic attributes from the variant, + // like `doc`, `allow`, and `deprecated` + for attr in &self.attrs { + if is_inheritable_attribute(attr) { + attrs.push(quote! { #attr }); + } + } + + // Then apply any user-provided partial attributes + if let Some(partial) = &self.args.partial { + attrs.extend(partial.get_attributes()); + } + + attrs + } + + pub fn get_partial_serde_attribute_args(&self) -> TokenStream { + let mut meta = vec![]; + + // Aliases can be provided by both, so combine them + let mut aliases: Vec<&String> = vec![]; + + for alias in self.args.alias.iter().chain(self.serde_args.alias.iter()) { + if !aliases.contains(&alias) { + aliases.push(alias); + } + } + + for alias in aliases { + meta.push(quote! { alias = #alias }); + } + + // Setting attributes take precedence over serde attributes + if let Some(rename) = self + .args + .rename + .as_ref() + .or(self.serde_args.rename.as_ref()) + .filter(|rename| !rename.is_empty()) + { + meta.push(rename.get_meta("rename")); + } + + if self.args.skip || self.serde_args.skip { + meta.push(quote! { skip }); + } else { + if self.args.skip_serializing || self.serde_args.skip_serializing { + meta.push(quote! { skip_serializing }); + } + + if self.args.skip_deserializing || self.serde_args.skip_deserializing { + meta.push(quote! { skip_deserializing }); + } + } + + if self.args.untagged || self.serde_args.untagged { + meta.push(quote! { untagged }); + } + + if self.serde_args.other { + meta.push(quote! { other }); + } + + quote! { + #(#meta),* + } + } + pub fn impl_full_from_partial(&self, partial_name: &Ident) -> ImplResult { let mut res = ImplResult::default(); let name = &self.ident; @@ -232,6 +311,61 @@ impl Variant { res } + /// Generate a deserialization attempt for this variant, for use within + /// untagged enums, where each variant is tried in order. + pub fn impl_partial_type_deserialize(&self, partial_name: &Ident) -> ImplResult { + let mut res = ImplResult::default(); + let name = &self.ident; + let name_string = self.get_name(); + + let deserializer = quote! { + schematic::serde_content::Deserializer::new(content.clone()) + .coerce_numbers() + .human_readable() + }; + + res.value = match &self.fields { + Fields::Named(_) => panic!("Enums with named fields are not supported!"), + Fields::Unnamed(_) => { + let types = self + .values + .iter() + .map(|value| value.get_partial_type()) + .collect::>(); + + if types.len() == 1 { + let ty = &types[0]; + + quote! { + match <#ty as serde::Deserialize>::deserialize(#deserializer) { + Ok(value) => return Ok(#partial_name::#name(value)), + Err(error) => errors.push((#name_string, error.to_string())), + } + } + } else { + // Deserialize multiple values as a tuple, then destructure + let indexes = (0..types.len()).map(Index::from).collect::>(); + + quote! { + match <(#(#types),*) as serde::Deserialize>::deserialize(#deserializer) { + Ok(value) => return Ok(#partial_name::#name(#(value.#indexes),*)), + Err(error) => errors.push((#name_string, error.to_string())), + } + } + } + } + // Unit variants in untagged enums are represented as null + Fields::Unit => quote! { + match <() as serde::Deserialize>::deserialize(#deserializer) { + Ok(_) => return Ok(#partial_name::#name), + Err(error) => errors.push((#name_string, error.to_string())), + } + }, + }; + + res + } + pub fn impl_partial_finalize(&self) -> ImplResult { let mut res = ImplResult::default(); @@ -339,38 +473,54 @@ impl Variant { } None => { if self.is_nested() { - if self - .values - .first() - .is_some_and(|value| value.is_collection()) - { + // Nested variants only support a single value + let value = &self.values[0]; + + if value.is_collection() { panic!( "Collections with nested configs must manually define `merge`." ); } + let mut requires_internal = false; + 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::>(); + let o = &outer_names[0]; + let i = &inner_names[0]; + + // Variant values are not wrapped by the partial, + // so all layers must be handled + let merge = value.impl_partial_merge_nested( + "e! { #o }, + "e! { #i }, + false, + ); + + // Wrap with the manager when the value is optional + let statement = if merge.requires_internal { + requires_internal = true; + + let inner = merge.value; + + quote! { MergeManager::new(context)#inner; } + } else { + merge.value + }; quote! { if let Self::#name(#(#inner_names),*) = next { - #(#statements)* + #statement } else { *self = next; } } }, ); + + res.requires_internal = requires_internal; } else { res.no_value = true; } @@ -399,10 +549,10 @@ impl Variant { use syn::Expr; let func = match expr { - // func(arg)() + // func(arg)() - already returns a boxed validator Expr::Call(func) => quote! { #func }, - // func() - Expr::Path(func) => quote! { #func }, + // func() - must be boxed + Expr::Path(func) => quote! { Box::new(#func) }, _ => { panic!("Unsupported `validate` syntax."); } @@ -429,8 +579,10 @@ impl Variant { .map(|(index, o)| { let name_index = format!("{name_string}.{index}"); + // Variant values are not wrapped by the partial, + // so all layers must be handled self.values[index] - .impl_partial_validate_nested(&name_index, o) + .impl_partial_validate_nested(&name_index, o, false) .value }) .collect::>(), @@ -456,7 +608,37 @@ impl Variant { self.map_unnamed_match_custom(name, &self_name, fields, factory) } +} + +// Only used for partials! +impl ToTokens for Variant { + fn to_tokens(&self, tokens: &mut TokenStream) { + let attrs = self.get_partial_attributes(); + let name = &self.ident; + tokens.extend(match &self.fields { + Fields::Named(_) => panic!("Enums with named fields are not supported!"), + Fields::Unnamed(_) => { + let types = self + .values + .iter() + .map(|value| value.get_partial_type()) + .collect::>(); + + quote! { + #(#attrs)* + #name(#(#types),*), + } + } + Fields::Unit => quote! { + #(#attrs)* + #name, + }, + }); + } +} + +impl Variant { fn map_unnamed_match_custom( &self, name: &Ident, diff --git a/crates/core/tests/container_env_test.rs b/crates/core/tests/container_env_test.rs index e33cf675..309ba5fa 100644 --- a/crates/core/tests/container_env_test.rs +++ b/crates/core/tests/container_env_test.rs @@ -31,6 +31,39 @@ mod container_env { assert_snapshot!(pretty(container.impl_partial_env_values())); } + #[test] + fn skips_derived_keys_without_a_prefix() { + // Keys are only derived when a prefix is declared, otherwise + // settings must opt in with an explicit `env` + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: String, + #[setting(env = "B_VAR")] + b: usize, + #[setting(nested)] + c: NestedConfig, + } + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + + #[test] + fn skips_unnamed_settings_for_prefix() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(env_prefix = "PREFIX_")] + struct Example( + String, + #[setting(env = "B_VAR")] + usize, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + #[test] fn skips_unsupported_types_for_prefix() { let container = Container::from(parse_quote! { diff --git a/crates/core/tests/container_test.rs b/crates/core/tests/container_test.rs index 8b132af3..90584a7d 100644 --- a/crates/core/tests/container_test.rs +++ b/crates/core/tests/container_test.rs @@ -163,6 +163,23 @@ mod settings { assert_snapshot!(pretty(container.impl_full_settings())); } + + #[test] + fn only_includes_explicit_env() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(env_prefix = "PREFIX_")] + struct Example { + a: String, + #[setting(env = "B_VAR")] + b: i32, + #[setting(nested)] + c: NestedExample, + } + }); + + assert_snapshot!(pretty(container.impl_full_settings())); + } } mod unnamed_struct { diff --git a/crates/core/tests/container_type_test.rs b/crates/core/tests/container_type_test.rs new file mode 100644 index 00000000..7f8c907c --- /dev/null +++ b/crates/core/tests/container_type_test.rs @@ -0,0 +1,410 @@ +mod utils; + +use quote::ToTokens; +use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; +use utils::pretty; + +mod partial_type { + 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, + c: String, + d: Option, + e: Vec, + f: Option>, + g: Box, + h: Arc>, + i: std::collections::BTreeSet, + } + }); + + assert_snapshot!(pretty(container.impl_partial_type())); + } + + #[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, + #[setting(nested)] + e: Vec, + #[setting(nested = CustomConfig)] + f: HashMap, + #[setting(nested)] + g: Option>, + #[setting(nested)] + h: config::NestedConfig, + } + }); + + assert_snapshot!(pretty(container.impl_partial_type())); + } + + #[test] + fn supports_attributes() { + let container = Container::from(parse_quote! { + /// Container docs. + #[derive(Config)] + #[config(allow_unknown_fields, partial(derive(Other)))] + #[serde(rename_all = "kebab-case")] + struct Example { + /// Field docs. + #[setting(rename = "aa", partial(serde(default)))] + a: bool, + #[serde(alias = "bb", flatten)] + b: HashMap, + #[setting(skip)] + c: usize, + #[serde(skip_serializing, skip_deserializing)] + d: usize, + #[setting(skip_serializing_if = "is_zero")] + e: usize, + #[allow(dead_code)] + #[deprecated] + #[cfg(feature = "example")] + #[serde(rename(deserialize = "ff"))] + f: usize, + } + }); + + assert_snapshot!(pretty(container.impl_partial_type())); + } + + #[test] + fn inherits_visibility() { + let container = Container::from(parse_quote! { + #[derive(Config)] + pub(crate) struct Example { + pub a: bool, + pub(super) b: bool, + c: bool, + } + }); + + assert_snapshot!(pretty(container.impl_partial_type())); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + pub struct Example( + pub bool, + Option, + #[setting(nested)] + NestedConfig, + #[setting(nested)] + Option>, + /// Docs. + #[setting(partial(serde(default)))] + Box, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_type())); + } + } + + mod unnamed_enum { + use super::*; + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + pub enum Example { + /// Docs. + #[setting(rename = "aa")] + A(bool), + #[serde(alias = "bb", skip)] + B(usize, String), + #[setting(nested)] + C(NestedConfig), + #[setting(nested)] + D(Option), + #[setting(nested = CustomConfig)] + E(Vec), + #[setting(untagged, partial(serde(default)))] + F(String), + #[serde(other)] + G, + } + }); + + assert_snapshot!(pretty(container.impl_partial_type())); + } + + #[test] + fn supports_tagged() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(tag = "type", content = "value")] + enum Example { + A(bool), + B(usize), + } + }); + + assert_snapshot!(pretty(container.impl_partial_type())); + } + + #[test] + fn supports_untagged() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(untagged)] + enum Example { + A(bool), + B(usize), + } + }); + + assert_snapshot!(pretty(container.impl_partial_type())); + } + } + + mod unit_enum { + use super::*; + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + /// Docs. + A, + #[setting(rename = "bb")] + B, + #[serde(skip_deserializing)] + C, + } + }); + + assert_snapshot!(pretty(container.impl_partial_type())); + } + } +} + +mod partial_type_default { + use super::*; + + #[test] + fn skips_structs() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: bool, + } + }); + + assert!(container.impl_partial_type_default().is_empty()); + } + + #[test] + fn uses_marked_variant() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A(bool), + #[setting(default)] + B(usize, String), + C, + } + }); + + assert_snapshot!(pretty(container.impl_partial_type_default())); + } + + #[test] + fn falls_back_to_first_variant() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A(bool), + B(usize), + } + }); + + assert_snapshot!(pretty(container.impl_partial_type_default())); + } + + #[test] + fn supports_unit_variants() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A, + #[setting(default)] + B, + } + }); + + assert_snapshot!(pretty(container.impl_partial_type_default())); + } + + #[test] + #[should_panic(expected = "Enums must have at least 1 variant.")] + fn errors_for_empty_enum() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example {} + }) + .impl_partial_type_default(); + } +} + +mod partial_type_deserialize { + use super::*; + + #[test] + fn skips_tagged_enums() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A(bool), + } + }); + + assert!(container.impl_partial_type_deserialize().is_empty()); + } + + #[test] + fn supports_untagged_enums() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(untagged)] + enum Example { + A(bool), + #[setting(rename = "bb")] + B(usize, String), + #[setting(nested)] + C(NestedConfig), + #[setting(nested)] + D(Option>), + E, + } + }); + + assert_snapshot!(pretty(container.impl_partial_type_deserialize())); + } +} + +mod schematic { + use super::*; + + #[test] + fn implements_both_types() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: bool, + } + }); + + assert_snapshot!(pretty(container.impl_schematic())); + } +} + +mod to_tokens { + use super::*; + + #[test] + fn named_struct() { + let container = Container::from(parse_quote! { + /// Docs. + #[derive(Config)] + #[config(env_prefix = "EXAMPLE_")] + pub struct Example { + #[setting(default = true, env = "ENABLED")] + pub enabled: bool, + #[setting(default = "abc", validate = validate_name)] + pub name: String, + #[setting(required)] + pub port: Option, + #[setting(nested)] + pub nested: NestedConfig, + #[setting(nested, merge = merge::append_vec)] + pub list: Vec, + #[setting(extend)] + pub extends: Vec, + #[setting(transform = transform_map)] + pub map: HashMap, + } + }); + + assert_snapshot!(pretty(container.to_token_stream())); + } + + #[test] + fn unnamed_struct() { + let container = Container::from(parse_quote! { + #[derive(Config)] + pub struct Example( + #[setting(default = 10)] + pub usize, + #[setting(nested)] + pub Option, + ); + }); + + assert_snapshot!(pretty(container.to_token_stream())); + } + + #[test] + fn unnamed_enum() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(untagged)] + pub enum Example { + #[setting(default)] + Bool(bool), + Number(usize), + #[setting(nested)] + Nested(NestedConfig), + #[setting(nested, merge = merge::append_vec, transform = transform_list)] + List(Vec), + } + }); + + assert_snapshot!(pretty(container.to_token_stream())); + } + + #[test] + fn unit_enum() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(rename_all = "kebab-case")] + pub enum Example { + A, + #[setting(default)] + B, + C, + } + }); + + assert_snapshot!(pretty(container.to_token_stream())); + } +} diff --git a/crates/core/tests/setting_env_test.rs b/crates/core/tests/setting_env_test.rs index 685d4ea7..f0b214d7 100644 --- a/crates/core/tests/setting_env_test.rs +++ b/crates/core/tests/setting_env_test.rs @@ -37,6 +37,23 @@ mod setting_env { .impl_partial_env_values(); } + #[test] + fn supports_unparsable_types_with_parse_env() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env = "LIST", parse_env = schematic::env::split_comma)] + a: Vec, + #[setting(env = "OPT_LIST", parse_env = schematic::env::split_comma)] + b: Option>, + #[setting(env = "BOXED", parse_env = parse_func)] + c: Box, + } + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + #[test] fn accepts_string() { let container = Container::from(parse_quote! { diff --git a/crates/core/tests/setting_merge_test.rs b/crates/core/tests/setting_merge_test.rs index 082a444c..59c62bba 100644 --- a/crates/core/tests/setting_merge_test.rs +++ b/crates/core/tests/setting_merge_test.rs @@ -88,6 +88,25 @@ mod setting_merge { assert_snapshot!(pretty(container.impl_partial_merge())); } + #[test] + fn supports_nested_wrappers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: Box, + #[setting(nested)] + b: Rc, + #[setting(nested)] + c: Option>, + #[setting(nested)] + d: Box>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + #[test] fn supports_nested_collections() { let container = Container::from(parse_quote! { @@ -407,6 +426,38 @@ mod setting_merge { assert_snapshot!(pretty(container.impl_partial_merge())); } + #[test] + fn supports_nested_wrappers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A(Box), + #[setting(nested)] + B(Rc), + #[setting(nested)] + C(Option>), + #[setting(nested)] + D(Box>), + } + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + + #[test] + #[should_panic(expected = "may only be wrapped in an outermost `Option`")] + fn errors_for_inner_option() { + Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A(Box>), + } + }) + .impl_partial_merge(); + } + #[test] fn supports_nested_collections() { let container = Container::from(parse_quote! { diff --git a/crates/core/tests/setting_validate_test.rs b/crates/core/tests/setting_validate_test.rs index 44fdaff0..72bcde22 100644 --- a/crates/core/tests/setting_validate_test.rs +++ b/crates/core/tests/setting_validate_test.rs @@ -97,6 +97,25 @@ mod setting_validate { assert_snapshot!(pretty(container.impl_partial_validate())); } + #[test] + fn supports_nested_wrappers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: Box, + #[setting(nested)] + b: Option>, + #[setting(nested)] + c: Box>, + #[setting(nested)] + d: Box>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } + #[test] fn supports_nested_collections() { let container = Container::from(parse_quote! { @@ -327,6 +346,27 @@ mod setting_validate { assert_snapshot!(pretty(container.impl_partial_validate())); } + #[test] + fn supports_nested_wrappers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A(Box), + #[setting(nested)] + B(Option>), + #[setting(nested)] + C(Box>), + #[setting(nested)] + D(Option>), + #[setting(nested)] + E(Box>), + } + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } + #[test] fn supports_nested_collections() { let container = Container::from(parse_quote! { diff --git a/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unit_enum__defaults.snap b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unit_enum__defaults.snap index c8745c3b..31c84821 100644 --- a/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unit_enum__defaults.snap +++ b/crates/core/tests/snapshots/container_attrs_test__partial_serde_args__unit_enum__defaults.snap @@ -2,4 +2,4 @@ source: crates/core/tests/container_attrs_test.rs expression: container.get_partial_serde_attribute_args().to_string() --- -untagged , rename_all = "kebab-case" +rename_all = "kebab-case" 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 index f52e1c72..707ab097 100644 --- 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 @@ -9,8 +9,8 @@ fn env_values_with_prefix( let mut env = EnvManager::new(prefix.or(Some("PREFIX_"))); let mut partial = Self::default(); partial.a = env.get("OVERRIDE")?; - partial.b = env.get("B")?; - partial.c = env.get("C")?; + partial.b = env.get_prefixed("B")?; + partial.c = env.get_prefixed("C")?; partial.d = env.nested(PartialNestedConfig::env_values()?)?; partial.e = env.nested(PartialCustomConfig::env_values()?)?; partial.f = env diff --git a/crates/core/tests/snapshots/container_env_test__container_env__skips_derived_keys_without_a_prefix.snap b/crates/core/tests/snapshots/container_env_test__container_env__skips_derived_keys_without_a_prefix.snap new file mode 100644 index 00000000..98a15d37 --- /dev/null +++ b/crates/core/tests/snapshots/container_env_test__container_env__skips_derived_keys_without_a_prefix.snap @@ -0,0 +1,14 @@ +--- +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.b = env.get("B_VAR")?; + partial.c = env.nested(PartialNestedConfig::env_values()?)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/container_env_test__container_env__skips_unnamed_settings_for_prefix.snap b/crates/core/tests/snapshots/container_env_test__container_env__skips_unnamed_settings_for_prefix.snap new file mode 100644 index 00000000..1f19a6bb --- /dev/null +++ b/crates/core/tests/snapshots/container_env_test__container_env__skips_unnamed_settings_for_prefix.snap @@ -0,0 +1,13 @@ +--- +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(Some("PREFIX_"))); + let mut partial = Self::default(); + partial.1 = env.get("B_VAR")?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/container_env_test__container_env__skips_unsupported_types_for_prefix.snap b/crates/core/tests/snapshots/container_env_test__container_env__skips_unsupported_types_for_prefix.snap index 6bb9f6c6..79ae2e29 100644 --- a/crates/core/tests/snapshots/container_env_test__container_env__skips_unsupported_types_for_prefix.snap +++ b/crates/core/tests/snapshots/container_env_test__container_env__skips_unsupported_types_for_prefix.snap @@ -8,7 +8,7 @@ fn env_values_with_prefix( use schematic::internal::*; let mut env = EnvManager::new(prefix.or(Some("PREFIX_"))); let mut partial = Self::default(); - partial.a = env.get("A")?; - partial.d = env.get("D")?; + partial.a = env.get_prefixed("A")?; + partial.d = env.get_prefixed("D")?; Ok(if env.is_empty() { None } else { Some(partial) }) } diff --git a/crates/core/tests/snapshots/container_test__settings__named_struct__only_includes_explicit_env.snap b/crates/core/tests/snapshots/container_test__settings__named_struct__only_includes_explicit_env.snap new file mode 100644 index 00000000..3c62cf7c --- /dev/null +++ b/crates/core/tests/snapshots/container_test__settings__named_struct__only_includes_explicit_env.snap @@ -0,0 +1,15 @@ +--- +source: crates/core/tests/container_test.rs +expression: pretty(container.impl_full_settings()) +--- +fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + std::collections::BTreeMap::from_iter([ + ("a".into(), ConfigSetting::new("String")), + ("b".into(), ConfigSetting::new("i32").env("B_VAR")), + ( + "c".into(), + ConfigSetting::new("NestedExample").nested(NestedExample::settings()), + ), + ]) +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__inherits_visibility.snap b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__inherits_visibility.snap new file mode 100644 index 00000000..135d84a5 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__inherits_visibility.snap @@ -0,0 +1,14 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type()) +--- +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(default, deny_unknown_fields)] +pub(crate) struct PartialExample { + #[serde(skip_serializing_if = "Option::is_none")] + pub a: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(super) b: Option, + #[serde(skip_serializing_if = "Option::is_none")] + c: Option, +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_attributes.snap b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_attributes.snap new file mode 100644 index 00000000..33fea6fa --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_attributes.snap @@ -0,0 +1,27 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type()) +--- +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(default, rename_all = "kebab-case")] +/// Container docs. +#[derive(Other)] +struct PartialExample { + #[serde(rename = "aa", skip_serializing_if = "Option::is_none")] + /// Field docs. + #[serde(default)] + a: Option, + #[serde(alias = "bb", flatten, skip_serializing_if = "Option::is_none")] + b: Option>, + #[serde(skip)] + c: Option, + #[serde(skip_serializing, skip_deserializing)] + d: Option, + #[serde(skip_serializing_if = "is_zero")] + e: Option, + #[serde(rename(deserialize = "ff"), skip_serializing_if = "Option::is_none")] + #[allow(dead_code)] + #[deprecated] + #[cfg(feature = "example")] + f: Option, +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_nested.snap b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_nested.snap new file mode 100644 index 00000000..58cabafc --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_nested.snap @@ -0,0 +1,24 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type()) +--- +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(default, deny_unknown_fields)] +struct PartialExample { + #[serde(skip_serializing_if = "Option::is_none")] + a: Option<::Partial>, + #[serde(skip_serializing_if = "Option::is_none")] + b: Option<::Partial>, + #[serde(skip_serializing_if = "Option::is_none")] + c: Option<::Partial>, + #[serde(skip_serializing_if = "Option::is_none")] + d: Option::Partial>>, + #[serde(skip_serializing_if = "Option::is_none")] + e: Option::Partial>>, + #[serde(skip_serializing_if = "Option::is_none")] + f: Option::Partial>>, + #[serde(skip_serializing_if = "Option::is_none")] + g: Option::Partial>>, + #[serde(skip_serializing_if = "Option::is_none")] + h: Option<::Partial>, +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_standard.snap b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_standard.snap new file mode 100644 index 00000000..7fffd4f6 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_standard.snap @@ -0,0 +1,26 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type()) +--- +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(default, deny_unknown_fields)] +struct PartialExample { + #[serde(skip_serializing_if = "Option::is_none")] + a: Option, + #[serde(skip_serializing_if = "Option::is_none")] + b: Option, + #[serde(skip_serializing_if = "Option::is_none")] + c: Option, + #[serde(skip_serializing_if = "Option::is_none")] + d: Option, + #[serde(skip_serializing_if = "Option::is_none")] + e: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + f: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + g: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + h: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + i: Option>, +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__unit_enum__supports_standard.snap b/crates/core/tests/snapshots/container_type_test__partial_type__unit_enum__supports_standard.snap new file mode 100644 index 00000000..b41be75b --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type__unit_enum__supports_standard.snap @@ -0,0 +1,13 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type()) +--- +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +enum PartialExample { + /// Docs. + A, + #[serde(rename = "bb")] + B, + #[serde(skip_deserializing)] + C, +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__supports_standard.snap b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__supports_standard.snap new file mode 100644 index 00000000..f130e0f9 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__supports_standard.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type()) +--- +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +pub enum PartialExample { + #[serde(rename = "aa")] + /// Docs. + A(bool), + #[serde(alias = "bb", skip)] + B(usize, String), + C(::Partial), + D(Option<::Partial>), + E(Vec<::Partial>), + #[serde(untagged)] + #[serde(default)] + F(String), + #[serde(other)] + G, +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__supports_tagged.snap b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__supports_tagged.snap new file mode 100644 index 00000000..f0b65e61 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__supports_tagged.snap @@ -0,0 +1,10 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type()) +--- +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(tag = "type", content = "value")] +enum PartialExample { + A(bool), + B(usize), +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__supports_untagged.snap b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__supports_untagged.snap new file mode 100644 index 00000000..e2674cee --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__supports_untagged.snap @@ -0,0 +1,10 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type()) +--- +#[derive(Clone, Debug, PartialEq, serde::Serialize)] +#[serde(untagged)] +enum PartialExample { + A(bool), + B(usize), +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_struct__supports_standard.snap b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_struct__supports_standard.snap new file mode 100644 index 00000000..15591a25 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_struct__supports_standard.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type()) +--- +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(default)] +pub struct PartialExample( + #[serde(skip_serializing_if = "Option::is_none")] + pub Option, + #[serde(skip_serializing_if = "Option::is_none")] + Option, + #[serde(skip_serializing_if = "Option::is_none")] + Option<::Partial>, + #[serde(skip_serializing_if = "Option::is_none")] + Option::Partial>>, + #[serde(skip_serializing_if = "Option::is_none")] + /// Docs. + #[serde(default)] + Option>, +); diff --git a/crates/core/tests/snapshots/container_type_test__partial_type_default__falls_back_to_first_variant.snap b/crates/core/tests/snapshots/container_type_test__partial_type_default__falls_back_to_first_variant.snap new file mode 100644 index 00000000..ccd6a66d --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type_default__falls_back_to_first_variant.snap @@ -0,0 +1,10 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type_default()) +--- +#[automatically_derived] +impl Default for PartialExample { + fn default() -> Self { + Self::A(Default::default()) + } +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type_default__supports_unit_variants.snap b/crates/core/tests/snapshots/container_type_test__partial_type_default__supports_unit_variants.snap new file mode 100644 index 00000000..086d1d75 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type_default__supports_unit_variants.snap @@ -0,0 +1,10 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type_default()) +--- +#[automatically_derived] +impl Default for PartialExample { + fn default() -> Self { + Self::B + } +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type_default__uses_marked_variant.snap b/crates/core/tests/snapshots/container_type_test__partial_type_default__uses_marked_variant.snap new file mode 100644 index 00000000..0b7f7ce3 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type_default__uses_marked_variant.snap @@ -0,0 +1,10 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type_default()) +--- +#[automatically_derived] +impl Default for PartialExample { + fn default() -> Self { + Self::B(Default::default(), Default::default()) + } +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type_deserialize__supports_untagged_enums.snap b/crates/core/tests/snapshots/container_type_test__partial_type_deserialize__supports_untagged_enums.snap new file mode 100644 index 00000000..5813b350 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type_deserialize__supports_untagged_enums.snap @@ -0,0 +1,68 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type_deserialize()) +--- +#[automatically_derived] +impl<'de> 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::A(value)), + Err(error) => errors.push(("A", error.to_string())), + } + match <( + usize, + String, + ) as serde::Deserialize>::deserialize( + schematic::serde_content::Deserializer::new(content.clone()) + .coerce_numbers() + .human_readable(), + ) { + Ok(value) => return Ok(PartialExample::B(value.0, value.1)), + Err(error) => errors.push(("bb", error.to_string())), + } + match <::Partial as serde::Deserialize>::deserialize( + schematic::serde_content::Deserializer::new(content.clone()) + .coerce_numbers() + .human_readable(), + ) { + Ok(value) => return Ok(PartialExample::C(value)), + Err(error) => errors.push(("C", error.to_string())), + } + match ::Partial>, + > as serde::Deserialize>::deserialize( + schematic::serde_content::Deserializer::new(content.clone()) + .coerce_numbers() + .human_readable(), + ) { + Ok(value) => return Ok(PartialExample::D(value)), + Err(error) => errors.push(("D", error.to_string())), + } + match <() as serde::Deserialize>::deserialize( + schematic::serde_content::Deserializer::new(content.clone()) + .coerce_numbers() + .human_readable(), + ) { + Ok(_) => return Ok(PartialExample::E), + Err(error) => errors.push(("E", 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 new file mode 100644 index 00000000..924c9922 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__schematic__implements_both_types.snap @@ -0,0 +1,8 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_schematic()) +--- +#[automatically_derived] +impl schematic::Schematic for Example {} +#[automatically_derived] +impl schematic::Schematic for PartialExample {} 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 new file mode 100644 index 00000000..c15c5178 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__to_tokens__named_struct.snap @@ -0,0 +1,185 @@ +--- +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)] +/// Docs. +pub struct PartialExample { + #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub port: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub nested: Option<::Partial>, + #[serde(skip_serializing_if = "Option::is_none")] + pub list: Option::Partial>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub extends: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub map: Option>, +} +#[automatically_derived] +impl schematic::PartialConfig for PartialExample { + type Context = (); + fn default_values( + context: &Self::Context, + ) -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + Ok( + Some(Self { + enabled: Some(true), + name: Some(handle_default_result(String::try_from("abc"))?), + nested: PartialNestedConfig::default_values(context)?, + list: Some(Vec::default()), + extends: Some(Vec::default()), + map: Some(HashMap::default()), + ..Default::default() + }), + ) + } + fn env_values_with_prefix( + prefix: Option<&str>, + ) -> std::result::Result, schematic::ConfigError> { + use schematic::internal::*; + let mut env = EnvManager::new(prefix.or(Some("EXAMPLE_"))); + let mut partial = Self::default(); + partial.enabled = env.get("ENABLED")?; + partial.name = env.get_prefixed("NAME")?; + partial.port = env.get_prefixed("PORT")?; + partial.nested = env.nested(PartialNestedConfig::env_values()?)?; + Ok(if env.is_empty() { None } else { Some(partial) }) + } + fn extends_from(&self) -> Option { + self.extends.as_ref().map(|inner| schematic::ExtendsFrom::List(inner.to_owned())) + } + 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.nested { + partial.nested = Some(layer.finalize(context)?); + } + if let Some(layer) = partial.list { + partial.list = Some({ + let mut list = Vec::default(); + for layer in layer { + list.push(layer.finalize(context)?); + } + list + }); + } + if let Some(layer) = partial.map { + partial.map = Some(transform_map(layer, context)?); + } + 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.enabled, next.enabled)? + .apply(&mut self.name, next.name)? + .apply(&mut self.port, next.port)? + .nested(&mut self.nested, next.nested)? + .apply_with(&mut self.list, next.list, merge::append_vec)? + .apply(&mut self.extends, next.extends)? + .apply(&mut self.map, next.map)?; + 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); + if let Some(setting) = &self.name { + validate.check("name", setting, self, Box::new(validate_name)); + } + if self.port.is_none() { + validate.required("port"); + } + if let Some(setting) = &self.nested { + validate.nested("nested", setting); + } + if let Some(setting) = &self.list { + validate.nested_list("list", setting.iter()); + } + 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 { + Self { + enabled: partial.enabled.unwrap_or_default(), + name: partial.name.unwrap_or_default(), + port: partial.port, + nested: { + let data = partial.nested.unwrap_or_default(); + NestedConfig::from_partial(data) + }, + list: { + let data = partial.list.unwrap_or_default(); + { + let mut list = Vec::default(); + for data in data { + list.push(NestedConfig::from_partial(data)); + } + list + } + }, + extends: Default::default(), + map: partial.map.unwrap_or_default(), + } + } + fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + std::collections::BTreeMap::from_iter([ + ("enabled".into(), ConfigSetting::new("bool").env("ENABLED")), + ("name".into(), ConfigSetting::new("String")), + ("port".into(), ConfigSetting::new("Option")), + ( + "nested".into(), + ConfigSetting::new("NestedConfig").nested(NestedConfig::settings()), + ), + ( + "list".into(), + ConfigSetting::new("Vec").nested(NestedConfig::settings()), + ), + ("extends".into(), ConfigSetting::new("Vec")), + ("map".into(), ConfigSetting::new("HashMap")), + ]) + } +} +#[automatically_derived] +impl Default for Example { + fn default() -> Self { + ::from_partial( + ::default_partial(), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for Example {} +#[automatically_derived] +impl schematic::Schematic for PartialExample {} 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 new file mode 100644 index 00000000..b505bdcc --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__to_tokens__unit_enum.snap @@ -0,0 +1,65 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.to_token_stream()) +--- +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum PartialExample { + A, + B, + C, +} +#[automatically_derived] +impl Default for PartialExample { + fn default() -> Self { + Self::B + } +} +#[automatically_derived] +impl schematic::PartialConfig for PartialExample { + type Context = (); + fn default_values( + context: &Self::Context, + ) -> std::result::Result, schematic::ConfigError> { + Ok(Some(Self::B)) + } + fn merge( + &mut self, + context: &Self::Context, + mut next: Self, + ) -> std::result::Result<(), schematic::ConfigError> { + *self = next; + Ok(()) + } +} +#[automatically_derived] +impl schematic::Config for Example { + type Partial = PartialExample; + fn from_partial(partial: Self::Partial) -> Self { + match partial { + PartialExample::A => Self::A, + PartialExample::B => Self::B, + PartialExample::C => Self::C, + } + } + fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + std::collections::BTreeMap::from_iter([ + ("A".into(), ConfigSetting::new("A")), + ("B".into(), ConfigSetting::new("B")), + ("C".into(), ConfigSetting::new("C")), + ]) + } +} +#[automatically_derived] +impl Default for Example { + fn default() -> Self { + ::from_partial( + ::default_partial(), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for Example {} +#[automatically_derived] +impl schematic::Schematic for PartialExample {} 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 new file mode 100644 index 00000000..dea9137f --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__to_tokens__unnamed_enum.snap @@ -0,0 +1,199 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.to_token_stream()) +--- +#[derive(Clone, Debug, PartialEq, serde::Serialize)] +#[serde(untagged)] +pub enum PartialExample { + Bool(bool), + Number(usize), + Nested(::Partial), + List(Vec<::Partial>), +} +#[automatically_derived] +impl Default for PartialExample { + fn default() -> Self { + Self::Bool(Default::default()) + } +} +#[automatically_derived] +impl<'de> 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::Bool(value)), + Err(error) => errors.push(("Bool", error.to_string())), + } + match ::deserialize( + schematic::serde_content::Deserializer::new(content.clone()) + .coerce_numbers() + .human_readable(), + ) { + Ok(value) => return Ok(PartialExample::Number(value)), + Err(error) => errors.push(("Number", error.to_string())), + } + match <::Partial as serde::Deserialize>::deserialize( + schematic::serde_content::Deserializer::new(content.clone()) + .coerce_numbers() + .human_readable(), + ) { + Ok(value) => return Ok(PartialExample::Nested(value)), + Err(error) => errors.push(("Nested", error.to_string())), + } + match ::Partial, + > as serde::Deserialize>::deserialize( + schematic::serde_content::Deserializer::new(content.clone()) + .coerce_numbers() + .human_readable(), + ) { + Ok(value) => return Ok(PartialExample::List(value)), + Err(error) => errors.push(("List", 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)) + } +} +#[automatically_derived] +impl schematic::PartialConfig for PartialExample { + type Context = (); + fn default_values( + context: &Self::Context, + ) -> std::result::Result, schematic::ConfigError> { + Ok(Some(Self::Bool(Default::default()))) + } + fn finalize( + self, + context: &Self::Context, + ) -> std::result::Result { + Ok( + match self { + Self::Nested(pa) => Self::Nested(pa.finalize(context)?), + Self::List(pa) => { + Self::List( + transform_list( + { + let mut list = Vec::default(); + for pa in pa { + list.push(pa.finalize(context)?); + } + list + }, + context, + )?, + ) + } + _ => self, + }, + ) + } + fn merge( + &mut self, + context: &Self::Context, + mut next: Self, + ) -> std::result::Result<(), schematic::ConfigError> { + match self { + Self::Nested(pa) => { + if let Self::Nested(na) = next { + pa.merge(context, na)?; + } else { + *self = next; + } + } + Self::List(pa) => { + if let Self::List(na) = next { + *self = Self::List( + merge::append_vec(pa.to_owned(), na, context)? + .unwrap_or_default(), + ); + } else { + *self = next; + } + } + _ => { + *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::Bool(pa) => {} + Self::Number(pa) => {} + Self::Nested(pa) => { + validate.nested("Nested.0", pa); + } + Self::List(pa) => { + validate.nested_list("List.0", pa.iter()); + } + _ => {} + }; + 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::Bool(pa) => Self::Bool(pa), + PartialExample::Number(pa) => Self::Number(pa), + PartialExample::Nested(pa) => Self::Nested(NestedConfig::from_partial(pa)), + PartialExample::List(pa) => { + Self::List({ + let mut list = Vec::default(); + for pa in pa { + list.push(NestedConfig::from_partial(pa)); + } + list + }) + } + } + } + fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + std::collections::BTreeMap::from_iter([ + ("Bool".into(), ConfigSetting::new("bool")), + ("Number".into(), ConfigSetting::new("usize")), + ("Nested".into(), ConfigSetting::new("NestedConfig")), + ("List".into(), ConfigSetting::new("Vec")), + ]) + } +} +#[automatically_derived] +impl Default for Example { + fn default() -> Self { + ::from_partial( + ::default_partial(), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for Example {} +#[automatically_derived] +impl schematic::Schematic for PartialExample {} 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 new file mode 100644 index 00000000..fbeccb55 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__to_tokens__unnamed_struct.snap @@ -0,0 +1,113 @@ +--- +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)] +pub struct PartialExample( + #[serde(skip_serializing_if = "Option::is_none")] + pub Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub Option<::Partial>, +); +#[automatically_derived] +impl schematic::PartialConfig for PartialExample { + type Context = (); + fn default_values( + context: &Self::Context, + ) -> std::result::Result, schematic::ConfigError> { + Ok(Some(Self(Some(10), None))) + } + 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.nested(PartialNestedConfig::env_values()?)?; + Ok(if env.is_empty() { None } else { Some(partial) }) + } + 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.1 { + partial.1 = Some(layer.finalize(context)?); + } + 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)? + .nested(&mut self.1, next.1)?; + 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); + if let Some(setting) = &self.1 { + validate.nested("1", setting); + } + 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 { + Self( + partial.0.unwrap_or_default(), + { + let data = partial.1; + match data { + Some(data) => Some(NestedConfig::from_partial(data)), + None => None, + } + }, + ) + } + fn settings() -> schematic::ConfigSettingMap { + use schematic::ConfigSetting; + std::collections::BTreeMap::from_iter([ + ("0".into(), ConfigSetting::new("usize")), + ( + "1".into(), + ConfigSetting::new("Option") + .nested(NestedConfig::settings()), + ), + ]) + } +} +#[automatically_derived] +impl Default for Example { + fn default() -> Self { + ::from_partial( + ::default_partial(), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for Example {} +#[automatically_derived] +impl schematic::Schematic for PartialExample {} diff --git a/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_unparsable_types_with_parse_env.snap b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_unparsable_types_with_parse_env.snap new file mode 100644 index 00000000..1c90deeb --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_unparsable_types_with_parse_env.snap @@ -0,0 +1,15 @@ +--- +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("LIST", schematic::env::split_comma)?; + partial.b = env.get_and_parse("OPT_LIST", schematic::env::split_comma)?; + partial.c = env.get_and_parse("BOXED", parse_func)?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap index 4bfeb116..0c267399 100644 --- a/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap @@ -12,6 +12,15 @@ fn merge( .nested(&mut self.a, next.a)? .nested(&mut self.b, next.b)? .nested(&mut self.c, next.c)? - .nested(&mut self.d, next.d)?; + .apply_with( + &mut self.d, + next.d, + |mut prev, next, context| { + (*Arc::make_mut(&mut prev)) + .merge(context, Arc::unwrap_or_clone(next)) + .map_err(|error| schematic::MergeError(error.to_string()))?; + Ok(Some(prev)) + }, + )?; Ok(()) } diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_wrappers.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_wrappers.snap new file mode 100644 index 00000000..ca0a0665 --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_wrappers.snap @@ -0,0 +1,53 @@ +--- +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, + |mut prev, next, context| { + (*prev) + .merge(context, *next) + .map_err(|error| schematic::MergeError(error.to_string()))?; + Ok(Some(prev)) + }, + )? + .apply_with( + &mut self.b, + next.b, + |mut prev, next, context| { + (*Rc::make_mut(&mut prev)) + .merge(context, Rc::unwrap_or_clone(next)) + .map_err(|error| schematic::MergeError(error.to_string()))?; + Ok(Some(prev)) + }, + )? + .apply_with( + &mut self.c, + next.c, + |mut prev, next, context| { + (*prev) + .merge(context, *next) + .map_err(|error| schematic::MergeError(error.to_string()))?; + Ok(Some(prev)) + }, + )? + .apply_with( + &mut self.d, + next.d, + |mut prev, next, context| { + (*Arc::make_mut(&mut *prev)) + .merge(context, Arc::unwrap_or_clone(*next)) + .map_err(|error| schematic::MergeError(error.to_string()))?; + Ok(Some(prev)) + }, + )?; + 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 index 6c3ed314..37e3c987 100644 --- 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 @@ -7,6 +7,7 @@ fn merge( context: &Self::Context, mut next: Self, ) -> std::result::Result<(), schematic::ConfigError> { + use schematic::internal::*; match self { Self::A(pa) => { if let Self::A(na) = next { @@ -24,14 +25,14 @@ fn merge( } Self::C(pa) => { if let Self::C(na) = next { - pa.merge(context, na)?; + MergeManager::new(context).nested(pa, na)?; } else { *self = next; } } Self::D(pa) => { if let Self::D(na) = next { - pa.merge(context, na)?; + (*Arc::make_mut(&mut *pa)).merge(context, Arc::unwrap_or_clone(na))?; } else { *self = next; } diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested_wrappers.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested_wrappers.snap new file mode 100644 index 00000000..610ccf14 --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested_wrappers.snap @@ -0,0 +1,55 @@ +--- +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::*; + 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 { + (*Rc::make_mut(&mut *pa)).merge(context, Rc::unwrap_or_clone(na))?; + } else { + *self = next; + } + } + Self::C(pa) => { + if let Self::C(na) = next { + MergeManager::new(context) + .apply_with( + pa, + na, + |mut prev, next, context| { + (*prev) + .merge(context, *next) + .map_err(|error| schematic::MergeError(error.to_string()))?; + Ok(Some(prev)) + }, + )?; + } else { + *self = next; + } + } + Self::D(pa) => { + if let Self::D(na) = next { + (*Arc::make_mut(&mut **pa)).merge(context, Arc::unwrap_or_clone(*na))?; + } else { + *self = next; + } + } + _ => { + *self = next; + } + }; + 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 index c1deb807..568a4a41 100644 --- 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 @@ -12,6 +12,15 @@ fn merge( .nested(&mut self.0, next.0)? .nested(&mut self.1, next.1)? .nested(&mut self.2, next.2)? - .nested(&mut self.3, next.3)?; + .apply_with( + &mut self.3, + next.3, + |mut prev, next, context| { + (*Arc::make_mut(&mut prev)) + .merge(context, Arc::unwrap_or_clone(next)) + .map_err(|error| schematic::MergeError(error.to_string()))?; + Ok(Some(prev)) + }, + )?; 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 index 6f8d0a01..2dc4197e 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap @@ -11,7 +11,7 @@ fn validate_with_path( 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.check("a", setting, self, Box::new(func_ref)); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap index 9e94e211..462f9a1c 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap @@ -11,20 +11,20 @@ fn validate_with_path( 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.check("a", setting, self, Box::new(func_ref)); validate.nested("a", setting); } if let Some(setting) = &self.b { - validate.check("b", setting, self, func_ref); + validate.check("b", setting, self, Box::new(func_ref)); validate.nested("b", setting); } if let Some(setting) = &self.c { - validate.check("c", setting, self, func_ref); + validate.check("c", setting, self, Box::new(func_ref)); validate.nested("c", setting); } if let Some(setting) = &self.d { - validate.check("d", setting, self, func_ref); - validate.nested("d", setting); + validate.check("d", setting, self, Box::new(func_ref)); + validate.nested("d", setting.as_ref()); } if let Some(setting) = &self.e { validate.nested("e", setting); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap index 73fefeb3..d35ed66b 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap @@ -11,15 +11,15 @@ fn validate_with_path( 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.check("a", setting, self, Box::new(func_ref)); validate.nested_list("a", setting.iter()); } if let Some(setting) = &self.b { - validate.check("b", setting, self, func_ref); + validate.check("b", setting, self, Box::new(func_ref)); validate.nested_map("b", setting.iter()); } if let Some(setting) = &self.c { - validate.check("c", setting, self, func_ref); + validate.check("c", setting, self, Box::new(func_ref)); validate.nested_list("c", setting.iter()); } if let Some(setting) = &self.d { diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_wrappers.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_wrappers.snap new file mode 100644 index 00000000..02cf8d62 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_wrappers.snap @@ -0,0 +1,29 @@ +--- +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.nested("a", setting.as_ref()); + } + if let Some(setting) = &self.b { + validate.nested("b", setting.as_ref()); + } + if let Some(setting) = &self.c { + validate.nested("c", setting.as_ref().as_ref()); + } + if let Some(setting) = &self.d { + validate.nested_list("d", setting.as_ref().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 index 66f92821..f66b7037 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap @@ -11,19 +11,19 @@ fn validate_with_path( 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.check("a", setting, self, Box::new(func_ref)); } if let Some(setting) = &self.b { - validate.check("b", setting, self, func_ref); + validate.check("b", setting, self, Box::new(func_ref)); } if let Some(setting) = &self.c { - validate.check("c", setting, self, func_ref); + validate.check("c", setting, self, Box::new(func_ref)); } if let Some(setting) = &self.d { - validate.check("d", setting, self, func_ref); + validate.check("d", setting, self, Box::new(func_ref)); } if let Some(setting) = &self.e { - validate.check("e", setting, self, func_ref); + validate.check("e", setting, self, Box::new(func_ref)); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap index 6cfbce8c..dd16a06f 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap @@ -12,7 +12,7 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check("A", (pa), self, func_ref); + validate.check("A", (pa), self, Box::new(func_ref)); } _ => {} }; diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap index b4465be0..d6668dd7 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap @@ -12,22 +12,22 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check("A", (pa), self, func_ref); + validate.check("A", (pa), self, Box::new(func_ref)); } Self::B(pa, pb) => { - validate.check("B", (pa, pb), self, func_ref); + validate.check("B", (pa, pb), self, Box::new(func_ref)); } Self::C(pa, pb, pc) => { - validate.check("C", (pa, pb, pc), self, func_ref); + validate.check("C", (pa, pb, pc), self, Box::new(func_ref)); } Self::A(pa) => { - validate.check("A", (pa), self, func_ref); + validate.check("A", (pa), self, Box::new(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); + validate.check("B", (pa, pb), self, Box::new(func_ref)); if [pa, pb].iter().any(|v| v.is_none()) { validate.required("B"); } diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap index 720b8137..e40497e6 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap @@ -12,20 +12,22 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check("A", (pa), self, func_ref); + validate.check("A", (pa), self, Box::new(func_ref)); validate.nested("A.0", pa); } Self::B(pa) => { - validate.check("B", (pa), self, func_ref); + validate.check("B", (pa), self, Box::new(func_ref)); validate.nested("B.0", pa); } Self::C(pa) => { - validate.check("C", (pa), self, func_ref); - validate.nested("C.0", pa); + validate.check("C", (pa), self, Box::new(func_ref)); + if let Some(pa) = pa { + validate.nested("C.0", pa); + } } Self::D(pa) => { - validate.check("D", (pa), self, func_ref); - validate.nested("D.0", pa); + validate.check("D", (pa), self, Box::new(func_ref)); + validate.nested("D.0", pa.as_ref()); } Self::E(pa) => { validate.nested("E.0", pa); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap index f1a42013..7fdce321 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap @@ -12,16 +12,18 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check("A", (pa), self, func_ref); + validate.check("A", (pa), self, Box::new(func_ref)); validate.nested_list("A.0", pa.iter()); } Self::B(pa) => { - validate.check("B", (pa), self, func_ref); + validate.check("B", (pa), self, Box::new(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()); + validate.check("C", (pa), self, Box::new(func_ref)); + if let Some(pa) = pa { + validate.nested_list("C.0", pa.iter()); + } } Self::D(pa) => { validate.nested_list("D.0", pa.iter()); @@ -30,7 +32,9 @@ fn validate_with_path( validate.nested_map("E.0", pa.iter()); } Self::F(pa) => { - validate.nested_list("F.0", pa.iter()); + if let Some(pa) = pa { + validate.nested_list("F.0", pa.iter()); + } } _ => {} }; diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_wrappers.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_wrappers.snap new file mode 100644 index 00000000..5f70f216 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_wrappers.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); + match self { + Self::A(pa) => { + validate.nested("A.0", pa.as_ref()); + } + Self::B(pa) => { + if let Some(pa) = pa { + validate.nested("B.0", pa.as_ref()); + } + } + Self::C(pa) => { + validate.nested("C.0", pa.as_ref().as_ref()); + } + Self::D(pa) => { + if let Some(pa) = pa { + validate.nested_list("D.0", pa.iter()); + } + } + Self::E(pa) => { + validate.nested_list("E.0", pa.as_ref().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 index f3d1822b..fbe85971 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap @@ -12,19 +12,19 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check("A", (pa), self, func_ref); + validate.check("A", (pa), self, Box::new(func_ref)); } Self::B(pa) => { - validate.check("B", (pa), self, func_ref); + validate.check("B", (pa), self, Box::new(func_ref)); } Self::C(pa) => { - validate.check("C", (pa), self, func_ref); + validate.check("C", (pa), self, Box::new(func_ref)); } Self::D(pa) => { - validate.check("D", (pa), self, func_ref); + validate.check("D", (pa), self, Box::new(func_ref)); } Self::E(pa) => { - validate.check("E", (pa), self, func_ref); + validate.check("E", (pa), self, Box::new(func_ref)); } _ => {} }; diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap index d19eb7c1..9cccf8c0 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap @@ -11,7 +11,7 @@ fn validate_with_path( 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.check("0", setting, self, Box::new(func_ref)); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap index 600c4718..7dffc691 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap @@ -11,20 +11,20 @@ fn validate_with_path( 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.check("0", setting, self, Box::new(func_ref)); validate.nested("0", setting); } if let Some(setting) = &self.1 { - validate.check("1", setting, self, func_ref); + validate.check("1", setting, self, Box::new(func_ref)); validate.nested("1", setting); } if let Some(setting) = &self.2 { - validate.check("2", setting, self, func_ref); + validate.check("2", setting, self, Box::new(func_ref)); validate.nested("2", setting); } if let Some(setting) = &self.3 { - validate.check("3", setting, self, func_ref); - validate.nested("3", setting); + validate.check("3", setting, self, Box::new(func_ref)); + validate.nested("3", setting.as_ref()); } if let Some(setting) = &self.4 { validate.nested("4", setting); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap index 84243014..ecbae2fa 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap @@ -11,15 +11,15 @@ fn validate_with_path( 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.check("0", setting, self, Box::new(func_ref)); validate.nested_list("0", setting.iter()); } if let Some(setting) = &self.1 { - validate.check("1", setting, self, func_ref); + validate.check("1", setting, self, Box::new(func_ref)); validate.nested_map("1", setting.iter()); } if let Some(setting) = &self.2 { - validate.check("2", setting, self, func_ref); + validate.check("2", setting, self, Box::new(func_ref)); validate.nested_list("2", setting.iter()); } if let Some(setting) = &self.3 { diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap index 409beb19..c762cef7 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap @@ -11,19 +11,19 @@ fn validate_with_path( 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.check("0", setting, self, Box::new(func_ref)); } if let Some(setting) = &self.1 { - validate.check("1", setting, self, func_ref); + validate.check("1", setting, self, Box::new(func_ref)); } if let Some(setting) = &self.2 { - validate.check("2", setting, self, func_ref); + validate.check("2", setting, self, Box::new(func_ref)); } if let Some(setting) = &self.3 { - validate.check("3", setting, self, func_ref); + validate.check("3", setting, self, Box::new(func_ref)); } if let Some(setting) = &self.4 { - validate.check("4", setting, self, func_ref); + validate.check("4", setting, self, Box::new(func_ref)); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/schematic/src/internal/env.rs b/crates/schematic/src/internal/env.rs index 6ab79f6b..db6f85c7 100644 --- a/crates/schematic/src/internal/env.rs +++ b/crates/schematic/src/internal/env.rs @@ -21,18 +21,50 @@ impl EnvManager { self.count == 0 } + /// Get a variable using the exact key, ignoring the prefix. + /// For explicit keys defined with `#[setting(env)]`. pub fn get(&mut self, key: &str) -> ParseEnvResult { self.get_and_parse(key, |value| parse_value(value).map(|v| Some(v))) } + /// Get and parse a variable using the exact key, ignoring the prefix. + /// For explicit keys defined with `#[setting(env)]`. pub fn get_and_parse( &mut self, key: &str, parser: impl Fn(String) -> ParseEnvResult, ) -> ParseEnvResult { - let key = format!("{}{key}", self.prefix); + self.read(key, parser) + } + + /// Get a variable using the key with the prefix applied. + /// For keys derived from setting names when using `env_prefix`. + pub fn get_prefixed(&mut self, key: &str) -> ParseEnvResult { + self.get_and_parse_prefixed(key, |value| parse_value(value).map(|v| Some(v))) + } - if let Ok(value) = std::env::var(&key) { + /// Get and parse a variable using the key with the prefix applied. + /// For keys derived from setting names when using `env_prefix`. + pub fn get_and_parse_prefixed( + &mut self, + key: &str, + parser: impl Fn(String) -> ParseEnvResult, + ) -> ParseEnvResult { + // Derived keys are only applicable when a prefix is in effect, + // otherwise we'd be reading arbitrary variables like `PATH` + if self.prefix.is_empty() { + return Ok(None); + } + + self.read(&format!("{}{key}", self.prefix), parser) + } + + fn read( + &mut self, + key: &str, + parser: impl Fn(String) -> ParseEnvResult, + ) -> ParseEnvResult { + if let Ok(value) = std::env::var(key) { return parser(value) .inspect(|inner| { if inner.is_some() { From d39bd74e749bd1dc41e77a9a1f1205d001642a08 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sat, 15 Aug 2026 22:19:06 -0700 Subject: [PATCH 07/18] internal: Core fixes and polish. (#193) --- crates/core/src/container.rs | 6 +- crates/core/src/field.rs | 21 +- crates/core/src/field_value.rs | 91 ++-- crates/core/src/value.rs | 433 +++++++++++------- crates/core/src/variant.rs | 93 ++-- crates/core/tests/container_type_test.rs | 86 ++++ crates/core/tests/setting_default_test.rs | 41 ++ crates/core/tests/setting_env_test.rs | 47 +- crates/core/tests/setting_merge_test.rs | 58 ++- crates/core/tests/setting_nested_test.rs | 45 +- crates/core/tests/setting_validate_test.rs | 37 ++ ...v__skips_unsupported_types_for_prefix.snap | 1 + ...nalize__named_struct__supports_nested.snap | 10 +- ...nalize__unnamed_enum__supports_nested.snap | 12 +- ...lize__unnamed_struct__supports_nested.snap | 10 +- ...artial__named_struct__supports_nested.snap | 10 +- ...tial__named_struct__supports_standard.snap | 5 +- ...artial__unnamed_enum__supports_nested.snap | 7 +- ...tial__unnamed_struct__supports_nested.snap | 10 +- ...al__unnamed_struct__supports_standard.snap | 5 +- ...l_type__named_struct__strips_wrappers.snap | 32 ++ ...struct__strips_wrappers_around_nested.snap | 20 + ...l_type__named_struct__supports_nested.snap | 2 +- ...type__named_struct__supports_standard.snap | 4 +- ...l_type__unnamed_enum__strips_wrappers.snap | 13 + ...type__unnamed_struct__strips_wrappers.snap | 18 + ...pe__unnamed_struct__supports_standard.snap | 2 +- ...er_type_test__to_tokens__unnamed_enum.snap | 4 +- ...test__setting_default__handles_layers.snap | 7 +- ...default__handles_layers_with_defaults.snap | 8 +- ...etting_default__handles_nested_layers.snap | 6 +- ...fault__handles_optional_with_defaults.snap | 18 + ...fault__unnamed_struct__handles_layers.snap | 21 + ..._env__named_struct__supports_wrappers.snap | 15 + ...nv__unnamed_struct__supports_wrappers.snap | 14 + ...places_collections_without_merge_attr.snap | 16 + ..._merge__named_struct__supports_nested.snap | 11 +- ...amed_struct__supports_nested_wrappers.snap | 44 +- ...places_collections_without_merge_attr.snap | 12 + ...enum__supports_multiple_nested_values.snap | 41 ++ ..._merge__unnamed_enum__supports_nested.snap | 2 +- ...nnamed_enum__supports_nested_wrappers.snap | 18 +- ...places_collections_without_merge_attr.snap | 13 + ...erge__unnamed_struct__supports_nested.snap | 11 +- ...lidate__named_struct__supports_nested.snap | 2 +- ...amed_struct__supports_nested_wrappers.snap | 8 +- ...idate__named_struct__uses_serde_names.snap | 26 ++ ...e__unnamed_enum__accepts_curried_func.snap | 2 +- ...idate__unnamed_enum__accepts_func_ref.snap | 2 +- ...e__unnamed_enum__supports_many_values.snap | 14 +- ...lidate__unnamed_enum__supports_nested.snap | 20 +- ...med_enum__supports_nested_collections.snap | 18 +- ...nnamed_enum__supports_nested_wrappers.snap | 10 +- ...date__unnamed_enum__supports_standard.snap | 10 +- ...idate__unnamed_enum__uses_serde_names.snap | 31 ++ ...date__unnamed_struct__supports_nested.snap | 2 +- crates/schematic/src/config/configs.rs | 7 +- crates/schematic/src/internal/validate.rs | 110 ++++- 58 files changed, 1140 insertions(+), 502 deletions(-) create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type__named_struct__strips_wrappers.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type__named_struct__strips_wrappers_around_nested.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__strips_wrappers.snap create mode 100644 crates/core/tests/snapshots/container_type_test__partial_type__unnamed_struct__strips_wrappers.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__handles_optional_with_defaults.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__handles_layers.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_wrappers.snap create mode 100644 crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_wrappers.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__replaces_collections_without_merge_attr.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__replaces_collections_without_merge_attr.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_multiple_nested_values.snap create mode 100644 crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__replaces_collections_without_merge_attr.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__uses_serde_names.snap create mode 100644 crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__uses_serde_names.snap diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 080b7c8b..6a8efd4a 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -297,11 +297,7 @@ impl Container { match &self.inner { ContainerInner::NamedStruct { fields } | ContainerInner::UnnamedStruct { fields } => { for field in fields { - let name = if field.ident.is_some() { - field.get_name() - } else { - field.index.to_string() - }; + let name = field.get_name_or_index(); // Only explicit keys are known statically, as derived // keys depend on the prefix in effect at runtime let env_key = match field.get_env_var() { diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index f12cbda7..fda541b6 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -200,6 +200,16 @@ impl Field { .expect("Name only usable on named fields!") } + /// Return the name that identifies this setting to users, which is the + /// serde name for named settings, and the position for unnamed ones. + pub fn get_name_or_index(&self) -> String { + if self.ident.is_some() { + self.get_name() + } else { + self.index.to_string() + } + } + pub fn is_excluded(&self) -> bool { #[cfg(feature = "schema")] { @@ -368,9 +378,10 @@ impl Field { }; } - let value = if self.is_nested() { + // Nested partials must be converted, and stripped wrappers reapplied + let value = if self.value.requires_from_partial_mapping() { let data_var = format_ident!("data"); - let inner = self.value.impl_full_from_partial_nested(&data_var).value; + let inner = self.value.impl_full_from_partial_value(&data_var).value; // When option wrapped, the partial's `Option` is the first layer, // so pass the value as-is and let the layer unwrap it @@ -463,8 +474,8 @@ impl Field { pub fn impl_partial_validate(&self) -> ImplResult { let key = self.get_key(); - let key_string = key.to_string(); - let res = self.value.impl_partial_validate(&self.args, &key); + let key_string = self.get_name_or_index(); + let res = self.value.impl_partial_validate(&self.args, &key_string); let mut inner = res.value; let mut has_inner = !res.no_value; @@ -473,7 +484,7 @@ impl Field { // The `if let` below consumes the partial's `Option` let nested_value = self .value - .impl_partial_validate_nested(&key_string, &setting_var, true) + .impl_partial_validate_nested("e! { #key_string }, &setting_var, false, true) .value; has_inner = true; diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index 29b9b421..0c346a30 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -10,13 +10,6 @@ use syn::{Expr, Lit, Type}; #[derive(Debug)] pub struct FieldValue(Value); -fn is_collection_layer(layer: &Layer) -> bool { - matches!( - layer, - Layer::Map(_) | Layer::Set(_) | Layer::Vec(_) | Layer::Unknown(_) - ) -} - fn wrap_layer(layer: &Layer, value: TokenStream) -> TokenStream { match layer { Layer::Arc => quote! { Arc::new(#value) }, @@ -32,8 +25,14 @@ fn wrap_layer(layer: &Layer, value: TokenStream) -> TokenStream { } } -fn wrap_default_layers(layers: &[Layer], mut value: TokenStream) -> TokenStream { +fn wrap_default_layers(layers: &[&Layer], mut value: TokenStream) -> TokenStream { for layer in layers.iter().rev() { + // Wrappers that were not stripped, like `Box`, provide a default + // for the entire value, as the inner value may not have one + if layer.is_wrapper() { + continue; + } + value = wrap_layer(layer, value); } @@ -54,12 +53,24 @@ impl FieldValue { } pub fn impl_partial_default_value(&self, field_args: &FieldArgs) -> ImplResult { - if self.is_outer_option_wrapped() { + let outer_option = self.is_outer_option_wrapped(); + + // Optional settings default to `None`, unless a value is provided + if outer_option && field_args.default.is_none() { return ImplResult::skipped(); }; let mut res = ImplResult::default(); + // Wrappers are stripped from the partial, so only the structural + // layers need to be applied. The outermost `Option` is the partial's + // own, and is applied last. + let mut layers = self.get_partial_layers(); + + if outer_option { + layers.remove(0); + } + // Nested configs source their defaults from the inner partial if let Some(nested_ident) = &self.nested_ident { if field_args.default.is_some() { @@ -70,16 +81,16 @@ impl FieldValue { res.value = if self.is_collection() { // Collections of nested configs start empty - let value = wrap_default_layers(&self.layers, quote! { Default::default() }); + let value = wrap_default_layers(&layers, quote! { Default::default() }); quote! { Some(#value) } - } else if self.layers.is_empty() { + } else if layers.is_empty() { quote! { #ident::default_values(context)? } } else { // Wrap the inner partial with each layer let mut value = quote! { inner }; - for layer in self.layers.iter().rev() { + for layer in layers.iter().rev() { value = wrap_layer(layer, value); } @@ -120,14 +131,13 @@ impl FieldValue { } }; - let wrappers = self - .layers + let outer = layers .iter() - .position(is_collection_layer) - .map(|index| &self.layers[..index]) - .unwrap_or(&self.layers[..]); + .position(|layer| layer.is_collection()) + .map(|index| &layers[..index]) + .unwrap_or(&layers[..]); - for layer in wrappers.iter().rev() { + for layer in outer.iter().rev() { value = wrap_layer(layer, value); } @@ -135,7 +145,7 @@ impl FieldValue { } // Otherwise fallback to the type default None => { - let value = wrap_default_layers(&self.layers, quote! { Default::default() }); + let value = wrap_default_layers(&layers, quote! { Default::default() }); res.value = quote! { Some(#value) }; } @@ -165,9 +175,10 @@ impl FieldValue { // is bare or wrapped in a single `Option`, as other layers and // collections cannot be parsed from a string. Unless a `parse_env` // function is provided, which handles the conversion itself. + let layers = self.get_partial_layers(); let supported = field_args.parse_env.is_some() - || self.layers.is_empty() - || (self.layers.len() == 1 && self.is_outer_option_wrapped()); + || layers.is_empty() + || (layers.len() == 1 && self.is_outer_option_wrapped()); if let Some(nested_ident) = &self.nested_ident { if !supported { @@ -310,24 +321,23 @@ impl FieldValue { } } _ => { - if self.nested { - if self.is_collection() { - panic!("Collections with nested configs must manually define `merge`."); - } - + // Nested configs are merged recursively, but collections of + // them are replaced, as there's no way to know how to pair + // up their items. Define `merge` to customize this. + if self.nested && !self.is_collection() { // The partial field is always wrapped in an `Option` return self.impl_partial_merge_nested( "e! { &mut self.#field_name }, "e! { next.#field_name }, true, ); - } else { - quote! { - .apply( - &mut self.#field_name, - next.#field_name, - )? - } + } + + quote! { + .apply( + &mut self.#field_name, + next.#field_name, + )? } } }; @@ -339,24 +349,15 @@ impl FieldValue { } #[cfg(not(feature = "validate"))] - pub fn impl_partial_validate( - &self, - _field_args: &FieldArgs, - _field_name: &TokenStream, - ) -> ImplResult { + pub fn impl_partial_validate(&self, _field_args: &FieldArgs, _field_name: &str) -> ImplResult { ImplResult::skipped() } #[cfg(feature = "validate")] - pub fn impl_partial_validate( - &self, - field_args: &FieldArgs, - field_name: &TokenStream, - ) -> ImplResult { + pub fn impl_partial_validate(&self, field_args: &FieldArgs, field_name: &str) -> 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)() - already returns a boxed validator Expr::Call(func) => quote! { #func }, @@ -368,7 +369,7 @@ impl FieldValue { }; res.value = quote! { - validate.check(#field_name_string, setting, self, #func); + validate.check(#field_name, setting, self, #func); }; } else { res.no_value = true; diff --git a/crates/core/src/value.rs b/crates/core/src/value.rs index cfba1b11..c1a90ede 100644 --- a/crates/core/src/value.rs +++ b/crates/core/src/value.rs @@ -17,12 +17,31 @@ pub enum Layer { Unknown(String), } +impl Layer { + /// Pointer types that only wrap a value, and are stripped from the + /// partial and re-applied when constructing the final configuration. + pub fn is_wrapper(&self) -> bool { + matches!(self, Self::Arc | Self::Box | Self::Rc) + } + + pub fn is_collection(&self) -> bool { + matches!( + self, + Self::Map(_) | Self::Set(_) | Self::Vec(_) | Self::Unknown(_) + ) + } +} + #[derive(Debug)] pub struct Value { pub inner_ty: Option, pub layers: Vec, pub nested: bool, pub nested_ident: Option, + /// Whether wrapper layers are stripped from the partial. Disabled when a + /// wrapper contains an unsized type, like `Box`, as the inner value + /// cannot exist on its own. + pub strip_wrappers: bool, pub ty: Type, pub ty_string: String, } @@ -57,6 +76,7 @@ impl Value { nested, nested_ident, layers: vec![], + strip_wrappers: !has_unsized_wrapper(&ty), ty_string, ty, }; @@ -73,11 +93,24 @@ impl Value { } }); - if self.nested && self.nested_ident.is_none() { + if !self.nested { + return; + } + + let Some(nested_ident) = &self.nested_ident else { panic!( "Unable to extract the nested configuration identifier from `{}`. Try explicitly passing the identifier with `nested = ConfigName`.", self.ty_string ) + }; + + // Primitives can never implement `Config`, so catch them here instead + // of failing later with an obscure trait error + if is_primitive_ident(nested_ident) { + panic!( + "Nested configurations must be a `Config` type, received `{}`.", + self.ty_string + ) } } @@ -86,113 +119,122 @@ impl Value { } /// Return the type to use within the partial, which replaces the nested - /// configuration type with its partial counterpart, while preserving all - /// wrapping layers. For example, `Vec` becomes + /// configuration type with its partial counterpart, and strips wrapper + /// types. For example, `Arc>` becomes /// `Vec<::Partial>`. pub fn get_partial_type(&self) -> Type { - match &self.nested_ident { - Some(nested_ident) => replace_nested_type(&self.ty, nested_ident), - None => self.ty.clone(), + let mut ty = self.ty.clone(); + + if let Some(nested_ident) = &self.nested_ident { + ty = replace_nested_type(&ty, nested_ident); + } + + if self.strip_wrappers { + ty = strip_wrapper_types(&ty); } + + ty + } + + /// Return the layers that exist within the partial, which excludes + /// wrappers, as those are only applied to the final configuration. + pub fn get_partial_layers(&self) -> Vec<&Layer> { + self.layers + .iter() + .filter(|layer| !self.strip_wrappers || !layer.is_wrapper()) + .collect() } pub fn is_collection(&self) -> bool { - self.layers.iter().any(|layer| { - matches!( - layer, - Layer::Map(_) | Layer::Set(_) | Layer::Vec(_) | Layer::Unknown(_) - ) - }) + self.layers.iter().any(|layer| layer.is_collection()) } + /// Whether the partial's outermost layer is an `Option`, in which case + /// it doubles as the partial's own optionality. pub fn is_outer_option_wrapped(&self) -> bool { - self.layers + self.get_partial_layers() .first() - .is_some_and(|layer| *layer == Layer::Option) + .is_some_and(|layer| **layer == Layer::Option) } - pub fn impl_full_from_partial_nested(&self, data_var: &Ident) -> ImplResult { - let Some(config) = &self.nested_ident else { - return ImplResult::skipped(); - }; + /// Whether converting from a partial requires rebuilding the value, + /// either to convert nested partials or to re-apply stripped wrappers. + pub fn requires_from_partial_mapping(&self) -> bool { + self.nested_ident.is_some() + || (self.strip_wrappers && self.layers.iter().any(|layer| layer.is_wrapper())) + } + /// Generate the value for the final configuration, by converting nested + /// partials and re-applying any wrappers that the partial stripped. + pub fn impl_full_from_partial_value(&self, data_var: &Ident) -> ImplResult { let mut res = ImplResult::default(); - let mut value = quote! { #config::from_partial(#data_var) }; + let mut value = match &self.nested_ident { + Some(config) => quote! { #config::from_partial(#data_var) }, + None => quote! { #data_var }, + }; - // Then wrap with each layer - if !self.layers.is_empty() { - for layer in self.layers.iter().rev() { - value = match layer { - Layer::Arc => quote! { - { - let #data_var = Arc::unwrap_or_clone(#data_var); - Arc::new(#value) - } - }, - Layer::Rc => quote! { - { - let #data_var = Rc::unwrap_or_clone(#data_var); - Rc::new(#value) - } - }, - Layer::Box => quote! { - { - let #data_var = *#data_var; - Box::new(#value) - } - }, - Layer::Option => quote! { - match #data_var { - Some(#data_var) => Some(#value), - None => None - } - }, - Layer::Map(name) => { - let collection = format_ident!("{name}"); + // Then wrap with each layer, from the innermost to the outermost. + // Wrappers are constructed here, as they don't exist in the partial. + for layer in self.layers.iter().rev() { + if layer.is_wrapper() && !self.strip_wrappers { + continue; + } - quote! { - { - let mut map = #collection::default(); - for (key, #data_var) in #data_var { - map.insert(key, #value); - } - map + 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 #data_var { + Some(#data_var) => Some(#value), + None => None + } + }, + Layer::Map(name) => { + let collection = format_ident!("{name}"); + + quote! { + { + let mut map = #collection::default(); + for (key, #data_var) in #data_var { + map.insert(key, #value); } + map } } - Layer::Set(name) => { - let collection = format_ident!("{name}"); + } + Layer::Set(name) => { + let collection = format_ident!("{name}"); - quote! { - { - let mut set = #collection::default(); - for #data_var in #data_var { - set.insert(#value); - } - set + quote! { + { + let mut set = #collection::default(); + for #data_var in #data_var { + set.insert(#value); } + set } } - Layer::Vec(name) => { - let collection = format_ident!("{name}"); + } + Layer::Vec(name) => { + let collection = format_ident!("{name}"); - quote! { - { - let mut list = #collection::default(); - for #data_var in #data_var { - list.push(#value); - } - list + quote! { + { + let mut list = #collection::default(); + for #data_var in #data_var { + list.push(#value); } + list } } - Layer::Unknown(name) => { - let collection = format_ident!("{name}"); + } + Layer::Unknown(name) => { + let collection = format_ident!("{name}"); - quote! { #collection::default() } - } - }; - } + quote! { #collection::default() } + } + }; } res.value = value; @@ -207,13 +249,14 @@ impl Value { let mut res = ImplResult::default(); let mut value = quote! { #layer_var.finalize(context)? }; - // The first `Option` layer is represented by the partial's own - // `Option`, so skip it when the caller has already unwrapped it - let layers = if skip_outer_option && self.is_outer_option_wrapped() { - &self.layers[1..] - } else { - &self.layers[..] - }; + // Wrappers don't exist in the partial, and the first `Option` layer is + // represented by the partial's own `Option`, so skip it when the + // caller has already unwrapped it + let mut layers = self.get_partial_layers(); + + if skip_outer_option && self.is_outer_option_wrapped() { + layers.remove(0); + } // Then wrap with each layer if !layers.is_empty() { @@ -295,12 +338,11 @@ impl Value { res } - /// Generate a merge for a nested configuration, unwrapping each layer of - /// `prev` and `next` until the inner partials can be merged. + /// Generate a merge for a nested configuration. /// /// When `optional` is true, `prev` and `next` are already wrapped in an - /// `Option` by the partial, so a `MergeManager` call is generated instead - /// of a statement, and the outermost `Option` layer is skipped. + /// `Option` by the partial, so a `MergeManager` call is generated + /// instead of a statement. pub fn impl_partial_merge_nested( &self, prev: &TokenStream, @@ -310,78 +352,27 @@ impl Value { let mut res = ImplResult::default(); let outer_option = self.is_outer_option_wrapped(); - // The outermost `Option` is handled by the manager, - // while the remaining layers must be unwrapped manually - let manager = optional || outer_option; - let wrappers = if outer_option { - &self.layers[1..] - } else { - &self.layers[..] - }; - - // Nothing to unwrap, so merge directly - if wrappers.is_empty() { - res.requires_internal = manager; - res.value = if manager { - quote! { .nested(#prev, #next)? } - } else { - quote! { #prev.merge(context, #next)?; } - }; + // The outermost `Option` is handled by the manager. Wrappers don't + // exist in the partial, so anything else is unmergeable. + let mut layers = self.get_partial_layers(); - return res; + if outer_option { + layers.remove(0); } - // Then unwrap each layer, from the outermost to the innermost - let mut place = if manager { - quote! { prev } - } else { - quote! { *#prev } - }; - let mut value = if manager { - quote! { next } - } else { - quote! { #next } - }; - - for layer in wrappers { - match layer { - Layer::Box => { - place = quote! { *#place }; - value = quote! { *#value }; - } - Layer::Arc => { - place = quote! { *Arc::make_mut(&mut #place) }; - value = quote! { Arc::unwrap_or_clone(#value) }; - } - Layer::Rc => { - place = quote! { *Rc::make_mut(&mut #place) }; - value = quote! { Rc::unwrap_or_clone(#value) }; - } - Layer::Option => { - panic!( - "Nested configs may only be wrapped in an outermost `Option` when using `merge`." - ); - } - Layer::Map(_) | Layer::Set(_) | Layer::Vec(_) | Layer::Unknown(_) => { - panic!("Collections with nested configs must manually define `merge`."); - } - }; + // Collections are replaced by the caller, so only `Option`s remain + if !layers.is_empty() { + panic!( + "Nested configs may only be wrapped in an outermost `Option` when using `merge`." + ); } + let manager = optional || outer_option; res.requires_internal = manager; res.value = if manager { - quote! { - .apply_with(#prev, #next, |mut prev, next, context| { - (#place).merge(context, #value) - .map_err(|error| schematic::MergeError(error.to_string()))?; - - Ok(Some(prev)) - })? - } + quote! { .nested(#prev, #next)? } } else { - quote! { - (#place).merge(context, #value)?; - } + quote! { #prev.merge(context, #next)?; } }; res @@ -390,8 +381,9 @@ impl Value { #[cfg(not(feature = "validate"))] pub fn impl_partial_validate_nested( &self, - _path_key: &str, + _target: &TokenStream, _setting_var: &Ident, + _variant: bool, _optional: bool, ) -> ImplResult { ImplResult::skipped() @@ -400,30 +392,37 @@ impl Value { /// Generate a validation for a nested configuration, unwrapping each /// layer of the setting until the inner partials can be validated. /// + /// The `target` identifies the setting within error paths, and is either + /// a key, or a variant name and position when `variant` is true. + /// /// When `optional` is true, the setting has already been unwrapped /// from the partial's `Option`, so the outermost `Option` layer is skipped. #[cfg(feature = "validate")] pub fn impl_partial_validate_nested( &self, - path_key: &str, + target: &TokenStream, setting_var: &Ident, + variant: bool, optional: bool, ) -> ImplResult { - if self.layers.len() >= 2 - && self - .layers - .get(1) - .is_some_and(|layer| matches!(layer, Layer::Option)) - { - return ImplResult::skipped(); + let prefix = if variant { "nested_variant" } else { "nested" }; + let nested = format_ident!("{prefix}"); + let nested_list = format_ident!("{prefix}_list"); + let nested_map = format_ident!("{prefix}_map"); + + // Wrappers don't exist in the partial, so only the structural + // layers need to be traversed + let mut layers = self.get_partial_layers(); + let outer_option = self.is_outer_option_wrapped(); + + if outer_option { + layers.remove(0); } - let outer_option = self.is_outer_option_wrapped(); - let layers = if outer_option { - &self.layers[1..] - } else { - &self.layers[..] - }; + // Nested `Option`s cannot be validated, as the inner value may not exist + if layers.first().is_some_and(|layer| **layer == Layer::Option) { + return ImplResult::skipped(); + } // Then unwrap each layer, from the outermost to the innermost, // stopping at the first collection @@ -437,13 +436,13 @@ impl Value { } Layer::Map(_) => { value = Some(quote! { - validate.nested_map(#path_key, #setting.iter()); + validate.#nested_map(#target, #setting.iter()); }); break; } Layer::Set(_) | Layer::Vec(_) => { value = Some(quote! { - validate.nested_list(#path_key, #setting.iter()); + validate.#nested_list(#target, #setting.iter()); }); break; } @@ -455,7 +454,7 @@ impl Value { let mut value = value.unwrap_or_else(|| { quote! { - validate.nested(#path_key, #setting); + validate.#nested(#target, #setting); } }); @@ -475,6 +474,94 @@ impl Value { } } +fn is_wrapper_ident(ident: &Ident) -> bool { + ident == "Arc" || ident == "Box" || ident == "Rc" +} + +fn is_primitive_ident(ident: &Ident) -> bool { + [ + "String", "bool", "char", "f32", "f64", "i8", "i16", "i32", "i64", "i128", "isize", "str", + "u8", "u16", "u32", "u64", "u128", "usize", + ] + .iter() + .any(|primitive| ident == primitive) +} + +/// Whether a type cannot exist without being wrapped in a pointer, +/// like `str`, `[T]`, and `dyn Trait`. +fn is_unsized_type(ty: &Type) -> bool { + match ty { + Type::Slice(_) | Type::TraitObject(_) => true, + Type::Path(ty_path) => ty_path + .path + .segments + .last() + .is_some_and(|segment| segment.ident == "str"), + _ => false, + } +} + +/// Whether any wrapper within the type contains an unsized value, +/// in which case the wrappers cannot be stripped from the partial. +fn has_unsized_wrapper(ty: &Type) -> bool { + let Type::Path(ty_path) = ty else { + return false; + }; + + let Some(last_segment) = ty_path.path.segments.last() else { + return false; + }; + + let PathArguments::AngleBracketed(args) = &last_segment.arguments else { + return false; + }; + + args.args.iter().any(|arg| { + let GenericArgument::Type(inner_ty) = arg else { + return false; + }; + + (is_wrapper_ident(&last_segment.ident) && is_unsized_type(inner_ty)) + || has_unsized_wrapper(inner_ty) + }) +} + +/// Remove all wrapper types, so that `Arc>>` becomes `Vec`. +fn strip_wrapper_types(ty: &Type) -> Type { + let Type::Path(ty_path) = ty else { + return ty.clone(); + }; + + let Some(last_segment) = ty_path.path.segments.last() else { + return ty.clone(); + }; + + let PathArguments::AngleBracketed(args) = &last_segment.arguments else { + return ty.clone(); + }; + + // Replace the wrapper with the type it wraps + if is_wrapper_ident(&last_segment.ident) + && let Some(GenericArgument::Type(inner_ty)) = args.args.last() + { + return strip_wrapper_types(inner_ty); + } + + // Otherwise drill into each argument, like the value of a map + let mut ty_path = ty_path.clone(); + let last_segment = ty_path.path.segments.last_mut().unwrap(); + + if let PathArguments::AngleBracketed(args) = &mut last_segment.arguments { + for arg in args.args.iter_mut() { + if let GenericArgument::Type(inner_ty) = arg { + *inner_ty = strip_wrapper_types(inner_ty); + } + } + } + + Type::Path(ty_path) +} + fn replace_nested_type(ty: &Type, nested_ident: &Ident) -> Type { // We don't need to traverse other types, just paths let Type::Path(ty_path) = ty else { diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index fc7cc87f..783e7881 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -88,10 +88,6 @@ impl 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 @@ -267,8 +263,8 @@ impl Variant { .iter() .enumerate() .map(|(index, o)| { - if self.is_nested() { - self.values[index].impl_full_from_partial_nested(o).value + if self.values[index].requires_from_partial_mapping() { + self.values[index].impl_full_from_partial_value(o).value } else { quote! { #o } } @@ -472,47 +468,55 @@ impl Variant { }); } None => { - if self.is_nested() { - // Nested variants only support a single value - let value = &self.values[0]; - - if value.is_collection() { - panic!( - "Collections with nested configs must manually define `merge`." - ); - } + // Nested configs are merged recursively, but collections + // of them are replaced, as there's no way to know how to + // pair up their items. Define `merge` to customize this. + let mergeable = self.is_nested() + && self.values.iter().any(|value| !value.is_collection()); + if mergeable { let mut requires_internal = false; res.value = self.map_unnamed_match( &self.ident, fields, |outer_names, inner_names| { - let o = &outer_names[0]; - let i = &inner_names[0]; - - // Variant values are not wrapped by the partial, - // so all layers must be handled - let merge = value.impl_partial_merge_nested( - "e! { #o }, - "e! { #i }, - false, - ); - - // Wrap with the manager when the value is optional - let statement = if merge.requires_internal { - requires_internal = true; - - let inner = merge.value; - - quote! { MergeManager::new(context)#inner; } - } else { - merge.value - }; + let statements = outer_names + .iter() + .enumerate() + .map(|(index, o)| { + let i = &inner_names[index]; + let value = &self.values[index]; + + // Collections are replaced in place + if value.is_collection() { + return quote! { *#o = #i; }; + } + + // Variant values are not wrapped by the + // partial, so all layers must be handled + let merge = value.impl_partial_merge_nested( + "e! { #o }, + "e! { #i }, + false, + ); + + // Wrap with the manager when the value is optional + if merge.requires_internal { + requires_internal = true; + + let inner = merge.value; + + quote! { MergeManager::new(context)#inner; } + } else { + merge.value + } + }) + .collect::>(); quote! { if let Self::#name(#(#inner_names),*) = next { - #statement + #(#statements)* } else { *self = next; } @@ -542,7 +546,7 @@ impl Variant { let value = self.map_unnamed_match(&self.ident, fields, |outer_names, _| { let mut statements = vec![]; - let name_string = self.ident.to_string(); + let name_string = self.get_name(); #[cfg(feature = "validate")] if let Some(expr) = self.args.validate.as_deref() { @@ -559,14 +563,14 @@ impl Variant { }; statements.push(quote! { - validate.check(#name_string, (#(#outer_names),*), self, #func); + validate.check_variant(#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); + validate.required_variant(#name_string); } }); } @@ -577,12 +581,15 @@ impl Variant { .iter() .enumerate() .map(|(index, o)| { - let name_index = format!("{name_string}.{index}"); - // Variant values are not wrapped by the partial, // so all layers must be handled self.values[index] - .impl_partial_validate_nested(&name_index, o, false) + .impl_partial_validate_nested( + "e! { #name_string, #index }, + o, + true, + false, + ) .value }) .collect::>(), diff --git a/crates/core/tests/container_type_test.rs b/crates/core/tests/container_type_test.rs index 7f8c907c..3c33b56c 100644 --- a/crates/core/tests/container_type_test.rs +++ b/crates/core/tests/container_type_test.rs @@ -59,6 +59,54 @@ mod partial_type { assert_snapshot!(pretty(container.impl_partial_type())); } + #[test] + fn strips_wrappers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: Box, + b: Arc, + c: Rc, + d: Option>, + e: Arc>, + f: Vec>, + g: Box>, + h: HashMap>, + // Unsized values keep their wrapper + i: Box, + j: Arc, + k: Box<[u8]>, + // The outer `Option` is found after stripping + l: Box>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_type())); + } + + #[test] + fn strips_wrappers_around_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: Box, + #[setting(nested)] + b: Arc, + #[setting(nested)] + c: Option>, + #[setting(nested)] + d: Arc>, + #[setting(nested)] + e: Vec>, + #[setting(nested = CustomConfig)] + f: HashMap>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_type())); + } + #[test] fn supports_attributes() { let container = Container::from(parse_quote! { @@ -107,6 +155,24 @@ mod partial_type { mod unnamed_struct { use super::*; + #[test] + fn strips_wrappers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + Box, + Option>, + Vec>, + #[setting(nested)] + Arc, + // Unsized values keep their wrapper + Box, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_type())); + } + #[test] fn supports_standard() { let container = Container::from(parse_quote! { @@ -157,6 +223,26 @@ mod partial_type { assert_snapshot!(pretty(container.impl_partial_type())); } + #[test] + fn strips_wrappers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A(Box), + B(Option>), + C(Vec>), + #[setting(nested)] + D(Arc), + #[setting(nested)] + E(Option>), + // Unsized values keep their wrapper + F(Box), + } + }); + + assert_snapshot!(pretty(container.impl_partial_type())); + } + #[test] fn supports_tagged() { let container = Container::from(parse_quote! { diff --git a/crates/core/tests/setting_default_test.rs b/crates/core/tests/setting_default_test.rs index db770766..a53f821d 100644 --- a/crates/core/tests/setting_default_test.rs +++ b/crates/core/tests/setting_default_test.rs @@ -62,6 +62,26 @@ mod setting_default { assert_snapshot!(pretty(container.impl_partial_default_values())); } + #[test] + fn handles_optional_with_defaults() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + no_default: Option, + #[setting(default = 10)] + a: Option, + #[setting(default = 10)] + b: Arc>, + #[setting(default = vec![1, 2])] + c: Option>, + #[setting(default = "abc")] + d: Option, + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + #[test] fn handles_nested_layers() { let container = Container::from(parse_quote! { @@ -166,6 +186,27 @@ mod setting_default { mod unnamed_struct { use super::*; + #[test] + fn handles_layers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + Arc, + Box, + Rc>, + Arc>>, + #[setting(default = 10)] + Box, + #[setting(default = 10)] + Arc>, + // Unsized values keep their wrapper + Box, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + #[test] fn supports_types() { let container = Container::from(parse_quote! { diff --git a/crates/core/tests/setting_env_test.rs b/crates/core/tests/setting_env_test.rs index f0b214d7..84909148 100644 --- a/crates/core/tests/setting_env_test.rs +++ b/crates/core/tests/setting_env_test.rs @@ -11,14 +11,34 @@ mod setting_env { mod named_struct { use super::*; + #[test] + fn supports_wrappers() { + // Wrappers are stripped from the partial, so the + // inner value can be parsed from a variable + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(env = "A")] + a: Arc, + #[setting(env = "B")] + b: Box, + #[setting(env = "C")] + c: Option>, + } + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + #[test] #[should_panic(expected = "Wrapper types cannot be used with `env`.")] - fn errors_if_using_wrappers() { + fn errors_if_using_unsized_wrappers() { + // Unsized values keep their wrapper, so they cannot be parsed Container::from(parse_quote! { #[derive(Config)] struct Example { #[setting(env = "KEY")] - a: Arc, + a: Box, } }) .impl_partial_env_values(); @@ -165,14 +185,29 @@ mod setting_env { mod unnamed_struct { use super::*; + #[test] + fn supports_wrappers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + #[setting(env = "A")] + Arc, + #[setting(env = "B")] + Option>, + ); + }); + + assert_snapshot!(pretty(container.impl_partial_env_values())); + } + #[test] #[should_panic(expected = "Wrapper types cannot be used with `env`.")] - fn errors_if_using_wrappers() { + fn errors_if_using_unsized_wrappers() { Container::from(parse_quote! { #[derive(Config)] struct Example( #[setting(env = "KEY")] - Arc, + Box, ); }) .impl_partial_env_values(); @@ -301,7 +336,7 @@ mod setting_env_prefix { #[derive(Config)] struct Example { #[setting(env_prefix = "", nested)] - a: String, + a: NestedConfig, } }) .impl_partial_env_values(); @@ -371,7 +406,7 @@ mod setting_env_prefix { #[derive(Config)] struct Example( #[setting(env_prefix = "", nested)] - String, + NestedConfig, ); }) .impl_partial_env_values(); diff --git a/crates/core/tests/setting_merge_test.rs b/crates/core/tests/setting_merge_test.rs index 59c62bba..1254d347 100644 --- a/crates/core/tests/setting_merge_test.rs +++ b/crates/core/tests/setting_merge_test.rs @@ -162,16 +162,20 @@ mod setting_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! { + fn replaces_collections_without_merge_attr() { + let container = Container::from(parse_quote! { #[derive(Config)] struct Example { #[setting(nested)] a: Vec, + #[setting(nested)] + b: HashMap, + #[setting(nested)] + c: Option>, } - }) - .impl_partial_merge(); + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); } } @@ -310,16 +314,18 @@ mod setting_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! { + fn replaces_collections_without_merge_attr() { + let container = Container::from(parse_quote! { #[derive(Config)] struct Example( #[setting(nested)] Vec, + #[setting(nested)] + HashMap, ); - }) - .impl_partial_merge(); + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); } } @@ -452,7 +458,7 @@ mod setting_merge { #[derive(Config)] enum Example { #[setting(nested)] - A(Box>), + A(Option>), } }) .impl_partial_merge(); @@ -491,16 +497,36 @@ mod setting_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! { + fn replaces_collections_without_merge_attr() { + let container = Container::from(parse_quote! { #[derive(Config)] enum Example { #[setting(nested)] A(Vec), + #[setting(nested)] + B(HashMap), } - }) - .impl_partial_merge(); + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); + } + + #[test] + fn supports_multiple_nested_values() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A(NestedConfig, CustomConfig), + #[setting(nested)] + B(NestedConfig, Option), + // Collections are replaced in place, alongside a merge + #[setting(nested)] + C(NestedConfig, Vec), + } + }); + + assert_snapshot!(pretty(container.impl_partial_merge())); } } diff --git a/crates/core/tests/setting_nested_test.rs b/crates/core/tests/setting_nested_test.rs index 5b745a62..bd2e1da0 100644 --- a/crates/core/tests/setting_nested_test.rs +++ b/crates/core/tests/setting_nested_test.rs @@ -148,6 +148,34 @@ mod setting_nested { } }); } + + #[test] + #[should_panic( + expected = "Nested configurations must be a `Config` type, received `bool`." + )] + fn panics_for_primitives() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: bool, + } + }); + } + + #[test] + #[should_panic( + expected = "Nested configurations must be a `Config` type, received `Vec`." + )] + fn panics_for_primitives_in_collections() { + Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: Vec, + } + }); + } } mod unnamed_struct { @@ -401,11 +429,24 @@ mod setting_nested { }); } + #[test] + fn supports_multiple_items() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(nested)] + A(NestedConfig, CustomConfig), + } + }); + + assert_eq!(container.inner.get_variants()[0].values.len(), 2); + } + #[test] #[should_panic( - expected = "Only 1 item is supported when using `nested` in a tuple variant." + expected = "Nested configurations must be a `Config` type, received `bool`." )] - fn panics_multiple_items() { + fn panics_if_any_item_is_not_a_config() { Container::from(parse_quote! { #[derive(Config)] enum Example { diff --git a/crates/core/tests/setting_validate_test.rs b/crates/core/tests/setting_validate_test.rs index 72bcde22..e57cbf5d 100644 --- a/crates/core/tests/setting_validate_test.rs +++ b/crates/core/tests/setting_validate_test.rs @@ -74,6 +74,25 @@ mod setting_validate { assert_snapshot!(pretty(container.impl_partial_validate())); } + #[test] + fn uses_serde_names() { + // Errors reference the renamed setting, as that's what users see + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(rename = "aa", validate = func_ref)] + a: String, + #[serde(rename = "bb")] + #[setting(required)] + b: Option, + #[setting(rename = "cc", nested)] + c: NestedConfig, + } + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } + #[test] fn supports_nested() { let container = Container::from(parse_quote! { @@ -323,6 +342,24 @@ mod setting_validate { assert_snapshot!(pretty(container.impl_partial_validate())); } + #[test] + fn uses_serde_names() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(rename = "aa", validate = func_ref)] + A(String), + #[serde(rename = "bb")] + #[setting(nested)] + B(NestedConfig), + #[setting(rename = "cc", required)] + C(Option), + } + }); + + assert_snapshot!(pretty(container.impl_partial_validate())); + } + #[test] fn supports_nested() { let container = Container::from(parse_quote! { diff --git a/crates/core/tests/snapshots/container_env_test__container_env__skips_unsupported_types_for_prefix.snap b/crates/core/tests/snapshots/container_env_test__container_env__skips_unsupported_types_for_prefix.snap index 79ae2e29..f46c7f23 100644 --- a/crates/core/tests/snapshots/container_env_test__container_env__skips_unsupported_types_for_prefix.snap +++ b/crates/core/tests/snapshots/container_env_test__container_env__skips_unsupported_types_for_prefix.snap @@ -9,6 +9,7 @@ fn env_values_with_prefix( let mut env = EnvManager::new(prefix.or(Some("PREFIX_"))); let mut partial = Self::default(); partial.a = env.get_prefixed("A")?; + partial.c = env.get_prefixed("C")?; partial.d = env.get_prefixed("D")?; 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 index e53b82fd..c70d5c2f 100644 --- 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 @@ -24,15 +24,7 @@ fn finalize( partial.c = Some(layer.finalize(context)?); } if let Some(layer) = partial.d { - partial.d = Some( - transform_config( - { - let layer = Arc::unwrap_or_clone(layer); - Arc::new(layer.finalize(context)?) - }, - context, - )?, - ); + partial.d = Some(transform_config(layer.finalize(context)?, 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 index bd671ba5..d2bb93f3 100644 --- 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 @@ -18,17 +18,7 @@ fn finalize( }, ) } - Self::D(pa) => { - Self::D( - transform_config( - { - let pa = Arc::unwrap_or_clone(pa); - Arc::new(pa.finalize(context)?) - }, - context, - )?, - ) - } + Self::D(pa) => Self::D(transform_config(pa.finalize(context)?, 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 index 5785c7a5..07b53b5a 100644 --- 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 @@ -24,15 +24,7 @@ fn finalize( partial.2 = Some(layer.finalize(context)?); } if let Some(layer) = partial.3 { - partial.3 = Some( - transform_config( - { - let layer = Arc::unwrap_or_clone(layer); - Arc::new(layer.finalize(context)?) - }, - context, - )?, - ); + partial.3 = Some(transform_config(layer.finalize(context)?, context)?); } Ok(partial) } diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_nested.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_nested.snap index 4312fa9e..ccd0d5d4 100644 --- a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_nested.snap @@ -21,17 +21,11 @@ fn from_partial(partial: Self::Partial) -> Self { }, d: { let data = partial.d.unwrap_or_default(); - { - let data = Arc::unwrap_or_clone(data); - Arc::new(CustomConfig::from_partial(data)) - } + Arc::new(CustomConfig::from_partial(data)) }, e: { let data = partial.e.unwrap_or_default(); - { - let data = *data; - Box::new(NestedConfig::from_partial(data)) - } + Box::new(NestedConfig::from_partial(data)) }, } } diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_standard.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_standard.snap index 6e14f0d9..63b428c6 100644 --- a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_standard.snap +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__named_struct__supports_standard.snap @@ -11,6 +11,9 @@ fn from_partial(partial: Self::Partial) -> Self { e: partial.e, f: partial.f.unwrap_or_default(), g: partial.g, - h: partial.h.unwrap_or_default(), + h: { + let data = partial.h.unwrap_or_default(); + Box::new(data) + }, } } diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_nested.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_nested.snap index 2e576c4a..b72275c4 100644 --- a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_nested.snap +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_enum__supports_nested.snap @@ -14,11 +14,6 @@ fn from_partial(partial: Self::Partial) -> Self { }, ) } - PartialExample::D(pa) => { - Self::D({ - let pa = Arc::unwrap_or_clone(pa); - Arc::new(CustomConfig::from_partial(pa)) - }) - } + PartialExample::D(pa) => Self::D(Arc::new(CustomConfig::from_partial(pa))), } } diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_nested.snap index 129fd764..b0ee2e7d 100644 --- a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_nested.snap @@ -21,17 +21,11 @@ fn from_partial(partial: Self::Partial) -> Self { }, { let data = partial.3.unwrap_or_default(); - { - let data = Arc::unwrap_or_clone(data); - Arc::new(CustomConfig::from_partial(data)) - } + Arc::new(CustomConfig::from_partial(data)) }, { let data = partial.4.unwrap_or_default(); - { - let data = *data; - Box::new(NestedConfig::from_partial(data)) - } + Box::new(NestedConfig::from_partial(data)) }, ) } diff --git a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_standard.snap b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_standard.snap index cba5d5d9..0d2b94d2 100644 --- a/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_standard.snap +++ b/crates/core/tests/snapshots/container_from_partial_test__container_from_partial__unnamed_struct__supports_standard.snap @@ -11,6 +11,9 @@ fn from_partial(partial: Self::Partial) -> Self { partial.4, partial.5.unwrap_or_default(), partial.6, - partial.7.unwrap_or_default(), + { + let data = partial.7.unwrap_or_default(); + Box::new(data) + }, ) } diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__strips_wrappers.snap b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__strips_wrappers.snap new file mode 100644 index 00000000..fa6abfbd --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__strips_wrappers.snap @@ -0,0 +1,32 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type()) +--- +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(default, deny_unknown_fields)] +struct PartialExample { + #[serde(skip_serializing_if = "Option::is_none")] + a: Option, + #[serde(skip_serializing_if = "Option::is_none")] + b: Option, + #[serde(skip_serializing_if = "Option::is_none")] + c: Option, + #[serde(skip_serializing_if = "Option::is_none")] + d: Option, + #[serde(skip_serializing_if = "Option::is_none")] + e: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + f: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + g: Option, + #[serde(skip_serializing_if = "Option::is_none")] + h: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + i: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + j: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + k: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + l: Option, +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__strips_wrappers_around_nested.snap b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__strips_wrappers_around_nested.snap new file mode 100644 index 00000000..2fc7a830 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__strips_wrappers_around_nested.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type()) +--- +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(default, deny_unknown_fields)] +struct PartialExample { + #[serde(skip_serializing_if = "Option::is_none")] + a: Option<::Partial>, + #[serde(skip_serializing_if = "Option::is_none")] + b: Option<::Partial>, + #[serde(skip_serializing_if = "Option::is_none")] + c: Option<::Partial>, + #[serde(skip_serializing_if = "Option::is_none")] + d: Option::Partial>>, + #[serde(skip_serializing_if = "Option::is_none")] + e: Option::Partial>>, + #[serde(skip_serializing_if = "Option::is_none")] + f: Option::Partial>>, +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_nested.snap b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_nested.snap index 58cabafc..142e08ce 100644 --- a/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_nested.snap @@ -12,7 +12,7 @@ struct PartialExample { #[serde(skip_serializing_if = "Option::is_none")] c: Option<::Partial>, #[serde(skip_serializing_if = "Option::is_none")] - d: Option::Partial>>, + d: Option<::Partial>, #[serde(skip_serializing_if = "Option::is_none")] e: Option::Partial>>, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_standard.snap b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_standard.snap index 7fffd4f6..345a45fb 100644 --- a/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_standard.snap +++ b/crates/core/tests/snapshots/container_type_test__partial_type__named_struct__supports_standard.snap @@ -18,9 +18,9 @@ struct PartialExample { #[serde(skip_serializing_if = "Option::is_none")] f: Option>, #[serde(skip_serializing_if = "Option::is_none")] - g: Option>, + g: Option, #[serde(skip_serializing_if = "Option::is_none")] - h: Option>>, + h: Option>, #[serde(skip_serializing_if = "Option::is_none")] i: Option>, } diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__strips_wrappers.snap b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__strips_wrappers.snap new file mode 100644 index 00000000..8de5a667 --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_enum__strips_wrappers.snap @@ -0,0 +1,13 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type()) +--- +#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] +enum PartialExample { + A(String), + B(Option), + C(Vec), + D(::Partial), + E(Option<::Partial>), + F(Box), +} diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_struct__strips_wrappers.snap b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_struct__strips_wrappers.snap new file mode 100644 index 00000000..cc1d082e --- /dev/null +++ b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_struct__strips_wrappers.snap @@ -0,0 +1,18 @@ +--- +source: crates/core/tests/container_type_test.rs +expression: pretty(container.impl_partial_type()) +--- +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(default)] +struct PartialExample( + #[serde(skip_serializing_if = "Option::is_none")] + Option, + #[serde(skip_serializing_if = "Option::is_none")] + Option, + #[serde(skip_serializing_if = "Option::is_none")] + Option>, + #[serde(skip_serializing_if = "Option::is_none")] + Option<::Partial>, + #[serde(skip_serializing_if = "Option::is_none")] + Option>, +); diff --git a/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_struct__supports_standard.snap b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_struct__supports_standard.snap index 15591a25..67e87acd 100644 --- a/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_struct__supports_standard.snap +++ b/crates/core/tests/snapshots/container_type_test__partial_type__unnamed_struct__supports_standard.snap @@ -16,5 +16,5 @@ pub struct PartialExample( #[serde(skip_serializing_if = "Option::is_none")] /// Docs. #[serde(default)] - Option>, + Option, ); 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 dea9137f..f21d9d87 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 @@ -143,10 +143,10 @@ impl schematic::PartialConfig for PartialExample { Self::Bool(pa) => {} Self::Number(pa) => {} Self::Nested(pa) => { - validate.nested("Nested.0", pa); + validate.nested_variant("Nested", 0usize, pa); } Self::List(pa) => { - validate.nested_list("List.0", pa.iter()); + validate.nested_variant_list("List", 0usize, pa.iter()); } _ => {} }; 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 index f72afa24..c4688a9d 100644 --- 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 @@ -7,10 +7,9 @@ fn default_values( ) -> 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())), + b: Some(Default::default()), + c: Some(Default::default()), + e: Some(Vec::default()), ..Default::default() }), ) diff --git a/crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers_with_defaults.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers_with_defaults.snap index 022728a6..4f02f8a1 100644 --- a/crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers_with_defaults.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_layers_with_defaults.snap @@ -8,11 +8,11 @@ fn default_values( use schematic::internal::*; Ok( Some(Self { - a: Some(Box::new(10)), - b: Some(Arc::new(Some(10))), + a: Some(10), + b: Some(10), c: Some(vec![1, 2, 3]), - d: Some(Arc::new(vec![1, 2, 3])), - e: Some(Box::new(handle_default_result(String::try_from("abc"))?)), + d: Some(vec![1, 2, 3]), + e: Some(handle_default_result(String::try_from("abc"))?), }), ) } diff --git a/crates/core/tests/snapshots/setting_default_test__setting_default__handles_nested_layers.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_nested_layers.snap index c4bf12d5..32689ccb 100644 --- a/crates/core/tests/snapshots/setting_default_test__setting_default__handles_nested_layers.snap +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_nested_layers.snap @@ -7,10 +7,8 @@ fn default_values( ) -> std::result::Result, schematic::ConfigError> { Ok( Some(Self { - a: PartialNestedConfig::default_values(context)? - .map(|inner| Box::new(inner)), - b: PartialCustomConfig::default_values(context)? - .map(|inner| Arc::new(inner)), + a: PartialNestedConfig::default_values(context)?, + b: PartialCustomConfig::default_values(context)?, c: Some(Vec::default()), }), ) diff --git a/crates/core/tests/snapshots/setting_default_test__setting_default__handles_optional_with_defaults.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_optional_with_defaults.snap new file mode 100644 index 00000000..07fbd1c9 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__handles_optional_with_defaults.snap @@ -0,0 +1,18 @@ +--- +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: Some(10), + b: Some(10), + c: Some(vec![1, 2]), + d: Some(handle_default_result(String::try_from("abc"))?), + ..Default::default() + }), + ) +} diff --git a/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__handles_layers.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__handles_layers.snap new file mode 100644 index 00000000..3b427c22 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__unnamed_struct__handles_layers.snap @@ -0,0 +1,21 @@ +--- +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( + Some(Default::default()), + Some(Default::default()), + None, + Some(Vec::default()), + Some(10), + Some(10), + Some(Default::default()), + ), + ), + ) +} diff --git a/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_wrappers.snap b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_wrappers.snap new file mode 100644 index 00000000..08209a5e --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_env__named_struct__supports_wrappers.snap @@ -0,0 +1,15 @@ +--- +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")?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_wrappers.snap b/crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_wrappers.snap new file mode 100644 index 00000000..c73bb2fd --- /dev/null +++ b/crates/core/tests/snapshots/setting_env_test__setting_env__unnamed_struct__supports_wrappers.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.get("A")?; + partial.1 = env.get("B")?; + Ok(if env.is_empty() { None } else { Some(partial) }) +} diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__replaces_collections_without_merge_attr.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__replaces_collections_without_merge_attr.snap new file mode 100644 index 00000000..da5d47c7 --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__replaces_collections_without_merge_attr.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(&mut self.a, next.a)? + .apply(&mut self.b, next.b)? + .apply(&mut self.c, next.c)?; + 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 index 0c267399..4bfeb116 100644 --- a/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested.snap @@ -12,15 +12,6 @@ fn merge( .nested(&mut self.a, next.a)? .nested(&mut self.b, next.b)? .nested(&mut self.c, next.c)? - .apply_with( - &mut self.d, - next.d, - |mut prev, next, context| { - (*Arc::make_mut(&mut prev)) - .merge(context, Arc::unwrap_or_clone(next)) - .map_err(|error| schematic::MergeError(error.to_string()))?; - Ok(Some(prev)) - }, - )?; + .nested(&mut self.d, next.d)?; Ok(()) } diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_wrappers.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_wrappers.snap index ca0a0665..4bfeb116 100644 --- a/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_wrappers.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__named_struct__supports_nested_wrappers.snap @@ -9,45 +9,9 @@ fn merge( ) -> std::result::Result<(), schematic::ConfigError> { use schematic::internal::*; MergeManager::new(context) - .apply_with( - &mut self.a, - next.a, - |mut prev, next, context| { - (*prev) - .merge(context, *next) - .map_err(|error| schematic::MergeError(error.to_string()))?; - Ok(Some(prev)) - }, - )? - .apply_with( - &mut self.b, - next.b, - |mut prev, next, context| { - (*Rc::make_mut(&mut prev)) - .merge(context, Rc::unwrap_or_clone(next)) - .map_err(|error| schematic::MergeError(error.to_string()))?; - Ok(Some(prev)) - }, - )? - .apply_with( - &mut self.c, - next.c, - |mut prev, next, context| { - (*prev) - .merge(context, *next) - .map_err(|error| schematic::MergeError(error.to_string()))?; - Ok(Some(prev)) - }, - )? - .apply_with( - &mut self.d, - next.d, - |mut prev, next, context| { - (*Arc::make_mut(&mut *prev)) - .merge(context, Arc::unwrap_or_clone(*next)) - .map_err(|error| schematic::MergeError(error.to_string()))?; - Ok(Some(prev)) - }, - )?; + .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__unnamed_enum__replaces_collections_without_merge_attr.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__replaces_collections_without_merge_attr.snap new file mode 100644 index 00000000..7c1f9979 --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__replaces_collections_without_merge_attr.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_enum__supports_multiple_nested_values.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_multiple_nested_values.snap new file mode 100644 index 00000000..877e0356 --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_multiple_nested_values.snap @@ -0,0 +1,41 @@ +--- +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::*; + match self { + Self::A(pa, pb) => { + if let Self::A(na, nb) = next { + pa.merge(context, na)?; + pb.merge(context, nb)?; + } else { + *self = next; + } + } + Self::B(pa, pb) => { + if let Self::B(na, nb) = next { + pa.merge(context, na)?; + MergeManager::new(context).nested(pb, nb)?; + } else { + *self = next; + } + } + Self::C(pa, pb) => { + if let Self::C(na, nb) = next { + pa.merge(context, na)?; + *pb = nb; + } 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 index 37e3c987..4c5bd7d0 100644 --- 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 @@ -32,7 +32,7 @@ fn merge( } Self::D(pa) => { if let Self::D(na) = next { - (*Arc::make_mut(&mut *pa)).merge(context, Arc::unwrap_or_clone(na))?; + pa.merge(context, na)?; } else { *self = next; } diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested_wrappers.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested_wrappers.snap index 610ccf14..4c5bd7d0 100644 --- a/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested_wrappers.snap +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_enum__supports_nested_wrappers.snap @@ -11,38 +11,28 @@ fn merge( match self { Self::A(pa) => { if let Self::A(na) = next { - (**pa).merge(context, *na)?; + pa.merge(context, na)?; } else { *self = next; } } Self::B(pa) => { if let Self::B(na) = next { - (*Rc::make_mut(&mut *pa)).merge(context, Rc::unwrap_or_clone(na))?; + pa.merge(context, na)?; } else { *self = next; } } Self::C(pa) => { if let Self::C(na) = next { - MergeManager::new(context) - .apply_with( - pa, - na, - |mut prev, next, context| { - (*prev) - .merge(context, *next) - .map_err(|error| schematic::MergeError(error.to_string()))?; - Ok(Some(prev)) - }, - )?; + MergeManager::new(context).nested(pa, na)?; } else { *self = next; } } Self::D(pa) => { if let Self::D(na) = next { - (*Arc::make_mut(&mut **pa)).merge(context, Arc::unwrap_or_clone(*na))?; + pa.merge(context, na)?; } else { *self = next; } diff --git a/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__replaces_collections_without_merge_attr.snap b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__replaces_collections_without_merge_attr.snap new file mode 100644 index 00000000..ec131529 --- /dev/null +++ b/crates/core/tests/snapshots/setting_merge_test__setting_merge__unnamed_struct__replaces_collections_without_merge_attr.snap @@ -0,0 +1,13 @@ +--- +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)?; + 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 index 568a4a41..c1deb807 100644 --- 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 @@ -12,15 +12,6 @@ fn merge( .nested(&mut self.0, next.0)? .nested(&mut self.1, next.1)? .nested(&mut self.2, next.2)? - .apply_with( - &mut self.3, - next.3, - |mut prev, next, context| { - (*Arc::make_mut(&mut prev)) - .merge(context, Arc::unwrap_or_clone(next)) - .map_err(|error| schematic::MergeError(error.to_string()))?; - Ok(Some(prev)) - }, - )?; + .nested(&mut self.3, next.3)?; 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 index 462f9a1c..728588aa 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap @@ -24,7 +24,7 @@ fn validate_with_path( } if let Some(setting) = &self.d { validate.check("d", setting, self, Box::new(func_ref)); - validate.nested("d", setting.as_ref()); + validate.nested("d", setting); } if let Some(setting) = &self.e { validate.nested("e", setting); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_wrappers.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_wrappers.snap index 02cf8d62..7534e099 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_wrappers.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_wrappers.snap @@ -11,16 +11,16 @@ fn validate_with_path( use schematic::internal::*; let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.a { - validate.nested("a", setting.as_ref()); + validate.nested("a", setting); } if let Some(setting) = &self.b { - validate.nested("b", setting.as_ref()); + validate.nested("b", setting); } if let Some(setting) = &self.c { - validate.nested("c", setting.as_ref().as_ref()); + validate.nested("c", setting); } if let Some(setting) = &self.d { - validate.nested_list("d", setting.as_ref().iter()); + validate.nested_list("d", setting.iter()); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__uses_serde_names.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__uses_serde_names.snap new file mode 100644 index 00000000..02c4b146 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__uses_serde_names.snap @@ -0,0 +1,26 @@ +--- +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("aa", setting, self, Box::new(func_ref)); + } + if self.b.is_none() { + validate.required("bb"); + } + if let Some(setting) = &self.c { + validate.nested("cc", setting); + } + 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 index b42e0daa..0cec4962 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap @@ -12,7 +12,7 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check("A", (pa), self, func_call()); + validate.check_variant("A", (pa), self, func_call()); } _ => {} }; diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap index dd16a06f..13a6fde7 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap @@ -12,7 +12,7 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check("A", (pa), self, Box::new(func_ref)); + validate.check_variant("A", (pa), self, Box::new(func_ref)); } _ => {} }; diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap index d6668dd7..6843ab39 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap @@ -12,24 +12,24 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check("A", (pa), self, Box::new(func_ref)); + validate.check_variant("A", (pa), self, Box::new(func_ref)); } Self::B(pa, pb) => { - validate.check("B", (pa, pb), self, Box::new(func_ref)); + validate.check_variant("B", (pa, pb), self, Box::new(func_ref)); } Self::C(pa, pb, pc) => { - validate.check("C", (pa, pb, pc), self, Box::new(func_ref)); + validate.check_variant("C", (pa, pb, pc), self, Box::new(func_ref)); } Self::A(pa) => { - validate.check("A", (pa), self, Box::new(func_ref)); + validate.check_variant("A", (pa), self, Box::new(func_ref)); if [pa].iter().any(|v| v.is_none()) { - validate.required("A"); + validate.required_variant("A"); } } Self::B(pa, pb) => { - validate.check("B", (pa, pb), self, Box::new(func_ref)); + validate.check_variant("B", (pa, pb), self, Box::new(func_ref)); if [pa, pb].iter().any(|v| v.is_none()) { - validate.required("B"); + validate.required_variant("B"); } } _ => {} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap index e40497e6..b205982b 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap @@ -12,28 +12,28 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check("A", (pa), self, Box::new(func_ref)); - validate.nested("A.0", pa); + validate.check_variant("A", (pa), self, Box::new(func_ref)); + validate.nested_variant("A", 0usize, pa); } Self::B(pa) => { - validate.check("B", (pa), self, Box::new(func_ref)); - validate.nested("B.0", pa); + validate.check_variant("B", (pa), self, Box::new(func_ref)); + validate.nested_variant("B", 0usize, pa); } Self::C(pa) => { - validate.check("C", (pa), self, Box::new(func_ref)); + validate.check_variant("C", (pa), self, Box::new(func_ref)); if let Some(pa) = pa { - validate.nested("C.0", pa); + validate.nested_variant("C", 0usize, pa); } } Self::D(pa) => { - validate.check("D", (pa), self, Box::new(func_ref)); - validate.nested("D.0", pa.as_ref()); + validate.check_variant("D", (pa), self, Box::new(func_ref)); + validate.nested_variant("D", 0usize, pa); } Self::E(pa) => { - validate.nested("E.0", pa); + validate.nested_variant("E", 0usize, pa); } Self::F(pa) => { - validate.nested("F.0", pa); + validate.nested_variant("F", 0usize, pa); } _ => {} }; diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap index 7fdce321..49694f97 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap @@ -12,28 +12,28 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check("A", (pa), self, Box::new(func_ref)); - validate.nested_list("A.0", pa.iter()); + validate.check_variant("A", (pa), self, Box::new(func_ref)); + validate.nested_variant_list("A", 0usize, pa.iter()); } Self::B(pa) => { - validate.check("B", (pa), self, Box::new(func_ref)); - validate.nested_map("B.0", pa.iter()); + validate.check_variant("B", (pa), self, Box::new(func_ref)); + validate.nested_variant_map("B", 0usize, pa.iter()); } Self::C(pa) => { - validate.check("C", (pa), self, Box::new(func_ref)); + validate.check_variant("C", (pa), self, Box::new(func_ref)); if let Some(pa) = pa { - validate.nested_list("C.0", pa.iter()); + validate.nested_variant_list("C", 0usize, pa.iter()); } } Self::D(pa) => { - validate.nested_list("D.0", pa.iter()); + validate.nested_variant_list("D", 0usize, pa.iter()); } Self::E(pa) => { - validate.nested_map("E.0", pa.iter()); + validate.nested_variant_map("E", 0usize, pa.iter()); } Self::F(pa) => { if let Some(pa) = pa { - validate.nested_list("F.0", pa.iter()); + validate.nested_variant_list("F", 0usize, pa.iter()); } } _ => {} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_wrappers.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_wrappers.snap index 5f70f216..c0044949 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_wrappers.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_wrappers.snap @@ -12,23 +12,23 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.nested("A.0", pa.as_ref()); + validate.nested_variant("A", 0usize, pa); } Self::B(pa) => { if let Some(pa) = pa { - validate.nested("B.0", pa.as_ref()); + validate.nested_variant("B", 0usize, pa); } } Self::C(pa) => { - validate.nested("C.0", pa.as_ref().as_ref()); + validate.nested_variant("C", 0usize, pa); } Self::D(pa) => { if let Some(pa) = pa { - validate.nested_list("D.0", pa.iter()); + validate.nested_variant_list("D", 0usize, pa.iter()); } } Self::E(pa) => { - validate.nested_list("E.0", pa.as_ref().iter()); + validate.nested_variant_list("E", 0usize, pa.iter()); } _ => {} }; diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap index fbe85971..a81458c8 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap @@ -12,19 +12,19 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check("A", (pa), self, Box::new(func_ref)); + validate.check_variant("A", (pa), self, Box::new(func_ref)); } Self::B(pa) => { - validate.check("B", (pa), self, Box::new(func_ref)); + validate.check_variant("B", (pa), self, Box::new(func_ref)); } Self::C(pa) => { - validate.check("C", (pa), self, Box::new(func_ref)); + validate.check_variant("C", (pa), self, Box::new(func_ref)); } Self::D(pa) => { - validate.check("D", (pa), self, Box::new(func_ref)); + validate.check_variant("D", (pa), self, Box::new(func_ref)); } Self::E(pa) => { - validate.check("E", (pa), self, Box::new(func_ref)); + validate.check_variant("E", (pa), self, Box::new(func_ref)); } _ => {} }; diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__uses_serde_names.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__uses_serde_names.snap new file mode 100644 index 00000000..c7bc7d13 --- /dev/null +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__uses_serde_names.snap @@ -0,0 +1,31 @@ +--- +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_variant("aa", (pa), self, Box::new(func_ref)); + } + Self::B(pa) => { + validate.nested_variant("bb", 0usize, pa); + } + Self::C(pa) => { + if [pa].iter().any(|v| v.is_none()) { + validate.required_variant("cc"); + } + } + _ => {} + }; + 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 index 7dffc691..d945d395 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap @@ -24,7 +24,7 @@ fn validate_with_path( } if let Some(setting) = &self.3 { validate.check("3", setting, self, Box::new(func_ref)); - validate.nested("3", setting.as_ref()); + validate.nested("3", setting); } if let Some(setting) = &self.4 { validate.nested("4", setting); diff --git a/crates/schematic/src/config/configs.rs b/crates/schematic/src/config/configs.rs index 34cc8d77..4068cb69 100644 --- a/crates/schematic/src/config/configs.rs +++ b/crates/schematic/src/config/configs.rs @@ -100,11 +100,16 @@ pub trait Config: Sized + Schematic { type Partial: PartialConfig; /// Return default values for the partial configuration. + /// + /// # Panics + /// + /// Panics if a default value could not be generated, as this is + /// infallible from the perspective of [`Default`]. fn default_partial() -> Self::Partial { let context = <::Partial as PartialConfig>::Context::default(); <::Partial as PartialConfig>::default_values(&context) - .unwrap_or_default() + .expect("Failed to generate default values.") .unwrap_or_default() } diff --git a/crates/schematic/src/internal/validate.rs b/crates/schematic/src/internal/validate.rs index bc1774c7..8726f80c 100644 --- a/crates/schematic/src/internal/validate.rs +++ b/crates/schematic/src/internal/validate.rs @@ -19,38 +19,93 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { } pub fn check(&mut self, key: &str, value: &V, data: &D, validator: Validator) { + self.check_at(self.path.join_key(key), value, data, validator); + } + + pub fn check_variant( + &mut self, + variant: &str, + value: &V, + data: &D, + validator: Validator, + ) { + self.check_at(self.path.join_variant(variant), value, data, validator); + } + + fn check_at(&mut self, path: Path, 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))); + self.errors.push(error.prepend_path(path)); } } pub fn required(&mut self, key: &str) { + self.required_at(self.path.join_key(key)); + } + + pub fn required_variant(&mut self, variant: &str) { + self.required_at(self.path.join_variant(variant)); + } + + fn required_at(&mut self, path: Path) { if self.finalizing { self.errors - .push(ValidateError::required().prepend_path(self.path.join_key(key))); + .push(ValidateError::required().prepend_path(path)); } } 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.nested_at(self.path.join_key(key), value); + } + + pub fn nested_variant>( + &mut self, + variant: &str, + index: usize, + value: &S, + ) { + self.nested_at(self.variant_path(variant, index), value); + } + + fn nested_at>(&mut self, path: Path, value: &S) { + if let Err(errors) = value.validate_with_path(self.context, self.finalizing, path) { self.errors.extend(errors); } } + fn variant_path(&self, variant: &str, index: usize) -> Path { + self.path.join_variant(variant).join_index(index) + } + pub fn nested_list<'v, I: IntoIterator, S: PartialConfig + 'v>( &mut self, key: &str, list: I, + ) { + self.nested_list_at(self.path.join_key(key), list); + } + + pub fn nested_variant_list< + 'v, + I: IntoIterator, + S: PartialConfig + 'v, + >( + &mut self, + variant: &str, + index: usize, + list: I, + ) { + self.nested_list_at(self.variant_path(variant, index), list); + } + + fn nested_list_at<'v, I: IntoIterator, S: PartialConfig + 'v>( + &mut self, + path: Path, + 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), - ) { + if let Err(errors) = + item.validate_with_path(self.context, self.finalizing, path.join_index(i)) + { self.errors.extend(errors); } } @@ -64,13 +119,36 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { &mut self, key: &str, map: I, + ) { + self.nested_map_at(self.path.join_key(key), map); + } + + pub fn nested_variant_map< + 'v, + I: IntoIterator, + S: PartialConfig + 'v, + >( + &mut self, + variant: &str, + index: usize, + map: I, + ) { + self.nested_map_at(self.variant_path(variant, index), map); + } + + fn nested_map_at< + 'v, + I: IntoIterator, + S: PartialConfig + 'v, + >( + &mut self, + path: Path, + 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), - ) { + if let Err(errors) = + value.validate_with_path(self.context, self.finalizing, path.join_key(sub_key)) + { self.errors.extend(errors); } } From 43e93064e921f413343bd602323e0b27a9167919 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 16 Aug 2026 12:55:46 -0700 Subject: [PATCH 08/18] internal: Implement `impl Schematic`. (#194) --- crates/core/src/container.rs | 186 +++++++- crates/core/src/field.rs | 145 ++++++ crates/core/src/utils.rs | 95 +++- crates/core/src/variant.rs | 167 +++++++ crates/core/tests/container_schema_test.rs | 445 ++++++++++++++++++ ...ma_type__named_struct__supports_empty.snap | 7 + ...type__named_struct__supports_metadata.snap | 45 ++ ...a_type__named_struct__supports_nested.snap | 36 ++ ...type__named_struct__supports_standard.snap | 74 +++ ...ype__unit_enum__supports_adjacent_tag.snap | 24 + ...ma_type__unit_enum__supports_external.snap | 34 ++ ...ype__unit_enum__supports_internal_tag.snap | 24 + ...ma_type__unit_enum__supports_untagged.snap | 24 + ...__unnamed_enum__supports_adjacent_tag.snap | 128 +++++ ...type__unnamed_enum__supports_external.snap | 83 ++++ ...__unnamed_enum__supports_internal_tag.snap | 132 ++++++ ...enum__supports_renames_and_exclusions.snap | 23 + ...type__unnamed_enum__supports_untagged.snap | 28 ++ ...named_enum__supports_variant_untagged.snap | 27 ++ ..._type__unnamed_struct__supports_empty.snap | 7 + ..._unnamed_struct__supports_many_values.snap | 20 + ...ct__supports_single_nested_collection.snap | 8 + ..._struct__supports_single_nested_value.snap | 8 + ...unnamed_struct__supports_single_value.snap | 9 + ..._schema_test__schematic__named_struct.snap | 30 ++ ...ner_schema_test__schematic__unit_enum.snap | 44 ++ ..._schema_test__schematic__unnamed_enum.snap | 35 ++ ...chema_test__schematic__unnamed_struct.snap | 25 + ...est__schematic__implements_both_types.snap | 24 +- ...er_type_test__to_tokens__named_struct.snap | 73 ++- ...ainer_type_test__to_tokens__unit_enum.snap | 46 +- ...er_type_test__to_tokens__unnamed_enum.snap | 32 +- ..._type_test__to_tokens__unnamed_struct.snap | 27 +- 33 files changed, 2102 insertions(+), 13 deletions(-) create mode 100644 crates/core/tests/container_schema_test.rs create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_empty.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_metadata.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_nested.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_standard.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_adjacent_tag.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_external.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_internal_tag.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_untagged.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_adjacent_tag.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_external.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_internal_tag.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_renames_and_exclusions.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_untagged.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_variant_untagged.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_empty.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_many_values.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_single_nested_collection.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_single_nested_value.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_single_value.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schematic__named_struct.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schematic__unit_enum.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schematic__unnamed_enum.snap create mode 100644 crates/core/tests/snapshots/container_schema_test__schematic__unnamed_struct.snap diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 6a8efd4a..e4abd878 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -1,4 +1,6 @@ -use crate::args::{PartialArg, SerdeContainerArgs, SerdeRenameArg}; +use crate::args::{ + PartialArg, SerdeContainerArgs, SerdeIoDirection, SerdeRenameArg, SerdeTagFormat, +}; use crate::field::{EnvKey, Field}; use crate::utils::{ImplResult, is_inheritable_attribute, to_type_string}; use crate::variant::Variant; @@ -107,6 +109,47 @@ impl Container { format_ident!("Partial{}", self.ident) } + /// Return the name that identifies this container to users, + /// which is used for schema references. + pub fn get_name(&self) -> String { + let dir = SerdeIoDirection::From; + + if let Some(name) = self.args.rename.as_ref().and_then(|rn| rn.get_name(dir)) { + return name.into(); + } + + if let Some(name) = self + .serde_args + .rename + .as_ref() + .and_then(|rn| rn.get_name(dir)) + { + return name.into(); + } + + self.ident.to_string() + } + + /// Return how the enum variants are tagged when serialized. + pub fn get_tag_format(&self) -> SerdeTagFormat { + // Every variant is a unit, so they serialize as their own name + if matches!(self.inner, ContainerInner::UnitEnum { .. }) { + return SerdeTagFormat::Unit; + } + + if self.serde_args.untagged { + return SerdeTagFormat::Untagged; + } + + match (&self.serde_args.tag, &self.serde_args.content) { + (Some(tag), Some(content)) => { + SerdeTagFormat::Adjacent(tag.to_owned(), content.to_owned()) + } + (Some(tag), None) => SerdeTagFormat::Internal(tag.to_owned()), + _ => SerdeTagFormat::External, + } + } + /// Whether the partial enum is untagged, and requires /// each variant to be attempted in order when deserializing. pub fn is_untagged(&self) -> bool { @@ -485,11 +528,11 @@ impl Container { /// Generate `Schematic` implementations for both types, which are /// required by the `Config` and `PartialConfig` traits. + #[cfg(not(feature = "schema"))] pub fn impl_schematic(&self) -> TokenStream { let base_name = &self.ident; let partial_name = self.get_partial_ident(); - // TODO: Generate schemas when the `schema` feature is enabled quote! { #[automatically_derived] impl schematic::Schematic for #base_name {} @@ -499,6 +542,145 @@ impl Container { } } + /// Generate `Schematic` implementations for both types. The partial + /// derives its schema from the full type, with all settings marked + /// as partial. + #[cfg(feature = "schema")] + pub fn impl_schematic(&self) -> TokenStream { + let base_name = &self.ident; + let partial_name = self.get_partial_ident(); + + let base_name_string = self.get_name(); + let partial_name_string = partial_name.to_string(); + let inner = self.impl_schematic_type(); + + quote! { + #[automatically_derived] + impl schematic::Schematic for #base_name { + fn schema_name() -> Option { + Some(#base_name_string.into()) + } + + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + + #inner + } + } + + #[automatically_derived] + impl schematic::Schematic for #partial_name { + fn schema_name() -> Option { + Some(#partial_name_string.into()) + } + + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = #base_name::build_schema(schema); + schematic::internal::partialize_schema(&mut schema, true); + schema + } + } + } + } + + /// Generate the schema for the full configuration. + #[cfg(feature = "schema")] + pub fn impl_schematic_type(&self) -> TokenStream { + use crate::utils::{extract_comment, extract_deprecated}; + + let mut meta = vec![]; + + if let Some(value) = extract_deprecated(&self.attrs) { + meta.push(quote! { schema.set_deprecated(#value); }); + } + + if let Some(value) = extract_comment(&self.attrs) { + meta.push(quote! { schema.set_description(#value); }); + } + + match &self.inner { + ContainerInner::NamedStruct { fields } => { + let types = fields + .iter() + .filter(|field| !field.is_excluded()) + .map(|field| field.impl_schema_type(true)) + .collect::>(); + + if types.is_empty() { + quote! { + #(#meta)* + schema.structure(StructType::default()) + } + } else { + quote! { + #(#meta)* + schema.structure(StructType::new([ + #(#types),* + ])) + } + } + } + ContainerInner::UnnamedStruct { fields } => { + let types = fields + .iter() + .filter(|field| !field.is_excluded()) + .map(|field| field.impl_schema_type(false)) + .collect::>(); + + // A single value is transparent, so use its schema directly + if types.len() == 1 { + let inner = &types[0]; + + quote! { + let mut schema = #inner; + #(#meta)* + schema + } + } else { + quote! { + #(#meta)* + schema.tuple(TupleType::new([ + #(#types),* + ])) + } + } + } + ContainerInner::UnnamedEnum { variants } | ContainerInner::UnitEnum { variants } => { + let unit = matches!(self.inner, ContainerInner::UnitEnum { .. }); + let tag_format = self.get_tag_format(); + let mut default_index = quote! { None }; + let mut types = vec![]; + + for variant in variants { + if variant.is_excluded() { + continue; + } + + if variant.is_default() { + let index = types.len(); + + default_index = quote! { Some(#index) }; + } + + types.push(variant.impl_schema_type(&tag_format)); + } + + // Enums of only units are enumerable values, + // otherwise they're a union of schemas + let builder = if unit { + quote! { schema.enumerable(EnumType::from_schemas([#(#types),*], #default_index)) } + } else { + quote! { schema.union(UnionType::from_schemas([#(#types),*], #default_index)) } + }; + + quote! { + #(#meta)* + #builder + } + } + } + } + pub fn impl_partial(&self) -> TokenStream { let partial_name = self.get_partial_ident(); let context = match self.args.context.as_ref() { diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index fda541b6..10584465 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -234,6 +234,10 @@ impl Field { } } + pub fn is_flatten(&self) -> bool { + self.args.flatten || self.serde_args.flatten + } + pub fn is_nested(&self) -> bool { self.args .nested @@ -241,9 +245,35 @@ impl Field { .is_some_and(|nested| nested.is_nested()) } + /// Whether the setting accepts a `null` value. + pub fn is_nullable(&self) -> bool { + self.value.is_outer_option_wrapped() + } + + /// Whether the setting can be omitted, because a default is provided. + pub fn is_optional(&self) -> bool { + self.args.default.is_some() || self.serde_args.default + } + pub fn is_required(&self) -> bool { self.args.required } + + pub fn is_skipped(&self) -> bool { + self.args.skip || self.serde_args.skip + } + + pub fn get_aliases(&self) -> Vec<&String> { + let mut aliases = vec![]; + + for alias in self.args.alias.iter().chain(self.serde_args.alias.iter()) { + if !aliases.contains(&alias) { + aliases.push(alias); + } + } + + aliases + } } impl Field { @@ -366,6 +396,121 @@ impl ToTokens for Field { } } +#[cfg(feature = "schema")] +impl Field { + /// Generate the schema for this setting. When `as_field` is true, it's + /// wrapped in a `SchemaField` keyed by name, otherwise the bare schema + /// is returned, for use within tuples. + pub fn impl_schema_type(&self, as_field: bool) -> TokenStream { + use crate::utils::{extract_comment, extract_deprecated}; + use syn::Lit; + + let ty = &self.value.ty; + + // Nested configs are inferred as partials, so that the + // schema can be marked as such + let mut schema = if self.is_nested() { + quote! { schema.infer_as_nested::<#ty>() } + } else { + quote! { schema.infer::<#ty>() } + }; + + // Literal defaults are rendered within the schema + if let Some(Expr::Lit(lit)) = &self.args.default { + let value = match &lit.lit { + Lit::Str(v) => Some(quote! { LiteralValue::String(#v.into()) }), + Lit::Int(v) => Some(if v.suffix().starts_with('u') { + quote! { LiteralValue::Uint(#v) } + } else { + quote! { LiteralValue::Int(#v) } + }), + Lit::Float(v) => Some(if v.suffix() == "f32" { + quote! { LiteralValue::F32(#v) } + } else { + quote! { LiteralValue::F64(#v) } + }), + Lit::Bool(v) => Some(quote! { LiteralValue::Bool(#v) }), + _ => None, + }; + + if let Some(value) = value { + schema = quote! { schema.infer_with_default::<#ty>(#value) }; + } + } + + let comment = extract_comment(&self.attrs); + let deprecated = extract_deprecated(&self.attrs); + + // Tuple items only support a description + if !as_field { + return match comment { + Some(comment) => quote! { + { + let mut schema = #schema; + schema.set_description(#comment); + schema + } + }, + None => schema, + }; + } + + let mut statements = vec![]; + + let aliases = self.get_aliases(); + + if !aliases.is_empty() { + statements.push(quote! { + field.aliases = [#(#aliases),*] + .into_iter() + .map(|alias| alias.to_string()) + .collect::>(); + }); + } + + if let Some(comment) = comment { + statements.push(quote! { field.comment = Some(#comment.into()); }); + } + + if let Some(deprecated) = deprecated { + statements.push(quote! { field.deprecated = Some(#deprecated.into()); }); + } + + // Only explicit keys are known statically, as derived + // keys depend on the prefix in effect at runtime + if let Some(EnvKey::Explicit(env_var)) = self.get_env_var() { + statements.push(quote! { field.env_var = Some(#env_var.into()); }); + } + + for (name, enabled) in [ + ("flatten", self.is_flatten()), + ("hidden", self.is_skipped()), + ("nullable", self.is_nullable()), + ("optional", self.is_optional()), + ] { + if enabled { + let name = format_ident!("{name}"); + + statements.push(quote! { field.#name = true; }); + } + } + + let name = self.get_name_or_index(); + + if statements.is_empty() { + quote! { (#name.into(), SchemaField::new(#schema)) } + } else { + quote! { + (#name.into(), { + let mut field = SchemaField::new(#schema); + #(#statements)* + field + }) + } + } + } +} + impl Field { pub fn impl_full_from_partial(&self) -> ImplResult { let key = self.get_key(); diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index 0dfa8e53..f4093c0a 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{Attribute, Expr, Meta, Path}; +use syn::{Attribute, Expr, ExprLit, Lit, Meta, Path}; pub fn get_meta_path(meta: &Meta) -> &Path { match meta { @@ -38,6 +38,99 @@ pub fn is_inheritable_attribute(attr: &Attribute) -> bool { .any(|n| path.is_ident(n)) } +/// Extract the doc comment from a list of attributes, as a single +/// block of text, with list items preserved on their own lines. +pub fn extract_comment(attrs: &[Attribute]) -> Option { + let mut lines = vec![]; + + for attr in attrs { + let Meta::NameValue(meta) = &attr.meta else { + continue; + }; + + if !meta.path.is_ident("doc") { + continue; + } + + let Expr::Lit(ExprLit { + lit: Lit::Str(value), + .. + }) = &meta.value + else { + continue; + }; + + for line in value.value().split('\n') { + let line = line.trim(); + + // Preserve list items as their own line + if line.starts_with("* ") || line.starts_with("- ") { + lines.push(format!("\n{line}")); + } else { + lines.push(line.to_owned()); + } + } + } + + if lines.is_empty() { + return None; + } + + Some(lines.join(" ").trim().to_owned()) +} + +/// Extract the deprecated message from a list of attributes. Returns an +/// empty string when deprecated without a message. +pub fn extract_deprecated(attrs: &[Attribute]) -> Option { + for attr in attrs { + if !get_meta_path(&attr.meta).is_ident("deprecated") { + continue; + } + + match &attr.meta { + // #[deprecated] + Meta::Path(_) => { + return Some(String::new()); + } + // #[deprecated = "message"] + Meta::NameValue(meta) => { + if let Expr::Lit(lit) = &meta.value { + match &lit.lit { + Lit::Bool(value) => { + if value.value() { + return Some(String::new()); + } + } + Lit::Str(value) => { + return Some(value.value().trim().to_owned()); + } + _ => {} + }; + } + } + // #[deprecated(since = "", note = "message")] + Meta::List(_) => { + let mut message = String::new(); + + let _ = attr.parse_nested_meta(|meta| { + if meta.path.is_ident("note") + && let Ok(value) = meta.value() + && let Ok(Lit::Str(value)) = value.parse::() + { + message = value.value().trim().to_owned(); + } + + Ok(()) + }); + + return Some(message); + } + }; + } + + None +} + pub fn to_type_string(ts: TokenStream) -> String { format!("{ts}") .replace(" :: ", "::") diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index 783e7881..523fbe04 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "schema")] +use crate::args::SerdeTagFormat; use crate::args::{ NestedArg, PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeIoDirection, SerdeRenameArg, }; @@ -251,6 +253,171 @@ impl Variant { } } + /// Generate the schema for this variant, shaped by how the + /// enum is tagged. + #[cfg(feature = "schema")] + pub fn impl_schema_type(&self, tag_format: &SerdeTagFormat) -> TokenStream { + use crate::utils::{extract_comment, extract_deprecated}; + + let name = self.get_name(); + let nested = self.is_nested(); + + // Untagged variants are represented by their value alone + let untagged = matches!(tag_format, SerdeTagFormat::Untagged) + || self.args.untagged + || self.serde_args.untagged; + + let inner = match &self.fields { + Fields::Named(_) => panic!("Enums with named fields are not supported!"), + Fields::Unnamed(fields) => { + let items = fields + .unnamed + .iter() + .map(|field| { + let ty = &field.ty; + + if nested { + quote! { schema.infer_as_nested::<#ty>() } + } else { + quote! { schema.infer::<#ty>() } + } + }) + .collect::>(); + + if items.len() == 1 { + let item = &items[0]; + + quote! { #item } + } else { + quote! { + Schema::tuple(TupleType::new([ + #(#items),* + ])) + } + } + } + // Unit variants carry no value, so they're the name itself + Fields::Unit => quote! { Schema::literal_value(LiteralValue::String(#name.into())) }, + }; + + // Wrap the tagged value so that nested configs can be partialized + let wrap = |value: TokenStream| { + if nested { + quote! { + { + let mut item = #value; + item.partialize(); + item + } + } + } else { + value + } + }; + + let unit = self.is_unit_variant(); + let name_literal = quote! { Schema::literal_value(LiteralValue::String(#name.into())) }; + + let mut schema = if let SerdeTagFormat::Unit = tag_format { + // Every variant is a unit, so the enum is a list of named values + let ty = if self.args.null { + quote! { Schema::null() } + } else { + inner + }; + + quote! { + Schema { + name: Some(#name.into()), + ty: #ty.ty, + ..Default::default() + } + } + } else if self.args.null { + // Explicitly represents null, so it's never tagged + quote! { Schema::null() } + } else if untagged { + // Untagged variants are represented by their value alone, + // and units have no value + if unit { + quote! { Schema::null() } + } else { + inner + } + } else { + match tag_format { + SerdeTagFormat::Unit | SerdeTagFormat::Untagged => unreachable!(), + // "name" | { "name": value } + SerdeTagFormat::External => { + if unit { + inner + } else { + wrap(quote! { + Schema::structure(StructType::new([ + (#name.into(), #inner), + ])) + }) + } + } + // { "tag": "name", ...value } + SerdeTagFormat::Internal(tag) => { + if unit { + quote! { + Schema::structure(StructType::new([ + (#tag.into(), #name_literal), + ])) + } + } else { + wrap(quote! { + { + let mut item = #inner; + item.ty.add_field(#tag, SchemaField::new(#name_literal)); + item + } + }) + } + } + // { "tag": "name" } | { "tag": "name", "content": value } + SerdeTagFormat::Adjacent(tag, content) => { + if unit { + quote! { + Schema::structure(StructType::new([ + (#tag.into(), #name_literal), + ])) + } + } else { + wrap(quote! { + Schema::structure(StructType::new([ + (#tag.into(), #name_literal), + (#content.into(), #inner), + ])) + }) + } + } + } + }; + + let comment = extract_comment(&self.attrs); + let deprecated = extract_deprecated(&self.attrs); + + if comment.is_some() || deprecated.is_some() { + let comment = comment.map(|value| quote! { item.description = Some(#value.into()); }); + let deprecated = + deprecated.map(|value| quote! { item.deprecated = Some(#value.into()); }); + + schema = quote! { + { + let mut item = #schema; + #comment + #deprecated + item + } + }; + } + + schema + } + pub fn impl_full_from_partial(&self, partial_name: &Ident) -> ImplResult { let mut res = ImplResult::default(); let name = &self.ident; diff --git a/crates/core/tests/container_schema_test.rs b/crates/core/tests/container_schema_test.rs new file mode 100644 index 00000000..332dc132 --- /dev/null +++ b/crates/core/tests/container_schema_test.rs @@ -0,0 +1,445 @@ +mod utils; + +use proc_macro2::TokenStream; +use quote::quote; +use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; +use utils::pretty; + +// `impl_schematic_type` returns a method body, so wrap it to be parsable +fn pretty_body(tokens: TokenStream) -> String { + pretty(quote! { + fn build_schema(mut schema: SchemaBuilder) -> Schema { + #tokens + } + }) +} + +// The trait implementations for both the full and partial types +mod schematic { + use super::*; + + #[test] + fn named_struct() { + let container = Container::from(parse_quote! { + /// Container docs. + #[derive(Config)] + #[config(rename = "Renamed")] + #[deprecated = "Use something else."] + struct Example { + a: bool, + } + }); + + assert_snapshot!(pretty(container.impl_schematic())); + } + + #[test] + fn unnamed_struct() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example(bool, usize); + }); + + assert_snapshot!(pretty(container.impl_schematic())); + } + + #[test] + fn unnamed_enum() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(rename = "Renamed")] + enum Example { + A(bool), + } + }); + + assert_snapshot!(pretty(container.impl_schematic())); + } + + #[test] + fn unit_enum() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + A, + B, + } + }); + + assert_snapshot!(pretty(container.impl_schematic())); + } +} + +mod schema_type { + use super::*; + + mod named_struct { + use super::*; + + #[test] + fn supports_standard() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + a: bool, + /// Some docs. + b: usize, + #[setting(default = "abc")] + c: String, + #[setting(default = 10)] + d: usize, + #[setting(default = 1.5)] + e: f32, + #[setting(default = true)] + f: bool, + g: Option, + h: Vec, + } + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_metadata() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[config(env_prefix = "APP_")] + struct Example { + /// Docs. + #[deprecated = "Gone soon."] + #[setting(rename = "aa", env = "A_VAR")] + a: String, + #[serde(alias = "bee", alias = "bb", flatten)] + b: HashMap, + #[setting(skip)] + c: usize, + // Derived env keys are not statically known + d: usize, + #[setting(exclude)] + e: usize, + #[setting(nested)] + f: NestedConfig, + } + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_nested() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(nested)] + a: NestedConfig, + #[setting(nested)] + b: Option, + #[setting(nested)] + c: Vec, + #[setting(nested = CustomConfig)] + d: HashMap, + #[setting(nested)] + e: Box, + } + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_empty() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example {} + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + } + + mod unnamed_struct { + use super::*; + + #[test] + fn supports_single_value() { + let container = Container::from(parse_quote! { + /// Docs. + #[derive(Config)] + struct Example(String); + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_single_nested_value() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example(#[setting(nested)] NestedConfig); + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_single_nested_collection() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example(#[setting(nested)] Vec); + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_many_values() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example( + String, + /// Item docs. + Vec, + #[setting(nested)] + NestedConfig, + #[setting(nested)] + Vec, + #[setting(nested = CustomConfig)] + Option, + #[setting(exclude)] + bool, + ); + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_empty() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example(); + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + } + + // Every tagging format uses the same variants, so that the + // output can be compared across them + mod unnamed_enum { + use super::*; + + #[test] + fn supports_external() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + /// Docs. + Text(String), + List(Vec), + #[setting(default)] + Pair(usize, bool), + #[setting(nested)] + Inner(NestedConfig), + #[setting(nested)] + InnerOpt(Option), + #[setting(nested)] + InnerList(Vec), + #[setting(nested = CustomConfig)] + InnerMap(HashMap), + Unit, + } + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_untagged() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(untagged)] + enum Example { + /// Docs. + Text(String), + List(Vec), + #[setting(default)] + Pair(usize, bool), + #[setting(nested)] + Inner(NestedConfig), + #[setting(nested)] + InnerOpt(Option), + #[setting(nested)] + InnerList(Vec), + #[setting(nested = CustomConfig)] + InnerMap(HashMap), + Unit, + } + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_internal_tag() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(tag = "type")] + enum Example { + /// Docs. + Text(String), + List(Vec), + #[setting(default)] + Pair(usize, bool), + #[setting(nested)] + Inner(NestedConfig), + #[setting(nested)] + InnerOpt(Option), + #[setting(nested)] + InnerList(Vec), + #[setting(nested = CustomConfig)] + InnerMap(HashMap), + Unit, + } + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_adjacent_tag() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(tag = "type", content = "value")] + enum Example { + /// Docs. + Text(String), + List(Vec), + #[setting(default)] + Pair(usize, bool), + #[setting(nested)] + Inner(NestedConfig), + #[setting(nested)] + InnerOpt(Option), + #[setting(nested)] + InnerList(Vec), + #[setting(nested = CustomConfig)] + InnerMap(HashMap), + Unit, + } + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_variant_untagged() { + // A single variant can opt out of the container's tagging + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(tag = "type")] + enum Example { + Text(String), + #[setting(untagged)] + Inner(String), + #[serde(untagged)] + Other(String), + } + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_renames_and_exclusions() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + #[setting(rename = "text")] + Text(String), + #[serde(rename = "number")] + Number(usize), + #[setting(exclude)] + Gone(bool), + #[setting(default)] + Last(bool), + } + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + } + + // All-unit enums are enumerable values, regardless of the + // tagging format that was configured + mod unit_enum { + use super::*; + + #[test] + fn supports_external() { + let container = Container::from(parse_quote! { + #[derive(Config)] + enum Example { + /// Docs. + A, + #[setting(default, rename = "bb")] + B, + #[setting(null)] + C, + #[setting(exclude)] + D, + } + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_untagged() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(untagged)] + enum Example { + A, + #[setting(default)] + B, + } + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_internal_tag() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(tag = "type")] + enum Example { + A, + #[setting(default)] + B, + } + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + + #[test] + fn supports_adjacent_tag() { + let container = Container::from(parse_quote! { + #[derive(Config)] + #[serde(tag = "type", content = "value")] + enum Example { + A, + #[setting(default)] + B, + } + }); + + assert_snapshot!(pretty_body(container.impl_schematic_type())); + } + } +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_empty.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_empty.snap new file mode 100644 index 00000000..ec14bfa3 --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_empty.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.structure(StructType::default()) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_metadata.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_metadata.snap new file mode 100644 index 00000000..a208b779 --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_metadata.snap @@ -0,0 +1,45 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .structure( + StructType::new([ + ( + "aa".into(), + { + let mut field = SchemaField::new(schema.infer::()); + field.comment = Some("Docs.".into()); + field.deprecated = Some("Gone soon.".into()); + field.env_var = Some("A_VAR".into()); + field + }, + ), + ( + "b".into(), + { + let mut field = SchemaField::new( + schema.infer::>(), + ); + field.aliases = ["bee", "bb"] + .into_iter() + .map(|alias| alias.to_string()) + .collect::>(); + field.flatten = true; + field + }, + ), + ( + "c".into(), + { + let mut field = SchemaField::new(schema.infer::()); + field.hidden = true; + field + }, + ), + ("d".into(), SchemaField::new(schema.infer::())), + ("f".into(), SchemaField::new(schema.infer_as_nested::())), + ]), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_nested.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_nested.snap new file mode 100644 index 00000000..420dd948 --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_nested.snap @@ -0,0 +1,36 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .structure( + StructType::new([ + ("a".into(), SchemaField::new(schema.infer_as_nested::())), + ( + "b".into(), + { + let mut field = SchemaField::new( + schema.infer_as_nested::>(), + ); + field.nullable = true; + field + }, + ), + ( + "c".into(), + SchemaField::new(schema.infer_as_nested::>()), + ), + ( + "d".into(), + SchemaField::new( + schema.infer_as_nested::>(), + ), + ), + ( + "e".into(), + SchemaField::new(schema.infer_as_nested::>()), + ), + ]), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_standard.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_standard.snap new file mode 100644 index 00000000..de764b89 --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__named_struct__supports_standard.snap @@ -0,0 +1,74 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .structure( + StructType::new([ + ("a".into(), SchemaField::new(schema.infer::())), + ( + "b".into(), + { + let mut field = SchemaField::new(schema.infer::()); + field.comment = Some("Some docs.".into()); + field + }, + ), + ( + "c".into(), + { + let mut field = SchemaField::new( + schema + .infer_with_default::< + String, + >(LiteralValue::String("abc".into())), + ); + field.optional = true; + field + }, + ), + ( + "d".into(), + { + let mut field = SchemaField::new( + schema.infer_with_default::(LiteralValue::Int(10)), + ); + field.optional = true; + field + }, + ), + ( + "e".into(), + { + let mut field = SchemaField::new( + schema.infer_with_default::(LiteralValue::F64(1.5)), + ); + field.optional = true; + field + }, + ), + ( + "f".into(), + { + let mut field = SchemaField::new( + schema.infer_with_default::(LiteralValue::Bool(true)), + ); + field.optional = true; + field + }, + ), + ( + "g".into(), + { + let mut field = SchemaField::new( + schema.infer::>(), + ); + field.nullable = true; + field + }, + ), + ("h".into(), SchemaField::new(schema.infer::>())), + ]), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_adjacent_tag.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_adjacent_tag.snap new file mode 100644 index 00000000..0c311b6e --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_adjacent_tag.snap @@ -0,0 +1,24 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .enumerable( + EnumType::from_schemas( + [ + Schema { + name: Some("A".into()), + ty: Schema::literal_value(LiteralValue::String("A".into())).ty, + ..Default::default() + }, + Schema { + name: Some("B".into()), + ty: Schema::literal_value(LiteralValue::String("B".into())).ty, + ..Default::default() + }, + ], + Some(1usize), + ), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_external.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_external.snap new file mode 100644 index 00000000..46d92a0b --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_external.snap @@ -0,0 +1,34 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .enumerable( + EnumType::from_schemas( + [ + { + let mut item = Schema { + name: Some("A".into()), + ty: Schema::literal_value(LiteralValue::String("A".into())) + .ty, + ..Default::default() + }; + item.description = Some("Docs.".into()); + item + }, + Schema { + name: Some("bb".into()), + ty: Schema::literal_value(LiteralValue::String("bb".into())).ty, + ..Default::default() + }, + Schema { + name: Some("C".into()), + ty: Schema::null().ty, + ..Default::default() + }, + ], + Some(1usize), + ), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_internal_tag.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_internal_tag.snap new file mode 100644 index 00000000..0c311b6e --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_internal_tag.snap @@ -0,0 +1,24 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .enumerable( + EnumType::from_schemas( + [ + Schema { + name: Some("A".into()), + ty: Schema::literal_value(LiteralValue::String("A".into())).ty, + ..Default::default() + }, + Schema { + name: Some("B".into()), + ty: Schema::literal_value(LiteralValue::String("B".into())).ty, + ..Default::default() + }, + ], + Some(1usize), + ), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_untagged.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_untagged.snap new file mode 100644 index 00000000..0c311b6e --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unit_enum__supports_untagged.snap @@ -0,0 +1,24 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .enumerable( + EnumType::from_schemas( + [ + Schema { + name: Some("A".into()), + ty: Schema::literal_value(LiteralValue::String("A".into())).ty, + ..Default::default() + }, + Schema { + name: Some("B".into()), + ty: Schema::literal_value(LiteralValue::String("B".into())).ty, + ..Default::default() + }, + ], + Some(1usize), + ), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_adjacent_tag.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_adjacent_tag.snap new file mode 100644 index 00000000..e9367127 --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_adjacent_tag.snap @@ -0,0 +1,128 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .union( + UnionType::from_schemas( + [ + { + let mut item = Schema::structure( + StructType::new([ + ( + "type".into(), + Schema::literal_value(LiteralValue::String("Text".into())), + ), + ("value".into(), schema.infer::()), + ]), + ); + item.description = Some("Docs.".into()); + item + }, + Schema::structure( + StructType::new([ + ( + "type".into(), + Schema::literal_value(LiteralValue::String("List".into())), + ), + ("value".into(), schema.infer::>()), + ]), + ), + Schema::structure( + StructType::new([ + ( + "type".into(), + Schema::literal_value(LiteralValue::String("Pair".into())), + ), + ( + "value".into(), + Schema::tuple( + TupleType::new([ + schema.infer::(), + schema.infer::(), + ]), + ), + ), + ]), + ), + { + let mut item = Schema::structure( + StructType::new([ + ( + "type".into(), + Schema::literal_value(LiteralValue::String("Inner".into())), + ), + ("value".into(), schema.infer_as_nested::()), + ]), + ); + item.partialize(); + item + }, + { + let mut item = Schema::structure( + StructType::new([ + ( + "type".into(), + Schema::literal_value( + LiteralValue::String("InnerOpt".into()), + ), + ), + ( + "value".into(), + schema.infer_as_nested::>(), + ), + ]), + ); + item.partialize(); + item + }, + { + let mut item = Schema::structure( + StructType::new([ + ( + "type".into(), + Schema::literal_value( + LiteralValue::String("InnerList".into()), + ), + ), + ( + "value".into(), + schema.infer_as_nested::>(), + ), + ]), + ); + item.partialize(); + item + }, + { + let mut item = Schema::structure( + StructType::new([ + ( + "type".into(), + Schema::literal_value( + LiteralValue::String("InnerMap".into()), + ), + ), + ( + "value".into(), + schema.infer_as_nested::>(), + ), + ]), + ); + item.partialize(); + item + }, + Schema::structure( + StructType::new([ + ( + "type".into(), + Schema::literal_value(LiteralValue::String("Unit".into())), + ), + ]), + ), + ], + Some(2usize), + ), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_external.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_external.snap new file mode 100644 index 00000000..c713f849 --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_external.snap @@ -0,0 +1,83 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .union( + UnionType::from_schemas( + [ + { + let mut item = Schema::structure( + StructType::new([("Text".into(), schema.infer::())]), + ); + item.description = Some("Docs.".into()); + item + }, + Schema::structure( + StructType::new([("List".into(), schema.infer::>())]), + ), + Schema::structure( + StructType::new([ + ( + "Pair".into(), + Schema::tuple( + TupleType::new([ + schema.infer::(), + schema.infer::(), + ]), + ), + ), + ]), + ), + { + let mut item = Schema::structure( + StructType::new([ + ("Inner".into(), schema.infer_as_nested::()), + ]), + ); + item.partialize(); + item + }, + { + let mut item = Schema::structure( + StructType::new([ + ( + "InnerOpt".into(), + schema.infer_as_nested::>(), + ), + ]), + ); + item.partialize(); + item + }, + { + let mut item = Schema::structure( + StructType::new([ + ( + "InnerList".into(), + schema.infer_as_nested::>(), + ), + ]), + ); + item.partialize(); + item + }, + { + let mut item = Schema::structure( + StructType::new([ + ( + "InnerMap".into(), + schema.infer_as_nested::>(), + ), + ]), + ); + item.partialize(); + item + }, + Schema::literal_value(LiteralValue::String("Unit".into())), + ], + Some(2usize), + ), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_internal_tag.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_internal_tag.snap new file mode 100644 index 00000000..e0eb53e4 --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_internal_tag.snap @@ -0,0 +1,132 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .union( + UnionType::from_schemas( + [ + { + let mut item = { + let mut item = schema.infer::(); + item.ty + .add_field( + "type", + SchemaField::new( + Schema::literal_value(LiteralValue::String("Text".into())), + ), + ); + item + }; + item.description = Some("Docs.".into()); + item + }, + { + let mut item = schema.infer::>(); + item.ty + .add_field( + "type", + SchemaField::new( + Schema::literal_value(LiteralValue::String("List".into())), + ), + ); + item + }, + { + let mut item = Schema::tuple( + TupleType::new([ + schema.infer::(), + schema.infer::(), + ]), + ); + item.ty + .add_field( + "type", + SchemaField::new( + Schema::literal_value(LiteralValue::String("Pair".into())), + ), + ); + item + }, + { + let mut item = { + let mut item = schema.infer_as_nested::(); + item.ty + .add_field( + "type", + SchemaField::new( + Schema::literal_value(LiteralValue::String("Inner".into())), + ), + ); + item + }; + item.partialize(); + item + }, + { + let mut item = { + let mut item = schema + .infer_as_nested::>(); + item.ty + .add_field( + "type", + SchemaField::new( + Schema::literal_value( + LiteralValue::String("InnerOpt".into()), + ), + ), + ); + item + }; + item.partialize(); + item + }, + { + let mut item = { + let mut item = schema.infer_as_nested::>(); + item.ty + .add_field( + "type", + SchemaField::new( + Schema::literal_value( + LiteralValue::String("InnerList".into()), + ), + ), + ); + item + }; + item.partialize(); + item + }, + { + let mut item = { + let mut item = schema + .infer_as_nested::>(); + item.ty + .add_field( + "type", + SchemaField::new( + Schema::literal_value( + LiteralValue::String("InnerMap".into()), + ), + ), + ); + item + }; + item.partialize(); + item + }, + Schema::structure( + StructType::new([ + ( + "type".into(), + Schema::literal_value(LiteralValue::String("Unit".into())), + ), + ]), + ), + ], + Some(2usize), + ), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_renames_and_exclusions.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_renames_and_exclusions.snap new file mode 100644 index 00000000..3e8371e9 --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_renames_and_exclusions.snap @@ -0,0 +1,23 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .union( + UnionType::from_schemas( + [ + Schema::structure( + StructType::new([("text".into(), schema.infer::())]), + ), + Schema::structure( + StructType::new([("number".into(), schema.infer::())]), + ), + Schema::structure( + StructType::new([("Last".into(), schema.infer::())]), + ), + ], + Some(2usize), + ), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_untagged.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_untagged.snap new file mode 100644 index 00000000..2054873b --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_untagged.snap @@ -0,0 +1,28 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .union( + UnionType::from_schemas( + [ + { + let mut item = schema.infer::(); + item.description = Some("Docs.".into()); + item + }, + schema.infer::>(), + Schema::tuple( + TupleType::new([schema.infer::(), schema.infer::()]), + ), + schema.infer_as_nested::(), + schema.infer_as_nested::>(), + schema.infer_as_nested::>(), + schema.infer_as_nested::>(), + Schema::null(), + ], + Some(2usize), + ), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_variant_untagged.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_variant_untagged.snap new file mode 100644 index 00000000..4e0f69e6 --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_variant_untagged.snap @@ -0,0 +1,27 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .union( + UnionType::from_schemas( + [ + { + let mut item = schema.infer::(); + item.ty + .add_field( + "type", + SchemaField::new( + Schema::literal_value(LiteralValue::String("Text".into())), + ), + ); + item + }, + schema.infer::(), + schema.infer::(), + ], + None, + ), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_empty.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_empty.snap new file mode 100644 index 00000000..24a76b4d --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_empty.snap @@ -0,0 +1,7 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.tuple(TupleType::new([])) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_many_values.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_many_values.snap new file mode 100644 index 00000000..02f9261d --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_many_values.snap @@ -0,0 +1,20 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema + .tuple( + TupleType::new([ + schema.infer::(), + { + let mut schema = schema.infer::>(); + schema.set_description("Item docs."); + schema + }, + schema.infer_as_nested::(), + schema.infer_as_nested::>(), + schema.infer_as_nested::>(), + ]), + ) +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_single_nested_collection.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_single_nested_collection.snap new file mode 100644 index 00000000..d556266b --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_single_nested_collection.snap @@ -0,0 +1,8 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + let mut schema = schema.infer_as_nested::>(); + schema +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_single_nested_value.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_single_nested_value.snap new file mode 100644 index 00000000..4dee5880 --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_single_nested_value.snap @@ -0,0 +1,8 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + let mut schema = schema.infer_as_nested::(); + schema +} diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_single_value.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_single_value.snap new file mode 100644 index 00000000..02e42f3a --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_struct__supports_single_value.snap @@ -0,0 +1,9 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty_body(container.impl_schematic_type()) +--- +fn build_schema(mut schema: SchemaBuilder) -> Schema { + let mut schema = schema.infer::(); + schema.set_description("Docs."); + schema +} 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 new file mode 100644 index 00000000..7684859b --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schematic__named_struct.snap @@ -0,0 +1,30 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty(container.impl_schematic()) +--- +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Renamed".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema.set_deprecated("Use something else."); + schema.set_description("Container docs."); + schema + .structure( + StructType::new([("a".into(), SchemaField::new(schema.infer::()))]), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for PartialExample { + fn schema_name() -> Option { + Some("PartialExample".into()) + } + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = Example::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 new file mode 100644 index 00000000..74e32506 --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schematic__unit_enum.snap @@ -0,0 +1,44 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty(container.impl_schematic()) +--- +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .enumerable( + EnumType::from_schemas( + [ + Schema { + name: Some("A".into()), + ty: Schema::literal_value(LiteralValue::String("A".into())) + .ty, + ..Default::default() + }, + Schema { + name: Some("B".into()), + ty: Schema::literal_value(LiteralValue::String("B".into())) + .ty, + ..Default::default() + }, + ], + None, + ), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for PartialExample { + fn schema_name() -> Option { + Some("PartialExample".into()) + } + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = Example::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 new file mode 100644 index 00000000..d7b64a51 --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schematic__unnamed_enum.snap @@ -0,0 +1,35 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty(container.impl_schematic()) +--- +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Renamed".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .union( + UnionType::from_schemas( + [ + Schema::structure( + StructType::new([("A".into(), schema.infer::())]), + ), + ], + None, + ), + ) + } +} +#[automatically_derived] +impl schematic::Schematic for PartialExample { + fn schema_name() -> Option { + Some("PartialExample".into()) + } + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = Example::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 new file mode 100644 index 00000000..adf61f56 --- /dev/null +++ b/crates/core/tests/snapshots/container_schema_test__schematic__unnamed_struct.snap @@ -0,0 +1,25 @@ +--- +source: crates/core/tests/container_schema_test.rs +expression: pretty(container.impl_schematic()) +--- +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + 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 { + Some("PartialExample".into()) + } + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = Example::build_schema(schema); + schematic::internal::partialize_schema(&mut schema, true); + schema + } +} 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 924c9922..0d1a1daf 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 @@ -3,6 +3,26 @@ source: crates/core/tests/container_type_test.rs expression: pretty(container.impl_schematic()) --- #[automatically_derived] -impl schematic::Schematic for Example {} +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .structure( + StructType::new([("a".into(), SchemaField::new(schema.infer::()))]), + ) + } +} #[automatically_derived] -impl schematic::Schematic for PartialExample {} +impl schematic::Schematic for PartialExample { + fn schema_name() -> Option { + Some("PartialExample".into()) + } + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = Example::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 c15c5178..cbf42529 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 @@ -180,6 +180,75 @@ impl Default for Example { } } #[automatically_derived] -impl schematic::Schematic for Example {} +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema.set_description("Docs."); + schema + .structure( + StructType::new([ + ( + "enabled".into(), + { + let mut field = SchemaField::new( + schema.infer_with_default::(LiteralValue::Bool(true)), + ); + field.env_var = Some("ENABLED".into()); + field.optional = true; + field + }, + ), + ( + "name".into(), + { + let mut field = SchemaField::new( + schema + .infer_with_default::< + String, + >(LiteralValue::String("abc".into())), + ); + field.optional = true; + field + }, + ), + ( + "port".into(), + { + let mut field = SchemaField::new( + schema.infer::>(), + ); + field.nullable = true; + field + }, + ), + ( + "nested".into(), + SchemaField::new(schema.infer_as_nested::()), + ), + ( + "list".into(), + SchemaField::new(schema.infer_as_nested::>()), + ), + ("extends".into(), SchemaField::new(schema.infer::>())), + ( + "map".into(), + SchemaField::new(schema.infer::>()), + ), + ]), + ) + } +} #[automatically_derived] -impl schematic::Schematic for PartialExample {} +impl schematic::Schematic for PartialExample { + fn schema_name() -> Option { + Some("PartialExample".into()) + } + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = Example::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 b505bdcc..374c2002 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 @@ -60,6 +60,48 @@ impl Default for Example { } } #[automatically_derived] -impl schematic::Schematic for Example {} +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .enumerable( + EnumType::from_schemas( + [ + Schema { + name: Some("A".into()), + ty: Schema::literal_value(LiteralValue::String("A".into())) + .ty, + ..Default::default() + }, + Schema { + name: Some("B".into()), + ty: Schema::literal_value(LiteralValue::String("B".into())) + .ty, + ..Default::default() + }, + Schema { + name: Some("C".into()), + ty: Schema::literal_value(LiteralValue::String("C".into())) + .ty, + ..Default::default() + }, + ], + Some(1usize), + ), + ) + } +} #[automatically_derived] -impl schematic::Schematic for PartialExample {} +impl schematic::Schematic for PartialExample { + fn schema_name() -> Option { + Some("PartialExample".into()) + } + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = Example::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 f21d9d87..4364c995 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 @@ -194,6 +194,34 @@ impl Default for Example { } } #[automatically_derived] -impl schematic::Schematic for Example {} +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .union( + UnionType::from_schemas( + [ + schema.infer::(), + schema.infer::(), + schema.infer_as_nested::(), + schema.infer_as_nested::>(), + ], + Some(0usize), + ), + ) + } +} #[automatically_derived] -impl schematic::Schematic for PartialExample {} +impl schematic::Schematic for PartialExample { + fn schema_name() -> Option { + Some("PartialExample".into()) + } + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = Example::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 fbeccb55..6cd0d463 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 @@ -108,6 +108,29 @@ impl Default for Example { } } #[automatically_derived] -impl schematic::Schematic for Example {} +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .tuple( + TupleType::new([ + schema.infer_with_default::(LiteralValue::Int(10)), + schema.infer_as_nested::>(), + ]), + ) + } +} #[automatically_derived] -impl schematic::Schematic for PartialExample {} +impl schematic::Schematic for PartialExample { + fn schema_name() -> Option { + Some("PartialExample".into()) + } + fn build_schema(schema: schematic::SchemaBuilder) -> schematic::Schema { + let mut schema = Example::build_schema(schema); + schematic::internal::partialize_schema(&mut schema, true); + schema + } +} From 702c03741be4185df219175771d4023455176adf Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Sun, 16 Aug 2026 20:29:20 -0700 Subject: [PATCH 09/18] internal: Improve types crate. (#195) --- AGENTS.md | 266 +++ CHANGELOG.md | 79 + CLAUDE.md | 1 + book/src/schema/array.md | 9 +- book/src/schema/integer.md | 4 +- book/src/schema/string.md | 7 +- book/src/schema/struct.md | 7 + book/src/schema/types.md | 5 + crates/schematic/src/internal/mod.rs | 8 + crates/schematic/src/schema/generator.rs | 26 + crates/schematic/src/schema/renderer.rs | 2 +- .../src/schema/renderers/json_schema.rs | 25 +- .../src/schema/renderers/jsonc_template.rs | 2 +- .../src/schema/renderers/pkl_template.rs | 2 +- .../src/schema/renderers/template.rs | 10 +- .../src/schema/renderers/toml_template.rs | 2 +- .../src/schema/renderers/typescript.rs | 4 +- .../src/schema/renderers/yaml_template.rs | 2 +- crates/schematic/tests/generator_test.rs | 63 + crates/schematic/tests/partialize_test.rs | 60 +- ...de_sources_test__generates_typescript.snap | 3 +- .../defaults_test__generates_json_schema.snap | 1 + .../defaults_test__generates_typescript.snap | 1 + .../env_test__generates_typescript.snap | 1 - .../snapshots/partialize_test__compounds.snap | 13 +- .../partialize_test__enum_adjacent.snap | 109 +- .../partialize_test__enum_external.snap | 1 - .../partialize_test__enum_internal.snap | 3 - .../snapshots/partialize_test__enums.snap | 1601 ++++++++--------- .../snapshots/partialize_test__nested.snap | 3 +- .../partialize_test__nested_list.snap | 9 +- .../partialize_test__nested_map.snap | 23 +- .../partialize_test__primitives.snap | 307 ++-- crates/types/Cargo.toml | 2 +- crates/types/src/arrays.rs | 42 +- crates/types/src/enums.rs | 53 +- crates/types/src/externals.rs | 7 + crates/types/src/lib.rs | 8 +- crates/types/src/literals.rs | 30 +- crates/types/src/numbers.rs | 42 +- crates/types/src/schema.rs | 119 +- crates/types/src/schema_builder.rs | 31 +- crates/types/src/schema_type.rs | 73 +- crates/types/src/strings.rs | 9 +- crates/types/src/structs.rs | 57 +- crates/types/tests/builder_test.rs | 51 +- crates/types/tests/schema_test.rs | 1091 +++++++++++ .../builder_test__map_shaped_scalars-2.snap | 28 + .../builder_test__map_shaped_scalars.snap | 28 + .../snapshots/builder_test__primitives-6.snap | 1 + .../snapshots/builder_test__strings-7.snap | 2 +- .../snapshots/builder_test__structs.snap | 10 +- 52 files changed, 3105 insertions(+), 1238 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 crates/types/tests/schema_test.rs create mode 100644 crates/types/tests/snapshots/builder_test__map_shaped_scalars-2.snap create mode 100644 crates/types/tests/snapshots/builder_test__map_shaped_scalars.snap diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..8952c454 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,266 @@ +# AGENTS.md + +Orientation for AI/agent sessions working in this repo. Read this before touching `crates/core` or +`crates/macros`. + +## What this project is + +Schematic is a layered configuration library. Users write a plain Rust struct/enum, add +`#[derive(Config)]`, and get back: + +- A **partial type** (`PartialExample`) where every setting is optional, used for parsing and + merging config layers (files, env vars, extends chains). +- A **full type** (`Example`) where every setting is populated, produced once all layers are merged + and finalized. + +Everything hard about this codebase comes from that split. If you understand Config↔Partial, the +rest follows. + +## Workspace map + +| Crate | Package | Role | +| -------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `crates/schematic` | `schematic` | The user-facing runtime: `Config`/`PartialConfig` traits, loader, merge/validate/env helpers, schema renderers. | +| `crates/types` | `schematic_types` | `Schema`, `SchemaBuilder`, `SchemaType`, the `Schematic` trait. No macro code. | +| `crates/macros` | `schematic_macros` | **The production derive.** Currently what ships. | +| `crates/core` | `schematic_core` | **The rewrite in progress.** Where new work goes. | +| `crates/macros-next` | `schematic_macros_next` | Thin proc-macro shell intended to consume `core`. Stale scaffolding — its `config` fn references modules that don't exist. | +| `crates/test-app` | `test_app` | Manual smoke-test binary using the production derive. | + +### Migration status — read this first + +`crates/macros` is production. `crates/core` is a piece-by-piece rewrite of it. **Nothing consumes +`core` in a real `#[derive(Config)]` yet** — `macros-next` isn't wired up. So changes in `core` are +only exercised by its own snapshot tests unless you deliberately compile the generated output (see +[Testing](#testing)). + +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 +behavior and the shapes users depend on. + +## The Config ↔ Partial model + +For `struct Example { name: String, port: Option }` the derive emits: + +```rust +// The partial: every setting optional, used for parsing + merging +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(default, deny_unknown_fields)] +struct PartialExample { + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + port: Option, // already optional — NOT double-wrapped +} + +impl PartialConfig for PartialExample { /* default_values, env_values, extends_from, finalize, merge, validate */ } +impl Config for Example { type Partial = PartialExample; /* from_partial, settings */ } +impl Default for Example { /* via from_partial(default_partial()) */ } +impl Schematic for Example { /* schema_name, build_schema */ } +impl Schematic for PartialExample { /* delegates + partialize_schema */ } +``` + +Runtime flow: each source is parsed into a partial layer, layers are combined with `merge`, then +`finalize` re-merges that result **over** `default_values` and **under** `env_values` (so env wins), +runs per-setting transforms and nested finalization, and finally `validate` runs before +`from_partial` produces the full type. Precedence, lowest to highest: defaults → files → env. + +Key invariants: + +- A setting's **outer `Option` doubles as the partial's `Option`** — `Option` becomes + `Option`, not `Option>`. +- **Enum variant payloads get no synthetic `Option`** — `A(bool)` stays `A(bool)`. +- **Wrapper types (`Box`/`Arc`/`Rc`) are stripped from the partial** and re-applied in + `from_partial`. This is why partials don't need serde's `rc` feature. Exception: kept when the + inner type is unsized (`Box`, `Arc`, `Box<[T]>`). + +## The derive pipeline in `core` + +Entry point is `Container::from(DeriveInput)` in `crates/core/src/container.rs`. + +``` +Container (the struct/enum) container.rs +├─ ContainerInner::NamedStruct { fields: Vec } +├─ ContainerInner::UnnamedStruct { fields: Vec } // tuple struct +├─ ContainerInner::UnnamedEnum { variants: Vec } +└─ ContainerInner::UnitEnum { variants: Vec } // all variants are units + +Field (a struct setting) field.rs → FieldValue → Value +Variant (an enum variant) variant.rs → VariantValue → Value +Value (type analysis + walkers) value.rs +``` + +`ToTokens for Container` emits the whole derive, in this order: + +```rust +impl_partial_type() // the partial struct/enum declaration +impl_partial_type_default() // manual Default for partial enums +impl_partial_type_deserialize() // try-each-variant Deserialize for untagged enums +impl_partial() // impl PartialConfig +impl_full() // impl Config + impl Default +impl_schematic() // impl Schematic for both types +``` + +Each piece is separately callable, which is what the snapshot tests do. + +### Naming conventions + +- `impl_partial_*` → code for the partial type / `PartialConfig`. +- `impl_full_*` → code for the full type / `Config`. +- `get_*` → derive-time values (names, keys, attributes), not codegen. +- Most codegen methods return `ImplResult { value, no_value, requires_internal }`. `no_value` means + "emit nothing" (the caller skips the row/statement); `requires_internal` means the generated + method needs `use schematic::internal::*;`, which the container adds via + `ImplResult::impl_use_internal`. + +### `Value` and the layer system + +`Value::new` decomposes a type into ordered `layers` plus an inner type: + +``` +Arc>> → layers: [Arc, Vec, Option], inner: NestedConfig +``` + +- `layers` — every layer, full fidelity. Used to _reconstruct_ the final type. +- `get_partial_layers()` — layers minus wrappers. This is the partial's view, and what + merge/validate/finalize/default operate on. +- `is_outer_option_wrapped()` — computed on the _partial_ view, so `Box>` correctly + collapses to one `Option`. +- `strip_wrappers` — false when a wrapper holds an unsized type. + +Codegen walks these layers to build nested expressions. Each walker composes inside-out, so position +is respected without special-casing: + +| Walker | Direction | +| ------------------------------ | -------------------------------------- | +| `impl_full_from_partial_value` | Partial → full. _Constructs_ wrappers. | +| `impl_partial_finalize_nested` | Within the partial. Skips wrappers. | +| `impl_partial_merge_nested` | Within the partial. Skips wrappers. | +| `impl_partial_validate_nested` | Within the partial. Skips wrappers. | + +`Vec>` wraps each item; `Arc>` wraps the whole loop — both fall out of the +same code. + +## Testing + +### The critical caveat + +**Core's snapshot tests never compile the generated code.** Each test calls one codegen method in +isolation and snapshots the pretty-printed tokens. Type errors, missing trait bounds, and +cross-method inconsistencies are invisible to them. Several real bugs in this codebase sat in green +snapshots for exactly this reason. + +Snapshot tests (starbase_sandbox/insta): + +```bash +cargo test -p schematic_core # run +INSTA_UPDATE=always cargo test -p schematic_core # regenerate snapshots +``` + +Always **read** regenerated snapshots. A fresh snapshot captures whatever the code did, correct or +not — it is not verification on its own. + +### Verifying generated code actually compiles + +For any codegen change, compile the output against the real runtime. The recipe: + +1. Write a temporary test in `crates/core/tests/` that builds `Container::from(...)` for your + fixtures, prints `container.to_token_stream()` through `pretty()`, and brackets the output with + markers. +2. Pipe that into a scratch crate's `src/lib.rs`, alongside hand-written base types (the derive only + emits impls, not the base struct) and any helper fns the fixtures reference. +3. Point the scratch crate at `crates/schematic` by path, copy the workspace `Cargo.lock`, and set + `CARGO_TARGET_DIR` to the workspace `target` so deps are cached. +4. `cargo check`, then add runtime assertions and `cargo test -- --test-threads=1` (env-var tests + share a process). +5. Delete the temporary test file when done. + +This is the only thing that catches type errors, and it has repeatedly found bugs that snapshots +could not. + +### Other suites + +```bash +cargo test -p schematic # runtime + old-derive integration tests +cargo test --workspace # what CI runs +cargo clippy --workspace --all-targets +cargo fmt --all --check +``` + +Known pre-existing clippy warnings (not yours): a `count` loop counter in `core/src/variant.rs` and +`macros/src/config/variant.rs`, and a collapsible `if` in `macros/src/utils.rs` and +`schematic/src/schema/renderers/template.rs`. + +Also run `cargo test -p schematic_types` on its own. `cargo test --workspace` unifies features across +crates, so a crate whose own feature list is incomplete still compiles there — that hid a broken +`serde` feature in `types` until it was audited. + +Also verify with **no features** (`cargo build -p schematic_core`) — much of the codegen is behind +`env`/`extends`/`schema`/`validate` cfgs, and dev-deps enable them all, so feature-gated mistakes +only show in a bare build. + +## Decided semantics — do not "fix" these + +These were deliberated and settled. If something looks wrong, it probably isn't. + +- **Env keys.** Explicit `#[setting(env = "KEY")]` is absolute — a prefix never applies to it. Only + keys _derived_ from a setting name get the prefix, and only when the container declares + `env_prefix`. Deriving keys for every container would force `FromStr` on every setting, which + breaks `Duration`, tuples, etc. A parent's `#[setting(nested, env_prefix)]` therefore only + overrides a child that declares one. `settings()` reports explicit keys only. +- **Nested collections replace by default** on merge; bare nested configs merge recursively. Supply + `merge` to change it. +- **Nested tuple variants support multiple values**, each position handled per its own shape (merge, + replace, or manager-wrapped). +- **`Config::default_partial` panics** on default-value errors. `Default::default()` can't return a + `Result`, and silently returning wrong values is worse. +- **Validation error paths use serde names** (renames honored) for fields and variants; variants use + `PathSegment::Variant` + index, rendering `Many[0].inner`. +- **Derive-time panics for wrong attribute usage are intentional.** The maintainer wants loud + failure over silent no-ops. `#[setting(nested)]` on a primitive panics. + +## Gotchas + +- `#[setting(...)]` and `#[serde(...)]` both feed the derive. Setting attrs take precedence; aliases + from both are merged. +- Unit enums must stay externally tagged — `#[serde(untagged)]` on a unit-only enum makes variants + deserializable only from `null`. +- `SchemaField` metadata is derive-time only; `partialize_schema` at runtime is what turns a full + schema into a partial one. +- `StructType.partial` means "refers to a partial config type", **not** "has been partialized". + `Schema::nullify()` moves a schema's name into the union variant it creates — surprising when + asserting on partial schemas. +- `SchemaType` is internally tagged, so any new variant must carry a map-shaped payload. This is why + `Reference { name, partial }` is a struct variant and not a newtype — serde refuses to internally + tag a newtype wrapping a bare string, which silently broke every cyclic schema. +- A recursive config resolves its cycle to a `Reference`, and that reference has to be renamed + alongside the type it points at. `Schema::partialize` sets `Reference.partial`, and + `partialize_schema` reads it to apply the `Partial` prefix — without both halves the partial + schema `$ref`s a type that was never rendered. +- `EnumType.values` is a *derived subset* of `EnumType.variants` — variants with a non-literal schema + (`#[setting(null)]`) contribute no value, so the two differ in length. `default_index` indexes + `variants`; always read it back through `EnumType::get_default`, never `values[index]`. +- `Schema` no longer derefs to `SchemaType` — the accessors it needs (`get_default`, `is_null`, + `add_field`, …) are forwarded explicitly. Reach for `schema.ty` when you want the type itself. +- `schema_name` must be unique per type; `SchemaGenerator::add` panics on a collision because schemas + are keyed by name and the loser would silently vanish behind the winner's `$ref`s. Adding the same + type twice is fine, as is a recursive type registering itself at several depths. +- Before adding a `Schematic` impl, serialize the type and look at the output. A schema describes + what serde accepts, not what the type looks like — `Duration` and `SystemTime` spent a long time + modelled as strings while encoding as `{ secs, nanos }`, which made the generated JSON Schema + reject valid config. `OsString` is why there is still no impl for it: it encodes as + `{"Unix": [bytes]}`, not a string. +- `StructType.fields` is an `IndexMap` so the *model* keeps declaration order, but renderers iterate + `StructType::sorted_fields()` so *output* stays alphabetical. Both halves are deliberate: the model + preserves what the user wrote, the generated file stays stable no matter how the type is ordered. + A new renderer that iterates `fields` directly will silently reintroduce declaration-ordered output. +- Internally-tagged enums with tuple variants don't compile (serde rejects them). Core doesn't + currently catch this at derive time. +- The maintainer's zsh wraps `git` in a `scmpuff` function that breaks in non-interactive shells. + Use `command git`. + +## Docs + +User-facing documentation lives in `book/src/` — `config/index.md`, `config/partial.md`, +`config/nested.md`, `config/struct/*.md` (default, env, extend, merge, transform, validate), +`config/enum/*.md`. When changing user-visible behavior, check whether the book states otherwise. diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e09c469..a2c37aa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,45 @@ #### 💥 Breaking +##### Config + - Removed `#[config(serde(...))]` on containers. Use `#[serde(...)]` instead. +##### Schema + +- Changed `SchemaType::Reference` from a newtype into a struct variant. Serde is unable to + internally tag a newtype whose value isn't a map, so any schema containing a reference (which is + how cycles are represented) previously failed to serialize. + + ```rust + // Before + SchemaType::Reference("Name".into()) + + // After + SchemaType::Reference { name: "Name".into(), partial: false } + // Or + Schema::reference("Name") + ``` + +- Changed `StructType.fields` from a `BTreeMap` to an `IndexMap`, so the schema preserves the order + fields were declared in. Generators still render alphabetically, via + `StructType::sorted_fields()`. +- Changed `ArrayType.contains` from `Option` to `Option>`. It's now the JSON + Schema `contains` subschema, and applies alongside `items_type` instead of reinterpreting it. +- Changed `SchemaType::add_field` and `SchemaType::set_default` to return a `bool` indicating + whether the operation applied. They previously did nothing when the type couldn't hold the + value. +- Removed `Deref`/`DerefMut` for `Schema` and `SchemaBuilder`. `Schema` now forwards the + `SchemaType` accessors directly (`get_default`, `set_default`, `add_field`, `is_null`, + `is_nullable`, `is_reference`, `is_struct`); reach for `schema.ty` for anything else. +- Removed `ArrayType.max_contains` and `ArrayType.min_contains`, which no renderer could emit. +- Removed `FloatType.name`, which was never rendered and duplicated `Schema.name`. +- Removed `LiteralType.format`, which was never rendered. +- Updated `Duration` and `SystemTime` to model as structs instead of strings. Serde encodes them as + `{ secs, nanos }` and `{ secs_since_epoch, nanos_since_epoch }`, so the previous string schemas + rejected valid documents. +- Updated `Schema.nullable` to serialize when true, instead of always being skipped. + #### 🚀 Updates ##### Config @@ -31,6 +68,48 @@ - Added support for `skip_deserializing_if` and `skip_serializing_if` on fields. - Updated `alias` to support multiple aliases: `#[serde(alias = "alias1", alias = "alias2")]` +##### Schema + +- Added `Schema::reference()`, to go with the constructors for the other types. +- Added `EnumType::get_default()` and `EnumType::set_default()`, which resolve `default_index` + against whichever list it indexes. +- Added `StructType::sorted_fields()`, which renderers use to keep generated output alphabetical. +- Added `Schematic` implementations for `[T]`, `VecDeque`, `LinkedList`, `BinaryHeap`, `IpAddr`, + `SocketAddr`, `SocketAddrV4`, `SocketAddrV6`, `Range`, and `RangeInclusive`. +- Added `Schematic` implementations for the `NonZero` integers. The unsigned ones carry a `min` of + 1; signed ones can't express "not zero" as a bound, so they stay unconstrained. +- Added a `Schematic` implementation for `ron::Value`, behind the existing `serde_ron` feature, + which until now enabled a dependency but no implementations. +- Updated `Box`, `Rc`, and `Arc` to accept unsized inner types, so `Box`, `Arc` and + `Box<[T]>` can now build a schema. This is required for partial configs, which keep the wrapper + when the inner type is unsized. +- Updated `SchemaBuilder::generate`, `build_root`, `infer`, `infer_as_nested`, and + `infer_with_default` to accept unsized types. +- Updated `SchemaGenerator::add` to panic when two types claim the same `schema_name`. Schemas are + keyed by name, so one would previously be dropped and every reference to it resolved to the other. + +#### 🐞 Fixes + +##### Schema + +- Fixed the `serde` feature not enabling `indexmap/serde`, which made `schematic_types` fail to + compile on its own. +- Fixed `SchemaType::set_default` doing nothing for unions and enums, which silently dropped the + default of every `Option` wrapped setting from the generated schema. +- Fixed `EnumType` resolving `default_index` against `values` instead of `variants`. The two differ + in length when a variant carries no literal value, so the default could be lost or point + elsewhere. +- Fixed `EnumType::from_schemas` panicking without an explanation when a variant schema had no name. +- Fixed `Schema::partialize` not recursing into tuples, and not marking references, which made a + recursive nested config render a `$ref` to a type that was never generated. +- Fixed `Schema::nullify` not detecting an already nullable type, which double wrapped named unions + and left the method non-idempotent. +- Fixed `Schema.nullable` never being set when inferring an `Option`, and never surviving a + serialization round trip. +- Fixed `Schema::get_nonnull_schema` not resolving nested unions, so a union of nothing but nulls + was returned as if it were non-null. +- Fixed `LiteralValue` comparing unequal to itself when holding a `NaN` float. + #### ⚙️ Internal - Updated `syn` to v3. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/book/src/schema/array.md b/book/src/schema/array.md index 0552ee09..d79c4937 100644 --- a/book/src/schema/array.md +++ b/book/src/schema/array.md @@ -24,7 +24,8 @@ method. schema.array(ArrayType::new(schema.infer::())); ``` -> Automatically implemented for `Vec`, `BTreeSet`, `HashSet`, `[T; N]`, and `&[T]`. +> Automatically implemented for `Vec`, `VecDeque`, `LinkedList`, `BinaryHeap`, `BTreeSet`, +> `HashSet`, `[T; N]`, `[T]`, and `&[T]`. ## Settings @@ -33,13 +34,13 @@ The following fields can be passed to [`ArrayType`][array], which are then fed i ### Contains -The `contains` field can be enabled to indicate that the array must contain at least one item of the -type defined by `items_type`, instead of all items. +The `contains` field takes a schema that at least one item must match. It applies alongside +`items_type`, which constrains every item, rather than replacing it. ```rust ArrayType { // ... - contains: Some(true), + contains: Some(Box::new(Schema::string(StringType::default()))), } ``` diff --git a/book/src/schema/integer.md b/book/src/schema/integer.md index 8a830e77..cf9ded32 100644 --- a/book/src/schema/integer.md +++ b/book/src/schema/integer.md @@ -23,7 +23,9 @@ method. schema.integer(IntegerType::new_kind(IntegerKind::U32)); ``` -> Automatically implemented for `usize`-`u128` and `isize`-`i128`. +> Automatically implemented for `usize`-`u128`, `isize`-`i128`, and their `NonZero` counterparts. +> Unsigned `NonZero` types carry a `min` of 1; signed ones cannot express "not zero" as a bound, so +> they stay unconstrained. ## Default value diff --git a/book/src/schema/string.md b/book/src/schema/string.md index 1f08e5da..d48824c4 100644 --- a/book/src/schema/string.md +++ b/book/src/schema/string.md @@ -12,8 +12,11 @@ impl Schematic for T { } ``` -> Automatically implemented for `char`, `str`, `String`, `Path`, `PathBuf`, `Ipv4Addr`, `Ipv6Addr`, -> `SystemTime`, and `Duration`. +> Automatically implemented for `char`, `str`, `String`, `Path`, `PathBuf`, `IpAddr`, `Ipv4Addr`, +> `Ipv6Addr`, `SocketAddr`, `SocketAddrV4`, and `SocketAddrV6`. + +> `SystemTime` and `Duration` are _not_ strings. Serde encodes them as maps — `{ secs, nanos }` and +> `{ secs_since_epoch, nanos_since_epoch }` — so they model as [structs](./struct.md). ## Default value diff --git a/book/src/schema/struct.md b/book/src/schema/struct.md index cfff9c61..73048685 100644 --- a/book/src/schema/struct.md +++ b/book/src/schema/struct.md @@ -59,6 +59,13 @@ schema.structure(StructType::new([ ])); ``` +> Automatically implemented for `Duration`, `SystemTime`, `Range`, and `RangeInclusive`. These look +> like scalars but serde encodes them as maps, so the schema describes the map. + +Fields keep the order they were inserted in, which is the declaration order when the schema comes +from a derive. [Generators](./generator/index.md) sort them by name when rendering, so generated +output stays alphabetical regardless of how the source type is written. + ## Settings The following fields can be passed to [`StructType`][struct], which are then fed into the diff --git a/book/src/schema/types.md b/book/src/schema/types.md index 163d5123..77d799fc 100644 --- a/book/src/schema/types.md +++ b/book/src/schema/types.md @@ -38,6 +38,11 @@ impl Schematic for T { > This method is optional, but is encouraged for non-primitive types. It will associate references > between types, and avoid circular references. +Names must be unique across every type added to a generator, since schemas are keyed by name alone. +Adding two different types under the same name panics rather than silently rendering one of them. +This is worth keeping in mind for generic types — a fixed name on `Wrapper` collides across every +`T`, so build the name from the parameter instead. + ## Inferring schemas When building a schema, you'll almost always need to reference schemas from other types that diff --git a/crates/schematic/src/internal/mod.rs b/crates/schematic/src/internal/mod.rs index c24031a9..1e6a1288 100644 --- a/crates/schematic/src/internal/mod.rs +++ b/crates/schematic/src/internal/mod.rs @@ -137,6 +137,14 @@ pub fn partialize_schema(schema: &mut Schema, force_partial: bool) { partialize_schema(variant, false); } } + // A cycle within a nested config resolves to a reference, which must + // follow the type it points at, otherwise it names a type that was + // never rendered. + SchemaType::Reference { name, partial } + if (*partial || force_partial) && !name.starts_with("Partial") => + { + *name = format!("Partial{name}"); + } _ => {} }; } diff --git a/crates/schematic/src/schema/generator.rs b/crates/schematic/src/schema/generator.rs index 2bc3cf5c..f075f9c3 100644 --- a/crates/schematic/src/schema/generator.rs +++ b/crates/schematic/src/schema/generator.rs @@ -2,6 +2,7 @@ use super::SchemaRenderer; use indexmap::IndexMap; use miette::IntoDiagnostic; use schematic_types::*; +use std::collections::HashMap; use std::fs; use std::path::Path; @@ -10,12 +11,37 @@ use std::path::Path; #[derive(Debug, Default)] pub struct SchemaGenerator { pub schemas: IndexMap, + + // Which Rust type claimed each schema name. Schemas are keyed by name + // alone, so two distinct types answering `schema_name` with the same + // string would silently collapse into one, and every reference to that + // name would resolve to whichever was added first. + sources: HashMap, } impl SchemaGenerator { /// Add a [`Schema`] to be rendered, derived from the provided [`Schematic`]. + /// + /// # Panics + /// + /// If another type has already been added under the same schema name. + /// Rendering both is impossible, and picking one silently would emit a + /// document whose references point at the wrong type. pub fn add(&mut self) { let schema = SchemaBuilder::build_root::(); + + if let Some(name) = T::schema_name() { + let source = std::any::type_name::(); + + if let Some(previous) = self.sources.insert(name.clone(), source) + && previous != source + { + panic!( + "A schema named `{name}` has already been added by `{previous}`, and `{source}` cannot reuse it. Give one of them a distinct `schema_name`." + ); + } + } + self.add_schema(&schema); } diff --git a/crates/schematic/src/schema/renderer.rs b/crates/schematic/src/schema/renderer.rs index 37589971..c33cba50 100644 --- a/crates/schematic/src/schema/renderer.rs +++ b/crates/schematic/src/schema/renderer.rs @@ -80,7 +80,7 @@ pub trait SchemaRenderer { SchemaType::String(string) => self.render_string(string, schema), SchemaType::Tuple(tuple) => self.render_tuple(tuple, schema), SchemaType::Union(uni) => self.render_union(uni, schema), - SchemaType::Reference(name) => self.render_reference(name, schema), + SchemaType::Reference { name, .. } => self.render_reference(name, schema), } } diff --git a/crates/schematic/src/schema/renderers/json_schema.rs b/crates/schematic/src/schema/renderers/json_schema.rs index 60c995bb..42037386 100644 --- a/crates/schematic/src/schema/renderers/json_schema.rs +++ b/crates/schematic/src/schema/renderers/json_schema.rs @@ -204,24 +204,21 @@ impl SchemaRenderer for JsonSchemaRenderer { } fn render_array(&mut self, array: &ArrayType, schema: &Schema) -> RenderResult { - let use_contains = array.contains.is_some_and(|v| v); + // `contains` constrains the array as a whole, and `items` every entry, + // so the two are independent and both are rendered when present. + let contains = match &array.contains { + Some(inner) => Some(Box::new(self.render_schema(inner)?)), + None => None, + }; let data = SchemaObject { metadata: Some(Box::new(self.create_metadata_from_schema(schema))), instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))), array: Some(Box::new(ArrayValidation { - contains: if use_contains { - Some(Box::new(self.render_schema(&array.items_type)?)) - } else { - None - }, - items: if use_contains { - None - } else { - Some(SingleOrVec::Single(Box::new( - self.render_schema(&array.items_type)?, - ))) - }, + contains, + items: Some(SingleOrVec::Single(Box::new( + self.render_schema(&array.items_type)?, + ))), max_items: array.max_length.map(|i| i as u32), min_items: array.min_length.map(|i| i as u32), unique_items: array.unique, @@ -441,7 +438,7 @@ impl SchemaRenderer for JsonSchemaRenderer { let mut additional_properties = Some(Box::new(JsonSchema::Bool(false))); let exclude_aliases = self.options.exclude_aliases; - for (name, field) in &structure.fields { + for (name, field) in structure.sorted_fields() { if field.hidden { continue; } diff --git a/crates/schematic/src/schema/renderers/jsonc_template.rs b/crates/schematic/src/schema/renderers/jsonc_template.rs index b85ddde4..55556ace 100644 --- a/crates/schematic/src/schema/renderers/jsonc_template.rs +++ b/crates/schematic/src/schema/renderers/jsonc_template.rs @@ -116,7 +116,7 @@ impl SchemaRenderer for JsoncTemplateRenderer { self.ctx.depth += 1; - for (index, (name, field)) in structure.fields.iter().enumerate() { + for (index, (name, field)) in structure.sorted_fields().into_iter().enumerate() { if field.flatten { continue; } diff --git a/crates/schematic/src/schema/renderers/pkl_template.rs b/crates/schematic/src/schema/renderers/pkl_template.rs index a5e462c8..940d4745 100644 --- a/crates/schematic/src/schema/renderers/pkl_template.rs +++ b/crates/schematic/src/schema/renderers/pkl_template.rs @@ -124,7 +124,7 @@ impl SchemaRenderer for PklTemplateRenderer { self.ctx.depth += 1; - for (name, field) in &structure.fields { + for (name, field) in structure.sorted_fields() { if field.flatten { continue; } diff --git a/crates/schematic/src/schema/renderers/template.rs b/crates/schematic/src/schema/renderers/template.rs index a2e938c5..3d9a70b1 100644 --- a/crates/schematic/src/schema/renderers/template.rs +++ b/crates/schematic/src/schema/renderers/template.rs @@ -78,7 +78,7 @@ pub fn is_nested_type(schema: &SchemaType) -> bool { uni.variants_types .iter() .find(|v| !v.is_null()) - .is_some_and(|v| is_nested_type(v)) + .is_some_and(|v| is_nested_type(&v.ty)) } else { false } @@ -230,7 +230,7 @@ impl TemplateContext { } pub fn resolve_schema(&self, initial: &Schema, schemas: &IndexMap) -> Schema { - if let SchemaType::Reference(name) = &initial.ty { + if let SchemaType::Reference { name, .. } = &initial.ty { if let Some(schema) = schemas.get(name) { return schema.to_owned(); } @@ -274,9 +274,9 @@ pub fn render_boolean(boolean: &BooleanType) -> RenderResult { } pub fn render_enum(enu: &EnumType) -> RenderResult { - let index = enu.default_index.unwrap_or(0); - - if let Some(value) = enu.values.get(index) { + // `default_index` indexes the variants, not the values, so resolve it + // through the enum rather than subscripting `values` directly. + if let Some(value) = enu.get_default().or_else(|| enu.values.first()) { return Ok(lit_to_string(value)); } diff --git a/crates/schematic/src/schema/renderers/toml_template.rs b/crates/schematic/src/schema/renderers/toml_template.rs index fc5b2f04..b35e8932 100644 --- a/crates/schematic/src/schema/renderers/toml_template.rs +++ b/crates/schematic/src/schema/renderers/toml_template.rs @@ -171,7 +171,7 @@ impl SchemaRenderer for TomlTemplateRenderer { fn render_struct(&mut self, structure: &StructType, _schema: &Schema) -> RenderResult { let mut out = vec![]; - for (name, field) in &structure.fields { + for (name, field) in structure.sorted_fields() { if field.flatten { continue; } diff --git a/crates/schematic/src/schema/renderers/typescript.rs b/crates/schematic/src/schema/renderers/typescript.rs index 54ed018d..f7cd4d2c 100644 --- a/crates/schematic/src/schema/renderers/typescript.rs +++ b/crates/schematic/src/schema/renderers/typescript.rs @@ -189,7 +189,7 @@ impl TypeScriptRenderer { // Extract flattened fields first as we'll need to use intersections // to support them correctly in TypeScript - for (field_name, field) in &structure.fields { + for (field_name, field) in structure.sorted_fields() { if field.flatten && let Some(schema) = field.schema.get_nonnull_schema() { @@ -518,7 +518,7 @@ impl SchemaRenderer for TypeScriptRenderer { Ok(()) }; - for (name, field) in &structure.fields { + for (name, field) in structure.sorted_fields() { // Handle in `export_object_types` if field.flatten { continue; diff --git a/crates/schematic/src/schema/renderers/yaml_template.rs b/crates/schematic/src/schema/renderers/yaml_template.rs index 4aa7351a..6336b6f4 100644 --- a/crates/schematic/src/schema/renderers/yaml_template.rs +++ b/crates/schematic/src/schema/renderers/yaml_template.rs @@ -118,7 +118,7 @@ impl SchemaRenderer for YamlTemplateRenderer { fn render_struct(&mut self, structure: &StructType, _schema: &Schema) -> RenderResult { let mut out = vec![]; - for (name, field) in &structure.fields { + for (name, field) in structure.sorted_fields() { if field.flatten { continue; } diff --git a/crates/schematic/tests/generator_test.rs b/crates/schematic/tests/generator_test.rs index ef664c86..563b6f83 100644 --- a/crates/schematic/tests/generator_test.rs +++ b/crates/schematic/tests/generator_test.rs @@ -537,3 +537,66 @@ mod typescript { })); } } + +mod name_collisions { + use super::*; + + // Schemas are keyed by name alone, so two types answering with the same + // one would collapse into a single definition and every reference to it + // would resolve to whichever was added first. + struct First; + struct Second; + + macro_rules! impl_collides { + ($type:ty) => { + impl Schematic for $type { + fn schema_name() -> Option { + Some("Collides".into()) + } + + fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.string_default() + } + } + }; + } + + impl_collides!(First); + impl_collides!(Second); + + #[test] + #[should_panic(expected = "A schema named `Collides` has already been added")] + fn reports_two_types_sharing_a_name() { + let mut generator = SchemaGenerator::default(); + + generator.add::(); + generator.add::(); + } + + #[test] + fn allows_adding_the_same_type_twice() { + let mut generator = SchemaGenerator::default(); + + generator.add::(); + generator.add::(); + + assert_eq!(generator.schemas.len(), 1); + } + + // Recursive configs legitimately register the same name at several + // expansion depths, which must not be mistaken for a collision. + #[derive(Config)] + struct Recurses { + #[setting(nested)] + children: Vec, + } + + #[test] + fn allows_a_recursive_type() { + let mut generator = SchemaGenerator::default(); + + generator.add::(); + + assert!(generator.schemas.contains_key("PartialRecurses")); + } +} diff --git a/crates/schematic/tests/partialize_test.rs b/crates/schematic/tests/partialize_test.rs index 03f92908..9f983260 100644 --- a/crates/schematic/tests/partialize_test.rs +++ b/crates/schematic/tests/partialize_test.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use schematic::internal::partialize_schema; -use schematic::{Config, ConfigEnum, SchemaBuilder, Schematic, derive_enum}; +use schematic::{Config, ConfigEnum, Schema, SchemaBuilder, SchemaType, Schematic, derive_enum}; use similar::{ChangeTag, TextDiff}; use starbase_sandbox::assert_snapshot; use std::collections::HashMap; @@ -247,3 +247,61 @@ fn partial_derive() { }; assert_eq!(&Some("test1".to_string()), basic2.as_ref()); } + +#[derive(Config)] +struct Recursive { + name: String, + #[setting(nested)] + children: Vec, +} + +fn collect_references(schema: &Schema, out: &mut Vec) { + match &schema.ty { + SchemaType::Reference { name, .. } => out.push(name.to_owned()), + SchemaType::Array(inner) => collect_references(&inner.items_type, out), + SchemaType::Object(inner) => collect_references(&inner.value_type, out), + SchemaType::Struct(inner) => { + for field in inner.fields.values() { + collect_references(&field.schema, out); + } + } + SchemaType::Tuple(inner) => { + for item in &inner.items_types { + collect_references(item, out); + } + } + SchemaType::Union(inner) => { + for variant in &inner.variants_types { + collect_references(variant, out); + } + } + _ => {} + }; +} + +// A cycle resolves to a reference, which has to be renamed alongside the type +// it points at, otherwise the partial schema references a type that was never +// rendered. +#[test] +fn recursive_references_follow_the_partial_name() { + let schema = SchemaBuilder::build_root::(); + + assert_eq!(schema.name.as_deref(), Some("PartialRecursive")); + + let mut references = vec![]; + collect_references(&schema, &mut references); + + assert_eq!(references, vec!["PartialRecursive"]); +} + +#[test] +fn recursive_references_are_untouched_when_not_partial() { + let schema = SchemaBuilder::build_root::(); + + assert_eq!(schema.name.as_deref(), Some("Recursive")); + + let mut references = vec![]; + collect_references(&schema, &mut references); + + assert_eq!(references, vec!["Recursive"]); +} diff --git a/crates/schematic/tests/snapshots/code_sources_test__generates_typescript.snap b/crates/schematic/tests/snapshots/code_sources_test__generates_typescript.snap index 1a85c90e..3f4e8a5e 100644 --- a/crates/schematic/tests/snapshots/code_sources_test__generates_typescript.snap +++ b/crates/schematic/tests/snapshots/code_sources_test__generates_typescript.snap @@ -1,5 +1,5 @@ --- -source: crates/config/tests/code_sources_test.rs +source: crates/schematic/tests/code_sources_test.rs expression: "std::fs::read_to_string(file).unwrap()" --- // Automatically generated by schematic. DO NOT MODIFY! @@ -13,4 +13,3 @@ export interface Config { string: string; vector: string[]; } - diff --git a/crates/schematic/tests/snapshots/defaults_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/defaults_test__generates_json_schema.snap index 35c5f2e0..d1ce4cd8 100644 --- a/crates/schematic/tests/snapshots/defaults_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/defaults_test__generates_json_schema.snap @@ -429,6 +429,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ] }, "optionalWithDefault": { + "default": 123, "anyOf": [ { "type": "number" diff --git a/crates/schematic/tests/snapshots/defaults_test__generates_typescript.snap b/crates/schematic/tests/snapshots/defaults_test__generates_typescript.snap index 5d186630..4420832b 100644 --- a/crates/schematic/tests/snapshots/defaults_test__generates_typescript.snap +++ b/crates/schematic/tests/snapshots/defaults_test__generates_typescript.snap @@ -20,6 +20,7 @@ export interface CustomDefaults { export interface ReqOptDefaults { optional: number | null; + /** @default 123 */ optionalWithDefault?: number | null; optionalWithDefaultFn?: number | null; required: number; diff --git a/crates/schematic/tests/snapshots/env_test__generates_typescript.snap b/crates/schematic/tests/snapshots/env_test__generates_typescript.snap index 3139e416..1acc3348 100644 --- a/crates/schematic/tests/snapshots/env_test__generates_typescript.snap +++ b/crates/schematic/tests/snapshots/env_test__generates_typescript.snap @@ -26,4 +26,3 @@ export interface EnvVarsPrefixed { /** @env ENV_STRING */ string: string; } - diff --git a/crates/schematic/tests/snapshots/partialize_test__compounds.snap b/crates/schematic/tests/snapshots/partialize_test__compounds.snap index 9ccf472b..044b5e24 100644 --- a/crates/schematic/tests/snapshots/partialize_test__compounds.snap +++ b/crates/schematic/tests/snapshots/partialize_test__compounds.snap @@ -40,9 +40,7 @@ expression: "create_diff::()" 🟥 }, 🟥 ), 🟥 }, -🟥 max_contains: None, 🟥 max_length: None, -🟥 min_contains: None, 🟥 min_length: None, 🟥 unique: None, 🟩 nullable: true, @@ -76,9 +74,7 @@ expression: "create_diff::()" 🟩 }, 🟩 ), 🟩 }, -🟩 max_contains: None, 🟩 max_length: None, -🟩 min_contains: None, 🟩 min_length: None, 🟩 unique: None, 🟩 }, @@ -113,8 +109,7 @@ expression: "create_diff::()" ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: None, -🟥 nullable: false, -🟩 nullable: true, +⬛️ nullable: true, ⬛️ ty: Union( ⬛️ UnionType { ⬛️ default_index: None, @@ -148,9 +143,7 @@ expression: "create_diff::()" ⬛️ }, ⬛️ ), ⬛️ }, -⬛️ max_contains: None, ⬛️ max_length: None, -⬛️ min_contains: None, ⬛️ min_length: None, ⬛️ unique: None, ⬛️ }, @@ -293,8 +286,7 @@ expression: "create_diff::()" ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: None, -🟥 nullable: false, -🟩 nullable: true, +⬛️ nullable: true, ⬛️ ty: Union( ⬛️ UnionType { ⬛️ default_index: None, @@ -346,7 +338,6 @@ expression: "create_diff::()" ⬛️ min: None, ⬛️ min_exclusive: None, ⬛️ multiple_of: None, -⬛️ name: None, ⬛️ }, ⬛️ ), ⬛️ }, diff --git a/crates/schematic/tests/snapshots/partialize_test__enum_adjacent.snap b/crates/schematic/tests/snapshots/partialize_test__enum_adjacent.snap index c8f7f88c..ecf07d00 100644 --- a/crates/schematic/tests/snapshots/partialize_test__enum_adjacent.snap +++ b/crates/schematic/tests/snapshots/partialize_test__enum_adjacent.snap @@ -24,7 +24,7 @@ expression: "create_diff::()" ⬛️ ty: Struct( ⬛️ StructType { ⬛️ fields: { -⬛️ "content": SchemaField { +⬛️ "type": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -34,7 +34,6 @@ expression: "create_diff::()" ⬛️ nullable: false, ⬛️ ty: Literal( ⬛️ LiteralType { -⬛️ format: None, ⬛️ value: String( ⬛️ "foo", ⬛️ ), @@ -50,7 +49,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "type": SchemaField { +⬛️ "content": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -60,7 +59,6 @@ expression: "create_diff::()" ⬛️ nullable: false, ⬛️ ty: Literal( ⬛️ LiteralType { -⬛️ format: None, ⬛️ value: String( ⬛️ "foo", ⬛️ ), @@ -90,7 +88,7 @@ expression: "create_diff::()" ⬛️ ty: Struct( ⬛️ StructType { ⬛️ fields: { -⬛️ "content": SchemaField { +⬛️ "type": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -98,9 +96,11 @@ expression: "create_diff::()" ⬛️ description: None, ⬛️ name: None, ⬛️ nullable: false, -⬛️ ty: Boolean( -⬛️ BooleanType { -⬛️ default: None, +⬛️ ty: Literal( +⬛️ LiteralType { +⬛️ value: String( +⬛️ "bar", +⬛️ ), ⬛️ }, ⬛️ ), ⬛️ }, @@ -113,7 +113,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "type": SchemaField { +⬛️ "content": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -121,12 +121,9 @@ expression: "create_diff::()" ⬛️ description: None, ⬛️ name: None, ⬛️ nullable: false, -⬛️ ty: Literal( -⬛️ LiteralType { -⬛️ format: None, -⬛️ value: String( -⬛️ "bar", -⬛️ ), +⬛️ ty: Boolean( +⬛️ BooleanType { +⬛️ default: None, ⬛️ }, ⬛️ ), ⬛️ }, @@ -153,6 +150,31 @@ expression: "create_diff::()" ⬛️ ty: Struct( ⬛️ StructType { ⬛️ fields: { +⬛️ "type": SchemaField { +⬛️ aliases: [], +⬛️ comment: None, +⬛️ schema: Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +⬛️ ty: Literal( +⬛️ LiteralType { +⬛️ value: String( +⬛️ "baz", +⬛️ ), +⬛️ }, +⬛️ ), +⬛️ }, +⬛️ deprecated: None, +⬛️ env_var: None, +⬛️ flatten: false, +⬛️ hidden: false, +⬛️ nullable: false, +⬛️ optional: false, +⬛️ read_only: false, +⬛️ write_only: false, +⬛️ }, ⬛️ "content": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, @@ -184,6 +206,20 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, +⬛️ }, +⬛️ partial: false, +⬛️ required: None, +⬛️ }, +⬛️ ), +⬛️ }, +⬛️ Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +⬛️ ty: Struct( +⬛️ StructType { +⬛️ fields: { ⬛️ "type": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, @@ -194,9 +230,8 @@ expression: "create_diff::()" ⬛️ nullable: false, ⬛️ ty: Literal( ⬛️ LiteralType { -⬛️ format: None, ⬛️ value: String( -⬛️ "baz", +⬛️ "qux", ⬛️ ), ⬛️ }, ⬛️ ), @@ -210,20 +245,6 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ }, -⬛️ partial: false, -⬛️ required: None, -⬛️ }, -⬛️ ), -⬛️ }, -⬛️ Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: Struct( -⬛️ StructType { -⬛️ fields: { ⬛️ "content": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, @@ -314,32 +335,6 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "type": SchemaField { -⬛️ aliases: [], -⬛️ comment: None, -⬛️ schema: Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: Literal( -⬛️ LiteralType { -⬛️ format: None, -⬛️ value: String( -⬛️ "qux", -⬛️ ), -⬛️ }, -⬛️ ), -⬛️ }, -⬛️ deprecated: None, -⬛️ env_var: None, -⬛️ flatten: false, -⬛️ hidden: false, -⬛️ nullable: false, -⬛️ optional: false, -⬛️ read_only: false, -⬛️ write_only: false, -⬛️ }, ⬛️ }, ⬛️ partial: false, ⬛️ required: None, diff --git a/crates/schematic/tests/snapshots/partialize_test__enum_external.snap b/crates/schematic/tests/snapshots/partialize_test__enum_external.snap index 4ccc45fc..2e9e995c 100644 --- a/crates/schematic/tests/snapshots/partialize_test__enum_external.snap +++ b/crates/schematic/tests/snapshots/partialize_test__enum_external.snap @@ -34,7 +34,6 @@ expression: "create_diff::()" ⬛️ nullable: false, ⬛️ ty: Literal( ⬛️ LiteralType { -⬛️ format: None, ⬛️ value: String( ⬛️ "foo", ⬛️ ), diff --git a/crates/schematic/tests/snapshots/partialize_test__enum_internal.snap b/crates/schematic/tests/snapshots/partialize_test__enum_internal.snap index 858e14ea..69e0902d 100644 --- a/crates/schematic/tests/snapshots/partialize_test__enum_internal.snap +++ b/crates/schematic/tests/snapshots/partialize_test__enum_internal.snap @@ -23,7 +23,6 @@ expression: "create_diff::()" ⬛️ nullable: false, ⬛️ ty: Literal( ⬛️ LiteralType { -⬛️ format: None, ⬛️ value: String( ⬛️ "foo", ⬛️ ), @@ -142,7 +141,6 @@ expression: "create_diff::()" 🟥 nullable: false, 🟥 ty: Literal( 🟥 LiteralType { -🟥 format: None, 🟥 value: String( 🟥 "qux", 🟥 ), @@ -160,7 +158,6 @@ expression: "create_diff::()" 🟩 nullable: false, 🟩 ty: Literal( 🟩 LiteralType { -🟩 format: None, 🟩 value: String( 🟩 "qux", 🟩 ), diff --git a/crates/schematic/tests/snapshots/partialize_test__enums.snap b/crates/schematic/tests/snapshots/partialize_test__enums.snap index 149b8d48..2908f6a3 100644 --- a/crates/schematic/tests/snapshots/partialize_test__enums.snap +++ b/crates/schematic/tests/snapshots/partialize_test__enums.snap @@ -13,14 +13,14 @@ expression: "create_diff::()" ⬛️ ty: Struct( ⬛️ StructType { ⬛️ fields: { -⬛️ "fallback": SchemaField { +⬛️ "unit": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { ⬛️ deprecated: None, ⬛️ description: None, 🟥 name: Some( -🟥 "UnitFallbackEnum", +🟥 "UnitEnum", 🟥 ), 🟥 nullable: false, 🟥 ty: Enum( @@ -51,63 +51,10 @@ expression: "create_diff::()" 🟥 nullable: false, 🟥 ty: Literal( 🟥 LiteralType { -🟥 format: None, 🟥 value: String( 🟥 "a", 🟥 ), 🟥 }, -🟥 ), -🟥 }, -🟥 deprecated: None, -🟥 env_var: None, -🟥 flatten: false, -🟥 hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟥 read_only: false, -🟥 write_only: false, -🟥 }, -🟥 "B": SchemaField { -🟥 aliases: [], -🟥 comment: None, -🟥 schema: Schema { -🟥 deprecated: None, -🟥 description: None, -🟥 name: None, -🟥 nullable: false, -🟥 ty: Literal( -🟥 LiteralType { -🟥 format: None, -🟥 value: String( -🟥 "b", -🟥 ), -🟥 }, -🟥 ), -🟥 }, -🟥 deprecated: None, -🟥 env_var: None, -🟥 flatten: false, -🟥 hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟥 read_only: false, -🟥 write_only: false, -🟥 }, -🟥 "C": SchemaField { -🟥 aliases: [], -🟥 comment: None, -🟥 schema: Schema { -🟥 deprecated: None, -🟥 description: None, -🟥 name: None, -🟥 nullable: false, -🟥 ty: Literal( -🟥 LiteralType { -🟥 format: None, -🟥 value: String( -🟥 "c", -🟥 ), -🟥 }, 🟩 name: None, 🟩 nullable: true, 🟩 ty: Union( @@ -120,7 +67,7 @@ expression: "create_diff::()" 🟩 deprecated: None, 🟩 description: None, 🟩 name: Some( -🟩 "UnitFallbackEnum", +🟩 "UnitEnum", 🟩 ), 🟩 nullable: false, 🟩 ty: Enum( @@ -138,7 +85,7 @@ expression: "create_diff::()" 🟥 read_only: false, 🟥 write_only: false, 🟥 }, -🟥 "Other": SchemaField { +🟥 "B": SchemaField { 🟥 aliases: [], 🟥 comment: None, 🟥 schema: Schema { @@ -146,14 +93,11 @@ expression: "create_diff::()" 🟥 description: None, 🟥 name: None, 🟥 nullable: false, -🟥 ty: String( -🟥 StringType { -🟥 default: None, -🟥 enum_values: None, -🟥 format: None, -🟥 max_length: None, -🟥 min_length: None, -🟥 pattern: None, +🟥 ty: Literal( +🟥 LiteralType { +🟥 value: String( +🟥 "b", +🟥 ), 🟩 values: [ 🟩 String( 🟩 "a", @@ -177,7 +121,6 @@ expression: "create_diff::()" 🟩 nullable: false, 🟩 ty: Literal( 🟩 LiteralType { -🟩 format: None, 🟩 value: String( 🟩 "a", 🟩 ), @@ -203,7 +146,6 @@ expression: "create_diff::()" 🟩 nullable: false, 🟩 ty: Literal( 🟩 LiteralType { -🟩 format: None, 🟩 value: String( 🟩 "b", 🟩 ), @@ -229,7 +171,6 @@ expression: "create_diff::()" 🟩 nullable: false, 🟩 ty: Literal( 🟩 LiteralType { -🟩 format: None, 🟩 value: String( 🟩 "c", 🟩 ), @@ -245,34 +186,6 @@ expression: "create_diff::()" 🟩 read_only: false, 🟩 write_only: false, 🟩 }, -🟩 "Other": SchemaField { -🟩 aliases: [], -🟩 comment: None, -🟩 schema: Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: String( -🟩 StringType { -🟩 default: None, -🟩 enum_values: None, -🟩 format: None, -🟩 max_length: None, -🟩 min_length: None, -🟩 pattern: None, -🟩 }, -🟩 ), -🟩 }, -🟩 deprecated: None, -🟩 env_var: None, -🟩 flatten: false, -🟩 hidden: false, -🟩 nullable: false, -🟩 optional: false, -🟩 read_only: false, -🟩 write_only: false, -🟩 }, ⬛️ }, ⬛️ ), ⬛️ }, @@ -285,16 +198,41 @@ expression: "create_diff::()" 🟥 read_only: false, 🟥 write_only: false, 🟥 }, +🟥 "C": SchemaField { +🟥 aliases: [], +🟥 comment: None, +🟥 schema: Schema { +🟥 deprecated: None, +🟥 description: None, +🟥 name: None, +🟥 nullable: false, +🟥 ty: Literal( +🟥 LiteralType { +🟥 value: String( +🟥 "c", +🟥 ), +🟥 }, +🟥 ), +🟥 }, +🟥 deprecated: None, +🟥 env_var: None, +🟥 flatten: false, +🟥 hidden: false, +🟥 nullable: false, +🟥 optional: false, +🟥 read_only: false, +🟥 write_only: false, +🟥 }, 🟩 ), -⬛️ }, -🟥 ), +🟩 }, 🟩 Schema { 🟩 deprecated: None, 🟩 description: None, 🟩 name: None, 🟩 nullable: false, 🟩 ty: Null, -🟩 }, +⬛️ }, +🟥 ), 🟩 ], ⬛️ }, ⬛️ ), @@ -310,15 +248,14 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "fallbackOpt": SchemaField { +⬛️ "unitOpt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: None, -🟥 nullable: false, -🟩 nullable: true, +⬛️ nullable: true, ⬛️ ty: Union( ⬛️ UnionType { ⬛️ default_index: None, @@ -329,7 +266,7 @@ expression: "create_diff::()" ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: Some( -⬛️ "UnitFallbackEnum", +⬛️ "UnitEnum", ⬛️ ), ⬛️ nullable: false, ⬛️ ty: Enum( @@ -360,7 +297,6 @@ expression: "create_diff::()" ⬛️ nullable: false, ⬛️ ty: Literal( ⬛️ LiteralType { -⬛️ format: None, ⬛️ value: String( ⬛️ "a", ⬛️ ), @@ -386,7 +322,6 @@ expression: "create_diff::()" ⬛️ nullable: false, ⬛️ ty: Literal( ⬛️ LiteralType { -⬛️ format: None, ⬛️ value: String( ⬛️ "b", ⬛️ ), @@ -412,7 +347,6 @@ expression: "create_diff::()" ⬛️ nullable: false, ⬛️ ty: Literal( ⬛️ LiteralType { -⬛️ format: None, ⬛️ value: String( ⬛️ "c", ⬛️ ), @@ -428,34 +362,6 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "Other": SchemaField { -⬛️ aliases: [], -⬛️ comment: None, -⬛️ schema: Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: String( -⬛️ StringType { -⬛️ default: None, -⬛️ enum_values: None, -⬛️ format: None, -⬛️ max_length: None, -⬛️ min_length: None, -⬛️ pattern: None, -⬛️ }, -⬛️ ), -⬛️ }, -⬛️ deprecated: None, -⬛️ env_var: None, -⬛️ flatten: false, -⬛️ hidden: false, -⬛️ nullable: false, -⬛️ optional: false, -⬛️ read_only: false, -⬛️ write_only: false, -⬛️ }, ⬛️ }, ⬛️ ), ⬛️ }, @@ -482,241 +388,283 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "tuple": SchemaField { +⬛️ "fallback": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { ⬛️ deprecated: None, ⬛️ description: None, 🟥 name: Some( -🟥 "TupleEnum", +🟥 "UnitFallbackEnum", 🟥 ), 🟥 nullable: false, +🟥 ty: Enum( +🟥 EnumType { +🟥 default_index: Some( +🟥 0, +🟥 ), +🟥 values: [ +🟥 String( +🟥 "a", +🟥 ), +🟥 String( +🟥 "b", +🟥 ), +🟥 String( +🟥 "c", +🟥 ), +🟥 ], +🟥 variants: Some( +🟥 { +🟥 "A": SchemaField { +🟥 aliases: [], +🟥 comment: None, +🟥 schema: Schema { +🟥 deprecated: None, +🟥 description: None, +🟥 name: None, +🟥 nullable: false, +🟥 ty: Literal( +🟥 LiteralType { +🟥 value: String( +🟥 "a", +🟥 ), +🟥 }, 🟩 name: None, 🟩 nullable: true, -⬛️ ty: Union( -⬛️ UnionType { -⬛️ default_index: None, -🟥 partial: true, +🟩 ty: Union( +🟩 UnionType { +🟩 default_index: None, 🟩 partial: false, -⬛️ operator: AnyOf, -⬛️ variants_types: [ -⬛️ Schema { -⬛️ deprecated: None, -⬛️ description: None, -🟥 name: None, +🟩 operator: AnyOf, +🟩 variants_types: [ +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, 🟩 name: Some( -🟩 "PartialTupleEnum", +🟩 "UnitFallbackEnum", 🟩 ), -⬛️ nullable: false, -🟥 ty: Struct( -🟥 StructType { -🟥 fields: { -🟥 "a": SchemaField { -🟥 aliases: [], -🟥 comment: None, -🟥 schema: Schema { -🟥 deprecated: None, -🟥 description: None, -🟥 name: Some( -🟥 "Empty", -🟥 ), -🟥 nullable: false, -🟥 ty: Struct( -🟥 StructType { -🟥 fields: {}, -🟥 partial: true, -🟥 required: None, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, -🟩 partial: true, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Struct( -🟩 StructType { -🟩 fields: { -🟩 "a": SchemaField { -🟩 aliases: [], -🟩 comment: None, -🟩 schema: Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: true, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, -🟩 partial: false, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: Some( -🟩 "PartialEmpty", -🟩 ), -🟩 nullable: false, -🟩 ty: Struct( -🟩 StructType { -🟩 fields: {}, -🟩 partial: true, -🟩 required: None, -🟩 }, -🟩 ), -🟩 }, -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Null, -🟩 }, -🟩 ], -🟩 }, -🟩 ), -🟩 }, -🟩 deprecated: None, -🟩 env_var: None, -🟩 flatten: false, -🟩 hidden: false, -🟩 nullable: true, -🟩 optional: true, -🟩 read_only: false, -🟩 write_only: false, +🟩 nullable: false, +🟩 ty: Enum( +🟩 EnumType { +🟩 default_index: Some( +🟩 0, +⬛️ ), +🟥 }, +🟥 deprecated: None, +🟥 env_var: None, +🟥 flatten: false, +🟥 hidden: false, +🟥 nullable: false, +🟥 optional: false, +🟥 read_only: false, +🟥 write_only: false, +🟥 }, +🟥 "B": SchemaField { +🟥 aliases: [], +🟥 comment: None, +🟥 schema: Schema { +🟥 deprecated: None, +🟥 description: None, +🟥 name: None, +🟥 nullable: false, +🟥 ty: Literal( +🟥 LiteralType { +🟥 value: String( +🟥 "b", +🟥 ), +🟥 }, +🟥 ), +🟥 }, +🟥 deprecated: None, +🟥 env_var: None, +🟥 flatten: false, +🟥 hidden: false, +🟥 nullable: false, +🟥 optional: false, +🟥 read_only: false, +🟥 write_only: false, +🟥 }, +🟥 "C": SchemaField { +🟥 aliases: [], +🟥 comment: None, +🟥 schema: Schema { +🟥 deprecated: None, +🟥 description: None, +🟥 name: None, +🟥 nullable: false, +🟥 ty: Literal( +🟥 LiteralType { +🟥 value: String( +🟥 "c", +🟥 ), +🟩 values: [ +🟩 String( +🟩 "a", +🟩 ), +🟩 String( +🟩 "b", +🟩 ), +🟩 String( +🟩 "c", +🟩 ), +🟩 ], +🟩 variants: Some( +🟩 { +🟩 "A": SchemaField { +🟩 aliases: [], +🟩 comment: None, +🟩 schema: Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: false, +🟩 ty: Literal( +🟩 LiteralType { +🟩 value: String( +🟩 "a", +🟩 ), 🟩 }, -⬛️ }, -🟥 ), -🟥 }, -🟩 partial: true, -🟩 required: None, +🟩 ), 🟩 }, -🟩 ), -🟩 }, -🟩 Schema { -⬛️ deprecated: None, -🟥 env_var: None, -🟥 flatten: false, -🟥 hidden: false, -🟩 description: None, -🟩 name: None, -⬛️ nullable: false, -🟥 optional: false, -🟥 read_only: false, -🟥 write_only: false, -🟩 ty: Struct( -🟩 StructType { -🟩 fields: { -🟩 "b": SchemaField { -🟩 aliases: [], -🟩 comment: None, -🟩 schema: Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: true, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, -🟩 partial: false, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: Some( -🟩 "PartialEmpty", -🟩 ), -🟩 nullable: false, -🟩 ty: Struct( -🟩 StructType { -🟩 fields: {}, -🟩 partial: true, -🟩 required: None, -🟩 }, -🟩 ), -🟩 }, -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Null, -🟩 }, -🟩 ], -🟩 }, -🟩 ), -🟩 }, -🟩 deprecated: None, -🟩 env_var: None, -🟩 flatten: false, -🟩 hidden: false, -🟩 nullable: true, -🟩 optional: true, -🟩 read_only: false, -🟩 write_only: false, +🟩 deprecated: None, +🟩 env_var: None, +🟩 flatten: false, +🟩 hidden: false, +🟩 nullable: false, +🟩 optional: false, +🟩 read_only: false, +🟩 write_only: false, +🟩 }, +🟩 "B": SchemaField { +🟩 aliases: [], +🟩 comment: None, +🟩 schema: Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: false, +🟩 ty: Literal( +🟩 LiteralType { +🟩 value: String( +🟩 "b", +🟩 ), 🟩 }, -🟩 }, -🟩 partial: true, -🟩 required: None, +🟩 ), 🟩 }, -🟩 ), -⬛️ }, -🟥 }, -🟥 partial: true, -🟥 required: None, -🟩 ], -⬛️ }, -⬛️ ), -⬛️ }, -⬛️ Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -🟥 ty: Struct( -🟥 StructType { -🟥 fields: { -🟥 "b": SchemaField { -🟥 aliases: [], -🟥 comment: None, -🟥 schema: Schema { -🟥 deprecated: None, -🟥 description: None, -🟥 name: Some( -🟥 "Empty", -🟥 ), -🟥 nullable: false, -🟥 ty: Struct( -🟥 StructType { -🟥 fields: {}, -🟥 partial: true, -🟥 required: None, -🟥 }, -🟥 ), -🟥 }, -🟥 deprecated: None, -🟥 env_var: None, -🟥 flatten: false, -🟥 hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟥 read_only: false, -🟥 write_only: false, +🟩 deprecated: None, +🟩 env_var: None, +🟩 flatten: false, +🟩 hidden: false, +🟩 nullable: false, +🟩 optional: false, +🟩 read_only: false, +🟩 write_only: false, +🟩 }, +🟩 "C": SchemaField { +🟩 aliases: [], +🟩 comment: None, +🟩 schema: Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: false, +🟩 ty: Literal( +🟩 LiteralType { +🟩 value: String( +🟩 "c", +🟩 ), +🟩 }, +🟩 ), +🟩 }, +🟩 deprecated: None, +🟩 env_var: None, +🟩 flatten: false, +🟩 hidden: false, +🟩 nullable: false, +🟩 optional: false, +🟩 read_only: false, +🟩 write_only: false, +🟩 }, +🟩 "Other": SchemaField { +🟩 aliases: [], +🟩 comment: None, +🟩 schema: Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: false, +🟩 ty: String( +🟩 StringType { +🟩 default: None, +🟩 enum_values: None, +🟩 format: None, +🟩 max_length: None, +🟩 min_length: None, +🟩 pattern: None, +🟩 }, +🟩 ), +🟩 }, +🟩 deprecated: None, +🟩 env_var: None, +🟩 flatten: false, +🟩 hidden: false, +🟩 nullable: false, +🟩 optional: false, +🟩 read_only: false, +🟩 write_only: false, +🟩 }, +⬛️ }, +⬛️ ), +⬛️ }, +🟥 deprecated: None, +🟥 env_var: None, +🟥 flatten: false, +🟥 hidden: false, +🟥 nullable: false, +🟥 optional: false, +🟥 read_only: false, +🟥 write_only: false, +🟥 }, +🟥 "Other": SchemaField { +🟥 aliases: [], +🟥 comment: None, +🟥 schema: Schema { +🟥 deprecated: None, +🟥 description: None, +🟥 name: None, +🟥 nullable: false, +🟥 ty: String( +🟥 StringType { +🟥 default: None, +🟥 enum_values: None, +🟥 format: None, +🟥 max_length: None, +🟥 min_length: None, +🟥 pattern: None, 🟥 }, -🟥 }, -🟥 partial: true, -🟥 required: None, +🟥 ), 🟥 }, -🟥 ), +🟥 deprecated: None, +🟥 env_var: None, +🟥 flatten: false, +🟥 hidden: false, +🟥 nullable: false, +🟥 optional: false, +🟥 read_only: false, +🟥 write_only: false, +🟥 }, +🟩 ), +🟩 }, +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: false, 🟩 ty: Null, ⬛️ }, -⬛️ ], +🟥 ), +🟩 ], ⬛️ }, ⬛️ ), ⬛️ }, @@ -731,184 +679,150 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "tupleOpt": SchemaField { +⬛️ "fallbackOpt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: None, -🟥 nullable: false, -🟩 nullable: true, +⬛️ nullable: true, ⬛️ ty: Union( ⬛️ UnionType { ⬛️ default_index: None, -⬛️ partial: true, +⬛️ partial: false, ⬛️ operator: AnyOf, ⬛️ variants_types: [ ⬛️ Schema { ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: Some( -🟥 "TupleEnum", -🟩 "PartialTupleEnum", +⬛️ "UnitFallbackEnum", ⬛️ ), ⬛️ nullable: false, -⬛️ ty: Union( -⬛️ UnionType { -⬛️ default_index: None, -⬛️ partial: true, -⬛️ operator: AnyOf, -⬛️ variants_types: [ -⬛️ Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: Struct( -⬛️ StructType { -⬛️ fields: { -⬛️ "a": SchemaField { -⬛️ aliases: [], -⬛️ comment: None, -⬛️ schema: Schema { -⬛️ deprecated: None, -⬛️ description: None, -🟥 name: Some( -🟥 "Empty", -🟥 ), -🟥 nullable: false, -🟥 ty: Struct( -🟥 StructType { -🟥 fields: {}, -🟥 partial: true, -🟥 required: None, -🟩 name: None, -🟩 nullable: true, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, -🟩 partial: false, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: Some( -🟩 "PartialEmpty", -🟩 ), -🟩 nullable: false, -🟩 ty: Struct( -🟩 StructType { -🟩 fields: {}, -🟩 partial: true, -🟩 required: None, -🟩 }, -🟩 ), -🟩 }, -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Null, -🟩 }, -🟩 ], -⬛️ }, -⬛️ ), -⬛️ }, -⬛️ deprecated: None, -⬛️ env_var: None, -⬛️ flatten: false, -⬛️ hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟩 nullable: true, -🟩 optional: true, -⬛️ read_only: false, -⬛️ write_only: false, +⬛️ ty: Enum( +⬛️ EnumType { +⬛️ default_index: Some( +⬛️ 0, +⬛️ ), +⬛️ values: [ +⬛️ String( +⬛️ "a", +⬛️ ), +⬛️ String( +⬛️ "b", +⬛️ ), +⬛️ String( +⬛️ "c", +⬛️ ), +⬛️ ], +⬛️ variants: Some( +⬛️ { +⬛️ "A": SchemaField { +⬛️ aliases: [], +⬛️ comment: None, +⬛️ schema: Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +⬛️ ty: Literal( +⬛️ LiteralType { +⬛️ value: String( +⬛️ "a", +⬛️ ), ⬛️ }, -⬛️ }, -⬛️ partial: true, -⬛️ required: None, -⬛️ }, -⬛️ ), -⬛️ }, -⬛️ Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: Struct( -⬛️ StructType { -⬛️ fields: { -⬛️ "b": SchemaField { -⬛️ aliases: [], -⬛️ comment: None, -⬛️ schema: Schema { -⬛️ deprecated: None, -⬛️ description: None, -🟥 name: Some( -🟥 "Empty", -🟥 ), -🟥 nullable: false, -🟥 ty: Struct( -🟥 StructType { -🟥 fields: {}, -🟥 partial: true, -🟥 required: None, -🟩 name: None, -🟩 nullable: true, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, -🟩 partial: false, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: Some( -🟩 "PartialEmpty", -🟩 ), -🟩 nullable: false, -🟩 ty: Struct( -🟩 StructType { -🟩 fields: {}, -🟩 partial: true, -🟩 required: None, -🟩 }, -🟩 ), -🟩 }, -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Null, -🟩 }, -🟩 ], -⬛️ }, -⬛️ ), -⬛️ }, -⬛️ deprecated: None, -⬛️ env_var: None, -⬛️ flatten: false, -⬛️ hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟩 nullable: true, -🟩 optional: true, -⬛️ read_only: false, -⬛️ write_only: false, +⬛️ ), +⬛️ }, +⬛️ deprecated: None, +⬛️ env_var: None, +⬛️ flatten: false, +⬛️ hidden: false, +⬛️ nullable: false, +⬛️ optional: false, +⬛️ read_only: false, +⬛️ write_only: false, +⬛️ }, +⬛️ "B": SchemaField { +⬛️ aliases: [], +⬛️ comment: None, +⬛️ schema: Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +⬛️ ty: Literal( +⬛️ LiteralType { +⬛️ value: String( +⬛️ "b", +⬛️ ), ⬛️ }, -⬛️ }, -⬛️ partial: true, -⬛️ required: None, +⬛️ ), ⬛️ }, -⬛️ ), +⬛️ deprecated: None, +⬛️ env_var: None, +⬛️ flatten: false, +⬛️ hidden: false, +⬛️ nullable: false, +⬛️ optional: false, +⬛️ read_only: false, +⬛️ write_only: false, +⬛️ }, +⬛️ "C": SchemaField { +⬛️ aliases: [], +⬛️ comment: None, +⬛️ schema: Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +⬛️ ty: Literal( +⬛️ LiteralType { +⬛️ value: String( +⬛️ "c", +⬛️ ), +⬛️ }, +⬛️ ), +⬛️ }, +⬛️ deprecated: None, +⬛️ env_var: None, +⬛️ flatten: false, +⬛️ hidden: false, +⬛️ nullable: false, +⬛️ optional: false, +⬛️ read_only: false, +⬛️ write_only: false, +⬛️ }, +⬛️ "Other": SchemaField { +⬛️ aliases: [], +⬛️ comment: None, +⬛️ schema: Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +⬛️ ty: String( +⬛️ StringType { +⬛️ default: None, +⬛️ enum_values: None, +⬛️ format: None, +⬛️ max_length: None, +⬛️ min_length: None, +⬛️ pattern: None, +⬛️ }, +⬛️ ), +⬛️ }, +⬛️ deprecated: None, +⬛️ env_var: None, +⬛️ flatten: false, +⬛️ hidden: false, +⬛️ nullable: false, +⬛️ optional: false, +⬛️ read_only: false, +⬛️ write_only: false, +⬛️ }, ⬛️ }, -⬛️ ], +⬛️ ), ⬛️ }, ⬛️ ), ⬛️ }, @@ -933,233 +847,241 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "unit": SchemaField { +⬛️ "tuple": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { ⬛️ deprecated: None, ⬛️ description: None, 🟥 name: Some( -🟥 "UnitEnum", +🟥 "TupleEnum", 🟥 ), 🟥 nullable: false, -🟥 ty: Enum( -🟥 EnumType { -🟥 default_index: Some( -🟥 0, -🟥 ), -🟥 values: [ -🟥 String( -🟥 "a", -🟥 ), -🟥 String( -🟥 "b", -🟥 ), -🟥 String( -🟥 "c", -🟥 ), -🟥 ], -🟥 variants: Some( -🟥 { -🟥 "A": SchemaField { -🟥 aliases: [], -🟥 comment: None, -🟥 schema: Schema { -🟥 deprecated: None, -🟥 description: None, -🟥 name: None, -🟥 nullable: false, -🟥 ty: Literal( -🟥 LiteralType { -🟥 format: None, -🟥 value: String( -🟥 "a", -🟥 ), -🟥 }, -🟥 ), -🟥 }, -🟥 deprecated: None, -🟥 env_var: None, -🟥 flatten: false, -🟥 hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟥 read_only: false, -🟥 write_only: false, -🟥 }, -🟥 "B": SchemaField { -🟥 aliases: [], -🟥 comment: None, -🟥 schema: Schema { -🟥 deprecated: None, -🟥 description: None, -🟥 name: None, -🟥 nullable: false, -🟥 ty: Literal( -🟥 LiteralType { -🟥 format: None, -🟥 value: String( -🟥 "b", -🟥 ), -🟥 }, 🟩 name: None, 🟩 nullable: true, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, +⬛️ ty: Union( +⬛️ UnionType { +⬛️ default_index: None, +🟥 partial: true, 🟩 partial: false, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: Some( -🟩 "UnitEnum", -🟩 ), -🟩 nullable: false, -🟩 ty: Enum( -🟩 EnumType { -🟩 default_index: Some( -🟩 0, -⬛️ ), -🟥 }, -🟥 deprecated: None, -🟥 env_var: None, -🟥 flatten: false, -🟥 hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟥 read_only: false, -🟥 write_only: false, -🟥 }, -🟥 "C": SchemaField { -🟥 aliases: [], -🟥 comment: None, -🟥 schema: Schema { -🟥 deprecated: None, -🟥 description: None, -🟥 name: None, -🟥 nullable: false, -🟥 ty: Literal( -🟥 LiteralType { -🟥 format: None, -🟥 value: String( -🟥 "c", -🟥 ), -🟩 values: [ -🟩 String( -🟩 "a", -🟩 ), -🟩 String( -🟩 "b", -🟩 ), -🟩 String( -🟩 "c", -🟩 ), -🟩 ], -🟩 variants: Some( -🟩 { -🟩 "A": SchemaField { -🟩 aliases: [], -🟩 comment: None, -🟩 schema: Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Literal( -🟩 LiteralType { -🟩 format: None, -🟩 value: String( -🟩 "a", -🟩 ), -🟩 }, -🟩 ), -🟩 }, -🟩 deprecated: None, -🟩 env_var: None, -🟩 flatten: false, -🟩 hidden: false, -🟩 nullable: false, -🟩 optional: false, -🟩 read_only: false, -🟩 write_only: false, -🟩 }, -🟩 "B": SchemaField { -🟩 aliases: [], -🟩 comment: None, -🟩 schema: Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Literal( -🟩 LiteralType { -🟩 format: None, -🟩 value: String( -🟩 "b", -🟩 ), +⬛️ operator: AnyOf, +⬛️ variants_types: [ +⬛️ Schema { +⬛️ deprecated: None, +⬛️ description: None, +🟥 name: None, +🟩 name: Some( +🟩 "PartialTupleEnum", +🟩 ), +⬛️ nullable: false, +🟥 ty: Struct( +🟥 StructType { +🟥 fields: { +🟥 "a": SchemaField { +🟥 aliases: [], +🟥 comment: None, +🟥 schema: Schema { +🟥 deprecated: None, +🟥 description: None, +🟥 name: Some( +🟥 "Empty", +🟥 ), +🟥 nullable: false, +🟥 ty: Struct( +🟥 StructType { +🟥 fields: {}, +🟥 partial: true, +🟥 required: None, +🟩 ty: Union( +🟩 UnionType { +🟩 default_index: None, +🟩 partial: true, +🟩 operator: AnyOf, +🟩 variants_types: [ +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: false, +🟩 ty: Struct( +🟩 StructType { +🟩 fields: { +🟩 "a": SchemaField { +🟩 aliases: [], +🟩 comment: None, +🟩 schema: Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: true, +🟩 ty: Union( +🟩 UnionType { +🟩 default_index: None, +🟩 partial: false, +🟩 operator: AnyOf, +🟩 variants_types: [ +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: Some( +🟩 "PartialEmpty", +🟩 ), +🟩 nullable: false, +🟩 ty: Struct( +🟩 StructType { +🟩 fields: {}, +🟩 partial: true, +🟩 required: None, +🟩 }, +🟩 ), +🟩 }, +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: false, +🟩 ty: Null, +🟩 }, +🟩 ], +🟩 }, +🟩 ), +🟩 }, +🟩 deprecated: None, +🟩 env_var: None, +🟩 flatten: false, +🟩 hidden: false, +🟩 nullable: true, +🟩 optional: true, +🟩 read_only: false, +🟩 write_only: false, 🟩 }, -🟩 ), +⬛️ }, +🟥 ), +🟥 }, +🟩 partial: true, +🟩 required: None, 🟩 }, -🟩 deprecated: None, -🟩 env_var: None, -🟩 flatten: false, -🟩 hidden: false, -🟩 nullable: false, -🟩 optional: false, -🟩 read_only: false, -🟩 write_only: false, -🟩 }, -🟩 "C": SchemaField { -🟩 aliases: [], -🟩 comment: None, -🟩 schema: Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Literal( -🟩 LiteralType { -🟩 format: None, -🟩 value: String( -🟩 "c", -🟩 ), +🟩 ), +🟩 }, +🟩 Schema { +⬛️ deprecated: None, +🟥 env_var: None, +🟥 flatten: false, +🟥 hidden: false, +🟩 description: None, +🟩 name: None, +⬛️ nullable: false, +🟥 optional: false, +🟥 read_only: false, +🟥 write_only: false, +🟩 ty: Struct( +🟩 StructType { +🟩 fields: { +🟩 "b": SchemaField { +🟩 aliases: [], +🟩 comment: None, +🟩 schema: Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: true, +🟩 ty: Union( +🟩 UnionType { +🟩 default_index: None, +🟩 partial: false, +🟩 operator: AnyOf, +🟩 variants_types: [ +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: Some( +🟩 "PartialEmpty", +🟩 ), +🟩 nullable: false, +🟩 ty: Struct( +🟩 StructType { +🟩 fields: {}, +🟩 partial: true, +🟩 required: None, +🟩 }, +🟩 ), +🟩 }, +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: false, +🟩 ty: Null, +🟩 }, +🟩 ], +🟩 }, +🟩 ), +🟩 }, +🟩 deprecated: None, +🟩 env_var: None, +🟩 flatten: false, +🟩 hidden: false, +🟩 nullable: true, +🟩 optional: true, +🟩 read_only: false, +🟩 write_only: false, 🟩 }, -🟩 ), +🟩 }, +🟩 partial: true, +🟩 required: None, 🟩 }, -🟩 deprecated: None, -🟩 env_var: None, -🟩 flatten: false, -🟩 hidden: false, -🟩 nullable: false, -🟩 optional: false, -🟩 read_only: false, -🟩 write_only: false, -🟩 }, +🟩 ), ⬛️ }, -⬛️ ), +🟥 }, +🟥 partial: true, +🟥 required: None, +🟩 ], ⬛️ }, -🟥 deprecated: None, -🟥 env_var: None, -🟥 flatten: false, -🟥 hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟥 read_only: false, -🟥 write_only: false, -🟥 }, -🟩 ), -🟩 }, -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, +⬛️ ), +⬛️ }, +⬛️ Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +🟥 ty: Struct( +🟥 StructType { +🟥 fields: { +🟥 "b": SchemaField { +🟥 aliases: [], +🟥 comment: None, +🟥 schema: Schema { +🟥 deprecated: None, +🟥 description: None, +🟥 name: Some( +🟥 "Empty", +🟥 ), +🟥 nullable: false, +🟥 ty: Struct( +🟥 StructType { +🟥 fields: {}, +🟥 partial: true, +🟥 required: None, +🟥 }, +🟥 ), +🟥 }, +🟥 deprecated: None, +🟥 env_var: None, +🟥 flatten: false, +🟥 hidden: false, +🟥 nullable: false, +🟥 optional: false, +🟥 read_only: false, +🟥 write_only: false, +🟥 }, +🟥 }, +🟥 partial: true, +🟥 required: None, +🟥 }, +🟥 ), 🟩 ty: Null, ⬛️ }, -🟥 ), -🟩 ], +⬛️ ], ⬛️ }, ⬛️ ), ⬛️ }, @@ -1174,126 +1096,183 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "unitOpt": SchemaField { +⬛️ "tupleOpt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: None, -🟥 nullable: false, -🟩 nullable: true, +⬛️ nullable: true, ⬛️ ty: Union( ⬛️ UnionType { ⬛️ default_index: None, -⬛️ partial: false, +⬛️ partial: true, ⬛️ operator: AnyOf, ⬛️ variants_types: [ ⬛️ Schema { ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: Some( -⬛️ "UnitEnum", +🟥 "TupleEnum", +🟩 "PartialTupleEnum", ⬛️ ), ⬛️ nullable: false, -⬛️ ty: Enum( -⬛️ EnumType { -⬛️ default_index: Some( -⬛️ 0, -⬛️ ), -⬛️ values: [ -⬛️ String( -⬛️ "a", -⬛️ ), -⬛️ String( -⬛️ "b", -⬛️ ), -⬛️ String( -⬛️ "c", -⬛️ ), -⬛️ ], -⬛️ variants: Some( -⬛️ { -⬛️ "A": SchemaField { -⬛️ aliases: [], -⬛️ comment: None, -⬛️ schema: Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: Literal( -⬛️ LiteralType { -⬛️ format: None, -⬛️ value: String( -⬛️ "a", -⬛️ ), -⬛️ }, -⬛️ ), -⬛️ }, -⬛️ deprecated: None, -⬛️ env_var: None, -⬛️ flatten: false, -⬛️ hidden: false, -⬛️ nullable: false, -⬛️ optional: false, -⬛️ read_only: false, -⬛️ write_only: false, -⬛️ }, -⬛️ "B": SchemaField { -⬛️ aliases: [], -⬛️ comment: None, -⬛️ schema: Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: Literal( -⬛️ LiteralType { -⬛️ format: None, -⬛️ value: String( -⬛️ "b", -⬛️ ), +⬛️ ty: Union( +⬛️ UnionType { +⬛️ default_index: None, +⬛️ partial: true, +⬛️ operator: AnyOf, +⬛️ variants_types: [ +⬛️ Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +⬛️ ty: Struct( +⬛️ StructType { +⬛️ fields: { +⬛️ "a": SchemaField { +⬛️ aliases: [], +⬛️ comment: None, +⬛️ schema: Schema { +⬛️ deprecated: None, +⬛️ description: None, +🟥 name: Some( +🟥 "Empty", +🟥 ), +🟥 nullable: false, +🟥 ty: Struct( +🟥 StructType { +🟥 fields: {}, +🟥 partial: true, +🟥 required: None, +🟩 name: None, +🟩 nullable: true, +🟩 ty: Union( +🟩 UnionType { +🟩 default_index: None, +🟩 partial: false, +🟩 operator: AnyOf, +🟩 variants_types: [ +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: Some( +🟩 "PartialEmpty", +🟩 ), +🟩 nullable: false, +🟩 ty: Struct( +🟩 StructType { +🟩 fields: {}, +🟩 partial: true, +🟩 required: None, +🟩 }, +🟩 ), +🟩 }, +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: false, +🟩 ty: Null, +🟩 }, +🟩 ], +⬛️ }, +⬛️ ), +⬛️ }, +⬛️ deprecated: None, +⬛️ env_var: None, +⬛️ flatten: false, +⬛️ hidden: false, +🟥 nullable: false, +🟥 optional: false, +🟩 nullable: true, +🟩 optional: true, +⬛️ read_only: false, +⬛️ write_only: false, ⬛️ }, -⬛️ ), +⬛️ }, +⬛️ partial: true, +⬛️ required: None, ⬛️ }, -⬛️ deprecated: None, -⬛️ env_var: None, -⬛️ flatten: false, -⬛️ hidden: false, -⬛️ nullable: false, -⬛️ optional: false, -⬛️ read_only: false, -⬛️ write_only: false, -⬛️ }, -⬛️ "C": SchemaField { -⬛️ aliases: [], -⬛️ comment: None, -⬛️ schema: Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: Literal( -⬛️ LiteralType { -⬛️ format: None, -⬛️ value: String( -⬛️ "c", -⬛️ ), +⬛️ ), +⬛️ }, +⬛️ Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +⬛️ ty: Struct( +⬛️ StructType { +⬛️ fields: { +⬛️ "b": SchemaField { +⬛️ aliases: [], +⬛️ comment: None, +⬛️ schema: Schema { +⬛️ deprecated: None, +⬛️ description: None, +🟥 name: Some( +🟥 "Empty", +🟥 ), +🟥 nullable: false, +🟥 ty: Struct( +🟥 StructType { +🟥 fields: {}, +🟥 partial: true, +🟥 required: None, +🟩 name: None, +🟩 nullable: true, +🟩 ty: Union( +🟩 UnionType { +🟩 default_index: None, +🟩 partial: false, +🟩 operator: AnyOf, +🟩 variants_types: [ +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: Some( +🟩 "PartialEmpty", +🟩 ), +🟩 nullable: false, +🟩 ty: Struct( +🟩 StructType { +🟩 fields: {}, +🟩 partial: true, +🟩 required: None, +🟩 }, +🟩 ), +🟩 }, +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: false, +🟩 ty: Null, +🟩 }, +🟩 ], +⬛️ }, +⬛️ ), +⬛️ }, +⬛️ deprecated: None, +⬛️ env_var: None, +⬛️ flatten: false, +⬛️ hidden: false, +🟥 nullable: false, +🟥 optional: false, +🟩 nullable: true, +🟩 optional: true, +⬛️ read_only: false, +⬛️ write_only: false, ⬛️ }, -⬛️ ), +⬛️ }, +⬛️ partial: true, +⬛️ required: None, ⬛️ }, -⬛️ deprecated: None, -⬛️ env_var: None, -⬛️ flatten: false, -⬛️ hidden: false, -⬛️ nullable: false, -⬛️ optional: false, -⬛️ read_only: false, -⬛️ write_only: false, -⬛️ }, +⬛️ ), ⬛️ }, -⬛️ ), +⬛️ ], ⬛️ }, ⬛️ ), ⬛️ }, diff --git a/crates/schematic/tests/snapshots/partialize_test__nested.snap b/crates/schematic/tests/snapshots/partialize_test__nested.snap index 9b514c47..28834667 100644 --- a/crates/schematic/tests/snapshots/partialize_test__nested.snap +++ b/crates/schematic/tests/snapshots/partialize_test__nested.snap @@ -156,8 +156,7 @@ expression: "create_diff::()" ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: None, -🟥 nullable: false, -🟩 nullable: true, +⬛️ nullable: true, ⬛️ ty: Union( ⬛️ UnionType { ⬛️ default_index: None, diff --git a/crates/schematic/tests/snapshots/partialize_test__nested_list.snap b/crates/schematic/tests/snapshots/partialize_test__nested_list.snap index 0dd01e03..9609d301 100644 --- a/crates/schematic/tests/snapshots/partialize_test__nested_list.snap +++ b/crates/schematic/tests/snapshots/partialize_test__nested_list.snap @@ -140,9 +140,7 @@ expression: "create_diff::()" 🟩 }, 🟩 ), ⬛️ }, -🟩 max_contains: None, 🟩 max_length: None, -🟩 min_contains: None, 🟩 min_length: None, 🟩 unique: None, ⬛️ }, @@ -151,9 +149,7 @@ expression: "create_diff::()" 🟥 }, 🟥 ), 🟥 }, -🟥 max_contains: None, 🟥 max_length: None, -🟥 min_contains: None, 🟥 min_length: None, 🟥 unique: None, 🟩 ), @@ -187,8 +183,7 @@ expression: "create_diff::()" ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: None, -🟥 nullable: false, -🟩 nullable: true, +⬛️ nullable: true, ⬛️ ty: Union( ⬛️ UnionType { ⬛️ default_index: None, @@ -281,9 +276,7 @@ expression: "create_diff::()" ⬛️ }, ⬛️ ), ⬛️ }, -⬛️ max_contains: None, ⬛️ max_length: None, -⬛️ min_contains: None, ⬛️ min_length: None, ⬛️ unique: None, ⬛️ }, diff --git a/crates/schematic/tests/snapshots/partialize_test__nested_map.snap b/crates/schematic/tests/snapshots/partialize_test__nested_map.snap index eb74599c..14c2c6bc 100644 --- a/crates/schematic/tests/snapshots/partialize_test__nested_map.snap +++ b/crates/schematic/tests/snapshots/partialize_test__nested_map.snap @@ -86,10 +86,16 @@ expression: "create_diff::()" 🟩 ty: Object( 🟩 ObjectType { 🟩 key_type: Schema { -🟩 deprecated: None, +⬛️ deprecated: None, +🟥 env_var: None, +🟥 flatten: false, +🟥 hidden: false, 🟩 description: None, 🟩 name: None, -🟩 nullable: false, +⬛️ nullable: false, +🟥 optional: false, +🟥 read_only: false, +🟥 write_only: false, 🟩 ty: String( 🟩 StringType { 🟩 default: None, @@ -105,18 +111,12 @@ expression: "create_diff::()" 🟩 min_length: None, 🟩 required: None, 🟩 value_type: Schema { -⬛️ deprecated: None, -🟥 env_var: None, -🟥 flatten: false, -🟥 hidden: false, +🟩 deprecated: None, 🟩 description: None, 🟩 name: Some( 🟩 "PartialBasic", 🟩 ), -⬛️ nullable: false, -🟥 optional: false, -🟥 read_only: false, -🟥 write_only: false, +🟩 nullable: false, 🟩 ty: Struct( 🟩 StructType { 🟩 fields: { @@ -213,8 +213,7 @@ expression: "create_diff::()" ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: None, -🟥 nullable: false, -🟩 nullable: true, +⬛️ nullable: true, ⬛️ ty: Union( ⬛️ UnionType { ⬛️ default_index: None, diff --git a/crates/schematic/tests/snapshots/partialize_test__primitives.snap b/crates/schematic/tests/snapshots/partialize_test__primitives.snap index fc2c9a15..f0277f23 100644 --- a/crates/schematic/tests/snapshots/partialize_test__primitives.snap +++ b/crates/schematic/tests/snapshots/partialize_test__primitives.snap @@ -13,7 +13,7 @@ expression: "create_diff::()" ⬛️ ty: Struct( ⬛️ StructType { ⬛️ fields: { -⬛️ "boolean": SchemaField { +⬛️ "string": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -21,9 +21,14 @@ expression: "create_diff::()" ⬛️ description: None, ⬛️ name: None, 🟥 nullable: false, -🟥 ty: Boolean( -🟥 BooleanType { +🟥 ty: String( +🟥 StringType { 🟥 default: None, +🟥 enum_values: None, +🟥 format: None, +🟥 max_length: None, +🟥 min_length: None, +🟥 pattern: None, 🟩 nullable: true, 🟩 ty: Union( 🟩 UnionType { @@ -36,9 +41,14 @@ expression: "create_diff::()" 🟩 description: None, 🟩 name: None, 🟩 nullable: false, -🟩 ty: Boolean( -🟩 BooleanType { +🟩 ty: String( +🟩 StringType { 🟩 default: None, +🟩 enum_values: None, +🟩 format: None, +🟩 max_length: None, +🟩 min_length: None, +🟩 pattern: None, 🟩 }, 🟩 ), 🟩 }, @@ -64,54 +74,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "booleanOpt": SchemaField { -⬛️ aliases: [], -⬛️ comment: None, -⬛️ schema: Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -🟥 nullable: false, -🟩 nullable: true, -⬛️ ty: Union( -⬛️ UnionType { -⬛️ default_index: None, -⬛️ partial: false, -⬛️ operator: AnyOf, -⬛️ variants_types: [ -⬛️ Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: Boolean( -⬛️ BooleanType { -⬛️ default: None, -⬛️ }, -⬛️ ), -⬛️ }, -⬛️ Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: Null, -⬛️ }, -⬛️ ], -⬛️ }, -⬛️ ), -⬛️ }, -⬛️ deprecated: None, -⬛️ env_var: None, -⬛️ flatten: false, -⬛️ hidden: false, -⬛️ nullable: true, -🟥 optional: false, -🟩 optional: true, -⬛️ read_only: false, -⬛️ write_only: false, -⬛️ }, -⬛️ "float": SchemaField { +⬛️ "number": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -119,18 +82,17 @@ expression: "create_diff::()" ⬛️ description: None, ⬛️ name: None, 🟥 nullable: false, -🟥 ty: Float( -🟥 FloatType { +🟥 ty: Integer( +🟥 IntegerType { 🟥 default: None, 🟥 enum_values: None, 🟥 format: None, -🟥 kind: F32, +🟥 kind: Usize, 🟥 max: None, 🟥 max_exclusive: None, 🟥 min: None, 🟥 min_exclusive: None, 🟥 multiple_of: None, -🟥 name: None, 🟩 nullable: true, 🟩 ty: Union( 🟩 UnionType { @@ -143,18 +105,17 @@ expression: "create_diff::()" 🟩 description: None, 🟩 name: None, 🟩 nullable: false, -🟩 ty: Float( -🟩 FloatType { +🟩 ty: Integer( +🟩 IntegerType { 🟩 default: None, 🟩 enum_values: None, 🟩 format: None, -🟩 kind: F32, +🟩 kind: Usize, 🟩 max: None, 🟩 max_exclusive: None, 🟩 min: None, 🟩 min_exclusive: None, 🟩 multiple_of: None, -🟩 name: None, 🟩 }, 🟩 ), 🟩 }, @@ -180,7 +141,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "floatOpt": SchemaField { +⬛️ "boolean": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -188,41 +149,35 @@ expression: "create_diff::()" ⬛️ description: None, ⬛️ name: None, 🟥 nullable: false, +🟥 ty: Boolean( +🟥 BooleanType { +🟥 default: None, 🟩 nullable: true, -⬛️ ty: Union( -⬛️ UnionType { -⬛️ default_index: None, -⬛️ partial: false, -⬛️ operator: AnyOf, -⬛️ variants_types: [ -⬛️ Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: Float( -⬛️ FloatType { -⬛️ default: None, -⬛️ enum_values: None, -⬛️ format: None, -⬛️ kind: F64, -⬛️ max: None, -⬛️ max_exclusive: None, -⬛️ min: None, -⬛️ min_exclusive: None, -⬛️ multiple_of: None, -⬛️ name: None, -⬛️ }, -⬛️ ), -⬛️ }, -⬛️ Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: Null, -⬛️ }, -⬛️ ], +🟩 ty: Union( +🟩 UnionType { +🟩 default_index: None, +🟩 partial: false, +🟩 operator: AnyOf, +🟩 variants_types: [ +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: false, +🟩 ty: Boolean( +🟩 BooleanType { +🟩 default: None, +🟩 }, +🟩 ), +🟩 }, +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: false, +🟩 ty: Null, +🟩 }, +🟩 ], ⬛️ }, ⬛️ ), ⬛️ }, @@ -230,13 +185,14 @@ expression: "create_diff::()" ⬛️ env_var: None, ⬛️ flatten: false, ⬛️ hidden: false, -⬛️ nullable: true, +🟥 nullable: false, 🟥 optional: false, +🟩 nullable: true, 🟩 optional: true, ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "number": SchemaField { +⬛️ "float": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -244,12 +200,12 @@ expression: "create_diff::()" ⬛️ description: None, ⬛️ name: None, 🟥 nullable: false, -🟥 ty: Integer( -🟥 IntegerType { +🟥 ty: Float( +🟥 FloatType { 🟥 default: None, 🟥 enum_values: None, 🟥 format: None, -🟥 kind: Usize, +🟥 kind: F32, 🟥 max: None, 🟥 max_exclusive: None, 🟥 min: None, @@ -267,12 +223,12 @@ expression: "create_diff::()" 🟩 description: None, 🟩 name: None, 🟩 nullable: false, -🟩 ty: Integer( -🟩 IntegerType { +🟩 ty: Float( +🟩 FloatType { 🟩 default: None, 🟩 enum_values: None, 🟩 format: None, -🟩 kind: Usize, +🟩 kind: F32, 🟩 max: None, 🟩 max_exclusive: None, 🟩 min: None, @@ -303,6 +259,57 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, +⬛️ "stringOpt": SchemaField { +⬛️ aliases: [], +⬛️ comment: None, +⬛️ schema: Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: true, +⬛️ ty: Union( +⬛️ UnionType { +⬛️ default_index: None, +⬛️ partial: false, +⬛️ operator: AnyOf, +⬛️ variants_types: [ +⬛️ Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +⬛️ ty: String( +⬛️ StringType { +⬛️ default: None, +⬛️ enum_values: None, +⬛️ format: None, +⬛️ max_length: None, +⬛️ min_length: None, +⬛️ pattern: None, +⬛️ }, +⬛️ ), +⬛️ }, +⬛️ Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +⬛️ ty: Null, +⬛️ }, +⬛️ ], +⬛️ }, +⬛️ ), +⬛️ }, +⬛️ deprecated: None, +⬛️ env_var: None, +⬛️ flatten: false, +⬛️ hidden: false, +⬛️ nullable: true, +🟥 optional: false, +🟩 optional: true, +⬛️ read_only: false, +⬛️ write_only: false, +⬛️ }, ⬛️ "numberOpt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, @@ -310,8 +317,7 @@ expression: "create_diff::()" ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: None, -🟥 nullable: false, -🟩 nullable: true, +⬛️ nullable: true, ⬛️ ty: Union( ⬛️ UnionType { ⬛️ default_index: None, @@ -358,53 +364,39 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "string": SchemaField { +⬛️ "booleanOpt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: None, -🟥 nullable: false, -🟥 ty: String( -🟥 StringType { -🟥 default: None, -🟥 enum_values: None, -🟥 format: None, -🟥 max_length: None, -🟥 min_length: None, -🟥 pattern: None, -🟩 nullable: true, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, -🟩 partial: false, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: String( -🟩 StringType { -🟩 default: None, -🟩 enum_values: None, -🟩 format: None, -🟩 max_length: None, -🟩 min_length: None, -🟩 pattern: None, -🟩 }, -🟩 ), -🟩 }, -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Null, -🟩 }, -🟩 ], +⬛️ nullable: true, +⬛️ ty: Union( +⬛️ UnionType { +⬛️ default_index: None, +⬛️ partial: false, +⬛️ operator: AnyOf, +⬛️ variants_types: [ +⬛️ Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +⬛️ ty: Boolean( +⬛️ BooleanType { +⬛️ default: None, +⬛️ }, +⬛️ ), +⬛️ }, +⬛️ Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +⬛️ ty: Null, +⬛️ }, +⬛️ ], ⬛️ }, ⬛️ ), ⬛️ }, @@ -412,22 +404,20 @@ expression: "create_diff::()" ⬛️ env_var: None, ⬛️ flatten: false, ⬛️ hidden: false, -🟥 nullable: false, +⬛️ nullable: true, 🟥 optional: false, -🟩 nullable: true, 🟩 optional: true, ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "stringOpt": SchemaField { +⬛️ "floatOpt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { ⬛️ deprecated: None, ⬛️ description: None, ⬛️ name: None, -🟥 nullable: false, -🟩 nullable: true, +⬛️ nullable: true, ⬛️ ty: Union( ⬛️ UnionType { ⬛️ default_index: None, @@ -439,14 +429,17 @@ expression: "create_diff::()" ⬛️ description: None, ⬛️ name: None, ⬛️ nullable: false, -⬛️ ty: String( -⬛️ StringType { +⬛️ ty: Float( +⬛️ FloatType { ⬛️ default: None, ⬛️ enum_values: None, ⬛️ format: None, -⬛️ max_length: None, -⬛️ min_length: None, -⬛️ pattern: None, +⬛️ kind: F64, +⬛️ max: None, +⬛️ max_exclusive: None, +⬛️ min: None, +⬛️ min_exclusive: None, +⬛️ multiple_of: None, ⬛️ }, ⬛️ ), ⬛️ }, diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index 69ff4a74..f5d08c6b 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -52,7 +52,7 @@ regex = ["dep:regex"] relative_path = ["dep:relative-path"] rust_decimal = ["dep:rust_decimal"] semver = ["dep:semver"] -serde = ["dep:serde"] +serde = ["dep:serde", "indexmap/serde"] uuid = ["dep:uuid"] serde_json = ["dep:serde_json"] serde_ron = ["dep:ron"] diff --git a/crates/types/src/arrays.rs b/crates/types/src/arrays.rs index 91ec19aa..435b6885 100644 --- a/crates/types/src/arrays.rs +++ b/crates/types/src/arrays.rs @@ -1,36 +1,26 @@ use crate::*; -use std::collections::{BTreeSet, HashSet}; +use std::collections::{BTreeSet, BinaryHeap, HashSet, LinkedList, VecDeque}; use std::fmt; #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct ArrayType { + /// A schema that at least one item must match. Unlike `items_type`, which + /// constrains every item, this constrains the array as a whole. #[cfg_attr( feature = "serde", serde(default, skip_serializing_if = "Option::is_none") )] - pub contains: Option, + pub contains: Option>, pub items_type: Box, - #[cfg_attr( - feature = "serde", - serde(default, skip_serializing_if = "Option::is_none") - )] - pub max_contains: Option, - #[cfg_attr( feature = "serde", serde(default, skip_serializing_if = "Option::is_none") )] pub max_length: Option, - #[cfg_attr( - feature = "serde", - serde(default, skip_serializing_if = "Option::is_none") - )] - pub min_contains: Option, - #[cfg_attr( feature = "serde", serde(default, skip_serializing_if = "Option::is_none") @@ -66,6 +56,12 @@ impl Schematic for Vec { } } +impl Schematic for [T] { + fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.array(ArrayType::new(schema.infer::())) + } +} + impl Schematic for &[T] { fn build_schema(mut schema: SchemaBuilder) -> Schema { schema.array(ArrayType::new(schema.infer::())) @@ -102,3 +98,21 @@ impl Schematic for BTreeSet { }) } } + +impl Schematic for VecDeque { + fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.array(ArrayType::new(schema.infer::())) + } +} + +impl Schematic for LinkedList { + fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.array(ArrayType::new(schema.infer::())) + } +} + +impl Schematic for BinaryHeap { + fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.array(ArrayType::new(schema.infer::())) + } +} diff --git a/crates/types/src/enums.rs b/crates/types/src/enums.rs index b467efbb..0d09d3a2 100644 --- a/crates/types/src/enums.rs +++ b/crates/types/src/enums.rs @@ -5,14 +5,21 @@ use std::fmt; #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct EnumType { + /// Position of the default entry. Indexes `variants` when variants are + /// present, otherwise `values`. #[cfg_attr( feature = "serde", serde(default, skip_serializing_if = "Option::is_none") )] pub default_index: Option, + /// The literal values of this enum. When `variants` is set, this is a + /// *derived subset* of it — variants that carry a non-literal schema + /// (a null, say) contribute no value, so the two can differ in length. + /// Never index this with `default_index`; use [`Self::get_default`]. pub values: Vec, + /// Every variant, in declaration order, keyed by name. #[cfg_attr( feature = "serde", serde(default, skip_serializing_if = "Option::is_none") @@ -32,6 +39,42 @@ impl EnumType { } } + /// Return the default value, resolving `default_index` against whichever + /// list it indexes. Returns `None` when the default entry carries no + /// literal value. + pub fn get_default(&self) -> Option<&LiteralValue> { + let index = self.default_index?; + + let Some(variants) = &self.variants else { + return self.values.get(index); + }; + + match &variants.get_index(index)?.1.schema.ty { + SchemaType::Literal(lit) => Some(&lit.value), + _ => None, + } + } + + /// Point `default_index` at the entry holding this value. Returns false + /// and keeps any existing default when no entry matches, as an enum can + /// only default to a value it declares. + pub fn set_default(&mut self, default: LiteralValue) -> bool { + let index = match &self.variants { + Some(variants) => variants.values().position(|variant| { + matches!(&variant.schema.ty, SchemaType::Literal(lit) if lit.value == default) + }), + None => self.values.iter().position(|value| *value == default), + }; + + if index.is_none() { + return false; + } + + self.default_index = index; + + true + } + #[doc(hidden)] pub fn from_schemas(schemas: I, default_index: Option) -> Self where @@ -45,10 +88,12 @@ impl EnumType { values.push(lit.value.clone()); } - variants.insert( - schema.name.take().unwrap(), - Box::new(SchemaField::new(schema)), - ); + let name = schema + .name + .take() + .expect("Enum variant schemas require a name, as variants are keyed by it."); + + variants.insert(name, Box::new(SchemaField::new(schema))); } EnumType { diff --git a/crates/types/src/externals.rs b/crates/types/src/externals.rs index bb977d22..17388d3c 100644 --- a/crates/types/src/externals.rs +++ b/crates/types/src/externals.rs @@ -139,6 +139,13 @@ mod serde_json_feature { } } +#[cfg(feature = "serde_ron")] +mod serde_ron_feature { + use super::*; + + impl_unknown!(ron::Value); +} + #[cfg(feature = "serde_rpkl")] mod serde_rpkl_feature { use super::*; diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index f669d902..075fdc61 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -66,19 +66,19 @@ impl Schematic for &mut T { } } -impl Schematic for Box { +impl Schematic for Box { fn build_schema(schema: SchemaBuilder) -> Schema { T::build_schema(schema) } } -impl Schematic for Rc { +impl Schematic for Rc { fn build_schema(schema: SchemaBuilder) -> Schema { T::build_schema(schema) } } -impl Schematic for Arc { +impl Schematic for Arc { fn build_schema(schema: SchemaBuilder) -> Schema { T::build_schema(schema) } @@ -92,6 +92,6 @@ impl Schematic for Cow<'_, T> { impl Schematic for Option { fn build_schema(mut schema: SchemaBuilder) -> Schema { - schema.union(UnionType::new_any([schema.infer::(), Schema::null()])) + schema.nullable(schema.infer::()) } } diff --git a/crates/types/src/literals.rs b/crates/types/src/literals.rs index 47f099bd..1d99bee4 100644 --- a/crates/types/src/literals.rs +++ b/crates/types/src/literals.rs @@ -1,6 +1,6 @@ use std::fmt; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(tag = "type", content = "value"))] pub enum LiteralValue { @@ -12,6 +12,23 @@ pub enum LiteralValue { String(String), } +// Hand written rather than derived so that a schema always equals itself. +// Deriving it inherits `f32`/`f64` equality, under which `NaN != NaN`, and a +// schema holding a NaN default then compares unequal to its own clone. +impl PartialEq for LiteralValue { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Bool(a), Self::Bool(b)) => a == b, + (Self::F32(a), Self::F32(b)) => a == b || (a.is_nan() && b.is_nan()), + (Self::F64(a), Self::F64(b)) => a == b || (a.is_nan() && b.is_nan()), + (Self::Int(a), Self::Int(b)) => a == b, + (Self::UInt(a), Self::UInt(b)) => a == b, + (Self::String(a), Self::String(b)) => a == b, + _ => false, + } + } +} + impl fmt::Display for LiteralValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( @@ -32,22 +49,13 @@ impl fmt::Display for LiteralValue { #[derive(Clone, Debug, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct LiteralType { - #[cfg_attr( - feature = "serde", - serde(default, skip_serializing_if = "Option::is_none") - )] - pub format: Option, - pub value: LiteralValue, } impl LiteralType { /// Create a literal schema with the provided value. pub fn new(value: LiteralValue) -> Self { - LiteralType { - format: None, - value, - } + LiteralType { value } } } diff --git a/crates/types/src/numbers.rs b/crates/types/src/numbers.rs index f04436eb..b377f780 100644 --- a/crates/types/src/numbers.rs +++ b/crates/types/src/numbers.rs @@ -1,5 +1,9 @@ use crate::*; use std::fmt; +use std::num::{ + NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize, NonZeroU8, + NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128, NonZeroUsize, +}; #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -150,6 +154,38 @@ impl_int!(i32, IntegerKind::I32); impl_int!(i64, IntegerKind::I64); impl_int!(i128, IntegerKind::I128); +// Serializes as a plain number, so the only difference from the sized +// integer is the excluded zero. +macro_rules! impl_non_zero_int { + ($type:ty, $kind:expr_2021) => { + impl Schematic for $type { + fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.integer(IntegerType { + // Unsigned starts at 1; signed cannot express "not zero" + // as a bound, so it stays unconstrained. + min: if $kind.is_unsigned() { Some(1) } else { None }, + kind: $kind, + ..IntegerType::default() + }) + } + } + }; +} + +impl_non_zero_int!(NonZeroUsize, IntegerKind::Usize); +impl_non_zero_int!(NonZeroU8, IntegerKind::U8); +impl_non_zero_int!(NonZeroU16, IntegerKind::U16); +impl_non_zero_int!(NonZeroU32, IntegerKind::U32); +impl_non_zero_int!(NonZeroU64, IntegerKind::U64); +impl_non_zero_int!(NonZeroU128, IntegerKind::U128); + +impl_non_zero_int!(NonZeroIsize, IntegerKind::Isize); +impl_non_zero_int!(NonZeroI8, IntegerKind::I8); +impl_non_zero_int!(NonZeroI16, IntegerKind::I16); +impl_non_zero_int!(NonZeroI32, IntegerKind::I32); +impl_non_zero_int!(NonZeroI64, IntegerKind::I64); +impl_non_zero_int!(NonZeroI128, IntegerKind::I128); + #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub enum FloatKind { @@ -210,12 +246,6 @@ pub struct FloatType { serde(default, skip_serializing_if = "Option::is_none") )] pub multiple_of: Option, - - #[cfg_attr( - feature = "serde", - serde(default, skip_serializing_if = "Option::is_none") - )] - pub name: Option, } impl FloatType { diff --git a/crates/types/src/schema.rs b/crates/types/src/schema.rs index 86435389..11f9450b 100644 --- a/crates/types/src/schema.rs +++ b/crates/types/src/schema.rs @@ -1,6 +1,5 @@ use crate::*; use std::fmt; -use std::ops::{Deref, DerefMut}; /// Describes the metadata and shape of a type. #[derive(Clone, Debug, Default, PartialEq)] @@ -24,7 +23,7 @@ pub struct Schema { )] pub name: Option, - #[cfg_attr(feature = "serde", serde(default, skip_serializing))] + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))] pub nullable: bool, pub ty: SchemaType, @@ -79,6 +78,14 @@ impl Schema { Self::new(SchemaType::Object(Box::new(value))) } + /// Create a schema that references another schema by name. + pub fn reference(name: impl AsRef) -> Self { + Self::new(SchemaType::Reference { + name: name.as_ref().to_owned(), + partial: false, + }) + } + /// Create a string schema. pub fn string(value: StringType) -> Self { Self::new(SchemaType::String(Box::new(value))) @@ -112,13 +119,16 @@ impl Schema { /// Convert the current schema to a nullable type. If already nullable, /// do nothing, otherwise convert to a union. pub fn nullify(&mut self) { - if self.nullable { - // May already be a null union through inference - return; - } + // Inference builds a nullable union without raising the flag, so the + // two can disagree — check both before deciding there's work to do. + let already_nullable = self.nullable || self.ty.is_nullable(); self.nullable = true; + if already_nullable { + return; + } + if let SchemaType::Union(inner) = &mut self.ty { // If the union has an explicit name, then we can assume it's a distinct // type, so we shouldn't add null to it and alter the intended type. @@ -140,12 +150,23 @@ impl Schema { self.ty = SchemaType::Union(Box::new(UnionType::new_any([new_schema, Schema::null()]))); } - /// Mark the inner schema type as partial. Only structs and unions can be marked partial, - /// but arrays and objects will also be recursively set to update the inner type. + /// Mark the inner schema type as partial. Only structs, unions, and references can be + /// marked partial, but arrays, objects, and tuples will also be recursively set to + /// update the inner types. pub fn partialize(&mut self) { match &mut self.ty { SchemaType::Array(inner) => inner.items_type.partialize(), SchemaType::Object(inner) => inner.value_type.partialize(), + SchemaType::Tuple(inner) => { + for item in inner.items_types.iter_mut() { + item.partialize(); + } + } + // A cycle resolves to a reference, so it has to point at the partial + // type once partialized, otherwise the reference dangles. + SchemaType::Reference { partial, .. } => { + *partial = true; + } SchemaType::Struct(inner) => { inner.partial = true; } @@ -190,23 +211,56 @@ impl Schema { self.ty = value; } + /// Return a `default` value from the inner schema type. + pub fn get_default(&self) -> Option<&LiteralValue> { + self.ty.get_default() + } + + /// Set the `default` of the inner schema type. Returns false when the + /// type holds no default. + pub fn set_default(&mut self, default: LiteralValue) -> bool { + self.ty.set_default(default) + } + + /// Add a field to the inner type if it's a struct. Returns false when it + /// isn't one. + pub fn add_field(&mut self, key: &str, value: impl Into) -> bool { + self.ty.add_field(key, value) + } + + /// Return true if the inner type is an explicit null. + pub fn is_null(&self) -> bool { + self.ty.is_null() + } + + /// Return true if the inner type is nullable (a union with a null). + pub fn is_nullable(&self) -> bool { + self.ty.is_nullable() + } + + /// Return true if the inner type is a reference. + pub fn is_reference(&self) -> bool { + self.ty.is_reference() + } + + /// Return true if the inner type is a struct. + pub fn is_struct(&self) -> bool { + self.ty.is_struct() + } + /// Return a non-null schema if available. If a null type, - /// returns `None`. If a union type, returns the first non-null - /// type or `None`. Otherwise, returns the current type. + /// returns `None`. If a union type, returns the first variant that + /// resolves to a non-null type, or `None`. Otherwise, returns the + /// current type. pub fn get_nonnull_schema(&self) -> Option<&Schema> { match &self.ty { SchemaType::Null => None, - SchemaType::Union(inner) => { - for ty in &inner.variants_types { - if ty.is_null() { - continue; - } - - return Some(ty); - } - - None - } + // Resolve each variant rather than taking the first non-null one, + // as a variant may itself be a union that holds nothing but nulls. + SchemaType::Union(inner) => inner + .variants_types + .iter() + .find_map(|variant| variant.get_nonnull_schema()), _ => Some(self), } } @@ -214,28 +268,13 @@ impl Schema { impl fmt::Display for Schema { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.name.is_some() && (self.ty.is_struct() || self.ty.is_reference()) { - write!(f, "{}", self.name.as_ref().unwrap()) - } else { - write!(f, "{}", self.ty) + match &self.name { + Some(name) if self.ty.is_struct() || self.ty.is_reference() => write!(f, "{name}"), + _ => write!(f, "{}", self.ty), } } } -impl Deref for Schema { - type Target = SchemaType; - - fn deref(&self) -> &Self::Target { - &self.ty - } -} - -impl DerefMut for Schema { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.ty - } -} - impl From for SchemaType { fn from(val: Schema) -> Self { val.ty @@ -245,7 +284,7 @@ impl From for SchemaType { impl Schematic for Schema {} #[cfg(feature = "serde")] -fn is_false(value: &bool) -> bool { +pub(crate) fn is_false(value: &bool) -> bool { !value } diff --git a/crates/types/src/schema_builder.rs b/crates/types/src/schema_builder.rs index e8d41f71..7723d5cd 100644 --- a/crates/types/src/schema_builder.rs +++ b/crates/types/src/schema_builder.rs @@ -1,7 +1,6 @@ use crate::*; use std::cell::RefCell; use std::mem; -use std::ops::{Deref, DerefMut}; use std::rc::Rc; /// A [`Schema`] builder. @@ -17,12 +16,12 @@ pub struct SchemaBuilder { impl SchemaBuilder { /// Generate a schema from the provided type. - pub fn generate() -> Schema { + pub fn generate() -> Schema { Self::build_root::() } /// Generate a schema from the provided type. - pub fn build_root() -> Schema { + pub fn build_root() -> Schema { let mut builder = SchemaBuilder::default(); if let Some(name) = T::schema_name() { @@ -136,6 +135,7 @@ impl SchemaBuilder { /// Build a schema that is also nullable (uses a union). pub fn nullable(&mut self, value: impl Into) -> Schema { + self.nullable = true; self.union(UnionType::new_any([value.into(), Schema::null()])) } @@ -170,7 +170,7 @@ impl SchemaBuilder { } /// Infer a [`Schema`] from a type that implements [`Schematic`]. - pub fn infer(&self) -> Schema { + pub fn infer(&self) -> Schema { let mut builder = self.nest(); // No name, so return the schema immediately @@ -181,7 +181,10 @@ impl SchemaBuilder { // If this name has already been used, create a reference // so that we avoid recursion! if self.name_stack.borrow().contains(&name) { - return builder.set_type_and_build(SchemaType::Reference(name)); + return builder.set_type_and_build(SchemaType::Reference { + name, + partial: false, + }); } // Otherwise generate a new schema and persist our name cache @@ -196,7 +199,7 @@ impl SchemaBuilder { /// Infer a [`Schema`] from a type that implements [`Schematic`], /// and mark the schema is partial (is marked as `nested`). - pub fn infer_as_nested(&self) -> Schema { + pub fn infer_as_nested(&self) -> Schema { let mut schema = self.infer::(); schema.partialize(); schema @@ -204,27 +207,13 @@ impl SchemaBuilder { /// Infer a [`Schema`] from a type that implements [`Schematic`], /// and also provide a default literal value. - pub fn infer_with_default(&self, default: LiteralValue) -> Schema { + pub fn infer_with_default(&self, default: LiteralValue) -> Schema { let mut schema = self.infer::(); schema.set_default(default); schema } } -impl Deref for SchemaBuilder { - type Target = SchemaType; - - fn deref(&self) -> &Self::Target { - &self.ty - } -} - -impl DerefMut for SchemaBuilder { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.ty - } -} - impl From for Schema { fn from(mut builder: SchemaBuilder) -> Self { builder.build() diff --git a/crates/types/src/schema_type.rs b/crates/types/src/schema_type.rs index 40c75e59..f1d2abe0 100644 --- a/crates/types/src/schema_type.rs +++ b/crates/types/src/schema_type.rs @@ -27,7 +27,17 @@ pub enum SchemaType { Integer(Box), Literal(Box), Object(Box), - Reference(String), + // A struct variant, and not a newtype, as serde is unable to + // internally tag a newtype whose value is not a map. + Reference { + name: String, + + // The name refers to a partial config type, like `StructType.partial`. + // Set by `Schema::partialize`, consumed when the schema is partialized + // at runtime and the name gains its `Partial` prefix. + #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_false"))] + partial: bool, + }, Struct(Box), String(Box), Tuple(Box), @@ -39,15 +49,7 @@ impl SchemaType { pub fn get_default(&self) -> Option<&LiteralValue> { match self { SchemaType::Boolean(inner) => inner.default.as_ref(), - SchemaType::Enum(inner) => { - if let Some(index) = &inner.default_index - && let Some(value) = inner.values.get(*index) - { - return Some(value); - } - - None - } + SchemaType::Enum(inner) => inner.get_default(), SchemaType::Float(inner) => inner.default.as_ref(), SchemaType::Integer(inner) => inner.default.as_ref(), SchemaType::String(inner) => inner.default.as_ref(), @@ -86,7 +88,7 @@ impl SchemaType { /// Return true if the schema is a reference. pub fn is_reference(&self) -> bool { - matches!(self, SchemaType::Reference(_)) + matches!(self, SchemaType::Reference { .. }) } /// Return true if the schema is a struct. @@ -94,12 +96,20 @@ impl SchemaType { matches!(self, SchemaType::Struct(_)) } - /// Set the `default` of the inner schema type. - pub fn set_default(&mut self, default: LiteralValue) { + /// Set the `default` of the inner schema type. Mirrors [`Self::get_default`], + /// so unions delegate to the variant that would be read back, and enums + /// record the position of the matching value. + /// + /// Returns false when the type holds no default, as most don't — a struct + /// or an array has nowhere to put one. + pub fn set_default(&mut self, default: LiteralValue) -> bool { match self { SchemaType::Boolean(inner) => { inner.default = Some(default); } + SchemaType::Enum(inner) => { + return inner.set_default(default); + } SchemaType::Float(inner) => { inner.default = Some(default); } @@ -109,15 +119,38 @@ impl SchemaType { SchemaType::String(inner) => { inner.default = Some(default); } - _ => {} + // A union itself holds no value, so push the default down into the + // variant it represents. This is how `Option` keeps its default. + SchemaType::Union(inner) => { + let index = inner.default_index.unwrap_or_else(|| { + inner + .variants_types + .iter() + .position(|variant| !variant.is_null()) + .unwrap_or(0) + }); + + return inner + .variants_types + .get_mut(index) + .is_some_and(|variant| variant.set_default(default)); + } + _ => return false, }; + + true } - /// Add a field to the type if it's a struct. - pub fn add_field(&mut self, key: &str, value: impl Into) { - if let SchemaType::Struct(map) = self { - map.fields.insert(key.to_owned(), Box::new(value.into())); - } + /// Add a field to the type if it's a struct. Returns false when it isn't + /// one, as there is nowhere to put the field. + pub fn add_field(&mut self, key: &str, value: impl Into) -> bool { + let SchemaType::Struct(map) = self else { + return false; + }; + + map.fields.insert(key.to_owned(), Box::new(value.into())); + + true } } @@ -144,7 +177,7 @@ impl fmt::Display for SchemaType { Self::Integer(inner) => inner.to_string(), Self::Literal(inner) => inner.to_string(), Self::Object(inner) => inner.to_string(), - Self::Reference(inner) => inner.to_string(), + Self::Reference { name, .. } => name.to_owned(), Self::Struct(inner) => inner.to_string(), Self::String(inner) => inner.to_string(), Self::Tuple(inner) => inner.to_string(), diff --git a/crates/types/src/strings.rs b/crates/types/src/strings.rs index 57e3fe61..1baf2981 100644 --- a/crates/types/src/strings.rs +++ b/crates/types/src/strings.rs @@ -1,8 +1,7 @@ use crate::*; use std::fmt; -use std::net::{Ipv4Addr, Ipv6Addr}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime}; #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] @@ -109,8 +108,10 @@ impl_string_format!(Path, "path"); impl_string_format!(&Path, "path"); impl_string_format!(PathBuf, "path"); +impl_string_format!(IpAddr, "ip"); impl_string_format!(Ipv4Addr, "ipv4"); impl_string_format!(Ipv6Addr, "ipv6"); -impl_string_format!(SystemTime, "time"); -impl_string_format!(Duration, "duration"); +impl_string_format!(SocketAddr, "socket-addr"); +impl_string_format!(SocketAddrV4, "socket-addr"); +impl_string_format!(SocketAddrV6, "socket-addr"); diff --git a/crates/types/src/structs.rs b/crates/types/src/structs.rs index 9b4d2647..ad7c369a 100644 --- a/crates/types/src/structs.rs +++ b/crates/types/src/structs.rs @@ -1,11 +1,14 @@ -use crate::schema::SchemaField; +use crate::*; use std::collections::BTreeMap; use std::fmt; +use std::ops::{Range, RangeInclusive}; +use std::time::{Duration, SystemTime}; #[derive(Clone, Debug, Default, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] pub struct StructType { - pub fields: BTreeMap>, + /// Fields in declaration order, keyed by name. + pub fields: IndexMap>, // The type is a partial nested config, like `PartialConfig`. // This doesn't mean it's been partialized. @@ -37,6 +40,17 @@ impl StructType { pub fn is_hidden(&self) -> bool { self.fields.values().all(|field| field.hidden) } + + /// Return the fields keyed and ordered by name. `fields` itself keeps the + /// order they were declared in, which is what the schema models, but + /// rendered output is alphabetical so that it stays stable regardless of + /// how the source type happens to be written. + pub fn sorted_fields(&self) -> BTreeMap<&String, &SchemaField> { + self.fields + .iter() + .map(|(name, field)| (name, field.as_ref())) + .collect() + } } impl fmt::Display for StructType { @@ -44,3 +58,42 @@ impl fmt::Display for StructType { write!(f, "struct") } } + +// The following types look like scalars but serde encodes them as maps, +// so the schema has to describe the map, not the type's `Display` form. + +impl Schematic for Duration { + fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.structure(StructType::new([ + ("secs".into(), schema.infer::()), + ("nanos".into(), schema.infer::()), + ])) + } +} + +impl Schematic for SystemTime { + fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.structure(StructType::new([ + ("secs_since_epoch".into(), schema.infer::()), + ("nanos_since_epoch".into(), schema.infer::()), + ])) + } +} + +impl Schematic for Range { + fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.structure(StructType::new([ + ("start".into(), schema.infer::()), + ("end".into(), schema.infer::()), + ])) + } +} + +impl Schematic for RangeInclusive { + fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.structure(StructType::new([ + ("start".into(), schema.infer::()), + ("end".into(), schema.infer::()), + ])) + } +} diff --git a/crates/types/tests/builder_test.rs b/crates/types/tests/builder_test.rs index 3255c8ce..26304910 100644 --- a/crates/types/tests/builder_test.rs +++ b/crates/types/tests/builder_test.rs @@ -3,7 +3,8 @@ use schematic_types::*; use starbase_sandbox::assert_snapshot; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use std::net::Ipv4Addr; +use std::net::{Ipv4Addr, SocketAddr}; +use std::ops::Range; use std::path::{Path, PathBuf}; use std::time::Duration; @@ -205,12 +206,47 @@ fn strings() { ); assert_build!( - Duration, + SocketAddr, SchemaType::String(Box::new(StringType { - format: Some("duration".into()), + format: Some("socket-addr".into()), ..StringType::default() })), - "string:duration" + "string:socket-addr" + ); +} + +// Serde encodes these as maps, so the schema describes the map rather than +// the scalar-looking `Display` form. +#[test] +fn map_shaped_scalars() { + assert_build!( + Duration, + SchemaType::Struct(Box::new(StructType::new([ + ( + "secs".into(), + Schema::integer(IntegerType::new_kind(IntegerKind::U64)) + ), + ( + "nanos".into(), + Schema::integer(IntegerType::new_kind(IntegerKind::U32)) + ), + ]))), + "struct" + ); + + assert_build!( + Range, + SchemaType::Struct(Box::new(StructType::new([ + ( + "start".into(), + Schema::integer(IntegerType::new_kind(IntegerKind::Usize)) + ), + ( + "end".into(), + Schema::integer(IntegerType::new_kind(IntegerKind::Usize)) + ), + ]))), + "struct" ); } @@ -288,12 +324,15 @@ fn supports_cycles() { assert_eq!( test_builder::().ty, SchemaType::Struct(Box::new(StructType { - fields: BTreeMap::from_iter([( + fields: IndexMap::from_iter([( "values".into(), Box::new(SchemaField { schema: Schema::object(ObjectType::new( Schema::string(StringType::default()), - SchemaType::Reference("Cycle".into()), + SchemaType::Reference { + name: "Cycle".into(), + partial: false + }, )), ..Default::default() }) diff --git a/crates/types/tests/schema_test.rs b/crates/types/tests/schema_test.rs new file mode 100644 index 00000000..b8714023 --- /dev/null +++ b/crates/types/tests/schema_test.rs @@ -0,0 +1,1091 @@ +#![allow(dead_code)] + +use schematic_types::*; + +mod references { + use super::*; + + // `SchemaType` is internally tagged, which serde cannot apply to a + // newtype variant holding a plain string, so `Reference` is a struct + // variant. Every recursive config depends on this serializing. + #[test] + fn serializes_a_reference() { + let schema = Schema::reference("Foo"); + + let json = serde_json::to_string(&schema).unwrap(); + + assert_eq!(json, r#"{"ty":{"type":"Reference","name":"Foo"}}"#); + assert_eq!(serde_json::from_str::(&json).unwrap(), schema); + } + + #[test] + fn serializes_a_nested_reference() { + let schema = Schema::object(ObjectType::new( + Schema::string(StringType::default()), + Schema::reference("Cycle"), + )); + + let json = serde_json::to_string(&schema).unwrap(); + + assert_eq!(serde_json::from_str::(&json).unwrap(), schema); + } + + pub struct Cycle; + + impl Schematic for Cycle { + fn schema_name() -> Option { + Some("Cycle".into()) + } + + fn build_schema(mut schema: SchemaBuilder) -> Schema { + schema.structure(StructType::new([( + "inner".into(), + schema.infer::>(), + )])) + } + } + + #[test] + fn round_trips_a_cyclic_schema() { + let schema = SchemaBuilder::build_root::(); + let json = serde_json::to_string(&schema).unwrap(); + + assert_eq!(serde_json::from_str::(&json).unwrap(), schema); + } + + #[test] + fn is_reference() { + assert!(Schema::reference("Foo").is_reference()); + assert!(!Schema::null().is_reference()); + } + + #[test] + fn displays_the_name() { + let mut schema = Schema::reference("Foo"); + + assert_eq!(schema.to_string(), "Foo"); + + // A named reference renders its name, not the inner value + schema.set_name("Bar"); + + assert_eq!(schema.to_string(), "Bar"); + } +} + +mod unsized_wrappers { + use super::*; + use std::borrow::Cow; + use std::rc::Rc; + use std::sync::Arc; + + // Partials keep `Box`/`Arc`/`Rc` when the inner type is unsized, so + // those wrappers must accept `?Sized` for the partial to derive a schema. + #[test] + fn builds_through_unsized_wrappers() { + assert_eq!( + SchemaBuilder::build_root::>().ty, + SchemaType::String(Box::default()) + ); + assert_eq!( + SchemaBuilder::build_root::>().ty, + SchemaType::String(Box::default()) + ); + assert_eq!( + SchemaBuilder::build_root::>().ty, + SchemaType::String(Box::default()) + ); + assert_eq!( + SchemaBuilder::build_root::>().ty, + SchemaType::String(Box::default()) + ); + } + + #[test] + fn builds_through_unsized_slices() { + let expected = SchemaType::Array(Box::new(ArrayType::new(Schema::string( + StringType::default(), + )))); + + assert_eq!(SchemaBuilder::build_root::>().ty, expected); + assert_eq!(SchemaBuilder::build_root::>().ty, expected); + assert_eq!(SchemaBuilder::build_root::<[String]>().ty, expected); + assert_eq!(SchemaBuilder::build_root::<&[String]>().ty, expected); + } +} + +mod defaults { + use super::*; + + #[test] + fn sets_and_gets_on_primitives() { + for mut schema in [ + Schema::boolean(BooleanType::default()), + Schema::float(FloatType::new_kind(FloatKind::F64)), + Schema::integer(IntegerType::new_kind(IntegerKind::Usize)), + Schema::string(StringType::default()), + ] { + let value = LiteralValue::String("abc".into()); + + schema.set_default(value.clone()); + + assert_eq!(schema.get_default(), Some(&value)); + } + } + + // `Option` builds a union, which holds no value of its own, so the + // default has to reach the non-null variant. + #[test] + fn sets_and_gets_through_an_option() { + let mut schema = SchemaBuilder::build_root::>(); + let value = LiteralValue::UInt(123); + + schema.set_default(value.clone()); + + assert_eq!(schema.get_default(), Some(&value)); + } + + #[test] + fn sets_through_a_union_honouring_default_index() { + let mut schema = Schema::union(UnionType::from_schemas( + [ + Schema::string(StringType::default()), + Schema::integer(IntegerType::new_kind(IntegerKind::Usize)), + ], + Some(1), + )); + let value = LiteralValue::UInt(7); + + schema.set_default(value.clone()); + + assert_eq!(schema.get_default(), Some(&value)); + + // It landed on the indexed variant, not the first one + let SchemaType::Union(inner) = &schema.ty else { + panic!("expected a union"); + }; + + assert_eq!(inner.variants_types[0].get_default(), None); + assert_eq!(inner.variants_types[1].get_default(), Some(&value)); + } + + #[test] + fn sets_and_gets_on_an_enum() { + let mut schema = Schema::enumerable(EnumType::new([ + LiteralValue::String("a".into()), + LiteralValue::String("b".into()), + ])); + let value = LiteralValue::String("b".into()); + + schema.set_default(value.clone()); + + assert_eq!(schema.get_default(), Some(&value)); + } + + #[test] + fn keeps_an_existing_enum_default_when_the_value_is_unknown() { + let mut schema = Schema::enumerable(EnumType { + default_index: Some(0), + values: vec![LiteralValue::String("a".into())], + variants: None, + }); + + schema.set_default(LiteralValue::String("nope".into())); + + assert_eq!( + schema.get_default(), + Some(&LiteralValue::String("a".into())) + ); + } +} + +mod enum_variants { + use super::*; + + fn named(name: &str, ty: SchemaType) -> Schema { + Schema { + name: Some(name.into()), + ty, + ..Default::default() + } + } + + fn literal(name: &str) -> Schema { + named( + name, + SchemaType::Literal(Box::new(LiteralType::new(LiteralValue::String( + name.into(), + )))), + ) + } + + // A `#[setting(null)]` variant carries no literal, so `values` is + // shorter than `variants` and `default_index` must resolve through + // the variants to stay aligned. + #[test] + fn resolves_the_default_past_a_valueless_variant() { + let ty = EnumType::from_schemas( + [named("C", SchemaType::Null), literal("A"), literal("B")], + Some(2), + ); + + assert_eq!(ty.variants.as_ref().unwrap().len(), 3); + assert_eq!(ty.values.len(), 2); + + assert_eq!( + Schema::enumerable(ty).get_default(), + Some(&LiteralValue::String("B".into())) + ); + } + + #[test] + fn returns_no_default_when_the_variant_has_no_value() { + let ty = EnumType::from_schemas([named("C", SchemaType::Null), literal("A")], Some(0)); + + assert_eq!(Schema::enumerable(ty).get_default(), None); + } + + #[test] + fn resolves_the_default_from_values_without_variants() { + let ty = EnumType { + default_index: Some(1), + values: vec![ + LiteralValue::String("a".into()), + LiteralValue::String("b".into()), + ], + variants: None, + }; + + assert_eq!( + Schema::enumerable(ty).get_default(), + Some(&LiteralValue::String("b".into())) + ); + } + + #[test] + fn sets_the_default_by_variant_value() { + let mut ty = EnumType::from_schemas([literal("A"), literal("B")], None); + + ty.set_default(LiteralValue::String("B".into())); + + assert_eq!(ty.default_index, Some(1)); + } + + #[test] + #[should_panic(expected = "Enum variant schemas require a name")] + fn panics_when_a_variant_is_unnamed() { + EnumType::from_schemas( + [Schema::literal_value(LiteralValue::String("A".into()))], + None, + ); + } +} + +mod nullable { + use super::*; + + #[test] + fn inference_marks_options_as_nullable() { + let schema = SchemaBuilder::build_root::>(); + + assert!(schema.nullable); + assert!(schema.ty.is_nullable()); + } + + #[test] + fn round_trips_through_serde() { + let mut schema = Schema::string(StringType::default()); + schema.nullify(); + + let json = serde_json::to_string(&schema).unwrap(); + + assert!(json.contains(r#""nullable":true"#)); + assert_eq!(serde_json::from_str::(&json).unwrap(), schema); + } + + #[test] + fn omits_the_flag_when_not_nullable() { + let json = serde_json::to_string(&Schema::string(StringType::default())).unwrap(); + + assert!(!json.contains("nullable")); + } + + #[test] + fn nullify_is_idempotent() { + let mut schema = Schema::string(StringType::default()); + + schema.nullify(); + let once = schema.clone(); + + schema.nullify(); + + assert_eq!(schema, once); + } + + // Inference builds a nullable union without raising the flag, so + // `nullify` has to inspect the type, not just the flag. + #[test] + fn nullify_does_not_re_wrap_an_inferred_option() { + let mut schema = SchemaBuilder::build_root::>(); + let before = schema.clone(); + + schema.nullify(); + + assert_eq!(schema, before); + } + + #[test] + fn nullify_does_not_re_wrap_a_named_nullable_union() { + let mut schema = Schema::union(UnionType::new_any([ + Schema::string(StringType::default()), + Schema::null(), + ])); + schema.set_name("MyUnion"); + + schema.nullify(); + + let SchemaType::Union(inner) = &schema.ty else { + panic!("expected a union"); + }; + + assert_eq!(inner.variants_types.len(), 2); + assert!(!inner.variants_types[0].ty.is_nullable()); + } + + #[test] + fn nullify_wraps_a_named_non_null_union() { + let mut schema = Schema::union(UnionType::new_any([ + Schema::string(StringType::default()), + Schema::integer(IntegerType::new_kind(IntegerKind::Usize)), + ])); + schema.set_name("MyUnion"); + + schema.nullify(); + + let SchemaType::Union(inner) = &schema.ty else { + panic!("expected a union"); + }; + + // The named union is preserved as a distinct variant + assert_eq!(inner.variants_types.len(), 2); + assert_eq!(inner.variants_types[0].name.as_deref(), Some("MyUnion")); + assert!(inner.variants_types[1].is_null()); + } + + #[test] + fn nullify_adds_null_to_an_unnamed_union() { + let mut schema = Schema::union(UnionType::new_any([ + Schema::string(StringType::default()), + Schema::integer(IntegerType::new_kind(IntegerKind::Usize)), + ])); + + schema.nullify(); + + let SchemaType::Union(inner) = &schema.ty else { + panic!("expected a union"); + }; + + assert_eq!(inner.variants_types.len(), 3); + assert!(inner.has_null()); + } +} + +mod partialize { + use super::*; + + fn structure() -> Schema { + Schema::structure(StructType::default()) + } + + fn is_partial(schema: &Schema) -> bool { + match &schema.ty { + SchemaType::Struct(inner) => inner.partial, + SchemaType::Union(inner) => inner.partial, + SchemaType::Reference { partial, .. } => *partial, + _ => false, + } + } + + #[test] + fn marks_a_struct() { + let mut schema = structure(); + schema.partialize(); + + assert!(is_partial(&schema)); + } + + #[test] + fn recurses_into_arrays() { + let mut schema = Schema::array(ArrayType::new(structure())); + schema.partialize(); + + let SchemaType::Array(inner) = &schema.ty else { + panic!("expected an array"); + }; + + assert!(is_partial(&inner.items_type)); + } + + #[test] + fn recurses_into_object_values() { + let mut schema = Schema::object(ObjectType::new( + Schema::string(StringType::default()), + structure(), + )); + schema.partialize(); + + let SchemaType::Object(inner) = &schema.ty else { + panic!("expected an object"); + }; + + assert!(is_partial(&inner.value_type)); + // Keys are always scalars, so they are left alone + assert!(!is_partial(&inner.key_type)); + } + + #[test] + fn recurses_into_tuples() { + let mut schema = Schema::tuple(TupleType::new([structure(), structure()])); + schema.partialize(); + + let SchemaType::Tuple(inner) = &schema.ty else { + panic!("expected a tuple"); + }; + + assert!(inner.items_types.iter().all(|item| is_partial(item))); + } + + // A cycle resolves to a reference, which must point at the partial type + // once partialized, or it names a type that was never rendered. + #[test] + fn marks_a_reference() { + let mut schema = Schema::reference("Nested"); + schema.partialize(); + + assert!(is_partial(&schema)); + } + + #[test] + fn marks_a_reference_within_a_collection() { + let mut schema = Schema::array(ArrayType::new(Schema::reference("Nested"))); + schema.partialize(); + + let SchemaType::Array(inner) = &schema.ty else { + panic!("expected an array"); + }; + + assert!(is_partial(&inner.items_type)); + } + + #[test] + fn recurses_into_nullable_unions() { + let mut schema = Schema::union(UnionType::new_any([structure(), Schema::null()])); + schema.partialize(); + + let SchemaType::Union(inner) = &schema.ty else { + panic!("expected a union"); + }; + + assert!(inner.partial); + assert!(is_partial(&inner.variants_types[0])); + } + + #[test] + fn round_trips_the_reference_flag() { + let mut schema = Schema::reference("Nested"); + schema.partialize(); + + let json = serde_json::to_string(&schema).unwrap(); + + assert!(json.contains(r#""partial":true"#)); + assert_eq!(serde_json::from_str::(&json).unwrap(), schema); + } + + #[test] + fn omits_the_reference_flag_when_not_partial() { + let json = serde_json::to_string(&Schema::reference("Nested")).unwrap(); + + assert!(!json.contains("partial")); + } +} + +mod nonnull { + use super::*; + + #[test] + fn returns_itself_for_a_plain_type() { + let schema = Schema::string(StringType::default()); + + assert_eq!(schema.get_nonnull_schema(), Some(&schema)); + } + + #[test] + fn returns_nothing_for_a_null() { + assert_eq!(Schema::null().get_nonnull_schema(), None); + } + + #[test] + fn finds_the_non_null_variant() { + let schema = SchemaBuilder::build_root::>(); + let inner = schema.get_nonnull_schema().unwrap(); + + assert_eq!(inner.ty, SchemaType::String(Box::default())); + } + + // A variant may itself be a union, so taking the first non-null variant + // is not enough — it has to resolve. + #[test] + fn resolves_through_a_nested_union() { + let nested = Schema::union(UnionType::new_any([Schema::null(), Schema::null()])); + let schema = Schema::union(UnionType::new_any([ + nested, + Schema::string(StringType::default()), + ])); + + let inner = schema.get_nonnull_schema().unwrap(); + + assert_eq!(inner.ty, SchemaType::String(Box::default())); + } + + #[test] + fn returns_nothing_when_every_variant_is_null() { + let nested = Schema::union(UnionType::new_any([Schema::null(), Schema::null()])); + let schema = Schema::union(UnionType::new_any([nested, Schema::null()])); + + assert_eq!(schema.get_nonnull_schema(), None); + } +} + +mod reports_application { + use super::*; + + #[test] + fn add_field_reports_whether_it_applied() { + let mut schema = Schema::structure(StructType::default()); + + assert!(schema.add_field("a", Schema::null())); + + let mut schema = Schema::string(StringType::default()); + + assert!(!schema.add_field("a", Schema::null())); + } + + #[test] + fn set_default_reports_whether_it_applied() { + let value = LiteralValue::String("abc".into()); + + assert!(Schema::string(StringType::default()).set_default(value.clone())); + assert!(SchemaBuilder::build_root::>().set_default(value.clone())); + + // Nowhere to put a default + assert!(!Schema::structure(StructType::default()).set_default(value.clone())); + assert!(!Schema::array(ArrayType::new(Schema::null())).set_default(value.clone())); + assert!(!Schema::null().set_default(value.clone())); + + // The enum does not declare this value + assert!(!Schema::enumerable(EnumType::new([LiteralValue::Bool(true)])).set_default(value)); + } +} + +// Every type below is asserted against what serde ACTUALLY emits, which is +// how `Duration` was caught claiming to be a string while encoding as a map. +mod std_coverage { + use super::*; + use std::collections::{BinaryHeap, LinkedList, VecDeque}; + use std::net::{IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6}; + use std::num::{NonZeroI32, NonZeroU8, NonZeroUsize}; + use std::ops::{Range, RangeInclusive}; + use std::time::{Duration, SystemTime}; + + fn ty() -> SchemaType { + SchemaBuilder::build_root::().ty + } + + fn array_of(inner: SchemaType) -> SchemaType { + SchemaType::Array(Box::new(ArrayType::new(Schema::new(inner)))) + } + + fn integer(kind: IntegerKind) -> SchemaType { + SchemaType::Integer(Box::new(IntegerType::new_kind(kind))) + } + + fn string_of(format: &str) -> SchemaType { + SchemaType::String(Box::new(StringType { + format: Some(format.into()), + ..StringType::default() + })) + } + + #[test] + fn collections_are_arrays() { + let expected = array_of(integer(IntegerKind::U8)); + + assert_eq!(ty::>(), expected); + assert_eq!(ty::>(), expected); + assert_eq!(ty::>(), expected); + } + + #[test] + fn non_zero_integers_keep_their_kind() { + assert_eq!( + ty::(), + SchemaType::Integer(Box::new(IntegerType { + min: Some(1), + kind: IntegerKind::Usize, + ..IntegerType::default() + })) + ); + assert_eq!( + ty::(), + SchemaType::Integer(Box::new(IntegerType { + min: Some(1), + kind: IntegerKind::U8, + ..IntegerType::default() + })) + ); + // Signed cannot express "not zero" as a bound + assert_eq!( + ty::(), + SchemaType::Integer(Box::new(IntegerType { + kind: IntegerKind::I32, + ..IntegerType::default() + })) + ); + } + + #[test] + fn network_addresses_are_strings() { + assert_eq!(ty::(), string_of("ip")); + assert_eq!(ty::(), string_of("socket-addr")); + assert_eq!(ty::(), string_of("socket-addr")); + assert_eq!(ty::(), string_of("socket-addr")); + } + + fn field_names(ty: &SchemaType) -> Vec<&str> { + let SchemaType::Struct(inner) = ty else { + panic!("expected a struct, got {ty:?}"); + }; + + inner.fields.keys().map(|key| key.as_str()).collect() + } + + #[test] + fn duration_models_the_map_serde_emits() { + let ty = ty::(); + + assert_eq!(field_names(&ty), vec!["secs", "nanos"]); + + // Matches `{"secs":3,"nanos":500}` + let value = serde_json::to_value(Duration::new(3, 500)).unwrap(); + let SchemaType::Struct(inner) = &ty else { + panic!("expected a struct"); + }; + + for key in value.as_object().unwrap().keys() { + assert!(inner.fields.contains_key(key), "missing `{key}`"); + } + } + + #[test] + fn system_time_models_the_map_serde_emits() { + let ty = ty::(); + + assert_eq!( + field_names(&ty), + vec!["secs_since_epoch", "nanos_since_epoch"] + ); + + let value = serde_json::to_value(SystemTime::UNIX_EPOCH).unwrap(); + let SchemaType::Struct(inner) = &ty else { + panic!("expected a struct"); + }; + + for key in value.as_object().unwrap().keys() { + assert!(inner.fields.contains_key(key), "missing `{key}`"); + } + } + + #[test] + fn ranges_model_the_map_serde_emits() { + for ty in [ty::>(), ty::>()] { + assert_eq!(field_names(&ty), vec!["start", "end"]); + } + + let value = serde_json::to_value(1usize..5).unwrap(); + + assert_eq!(value.as_object().unwrap().len(), 2); + } +} + +mod field_ordering { + use super::*; + + // The model keeps what the user declared... + #[test] + fn preserves_insertion_order() { + let ty = StructType::new([ + ("zebra".to_string(), Schema::null()), + ("apple".to_string(), Schema::null()), + ("mango".to_string(), Schema::null()), + ]); + + assert_eq!( + ty.fields.keys().collect::>(), + vec!["zebra", "apple", "mango"] + ); + } + + // ...while renderers read through this, so generated output stays + // alphabetical no matter how the source type is ordered. + #[test] + fn sorted_fields_is_alphabetical() { + let ty = StructType::new([ + ("zebra".to_string(), Schema::null()), + ("apple".to_string(), Schema::null()), + ("mango".to_string(), Schema::null()), + ]); + + assert_eq!( + ty.sorted_fields().into_keys().collect::>(), + vec!["apple", "mango", "zebra"] + ); + + // The declared order is untouched by reading it + assert_eq!( + ty.fields.keys().collect::>(), + vec!["zebra", "apple", "mango"] + ); + } + + #[test] + fn sorted_fields_sees_every_field() { + let ty = StructType::new([ + ("b".to_string(), Schema::null()), + ("a".to_string(), Schema::null()), + ]); + + assert_eq!(ty.sorted_fields().len(), ty.fields.len()); + } + + #[test] + fn add_field_appends() { + let mut schema = Schema::structure(StructType::new([("b".to_string(), Schema::null())])); + + schema.add_field("a", Schema::null()); + + let SchemaType::Struct(inner) = &schema.ty else { + panic!("expected a struct"); + }; + + assert_eq!(inner.fields.keys().collect::>(), vec!["b", "a"]); + } + + #[test] + fn survives_a_serde_round_trip() { + let schema = Schema::structure(StructType::new([ + ("zebra".to_string(), Schema::null()), + ("apple".to_string(), Schema::null()), + ])); + + let json = serde_json::to_string(&schema).unwrap(); + let back: Schema = serde_json::from_str(&json).unwrap(); + + assert_eq!(back, schema); + + let SchemaType::Struct(inner) = &back.ty else { + panic!("expected a struct"); + }; + + assert_eq!( + inner.fields.keys().collect::>(), + vec!["zebra", "apple"] + ); + } +} + +mod array_contains { + use super::*; + + #[test] + fn holds_a_schema_not_a_flag() { + let ty = ArrayType { + contains: Some(Box::new(Schema::string(StringType::default()))), + ..ArrayType::new(Schema::null()) + }; + + assert_eq!( + ty.contains.as_deref().map(|schema| &schema.ty), + Some(&SchemaType::String(Box::default())) + ); + } + + #[test] + fn round_trips_through_serde() { + let schema = Schema::array(ArrayType { + contains: Some(Box::new(Schema::string(StringType::default()))), + ..ArrayType::new(Schema::null()) + }); + + let json = serde_json::to_string(&schema).unwrap(); + + assert_eq!(serde_json::from_str::(&json).unwrap(), schema); + } +} + +mod display { + use super::*; + + #[test] + fn renders_scalars() { + assert_eq!(Schema::null().to_string(), "null"); + assert_eq!(Schema::unknown().to_string(), "unknown"); + assert_eq!(Schema::boolean(BooleanType::default()).to_string(), "bool"); + assert_eq!(Schema::string(StringType::default()).to_string(), "string"); + assert_eq!( + Schema::integer(IntegerType::new_kind(IntegerKind::U16)).to_string(), + "u16" + ); + assert_eq!( + Schema::float(FloatType::new_kind(FloatKind::F64)).to_string(), + "f64" + ); + } + + #[test] + fn renders_a_formatted_string() { + let schema = Schema::string(StringType { + format: Some("path".into()), + ..StringType::default() + }); + + assert_eq!(schema.to_string(), "string:path"); + } + + #[test] + fn renders_a_char_as_a_length_one_string() { + let schema = Schema::string(StringType { + min_length: Some(1), + max_length: Some(1), + ..StringType::default() + }); + + assert_eq!(schema.to_string(), "char"); + } + + #[test] + fn renders_containers() { + assert_eq!( + Schema::array(ArrayType::new(Schema::string(StringType::default()))).to_string(), + "[string]" + ); + assert_eq!( + Schema::object(ObjectType::new( + Schema::string(StringType::default()), + Schema::boolean(BooleanType::default()), + )) + .to_string(), + "{string: bool}" + ); + assert_eq!( + Schema::tuple(TupleType::new([ + Schema::string(StringType::default()), + Schema::null(), + ])) + .to_string(), + "(string, null)" + ); + } + + #[test] + fn renders_unions_and_enums() { + assert_eq!( + SchemaBuilder::build_root::>().to_string(), + "string | null" + ); + assert_eq!( + Schema::enumerable(EnumType::new([ + LiteralValue::String("a".into()), + LiteralValue::UInt(2), + ])) + .to_string(), + "\"a\" | 2" + ); + } + + // Only structs and references render as their name; everything else + // renders structurally even when named. + #[test] + fn renders_names_for_structs_and_references_only() { + let mut schema = Schema::structure(StructType::default()); + schema.set_name("MyStruct"); + assert_eq!(schema.to_string(), "MyStruct"); + + assert_eq!(Schema::reference("MyRef").to_string(), "MyRef"); + + let mut schema = Schema::string(StringType::default()); + schema.set_name("MyString"); + assert_eq!(schema.to_string(), "string"); + } + + #[test] + fn renders_literals() { + assert_eq!( + Schema::literal_value(LiteralValue::String("a".into())).to_string(), + "\"a\"" + ); + assert_eq!( + Schema::literal_value(LiteralValue::Bool(true)).to_string(), + "true" + ); + assert_eq!( + Schema::literal_value(LiteralValue::Int(-3)).to_string(), + "-3" + ); + } +} + +mod unions { + use super::*; + + fn variants() -> [Schema; 2] { + [Schema::string(StringType::default()), Schema::null()] + } + + #[test] + fn new_any_defaults_to_any_of() { + let ty = UnionType::new_any(variants()); + + assert_eq!(ty.operator, UnionOperator::AnyOf); + assert_eq!(ty.variants_types.len(), 2); + assert!(!ty.partial); + assert_eq!(ty.default_index, None); + } + + #[test] + fn new_one_uses_one_of() { + assert_eq!( + UnionType::new_one(variants()).operator, + UnionOperator::OneOf + ); + } + + #[test] + fn from_schemas_carries_the_default_index() { + let ty = UnionType::from_schemas(variants(), Some(1)); + + assert_eq!(ty.default_index, Some(1)); + assert_eq!(ty.operator, UnionOperator::AnyOf); + } + + #[test] + fn has_null_detects_a_null_variant() { + assert!(UnionType::new_any(variants()).has_null()); + assert!( + !UnionType::new_any([ + Schema::string(StringType::default()), + Schema::boolean(BooleanType::default()), + ]) + .has_null() + ); + } +} + +mod enums { + use super::*; + + #[test] + fn new_collects_values_without_variants() { + let ty = EnumType::new([ + LiteralValue::String("a".into()), + LiteralValue::String("b".into()), + ]); + + assert_eq!(ty.values.len(), 2); + assert!(ty.variants.is_none()); + assert_eq!(ty.default_index, None); + } + + #[test] + fn from_fields_keeps_declaration_order() { + let ty = EnumType::from_fields( + [ + ( + "zebra".to_string(), + SchemaField::new(Schema::literal_value(LiteralValue::String("z".into()))), + ), + ( + "apple".to_string(), + SchemaField::new(Schema::literal_value(LiteralValue::String("a".into()))), + ), + ], + Some(1), + ); + + assert_eq!( + ty.variants.as_ref().unwrap().keys().collect::>(), + vec!["zebra", "apple"] + ); + assert_eq!(ty.values.len(), 2); + assert_eq!( + Schema::enumerable(ty).get_default(), + Some(&LiteralValue::String("a".into())) + ); + } + + #[test] + fn from_fields_skips_non_literal_values() { + let ty = EnumType::from_fields( + [ + ("null".to_string(), SchemaField::new(Schema::null())), + ( + "text".to_string(), + SchemaField::new(Schema::literal_value(LiteralValue::String("t".into()))), + ), + ], + None, + ); + + assert_eq!(ty.variants.as_ref().unwrap().len(), 2); + assert_eq!(ty.values.len(), 1); + } +} + +mod objects { + use super::*; + + #[test] + fn new_sets_both_types() { + let ty = ObjectType::new( + Schema::string(StringType::default()), + Schema::boolean(BooleanType::default()), + ); + + assert_eq!(ty.key_type.ty, SchemaType::String(Box::default())); + assert_eq!(ty.value_type.ty, SchemaType::Boolean(Box::default())); + assert_eq!(ty.required, None); + } +} + +mod literal_equality { + use super::*; + + // Derived `PartialEq` would inherit `NaN != NaN`, making a schema + // unequal to its own clone. + #[test] + fn nan_equals_itself() { + for schema in [ + Schema::literal_value(LiteralValue::F64(f64::NAN)), + Schema::literal_value(LiteralValue::F32(f32::NAN)), + ] { + assert_eq!(schema, schema.clone()); + } + } + + #[test] + fn ordinary_values_still_compare_normally() { + assert_eq!(LiteralValue::F64(1.5), LiteralValue::F64(1.5)); + assert_ne!(LiteralValue::F64(1.5), LiteralValue::F64(2.5)); + assert_ne!(LiteralValue::F64(1.5), LiteralValue::F32(1.5)); + assert_eq!(LiteralValue::F64(0.0), LiteralValue::F64(-0.0)); + assert_ne!(LiteralValue::Int(1), LiteralValue::UInt(1)); + } +} diff --git a/crates/types/tests/snapshots/builder_test__map_shaped_scalars-2.snap b/crates/types/tests/snapshots/builder_test__map_shaped_scalars-2.snap new file mode 100644 index 00000000..e9a55b8b --- /dev/null +++ b/crates/types/tests/snapshots/builder_test__map_shaped_scalars-2.snap @@ -0,0 +1,28 @@ +--- +source: crates/types/tests/builder_test.rs +expression: "& input" +--- +{ + "ty": { + "type": "Struct", + "fields": { + "start": { + "schema": { + "ty": { + "type": "Integer", + "kind": "Usize" + } + } + }, + "end": { + "schema": { + "ty": { + "type": "Integer", + "kind": "Usize" + } + } + } + }, + "partial": false + } +} diff --git a/crates/types/tests/snapshots/builder_test__map_shaped_scalars.snap b/crates/types/tests/snapshots/builder_test__map_shaped_scalars.snap new file mode 100644 index 00000000..490d3486 --- /dev/null +++ b/crates/types/tests/snapshots/builder_test__map_shaped_scalars.snap @@ -0,0 +1,28 @@ +--- +source: crates/types/tests/builder_test.rs +expression: "& input" +--- +{ + "ty": { + "type": "Struct", + "fields": { + "secs": { + "schema": { + "ty": { + "type": "Integer", + "kind": "U64" + } + } + }, + "nanos": { + "schema": { + "ty": { + "type": "Integer", + "kind": "U32" + } + } + } + }, + "partial": false + } +} diff --git a/crates/types/tests/snapshots/builder_test__primitives-6.snap b/crates/types/tests/snapshots/builder_test__primitives-6.snap index 6a47a6f5..b78c4275 100644 --- a/crates/types/tests/snapshots/builder_test__primitives-6.snap +++ b/crates/types/tests/snapshots/builder_test__primitives-6.snap @@ -3,6 +3,7 @@ source: crates/types/tests/builder_test.rs expression: "& input" --- { + "nullable": true, "ty": { "type": "Union", "partial": false, diff --git a/crates/types/tests/snapshots/builder_test__strings-7.snap b/crates/types/tests/snapshots/builder_test__strings-7.snap index 7ba09a3a..745a2a45 100644 --- a/crates/types/tests/snapshots/builder_test__strings-7.snap +++ b/crates/types/tests/snapshots/builder_test__strings-7.snap @@ -5,6 +5,6 @@ expression: "& input" { "ty": { "type": "String", - "format": "duration" + "format": "socket-addr" } } diff --git a/crates/types/tests/snapshots/builder_test__structs.snap b/crates/types/tests/snapshots/builder_test__structs.snap index 713cdaa4..a4067e62 100644 --- a/crates/types/tests/snapshots/builder_test__structs.snap +++ b/crates/types/tests/snapshots/builder_test__structs.snap @@ -7,18 +7,18 @@ expression: "& input" "ty": { "type": "Struct", "fields": { - "num": { + "str": { "schema": { "ty": { - "type": "Integer", - "kind": "Usize" + "type": "String" } } }, - "str": { + "num": { "schema": { "ty": { - "type": "String" + "type": "Integer", + "kind": "Usize" } } } From 79986755ba157c5b76c3965e6ba4b9ee356c3bd5 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Wed, 19 Aug 2026 10:02:18 -0700 Subject: [PATCH 10/18] new: Add `Schematic` macro. (#196) --- AGENTS.md | 34 +- CHANGELOG.md | 3 + Cargo.lock | 6 + book/src/schema/types.md | 16 +- crates/core/src/container.rs | 106 +++++- crates/core/src/lib.rs | 7 - crates/core/src/schematic/mod.rs | 1 - crates/core/tests/derive_schematic_test.rs | 155 ++++++++ ...matic_test__generics__supports_bounds.snap | 21 ++ ...rics__supports_lifetimes_and_defaults.snap | 22 ++ ...cs__supports_multiple_type_parameters.snap | 23 ++ ...t__generics__supports_type_parameters.snap | 21 ++ ...est__generics__supports_where_clauses.snap | 24 ++ ...ematic_test__standalone__named_struct.snap | 30 ++ ...ic_test__standalone__supports_renames.snap | 17 + ...schematic_test__standalone__unit_enum.snap | 33 ++ ...ematic_test__standalone__unnamed_enum.snap | 27 ++ ...atic_test__standalone__unnamed_struct.snap | 14 + crates/macros-next/Cargo.toml | 36 +- crates/macros-next/src/lib.rs | 26 +- crates/macros-next/tests/config_test.rs | 357 ++++++++++++++++++ crates/macros-next/tests/schematic_test.rs | 327 ++++++++++++++++ crates/types/src/lib.rs | 33 ++ crates/types/tests/schema_test.rs | 63 ++++ 24 files changed, 1344 insertions(+), 58 deletions(-) delete mode 100644 crates/core/src/schematic/mod.rs create mode 100644 crates/core/tests/derive_schematic_test.rs create mode 100644 crates/core/tests/snapshots/derive_schematic_test__generics__supports_bounds.snap create mode 100644 crates/core/tests/snapshots/derive_schematic_test__generics__supports_lifetimes_and_defaults.snap create mode 100644 crates/core/tests/snapshots/derive_schematic_test__generics__supports_multiple_type_parameters.snap create mode 100644 crates/core/tests/snapshots/derive_schematic_test__generics__supports_type_parameters.snap create mode 100644 crates/core/tests/snapshots/derive_schematic_test__generics__supports_where_clauses.snap create mode 100644 crates/core/tests/snapshots/derive_schematic_test__standalone__named_struct.snap create mode 100644 crates/core/tests/snapshots/derive_schematic_test__standalone__supports_renames.snap create mode 100644 crates/core/tests/snapshots/derive_schematic_test__standalone__unit_enum.snap create mode 100644 crates/core/tests/snapshots/derive_schematic_test__standalone__unnamed_enum.snap create mode 100644 crates/core/tests/snapshots/derive_schematic_test__standalone__unnamed_struct.snap create mode 100644 crates/macros-next/tests/config_test.rs create mode 100644 crates/macros-next/tests/schematic_test.rs diff --git a/AGENTS.md b/AGENTS.md index 8952c454..7f40d56f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,15 +24,32 @@ rest follows. | `crates/types` | `schematic_types` | `Schema`, `SchemaBuilder`, `SchemaType`, the `Schematic` trait. No macro code. | | `crates/macros` | `schematic_macros` | **The production derive.** Currently what ships. | | `crates/core` | `schematic_core` | **The rewrite in progress.** Where new work goes. | -| `crates/macros-next` | `schematic_macros_next` | Thin proc-macro shell intended to consume `core`. Stale scaffolding — its `config` fn references modules that don't exist. | +| `crates/macros-next` | `schematic_macros_next` | Thin proc-macro shell over `core`. Exports `Config` and `Schematic`; `ConfigEnum` is not ported yet. | | `crates/test-app` | `test_app` | Manual smoke-test binary using the production derive. | ### Migration status — read this first -`crates/macros` is production. `crates/core` is a piece-by-piece rewrite of it. **Nothing consumes -`core` in a real `#[derive(Config)]` yet** — `macros-next` isn't wired up. So changes in `core` are -only exercised by its own snapshot tests unless you deliberately compile the generated output (see -[Testing](#testing)). +`crates/macros` is production — it is what `crates/schematic` re-exports, and what users get today. +`crates/core` is a piece-by-piece rewrite of it, consumed by `crates/macros-next`. + +`macros-next` exports working `Config` and `Schematic` derives, exercised by real compile-and-run +tests in `crates/macros-next/tests/`. **Nothing depends on it yet** — `crates/schematic` still +re-exports `schematic_macros`, so swapping the two is a separate step blocked on the gaps below. + +Still unported, in rough order of what blocks the swap: + +- **Default casing.** `rename_all` doesn't default to anything, so `enum Level { High }` serializes + as `"High"` where production emits `"high"`. moon depends on the production behavior. +- **`ConfigEnum`.** Not started; `macros-next` doesn't export it. +- **`tracing`.** The feature is declared and forwarded to `core`, but no codegen reads it, so it is + currently inert. +- Named-field enum variants, and `#[config(serde(...))]`. + +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. 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 @@ -150,6 +167,13 @@ isolation and snapshots the pretty-printed tokens. Type errors, missing trait bo cross-method inconsistencies are invisible to them. Several real bugs in this codebase sat in green snapshots for exactly this reason. +`crates/macros-next/tests/` is the antidote: it derives against the real runtime and asserts on +behavior, so it catches what snapshots can't. Run it for any `core` change that alters codegen. + +```bash +cargo test -p schematic_macros_next +``` + Snapshot tests (starbase_sandbox/insta): ```bash diff --git a/CHANGELOG.md b/CHANGELOG.md index a2c37aa6..e4bc2d2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,9 @@ - Added `EnumType::get_default()` and `EnumType::set_default()`, which resolve `default_index` against whichever list it indexes. - Added `StructType::sorted_fields()`, which renderers use to keep generated output alphabetical. +- Added `schema_name_of()`, which resolves the schema name of a type, for composing the name of a + generic type from its arguments. Types without a name of their own fall back to their Rust type + name, so instantiations over primitives still resolve distinctly. - Added `Schematic` implementations for `[T]`, `VecDeque`, `LinkedList`, `BinaryHeap`, `IpAddr`, `SocketAddr`, `SocketAddrV4`, `SocketAddrV6`, `Range`, and `RangeInclusive`. - Added `Schematic` implementations for the `NonZero` integers. The unsigned ones carry a `min` of diff --git a/Cargo.lock b/Cargo.lock index a1b6a412..e29a6ed7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1995,8 +1995,14 @@ dependencies = [ name = "schematic_macros_next" version = "0.18.7" dependencies = [ + "proc-macro2", "quote", + "schematic", "schematic_core", + "schematic_macros_next", + "serde", + "serde_json", + "starbase_sandbox", "syn 3.0.3", ] diff --git a/book/src/schema/types.md b/book/src/schema/types.md index 77d799fc..58148958 100644 --- a/book/src/schema/types.md +++ b/book/src/schema/types.md @@ -40,8 +40,20 @@ impl Schematic for T { Names must be unique across every type added to a generator, since schemas are keyed by name alone. Adding two different types under the same name panics rather than silently rendering one of them. -This is worth keeping in mind for generic types — a fixed name on `Wrapper` collides across every -`T`, so build the name from the parameter instead. + +For generic types, the derive appends the name of each type argument, so `Wrapper` becomes +`WrapperString`, `WrapperBool`, and so on, and instantiations don't collide. Arguments that name +themselves are used as-is; the rest fall back to their Rust type name. When implementing the trait +by hand, do the same with +[`schema_name_of`](https://docs.rs/schematic/latest/schematic/schema/fn.schema_name_of.html). + +```rust +impl Schematic for Wrapper { + fn schema_name() -> Option { + Some(format!("Wrapper{}", schema_name_of::())) + } +} +``` ## Inferring schemas diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index e4abd878..f9023025 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -8,7 +8,7 @@ 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}; +use syn::{Attribute, Data, DeriveInput, ExprPath, Fields, Generics, Ident, Visibility}; // #[config()], #[schematic()] #[derive(Debug, Default, FromDeriveInput)] @@ -33,8 +33,14 @@ pub struct Container { pub inner: ContainerInner, pub serde_args: Rc, + /// Render only the `Schematic` implementation, for a standalone + /// `#[derive(Schematic)]`. It describes the type it's placed on and + /// nothing else, so there's no partial type to pair it with. + pub schematic_only: bool, + // inherited pub attrs: Vec, + pub generics: Generics, pub ident: Ident, pub vis: Visibility, } @@ -94,8 +100,10 @@ impl Container { let container = Self { args, attrs: input.attrs, + generics: input.generics, ident: input.ident, inner, + schematic_only: false, serde_args, vis: input.vis, }; @@ -528,37 +536,72 @@ impl Container { /// Generate `Schematic` implementations for both types, which are /// required by the `Config` and `PartialConfig` traits. - #[cfg(not(feature = "schema"))] pub fn impl_schematic(&self) -> TokenStream { + let full = self.impl_schematic_full(); + let partial = self.impl_schematic_partial(); + + quote! { + #full + #partial + } + } + + /// Generate the `Schematic` implementation for the full type. This is + /// also the whole of a standalone `#[derive(Schematic)]`, which has no + /// partial type to pair with. + #[cfg(not(feature = "schema"))] + pub fn impl_schematic_full(&self) -> TokenStream { let base_name = &self.ident; - 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 #base_name {} + impl #impl_generics schematic::Schematic for #base_name #ty_generics #where_clause {} + } + } - #[automatically_derived] - impl schematic::Schematic for #partial_name {} + /// Generate the body of `schema_name`. A generic container appends the + /// name of each type argument, since schemas are keyed by name alone and + /// 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(); + let params = self + .generics + .type_params() + .map(|param| ¶m.ident) + .collect::>(); + + if params.is_empty() { + return quote! { Some(#base_name_string.into()) }; + } + + quote! { + let mut name = String::from(#base_name_string); + + #( + name.push_str(&schematic::schema::schema_name_of::<#params>()); + )* + + Some(name) } } - /// Generate `Schematic` implementations for both types. The partial - /// derives its schema from the full type, with all settings marked - /// as partial. + /// Generate the `Schematic` implementation for the full type. This is + /// also the whole of a standalone `#[derive(Schematic)]`, which has no + /// partial type to pair with. #[cfg(feature = "schema")] - pub fn impl_schematic(&self) -> TokenStream { + pub fn impl_schematic_full(&self) -> TokenStream { let base_name = &self.ident; - let partial_name = self.get_partial_ident(); - - let base_name_string = self.get_name(); - let partial_name_string = partial_name.to_string(); let inner = self.impl_schematic_type(); + let name = self.impl_schematic_name(); + let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); quote! { #[automatically_derived] - impl schematic::Schematic for #base_name { + impl #impl_generics schematic::Schematic for #base_name #ty_generics #where_clause { fn schema_name() -> Option { - Some(#base_name_string.into()) + #name } fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { @@ -567,7 +610,30 @@ impl Container { #inner } } + } + } + + /// Generate the `Schematic` implementation for the partial type, which + /// derives its schema from the full type with all settings partialized. + #[cfg(not(feature = "schema"))] + pub fn impl_schematic_partial(&self) -> TokenStream { + let partial_name = self.get_partial_ident(); + quote! { + #[automatically_derived] + impl schematic::Schematic for #partial_name {} + } + } + + /// Generate the `Schematic` implementation for the partial type, which + /// derives its schema from the full type with all settings partialized. + #[cfg(feature = "schema")] + 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(); + + quote! { #[automatically_derived] impl schematic::Schematic for #partial_name { fn schema_name() -> Option { @@ -1180,6 +1246,14 @@ impl Container { // #[derive(Config)] impl ToTokens for Container { fn to_tokens(&self, tokens: &mut TokenStream) { + // A standalone `Schematic` has no partial type, so none of the + // machinery below applies to it + if self.schematic_only { + tokens.extend(self.impl_schematic_full()); + + return; + } + // Partial type tokens.extend(self.impl_partial_type()); tokens.extend(self.impl_partial_type_default()); diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 9c0453f9..25918948 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -6,10 +6,3 @@ 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 deleted file mode 100644 index 8b137891..00000000 --- a/crates/core/src/schematic/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/crates/core/tests/derive_schematic_test.rs b/crates/core/tests/derive_schematic_test.rs new file mode 100644 index 00000000..b04f0469 --- /dev/null +++ b/crates/core/tests/derive_schematic_test.rs @@ -0,0 +1,155 @@ +mod utils; + +use quote::ToTokens; +use schematic_core::container::Container; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; +use utils::pretty; + +// The standalone derive is the same container with the flag flipped +fn schematic_only(input: syn::DeriveInput) -> Container { + let mut container = Container::from(input); + container.schematic_only = true; + container +} + +// The standalone derive emits a single `Schematic` impl, with no partial +// type alongside it. That is the whole difference from `Config`. +mod standalone { + use super::*; + + #[test] + fn named_struct() { + let output = schematic_only(parse_quote! { + /// Container docs. + #[derive(Schematic)] + struct Example { + a: bool, + /// Field docs. + b: Vec, + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } + + #[test] + fn unnamed_struct() { + let output = schematic_only(parse_quote! { + #[derive(Schematic)] + struct Example(bool, usize); + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } + + #[test] + fn unnamed_enum() { + let output = schematic_only(parse_quote! { + #[derive(Schematic)] + enum Example { + A(bool), + B(String), + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } + + #[test] + fn unit_enum() { + let output = schematic_only(parse_quote! { + #[derive(Schematic)] + enum Example { + A, + B, + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } + + #[test] + fn supports_renames() { + let output = schematic_only(parse_quote! { + #[derive(Schematic)] + #[schematic(rename = "Renamed")] + struct Example { + a: bool, + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } +} + +// A generic type cannot derive at all unless the parameters are carried +// through to the impl. +mod generics { + use super::*; + + #[test] + fn supports_type_parameters() { + let output = schematic_only(parse_quote! { + #[derive(Schematic)] + struct Example { + inner: T, + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } + + #[test] + fn supports_bounds() { + let output = schematic_only(parse_quote! { + #[derive(Schematic)] + struct Example { + inner: T, + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } + + #[test] + fn supports_multiple_type_parameters() { + let output = schematic_only(parse_quote! { + #[derive(Schematic)] + struct Example { + first: T, + second: U, + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } + + #[test] + fn supports_where_clauses() { + let output = schematic_only(parse_quote! { + #[derive(Schematic)] + struct Example + where + T: Schematic, + { + inner: T, + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } + + #[test] + fn supports_lifetimes_and_defaults() { + let output = schematic_only(parse_quote! { + #[derive(Schematic)] + struct Example<'a, T = bool> { + inner: T, + name: &'a str, + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } +} diff --git a/crates/core/tests/snapshots/derive_schematic_test__generics__supports_bounds.snap b/crates/core/tests/snapshots/derive_schematic_test__generics__supports_bounds.snap new file mode 100644 index 00000000..35d2abf0 --- /dev/null +++ b/crates/core/tests/snapshots/derive_schematic_test__generics__supports_bounds.snap @@ -0,0 +1,21 @@ +--- +source: crates/core/tests/derive_schematic_test.rs +expression: pretty(output.to_token_stream()) +--- +#[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::())), + ]), + ) + } +} diff --git a/crates/core/tests/snapshots/derive_schematic_test__generics__supports_lifetimes_and_defaults.snap b/crates/core/tests/snapshots/derive_schematic_test__generics__supports_lifetimes_and_defaults.snap new file mode 100644 index 00000000..f37229aa --- /dev/null +++ b/crates/core/tests/snapshots/derive_schematic_test__generics__supports_lifetimes_and_defaults.snap @@ -0,0 +1,22 @@ +--- +source: crates/core/tests/derive_schematic_test.rs +expression: pretty(output.to_token_stream()) +--- +#[automatically_derived] +impl<'a, T> schematic::Schematic for Example<'a, T> { + 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::())), + ("name".into(), SchemaField::new(schema.infer::<&'a str>())), + ]), + ) + } +} diff --git a/crates/core/tests/snapshots/derive_schematic_test__generics__supports_multiple_type_parameters.snap b/crates/core/tests/snapshots/derive_schematic_test__generics__supports_multiple_type_parameters.snap new file mode 100644 index 00000000..09d2084f --- /dev/null +++ b/crates/core/tests/snapshots/derive_schematic_test__generics__supports_multiple_type_parameters.snap @@ -0,0 +1,23 @@ +--- +source: crates/core/tests/derive_schematic_test.rs +expression: pretty(output.to_token_stream()) +--- +#[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 + .structure( + StructType::new([ + ("first".into(), SchemaField::new(schema.infer::())), + ("second".into(), SchemaField::new(schema.infer::())), + ]), + ) + } +} diff --git a/crates/core/tests/snapshots/derive_schematic_test__generics__supports_type_parameters.snap b/crates/core/tests/snapshots/derive_schematic_test__generics__supports_type_parameters.snap new file mode 100644 index 00000000..333b4620 --- /dev/null +++ b/crates/core/tests/snapshots/derive_schematic_test__generics__supports_type_parameters.snap @@ -0,0 +1,21 @@ +--- +source: crates/core/tests/derive_schematic_test.rs +expression: pretty(output.to_token_stream()) +--- +#[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::())), + ]), + ) + } +} diff --git a/crates/core/tests/snapshots/derive_schematic_test__generics__supports_where_clauses.snap b/crates/core/tests/snapshots/derive_schematic_test__generics__supports_where_clauses.snap new file mode 100644 index 00000000..5d9b1e70 --- /dev/null +++ b/crates/core/tests/snapshots/derive_schematic_test__generics__supports_where_clauses.snap @@ -0,0 +1,24 @@ +--- +source: crates/core/tests/derive_schematic_test.rs +expression: pretty(output.to_token_stream()) +--- +#[automatically_derived] +impl schematic::Schematic for Example +where + T: Schematic, +{ + 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::())), + ]), + ) + } +} diff --git a/crates/core/tests/snapshots/derive_schematic_test__standalone__named_struct.snap b/crates/core/tests/snapshots/derive_schematic_test__standalone__named_struct.snap new file mode 100644 index 00000000..d1196a72 --- /dev/null +++ b/crates/core/tests/snapshots/derive_schematic_test__standalone__named_struct.snap @@ -0,0 +1,30 @@ +--- +source: crates/core/tests/derive_schematic_test.rs +expression: pretty(output.to_token_stream()) +--- +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema.set_description("Container docs."); + schema + .structure( + StructType::new([ + ("a".into(), SchemaField::new(schema.infer::())), + ( + "b".into(), + { + let mut field = SchemaField::new( + schema.infer::>(), + ); + field.comment = Some("Field docs.".into()); + field + }, + ), + ]), + ) + } +} diff --git a/crates/core/tests/snapshots/derive_schematic_test__standalone__supports_renames.snap b/crates/core/tests/snapshots/derive_schematic_test__standalone__supports_renames.snap new file mode 100644 index 00000000..8d0f6615 --- /dev/null +++ b/crates/core/tests/snapshots/derive_schematic_test__standalone__supports_renames.snap @@ -0,0 +1,17 @@ +--- +source: crates/core/tests/derive_schematic_test.rs +expression: pretty(output.to_token_stream()) +--- +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Renamed".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .structure( + StructType::new([("a".into(), SchemaField::new(schema.infer::()))]), + ) + } +} diff --git a/crates/core/tests/snapshots/derive_schematic_test__standalone__unit_enum.snap b/crates/core/tests/snapshots/derive_schematic_test__standalone__unit_enum.snap new file mode 100644 index 00000000..6d4127ab --- /dev/null +++ b/crates/core/tests/snapshots/derive_schematic_test__standalone__unit_enum.snap @@ -0,0 +1,33 @@ +--- +source: crates/core/tests/derive_schematic_test.rs +expression: pretty(output.to_token_stream()) +--- +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .enumerable( + EnumType::from_schemas( + [ + Schema { + name: Some("A".into()), + ty: Schema::literal_value(LiteralValue::String("A".into())) + .ty, + ..Default::default() + }, + Schema { + name: Some("B".into()), + ty: Schema::literal_value(LiteralValue::String("B".into())) + .ty, + ..Default::default() + }, + ], + None, + ), + ) + } +} diff --git a/crates/core/tests/snapshots/derive_schematic_test__standalone__unnamed_enum.snap b/crates/core/tests/snapshots/derive_schematic_test__standalone__unnamed_enum.snap new file mode 100644 index 00000000..fc18ab01 --- /dev/null +++ b/crates/core/tests/snapshots/derive_schematic_test__standalone__unnamed_enum.snap @@ -0,0 +1,27 @@ +--- +source: crates/core/tests/derive_schematic_test.rs +expression: pretty(output.to_token_stream()) +--- +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .union( + UnionType::from_schemas( + [ + Schema::structure( + StructType::new([("A".into(), schema.infer::())]), + ), + Schema::structure( + StructType::new([("B".into(), schema.infer::())]), + ), + ], + None, + ), + ) + } +} diff --git a/crates/core/tests/snapshots/derive_schematic_test__standalone__unnamed_struct.snap b/crates/core/tests/snapshots/derive_schematic_test__standalone__unnamed_struct.snap new file mode 100644 index 00000000..13a2055d --- /dev/null +++ b/crates/core/tests/snapshots/derive_schematic_test__standalone__unnamed_struct.snap @@ -0,0 +1,14 @@ +--- +source: crates/core/tests/derive_schematic_test.rs +expression: pretty(output.to_token_stream()) +--- +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema.tuple(TupleType::new([schema.infer::(), schema.infer::()])) + } +} diff --git a/crates/macros-next/Cargo.toml b/crates/macros-next/Cargo.toml index d283526e..2e1ae64d 100644 --- a/crates/macros-next/Cargo.toml +++ b/crates/macros-next/Cargo.toml @@ -15,14 +15,36 @@ proc-macro = true [dependencies] schematic_core = { version = "0.18.7", path = "../core" } +proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true } +[dev-dependencies] +schematic = { path = "../schematic", features = [ + "config", + "env", + "extends", + "json", + "schema", + "schema_serde", + "validate", +] } +schematic_macros_next = { path = ".", features = [ + "config", + "env", + "extends", + "schema", + "validate", +] } +serde = { workspace = true } +serde_json = { workspace = true } +starbase_sandbox = { workspace = true } + [features] -default = ["schema"] -config = [] -env = [] -extends = [] -schema = [] -tracing = [] -validate = [] +default = [] +config = ["schematic_core/config"] +env = ["schematic_core/env"] +extends = ["schematic_core/extends"] +schema = ["schematic_core/schema"] +tracing = ["schematic_core/tracing"] +validate = ["schematic_core/validate"] diff --git a/crates/macros-next/src/lib.rs b/crates/macros-next/src/lib.rs index e15d4257..d622cafe 100644 --- a/crates/macros-next/src/lib.rs +++ b/crates/macros-next/src/lib.rs @@ -1,15 +1,8 @@ -use schematic_core::container::Container; - -// #[cfg(feature = "config")] -// mod config; -// #[cfg(feature = "config")] -// mod config_enum; -// #[cfg(feature = "schema")] -// mod schematic; +#![allow(unused)] -// use common::Macro; use proc_macro::TokenStream; use quote::quote; +use schematic_core::container::Container; use syn::{DeriveInput, parse_macro_input}; // #[derive(Config)] @@ -17,25 +10,18 @@ use syn::{DeriveInput, parse_macro_input}; #[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)); + let output = Container::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); + let mut output = Container::from(input); + output.schematic_only = true; - quote! { #container }.into() + quote! { #output }.into() } diff --git a/crates/macros-next/tests/config_test.rs b/crates/macros-next/tests/config_test.rs new file mode 100644 index 00000000..3722bb1c --- /dev/null +++ b/crates/macros-next/tests/config_test.rs @@ -0,0 +1,357 @@ +//! Exercises `#[derive(Config)]` as emitted by `schematic_core`. +//! +//! Core's own suite only snapshots pretty printed tokens, so this is the +//! first place the generated code is actually compiled and run. +#![allow(dead_code)] + +// `schematic` re-exports the production derives, so the runtime pieces are +// imported by name (the trait aliased out of the way) and the derives come +// from this crate. +use schematic::{Config as ConfigTrait, ConfigLoader, PartialConfig}; +use schematic_macros_next::Config; +use serde::Serialize; +use std::collections::HashMap; + +mod named_struct { + use super::*; + + #[derive(Debug, Config)] + pub struct Basic { + boolean: bool, + string: String, + number: usize, + vector: Vec, + } + + #[test] + fn uses_native_defaults() { + let result = ConfigLoader::::new().load().unwrap(); + + assert!(!result.config.boolean); + assert_eq!(result.config.string, ""); + assert_eq!(result.config.number, 0); + assert_eq!(result.config.vector, Vec::::new()); + } + + #[derive(Debug, Config)] + pub struct Defaults { + #[setting(default = true)] + boolean: bool, + #[setting(default = "foo")] + string: String, + #[setting(default = 123)] + number: usize, + #[setting(default = 1.5)] + float: f32, + } + + #[test] + fn uses_literal_defaults() { + let result = ConfigLoader::::new().load().unwrap(); + + assert!(result.config.boolean); + assert_eq!(result.config.string, "foo"); + assert_eq!(result.config.number, 123); + assert_eq!(result.config.float, 1.5); + } + + #[test] + fn implements_default_via_from_partial() { + let config = Defaults::default(); + + assert!(config.boolean); + assert_eq!(config.string, "foo"); + } + + #[derive(Debug, Config)] + pub struct Optionals { + required: String, + optional: Option, + #[setting(default = 10)] + optional_with_default: Option, + } + + // The outer `Option` doubles as the partial's, so it must not be + // wrapped a second time. + #[test] + fn does_not_double_wrap_options() { + let partial = PartialOptionals { + required: Some("a".into()), + optional: Some("b".into()), + optional_with_default: None, + }; + + let config = Optionals::from_partial(partial); + + assert_eq!(config.required, "a"); + assert_eq!(config.optional, Some("b".into())); + } + + #[test] + fn applies_defaults_to_optionals() { + let result = ConfigLoader::::new().load().unwrap(); + + assert_eq!(result.config.optional, None); + assert_eq!(result.config.optional_with_default, Some(10)); + } +} + +mod merging { + use super::*; + + #[derive(Debug, Config)] + pub struct Merged { + string: String, + list: Vec, + } + + #[test] + fn later_layers_win() { + let mut base = PartialMerged { + string: Some("first".into()), + list: Some(vec![1]), + }; + + base.merge( + &(), + PartialMerged { + string: Some("second".into()), + list: Some(vec![2]), + }, + ) + .unwrap(); + + assert_eq!(base.string, Some("second".into())); + assert_eq!(base.list, Some(vec![2])); + } + + #[test] + fn none_does_not_clobber() { + let mut base = PartialMerged { + string: Some("first".into()), + list: None, + }; + + base.merge( + &(), + PartialMerged { + string: None, + list: Some(vec![9]), + }, + ) + .unwrap(); + + assert_eq!(base.string, Some("first".into())); + assert_eq!(base.list, Some(vec![9])); + } +} + +mod nested { + use super::*; + + #[derive(Debug, Config)] + pub struct Child { + #[setting(default = "child")] + name: String, + value: usize, + } + + #[derive(Debug, Config)] + pub struct Parent { + #[setting(nested)] + one: Child, + #[setting(nested)] + many: Vec, + #[setting(nested)] + keyed: HashMap, + #[setting(nested)] + maybe: Option, + } + + #[test] + fn finalizes_nested_defaults() { + let result = ConfigLoader::::new().load().unwrap(); + + assert_eq!(result.config.one.name, "child"); + assert!(result.config.many.is_empty()); + assert!(result.config.keyed.is_empty()); + assert!(result.config.maybe.is_none()); + } + + // A bare nested config merges recursively, while nested collections + // replace by default. + #[test] + fn merges_bare_nested_but_replaces_collections() { + let mut base = PartialParent { + one: Some(PartialChild { + name: Some("kept".into()), + value: None, + }), + many: Some(vec![PartialChild { + name: Some("old".into()), + value: None, + }]), + ..Default::default() + }; + + base.merge( + &(), + PartialParent { + one: Some(PartialChild { + name: None, + value: Some(7), + }), + many: Some(vec![PartialChild { + name: Some("new".into()), + value: None, + }]), + ..Default::default() + }, + ) + .unwrap(); + + let one = base.one.unwrap(); + assert_eq!(one.name, Some("kept".into())); + assert_eq!(one.value, Some(7)); + + let many = base.many.unwrap(); + assert_eq!(many.len(), 1); + assert_eq!(many[0].name, Some("new".into())); + } +} + +mod wrappers { + use super::*; + use std::sync::Arc; + + #[derive(Debug, Config)] + pub struct Wrapped { + boxed: Box, + shared: Arc, + // Kept in the partial, since the inner type is unsized + unsized_box: Box, + } + + // Sized wrappers are stripped from the partial and rebuilt in + // `from_partial`, which is why partials don't need serde's `rc` feature. + #[test] + fn strips_and_rebuilds_wrappers() { + let partial = PartialWrapped { + boxed: Some("a".into()), + shared: Some(3), + unsized_box: Some("c".into()), + }; + + let config = Wrapped::from_partial(partial); + + assert_eq!(*config.boxed, "a".to_string()); + assert_eq!(*config.shared, 3); + assert_eq!(&*config.unsized_box, "c"); + } +} + +mod unnamed_struct { + use super::*; + + #[derive(Debug, Config)] + pub struct Single(String); + + #[derive(Debug, Config)] + pub struct Pair(String, usize); + + #[test] + fn supports_tuple_structs() { + let single = Single::from_partial(PartialSingle(Some("a".into()))); + assert_eq!(single.0, "a"); + + let pair = Pair::from_partial(PartialPair(Some("a".into()), Some(2))); + assert_eq!(pair.0, "a"); + assert_eq!(pair.1, 2); + } +} + +mod enums { + use super::*; + + #[derive(Debug, Config)] + pub enum Value { + Text(String), + Number(usize), + #[setting(default)] + Flag(bool), + } + + #[test] + fn defaults_to_the_marked_variant() { + assert!(matches!(PartialValue::default(), PartialValue::Flag(false))); + + let config = Value::from_partial(PartialValue::Text("a".into())); + assert!(matches!(config, Value::Text(_))); + } + + // Variant payloads get no synthetic `Option` + #[test] + fn payloads_are_not_optional() { + let partial = PartialValue::Number(5); + + assert!(matches!(Value::from_partial(partial), Value::Number(5))); + } + + #[derive(Debug, Config)] + pub enum Level { + Low, + #[setting(default)] + Medium, + High, + } + + #[test] + fn supports_unit_enums() { + assert!(matches!(PartialLevel::default(), PartialLevel::Medium)); + assert!(matches!( + Level::from_partial(PartialLevel::High), + Level::High + )); + } + + // Unit enums stay externally tagged, otherwise they only deserialize + // from `null`. + // + // Note the casing: core does not yet apply a default `rename_all`, so + // variants keep their Rust names. The production derive lowercases them, + // and this assertion should flip to "high" once that is ported. + #[test] + fn unit_enums_deserialize_from_a_string() { + let partial: PartialLevel = serde_json::from_str("\"High\"").unwrap(); + + assert!(matches!(partial, PartialLevel::High)); + + assert!(serde_json::from_str::("\"high\"").is_err()); + } +} + +mod serde_attrs { + use super::*; + + #[derive(Debug, Config, Serialize)] + pub struct Renamed { + #[setting(rename = "otherName")] + name: String, + #[serde(alias = "second", alias = "third")] + first: String, + } + + #[test] + fn honours_renames_and_aliases() { + let partial: PartialRenamed = + serde_json::from_str(r#"{"otherName": "a", "second": "b"}"#).unwrap(); + + assert_eq!(partial.name, Some("a".into())); + assert_eq!(partial.first, Some("b".into())); + } + + #[test] + fn rejects_unknown_fields() { + assert!(serde_json::from_str::(r#"{"nope": 1}"#).is_err()); + } +} diff --git a/crates/macros-next/tests/schematic_test.rs b/crates/macros-next/tests/schematic_test.rs new file mode 100644 index 00000000..dfd8c0b3 --- /dev/null +++ b/crates/macros-next/tests/schematic_test.rs @@ -0,0 +1,327 @@ +//! Exercises the standalone `#[derive(Schematic)]` as emitted by +//! `schematic_core`. Unlike `Config`, this emits one impl and no partial. +#![allow(dead_code)] + +use schematic::{Schema, SchemaBuilder, SchemaType}; +use schematic_macros_next::Schematic; +use std::collections::HashMap; + +fn build() -> Schema { + SchemaBuilder::build_root::() +} + +fn field_names(schema: &Schema) -> Vec<&str> { + let SchemaType::Struct(inner) = &schema.ty else { + panic!("expected a struct, got {:?}", schema.ty); + }; + + inner.fields.keys().map(|key| key.as_str()).collect() +} + +mod structs { + use super::*; + + /// Some docs. + #[derive(Schematic)] + struct Basic { + name: String, + count: usize, + tags: Vec, + } + + #[test] + fn builds_a_named_struct() { + let schema = build::(); + + assert_eq!(schema.name.as_deref(), Some("Basic")); + assert_eq!(field_names(&schema), vec!["name", "count", "tags"]); + } + + #[test] + fn carries_the_container_comment() { + assert_eq!(build::().description.as_deref(), Some("Some docs.")); + } + + // No partial type is emitted, so nothing named `PartialBasic` exists. + // This is the difference from `#[derive(Config)]`. + #[test] + fn does_not_emit_a_partial() { + // Compiles only because `Basic` alone implements `Schematic` + fn assert_schematic() {} + assert_schematic::(); + } + + #[derive(Schematic)] + #[schematic(rename = "Renamed")] + struct HasRename { + value: bool, + } + + #[test] + fn honours_a_container_rename() { + assert_eq!(build::().name.as_deref(), Some("Renamed")); + } + + #[derive(Schematic)] + struct Tuple(String, usize); + + #[test] + fn builds_a_tuple_struct() { + let schema = build::(); + + assert!(matches!(schema.ty, SchemaType::Tuple(_))); + } + + #[derive(Schematic)] + struct Single(String); + + // A single unnamed value is transparent + #[test] + fn builds_a_newtype_struct() { + assert_eq!(build::().ty, SchemaType::String(Box::default())); + } +} + +mod enums { + use super::*; + + #[derive(Schematic)] + enum Value { + Text(String), + Number(usize), + } + + #[test] + fn builds_a_union() { + let schema = build::(); + + let SchemaType::Union(inner) = &schema.ty else { + panic!("expected a union"); + }; + + assert_eq!(inner.variants_types.len(), 2); + } + + #[derive(Schematic)] + enum Level { + Low, + High, + } + + #[test] + fn builds_an_enumerable_from_units() { + let schema = build::(); + + let SchemaType::Enum(inner) = &schema.ty else { + panic!("expected an enum"); + }; + + assert_eq!(inner.values.len(), 2); + assert_eq!(inner.variants.as_ref().unwrap().len(), 2); + } +} + +mod generics { + use super::*; + + #[derive(Schematic)] + struct Wrapper { + inner: T, + label: String, + } + + // The impl has to carry the type parameters through, otherwise a + // generic type cannot derive at all. + #[test] + fn supports_type_parameters() { + let schema = build::>(); + + assert_eq!(field_names(&schema), vec!["inner", "label"]); + + let SchemaType::Struct(inner) = &schema.ty else { + panic!("expected a struct"); + }; + + assert_eq!( + inner.fields["inner"].schema.ty, + SchemaType::Boolean(Box::default()) + ); + } + + #[derive(Schematic)] + struct Bounded + where + T: schematic::Schematic + Clone, + { + value: T, + } + + // Schemas are keyed by name alone, so every instantiation has to resolve + // to a distinct one or they collapse into a single definition. + #[test] + fn names_include_the_type_arguments() { + assert_eq!( + build::>().name.as_deref(), + Some("WrapperBool") + ); + assert_eq!( + build::>().name.as_deref(), + Some("WrapperString") + ); + assert_eq!( + build::>>().name.as_deref(), + Some("WrapperVecString") + ); + } + + #[derive(Schematic)] + struct Named { + value: bool, + } + + // A type argument that names itself is used as-is + #[test] + fn names_use_the_arguments_own_schema_name() { + assert_eq!( + build::>().name.as_deref(), + Some("WrapperNamed") + ); + } + + #[derive(Schematic)] + struct TwoParams { + first: T, + second: U, + } + + #[test] + fn names_append_every_type_argument() { + assert_eq!( + build::>().name.as_deref(), + Some("TwoParamsBoolNamed") + ); + } + + // The collision this prevents: without the arguments in the name, both + // instantiations claim "Wrapper" and the generator rejects the second. + #[test] + fn instantiations_coexist_in_a_generator() { + use schematic::schema::SchemaGenerator; + + let mut generator = SchemaGenerator::default(); + + generator.add::>(); + generator.add::>(); + + assert!(generator.schemas.contains_key("WrapperBool")); + assert!(generator.schemas.contains_key("WrapperString")); + } + + #[test] + fn supports_where_clauses() { + let schema = build::>(); + + let SchemaType::Struct(inner) = &schema.ty else { + panic!("expected a struct"); + }; + + assert!(matches!( + inner.fields["value"].schema.ty, + SchemaType::Integer(_) + )); + } +} + +mod nested_types { + use super::*; + + #[derive(Schematic)] + struct Leaf { + value: String, + } + + #[derive(Schematic)] + struct Tree { + leaf: Leaf, + leaves: Vec, + keyed: HashMap, + maybe: Option, + } + + #[test] + fn infers_nested_schemas() { + let schema = build::(); + let SchemaType::Struct(inner) = &schema.ty else { + panic!("expected a struct"); + }; + + assert_eq!( + inner.fields["leaf"].schema.name.as_deref(), + Some("Leaf"), + "a bare nested type keeps its name" + ); + assert!(matches!( + inner.fields["leaves"].schema.ty, + SchemaType::Array(_) + )); + assert!(matches!( + inner.fields["keyed"].schema.ty, + SchemaType::Object(_) + )); + assert!(inner.fields["maybe"].schema.ty.is_nullable()); + } + + #[derive(Schematic)] + struct Recursive { + children: Vec, + } + + #[test] + fn resolves_cycles_to_a_reference() { + let schema = build::(); + let SchemaType::Struct(inner) = &schema.ty else { + panic!("expected a struct"); + }; + let SchemaType::Array(array) = &inner.fields["children"].schema.ty else { + panic!("expected an array"); + }; + + assert_eq!( + array.items_type.ty, + SchemaType::Reference { + name: "Recursive".into(), + partial: false, + } + ); + } +} + +mod field_metadata { + use super::*; + + #[derive(Schematic)] + struct Documented { + /// A documented field. + described: String, + #[schema(rename = "renamed")] + original: String, + #[schema(exclude)] + hidden: bool, + } + + #[test] + fn carries_field_metadata() { + let schema = build::(); + let SchemaType::Struct(inner) = &schema.ty else { + panic!("expected a struct"); + }; + + assert_eq!( + inner.fields["described"].comment.as_deref(), + Some("A documented field.") + ); + assert!(inner.fields.contains_key("renamed")); + assert!(!inner.fields.contains_key("original")); + assert!(!inner.fields.contains_key("hidden")); + } +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 075fdc61..a6257939 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -46,6 +46,39 @@ pub trait Schematic { } } +/// Resolve the schema name of a type, for composing the name of a generic +/// type from its arguments. +/// +/// Types that define a `schema_name` use it as-is. Anything else falls back +/// to a cleaned up form of its Rust type name, so that instantiations over +/// unnamed types (primitives, collections) still resolve to distinct names +/// instead of silently colliding. +pub fn schema_name_of() -> String { + if let Some(name) = T::schema_name() { + return name; + } + + let mut name = String::new(); + + // `alloc::vec::Vec` becomes `VecString` + for chunk in + std::any::type_name::().split(|c: char| !(c.is_alphanumeric() || c == '_' || c == ':')) + { + let Some(segment) = chunk.rsplit("::").next() else { + continue; + }; + + let mut chars = segment.chars(); + + if let Some(first) = chars.next() { + name.extend(first.to_uppercase()); + name.push_str(chars.as_str()); + } + } + + name +} + // CORE impl Schematic for () { diff --git a/crates/types/tests/schema_test.rs b/crates/types/tests/schema_test.rs index b8714023..2b91b231 100644 --- a/crates/types/tests/schema_test.rs +++ b/crates/types/tests/schema_test.rs @@ -1089,3 +1089,66 @@ mod literal_equality { assert_ne!(LiteralValue::Int(1), LiteralValue::UInt(1)); } } + +mod generic_names { + use super::*; + use std::collections::HashMap; + + struct Named; + + impl Schematic for Named { + fn schema_name() -> Option { + Some("Named".into()) + } + } + + #[test] + fn uses_a_types_own_name_when_it_has_one() { + assert_eq!(schema_name_of::(), "Named"); + } + + // Primitives and collections have no name of their own, so they fall + // back to their Rust type name with module paths stripped. Without this + // every instantiation over an unnamed type would resolve identically. + #[test] + fn falls_back_to_the_rust_type_name() { + assert_eq!(schema_name_of::(), "Bool"); + assert_eq!(schema_name_of::(), "Usize"); + assert_eq!(schema_name_of::(), "String"); + assert_eq!(schema_name_of::(), "Str"); + } + + #[test] + fn flattens_nested_type_arguments() { + assert_eq!(schema_name_of::>(), "VecString"); + assert_eq!(schema_name_of::>(), "OptionBool"); + assert_eq!( + schema_name_of::>(), + "HashMapStringUsize" + ); + assert_eq!(schema_name_of::>(), "VecNamed"); + } + + #[test] + fn produces_distinct_names_for_distinct_arguments() { + assert_ne!(schema_name_of::(), schema_name_of::()); + assert_ne!( + schema_name_of::>(), + schema_name_of::>() + ); + } + + #[test] + fn produces_identifier_safe_names() { + for name in [ + schema_name_of::>(), + schema_name_of::>>(), + schema_name_of::<(bool, usize)>(), + ] { + assert!( + name.chars().all(|c| c.is_alphanumeric() || c == '_'), + "`{name}` is not usable as an identifier" + ); + } + } +} From 5b372478af4c60c572eb25256157b006d9381043 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Wed, 19 Aug 2026 14:49:58 -0700 Subject: [PATCH 11/18] Remove features on fields. --- AGENTS.md | 20 +++++++++++++++----- crates/core/src/container.rs | 1 - crates/core/src/field.rs | 6 ------ crates/core/src/variant.rs | 2 -- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7f40d56f..3be8bd6f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,12 +38,22 @@ re-exports `schematic_macros`, so swapping the two is a separate step blocked on Still unported, in rough order of what blocks the swap: -- **Default casing.** `rename_all` doesn't default to anything, so `enum Level { High }` serializes - as `"High"` where production emits `"high"`. moon depends on the production behavior. +- **Casing.** Production resolves a name *at derive time* via `format_case`, defaulting to + `camelCase` for struct fields and `kebab-case` for enum variants. Core does neither: it forwards + `rename_all` to the partial's serde attribute and otherwise uses the raw Rust name. Two + consequences — the serialized shape differs from production for every config, and an explicit + `rename_all` reaches serde but *not* the derive-time name, so the schema, `settings()`, and + validation paths disagree with what serde actually accepts. - **`ConfigEnum`.** Not started; `macros-next` doesn't export it. -- **`tracing`.** The feature is declared and forwarded to `core`, but no codegen reads it, so it is - currently inert. -- Named-field enum variants, and `#[config(serde(...))]`. +- **`tracing`.** The feature is declared and forwarded to `core`, but no codegen reads it. Production + puts `#[instrument(skip_all)]` on `default_values`, `env_values`, `extends_from`, `finalize`, + `merge`, `validate_with_path`, `from_partial`, `settings`, `default`, and `build_schema`. +- **`#[setting(skip_deserializing_if)]`** parses but emits nothing (serde has no such attribute). +- `#[setting(exclude)]` is behind `cfg(schema)` in core but unconditional in production, so it fails + to parse when the feature is off. + +Not gaps, despite looking like them: named-field enum variants panic in *both* crates, and +`#[config(serde(...))]` was deliberately removed in the rewrite. 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 diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index f9023025..9e3488c7 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -17,7 +17,6 @@ pub struct ContainerArgs { // config pub allow_unknown_fields: bool, pub context: Option, - #[cfg(feature = "env")] pub env_prefix: Option, pub partial: Option, diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 10584465..2203ce2a 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -16,22 +16,16 @@ use syn::{Attribute, Expr, ExprPath, Field as NativeField, FieldModifiers, Ident 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 diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index 523fbe04..d76e27d9 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -17,7 +17,6 @@ use syn::{Attribute, ExprPath, Fields, FieldsUnnamed, Ident, Index, Variant as N #[darling(default, attributes(setting, schema))] pub struct VariantArgs { pub default: bool, - #[cfg(feature = "schema")] pub exclude: bool, pub merge: Option, pub nested: Option, @@ -25,7 +24,6 @@ pub struct VariantArgs { pub partial: Option, pub required: bool, pub transform: Option, - #[cfg(feature = "validate")] pub validate: Option, // serde From 92630fad5eb86e9b36e11a7bbf32a28d0c5f7e9c Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Wed, 19 Aug 2026 15:14:59 -0700 Subject: [PATCH 12/18] breaking: Rework default casing. --- AGENTS.md | 13 +- CHANGELOG.md | 5 +- Cargo.lock | 1 + crates/core/Cargo.toml | 2 +- crates/core/src/field.rs | 50 ++-- crates/core/src/utils.rs | 48 ++++ crates/core/src/variant.rs | 98 +++---- crates/core/tests/casing_test.rs | 246 ++++++++++++++++++ ...ainer_type_test__to_tokens__unit_enum.snap | 18 +- crates/macros-next/tests/config_test.rs | 124 ++++++++- crates/macros/src/config/variant.rs | 1 + crates/macros/src/utils.rs | 6 +- .../src/schema/renderers/template.rs | 15 +- 13 files changed, 527 insertions(+), 100 deletions(-) create mode 100644 crates/core/tests/casing_test.rs diff --git a/AGENTS.md b/AGENTS.md index 3be8bd6f..0bf18718 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,12 +38,6 @@ re-exports `schematic_macros`, so swapping the two is a separate step blocked on Still unported, in rough order of what blocks the swap: -- **Casing.** Production resolves a name *at derive time* via `format_case`, defaulting to - `camelCase` for struct fields and `kebab-case` for enum variants. Core does neither: it forwards - `rename_all` to the partial's serde attribute and otherwise uses the raw Rust name. Two - consequences — the serialized shape differs from production for every config, and an explicit - `rename_all` reaches serde but *not* the derive-time name, so the schema, `settings()`, and - validation paths disagree with what serde actually accepts. - **`ConfigEnum`.** Not started; `macros-next` doesn't export it. - **`tracing`.** The feature is declared and forwarded to `core`, but no codegen reads it. Production puts `#[instrument(skip_all)]` on `default_values`, `env_values`, `extends_from`, `finalize`, @@ -55,6 +49,13 @@ Still unported, in rough order of what blocks the swap: Not gaps, despite looking like them: named-field enum variants panic in *both* crates, and `#[config(serde(...))]` was deliberately removed in the rewrite. +**Casing is a deliberate divergence, not a gap.** Production defaults to `camelCase` for struct +fields and `kebab-case` for enum variants. Core has *no default* — a name is used exactly as +written, so Rust's `snake_case` is what ships unless `rename_all` says otherwise. When `rename_all` +is set, `Field::get_name` and `Variant::get_name` apply it via `format_case`, so schemas, +`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` diff --git a/CHANGELOG.md b/CHANGELOG.md index e4bc2d2b..037ac769 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ##### 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. ##### Schema @@ -30,8 +32,7 @@ - Changed `ArrayType.contains` from `Option` to `Option>`. It's now the JSON Schema `contains` subschema, and applies alongside `items_type` instead of reinterpreting it. - Changed `SchemaType::add_field` and `SchemaType::set_default` to return a `bool` indicating - whether the operation applied. They previously did nothing when the type couldn't hold the - value. + whether the operation applied. They previously did nothing when the type couldn't hold the value. - Removed `Deref`/`DerefMut` for `Schema` and `SchemaBuilder`. `Schema` now forwards the `SchemaType` accessors directly (`get_default`, `set_default`, `add_field`, `is_null`, `is_nullable`, `is_reference`, `is_struct`); reach for `schema.ty` for anything else. diff --git a/Cargo.lock b/Cargo.lock index e29a6ed7..4ffdbda4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1971,6 +1971,7 @@ dependencies = [ name = "schematic_core" version = "0.18.7" dependencies = [ + "convert_case", "darling", "prettyplease", "proc-macro2", diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 8afcddd3..ad64b281 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/moonrepo/schematic" all-features = true [dependencies] -# convert_case = { workspace = true } +convert_case = { workspace = true } darling = { workspace = true } proc-macro2 = { workspace = true } quote = { workspace = true } diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 2203ce2a..9d8f2363 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -1,9 +1,9 @@ -use crate::args::{ - NestedArg, PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeIoDirection, SerdeRenameArg, -}; +use crate::args::{NestedArg, PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; use crate::container::ContainerArgs; use crate::field_value::FieldValue; -use crate::utils::{ImplResult, is_inheritable_attribute, preserve_str_literal}; +use crate::utils::{ + ImplResult, format_case, get_renamed_value, is_inheritable_attribute, preserve_str_literal, +}; use darling::FromAttributes; use proc_macro2::{Literal, TokenStream}; use quote::{ToTokens, TokenStreamExt, format_ident, quote}; @@ -152,7 +152,14 @@ impl Field { // Unnamed settings have no name to derive from, so they may only // be sourced with an explicit `env`. if self.container_args.env_prefix.is_some() && self.ident.is_some() { - return Some(EnvKey::Derived(self.get_name().to_uppercase())); + // Deliberately un-cased: `rename_all` shapes the serialized key, + // while the variable name stays derived from the Rust name (or + // an explicit rename). + let name = self + .get_name_renamed() + .unwrap_or_else(|| self.get_name_original().to_string()); + + return Some(EnvKey::Derived(name.to_uppercase())); } None @@ -169,23 +176,30 @@ impl Field { }) } - pub fn get_name(&self) -> String { - let dir = SerdeIoDirection::From; + /// Return an explicit `rename`, if one was provided. Casing is not + /// applied, as a rename is used exactly as written. + fn get_name_renamed(&self) -> Option { + get_renamed_value(self.args.rename.as_ref(), self.serde_args.rename.as_ref()) + } - if let Some(name) = self.args.rename.as_ref().and_then(|rn| rn.get_name(dir)) { - return name.into(); + /// Return the name this setting serializes as. A container `rename_all` + /// is applied here, and not only forwarded to serde, so that schemas, + /// `settings()`, and validation paths describe the key that is actually + /// accepted. There is no default case. + pub fn get_name(&self) -> String { + if let Some(name) = self.get_name_renamed() { + return name; } - if let Some(name) = self - .serde_args - .rename - .as_ref() - .and_then(|rn| rn.get_name(dir)) - { - return name.into(); - } + let name = self.get_name_original().to_string(); - self.get_name_original().to_string() + match get_renamed_value( + self.container_args.rename_all.as_ref(), + self.serde_container_args.rename_all.as_ref(), + ) { + Some(format) => format_case(&format, &name, false), + None => name, + } } pub fn get_name_original(&self) -> &Ident { diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index f4093c0a..ad9b7c35 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -1,7 +1,55 @@ +use crate::args::{SerdeIoDirection, SerdeRenameArg}; +use convert_case::{Boundary, Case, Casing}; use proc_macro2::TokenStream; use quote::quote; use syn::{Attribute, Expr, ExprLit, Lit, Meta, Path}; +/// Resolve the container's `rename[_all]`, preferring the `config`/`schematic` +/// attribute over the `serde` one. Returns `None` when neither is set, as +/// there is no default case — a name is left exactly as it was written. +pub fn get_renamed_value( + config: Option<&SerdeRenameArg>, + serde: Option<&SerdeRenameArg>, +) -> Option { + let dir = SerdeIoDirection::From; + + config + .and_then(|rename| rename.get_name(dir)) + .or_else(|| serde.and_then(|rename| rename.get_name(dir))) + .map(|format| format.to_owned()) +} + +/// Apply a serde `rename_all` casing to a name. Mirrors what serde does to +/// the serialized key, so that derive-time names (schemas, settings, +/// validation paths) describe what is actually accepted. +pub fn format_case(format: &str, value: &str, is_variant: bool) -> String { + let case = match format { + "lowercase" => return value.to_lowercase(), + "UPPERCASE" => return value.to_uppercase(), + "PascalCase" => Case::Pascal, + "camelCase" => Case::Camel, + "snake_case" => Case::Snake, + "SCREAMING_SNAKE_CASE" => Case::UpperSnake, + "kebab-case" => Case::Kebab, + "SCREAMING-KEBAB-CASE" => Case::UpperKebab, + other => { + panic!( + "Unknown `rename_all` value `{other}`. Supported values are lowercase, UPPERCASE, PascalCase, camelCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, and SCREAMING-KEBAB-CASE." + ); + } + }; + + value + .from_case(if is_variant { + Case::Pascal + } else { + Case::Snake + }) + // Keeps `field2` from becoming `field_2` + .remove_boundaries(&[Boundary::UpperDigit, Boundary::LowerDigit]) + .to_case(case) +} + pub fn get_meta_path(meta: &Meta) -> &Path { match meta { Meta::Path(path) => path, diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index d76e27d9..fa4ac0cf 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -1,10 +1,8 @@ #[cfg(feature = "schema")] use crate::args::SerdeTagFormat; -use crate::args::{ - NestedArg, PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeIoDirection, SerdeRenameArg, -}; +use crate::args::{NestedArg, PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; use crate::container::ContainerArgs; -use crate::utils::{ImplResult, is_inheritable_attribute}; +use crate::utils::{ImplResult, format_case, get_renamed_value, is_inheritable_attribute}; use crate::variant_value::VariantValue; use darling::FromAttributes; use proc_macro2::TokenStream; @@ -122,23 +120,30 @@ impl Variant { } } - pub fn get_name(&self) -> String { - let dir = SerdeIoDirection::From; + /// Return an explicit `rename`, if one was provided. Casing is not + /// applied, as a rename is used exactly as written. + fn get_name_renamed(&self) -> Option { + get_renamed_value(self.args.rename.as_ref(), self.serde_args.rename.as_ref()) + } - if let Some(name) = self.args.rename.as_ref().and_then(|rn| rn.get_name(dir)) { - return name.into(); + /// Return the name this variant serializes as. A container `rename_all` + /// is applied here, and not only forwarded to serde, so that schemas, + /// `settings()`, and validation paths describe the key that is actually + /// accepted. There is no default case. + pub fn get_name(&self) -> String { + if let Some(name) = self.get_name_renamed() { + return name; } - if let Some(name) = self - .serde_args - .rename - .as_ref() - .and_then(|rn| rn.get_name(dir)) - { - return name.into(); - } + let name = self.ident.to_string(); - self.ident.to_string() + match get_renamed_value( + self.container_args.rename_all.as_ref(), + self.serde_container_args.rename_all.as_ref(), + ) { + Some(format) => format_case(&format, &name, true), + None => name, + } } pub fn is_default(&self) -> bool { @@ -780,37 +785,8 @@ impl Variant { self.map_unnamed_match_custom(name, &self_name, fields, factory) } -} - -// Only used for partials! -impl ToTokens for Variant { - fn to_tokens(&self, tokens: &mut TokenStream) { - let attrs = self.get_partial_attributes(); - let name = &self.ident; - - tokens.extend(match &self.fields { - Fields::Named(_) => panic!("Enums with named fields are not supported!"), - Fields::Unnamed(_) => { - let types = self - .values - .iter() - .map(|value| value.get_partial_type()) - .collect::>(); - - quote! { - #(#attrs)* - #name(#(#types),*), - } - } - Fields::Unit => quote! { - #(#attrs)* - #name, - }, - }); - } -} -impl Variant { + #[allow(clippy::explicit_counter_loop)] fn map_unnamed_match_custom( &self, name: &Ident, @@ -844,3 +820,31 @@ impl Variant { } } } + +// Only used for partials! +impl ToTokens for Variant { + fn to_tokens(&self, tokens: &mut TokenStream) { + let attrs = self.get_partial_attributes(); + let name = &self.ident; + + tokens.extend(match &self.fields { + Fields::Named(_) => panic!("Enums with named fields are not supported!"), + Fields::Unnamed(_) => { + let types = self + .values + .iter() + .map(|value| value.get_partial_type()) + .collect::>(); + + quote! { + #(#attrs)* + #name(#(#types),*), + } + } + Fields::Unit => quote! { + #(#attrs)* + #name, + }, + }); + } +} diff --git a/crates/core/tests/casing_test.rs b/crates/core/tests/casing_test.rs new file mode 100644 index 00000000..64facaee --- /dev/null +++ b/crates/core/tests/casing_test.rs @@ -0,0 +1,246 @@ +use schematic_core::container::Container; +use syn::parse_quote; + +// There is no default case. A name is used exactly as it was written unless +// `rename_all` asks otherwise, and the casing is resolved here rather than +// only forwarded to serde, so schemas and `settings()` describe the key that +// is actually accepted. +mod fields { + use super::*; + + fn names(input: syn::DeriveInput) -> Vec { + Container::from(input) + .inner + .get_fields() + .iter() + .map(|field| field.get_name()) + .collect() + } + + #[test] + fn leaves_names_alone_without_rename_all() { + assert_eq!( + names(parse_quote! { + #[derive(Config)] + struct Example { + some_field_name: String, + } + }), + vec!["some_field_name"] + ); + } + + #[test] + fn applies_the_config_attribute() { + assert_eq!( + names(parse_quote! { + #[derive(Config)] + #[config(rename_all = "camelCase")] + struct Example { + some_field_name: String, + another: usize, + } + }), + vec!["someFieldName", "another"] + ); + } + + #[test] + fn applies_the_serde_attribute() { + assert_eq!( + names(parse_quote! { + #[derive(Config)] + #[serde(rename_all = "kebab-case")] + struct Example { + some_field_name: String, + } + }), + vec!["some-field-name"] + ); + } + + #[test] + fn config_takes_precedence_over_serde() { + assert_eq!( + names(parse_quote! { + #[derive(Config)] + #[config(rename_all = "camelCase")] + #[serde(rename_all = "kebab-case")] + struct Example { + some_field_name: String, + } + }), + vec!["someFieldName"] + ); + } + + // An explicit rename is used verbatim, never re-cased + #[test] + fn does_not_case_an_explicit_rename() { + assert_eq!( + names(parse_quote! { + #[derive(Config)] + #[config(rename_all = "camelCase")] + struct Example { + #[setting(rename = "kept_as_is")] + some_field_name: String, + #[serde(rename = "also_kept")] + another_one: String, + } + }), + vec!["kept_as_is", "also_kept"] + ); + } + + #[test] + fn supports_every_serde_case() { + // `lowercase`/`UPPERCASE` only change case, they don't restructure + // the name, so a snake_case field is already lowercase + for (format, expected) in [ + ("lowercase", "some_field_name"), + ("UPPERCASE", "SOME_FIELD_NAME"), + ("PascalCase", "SomeFieldName"), + ("camelCase", "someFieldName"), + ("snake_case", "some_field_name"), + ("SCREAMING_SNAKE_CASE", "SOME_FIELD_NAME"), + ("kebab-case", "some-field-name"), + ("SCREAMING-KEBAB-CASE", "SOME-FIELD-NAME"), + ] { + let got = names(parse_quote! { + #[derive(Config)] + #[config(rename_all = #format)] + struct Example { + some_field_name: String, + } + }); + + assert_eq!(got, vec![expected], "`{format}` produced the wrong name"); + } + } + + #[test] + #[should_panic(expected = "Unknown `rename_all` value `nope`")] + fn rejects_an_unknown_case() { + names(parse_quote! { + #[derive(Config)] + #[config(rename_all = "nope")] + struct Example { + some_field_name: String, + } + }); + } +} + +mod variants { + use super::*; + + fn names(input: syn::DeriveInput) -> Vec { + Container::from(input) + .inner + .get_variants() + .iter() + .map(|variant| variant.get_name()) + .collect() + } + + #[test] + fn leaves_names_alone_without_rename_all() { + assert_eq!( + names(parse_quote! { + #[derive(Config)] + enum Example { + SomeVariant, + AnotherOne, + } + }), + vec!["SomeVariant", "AnotherOne"] + ); + } + + #[test] + fn applies_rename_all() { + assert_eq!( + names(parse_quote! { + #[derive(Config)] + #[serde(rename_all = "kebab-case")] + enum Example { + SomeVariant, + AnotherOne, + } + }), + vec!["some-variant", "another-one"] + ); + } + + #[test] + fn applies_rename_all_to_tuple_variants() { + assert_eq!( + names(parse_quote! { + #[derive(Config)] + #[config(rename_all = "snake_case")] + enum Example { + SomeVariant(String), + AnotherOne(bool), + } + }), + vec!["some_variant", "another_one"] + ); + } + + #[test] + fn does_not_case_an_explicit_rename() { + assert_eq!( + names(parse_quote! { + #[derive(Config)] + #[config(rename_all = "kebab-case")] + enum Example { + #[setting(rename = "KeptAsIs")] + SomeVariant, + AnotherOne, + } + }), + vec!["KeptAsIs", "another-one"] + ); + } +} + +// Environment variables stay derived from the Rust name, so a casing choice +// for the serialized shape doesn't reshape variable names. +mod env_keys { + use super::*; + + fn keys(input: syn::DeriveInput) -> Vec { + Container::from(input) + .inner + .get_fields() + .iter() + .filter_map(|field| field.get_env_var().map(|key| format!("{key:?}"))) + .collect() + } + + #[test] + fn are_not_affected_by_rename_all() { + let without = keys(parse_quote! { + #[derive(Config)] + #[config(env_prefix = "APP_")] + struct Example { + some_field_name: String, + } + }); + + let with = keys(parse_quote! { + #[derive(Config)] + #[config(env_prefix = "APP_", rename_all = "camelCase")] + struct Example { + some_field_name: String, + } + }); + + assert_eq!(without, with); + assert!( + without[0].contains("SOME_FIELD_NAME"), + "got {:?}", + without[0] + ); + } +} 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 374c2002..e9c8fddd 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 @@ -45,9 +45,9 @@ impl schematic::Config for Example { fn settings() -> schematic::ConfigSettingMap { use schematic::ConfigSetting; std::collections::BTreeMap::from_iter([ - ("A".into(), ConfigSetting::new("A")), - ("B".into(), ConfigSetting::new("B")), - ("C".into(), ConfigSetting::new("C")), + ("a".into(), ConfigSetting::new("a")), + ("b".into(), ConfigSetting::new("b")), + ("c".into(), ConfigSetting::new("c")), ]) } } @@ -71,20 +71,20 @@ impl schematic::Schematic for Example { EnumType::from_schemas( [ Schema { - name: Some("A".into()), - ty: Schema::literal_value(LiteralValue::String("A".into())) + name: Some("a".into()), + ty: Schema::literal_value(LiteralValue::String("a".into())) .ty, ..Default::default() }, Schema { - name: Some("B".into()), - ty: Schema::literal_value(LiteralValue::String("B".into())) + name: Some("b".into()), + ty: Schema::literal_value(LiteralValue::String("b".into())) .ty, ..Default::default() }, Schema { - name: Some("C".into()), - ty: Schema::literal_value(LiteralValue::String("C".into())) + name: Some("c".into()), + ty: Schema::literal_value(LiteralValue::String("c".into())) .ty, ..Default::default() }, diff --git a/crates/macros-next/tests/config_test.rs b/crates/macros-next/tests/config_test.rs index 3722bb1c..7a4f67b5 100644 --- a/crates/macros-next/tests/config_test.rs +++ b/crates/macros-next/tests/config_test.rs @@ -226,6 +226,7 @@ mod wrappers { #[derive(Debug, Config)] pub struct Wrapped { + #[allow(clippy::box_collection)] boxed: Box, shared: Arc, // Kept in the partial, since the inner type is unsized @@ -317,9 +318,8 @@ mod enums { // Unit enums stay externally tagged, otherwise they only deserialize // from `null`. // - // Note the casing: core does not yet apply a default `rename_all`, so - // variants keep their Rust names. The production derive lowercases them, - // and this assertion should flip to "high" once that is ported. + // Names are used exactly as written: there is no default casing, so a + // variant only changes shape when `rename_all` asks it to. #[test] fn unit_enums_deserialize_from_a_string() { let partial: PartialLevel = serde_json::from_str("\"High\"").unwrap(); @@ -355,3 +355,121 @@ mod serde_attrs { assert!(serde_json::from_str::(r#"{"nope": 1}"#).is_err()); } } + +// `rename_all` has to reach the derive-time name, not only the partial's +// serde attribute, or the schema and `settings()` would describe a key that +// serde does not accept. There is no default case: without `rename_all` the +// Rust name is used exactly as written. +mod casing { + use super::*; + use schematic::{SchemaBuilder, SchemaType}; + + fn field_names() -> Vec { + let schema = SchemaBuilder::build_root::(); + let SchemaType::Struct(inner) = &schema.ty else { + panic!("expected a struct"); + }; + + inner.fields.keys().cloned().collect() + } + + #[derive(Debug, Config)] + pub struct Untouched { + some_field_name: String, + } + + #[test] + fn leaves_names_alone_without_rename_all() { + assert!(serde_json::from_str::(r#"{"some_field_name": "a"}"#).is_ok()); + assert!(serde_json::from_str::(r#"{"someFieldName": "a"}"#).is_err()); + + assert_eq!(field_names::(), vec!["some_field_name"]); + assert_eq!( + Untouched::settings().keys().collect::>(), + vec!["some_field_name"] + ); + } + + #[derive(Debug, Config)] + #[config(rename_all = "camelCase")] + pub struct Camel { + some_field_name: String, + another_one: usize, + #[setting(rename = "kept_as_is")] + explicitly_renamed: bool, + } + + #[test] + fn applies_rename_all_to_serde() { + let partial: PartialCamel = + serde_json::from_str(r#"{"someFieldName": "a", "anotherOne": 1}"#).unwrap(); + + assert_eq!(partial.some_field_name, Some("a".into())); + assert_eq!(partial.another_one, Some(1)); + } + + #[test] + fn applies_rename_all_to_derive_time_names() { + assert_eq!( + field_names::(), + vec!["someFieldName", "anotherOne", "kept_as_is"] + ); + // `settings()` is a `BTreeMap`, so it reports alphabetically + assert_eq!( + Camel::settings().keys().collect::>(), + vec!["anotherOne", "kept_as_is", "someFieldName"] + ); + } + + // An explicit rename is used exactly as written, never re-cased + #[test] + fn does_not_case_an_explicit_rename() { + assert!(serde_json::from_str::(r#"{"kept_as_is": true}"#).is_ok()); + assert!(serde_json::from_str::(r#"{"keptAsIs": true}"#).is_err()); + } + + #[derive(Debug, Config, Serialize)] + #[serde(rename_all = "kebab-case")] + pub struct ViaSerde { + some_field_name: String, + } + + #[test] + fn honours_rename_all_from_the_serde_attribute() { + assert!(serde_json::from_str::(r#"{"some-field-name": "a"}"#).is_ok()); + assert_eq!(field_names::(), vec!["some-field-name"]); + } + + #[derive(Debug, Config)] + #[config(rename_all = "kebab-case")] + pub enum Variants { + SomeVariant(String), + #[setting(default)] + AnotherOne(bool), + } + + #[test] + fn applies_rename_all_to_variants() { + let partial: PartialVariants = serde_json::from_str(r#"{"some-variant": "a"}"#).unwrap(); + + assert!(matches!(partial, PartialVariants::SomeVariant(_))); + assert!(serde_json::from_str::(r#"{"SomeVariant": "a"}"#).is_err()); + + assert_eq!( + Variants::settings().keys().collect::>(), + vec!["another-one", "some-variant"] + ); + } + + #[derive(Debug, Config)] + #[config(rename_all = "SCREAMING_SNAKE_CASE")] + pub struct Screaming { + some_field_name: String, + } + + #[test] + fn supports_every_serde_case() { + assert!(serde_json::from_str::(r#"{"SOME_FIELD_NAME": "a"}"#).is_ok()); + assert_eq!(field_names::(), vec!["SOME_FIELD_NAME"]); + } +} diff --git a/crates/macros/src/config/variant.rs b/crates/macros/src/config/variant.rs index 93bfe9a5..bae45c58 100644 --- a/crates/macros/src/config/variant.rs +++ b/crates/macros/src/config/variant.rs @@ -251,6 +251,7 @@ impl Variant<'_> { self.map_unnamed_match_custom(name, &self_name, fields, factory) } + #[allow(clippy::explicit_counter_loop)] fn map_unnamed_match_custom( &self, name: &Ident, diff --git a/crates/macros/src/utils.rs b/crates/macros/src/utils.rs index c76f8d5b..444e9679 100644 --- a/crates/macros/src/utils.rs +++ b/crates/macros/src/utils.rs @@ -109,10 +109,8 @@ pub fn extract_deprecated(attrs: &[&Attribute]) -> Option { }; } } - Meta::Path(_) => { - if get_meta_path(&attr.meta).is_ident("deprecated") { - return Some(String::new()); // No message, handle in renderer - } + Meta::Path(_) if get_meta_path(&attr.meta).is_ident("deprecated") => { + return Some(String::new()); // No message, handle in renderer } _ => {} } diff --git a/crates/schematic/src/schema/renderers/template.rs b/crates/schematic/src/schema/renderers/template.rs index 3d9a70b1..2742046c 100644 --- a/crates/schematic/src/schema/renderers/template.rs +++ b/crates/schematic/src/schema/renderers/template.rs @@ -73,16 +73,11 @@ pub fn lit_to_string(lit: &LiteralValue) -> String { pub fn is_nested_type(schema: &SchemaType) -> bool { match schema { SchemaType::Struct(sct) => !sct.fields.is_empty(), - SchemaType::Union(uni) => { - if uni.has_null() && uni.variants_types.len() == 2 { - uni.variants_types - .iter() - .find(|v| !v.is_null()) - .is_some_and(|v| is_nested_type(&v.ty)) - } else { - false - } - } + SchemaType::Union(uni) if uni.has_null() && uni.variants_types.len() == 2 => uni + .variants_types + .iter() + .find(|v| !v.is_null()) + .is_some_and(|v| is_nested_type(&v.ty)), _ => false, } } From 7cbd163a689894849458660a8e7cbe241886f38f Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Wed, 19 Aug 2026 21:20:28 -0700 Subject: [PATCH 13/18] internal: Implement `ConfigEnum`. (#197) --- AGENTS.md | 24 +- CHANGELOG.md | 7 + book/src/config/enum/index.md | 15 +- book/src/config/index.md | 1 - book/src/config/struct/index.md | 2 +- crates/core/Cargo.toml | 2 - crates/core/src/container.rs | 191 +++++++-- crates/core/src/utils.rs | 33 +- crates/core/src/variant.rs | 105 ++++- crates/core/tests/derive_config_enum_test.rs | 174 ++++++++ crates/core/tests/derive_schematic_test.rs | 4 +- ...m_test__rendering__supports_a_default.snap | 87 ++++ ..._test__rendering__supports_a_fallback.snap | 92 +++++ ...est__rendering__supports_before_parse.snap | 80 ++++ ...ndering__supports_renames_and_aliases.snap | 99 +++++ ...onfig_enum_test__rendering__unit_enum.snap | 98 +++++ crates/macros-next/Cargo.toml | 1 - crates/macros-next/src/lib.rs | 15 +- crates/macros-next/tests/config_enum_test.rs | 376 ++++++++++++++++++ crates/macros/Cargo.toml | 1 - crates/macros/src/config/mod.rs | 13 - crates/macros/src/config_enum/mod.rs | 6 +- crates/macros/src/schematic/mod.rs | 3 - crates/macros/src/utils.rs | 8 - crates/schematic/Cargo.toml | 3 +- crates/schematic/src/internal/mod.rs | 34 ++ 26 files changed, 1371 insertions(+), 103 deletions(-) create mode 100644 crates/core/tests/derive_config_enum_test.rs create mode 100644 crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_a_default.snap create mode 100644 crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_a_fallback.snap create mode 100644 crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_before_parse.snap create mode 100644 crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_renames_and_aliases.snap create mode 100644 crates/core/tests/snapshots/derive_config_enum_test__rendering__unit_enum.snap create mode 100644 crates/macros-next/tests/config_enum_test.rs diff --git a/AGENTS.md b/AGENTS.md index 0bf18718..9b20db2a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,7 +24,7 @@ rest follows. | `crates/types` | `schematic_types` | `Schema`, `SchemaBuilder`, `SchemaType`, the `Schematic` trait. No macro code. | | `crates/macros` | `schematic_macros` | **The production derive.** Currently what ships. | | `crates/core` | `schematic_core` | **The rewrite in progress.** Where new work goes. | -| `crates/macros-next` | `schematic_macros_next` | Thin proc-macro shell over `core`. Exports `Config` and `Schematic`; `ConfigEnum` is not ported yet. | +| `crates/macros-next` | `schematic_macros_next` | Thin proc-macro shell over `core`. Exports `Config`, `ConfigEnum`, and `Schematic`. | | `crates/test-app` | `test_app` | Manual smoke-test binary using the production derive. | ### Migration status — read this first @@ -32,22 +32,28 @@ rest follows. `crates/macros` is production — it is what `crates/schematic` re-exports, and what users get today. `crates/core` is a piece-by-piece rewrite of it, consumed by `crates/macros-next`. -`macros-next` exports working `Config` and `Schematic` derives, exercised by real compile-and-run -tests in `crates/macros-next/tests/`. **Nothing depends on it yet** — `crates/schematic` still -re-exports `schematic_macros`, so swapping the two is a separate step blocked on the gaps below. +`macros-next` exports working `Config`, `ConfigEnum`, and `Schematic` derives, exercised by real +compile-and-run tests in `crates/macros-next/tests/`. **Nothing depends on it yet** — +`crates/schematic` still re-exports `schematic_macros`, so swapping the two is a separate step +blocked on the gaps below. Still unported, in rough order of what blocks the swap: -- **`ConfigEnum`.** Not started; `macros-next` doesn't export it. -- **`tracing`.** The feature is declared and forwarded to `core`, but no codegen reads it. Production - puts `#[instrument(skip_all)]` on `default_values`, `env_values`, `extends_from`, `finalize`, - `merge`, `validate_with_path`, `from_partial`, `settings`, `default`, and `build_schema`. - **`#[setting(skip_deserializing_if)]`** parses but emits nothing (serde has no such attribute). - `#[setting(exclude)]` is behind `cfg(schema)` in core but unconditional in production, so it fails to parse when the feature is off. Not gaps, despite looking like them: named-field enum variants panic in *both* crates, and -`#[config(serde(...))]` was deliberately removed in the rewrite. +`#[config(serde(...))]`, `#[variant(value)]`, and the `tracing` feature were all deliberately +removed in the rewrite — `#[variant(rename)]` covers what `value` did, and generated code is no +longer wrapped in `#[tracing::instrument]` (the loader still is). + +`ConfigEnum` has no implementation of its own. It is the same `Container` with +`macro_type = ContainerMacro::ConfigUnitEnum`, which swaps what `ToTokens` renders — the +`ConfigEnum`/`FromStr`/`TryFrom`/`Display` impls plus `impl_schematic_full`. The schema comes out of +the ordinary unit-enum path: `get_tag_format` reports `Unit` for a config enum (even when a +`fallback` variant makes it structurally unnamed), so `impl_schema_type` emits a literal per unit +variant and infers the fallback's inner type as a string, matching production. **Casing is a deliberate divergence, not a gap.** Production defaults to `camelCase` for struct fields and `kebab-case` for enum variants. Core has *no default* — a name is used exactly as diff --git a/CHANGELOG.md b/CHANGELOG.md index 037ac769..9583a3d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ - 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 `#[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. ##### Schema @@ -61,6 +65,9 @@ - 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. +- 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, + so `very_high`, `VeryHigh`, and `VERY HIGH` can all resolve to the same variant. ##### Serde diff --git a/book/src/config/enum/index.md b/book/src/config/enum/index.md index ea5810bf..2380fe22 100644 --- a/book/src/config/enum/index.md +++ b/book/src/config/enum/index.md @@ -37,34 +37,35 @@ This enum will generate the following implementations: The following fields are supported for the `#[config]` container attribute: -- `before_parse` - Transform the variant string value before parsing. Supports `lowercase` or - `UPPERCASE`. +- `before_parse` - Transform the incoming string before parsing, so that loosely cased input still + matches. Accepts the same case names as `rename_all`: `lowercase`, `UPPERCASE`, `PascalCase`, + `camelCase`, `snake_case`, `SCREAMING_SNAKE_CASE`, `kebab-case`, and `SCREAMING-KEBAB-CASE`. ```rust #[derive(ConfigEnum)] -#[config(before_parse = "UPPERCASE")] +#[config(before_parse = "kebab-case")] enum ExampleEnum { // ... } ``` +With the above, `very_high`, `VeryHigh`, and `VERY HIGH` all parse into the same variant. + And the following for serde compatibility: - `rename` -- `rename_all` - Defaults to `kebab-case`. +- `rename_all` - Has no default. Variant names are used exactly as written unless this is set. ### Variants The following fields are supported for the `#[variant]` variant attribute: - `fallback` - Marks the variant as the [fallback](./fallback.md). -- `value` - Overrides (explicitly sets) the string value used for parsing and formatting. This is - similar to serde's `rename`. And the following for serde compatibility: - `alias` -- `rename` +- `rename` - Overrides (explicitly sets) the string value used for parsing and formatting. ## Deriving common traits diff --git a/book/src/config/index.md b/book/src/config/index.md index 1083d153..cf11d488 100644 --- a/book/src/config/index.md +++ b/book/src/config/index.md @@ -141,7 +141,6 @@ The following Cargo features are available: - `pkl` - Enables Pkl. - `ron` - Enables RON. - `toml` - Enables TOML. -- `tracing` - Wrap generated code in tracing instrumentations. - `url` - Enables loading, extending, and parsing configs from URLs. - `validate` (default) - Enables setting value validation. - `yaml` - Enables YAML. diff --git a/book/src/config/struct/index.md b/book/src/config/struct/index.md index 85be349d..19fe77d5 100644 --- a/book/src/config/struct/index.md +++ b/book/src/config/struct/index.md @@ -51,7 +51,7 @@ struct ExampleConfig { And the following for serde compatibility: - `rename` -- `rename_all` - Defaults to `camelCase`. +- `rename_all` - Has no default. Field names are used exactly as written unless this is set. ## Serde support diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index ad64b281..40b84c80 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -23,7 +23,6 @@ schematic_core = { path = ".", features = [ "env", "extends", "schema", - "tracing", "validate", ] } prettyplease = "0.3.0" @@ -36,5 +35,4 @@ config = [] env = [] extends = [] schema = [] -tracing = [] validate = [] diff --git a/crates/core/src/container.rs b/crates/core/src/container.rs index 9e3488c7..747acd2e 100644 --- a/crates/core/src/container.rs +++ b/crates/core/src/container.rs @@ -2,7 +2,7 @@ use crate::args::{ PartialArg, SerdeContainerArgs, SerdeIoDirection, SerdeRenameArg, SerdeTagFormat, }; use crate::field::{EnvKey, Field}; -use crate::utils::{ImplResult, is_inheritable_attribute, to_type_string}; +use crate::utils::{ImplResult, is_inheritable_attribute, to_type_string, validate_case_format}; use crate::variant::Variant; use darling::FromDeriveInput; use proc_macro2::TokenStream; @@ -10,12 +10,20 @@ use quote::{ToTokens, format_ident, quote}; use std::rc::Rc; use syn::{Attribute, Data, DeriveInput, ExprPath, Fields, Generics, Ident, Visibility}; +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ContainerMacro { + Config, + ConfigUnitEnum, + Schematic, +} + // #[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 before_parse: Option, pub context: Option, pub env_prefix: Option, pub partial: Option, @@ -31,11 +39,7 @@ pub struct Container { pub args: Rc, pub inner: ContainerInner, pub serde_args: Rc, - - /// Render only the `Schematic` implementation, for a standalone - /// `#[derive(Schematic)]`. It describes the type it's placed on and - /// nothing else, so there's no partial type to pair it with. - pub schematic_only: bool, + pub macro_type: ContainerMacro, // inherited pub attrs: Vec, @@ -102,7 +106,7 @@ impl Container { generics: input.generics, ident: input.ident, inner, - schematic_only: false, + macro_type: ContainerMacro::Config, serde_args, vis: input.vis, }; @@ -137,10 +141,13 @@ impl Container { self.ident.to_string() } + pub fn is_config_enum(&self) -> bool { + matches!(self.macro_type, ContainerMacro::ConfigUnitEnum) + } + /// Return how the enum variants are tagged when serialized. pub fn get_tag_format(&self) -> SerdeTagFormat { - // Every variant is a unit, so they serialize as their own name - if matches!(self.inner, ContainerInner::UnitEnum { .. }) { + if matches!(self.inner, ContainerInner::UnitEnum { .. }) || self.is_config_enum() { return SerdeTagFormat::Unit; } @@ -545,6 +552,130 @@ impl Container { } } + /// Generate the `ConfigEnum`, `FromStr`, `TryFrom`, and `Display` + /// implementations for a unit-only enum. + pub fn impl_config_enum(&self) -> TokenStream { + let (ContainerInner::UnnamedEnum { variants } | ContainerInner::UnitEnum { variants }) = + &self.inner + else { + panic!("Only enums are supported."); + }; + + let mut values = vec![]; + let mut display_arms = vec![]; + let mut from_str_arms = vec![]; + let mut fallback_arm = None; + + for variant in variants { + variant.validate_config_enum(); + + values.push(variant.impl_config_enum_value()); + display_arms.push(variant.impl_config_enum_display()); + + // A fallback absorbs anything left over, so it has to be matched + // after every named value + if variant.is_fallback() { + if fallback_arm.is_some() { + panic!("Only 1 fallback variant is supported."); + } + + fallback_arm = Some(variant.impl_config_enum_from_str()); + } else { + from_str_arms.push(variant.impl_config_enum_from_str()); + } + } + + // Without a fallback, an unknown value is an error + let fallback_arm = fallback_arm.unwrap_or_else(|| { + quote! { + unknown => { + return Err(schematic::ConfigError::EnumUnknownVariant(unknown.to_owned())); + } + } + }); + + let name = &self.ident; + let before_parse = self.impl_config_enum_before_parse(); + let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); + + quote! { + #[automatically_derived] + impl #impl_generics schematic::ConfigEnum for #name #ty_generics #where_clause { + fn variants() -> Vec<#name #ty_generics> { + vec![ + #(#values),* + ] + } + } + + #[automatically_derived] + impl #impl_generics std::str::FromStr for #name #ty_generics #where_clause { + type Err = schematic::ConfigError; + + fn from_str(value: &str) -> std::result::Result { + #before_parse + + Ok(match value { + #(#from_str_arms)* + #fallback_arm + }) + } + } + + #[automatically_derived] + impl #impl_generics std::convert::TryFrom for #name #ty_generics #where_clause { + type Error = schematic::ConfigError; + + fn try_from(value: String) -> std::result::Result { + std::str::FromStr::from_str(&value) + } + } + + #[automatically_derived] + impl #impl_generics std::convert::TryFrom<&String> for #name #ty_generics #where_clause { + type Error = schematic::ConfigError; + + fn try_from(value: &String) -> std::result::Result { + std::str::FromStr::from_str(value) + } + } + + #[automatically_derived] + impl #impl_generics std::convert::TryFrom<&str> for #name #ty_generics #where_clause { + type Error = schematic::ConfigError; + + fn try_from(value: &str) -> std::result::Result { + std::str::FromStr::from_str(value) + } + } + + #[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 { + #(#display_arms)* + }) + } + } + } + } + + /// Normalize the incoming value before it is matched against a variant. + /// Accepts the same case names as serde's `rename_all`. + fn impl_config_enum_before_parse(&self) -> TokenStream { + let Some(format) = self.args.before_parse.as_deref() else { + return quote! {}; + }; + + // Validated here so a typo fails the build rather than every parse + validate_case_format("before_parse", format); + + quote! { + let value = schematic::internal::format_case(value, #format); + let value = value.as_str(); + } + } + /// Generate the `Schematic` implementation for the full type. This is /// also the whole of a standalone `#[derive(Schematic)]`, which has no /// partial type to pair with. @@ -711,7 +842,8 @@ impl Container { } } ContainerInner::UnnamedEnum { variants } | ContainerInner::UnitEnum { variants } => { - let unit = matches!(self.inner, ContainerInner::UnitEnum { .. }); + let unit = + matches!(self.inner, ContainerInner::UnitEnum { .. }) || self.is_config_enum(); let tag_format = self.get_tag_format(); let mut default_index = quote! { None }; let mut types = vec![]; @@ -1245,25 +1377,28 @@ impl Container { // #[derive(Config)] impl ToTokens for Container { fn to_tokens(&self, tokens: &mut TokenStream) { - // A standalone `Schematic` has no partial type, so none of the - // machinery below applies to it - if self.schematic_only { - tokens.extend(self.impl_schematic_full()); - - return; + match self.macro_type { + ContainerMacro::Config => { + // Partial type + tokens.extend(self.impl_partial_type()); + tokens.extend(self.impl_partial_type_default()); + tokens.extend(self.impl_partial_type_deserialize()); + tokens.extend(self.impl_partial()); + + // Full type + tokens.extend(self.impl_full()); + + // Both types + tokens.extend(self.impl_schematic()); + } + ContainerMacro::ConfigUnitEnum => { + tokens.extend(self.impl_config_enum()); + tokens.extend(self.impl_schematic_full()); + } + ContainerMacro::Schematic => { + tokens.extend(self.impl_schematic_full()); + } } - - // Partial type - tokens.extend(self.impl_partial_type()); - tokens.extend(self.impl_partial_type_default()); - tokens.extend(self.impl_partial_type_deserialize()); - tokens.extend(self.impl_partial()); - - // Full type - tokens.extend(self.impl_full()); - - // Both types - tokens.extend(self.impl_schematic()); } } diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index ad9b7c35..75a76d04 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -22,7 +22,34 @@ pub fn get_renamed_value( /// Apply a serde `rename_all` casing to a name. Mirrors what serde does to /// the serialized key, so that derive-time names (schemas, settings, /// validation paths) describe what is actually accepted. +/// The case names serde accepts for `rename_all`, and that `before_parse` +/// reuses. Kept in step with `schematic::internal::format_case`, which does +/// the same conversion at runtime. +pub const CASE_FORMATS: [&str; 8] = [ + "lowercase", + "UPPERCASE", + "PascalCase", + "camelCase", + "snake_case", + "SCREAMING_SNAKE_CASE", + "kebab-case", + "SCREAMING-KEBAB-CASE", +]; + +/// Panic when a case format isn't one serde recognizes. Takes the attribute +/// name so the message points at whichever one was misspelled. +pub fn validate_case_format(attr: &str, format: &str) { + if !CASE_FORMATS.contains(&format) { + panic!( + "Unknown `{attr}` value `{format}`. Supported values are {}.", + CASE_FORMATS.join(", ") + ); + } +} + pub fn format_case(format: &str, value: &str, is_variant: bool) -> String { + validate_case_format("rename_all", format); + let case = match format { "lowercase" => return value.to_lowercase(), "UPPERCASE" => return value.to_uppercase(), @@ -32,11 +59,7 @@ pub fn format_case(format: &str, value: &str, is_variant: bool) -> String { "SCREAMING_SNAKE_CASE" => Case::UpperSnake, "kebab-case" => Case::Kebab, "SCREAMING-KEBAB-CASE" => Case::UpperKebab, - other => { - panic!( - "Unknown `rename_all` value `{other}`. Supported values are lowercase, UPPERCASE, PascalCase, camelCase, snake_case, SCREAMING_SNAKE_CASE, kebab-case, and SCREAMING-KEBAB-CASE." - ); - } + _ => unreachable!("Validated above."), }; value diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index fa4ac0cf..779845f8 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -2,7 +2,9 @@ use crate::args::SerdeTagFormat; use crate::args::{NestedArg, PartialArg, SerdeContainerArgs, SerdeFieldArgs, SerdeRenameArg}; use crate::container::ContainerArgs; -use crate::utils::{ImplResult, format_case, get_renamed_value, is_inheritable_attribute}; +use crate::utils::{ + ImplResult, format_case, get_meta_path, get_renamed_value, is_inheritable_attribute, +}; use crate::variant_value::VariantValue; use darling::FromAttributes; use proc_macro2::TokenStream; @@ -10,12 +12,13 @@ use quote::{ToTokens, format_ident, quote}; use std::rc::Rc; use syn::{Attribute, ExprPath, Fields, FieldsUnnamed, Ident, Index, Variant as NativeVariant}; -// #[setting()], #[schema()] +// #[setting()], #[schema()], #[variant()] #[derive(Debug, Default, FromAttributes)] -#[darling(default, attributes(setting, schema))] +#[darling(default, attributes(setting, schema, variant))] pub struct VariantArgs { pub default: bool, pub exclude: bool, + pub fallback: bool, pub merge: Option, pub nested: Option, pub null: bool, @@ -148,6 +151,91 @@ impl Variant { pub fn is_default(&self) -> bool { self.args.default + || self + .attrs + .iter() + .any(|attr| get_meta_path(&attr.meta).is_ident("default")) + } + + /// Whether this variant absorbs any value that no other variant matches. + pub fn is_fallback(&self) -> bool { + self.args.fallback + } + + /// Aliases can be provided by both attributes, so combine them. + pub fn get_aliases(&self) -> Vec<&String> { + let mut aliases = vec![]; + + for alias in self.args.alias.iter().chain(self.serde_args.alias.iter()) { + if !aliases.contains(&alias) { + aliases.push(alias); + } + } + + aliases + } + + /// Validate this variant within a `ConfigEnum`, which only models a list + /// of named string values. + pub fn validate_config_enum(&self) { + if self.is_fallback() { + match &self.fields { + Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {} + Fields::Unnamed(_) => { + panic!("Only 1 unnamed field is supported for `fallback`."); + } + _ => { + panic!("Only unnamed tuple variants are supported for `fallback`."); + } + }; + } else if !self.is_unit_variant() { + panic!("Only unit variants are supported, unless marked as `fallback`."); + } + } + + /// An expression that constructs this variant, for `ConfigEnum::variants`. + pub fn impl_config_enum_value(&self) -> TokenStream { + let name = &self.ident; + + if self.is_fallback() { + quote! { Self::#name(Default::default()) } + } else { + quote! { Self::#name } + } + } + + /// A match arm that formats this variant back into its string value. + pub fn impl_config_enum_display(&self) -> TokenStream { + let name = &self.ident; + + if self.is_fallback() { + return quote! { Self::#name(fallback) => fallback, }; + } + + let value = self.get_name(); + + quote! { Self::#name => #value, } + } + + /// A match arm that parses a string into this variant. A fallback absorbs + /// anything left over, so it must be matched last. + pub fn impl_config_enum_from_str(&self) -> TokenStream { + let name = &self.ident; + + if self.is_fallback() { + return quote! { + fallback => Self::#name( + fallback.try_into().map_err(|_| { + schematic::ConfigError::EnumInvalidFallback(fallback.to_string()) + })? + ), + }; + } + + let value = self.get_name(); + let aliases = self.get_aliases(); + + quote! { #value #(| #aliases)* => Self::#name, } } pub fn is_excluded(&self) -> bool { @@ -207,16 +295,7 @@ impl Variant { pub fn get_partial_serde_attribute_args(&self) -> TokenStream { let mut meta = vec![]; - // Aliases can be provided by both, so combine them - let mut aliases: Vec<&String> = vec![]; - - for alias in self.args.alias.iter().chain(self.serde_args.alias.iter()) { - if !aliases.contains(&alias) { - aliases.push(alias); - } - } - - for alias in aliases { + for alias in self.get_aliases() { meta.push(quote! { alias = #alias }); } diff --git a/crates/core/tests/derive_config_enum_test.rs b/crates/core/tests/derive_config_enum_test.rs new file mode 100644 index 00000000..5249aee3 --- /dev/null +++ b/crates/core/tests/derive_config_enum_test.rs @@ -0,0 +1,174 @@ +mod utils; + +use quote::ToTokens; +use schematic_core::container::{Container, ContainerMacro}; +use starbase_sandbox::assert_snapshot; +use syn::parse_quote; +use utils::pretty; + +// The same container as `Config`, rendering a different set of impls +fn config_enum(input: syn::DeriveInput) -> Container { + let mut container = Container::from(input); + container.macro_type = ContainerMacro::ConfigUnitEnum; + container +} + +mod rendering { + use super::*; + + #[test] + fn unit_enum() { + let output = config_enum(parse_quote! { + /// Container docs. + #[derive(ConfigEnum)] + enum Example { + Info, + Error, + Off, + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } + + #[test] + fn supports_renames_and_aliases() { + let output = config_enum(parse_quote! { + #[derive(ConfigEnum)] + #[config(rename_all = "kebab-case")] + enum Example { + VeryHigh, + #[variant(rename = "custom")] + Explicit, + #[variant(alias = "second", alias = "third")] + Aliased, + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } + + #[test] + fn supports_before_parse() { + let output = config_enum(parse_quote! { + #[derive(ConfigEnum)] + #[config(before_parse = "lowercase")] + enum Example { + Info, + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } + + #[test] + fn supports_a_fallback() { + let output = config_enum(parse_quote! { + #[derive(ConfigEnum)] + enum Example { + Known, + #[variant(fallback)] + Other(String), + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } + + #[test] + fn supports_a_default() { + let output = config_enum(parse_quote! { + #[derive(ConfigEnum, Default)] + enum Example { + #[default] + Info, + Error, + } + }); + + assert_snapshot!(pretty(output.to_token_stream())); + } +} + +mod validation { + use super::*; + + fn render(input: syn::DeriveInput) -> String { + pretty(config_enum(input).to_token_stream()) + } + + #[test] + #[should_panic(expected = "Only unit variants are supported, unless marked as `fallback`")] + fn rejects_a_non_unit_variant() { + render(parse_quote! { + #[derive(ConfigEnum)] + enum Example { + Known, + Invalid(String), + } + }); + } + + #[test] + #[should_panic(expected = "Only 1 unnamed field is supported for `fallback`")] + fn rejects_a_multi_field_fallback() { + render(parse_quote! { + #[derive(ConfigEnum)] + enum Example { + Known, + #[variant(fallback)] + Other(String, usize), + } + }); + } + + #[test] + #[should_panic(expected = "Only unnamed tuple variants are supported for `fallback`")] + fn rejects_a_unit_fallback() { + render(parse_quote! { + #[derive(ConfigEnum)] + enum Example { + Known, + #[variant(fallback)] + Other, + } + }); + } + + #[test] + #[should_panic(expected = "Only 1 fallback variant is supported")] + fn rejects_multiple_fallbacks() { + render(parse_quote! { + #[derive(ConfigEnum)] + enum Example { + #[variant(fallback)] + One(String), + #[variant(fallback)] + Two(String), + } + }); + } + + #[test] + #[should_panic(expected = "Unknown `before_parse` value `nope`. Supported values are")] + fn rejects_an_unknown_before_parse() { + render(parse_quote! { + #[derive(ConfigEnum)] + #[config(before_parse = "nope")] + enum Example { + Info, + } + }); + } + + #[test] + #[should_panic(expected = "Only enums are supported")] + fn rejects_a_struct() { + render(parse_quote! { + #[derive(ConfigEnum)] + struct Example { + a: bool, + } + }); + } +} diff --git a/crates/core/tests/derive_schematic_test.rs b/crates/core/tests/derive_schematic_test.rs index b04f0469..a6794e20 100644 --- a/crates/core/tests/derive_schematic_test.rs +++ b/crates/core/tests/derive_schematic_test.rs @@ -1,7 +1,7 @@ mod utils; use quote::ToTokens; -use schematic_core::container::Container; +use schematic_core::container::{Container, ContainerMacro}; use starbase_sandbox::assert_snapshot; use syn::parse_quote; use utils::pretty; @@ -9,7 +9,7 @@ use utils::pretty; // The standalone derive is the same container with the flag flipped fn schematic_only(input: syn::DeriveInput) -> Container { let mut container = Container::from(input); - container.schematic_only = true; + container.macro_type = ContainerMacro::Schematic; container } 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 new file mode 100644 index 00000000..6ebb76ac --- /dev/null +++ b/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_a_default.snap @@ -0,0 +1,87 @@ +--- +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::Info, Self::Error] + } +} +#[automatically_derived] +impl std::str::FromStr for Example { + type Err = schematic::ConfigError; + fn from_str(value: &str) -> std::result::Result { + Ok( + match value { + "Info" => Self::Info, + "Error" => Self::Error, + unknown => { + return Err( + schematic::ConfigError::EnumUnknownVariant(unknown.to_owned()), + ); + } + }, + ) + } +} +#[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 { + write!(f, "{}", match self { Self::Info => "Info", Self::Error => "Error", }) + } +} +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .enumerable( + EnumType::from_schemas( + [ + Schema { + name: Some("Info".into()), + ty: Schema::literal_value( + LiteralValue::String("Info".into()), + ) + .ty, + ..Default::default() + }, + Schema { + name: Some("Error".into()), + ty: Schema::literal_value( + LiteralValue::String("Error".into()), + ) + .ty, + ..Default::default() + }, + ], + Some(0usize), + ), + ) + } +} 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 new file mode 100644 index 00000000..85be7a53 --- /dev/null +++ b/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_a_fallback.snap @@ -0,0 +1,92 @@ +--- +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 { + write!( + f, "{}", match self { Self::Known => "Known", Self::Other(fallback) => + fallback, } + ) + } +} +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + 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_before_parse.snap b/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_before_parse.snap new file mode 100644 index 00000000..76d29b0f --- /dev/null +++ b/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_before_parse.snap @@ -0,0 +1,80 @@ +--- +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::Info] + } +} +#[automatically_derived] +impl std::str::FromStr for Example { + type Err = schematic::ConfigError; + fn from_str(value: &str) -> std::result::Result { + let value = schematic::internal::format_case(value, "lowercase"); + let value = value.as_str(); + Ok( + match value { + "Info" => Self::Info, + unknown => { + return Err( + schematic::ConfigError::EnumUnknownVariant(unknown.to_owned()), + ); + } + }, + ) + } +} +#[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 { + write!(f, "{}", match self { Self::Info => "Info", }) + } +} +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .enumerable( + EnumType::from_schemas( + [ + Schema { + name: Some("Info".into()), + ty: Schema::literal_value( + LiteralValue::String("Info".into()), + ) + .ty, + ..Default::default() + }, + ], + None, + ), + ) + } +} 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 new file mode 100644 index 00000000..8b6c7db2 --- /dev/null +++ b/crates/core/tests/snapshots/derive_config_enum_test__rendering__supports_renames_and_aliases.snap @@ -0,0 +1,99 @@ +--- +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::VeryHigh, Self::Explicit, Self::Aliased] + } +} +#[automatically_derived] +impl std::str::FromStr for Example { + type Err = schematic::ConfigError; + fn from_str(value: &str) -> std::result::Result { + Ok( + match value { + "very-high" => Self::VeryHigh, + "custom" => Self::Explicit, + "aliased" | "second" | "third" => Self::Aliased, + unknown => { + return Err( + schematic::ConfigError::EnumUnknownVariant(unknown.to_owned()), + ); + } + }, + ) + } +} +#[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 { + write!( + f, "{}", match self { Self::VeryHigh => "very-high", Self::Explicit => + "custom", Self::Aliased => "aliased", } + ) + } +} +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema + .enumerable( + EnumType::from_schemas( + [ + Schema { + name: Some("very-high".into()), + ty: Schema::literal_value( + LiteralValue::String("very-high".into()), + ) + .ty, + ..Default::default() + }, + Schema { + name: Some("custom".into()), + ty: Schema::literal_value( + LiteralValue::String("custom".into()), + ) + .ty, + ..Default::default() + }, + Schema { + name: Some("aliased".into()), + ty: Schema::literal_value( + LiteralValue::String("aliased".into()), + ) + .ty, + ..Default::default() + }, + ], + None, + ), + ) + } +} 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 new file mode 100644 index 00000000..d8d7459e --- /dev/null +++ b/crates/core/tests/snapshots/derive_config_enum_test__rendering__unit_enum.snap @@ -0,0 +1,98 @@ +--- +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::Info, Self::Error, Self::Off] + } +} +#[automatically_derived] +impl std::str::FromStr for Example { + type Err = schematic::ConfigError; + fn from_str(value: &str) -> std::result::Result { + Ok( + match value { + "Info" => Self::Info, + "Error" => Self::Error, + "Off" => Self::Off, + unknown => { + return Err( + schematic::ConfigError::EnumUnknownVariant(unknown.to_owned()), + ); + } + }, + ) + } +} +#[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 { + write!( + f, "{}", match self { Self::Info => "Info", Self::Error => "Error", Self::Off + => "Off", } + ) + } +} +#[automatically_derived] +impl schematic::Schematic for Example { + fn schema_name() -> Option { + Some("Example".into()) + } + fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { + use schematic::schema::*; + schema.set_description("Container docs."); + schema + .enumerable( + EnumType::from_schemas( + [ + Schema { + name: Some("Info".into()), + ty: Schema::literal_value( + LiteralValue::String("Info".into()), + ) + .ty, + ..Default::default() + }, + Schema { + name: Some("Error".into()), + ty: Schema::literal_value( + LiteralValue::String("Error".into()), + ) + .ty, + ..Default::default() + }, + Schema { + name: Some("Off".into()), + ty: Schema::literal_value(LiteralValue::String("Off".into())) + .ty, + ..Default::default() + }, + ], + None, + ), + ) + } +} diff --git a/crates/macros-next/Cargo.toml b/crates/macros-next/Cargo.toml index 2e1ae64d..debeeb64 100644 --- a/crates/macros-next/Cargo.toml +++ b/crates/macros-next/Cargo.toml @@ -46,5 +46,4 @@ config = ["schematic_core/config"] env = ["schematic_core/env"] extends = ["schematic_core/extends"] schema = ["schematic_core/schema"] -tracing = ["schematic_core/tracing"] validate = ["schematic_core/validate"] diff --git a/crates/macros-next/src/lib.rs b/crates/macros-next/src/lib.rs index d622cafe..28b236b8 100644 --- a/crates/macros-next/src/lib.rs +++ b/crates/macros-next/src/lib.rs @@ -2,7 +2,7 @@ use proc_macro::TokenStream; use quote::quote; -use schematic_core::container::Container; +use schematic_core::container::{Container, ContainerMacro}; use syn::{DeriveInput, parse_macro_input}; // #[derive(Config)] @@ -15,13 +15,24 @@ pub fn config(item: TokenStream) -> TokenStream { quote! { #output }.into() } +// #[derive(ConfigEnum)] +#[cfg(feature = "config")] +#[proc_macro_derive(ConfigEnum, attributes(config, variant))] +pub fn config_enum(item: TokenStream) -> TokenStream { + let input: DeriveInput = parse_macro_input!(item); + let mut output = Container::from(input); + output.macro_type = ContainerMacro::ConfigUnitEnum; + + quote! { #output }.into() +} + // #[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 mut output = Container::from(input); - output.schematic_only = true; + output.macro_type = ContainerMacro::Schematic; quote! { #output }.into() } diff --git a/crates/macros-next/tests/config_enum_test.rs b/crates/macros-next/tests/config_enum_test.rs new file mode 100644 index 00000000..920d3a34 --- /dev/null +++ b/crates/macros-next/tests/config_enum_test.rs @@ -0,0 +1,376 @@ +//! Exercises `#[derive(ConfigEnum)]` as emitted by `schematic_core`. +//! +//! `ConfigEnum` reuses the same `Container` as `Config` and `Schematic`, +//! diverging only in what it renders. +#![allow(dead_code)] + +use schematic::{ConfigEnum as ConfigEnumTrait, ConfigError, SchemaBuilder, SchemaType}; +use schematic_macros_next::ConfigEnum; +use std::str::FromStr; + +mod basic { + use super::*; + + #[derive(Clone, Debug, PartialEq, ConfigEnum)] + enum Level { + Info, + Error, + Off, + } + + #[test] + fn lists_every_variant() { + assert_eq!( + Level::variants(), + vec![Level::Info, Level::Error, Level::Off] + ); + } + + // No default casing: a name is used exactly as written + #[test] + fn parses_from_a_string() { + assert_eq!(Level::from_str("Info").unwrap(), Level::Info); + assert_eq!(Level::from_str("Off").unwrap(), Level::Off); + } + + #[test] + fn formats_back_into_a_string() { + assert_eq!(Level::Info.to_string(), "Info"); + assert_eq!(Level::Error.to_string(), "Error"); + } + + #[test] + fn round_trips() { + for variant in Level::variants() { + assert_eq!(Level::from_str(&variant.to_string()).unwrap(), variant); + } + } + + #[test] + fn errors_on_an_unknown_value() { + let error = Level::from_str("nope").unwrap_err(); + + assert!(matches!(error, ConfigError::EnumUnknownVariant(_))); + } + + #[test] + fn converts_from_owned_and_borrowed() { + assert_eq!(Level::try_from("Info".to_string()).unwrap(), Level::Info); + assert_eq!(Level::try_from(&"Info".to_string()).unwrap(), Level::Info); + assert_eq!(Level::try_from("Info").unwrap(), Level::Info); + } +} + +mod casing { + use super::*; + + #[derive(Clone, Debug, PartialEq, ConfigEnum)] + #[config(rename_all = "kebab-case")] + enum Level { + VeryHigh, + Low, + } + + #[test] + fn applies_rename_all() { + assert_eq!(Level::from_str("very-high").unwrap(), Level::VeryHigh); + assert_eq!(Level::VeryHigh.to_string(), "very-high"); + assert!(Level::from_str("VeryHigh").is_err()); + } + + #[derive(Clone, Debug, PartialEq, ConfigEnum)] + enum Renamed { + #[variant(rename = "custom")] + Explicit, + #[variant(alias = "second", alias = "third")] + Aliased, + } + + #[test] + fn honours_explicit_renames() { + assert_eq!(Renamed::from_str("custom").unwrap(), Renamed::Explicit); + assert_eq!(Renamed::Explicit.to_string(), "custom"); + } + + #[test] + fn accepts_aliases_when_parsing() { + assert_eq!(Renamed::from_str("Aliased").unwrap(), Renamed::Aliased); + assert_eq!(Renamed::from_str("second").unwrap(), Renamed::Aliased); + assert_eq!(Renamed::from_str("third").unwrap(), Renamed::Aliased); + + // Formatting always uses the canonical value + assert_eq!(Renamed::Aliased.to_string(), "Aliased"); + } +} + +mod before_parse { + use super::*; + + #[derive(Clone, Debug, PartialEq, ConfigEnum)] + #[config(before_parse = "lowercase", rename_all = "lowercase")] + enum Lower { + Info, + Error, + } + + #[test] + fn lowercases_the_input() { + assert_eq!(Lower::from_str("INFO").unwrap(), Lower::Info); + assert_eq!(Lower::from_str("Info").unwrap(), Lower::Info); + assert_eq!(Lower::from_str("info").unwrap(), Lower::Info); + } + + #[derive(Clone, Debug, PartialEq, ConfigEnum)] + #[config(before_parse = "UPPERCASE", rename_all = "UPPERCASE")] + enum Upper { + Info, + } + + #[test] + fn uppercases_the_input() { + assert_eq!(Upper::from_str("info").unwrap(), Upper::Info); + assert_eq!(Upper::Info.to_string(), "INFO"); + } + + // Every case serde's `rename_all` accepts is accepted here too, so + // loosely cased input can be normalized before it is matched. + #[derive(Clone, Debug, PartialEq, ConfigEnum)] + #[config(before_parse = "kebab-case", rename_all = "kebab-case")] + enum Kebab { + VeryHigh, + } + + #[test] + fn supports_kebab_case() { + for input in [ + "very_high", + "VeryHigh", + "VERY_HIGH", + "very high", + "veryHigh", + ] { + assert_eq!( + Kebab::from_str(input).unwrap(), + Kebab::VeryHigh, + "`{input}` did not normalize" + ); + } + } + + #[derive(Clone, Debug, PartialEq, ConfigEnum)] + #[config(before_parse = "camelCase")] + enum Camel { + #[variant(rename = "veryHigh")] + VeryHigh, + } + + #[test] + fn supports_camel_case() { + for input in ["very_high", "VeryHigh", "very-high"] { + assert_eq!(Camel::from_str(input).unwrap(), Camel::VeryHigh); + } + } + + #[derive(Clone, Debug, PartialEq, ConfigEnum)] + #[config( + before_parse = "SCREAMING_SNAKE_CASE", + rename_all = "SCREAMING_SNAKE_CASE" + )] + enum Screaming { + VeryHigh, + } + + #[test] + fn supports_screaming_snake_case() { + for input in ["very-high", "veryHigh", "very_high"] { + assert_eq!(Screaming::from_str(input).unwrap(), Screaming::VeryHigh); + } + + assert_eq!(Screaming::VeryHigh.to_string(), "VERY_HIGH"); + } + + #[derive(Clone, Debug, PartialEq, ConfigEnum)] + #[config(before_parse = "PascalCase")] + enum Pascal { + VeryHigh, + } + + #[test] + fn supports_pascal_case() { + for input in ["very-high", "very_high", "veryHigh"] { + assert_eq!(Pascal::from_str(input).unwrap(), Pascal::VeryHigh); + } + } + + // Digit boundaries are preserved, matching the derive-time helper + #[derive(Clone, Debug, PartialEq, ConfigEnum)] + #[config(before_parse = "snake_case", rename_all = "snake_case")] + enum Digits { + Version2, + } + + #[test] + fn keeps_digits_attached() { + assert_eq!(Digits::Version2.to_string(), "version2"); + assert_eq!(Digits::from_str("Version2").unwrap(), Digits::Version2); + } +} + +mod fallback { + use super::*; + + #[derive(Clone, Debug, PartialEq, ConfigEnum)] + #[config(rename_all = "kebab-case")] + enum Value { + Known, + Other, + #[variant(fallback)] + Custom(String), + } + + #[test] + fn absorbs_unknown_values() { + assert_eq!(Value::from_str("known").unwrap(), Value::Known); + assert_eq!( + Value::from_str("anything").unwrap(), + Value::Custom("anything".into()) + ); + } + + // The fallback is matched last, so named variants still win + #[test] + fn does_not_shadow_named_variants() { + assert_eq!(Value::from_str("other").unwrap(), Value::Other); + } + + #[test] + fn formats_from_the_inner_value() { + assert_eq!(Value::Custom("abc".into()).to_string(), "abc"); + assert_eq!(Value::Known.to_string(), "known"); + } + + #[test] + fn round_trips_through_the_fallback() { + let value = Value::from_str("unmatched").unwrap(); + + assert_eq!(Value::from_str(&value.to_string()).unwrap(), value); + } + + #[test] + fn variants_includes_a_default_fallback() { + assert_eq!( + Value::variants(), + vec![Value::Known, Value::Other, Value::Custom(String::new())] + ); + } +} + +mod schema { + use super::*; + + /// Some docs. + #[derive(Clone, Debug, PartialEq, ConfigEnum, Default)] + #[config(rename_all = "kebab-case")] + enum Level { + #[default] + VeryHigh, + Low, + } + + #[test] + fn builds_an_enumerable_schema() { + let schema = SchemaBuilder::build_root::(); + + assert_eq!(schema.name.as_deref(), Some("Level")); + assert_eq!(schema.description.as_deref(), Some("Some docs.")); + + let SchemaType::Enum(inner) = &schema.ty else { + panic!("expected an enum, got {:?}", schema.ty); + }; + + assert_eq!( + inner.variants.as_ref().unwrap().keys().collect::>(), + vec!["very-high", "low"] + ); + } + + // The `#[default]` attribute from `#[derive(Default)]` marks the default + #[test] + fn records_the_default_variant() { + let schema = SchemaBuilder::build_root::(); + + assert_eq!( + schema.get_default(), + Some(&schematic::schema::LiteralValue::String("very-high".into())) + ); + } + + #[derive(Clone, Debug, PartialEq, ConfigEnum)] + enum WithFallback { + Known, + #[variant(fallback)] + Other(String), + } + + // A fallback accepts any string, so it widens the schema + #[test] + fn renders_a_fallback_as_a_string() { + let schema = SchemaBuilder::build_root::(); + let SchemaType::Enum(inner) = &schema.ty else { + panic!("expected an enum"); + }; + let variants = inner.variants.as_ref().unwrap(); + + assert!(matches!( + variants["Known"].schema.ty, + SchemaType::Literal(_) + )); + assert!(matches!(variants["Other"].schema.ty, SchemaType::String(_))); + + // Only the literal variants contribute values + assert_eq!(inner.values.len(), 1); + } +} + +// `derive_enum!` injects `#[serde(rename_all = "kebab-case")]`, which core +// reads like any other container attribute. That keeps the documented +// migration path producing the same values as the production derive, even +// though core itself applies no default casing. +mod derive_enum_helper { + use super::*; + use schematic::derive_enum; + + derive_enum!( + #[derive(ConfigEnum, Default)] + pub enum Level { + #[default] + VeryHigh, + Low, + } + ); + + #[test] + fn picks_up_the_injected_rename_all() { + assert_eq!(Level::from_str("very-high").unwrap(), Level::VeryHigh); + assert_eq!(Level::VeryHigh.to_string(), "very-high"); + } + + #[test] + fn serializes_consistently_with_the_schema() { + // serde uses the injected attribute, the schema uses the same names + assert_eq!( + serde_json::to_string(&Level::VeryHigh).unwrap(), + "\"very-high\"" + ); + + let schema = SchemaBuilder::build_root::(); + let SchemaType::Enum(inner) = &schema.ty else { + panic!("expected an enum"); + }; + + assert_eq!( + inner.variants.as_ref().unwrap().keys().collect::>(), + vec!["very-high", "low"] + ); + } +} diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index 4cc2fd35..cf436a34 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -26,5 +26,4 @@ config = [] env = [] extends = [] schema = [] -tracing = [] validate = [] diff --git a/crates/macros/src/config/mod.rs b/crates/macros/src/config/mod.rs index a74f2114..9aa277a1 100644 --- a/crates/macros/src/config/mod.rs +++ b/crates/macros/src/config/mod.rs @@ -4,7 +4,6 @@ pub mod field_value; pub mod variant; use crate::common::Macro; -use crate::utils::instrument_quote; use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote}; @@ -33,7 +32,6 @@ impl ToTokens for ConfigMacro<'_> { let merge = cfg.type_of.generate_merge(); let from_partial = cfg.type_of.generate_from_partial(&partial_name); let settings_metadata = cfg.type_of.generate_settings_metadata(); - let instrument = instrument_quote(); let context = match cfg.args.context.as_ref() { Some(ctx) => quote! { #ctx }, @@ -44,7 +42,6 @@ impl ToTokens for ConfigMacro<'_> { let env_values = cfg.type_of.generate_env_values(); quote! { - #instrument fn env_values() -> std::result::Result, schematic::ConfigError> { use schematic::internal::*; #env_values @@ -58,7 +55,6 @@ impl ToTokens for ConfigMacro<'_> { let extends_from = cfg.type_of.generate_extends_from(); quote! { - #instrument fn extends_from(&self) -> Option { #extends_from } @@ -71,7 +67,6 @@ impl ToTokens for ConfigMacro<'_> { let validate = cfg.type_of.generate_validate(); quote! { - #instrument fn validate_with_path( &self, context: &Self::Context, @@ -98,7 +93,6 @@ impl ToTokens for ConfigMacro<'_> { impl schematic::PartialConfig for #partial_name { type Context = #context; - #instrument fn default_values(context: &Self::Context) -> std::result::Result, schematic::ConfigError> { use schematic::internal::*; #default_values @@ -108,12 +102,10 @@ impl ToTokens for ConfigMacro<'_> { #extends_method - #instrument fn finalize(self, context: &Self::Context) -> std::result::Result { #finalize } - #instrument fn merge( &mut self, context: &Self::Context, @@ -128,7 +120,6 @@ impl ToTokens for ConfigMacro<'_> { #[automatically_derived] impl Default for #name { - #instrument fn default() -> Self { let context = <::Partial as schematic::PartialConfig>::Context::default(); @@ -142,12 +133,10 @@ impl ToTokens for ConfigMacro<'_> { impl schematic::Config for #name { type Partial = #partial_name; - #instrument fn from_partial(partial: Self::Partial) -> Self { #from_partial } - #instrument fn settings() -> schematic::ConfigSettingMap { #settings_metadata } @@ -169,7 +158,6 @@ impl ToTokens for ConfigMacro<'_> { Some(#schema_name.into()) } - #instrument fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { use schematic::schema::*; @@ -183,7 +171,6 @@ impl ToTokens for ConfigMacro<'_> { Some(#partial_schema_name.into()) } - #instrument fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { #partial_schema_impl schema diff --git a/crates/macros/src/config_enum/mod.rs b/crates/macros/src/config_enum/mod.rs index e5251f7a..a25d68a3 100644 --- a/crates/macros/src/config_enum/mod.rs +++ b/crates/macros/src/config_enum/mod.rs @@ -177,12 +177,9 @@ pub fn macro_impl(item: TokenStream) -> TokenStream { #[cfg(feature = "schema")] { - use crate::utils::{ - extract_comment, extract_deprecated, instrument_quote, map_option_argument_quote, - }; + use crate::utils::{extract_comment, extract_deprecated, map_option_argument_quote}; let default_index = map_option_argument_quote(default_index); - let instrument = instrument_quote(); let deprecated = if let Some(comment) = extract_deprecated(&attrs) { quote! { schema.set_deprecated(#comment); } @@ -202,7 +199,6 @@ pub fn macro_impl(item: TokenStream) -> TokenStream { Some(#meta_name.into()) } - #instrument fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { use schematic::schema::*; #deprecated diff --git a/crates/macros/src/schematic/mod.rs b/crates/macros/src/schematic/mod.rs index 92a42451..29c8d22a 100644 --- a/crates/macros/src/schematic/mod.rs +++ b/crates/macros/src/schematic/mod.rs @@ -1,5 +1,4 @@ use crate::common::Macro; -use crate::utils::instrument_quote; use proc_macro2::TokenStream; use quote::{ToTokens, quote}; @@ -12,7 +11,6 @@ impl ToTokens for SchematicMacro<'_> { let schema_name = cfg.get_name(); let schema_impl = cfg.type_of.generate_schema(&cfg.attrs); - let instrument = instrument_quote(); let (impl_generics, ty_generics, where_clause) = cfg.generics.split_for_impl(); tokens.extend(quote! { @@ -22,7 +20,6 @@ impl ToTokens for SchematicMacro<'_> { Some(#schema_name.into()) } - #instrument fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { use schematic::schema::*; diff --git a/crates/macros/src/utils.rs b/crates/macros/src/utils.rs index 444e9679..c4beeae3 100644 --- a/crates/macros/src/utils.rs +++ b/crates/macros/src/utils.rs @@ -193,11 +193,3 @@ pub fn map_option_argument_quote(value: Option) -> proc_macro2:: } } } - -pub fn instrument_quote() -> proc_macro2::TokenStream { - #[cfg(feature = "tracing")] - quote! { #[tracing::instrument(skip_all)] } - - #[cfg(not(feature = "tracing"))] - quote! {} -} diff --git a/crates/schematic/Cargo.toml b/crates/schematic/Cargo.toml index 450f49e1..6bcaa7e3 100644 --- a/crates/schematic/Cargo.toml +++ b/crates/schematic/Cargo.toml @@ -69,13 +69,13 @@ reqwest = { workspace = true, optional = true, features = ["blocking"] } [features] default = ["config", "env", "extends", "validate"] config = [ + "dep:convert_case", "dep:serde_path_to_error", "dep:starbase_styles", "schematic_macros/config", ] schema = ["dep:convert_case", "dep:indexmap", "schematic_macros/schema"] schema_serde = ["schema", "schematic_types/serde"] -tracing = ["schematic_macros/tracing"] # Features env = ["schematic_macros/env"] @@ -127,7 +127,6 @@ schematic = { path = ".", features = [ "schema", "schema_serde", "toml", - "tracing", "type_chrono", "type_indexmap", "type_regex", diff --git a/crates/schematic/src/internal/mod.rs b/crates/schematic/src/internal/mod.rs index 1e6a1288..e4ab76c8 100644 --- a/crates/schematic/src/internal/mod.rs +++ b/crates/schematic/src/internal/mod.rs @@ -12,6 +12,40 @@ use crate::config::{ConfigError, HandlerError, MergeError, MergeResult, PartialC use schematic_types::Schema; use std::str::FromStr; +// CASING + +/// Normalize a string into the provided case, using the same format names as +/// serde's `rename_all`. Backs `#[config(before_parse)]`, where the incoming +/// value is reshaped before it is matched against a variant. +/// +/// The format is validated at derive time, so an unknown one is unreachable +/// from generated code. +pub fn format_case(value: &str, format: &str) -> String { + use convert_case::{Boundary, Case, Casing}; + + let case = match format { + // `Case::Lower`/`Case::Upper` are space delimited, so these two are + // handled directly rather than through a case conversion + "lowercase" => return value.to_lowercase(), + "UPPERCASE" => return value.to_uppercase(), + "PascalCase" => Case::Pascal, + "camelCase" => Case::Camel, + "snake_case" => Case::Snake, + "SCREAMING_SNAKE_CASE" => Case::UpperSnake, + "kebab-case" => Case::Kebab, + "SCREAMING-KEBAB-CASE" => Case::UpperKebab, + other => { + panic!("Unknown `before_parse` value `{other}`."); + } + }; + + value + // Unlike the derive-time equivalent this does not hint at an incoming + // case, since the value came from a user rather than a Rust ident + .remove_boundaries(&[Boundary::UpperDigit, Boundary::LowerDigit]) + .to_case(case) +} + // DEFAULT VALUES pub fn handle_default_result( From 24310688820f1219a17130ef96239276785e6310 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Wed, 19 Aug 2026 22:48:32 -0700 Subject: [PATCH 14/18] new: Support generics. (#198) --- AGENTS.md | 23 ++- CHANGELOG.md | 17 +- crates/core/src/container.rs | 109 +++++++++-- crates/core/src/variant.rs | 6 +- crates/core/tests/container_type_test.rs | 74 ++++++++ crates/core/tests/derive_config_enum_test.rs | 18 ++ ..._schema_test__schematic__named_struct.snap | 2 +- ...ner_schema_test__schematic__unit_enum.snap | 2 +- ..._schema_test__schematic__unnamed_enum.snap | 2 +- ...chema_test__schematic__unnamed_struct.snap | 2 +- ...ner_type_test__generics__named_struct.snap | 111 +++++++++++ ...cs__supports_bounds_and_where_clauses.snap | 121 ++++++++++++ ...ner_type_test__generics__unnamed_enum.snap | 117 ++++++++++++ ...r_type_test__generics__unnamed_struct.snap | 98 ++++++++++ ...ged_enum_deserialize_carries_generics.snap | 39 ++++ ...est__schematic__implements_both_types.snap | 2 +- ...er_type_test__to_tokens__named_struct.snap | 2 +- ...ainer_type_test__to_tokens__unit_enum.snap | 2 +- ...er_type_test__to_tokens__unnamed_enum.snap | 2 +- ..._type_test__to_tokens__unnamed_struct.snap | 2 +- ...enum_test__generics__generic_fallback.snap | 94 ++++++++++ ...m_test__rendering__supports_a_default.snap | 5 +- ..._test__rendering__supports_a_fallback.snap | 8 +- ...est__rendering__supports_before_parse.snap | 4 +- ...ndering__supports_renames_and_aliases.snap | 9 +- ...onfig_enum_test__rendering__unit_enum.snap | 9 +- crates/macros-next/tests/config_enum_test.rs | 76 ++++++++ crates/macros-next/tests/config_test.rs | 173 ++++++++++++++++++ 28 files changed, 1078 insertions(+), 51 deletions(-) create mode 100644 crates/core/tests/snapshots/container_type_test__generics__named_struct.snap create mode 100644 crates/core/tests/snapshots/container_type_test__generics__supports_bounds_and_where_clauses.snap create mode 100644 crates/core/tests/snapshots/container_type_test__generics__unnamed_enum.snap create mode 100644 crates/core/tests/snapshots/container_type_test__generics__unnamed_struct.snap create mode 100644 crates/core/tests/snapshots/container_type_test__generics__untagged_enum_deserialize_carries_generics.snap create mode 100644 crates/core/tests/snapshots/derive_config_enum_test__generics__generic_fallback.snap 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); + } +} From 181ccb1ae8f8ddf4e75f569b36d8489fdb8ab564 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Thu, 20 Aug 2026 11:33:33 -0700 Subject: [PATCH 15/18] breaking: Migrate to the new macros crate. (#199) --- AGENTS.md | 115 ++-- CHANGELOG.md | 40 +- Cargo.lock | 29 +- Cargo.toml | 4 +- book/src/SUMMARY.md | 1 + book/src/config/context.md | 28 + book/src/config/experimental.md | 2 +- book/src/config/generics.md | 84 +++ book/src/config/partial.md | 4 +- book/src/config/settings.md | 50 +- book/src/config/struct/env.md | 36 +- book/src/config/struct/index.md | 32 +- book/src/config/struct/merge.md | 2 +- book/src/config/struct/validate.md | 27 +- book/src/schema/enum.md | 44 +- book/src/schema/external.md | 25 +- book/src/schema/generator/index.md | 8 +- book/src/schema/struct.md | 2 +- book/src/schema/unknown.md | 2 +- crates/core/src/field.rs | 6 +- crates/core/src/field_value.rs | 12 +- crates/core/src/utils.rs | 25 +- crates/core/src/value.rs | 36 +- crates/core/src/variant.rs | 34 +- crates/core/tests/setting_env_test.rs | 8 +- ...__unnamed_enum__supports_adjacent_tag.snap | 134 ++-- ...type__unnamed_enum__supports_external.snap | 96 +-- ...er_type_test__to_tokens__named_struct.snap | 5 +- ...er_type_test__to_tokens__unnamed_enum.snap | 2 +- ...e__named_struct__accepts_curried_func.snap | 2 +- ...idate__named_struct__accepts_func_ref.snap | 2 +- ...lidate__named_struct__supports_nested.snap | 8 +- ...d_struct__supports_nested_collections.snap | 18 +- ...amed_struct__supports_nested_wrappers.snap | 2 +- ...date__named_struct__supports_standard.snap | 10 +- ...idate__named_struct__uses_serde_names.snap | 2 +- ...e__unnamed_enum__accepts_curried_func.snap | 3 +- ...idate__unnamed_enum__accepts_func_ref.snap | 2 +- ...e__unnamed_enum__supports_many_values.snap | 18 +- ...lidate__unnamed_enum__supports_nested.snap | 8 +- ...med_enum__supports_nested_collections.snap | 28 +- ...nnamed_enum__supports_nested_wrappers.snap | 4 +- ...date__unnamed_enum__supports_standard.snap | 10 +- ...idate__unnamed_enum__uses_serde_names.snap | 2 +- ..._unnamed_struct__accepts_curried_func.snap | 2 +- ...ate__unnamed_struct__accepts_func_ref.snap | 2 +- ...date__unnamed_struct__supports_nested.snap | 8 +- ...d_struct__supports_nested_collections.snap | 18 +- ...te__unnamed_struct__supports_standard.snap | 10 +- crates/macros-next/Cargo.toml | 49 -- crates/macros-next/src/lib.rs | 38 -- crates/macros/Cargo.toml | 36 +- crates/macros/src/common/container.rs | 211 ------- crates/macros/src/common/field.rs | 389 ------------ crates/macros/src/common/field_value.rs | 236 ------- crates/macros/src/common/macros.rs | 305 ---------- crates/macros/src/common/mod.rs | 20 - crates/macros/src/common/variant.rs | 307 ---------- crates/macros/src/config/container.rs | 576 ------------------ crates/macros/src/config/field.rs | 211 ------- crates/macros/src/config/field_value.rs | 227 ------- crates/macros/src/config/mod.rs | 193 ------ crates/macros/src/config/variant.rs | 292 --------- crates/macros/src/config_enum/mod.rs | 229 ------- crates/macros/src/config_enum/variant.rs | 161 ----- crates/macros/src/lib.rs | 29 +- crates/macros/src/schematic/mod.rs | 31 - crates/macros/src/utils.rs | 195 ------ .../tests/config_enum_test.rs | 2 +- .../tests/config_test.rs | 2 +- .../tests/schematic_test.rs | 2 +- crates/schematic/examples/show_error.rs | 2 +- crates/schematic/src/config/loader.rs | 2 + crates/schematic/src/config/mod.rs | 2 + crates/schematic/src/config/settings/mod.rs | 12 + crates/schematic/src/config/settings/regex.rs | 15 + .../schematic/src/config/settings/semver.rs | 15 + crates/schematic/src/internal/env.rs | 2 +- crates/schematic/src/internal/mod.rs | 2 + crates/schematic/src/internal/validate.rs | 64 +- crates/schematic/src/lib.rs | 3 + crates/schematic/src/schema/renderer.rs | 4 +- crates/schematic/tests/env_test.rs | 2 +- crates/schematic/tests/generator_test.rs | 8 +- crates/schematic/tests/macro_enum_test.rs | 18 +- crates/schematic/tests/macros_test.rs | 2 +- crates/schematic/tests/merge_test.rs | 2 +- crates/schematic/tests/partialize_test.rs | 10 +- crates/schematic/tests/settings_test.rs | 30 +- ...e_sources_test__generates_json_schema.snap | 1 - .../defaults_test__generates_json_schema.snap | 44 +- .../defaults_test__generates_typescript.snap | 26 +- .../env_test__generates_json_schema.snap | 1 - .../env_test__generates_typescript.snap | 6 - .../extends_test__generates_json_schema.snap | 1 - .../extends_test__generates_typescript.snap | 3 +- ...generator_test__json_schema__defaults.snap | 24 +- ...rator_test__json_schema__not_required.snap | 12 +- ...generator_test__json_schema__partials.snap | 36 +- ...est__json_schema__with_markdown_descs.snap | 24 +- ...erator_test__json_schema__with_titles.snap | 44 +- ...or_test__template_json__custom_values.snap | 20 +- ...nerator_test__template_json__defaults.snap | 18 +- ...test__template_json__without_comments.snap | 14 +- ...enerator_test__template_pkl__defaults.snap | 18 +- ...nerator_test__template_toml__defaults.snap | 18 +- ...nerator_test__template_yaml__defaults.snap | 18 +- ...nerator_test__typescript__const_enums.snap | 18 +- .../generator_test__typescript__defaults.snap | 12 +- .../generator_test__typescript__enums.snap | 18 +- ...erator_test__typescript__exclude_refs.snap | 12 +- ...ator_test__typescript__external_types.snap | 12 +- .../generator_test__typescript__no_refs.snap | 12 +- ...ator_test__typescript__object_aliases.snap | 12 +- .../generator_test__typescript__partials.snap | 24 +- ...ator_test__typescript__props_optional.snap | 12 +- ..._typescript__props_optional_undefined.snap | 12 +- ...nerator_test__typescript__value_enums.snap | 18 +- ...acro_enum_test__generates_json_schema.snap | 216 +++---- ...macro_enum_test__generates_typescript.snap | 82 ++- .../macros_test__generates_json_schema.snap | 70 +-- .../macros_test__generates_typescript-2.snap | 69 +-- .../macros_test__generates_typescript.snap | 60 +- .../snapshots/partialize_test__compounds.snap | 4 +- .../partialize_test__enum_adjacent.snap | 33 +- .../partialize_test__enum_external.snap | 198 ++---- .../partialize_test__enum_internal.snap | 40 +- .../snapshots/partialize_test__enums.snap | 322 ++++------ .../snapshots/partialize_test__nested.snap | 2 +- .../partialize_test__nested_list.snap | 2 +- .../partialize_test__nested_map.snap | 2 +- .../partialize_test__primitives.snap | 8 +- .../settings_test__generates_json_schema.snap | 26 +- .../settings_test__generates_typescript.snap | 14 +- .../variants_test__generates_json_schema.snap | 3 +- .../variants_test__generates_typescript.snap | 3 +- crates/schematic/tests/validate_test.rs | 8 +- crates/schematic/tests/variants_test.rs | 3 +- rust-toolchain.toml | 2 +- 139 files changed, 1627 insertions(+), 5018 deletions(-) create mode 100644 book/src/config/generics.md delete mode 100644 crates/macros-next/Cargo.toml delete mode 100644 crates/macros-next/src/lib.rs delete mode 100644 crates/macros/src/common/container.rs delete mode 100644 crates/macros/src/common/field.rs delete mode 100644 crates/macros/src/common/field_value.rs delete mode 100644 crates/macros/src/common/macros.rs delete mode 100644 crates/macros/src/common/mod.rs delete mode 100644 crates/macros/src/common/variant.rs delete mode 100644 crates/macros/src/config/container.rs delete mode 100644 crates/macros/src/config/field.rs delete mode 100644 crates/macros/src/config/field_value.rs delete mode 100644 crates/macros/src/config/mod.rs delete mode 100644 crates/macros/src/config/variant.rs delete mode 100644 crates/macros/src/config_enum/mod.rs delete mode 100644 crates/macros/src/config_enum/variant.rs delete mode 100644 crates/macros/src/schematic/mod.rs delete mode 100644 crates/macros/src/utils.rs rename crates/{macros-next => macros}/tests/config_enum_test.rs (99%) rename crates/{macros-next => macros}/tests/config_test.rs (99%) rename crates/{macros-next => macros}/tests/schematic_test.rs (99%) diff --git a/AGENTS.md b/AGENTS.md index b0ed2c11..1ed9361f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,52 +18,54 @@ rest follows. ## Workspace map -| Crate | Package | Role | -| -------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| `crates/schematic` | `schematic` | The user-facing runtime: `Config`/`PartialConfig` traits, loader, merge/validate/env helpers, schema renderers. | -| `crates/types` | `schematic_types` | `Schema`, `SchemaBuilder`, `SchemaType`, the `Schematic` trait. No macro code. | -| `crates/macros` | `schematic_macros` | **The production derive.** Currently what ships. | -| `crates/core` | `schematic_core` | **The rewrite in progress.** Where new work goes. | -| `crates/macros-next` | `schematic_macros_next` | Thin proc-macro shell over `core`. Exports `Config`, `ConfigEnum`, and `Schematic`. | -| `crates/test-app` | `test_app` | Manual smoke-test binary using the production derive. | +| Crate | Package | Role | +| ------------------ | ------------------ | -------------------------------------------------------------------------------------------------------------- | +| `crates/schematic` | `schematic` | The user-facing runtime: `Config`/`PartialConfig` traits, loader, merge/validate/env helpers, schema renderers. | +| `crates/types` | `schematic_types` | `Schema`, `SchemaBuilder`, `SchemaType`, the `Schematic` trait. No macro code. | +| `crates/core` | `schematic_core` | **Where the derive lives.** Container/field/variant analysis and all codegen. | +| `crates/macros` | `schematic_macros` | Thin proc-macro shell over `core`. Exports `Config`, `ConfigEnum`, and `Schematic`. | +| `crates/test-app` | `test_app` | Manual smoke-test binary. | ### Migration status — read this first -`crates/macros` is production — it is what `crates/schematic` re-exports, and what users get today. -`crates/core` is a piece-by-piece rewrite of it, consumed by `crates/macros-next`. +The rewrite has landed. The old hand-written derive was deleted and `crates/macros-next` was renamed +to `crates/macros`, so `schematic_macros` is now a thin shell over `core` and there is only one +derive implementation. Anything describing `macros` as a separate production implementation is +stale. -`macros-next` exports working `Config`, `ConfigEnum`, and `Schematic` derives, exercised by real -compile-and-run tests in `crates/macros-next/tests/`. **Nothing depends on it yet** — -`crates/schematic` still re-exports `schematic_macros`, so swapping the two is a separate step -blocked on the gaps below. +All three derives declare `serde` as a helper attribute, so `#[serde(untagged)]`, `#[serde(tag)]`, +and friends are legal on a type that derives only `Config`. That is the replacement for the removed +`#[config(serde(...))]`. -Still unported, in rough order of what blocks the swap: +Deliberately dropped in the rewrite, and not coming back: -- **`#[setting(skip_deserializing_if)]`** parses but emits nothing (serde has no such attribute). -- `#[setting(exclude)]` is behind `cfg(schema)` in core but unconditional in production, so it fails - to parse when the feature is off. +- `#[config(serde(...))]` — write the native `#[serde(...)]` attribute instead. +- `#[variant(value)]` — `#[variant(rename)]` covers it. +- The `tracing` feature. Generated code is no longer wrapped in `#[tracing::instrument]`; the loader + still is. +- Named-field enum variants panic, as they did in the old derive too. -Not gaps, despite looking like them: named-field enum variants panic in *both* crates, and -`#[config(serde(...))]`, `#[variant(value)]`, and the `tracing` feature were all deliberately -removed in the rewrite — `#[variant(rename)]` covers what `value` did, and generated code is no -longer wrapped in `#[tracing::instrument]` (the loader still is). +Known remaining hole: **`#[setting(skip_deserializing_if)]`** parses but emits nothing, because +serde has no such attribute. `ConfigEnum` has no implementation of its own. It is the same `Container` with `macro_type = ContainerMacro::ConfigUnitEnum`, which swaps what `ToTokens` renders — the `ConfigEnum`/`FromStr`/`TryFrom`/`Display` impls plus `impl_schematic_full`. The schema comes out of the ordinary unit-enum path: `get_tag_format` reports `Unit` for a config enum (even when a `fallback` variant makes it structurally unnamed), so `impl_schema_type` emits a literal per unit -variant and infers the fallback's inner type as a string, matching production. +variant and infers the fallback's inner type as a string, as the old derive did. -**Casing is a deliberate divergence, not a gap.** Production defaults to `camelCase` for struct -fields and `kebab-case` for enum variants. Core has *no default* — a name is used exactly as -written, so Rust's `snake_case` is what ships unless `rename_all` says otherwise. When `rename_all` +**Casing has no default.** The old derive applied `camelCase` to struct fields and `kebab-case` to +enum variants. Core applies nothing — a name is used exactly as written, so Rust's `snake_case` is +what ships unless `rename_all` says otherwise. This was the single largest source of churn when the +rewrite landed, and it is why so many fixtures and snapshots in `crates/schematic/tests` are +snake_case. When `rename_all` is set, `Field::get_name` and `Variant::get_name` apply it via `format_case`, so schemas, `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. +casing and stay derived from the Rust name (or an explicit `rename`). -**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 +**All three derives support generics**, where the old derive supported them only on `Schematic`. +`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: @@ -81,9 +83,11 @@ A generic `ConfigEnum` only makes sense with a `fallback`, since unit variants c `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 -behavior and the shapes users depend on. +The old hand-written derive is gone from the worktree but still reachable through git history, at +`git show :crates/macros/...`. It is a useful reference for *intended* behavior +and the shapes users depend on, but it is **not** a correctness oracle — the rewrite fixed a long +list of real bugs in it, several of which only surfaced when `schematic`'s own suite was pointed at +`core`. ## The Config ↔ Partial model @@ -197,11 +201,13 @@ isolation and snapshots the pretty-printed tokens. Type errors, missing trait bo cross-method inconsistencies are invisible to them. Several real bugs in this codebase sat in green snapshots for exactly this reason. -`crates/macros-next/tests/` is the antidote: it derives against the real runtime and asserts on -behavior, so it catches what snapshots can't. Run it for any `core` change that alters codegen. +`crates/macros/tests/` is the antidote: it derives against the real runtime and asserts on +behavior, so it catches what snapshots can't. `crates/schematic/tests/` now does too, since it +compiles against `core`. Run both for any `core` change that alters codegen. ```bash -cargo test -p schematic_macros_next +cargo test -p schematic_macros +cargo test -p schematic ``` Snapshot tests (starbase_sandbox/insta): @@ -241,9 +247,22 @@ cargo clippy --workspace --all-targets cargo fmt --all --check ``` -Known pre-existing clippy warnings (not yours): a `count` loop counter in `core/src/variant.rs` and -`macros/src/config/variant.rs`, and a collapsible `if` in `macros/src/utils.rs` and -`schematic/src/schema/renderers/template.rs`. +`cargo clippy --workspace --all-targets` is clean, and so is `cargo build -p schematic` on its own. +If you see a warning now, it is yours. + +Those two are not the same check. `--workspace --all-targets` unifies features across crates, and +dev-dependencies turn nearly everything on, so a re-export or `mut` that is dead under a narrower +feature set stays invisible there. Build individual crates, and a few feature combinations, before +claiming a change is warning-free: + +```bash +cargo build -p schematic # default features only +cargo build -p schematic --no-default-features # nothing on +cargo build -p schematic --no-default-features --features config # one feature at a time +``` + +The same unification is why `cargo test --workspace` can pass while a real feature combination +fails to compile — see the note about `schematic_types` below. Also run `cargo test -p schematic_types` on its own. `cargo test --workspace` unifies features across crates, so a crate whose own feature list is incomplete still compiles there — that hid a broken @@ -261,7 +280,9 @@ These were deliberated and settled. If something looks wrong, it probably isn't. keys _derived_ from a setting name get the prefix, and only when the container declares `env_prefix`. Deriving keys for every container would force `FromStr` on every setting, which breaks `Duration`, tuples, etc. A parent's `#[setting(nested, env_prefix)]` therefore only - overrides a child that declares one. `settings()` reports explicit keys only. + overrides a child that declares one. `settings()` reports explicit keys only, and so does + `SchemaField.env_var` — which is why a derived key no longer shows up as an `@env` annotation in + a rendered template, where the old derive emitted one. - **Nested collections replace by default** on merge; bare nested configs merge recursively. Supply `merge` to change it. - **Nested tuple variants support multiple values**, each position handled per its own shape (merge, @@ -273,10 +294,26 @@ These were deliberated and settled. If something looks wrong, it probably isn't. - **Derive-time panics for wrong attribute usage are intentional.** The maintainer wants loud failure over silent no-ops. `#[setting(nested)]` on a primitive panics. +- **Validators are called through a generated closure**, never passed by value, so that the value + deref coerces on its way in — a `String` setting reaches a `&str` validator and a `Vec` a + `&[T]` one. `ValidateManager::check` therefore takes `impl FnOnce(V, &D, &Ctx, bool)` and takes + the value by move, because a variant of several values passes a *tuple of references*, not a + reference to a tuple. Boxing the validator instead breaks built-ins like `validate::extends_string`. +- **A doc comment becomes one flowing paragraph**, not one line per source line, with markdown list + items kept on their own line. A block comment (`/** ... */`) arrives as a single multi-line + attribute value, so its leading `*` is a continuation marker and gets stripped; a `///` line + arrives as its own attribute, so a `*` there is markdown and survives. + ## Gotchas - `#[setting(...)]` and `#[serde(...)]` both feed the derive. Setting attrs take precedence; aliases from both are merged. +- All three derives declare `serde` as a helper attribute, so `#[serde(...)]` is accepted on a type + that derives only `Config`. Two derives declaring the same helper is fine — serde's own + `Serialize`/`Deserialize` pair does it. +- `Schema::partialize()` on a tagged enum variant marks the **value**, never the `{tag, content}` + struct synthesized around it. Marking the wrapper makes the runtime nullify the tag, so the + partial schema claims `{"type": null}` is valid. - Unit enums must stay externally tagged — `#[serde(untagged)]` on a unit-only enum makes variants deserializable only from `null`. - `SchemaField` metadata is derive-time only; `partialize_schema` at runtime is what turns a full diff --git a/CHANGELOG.md b/CHANGELOG.md index cc0a194f..93dcabc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,12 @@ while the schema types have been updated to be more flexible and composable. 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. +- Changed doc comments to render as a single flowing paragraph instead of one line per source line. + Markdown list items are still kept on their own line. +- Changed `#[setting(env_prefix)]` keys to no longer appear as `@env` annotations in generated + templates. A derived key depends on the prefix in effect at runtime, which a parent's + `#[setting(nested, env_prefix)]` can override, so it isn't known when the schema is built. + Explicit `#[setting(env)]` keys are unaffected. ##### Schema @@ -75,6 +81,10 @@ while the schema types have been updated to be more flexible and composable. - Bounds are not inferred. A generic `Config` needs `Clone + Default + DeserializeOwned + Schematic + Serialize` on its type parameters, since `PartialConfig` requires them. +- Added support for `#[setting(parse_env)]` alongside a container `env_prefix`. It previously + required an explicit `#[setting(env)]` and panicked at derive time otherwise. +- Added support for `#[deprecated(since = "...", note = "...")]`, whose note is now used as the + deprecation message. Only `#[deprecated]` and `#[deprecated = "..."]` were recognized before. - 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, @@ -84,7 +94,7 @@ while the schema types have been updated to be more flexible and composable. - 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. +- Added support for `skip_serializing_if` on fields. - Updated `alias` to support multiple aliases: `#[serde(alias = "alias1", alias = "alias2")]` ##### Schema @@ -112,6 +122,27 @@ while the schema types have been updated to be more flexible and composable. #### 🐞 Fixes +##### Config + +- Fixed validators being unable to accept a borrowed form of the setting. A `String` setting now + reaches a `&str` validator and a `Vec` a `&[T]` one, which built-ins like + `validate::extends_string` and `validate::extends_list` rely on. +- Fixed nested configs in a map keyed by anything other than `String` failing to compile. +- Fixed nested configs wrapped in an `Option` inside a collection, such as `Vec>`, + not being validated. +- Fixed adjacently tagged unit variants declaring a `content` field in their schema. Serde emits + only the tag for a unit variant. +- Fixed the schema of externally and internally tagged unit variants. An externally tagged unit is a + bare string, and an internally tagged one is an object holding just the tag. +- Fixed the partial schema of a tagged enum marking the tag itself as nullable and optional, which + claimed that a variant with a missing or null tag was valid. +- Fixed a block doc comment (`/** ... */`) keeping its leading `*` continuation markers, which + rendered them as stray markdown list items. +- Fixed the `config` feature failing to compile without `env`. +- Fixed the `type_regex` and `type_semver` features failing to compile alongside `config` without + `schema`. Their `Schematic` implementations are now gated, so the setting types themselves no + longer require the schema layer. + ##### Schema - Fixed the `serde` feature not enabling `indexmap/serde`, which made `schematic_types` fail to @@ -134,11 +165,16 @@ while the schema types have been updated to be more flexible and composable. #### ⚙️ Internal +- Moved the derive implementation into a new `schematic_core` crate. `schematic_macros` is now a + thin proc-macro shell over it, and every derive shares one code path. +- Changed the signatures of `internal::ValidateManager`. `check` and `check_variant` take the value + by move and an opaque callable instead of a boxed `Validator`, and `nested_list`/`nested_map` take + items as `Option`s. These are `#[doc(hidden)]` and only called by generated code. - Updated `syn` to v3. - Updated `darling` to v0.24. - Updated `pkl` to v0.8. - Updated `garde` (validation) to v0.23. -- Updated Rust to v1.97. +- Updated Rust to v1.98. - Updated dependencies. ## 0.19.7 diff --git a/Cargo.lock b/Cargo.lock index 4ffdbda4..d7e0d523 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,9 +405,9 @@ checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "darling" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ "darling_core", "darling_macro", @@ -415,9 +415,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ "ident_case", "proc-macro2", @@ -428,9 +428,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core", "quote", @@ -1984,23 +1984,12 @@ dependencies = [ [[package]] name = "schematic_macros" version = "0.19.4" -dependencies = [ - "convert_case", - "darling", - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "schematic_macros_next" -version = "0.18.7" dependencies = [ "proc-macro2", "quote", "schematic", "schematic_core", - "schematic_macros_next", + "schematic_macros", "serde", "serde_json", "starbase_sandbox", @@ -2745,9 +2734,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.24.0" +version = "1.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" dependencies = [ "js-sys", "serde_core", diff --git a/Cargo.toml b/Cargo.toml index b80db19c..cbc86f9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = ["crates/*"] [workspace.dependencies] chrono = "0.4.45" convert_case = "0.11.0" -darling = "0.24.0" +darling = "0.24.1" indexmap = "2.14.0" miette = "7.6.0" proc-macro2 = "1.0.107" @@ -26,4 +26,4 @@ syn = "3.0.3" toml = "1.1.4" tracing = "0.1.44" url = "2.5.8" -uuid = "1.24.0" +uuid = "1.24.1" diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 8dc2137c..34bcb221 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -6,6 +6,7 @@ - [Partials](./config/partial.md) - [Nesting](./config/nested.md) - [Context](./config/context.md) + - [Generics](./config/generics.md) - [Structs & enums](./config/struct/index.md) - [Default values](./config/struct/default.md) - [Transforming values](./config/struct/transform.md) diff --git a/book/src/config/context.md b/book/src/config/context.md index f109d87b..871b8070 100644 --- a/book/src/config/context.md +++ b/book/src/config/context.md @@ -43,3 +43,31 @@ let result = ConfigLoader::::new() > Refer to the [default values](./struct/default.md), [merge strategies](./struct/merge.md), and > [validation rules](./struct/validate.md) sections for more information on how to use context. + +## Metadata + +Alongside the configuration itself, the derive records a little metadata about each +[setting](./settings.md), reachable with +[`Config::settings()`](https://docs.rs/schematic/latest/schematic/trait.Config.html#method.settings). +It returns a map keyed by the serde name of each setting, or by position for unnamed ones. + +```rust +for (name, setting) in ExampleConfig::settings() { + println!("{name}: {}", setting.type_alias); + + if let Some(key) = &setting.env_key { + println!(" reads {key}"); + } + + if let Some(nested) = &setting.nested { + println!(" has {} nested settings", nested.len()); + } +} +``` + +Each entry carries the setting's `type_alias` (the Rust type as written), its `env_key`, and a +`nested` map when the setting holds another [nested config](./nested.md). + +> Only an explicit `#[setting(env)]` populates `env_key`. A key derived from an +> [`env_prefix`](./struct/env.md#container-prefixes) depends on the prefix in effect at runtime, so +> it isn't known here. diff --git a/book/src/config/experimental.md b/book/src/config/experimental.md index 599e121c..27163af4 100644 --- a/book/src/config/experimental.md +++ b/book/src/config/experimental.md @@ -9,7 +9,7 @@ configuration format built and maintained by Apple. ```pkl port = 3000 secure = true -allowedHosts = List(".localhost") +allowed_hosts = List(".localhost") ``` > Pkl support can be enabled with the `pkl` Cargo feature. diff --git a/book/src/config/generics.md b/book/src/config/generics.md new file mode 100644 index 00000000..095cc6a5 --- /dev/null +++ b/book/src/config/generics.md @@ -0,0 +1,84 @@ +# Generics + +[`Config`](./struct/index.md), [`ConfigEnum`](./enum/index.md), and +[`Schematic`](../schema/index.md) can all be derived for types with type parameters. The parameters +are carried into the [partial](./partial.md) and into every generated implementation. + +```rust +#[derive(Config)] +struct Wrapper { + pub inner: T, + pub label: String, +} +``` + +## Bounds are not inferred + +The derive does not add bounds for you. A partial has to be `Clone + Default + DeserializeOwned + +Schematic + Serialize`, because that is what +[`PartialConfig`](https://docs.rs/schematic/latest/schematic/trait.PartialConfig.html) requires, so +every type parameter needs them too. A type that is missing one fails to compile at the generated +`impl`, not at the definition. + +Declaring them once as a trait alias keeps the noise down: + +```rust +use serde::{Serialize, de::DeserializeOwned}; + +pub trait Setting: + Clone + Default + PartialEq + Serialize + DeserializeOwned + schematic::Schematic +{ +} + +impl Setting for T where + T: Clone + Default + PartialEq + Serialize + DeserializeOwned + schematic::Schematic +{ +} + +#[derive(Config)] +struct Wrapper { + pub inner: T, + pub label: String, +} +``` + +A generic `Schematic` only needs `T: Schematic`, since it has no partial. + +## Serde bounds + +The partial is emitted with an explicit `#[serde(bound(deserialize = "T: DeserializeOwned"))]`. +Without it serde would infer a `T: Deserialize<'de>` bound of its own, which is ambiguous against +the `DeserializeOwned` already in scope. Supplying your own bound through +`#[config(partial(serde(bound(...))))]` suppresses the generated one. + +## Schema names + +Schemas are keyed by name alone, so every instantiation of a generic type would otherwise claim the +same one. The derive appends each type argument to the name, so `Wrapper` becomes +`WrapperString` and its partial `PartialWrapperString`. See +[defining names](../schema/types.md#defining-names) for the manual equivalent. + +## Enums + +A generic [`ConfigEnum`](./enum/index.md) only makes sense with a +[fallback variant](./enum/fallback.md), since unit variants carry no data. The parameter needs +whatever the generated implementations use: `Default` for +[`variants()`](https://docs.rs/schematic/latest/schematic/trait.ConfigEnum.html#tymethod.variants), +`TryFrom<&str>` for parsing into the fallback, `Display` for formatting back out, and `Schematic` +for the schema name. + +```rust +#[derive(Clone, Debug, PartialEq, ConfigEnum)] +enum Value +where + T: Clone + Default + std::fmt::Display + for<'a> TryFrom<&'a str> + schematic::Schematic, +{ + Known, + Other, + #[variant(fallback)] + Custom(T), +} +``` + +`Display` is written one arm at a time, so the fallback goes through `T: Display` rather than having +to resolve to a `&str`. diff --git a/book/src/config/partial.md b/book/src/config/partial.md index ff5d055c..c8080e3b 100644 --- a/book/src/config/partial.md +++ b/book/src/config/partial.md @@ -9,8 +9,8 @@ For example, the `ExampleConfig` from the [first chapter](../config/index.md) wo following partial struct: ```rust -#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -#[serde(default, deny_unknown_fields, rename_all = "camelCase")] +#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] +#[serde(default, deny_unknown_fields)] pub struct PartialExampleConfig { #[serde(skip_serializing_if = "Option::is_none")] pub number: Option, diff --git a/book/src/config/settings.md b/book/src/config/settings.md index 55987c68..386f011d 100644 --- a/book/src/config/settings.md +++ b/book/src/config/settings.md @@ -3,19 +3,56 @@ Settings are the individual fields of a [`Config` struct](./struct/index.md) or variants of a [`Config` enum](./struct/index.md), and can be annotated with the optional `#[setting]` attribute. +## Third-party types + +A setting that isn't wrapped in `Option` falls back to `Default::default()` when the config provides +no value and no [default value](./struct/default.md) is declared, so every required setting has to +implement `Default`. Plenty of third-party types don't, and being foreign, can't be given it. + +For the two we support directly, we provide a newtype that does: + +| Setting | Wraps | Default | Cargo feature | +| ------------------------------------------------------------------------------------ | ---------------- | ------- | -------------- | +| [`RegexSetting`](https://docs.rs/schematic/latest/schematic/struct.RegexSetting.html) | `regex::Regex` | `.` | `type_regex` | +| [`VersionSetting`](https://docs.rs/schematic/latest/schematic/struct.VersionSetting.html) | `semver::Version` | `0.0.0` | `type_semver` | + +```rust +use schematic::{Config, RegexSetting, VersionSetting}; + +#[derive(Config)] +struct AppConfig { + pub allowed: RegexSetting, + pub version: VersionSetting, +} +``` + +Each derefs to the type it wraps, so its methods can be called directly, and each parses from a +string, so the wrapper works without the upstream crate's `serde` feature. + +> `semver::VersionReq` needs no wrapper, as it already defaults to `*`. Types that are only ever +> optional don't need one either, since an `Option` setting defaults to `None`. + ## Attribute fields The following fields are supported for the `#[setting]` field/variant attribute: -- `default` - Sets the [default value](./struct/default.md). +- `default` - Sets the [default value](./struct/default.md). On an enum variant, this is a marker + that takes no value, and names the variant the partial defaults to. - `env` _(struct only)_ - Sets the [environment variable](./struct/env.md) to receive a value from. +- `env_prefix` _(struct only)_ - Overrides the [environment variable](./struct/env.md#container-prefixes) + prefix of the nested config this field holds. Requires `nested`. +- `exclude` - Omits the field or variant from the generated [schema](../schema/index.md). Has no + effect without the `schema` Cargo feature. - `extend` _(struct only)_ - Enables a configuration to [extend other configs](./struct/extend.md). - `merge` - Defines a function to use for [merging values](./struct/merge.md). - `nested` - Marks the field as using a [nested `Config`](./nested.md). +- `null` _(enum only)_ - Marks the variant as representing `null`, so it is never tagged. - `parse_env` _(struct only)_ - Parses the [environment variable](./struct/env.md) value using a - function. + function. Requires either `env`, or an `env_prefix` on the container. +- `partial` - Forwards attributes to the field on the [partial](./partial.md). - `required` - Marks the field as required. This is useful for `Option` types that do not support `Default`, but require a value. +- `transform` - Defines a function to use for [transforming values](./struct/transform.md). - `validate` - Defines a function to use for [validating values](./struct/validate.md). And the following for serde compatibility: @@ -26,6 +63,8 @@ And the following for serde compatibility: - `skip` - `skip_deserializing` - `skip_serializing` +- `skip_serializing_if` +- `untagged` _(enum only)_ ### Serde support @@ -41,4 +80,9 @@ struct Example { ``` > These values can also be applied using `#[serde]`, which is useful if you want to apply them to -> the main struct as well, and not just the partial struct. +> the main struct as well, and not just the partial struct. When a field is set through both, the +> `#[setting]` value wins, except for `alias`, where the two lists are merged. + +Names are used exactly as written. There is no default casing, so a field named `allowed_hosts` is +parsed as `allowed_hosts` unless a `rename` or `rename_all` says otherwise. An explicit `rename` is +never re-cased by `rename_all`. diff --git a/book/src/config/struct/env.md b/book/src/config/struct/env.md index 1524c9bb..a8796467 100644 --- a/book/src/config/struct/env.md +++ b/book/src/config/struct/env.md @@ -18,9 +18,9 @@ struct AppConfig { ## Container prefixes If you'd prefer to not define `env` for _every_ setting, you can instead define a prefix on the -containing struct using the `#[setting(env_prefix)]` attribute field. This will define an +containing struct using the `#[config(env_prefix)]` container attribute field. This will define an environment variable for _all_ direct fields in the struct, in the format of "env prefix + field -name" in UPPER_SNAKE_CASE. +name" in uppercase. For example, the environment variable below for `port` is now `APP_PORT`. @@ -33,11 +33,17 @@ struct AppConfig { } ``` +A derived key is the field's Rust name (or its explicit `rename`) uppercased, and nothing more. It +is deliberately not reshaped by `rename_all`, so changing how a setting is spelled in a config file +never moves its environment variable. + +An explicit `#[setting(env)]` is absolute. A prefix is never applied to it, and the two cannot be +combined on the same setting. + ### Nested prefixes -Since `env_prefix` only applies to direct fields and not for nested/children structs, you'll need to -define `env_prefix` for each struct, and manually set the prefixes. Schematic _does not concatenate_ -the prefixes between parent and child. +`env_prefix` only applies to direct fields, not to nested children, and prefixes are _not_ +concatenated between parent and child. Each struct declares its own. ```rust #[derive(Config)] @@ -54,6 +60,23 @@ struct AppConfig { } ``` +A parent can override the prefix a nested child uses, with `env_prefix` on the field itself. The +child must still declare an `env_prefix` of its own, as that is what opts its fields into being +derived at all. + +```rust +#[derive(Config)] +struct AppConfig { + #[setting(nested, env_prefix = "OVERRIDE_")] + pub server: AppServerConfig, +} +``` + +> Derived keys aren't known when the [schema](../../schema/index.md) is built, since the prefix in +> effect depends on how the type is nested at runtime. Only explicit `#[setting(env)]` keys appear +> in a generated [config template](../../schema/generator/template.md) or in +> [`Config::settings()`](https://docs.rs/schematic/latest/schematic/trait.Config.html#method.settings). + ## Parsing values We also support parsing environment variables into the required type. For example, the variable may @@ -73,6 +96,9 @@ struct AppConfig { > We provide a handful of built-in parsing functions in the > [`env` module](https://docs.rs/schematic/latest/schematic/env/index.html). +`parse_env` needs a variable to read, so it requires either an `env` on the setting, or an +`env_prefix` on the container. It's a derive-time error otherwise. + ## Parse handler function You can also define your own function for parsing values out of environment variables. diff --git a/book/src/config/struct/index.md b/book/src/config/struct/index.md index 19fe77d5..0a50949e 100644 --- a/book/src/config/struct/index.md +++ b/book/src/config/struct/index.md @@ -14,6 +14,7 @@ struct AppConfig { #[derive(Config)] enum Host { Local, + #[setting(nested)] Remote(HostConfig), } ``` @@ -37,8 +38,7 @@ The following fields are supported for the `#[config]` container attribute: - `context` - Sets the struct to be used as the [context](../context.md). Defaults to `None`. - `env_prefix` - Sets the prefix to use for [environment variable](./env.md#container-prefixes) mapping. Defaults to `None`. -- `serde` - A nested attribute that sets tagging related fields for the [partial](../partial.md). - Defaults to `None`. +- `partial` - Forwards attributes to the [partial](../partial.md). ```rust #[derive(Config)] @@ -52,6 +52,7 @@ And the following for serde compatibility: - `rename` - `rename_all` - Has no default. Field names are used exactly as written unless this is set. +- `rename_all_fields` _(enum only)_ - Applies a casing to the fields of every variant. ## Serde support @@ -60,11 +61,11 @@ By default the [`Config`][config] macro will apply the following `#[serde]` to t layer merging. ```rust -#[serde(default, deny_unknown_fields, rename_all = "camelCase")] +#[serde(default, deny_unknown_fields)] ``` -However, the `deny_unknown_fields` and `rename_all` fields can be customized, and we also support -the `rename` field, both via the top-level `#[config]` attribute. +However, the `deny_unknown_fields` field can be customized, and we also support the `rename` and +`rename_all` fields, all via the top-level `#[config]` attribute. ```rust #[derive(Config)] @@ -77,4 +78,25 @@ struct Example { > These values can also be applied using `#[serde]`, which is useful if you want to apply them to > the main struct as well, and not just the partial struct. +### Enum tagging + +Serde's tagging attributes are read straight off the `#[serde]` attribute and forwarded to the +partial, so `untagged`, `tag`, `content`, and `expecting` work as they normally would. + +```rust +#[derive(Config)] +#[serde(untagged)] +enum Host { + Local, + #[setting(nested)] + Remote(HostConfig), +} +``` + +The derive registers `serde` as a helper attribute, so this is accepted even when the type derives +only [`Config`][config] and not `Serialize` or `Deserialize`. + +> A unit-only enum is always externally tagged, no matter what you set. Marking one `untagged` +> would leave its variants deserializable only from `null`. + [config]: https://docs.rs/schematic/latest/schematic/trait.Config.html diff --git a/book/src/config/struct/merge.md b/book/src/config/struct/merge.md index ac51e5dc..e6f66318 100644 --- a/book/src/config/struct/merge.md +++ b/book/src/config/struct/merge.md @@ -36,7 +36,7 @@ result. If `None` is returned, neither value will be used. Here's an example of the merge function above. ```rust -fn append_vec(mut prev: Vec, next: Vec, context: &Context) -> MergeResult>> { +fn append_vec(mut prev: Vec, next: Vec, context: &Context) -> MergeResult> { prev.extend(next); Ok(Some(prev)) diff --git a/book/src/config/struct/validate.md b/book/src/config/struct/validate.md index 5f035628..67d79ed2 100644 --- a/book/src/config/struct/validate.md +++ b/book/src/config/struct/validate.md @@ -36,6 +36,22 @@ enum Projects { } ``` +A variant holding several values passes them as a tuple of references, not as a reference to a +tuple. + +```rust +fn validate_pair(value: (&String, &usize), partial: &P, context: &C, finalize: bool) -> ValidateResult { + // ... +} + +#[derive(Config)] +enum Entry { + #[setting(validate = validate_pair)] + Pair(String, usize), + // ... +} +``` + > We provide a handful of built-in validation functions in the > [`validate` module](https://docs.rs/schematic/latest/schematic/validate/index.html). Furthermore, > some functions are factories which can be called to produce a validator. @@ -67,14 +83,19 @@ If validation fails, you must return a [`ValidateError`](https://docs.rs/schematic/latest/schematic/struct.ValidateError.html) with a failure message. +The setting is passed by reference, and deref coerces on its way in, so the first argument can take +a borrowed form of the type instead of the type itself. A `String` setting reaches a `&str` +validator, as above, and a `Vec` setting reaches a `&[T]` one. + ### Factories For composition and reusability concerns, we also support factory functions that can be called to create a unique validator. This can be seen above with `schematic::validate::regex`. To create your own factory, declare a normal function, with any number of arguments, that returns a -[`Validator`](https://docs.rs/schematic/latest/schematic/validate/type.Validator.html). +[`Validator`](https://docs.rs/schematic/latest/schematic/type.Validator.html). -Using the `regex` factory as an example, it would look something like this. +Using the `regex` factory as an example, it would look something like this. The closure takes the +same four arguments a plain validator does. ```rust use schematic::Validator; @@ -82,7 +103,7 @@ use schematic::Validator; fn regex(pattern: &str) -> Validator { let pattern = regex::Regex::new(pattern).unwrap(); - Box::new(move |value, _, _| { + Box::new(move |value, _, _, _| { if !pattern.is_match(value) { return Err(ValidateError::new("Some failure message")); } diff --git a/book/src/schema/enum.md b/book/src/schema/enum.md index fa5a507e..150e8c9f 100644 --- a/book/src/schema/enum.md +++ b/book/src/schema/enum.md @@ -3,7 +3,7 @@ The [`EnumType`][enum] can be used to represent a list of [literal values](./literal.md). ```rust -use schematic::{Schematic, Schema, SchemaBuilder, SchemaType, schema::{EnumType, LiteralValue}}; +use schematic::{Schematic, Schema, SchemaBuilder, schema::{EnumType, LiteralValue}}; impl Schematic for T { fn build_schema(mut schema: SchemaBuilder) -> Schema { @@ -47,27 +47,27 @@ schema.enumerable(EnumType { variants: Some(IndexMap::from_iter([ ( "Debug".into(), - SchemaField { + Box::new(SchemaField { comment: Some("Shows debug messages and above".into()), - schema: Schema::new(SchemaType::literal(LiteralValue::String("debug".into()))), + schema: Schema::literal_value(LiteralValue::String("debug".into())), ..SchemaField::default() - } + }) ), ( "Error".into(), - SchemaField { + Box::new(SchemaField { comment: Some("Shows only error messages".into()), - schema: Schema::new(SchemaType::literal(LiteralValue::String("error".into()))), + schema: Schema::literal_value(LiteralValue::String("error".into())), ..SchemaField::default() - } + }) ), ( "Warning".into(), - SchemaField { + Box::new(SchemaField { comment: Some("Shows warning and error messages".into()), - schema: Schema::new(SchemaType::literal(LiteralValue::String("warning".into()))), + schema: Schema::literal_value(LiteralValue::String("warning".into())), ..SchemaField::default() - } + }) ), ])), ..EnumType::default() @@ -76,4 +76,28 @@ schema.enumerable(EnumType { > This comes in handy when working with specific generators, like TypeScript. +> `variants` is the source of truth, and `values` is a derived subset of it. A variant whose schema +> holds no literal value contributes no entry to `values`, so the two can differ in length. + +## Default value + +The `default_index` field points at the entry that is the default. It indexes `variants` when that +map is set, and `values` otherwise, so prefer the +[`EnumType::get_default()`](https://docs.rs/schematic/latest/schematic/schema/struct.EnumType.html#method.get_default) +and +[`EnumType::set_default()`](https://docs.rs/schematic/latest/schematic/schema/struct.EnumType.html#method.set_default) +methods over indexing either list yourself. + +```rust +let mut ty = EnumType::new([ + LiteralValue::String("debug".into()), + LiteralValue::String("error".into()), +]); + +// Returns false and keeps the current default when no entry matches +ty.set_default(LiteralValue::String("error".into())); + +assert_eq!(ty.get_default(), Some(&LiteralValue::String("error".into()))); +``` + [enum]: https://docs.rs/schematic/latest/schematic/schema/struct.EnumType.html diff --git a/book/src/schema/external.md b/book/src/schema/external.md index 759c271c..44183d22 100644 --- a/book/src/schema/external.md +++ b/book/src/schema/external.md @@ -24,6 +24,9 @@ Implements a schema for `IndexMap` and `IndexSet` from the Implements a schema for `Regex` from the [regex](https://crates.io/crates/regex) crate. +> To use `Regex` as a [setting](../config/settings.md#third-party-types), reach for `RegexSetting`, which +> supplies the `Default` a required setting needs. + ## relative-path > Requires the `type_relative_path` Cargo feature. @@ -33,9 +36,9 @@ Implements schemas for `RelativePath` and `RelativePathBuf` from the ## rpkl -> Requires the `type_serde_rpkl` Cargo feature. +> Requires the `pkl` Cargo feature. -Implements schemas for `Value` from the [rpkl](https://crates.io/crates/rpkl) crate. +Implements a schema for `Value` from the [rpkl](https://crates.io/crates/rpkl) crate. ## rust_decimal @@ -51,30 +54,40 @@ crate. Implements schemas for `Version` and `VersionReq` from the [semver](https://crates.io/crates/semver) crate. +> To use `Version` as a [setting](../config/settings.md#third-party-types), reach for `VersionSetting`, which +> supplies the `Default` a required setting needs. + +## ron + +> Requires the `ron` Cargo feature. + +Implements a schema for `Value` from the [ron](https://crates.io/crates/ron) crate. + ## serde_json -> Requires the `type_serde_json` Cargo feature. +> Requires the `json` Cargo feature. Implements schemas for `Value`, `Number`, and `Map` from the [serde_json](https://crates.io/crates/serde_json) crate. ## serde_yaml -> Requires the `type_serde_yaml` Cargo feature. +> Requires the `serde_yaml` Cargo feature on `schematic_types` directly. The `schematic` crate does +> not enable it, as its `yaml` feature uses `serde_norway` instead. Implements schemas for `Value`, `Number`, and `Mapping` from the [serde_yaml](https://crates.io/crates/serde_yaml) crate. ## serde_yaml_norway -> Requires the `serde_yaml_norway` Cargo feature. +> Requires the `yaml` Cargo feature. Implements schemas for `Value`, `Number`, and `Mapping` from the [serde_norway](https://crates.io/crates/serde_norway) crate. ## toml -> Requires the `type_serde_toml` Cargo feature. +> Requires the `toml` Cargo feature. Implements schemas for `Value` and `Map` from the [toml](https://crates.io/crates/toml) crate. diff --git a/book/src/schema/generator/index.md b/book/src/schema/generator/index.md index 9d7afb14..2457d4a6 100644 --- a/book/src/schema/generator/index.md +++ b/book/src/schema/generator/index.md @@ -26,8 +26,8 @@ From here, for every type that implements [`Schematic`](https://docs.rs/schematic/latest/schematic/trait.Schematic.html) and you want to include in the generated output, call [`SchemaGenerator::add()`](https://docs.rs/schematic/latest/schematic/schema/struct.SchemaGenerator.html#method.add). -If you only have a [`SchemaType`](https://docs.rs/schematic/latest/schematic/enum.SchemaType.html), -you can use the +If you already have a [`Schema`](https://docs.rs/schematic/latest/schematic/struct.Schema.html) +rather than a type, you can use the [`SchemaGenerator::add_schema()`](https://docs.rs/schematic/latest/schematic/schema/struct.SchemaGenerator.html#method.add_schema) method instead. @@ -43,6 +43,10 @@ generator.add::(); > We'll recursively add referenced and nested schemas for types that are added. No need to > explicitly add all required types! +Schemas are keyed by [name](../types.md#defining-names), so adding two different types that report +the same one panics. Rendering both is impossible, and silently keeping one would leave every +reference to the other pointing at the wrong type. Adding the same type twice is fine. + ### Generating output From here, call diff --git a/book/src/schema/struct.md b/book/src/schema/struct.md index 73048685..7a2d886e 100644 --- a/book/src/schema/struct.md +++ b/book/src/schema/struct.md @@ -9,7 +9,7 @@ use schematic::{Schematic, Schema, SchemaBuilder, SchemaType, schema::StructType impl Schematic for T { fn build_schema(mut schema: SchemaBuilder) -> Schema { schema.structure(StructType { - fields: HashMap::from_iter([ + fields: IndexMap::from_iter([ ( "name".into(), Box::new(SchemaField { diff --git a/book/src/schema/unknown.md b/book/src/schema/unknown.md index cfd82daf..f91164ea 100644 --- a/book/src/schema/unknown.md +++ b/book/src/schema/unknown.md @@ -7,7 +7,7 @@ sometimes known as an "any" or "mixed" type. use schematic::{Schematic, Schema, SchemaBuilder, SchemaType}; impl Schematic for T { - fn build_schema(schema: SchemaBuilder) -> Schema { + fn build_schema(mut schema: SchemaBuilder) -> Schema { schema.build() } } diff --git a/crates/core/src/field.rs b/crates/core/src/field.rs index 9d8f2363..18b12da0 100644 --- a/crates/core/src/field.rs +++ b/crates/core/src/field.rs @@ -112,8 +112,10 @@ impl Field { 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`."); + // A key may be explicit, or derived from the container's prefix, + // but without one there is nothing for the parser to receive + if self.args.parse_env.is_some() && self.get_env_var().is_none() { + panic!("Cannot use `parse_env` without `env` or a container `env_prefix`."); } } diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index 0c346a30..ef408408 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -359,17 +359,19 @@ impl FieldValue { if let Some(expr) = field_args.validate.as_deref() { let func = match expr { - // func(arg)() - already returns a boxed validator - Expr::Call(func) => quote! { #func }, - // func() - must be boxed - Expr::Path(func) => quote! { Box::new(#func) }, + // func(arg)() - returns a validator + // func() - is the validator itself + Expr::Call(_) | Expr::Path(_) => quote! { #expr }, _ => { panic!("Unsupported `validate` syntax."); } }; + // Called through a closure rather than passed by value, so that + // the setting deref coerces on its way in, letting a `String` + // reach a `&str` validator and a `Vec` a `&[T]` one res.value = quote! { - validate.check(#field_name, setting, self, #func); + validate.check(#field_name, setting, self, |v, d, c, f| #func(v, d, c, f)); }; } else { res.no_value = true; diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index 75a76d04..0e451172 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -131,8 +131,22 @@ pub fn extract_comment(attrs: &[Attribute]) -> Option { continue; }; - for line in value.value().split('\n') { - let line = line.trim(); + let value = value.value(); + + // A block comment arrives as a single multi-line value, where a + // leading `*` continues the block rather than starting a list. + // Line comments arrive one attribute per line, so a `*` there is + // markdown and must survive. + let block = value.contains('\n'); + + for line in value.split('\n') { + let mut line = line.trim(); + + if block { + line = line + .strip_prefix("* ") + .unwrap_or_else(|| if line == "*" { "" } else { line }); + } // Preserve list items as their own line if line.starts_with("* ") || line.starts_with("- ") { @@ -184,9 +198,12 @@ pub fn extract_deprecated(attrs: &[Attribute]) -> Option { let mut message = String::new(); let _ = attr.parse_nested_meta(|meta| { + // Every value is consumed, not just `note`, otherwise + // parsing stops at the first key that isn't one + let value = meta.value()?.parse::()?; + if meta.path.is_ident("note") - && let Ok(value) = meta.value() - && let Ok(Lit::Str(value)) = value.parse::() + && let Lit::Str(value) = value { message = value.value().trim().to_owned(); } diff --git a/crates/core/src/value.rs b/crates/core/src/value.rs index c1a90ede..fa92a943 100644 --- a/crates/core/src/value.rs +++ b/crates/core/src/value.rs @@ -429,20 +429,36 @@ impl Value { let mut setting = quote! { #setting_var }; let mut value = None; - for layer in layers { + for (index, layer) in layers.iter().enumerate() { match layer { Layer::Arc | Layer::Box | Layer::Rc => { setting = quote! { #setting.as_ref() }; } - Layer::Map(_) => { - value = Some(quote! { - validate.#nested_map(#target, #setting.iter()); - }); - break; - } - Layer::Set(_) | Layer::Vec(_) => { - value = Some(quote! { - validate.#nested_list(#target, #setting.iter()); + Layer::Map(_) | Layer::Set(_) | Layer::Vec(_) => { + // An item may itself be optional, as in `Vec>`, + // and a `None` has nothing to validate. Items are handed + // over as `Option`s either way, so that a missing one + // still holds its position in the reported path. + let optional_items = layers + .get(index + 1) + .is_some_and(|layer| **layer == Layer::Option); + + value = Some(if matches!(layer, Layer::Map(_)) { + let items = if optional_items { + quote! { #setting.iter().map(|(key, value)| (key, value.as_ref())) } + } else { + quote! { #setting.iter().map(|(key, value)| (key, Some(value))) } + }; + + quote! { validate.#nested_map(#target, #items); } + } else { + let items = if optional_items { + quote! { #setting.iter().map(|item| item.as_ref()) } + } else { + quote! { #setting.iter().map(Some) } + }; + + quote! { validate.#nested_list(#target, #items); } }); break; } diff --git a/crates/core/src/variant.rs b/crates/core/src/variant.rs index ed9e9a6c..ed98c3b3 100644 --- a/crates/core/src/variant.rs +++ b/crates/core/src/variant.rs @@ -384,7 +384,10 @@ impl Variant { Fields::Unit => quote! { Schema::literal_value(LiteralValue::String(#name.into())) }, }; - // Wrap the tagged value so that nested configs can be partialized + // Mark a nested value so that it resolves to its partial. Only the + // value itself is marked, never a struct synthesized around it for + // tagging, as that struct is not a partial config -- its tag is a + // literal that is always present. let wrap = |value: TokenStream| { if nested { quote! { @@ -398,6 +401,7 @@ impl Variant { value } }; + let partial_inner = wrap(inner.clone()); let unit = self.is_unit_variant(); let name_literal = quote! { Schema::literal_value(LiteralValue::String(#name.into())) }; @@ -436,11 +440,11 @@ impl Variant { if unit { inner } else { - wrap(quote! { + quote! { Schema::structure(StructType::new([ - (#name.into(), #inner), + (#name.into(), #partial_inner), ])) - }) + } } } // { "tag": "name", ...value } @@ -470,12 +474,12 @@ impl Variant { ])) } } else { - wrap(quote! { + quote! { Schema::structure(StructType::new([ (#tag.into(), #name_literal), - (#content.into(), #inner), + (#content.into(), #partial_inner), ])) - }) + } } } } @@ -804,17 +808,23 @@ impl Variant { use syn::Expr; let func = match expr { - // func(arg)() - already returns a boxed validator - Expr::Call(func) => quote! { #func }, - // func() - must be boxed - Expr::Path(func) => quote! { Box::new(#func) }, + // func(arg)() - returns a validator + // func() - is the validator itself + Expr::Call(_) | Expr::Path(_) => quote! { #expr }, _ => { panic!("Unsupported `validate` syntax."); } }; + // See `FieldValue::impl_partial_validate` for why this is + // called through a closure instead of passed by value statements.push(quote! { - validate.check_variant(#name_string, (#(#outer_names),*), self, #func); + validate.check_variant( + #name_string, + (#(#outer_names),*), + self, + |v, d, c, f| #func(v, d, c, f), + ); }); } diff --git a/crates/core/tests/setting_env_test.rs b/crates/core/tests/setting_env_test.rs index 84909148..7db47189 100644 --- a/crates/core/tests/setting_env_test.rs +++ b/crates/core/tests/setting_env_test.rs @@ -495,7 +495,9 @@ mod setting_parse_env { } #[test] - #[should_panic(expected = "Cannot use `parse_env` without `env`.")] + #[should_panic( + expected = "Cannot use `parse_env` without `env` or a container `env_prefix`." + )] fn errors_without_env() { Container::from(parse_quote! { #[derive(Config)] @@ -553,7 +555,9 @@ mod setting_parse_env { } #[test] - #[should_panic(expected = "Cannot use `parse_env` without `env`.")] + #[should_panic( + expected = "Cannot use `parse_env` without `env` or a container `env_prefix`." + )] fn errors_without_env() { Container::from(parse_quote! { #[derive(Config)] diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_adjacent_tag.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_adjacent_tag.snap index e9367127..76e2765f 100644 --- a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_adjacent_tag.snap +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_adjacent_tag.snap @@ -46,73 +46,79 @@ fn build_schema(mut schema: SchemaBuilder) -> Schema { ), ]), ), - { - let mut item = Schema::structure( - StructType::new([ - ( - "type".into(), - Schema::literal_value(LiteralValue::String("Inner".into())), - ), - ("value".into(), schema.infer_as_nested::()), - ]), - ); - item.partialize(); - item - }, - { - let mut item = Schema::structure( - StructType::new([ - ( - "type".into(), - Schema::literal_value( - LiteralValue::String("InnerOpt".into()), - ), - ), - ( - "value".into(), - schema.infer_as_nested::>(), - ), - ]), - ); - item.partialize(); - item - }, - { - let mut item = Schema::structure( - StructType::new([ - ( - "type".into(), - Schema::literal_value( - LiteralValue::String("InnerList".into()), - ), - ), - ( - "value".into(), - schema.infer_as_nested::>(), + Schema::structure( + StructType::new([ + ( + "type".into(), + Schema::literal_value(LiteralValue::String("Inner".into())), + ), + ( + "value".into(), + { + let mut item = schema.infer_as_nested::(); + item.partialize(); + item + }, + ), + ]), + ), + Schema::structure( + StructType::new([ + ( + "type".into(), + Schema::literal_value( + LiteralValue::String("InnerOpt".into()), ), - ]), - ); - item.partialize(); - item - }, - { - let mut item = Schema::structure( - StructType::new([ - ( - "type".into(), - Schema::literal_value( - LiteralValue::String("InnerMap".into()), - ), + ), + ( + "value".into(), + { + let mut item = schema + .infer_as_nested::>(); + item.partialize(); + item + }, + ), + ]), + ), + Schema::structure( + StructType::new([ + ( + "type".into(), + Schema::literal_value( + LiteralValue::String("InnerList".into()), ), - ( - "value".into(), - schema.infer_as_nested::>(), + ), + ( + "value".into(), + { + let mut item = schema + .infer_as_nested::>(); + item.partialize(); + item + }, + ), + ]), + ), + Schema::structure( + StructType::new([ + ( + "type".into(), + Schema::literal_value( + LiteralValue::String("InnerMap".into()), ), - ]), - ); - item.partialize(); - item - }, + ), + ( + "value".into(), + { + let mut item = schema + .infer_as_nested::>(); + item.partialize(); + item + }, + ), + ]), + ), Schema::structure( StructType::new([ ( diff --git a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_external.snap b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_external.snap index c713f849..c2cb6ef6 100644 --- a/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_external.snap +++ b/crates/core/tests/snapshots/container_schema_test__schema_type__unnamed_enum__supports_external.snap @@ -30,51 +30,57 @@ fn build_schema(mut schema: SchemaBuilder) -> Schema { ), ]), ), - { - let mut item = Schema::structure( - StructType::new([ - ("Inner".into(), schema.infer_as_nested::()), - ]), - ); - item.partialize(); - item - }, - { - let mut item = Schema::structure( - StructType::new([ - ( - "InnerOpt".into(), - schema.infer_as_nested::>(), - ), - ]), - ); - item.partialize(); - item - }, - { - let mut item = Schema::structure( - StructType::new([ - ( - "InnerList".into(), - schema.infer_as_nested::>(), - ), - ]), - ); - item.partialize(); - item - }, - { - let mut item = Schema::structure( - StructType::new([ - ( - "InnerMap".into(), - schema.infer_as_nested::>(), - ), - ]), - ); - item.partialize(); - item - }, + Schema::structure( + StructType::new([ + ( + "Inner".into(), + { + let mut item = schema.infer_as_nested::(); + item.partialize(); + item + }, + ), + ]), + ), + Schema::structure( + StructType::new([ + ( + "InnerOpt".into(), + { + let mut item = schema + .infer_as_nested::>(); + item.partialize(); + item + }, + ), + ]), + ), + Schema::structure( + StructType::new([ + ( + "InnerList".into(), + { + let mut item = schema + .infer_as_nested::>(); + item.partialize(); + item + }, + ), + ]), + ), + Schema::structure( + StructType::new([ + ( + "InnerMap".into(), + { + let mut item = schema + .infer_as_nested::>(); + item.partialize(); + item + }, + ), + ]), + ), Schema::literal_value(LiteralValue::String("Unit".into())), ], Some(2usize), 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 a54f6b15..487ca064 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 @@ -109,7 +109,8 @@ impl schematic::PartialConfig for PartialExample { use schematic::internal::*; let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.name { - validate.check("name", setting, self, Box::new(validate_name)); + validate + .check("name", setting, self, |v, d, c, f| validate_name(v, d, c, f)); } if self.port.is_none() { validate.required("port"); @@ -118,7 +119,7 @@ impl schematic::PartialConfig for PartialExample { validate.nested("nested", setting); } if let Some(setting) = &self.list { - validate.nested_list("list", setting.iter()); + validate.nested_list("list", setting.iter().map(Some)); } if !validate.errors.is_empty() { return Err(validate.errors); 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 74db6790..c554fc5b 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 @@ -146,7 +146,7 @@ impl schematic::PartialConfig for PartialExample { validate.nested_variant("Nested", 0usize, pa); } Self::List(pa) => { - validate.nested_variant_list("List", 0usize, pa.iter()); + validate.nested_variant_list("List", 0usize, pa.iter().map(Some)); } _ => {} }; diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_curried_func.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_curried_func.snap index 59eba0d9..ce8596ee 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_curried_func.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_curried_func.snap @@ -11,7 +11,7 @@ fn validate_with_path( use schematic::internal::*; let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.a { - validate.check("a", setting, self, func_call()); + validate.check("a", setting, self, |v, d, c, f| func_call()(v, d, c, f)); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap index 2dc4197e..951bf32f 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__accepts_func_ref.snap @@ -11,7 +11,7 @@ fn validate_with_path( use schematic::internal::*; let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.a { - validate.check("a", setting, self, Box::new(func_ref)); + validate.check("a", setting, self, |v, d, c, f| func_ref(v, d, c, f)); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap index 728588aa..74a04550 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested.snap @@ -11,19 +11,19 @@ fn validate_with_path( use schematic::internal::*; let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.a { - validate.check("a", setting, self, Box::new(func_ref)); + validate.check("a", setting, self, |v, d, c, f| func_ref(v, d, c, f)); validate.nested("a", setting); } if let Some(setting) = &self.b { - validate.check("b", setting, self, Box::new(func_ref)); + validate.check("b", setting, self, |v, d, c, f| func_ref(v, d, c, f)); validate.nested("b", setting); } if let Some(setting) = &self.c { - validate.check("c", setting, self, Box::new(func_ref)); + validate.check("c", setting, self, |v, d, c, f| func_ref(v, d, c, f)); validate.nested("c", setting); } if let Some(setting) = &self.d { - validate.check("d", setting, self, Box::new(func_ref)); + validate.check("d", setting, self, |v, d, c, f| func_ref(v, d, c, f)); validate.nested("d", setting); } if let Some(setting) = &self.e { diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap index d35ed66b..bceeef24 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_collections.snap @@ -11,25 +11,25 @@ fn validate_with_path( use schematic::internal::*; let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.a { - validate.check("a", setting, self, Box::new(func_ref)); - validate.nested_list("a", setting.iter()); + validate.check("a", setting, self, |v, d, c, f| func_ref(v, d, c, f)); + validate.nested_list("a", setting.iter().map(Some)); } if let Some(setting) = &self.b { - validate.check("b", setting, self, Box::new(func_ref)); - validate.nested_map("b", setting.iter()); + validate.check("b", setting, self, |v, d, c, f| func_ref(v, d, c, f)); + validate.nested_map("b", setting.iter().map(|(key, value)| (key, Some(value)))); } if let Some(setting) = &self.c { - validate.check("c", setting, self, Box::new(func_ref)); - validate.nested_list("c", setting.iter()); + validate.check("c", setting, self, |v, d, c, f| func_ref(v, d, c, f)); + validate.nested_list("c", setting.iter().map(Some)); } if let Some(setting) = &self.d { - validate.nested_list("d", setting.iter()); + validate.nested_list("d", setting.iter().map(Some)); } if let Some(setting) = &self.e { - validate.nested_map("e", setting.iter()); + validate.nested_map("e", setting.iter().map(|(key, value)| (key, Some(value)))); } if let Some(setting) = &self.f { - validate.nested_list("f", setting.iter()); + validate.nested_list("f", setting.iter().map(Some)); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_wrappers.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_wrappers.snap index 7534e099..ac9fb67e 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_wrappers.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_nested_wrappers.snap @@ -20,7 +20,7 @@ fn validate_with_path( validate.nested("c", setting); } if let Some(setting) = &self.d { - validate.nested_list("d", setting.iter()); + validate.nested_list("d", setting.iter().map(Some)); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap index f66b7037..3c1e09d9 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__supports_standard.snap @@ -11,19 +11,19 @@ fn validate_with_path( use schematic::internal::*; let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.a { - validate.check("a", setting, self, Box::new(func_ref)); + validate.check("a", setting, self, |v, d, c, f| func_ref(v, d, c, f)); } if let Some(setting) = &self.b { - validate.check("b", setting, self, Box::new(func_ref)); + validate.check("b", setting, self, |v, d, c, f| func_ref(v, d, c, f)); } if let Some(setting) = &self.c { - validate.check("c", setting, self, Box::new(func_ref)); + validate.check("c", setting, self, |v, d, c, f| func_ref(v, d, c, f)); } if let Some(setting) = &self.d { - validate.check("d", setting, self, Box::new(func_ref)); + validate.check("d", setting, self, |v, d, c, f| func_ref(v, d, c, f)); } if let Some(setting) = &self.e { - validate.check("e", setting, self, Box::new(func_ref)); + validate.check("e", setting, self, |v, d, c, f| func_ref(v, d, c, f)); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__uses_serde_names.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__uses_serde_names.snap index 02c4b146..bcf42d6c 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__uses_serde_names.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__named_struct__uses_serde_names.snap @@ -11,7 +11,7 @@ fn validate_with_path( use schematic::internal::*; let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.a { - validate.check("aa", setting, self, Box::new(func_ref)); + validate.check("aa", setting, self, |v, d, c, f| func_ref(v, d, c, f)); } if self.b.is_none() { validate.required("bb"); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap index 0cec4962..6643da05 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_curried_func.snap @@ -12,7 +12,8 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check_variant("A", (pa), self, func_call()); + validate + .check_variant("A", (pa), self, |v, d, c, f| func_call()(v, d, c, f)); } _ => {} }; diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap index 13a6fde7..05e24faf 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__accepts_func_ref.snap @@ -12,7 +12,7 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check_variant("A", (pa), self, Box::new(func_ref)); + validate.check_variant("A", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); } _ => {} }; diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap index 6843ab39..79ad320c 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_many_values.snap @@ -12,22 +12,30 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check_variant("A", (pa), self, Box::new(func_ref)); + validate.check_variant("A", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); } Self::B(pa, pb) => { - validate.check_variant("B", (pa, pb), self, Box::new(func_ref)); + validate + .check_variant("B", (pa, pb), self, |v, d, c, f| func_ref(v, d, c, f)); } Self::C(pa, pb, pc) => { - validate.check_variant("C", (pa, pb, pc), self, Box::new(func_ref)); + validate + .check_variant( + "C", + (pa, pb, pc), + self, + |v, d, c, f| func_ref(v, d, c, f), + ); } Self::A(pa) => { - validate.check_variant("A", (pa), self, Box::new(func_ref)); + validate.check_variant("A", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); if [pa].iter().any(|v| v.is_none()) { validate.required_variant("A"); } } Self::B(pa, pb) => { - validate.check_variant("B", (pa, pb), self, Box::new(func_ref)); + validate + .check_variant("B", (pa, pb), self, |v, d, c, f| func_ref(v, d, c, f)); if [pa, pb].iter().any(|v| v.is_none()) { validate.required_variant("B"); } diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap index b205982b..cbd4f51d 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested.snap @@ -12,21 +12,21 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check_variant("A", (pa), self, Box::new(func_ref)); + validate.check_variant("A", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); validate.nested_variant("A", 0usize, pa); } Self::B(pa) => { - validate.check_variant("B", (pa), self, Box::new(func_ref)); + validate.check_variant("B", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); validate.nested_variant("B", 0usize, pa); } Self::C(pa) => { - validate.check_variant("C", (pa), self, Box::new(func_ref)); + validate.check_variant("C", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); if let Some(pa) = pa { validate.nested_variant("C", 0usize, pa); } } Self::D(pa) => { - validate.check_variant("D", (pa), self, Box::new(func_ref)); + validate.check_variant("D", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); validate.nested_variant("D", 0usize, pa); } Self::E(pa) => { diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap index 49694f97..7da95c1d 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_collections.snap @@ -12,28 +12,38 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check_variant("A", (pa), self, Box::new(func_ref)); - validate.nested_variant_list("A", 0usize, pa.iter()); + validate.check_variant("A", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); + validate.nested_variant_list("A", 0usize, pa.iter().map(Some)); } Self::B(pa) => { - validate.check_variant("B", (pa), self, Box::new(func_ref)); - validate.nested_variant_map("B", 0usize, pa.iter()); + validate.check_variant("B", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); + validate + .nested_variant_map( + "B", + 0usize, + pa.iter().map(|(key, value)| (key, Some(value))), + ); } Self::C(pa) => { - validate.check_variant("C", (pa), self, Box::new(func_ref)); + validate.check_variant("C", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); if let Some(pa) = pa { - validate.nested_variant_list("C", 0usize, pa.iter()); + validate.nested_variant_list("C", 0usize, pa.iter().map(Some)); } } Self::D(pa) => { - validate.nested_variant_list("D", 0usize, pa.iter()); + validate.nested_variant_list("D", 0usize, pa.iter().map(Some)); } Self::E(pa) => { - validate.nested_variant_map("E", 0usize, pa.iter()); + validate + .nested_variant_map( + "E", + 0usize, + pa.iter().map(|(key, value)| (key, Some(value))), + ); } Self::F(pa) => { if let Some(pa) = pa { - validate.nested_variant_list("F", 0usize, pa.iter()); + validate.nested_variant_list("F", 0usize, pa.iter().map(Some)); } } _ => {} diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_wrappers.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_wrappers.snap index c0044949..af15c7f3 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_wrappers.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_nested_wrappers.snap @@ -24,11 +24,11 @@ fn validate_with_path( } Self::D(pa) => { if let Some(pa) = pa { - validate.nested_variant_list("D", 0usize, pa.iter()); + validate.nested_variant_list("D", 0usize, pa.iter().map(Some)); } } Self::E(pa) => { - validate.nested_variant_list("E", 0usize, pa.iter()); + validate.nested_variant_list("E", 0usize, pa.iter().map(Some)); } _ => {} }; diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap index a81458c8..d0c6a081 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__supports_standard.snap @@ -12,19 +12,19 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check_variant("A", (pa), self, Box::new(func_ref)); + validate.check_variant("A", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); } Self::B(pa) => { - validate.check_variant("B", (pa), self, Box::new(func_ref)); + validate.check_variant("B", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); } Self::C(pa) => { - validate.check_variant("C", (pa), self, Box::new(func_ref)); + validate.check_variant("C", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); } Self::D(pa) => { - validate.check_variant("D", (pa), self, Box::new(func_ref)); + validate.check_variant("D", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); } Self::E(pa) => { - validate.check_variant("E", (pa), self, Box::new(func_ref)); + validate.check_variant("E", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); } _ => {} }; diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__uses_serde_names.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__uses_serde_names.snap index c7bc7d13..d78c8198 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__uses_serde_names.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_enum__uses_serde_names.snap @@ -12,7 +12,7 @@ fn validate_with_path( let mut validate = ValidateManager::new(context, finalizing, path); match self { Self::A(pa) => { - validate.check_variant("aa", (pa), self, Box::new(func_ref)); + validate.check_variant("aa", (pa), self, |v, d, c, f| func_ref(v, d, c, f)); } Self::B(pa) => { validate.nested_variant("bb", 0usize, pa); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_curried_func.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_curried_func.snap index 3f32edc4..d5f2b426 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_curried_func.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_curried_func.snap @@ -11,7 +11,7 @@ fn validate_with_path( use schematic::internal::*; let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.0 { - validate.check("0", setting, self, func_call()); + validate.check("0", setting, self, |v, d, c, f| func_call()(v, d, c, f)); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap index 9cccf8c0..77c7ffcb 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__accepts_func_ref.snap @@ -11,7 +11,7 @@ fn validate_with_path( use schematic::internal::*; let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.0 { - validate.check("0", setting, self, Box::new(func_ref)); + validate.check("0", setting, self, |v, d, c, f| func_ref(v, d, c, f)); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap index d945d395..77695137 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested.snap @@ -11,19 +11,19 @@ fn validate_with_path( use schematic::internal::*; let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.0 { - validate.check("0", setting, self, Box::new(func_ref)); + validate.check("0", setting, self, |v, d, c, f| func_ref(v, d, c, f)); validate.nested("0", setting); } if let Some(setting) = &self.1 { - validate.check("1", setting, self, Box::new(func_ref)); + validate.check("1", setting, self, |v, d, c, f| func_ref(v, d, c, f)); validate.nested("1", setting); } if let Some(setting) = &self.2 { - validate.check("2", setting, self, Box::new(func_ref)); + validate.check("2", setting, self, |v, d, c, f| func_ref(v, d, c, f)); validate.nested("2", setting); } if let Some(setting) = &self.3 { - validate.check("3", setting, self, Box::new(func_ref)); + validate.check("3", setting, self, |v, d, c, f| func_ref(v, d, c, f)); validate.nested("3", setting); } if let Some(setting) = &self.4 { diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap index ecbae2fa..0874f476 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_nested_collections.snap @@ -11,25 +11,25 @@ fn validate_with_path( use schematic::internal::*; let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.0 { - validate.check("0", setting, self, Box::new(func_ref)); - validate.nested_list("0", setting.iter()); + validate.check("0", setting, self, |v, d, c, f| func_ref(v, d, c, f)); + validate.nested_list("0", setting.iter().map(Some)); } if let Some(setting) = &self.1 { - validate.check("1", setting, self, Box::new(func_ref)); - validate.nested_map("1", setting.iter()); + validate.check("1", setting, self, |v, d, c, f| func_ref(v, d, c, f)); + validate.nested_map("1", setting.iter().map(|(key, value)| (key, Some(value)))); } if let Some(setting) = &self.2 { - validate.check("2", setting, self, Box::new(func_ref)); - validate.nested_list("2", setting.iter()); + validate.check("2", setting, self, |v, d, c, f| func_ref(v, d, c, f)); + validate.nested_list("2", setting.iter().map(Some)); } if let Some(setting) = &self.3 { - validate.nested_list("3", setting.iter()); + validate.nested_list("3", setting.iter().map(Some)); } if let Some(setting) = &self.4 { - validate.nested_map("4", setting.iter()); + validate.nested_map("4", setting.iter().map(|(key, value)| (key, Some(value)))); } if let Some(setting) = &self.5 { - validate.nested_list("5", setting.iter()); + validate.nested_list("5", setting.iter().map(Some)); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap index c762cef7..e4c23e40 100644 --- a/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap +++ b/crates/core/tests/snapshots/setting_validate_test__setting_validate__unnamed_struct__supports_standard.snap @@ -11,19 +11,19 @@ fn validate_with_path( use schematic::internal::*; let mut validate = ValidateManager::new(context, finalizing, path); if let Some(setting) = &self.0 { - validate.check("0", setting, self, Box::new(func_ref)); + validate.check("0", setting, self, |v, d, c, f| func_ref(v, d, c, f)); } if let Some(setting) = &self.1 { - validate.check("1", setting, self, Box::new(func_ref)); + validate.check("1", setting, self, |v, d, c, f| func_ref(v, d, c, f)); } if let Some(setting) = &self.2 { - validate.check("2", setting, self, Box::new(func_ref)); + validate.check("2", setting, self, |v, d, c, f| func_ref(v, d, c, f)); } if let Some(setting) = &self.3 { - validate.check("3", setting, self, Box::new(func_ref)); + validate.check("3", setting, self, |v, d, c, f| func_ref(v, d, c, f)); } if let Some(setting) = &self.4 { - validate.check("4", setting, self, Box::new(func_ref)); + validate.check("4", setting, self, |v, d, c, f| func_ref(v, d, c, f)); } if !validate.errors.is_empty() { return Err(validate.errors); diff --git a/crates/macros-next/Cargo.toml b/crates/macros-next/Cargo.toml deleted file mode 100644 index debeeb64..00000000 --- a/crates/macros-next/Cargo.toml +++ /dev/null @@ -1,49 +0,0 @@ -[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" } -proc-macro2 = { workspace = true } -quote = { workspace = true } -syn = { workspace = true } - -[dev-dependencies] -schematic = { path = "../schematic", features = [ - "config", - "env", - "extends", - "json", - "schema", - "schema_serde", - "validate", -] } -schematic_macros_next = { path = ".", features = [ - "config", - "env", - "extends", - "schema", - "validate", -] } -serde = { workspace = true } -serde_json = { workspace = true } -starbase_sandbox = { workspace = true } - -[features] -default = [] -config = ["schematic_core/config"] -env = ["schematic_core/env"] -extends = ["schematic_core/extends"] -schema = ["schematic_core/schema"] -validate = ["schematic_core/validate"] diff --git a/crates/macros-next/src/lib.rs b/crates/macros-next/src/lib.rs deleted file mode 100644 index 28b236b8..00000000 --- a/crates/macros-next/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -#![allow(unused)] - -use proc_macro::TokenStream; -use quote::quote; -use schematic_core::container::{Container, ContainerMacro}; -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 = Container::from(input); - - quote! { #output }.into() -} - -// #[derive(ConfigEnum)] -#[cfg(feature = "config")] -#[proc_macro_derive(ConfigEnum, attributes(config, variant))] -pub fn config_enum(item: TokenStream) -> TokenStream { - let input: DeriveInput = parse_macro_input!(item); - let mut output = Container::from(input); - output.macro_type = ContainerMacro::ConfigUnitEnum; - - quote! { #output }.into() -} - -// #[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 mut output = Container::from(input); - output.macro_type = ContainerMacro::Schematic; - - quote! { #output }.into() -} diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index cf436a34..d792a129 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -14,16 +14,36 @@ all-features = true proc-macro = true [dependencies] -convert_case = { workspace = true } -darling = { workspace = true } +schematic_core = { version = "0.18.7", path = "../core" } proc-macro2 = { workspace = true } quote = { workspace = true } -syn = { workspace = true, features = ["full"] } +syn = { workspace = true } + +[dev-dependencies] +schematic = { path = "../schematic", features = [ + "config", + "env", + "extends", + "json", + "schema", + "schema_serde", + "validate", +] } +schematic_macros = { path = ".", features = [ + "config", + "env", + "extends", + "schema", + "validate", +] } +serde = { workspace = true } +serde_json = { workspace = true } +starbase_sandbox = { workspace = true } [features] default = [] -config = [] -env = [] -extends = [] -schema = [] -validate = [] +config = ["schematic_core/config"] +env = ["schematic_core/env"] +extends = ["schematic_core/extends"] +schema = ["schematic_core/schema"] +validate = ["schematic_core/validate"] diff --git a/crates/macros/src/common/container.rs b/crates/macros/src/common/container.rs deleted file mode 100644 index 46d28216..00000000 --- a/crates/macros/src/common/container.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::common::{Field, Variant}; -use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; - -pub enum Container<'l> { - NamedStruct { fields: Vec> }, - UnnamedStruct { fields: Vec> }, - Enum { variants: Vec> }, -} - -impl Container<'_> { - pub fn has_nested(&self) -> bool { - match self { - Self::NamedStruct { fields } => fields.iter().any(|v| v.is_nested()), - Self::UnnamedStruct { fields } => fields.iter().any(|v| v.is_nested()), - Self::Enum { variants } => variants.iter().any(|v| v.is_nested()), - } - } - - pub fn generate_settings_metadata(&self) -> TokenStream { - let mut settings = vec![]; - - let format_alias = |ts: TokenStream| { - format!("{ts}") - .replace(" < ", "<") - .replace("< ", "<") - .replace(" <", "<") - .replace(" > ", ">") - .replace("> ", ">") - .replace(" >", ">") - }; - - match self { - Self::NamedStruct { fields } | Self::UnnamedStruct { fields } => { - for field in fields { - let name = if field.name.is_some() { - field.get_name(Some(&field.casing_format)) - } else { - field.index.to_string() - }; - let env_key = if let Some(value) = field.get_env_var() { - quote!(Some(#value.into())) - } else { - quote!(None) - }; - let nested = if field.is_nested() { - let value = field.value_type.get_config_type(); - quote!(Some(#value::settings())) - } else { - quote!(None) - }; - let type_alias = format_alias(field.value.to_token_stream()); - - settings.push(quote! { - (#name.into(), schematic::ConfigSetting { - env_key: #env_key, - nested: #nested, - type_alias: #type_alias.into(), - }), - }); - } - } - Self::Enum { variants } => { - for variant in variants { - let name = variant.get_name(Some(&variant.casing_format)); - let type_alias = format_alias(variant.value.to_token_stream()); - - settings.push(quote! { - (#name.into(), schematic::ConfigSetting { - type_alias: #type_alias.into(), - ..Default::default() - }), - }); - } - } - }; - - quote! { - std::collections::BTreeMap::from_iter([ - #(#settings)* - ]) - } - } - - #[cfg(feature = "schema")] - pub fn generate_schema(&self, attrs: &[&syn::Attribute]) -> TokenStream { - use crate::utils::{extract_comment, extract_deprecated, map_option_argument_quote}; - use syn::Fields; - - let deprecated = if let Some(comment) = extract_deprecated(attrs) { - quote! { schema.set_deprecated(#comment); } - } else { - quote! {} - }; - let description = if let Some(comment) = extract_comment(attrs) { - quote! { schema.set_description(#comment); } - } else { - quote! {} - }; - - match self { - Self::NamedStruct { fields, .. } => { - let schema_types = fields - .iter() - .filter_map(|f| { - if f.is_excluded() { - None - } else { - Some(f.generate_schema_type(true)) - } - }) - .collect::>(); - - if fields.is_empty() { - quote! { - #deprecated - #description - schema.structure(StructType::default()) - } - } else { - quote! { - #deprecated - #description - schema.structure(StructType::new([ - #(#schema_types),* - ])) - } - } - } - Self::UnnamedStruct { fields, .. } => { - let schema_types = fields - .iter() - .filter_map(|f| { - if f.is_excluded() { - None - } else { - Some(f.generate_schema_type(false)) - } - }) - .collect::>(); - - if fields.len() == 1 { - let single_type = &schema_types[0]; - - quote! { - let mut schema = #single_type; - #deprecated - #description - schema - } - } else { - quote! { - #deprecated - #description - schema.tuple(TupleType::new([ - #(#schema_types),* - ])) - } - } - } - Self::Enum { variants } => { - let is_all_unit_enum = variants - .iter() - .all(|v| matches!(v.value.fields, Fields::Unit)); - let mut default_index = None; - - let variants_types = variants - .iter() - .enumerate() - .filter_map(|(i, v)| { - if v.is_default() { - default_index = Some(i); - } - - if v.is_excluded() { - None - } else { - Some(v.generate_schema_type(is_all_unit_enum)) - } - }) - .collect::>(); - - let default_index = map_option_argument_quote(default_index); - - if is_all_unit_enum { - quote! { - #deprecated - #description - schema.enumerable(EnumType::from_schemas( - [ - #(#variants_types),* - ], - #default_index, - )) - } - } else { - quote! { - #deprecated - #description - schema.union(UnionType::from_schemas( - [ - #(#variants_types),* - ], - #default_index, - )) - } - } - } - } - } -} diff --git a/crates/macros/src/common/field.rs b/crates/macros/src/common/field.rs deleted file mode 100644 index 19b4648a..00000000 --- a/crates/macros/src/common/field.rs +++ /dev/null @@ -1,389 +0,0 @@ -use crate::common::FieldValue; -use crate::common::PartialAttr; -use crate::common::macros::ContainerSerdeArgs; -use crate::utils::{extract_common_attrs, format_case, preserve_str_literal}; -use darling::FromAttributes; -use proc_macro2::{Ident, TokenStream}; -use quote::{ToTokens, quote}; -use std::collections::HashSet; -use syn::{Attribute, Expr, ExprPath, Field as NativeField, Type}; - -// #[serde()] -#[derive(FromAttributes, Default)] -#[darling(default, allow_unknown_fields, attributes(serde))] -pub struct FieldSerdeArgs { - pub alias: Option, - pub default: bool, - pub flatten: bool, - pub rename: Option, - pub skip: bool, - pub skip_deserializing: bool, - pub skip_serializing: bool, - - // variant - pub untagged: bool, -} - -impl FieldSerdeArgs { - pub fn inherit_from_container(&mut self, container: &ContainerSerdeArgs) { - if !self.default && container.default { - self.default = true; - } - } -} - -// #[schema()], #[setting()] -#[derive(FromAttributes, Default)] -#[darling(default, attributes(schema, setting))] -pub struct FieldArgs { - // schema - pub exclude: bool, - - // config - #[darling(with = preserve_str_literal, map = "Some")] - pub default: Option, - #[cfg(feature = "env")] - pub env: Option, - #[cfg(feature = "extends")] - pub extend: bool, - pub merge: Option, - pub nested: bool, - #[cfg(feature = "env")] - pub parse_env: Option, - pub required: bool, - pub transform: Option, - #[cfg(feature = "validate")] - pub validate: Option, - pub partial: PartialAttr, - - // serde - pub alias: Option, - pub flatten: bool, - pub rename: Option, - pub skip: bool, - pub skip_deserializing: bool, - pub skip_serializing: bool, -} - -pub struct Field<'l> { - pub args: FieldArgs, - pub serde_args: FieldSerdeArgs, - pub attrs: Vec<&'l Attribute>, - pub casing_format: String, - pub name: Option<&'l Ident>, // Named - pub index: usize, // Unnamed - pub value: &'l Type, - pub value_type: FieldValue<'l>, - pub env_prefix: Option, -} - -impl Field<'_> { - pub fn from(field: &NativeField) -> Field<'_> { - let args = FieldArgs::from_attributes(&field.attrs).unwrap_or_default(); - let serde_args = FieldSerdeArgs::from_attributes(&field.attrs).unwrap_or_default(); - - let field = Field { - name: field.ident.as_ref(), - index: 0, - attrs: extract_common_attrs(&field.attrs), - casing_format: String::new(), - value: &field.ty, - value_type: if args.nested { - FieldValue::nested(&field.ty) - } else { - FieldValue::value(&field.ty) - }, - args, - serde_args, - env_prefix: None, - }; - - if field.args.default.is_some() && field.is_nested() { - panic!("Cannot use defaults with `nested` configs."); - } - - if field.is_required() && !field.is_nullable() { - panic!("Cannot use required with non-optional settings."); - } - - field - } - - #[cfg(feature = "schema")] - pub fn is_excluded(&self) -> bool { - self.args.exclude - } - - #[cfg(feature = "extends")] - pub fn is_extendable(&self) -> bool { - self.args.extend - } - - #[cfg(feature = "schema")] - pub fn is_flatten(&self) -> bool { - self.serde_args.flatten || self.args.flatten - } - - pub fn is_nested(&self) -> bool { - self.args.nested - } - - pub fn is_nullable(&self) -> bool { - self.value_type.is_outer_optional() - } - - #[cfg(feature = "schema")] - pub fn is_optional(&self) -> bool { - self.serde_args.default || self.args.default.is_some() - } - - pub fn is_required(&self) -> bool { - self.args.required - } - - #[cfg(feature = "schema")] - pub fn is_skipped(&self) -> bool { - self.args.skip || self.serde_args.skip - } - - pub fn get_name_raw(&self) -> &Ident { - self.name.as_ref().expect("Missing name for field") - } - - pub fn get_name(&self, casing_format: Option<&str>) -> String { - let Some(name) = &self.name else { - return String::new(); - }; - - match &self.args.rename { - Some(local) => local.to_owned(), - _ => { - if let Some(serde) = &self.serde_args.rename { - serde.to_owned() - } else if let Some(format) = casing_format { - format_case(format, &name.to_string(), false) - } else { - name.to_string() - } - } - } - } - - pub fn get_aliases(&self) -> Vec { - let mut aliases = HashSet::new(); - - if let Some(alias) = &self.args.alias { - aliases.insert(alias.to_owned()); - } - - if let Some(alias) = &self.serde_args.alias { - aliases.insert(alias.to_owned()); - } - - aliases.into_iter().collect() - } - - pub fn get_env_var(&self) -> Option { - if self.is_nested() { - return None; - } - - #[cfg(feature = "env")] - if let Some(env_name) = &self.args.env { - return Some(env_name.to_owned()); - } - - self.env_prefix - .as_ref() - .map(|env_prefix| format!("{env_prefix}{}", self.get_name(None)).to_uppercase()) - } - - pub fn get_serde_meta(&self) -> Option { - let mut meta = vec![]; - - match &self.args.alias { - Some(alias) => { - meta.push(quote! { alias = #alias }); - } - _ => { - if let Some(alias) = &self.serde_args.alias { - meta.push(quote! { alias = #alias }); - } - } - } - - if self.args.flatten || self.serde_args.flatten { - meta.push(quote! { flatten }); - } - - match &self.args.rename { - Some(rename) => { - meta.push(quote! { rename = #rename }); - } - _ => { - if let Some(rename) = &self.serde_args.rename { - meta.push(quote! { rename = #rename }); - } - } - } - - let mut skipped = false; - - if self.args.skip || self.serde_args.skip { - meta.push(quote! { skip }); - skipped = true; - } - - if !skipped { - if self.args.skip_serializing || self.serde_args.skip_serializing { - meta.push(quote! { skip_serializing }); - } else { - meta.push(quote! { skip_serializing_if = "Option::is_none" }); - } - - if self.args.skip_deserializing || self.serde_args.skip_deserializing { - meta.push(quote! { skip_deserializing }); - } - } - - if meta.is_empty() { - return None; - } - - Some(quote! { - #(#meta),* - }) - } - - #[cfg(feature = "schema")] - pub fn generate_schema_type(&self, as_field: bool) -> TokenStream { - use crate::utils::{ - extract_comment, extract_deprecated, map_bool_field_quote, map_option_field_quote, - map_vec_field_quote, - }; - use syn::Lit; - - let aliases = map_vec_field_quote("aliases", self.get_aliases()); - let hidden = map_bool_field_quote("hidden", self.is_skipped()); - let flatten = map_bool_field_quote("flatten", self.is_flatten()); - let nullable = map_bool_field_quote("nullable", self.is_nullable()); - let optional = map_bool_field_quote("optional", self.is_optional()); - let comment = map_option_field_quote("comment", extract_comment(&self.attrs)); - let description = map_option_field_quote("description", extract_comment(&self.attrs)); - let deprecated = map_option_field_quote("deprecated", extract_deprecated(&self.attrs)); - let env_var = map_option_field_quote("env_var", self.get_env_var()); - - let value = self.value; - let mut inner_schema = if self.is_nested() { - quote! { schema.infer_as_nested::<#value>() } - } else { - quote! { schema.infer::<#value>() } - }; - - if let Some(Expr::Lit(lit)) = &self.args.default { - let lit_value = match &lit.lit { - Lit::Str(v) => quote! { LiteralValue::String(#v.into()) }, - Lit::Int(v) => { - if v.suffix().starts_with('u') { - quote! { LiteralValue::Uint(#v) } - } else { - quote! { LiteralValue::Int(#v) } - } - } - Lit::Float(v) => { - if v.suffix() == "f32" { - quote! { LiteralValue::F32(#v) } - } else { - quote! { LiteralValue::F64(#v) } - } - } - Lit::Bool(v) => quote! { LiteralValue::Bool(#v) }, - _ => unimplemented!(), - }; - - inner_schema = quote! { schema.infer_with_default::<#value>(#lit_value) }; - } - - // Struct field (named) - if as_field { - let name = self.get_name(Some(&self.casing_format)); - let value = if aliases.is_none() - && comment.is_none() - && deprecated.is_none() - && env_var.is_none() - && flatten.is_none() - && hidden.is_none() - && nullable.is_none() - && optional.is_none() - { - quote! { - SchemaField::new(#inner_schema) - } - } else { - quote! { - { - let mut field = SchemaField::new(#inner_schema); - #aliases - #comment - #deprecated - #env_var - #flatten - #hidden - #nullable - #optional - field - } - } - }; - - quote! { - (#name.into(), #value) - } - } - // Tuple item (unnamed) - else { - #[allow(clippy::collapsible_else_if)] - if description.is_none() { - inner_schema - } else { - quote! { - { - let mut schema = #inner_schema; - #description - schema - } - } - } - } - } -} - -impl ToTokens for Field<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { - let value = &self.value_type; - - // Gather all attributes - let mut attrs = vec![]; - - if let Some(serde_meta) = self.get_serde_meta() { - attrs.push(quote! { #[serde(#serde_meta)] }); - } - - for attr in &self.attrs { - attrs.push(quote! { #attr }); - } - let partial = &self.args.partial; - attrs.push(quote! {#partial}); - - if let Some(name) = &self.name { - tokens.extend(quote! { - #(#attrs)* - pub #name: #value, - }); - } else { - tokens.extend(quote! { - pub #value, - }); - } - } -} diff --git a/crates/macros/src/common/field_value.rs b/crates/macros/src/common/field_value.rs deleted file mode 100644 index d7e45996..00000000 --- a/crates/macros/src/common/field_value.rs +++ /dev/null @@ -1,236 +0,0 @@ -use proc_macro2::{Ident, TokenStream}; -use quote::{ToTokens, quote}; -use syn::{GenericArgument, PathArguments, Type}; - -fn is_collection_type(ident: &Ident) -> bool { - let name = ident.to_string(); - - name.ends_with("Vec") || name.ends_with("Set") || name.ends_with("Map") -} - -#[derive(Debug, Default)] -pub struct TypeInfo { - pub boxed: bool, - pub optional: bool, - pub config: Option, -} - -fn extract_inner_type<'a>(ty: &'a Type, info: &mut TypeInfo) -> &'a Type { - // We don't need to traverse other types, just paths - let Type::Path(type_path) = ty else { - return ty; - }; - - // Extract the last segment of the path, for example `Option`, - // instead of the full path `std::option::Option` - let last_segment = type_path.path.segments.last().unwrap(); - - // If a collecion type, return the path immediately, as we'll need - // to extract inner information later on - if is_collection_type(&last_segment.ident) { - return ty; - } - - // If a wrapper type, mark the information for later - let mut nested = false; - - if last_segment.ident == "Option" { - info.optional = true; - nested = true; - } else if last_segment.ident == "Box" { - info.boxed = true; - nested = true; - } - - // If a nested type, drill down deeper to find the inner type - if nested { - if let PathArguments::AngleBracketed(args) = &last_segment.arguments - && let GenericArgument::Type(inner_ty) = args.args.last().unwrap() - { - return extract_inner_type(inner_ty, info); - } - } - // Otherwise we found the inner type, so extract the ident name - else { - info.config = Some(last_segment.ident.clone()); - } - - ty -} - -#[derive(Debug)] -pub enum FieldValue<'l> { - // Vec - NestedList { - collection: &'l Ident, - collection_info: TypeInfo, - item: &'l Type, - item_info: TypeInfo, - }, - // HashMap - NestedMap { - collection: &'l Ident, - collection_info: TypeInfo, - key: &'l Type, - value: &'l Type, - value_info: TypeInfo, - }, - // config - NestedValue { - info: TypeInfo, - value: &'l Type, - }, - // value - Value { - info: TypeInfo, - value: &'l Type, - }, -} - -impl<'l> FieldValue<'l> { - pub fn nested(raw: &'l Type) -> FieldValue<'l> { - let mut outer_info = TypeInfo::default(); - let ty = extract_inner_type(raw, &mut outer_info); - - let Type::Path(ty_path) = ty else { - panic!("Nested values may only be paths/type references."); - }; - - let segment = ty_path.path.segments.last().unwrap(); - let name = segment.ident.to_string(); - - if name.ends_with("Vec") || name.ends_with("Set") { - let PathArguments::AngleBracketed(args) = &segment.arguments else { - panic!("Received a {name} without inner arguments!"); - }; - - let Some(GenericArgument::Type(inner_ty)) = args.args.first() else { - panic!("{name} item type must be a path!"); - }; - - let mut inner_info = TypeInfo::default(); - let item = extract_inner_type(inner_ty, &mut inner_info); - - Self::NestedList { - collection: &segment.ident, - collection_info: outer_info, - item, - item_info: inner_info, - } - } else if name.ends_with("Map") { - let PathArguments::AngleBracketed(args) = &segment.arguments else { - panic!("Received a {name} without inner arguments!"); - }; - - let Some(GenericArgument::Type(key_ty)) = args.args.first() else { - panic!("{name} key type must be a path!"); - }; - - let Some(GenericArgument::Type(value_ty)) = args.args.last() else { - panic!("{name} value type must be a path!"); - }; - - let mut inner_info = TypeInfo::default(); - let value = extract_inner_type(value_ty, &mut inner_info); - - Self::NestedMap { - collection: &segment.ident, - collection_info: outer_info, - key: key_ty, - value, - value_info: inner_info, - } - } else { - Self::NestedValue { - info: outer_info, - value: ty, - } - } - } - - pub fn value(raw: &'l Type) -> FieldValue<'l> { - let mut info = TypeInfo::default(); - let value = extract_inner_type(raw, &mut info); - - Self::Value { info, value } - } - - pub fn is_outer_boxed(&self) -> bool { - match self { - Self::NestedValue { info, .. } => info.boxed, - Self::NestedList { - collection_info, .. - } => collection_info.boxed, - Self::NestedMap { - collection_info, .. - } => collection_info.boxed, - Self::Value { info, .. } => info.boxed, - } - } - - pub fn is_outer_optional(&self) -> bool { - match self { - Self::NestedValue { info, .. } => info.optional, - Self::NestedList { - collection_info, .. - } => collection_info.optional, - Self::NestedMap { - collection_info, .. - } => collection_info.optional, - Self::Value { info, .. } => info.optional, - } - } - - pub fn get_config_type(&self) -> &'l Type { - match self { - Self::NestedList { item, .. } => item, - Self::NestedMap { value, .. } => value, - Self::NestedValue { value, .. } => value, - Self::Value { value, .. } => value, - } - } - - pub fn get_inner_type(&self) -> Option<&'l Type> { - match self { - Self::Value { value, .. } => Some(value), - _ => None, - } - } -} - -// Only used for partials!!! -impl ToTokens for FieldValue<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { - let inner = match self { - Self::NestedList { - collection, item, .. - } => { - quote! { #collection<<#item as schematic::Config>::Partial> } - } - Self::NestedMap { - collection, - key, - value, - .. - } => { - quote! { - #collection<#key, <#value as schematic::Config>::Partial> - } - } - Self::NestedValue { value, .. } => { - quote! { <#value as schematic::Config>::Partial } - } - Self::Value { value, .. } => { - quote! { #value } - } - }; - - // Boxes are ignored for the partial type, - // and will only be used for the final type! - // if self.is_boxed() { - // inner = quote! { Box<#inner> }; - // } - - tokens.extend(quote! { Option<#inner> }) - } -} diff --git a/crates/macros/src/common/macros.rs b/crates/macros/src/common/macros.rs deleted file mode 100644 index bb5adac1..00000000 --- a/crates/macros/src/common/macros.rs +++ /dev/null @@ -1,305 +0,0 @@ -use crate::common::{Container, Field, SerdeMeta, TaggedFormat, Variant}; -use crate::utils::extract_common_attrs; -use darling::ast::NestedMeta; -use darling::{FromDeriveInput, FromMeta}; -use proc_macro2::{Ident, TokenStream}; -use quote::{ToTokens, quote}; -use syn::{Attribute, Data, DeriveInput, ExprPath, Fields, Generics}; - -// #[serde()] -#[derive(FromDeriveInput, Default)] -#[darling(default, allow_unknown_fields, attributes(serde))] -pub struct ContainerSerdeArgs { - 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, -} - -// #[config()], #[schematic()] -#[derive(FromDeriveInput, Default)] -#[darling( - default, - attributes(config, schematic), - supports(struct_named, enum_any) -)] -pub struct MacroArgs { - // config - pub allow_unknown_fields: bool, - pub context: Option, - pub partial: PartialAttr, - #[cfg(feature = "env")] - pub env_prefix: Option, - - // serde - pub rename: Option, - pub rename_all: Option, - pub rename_all_fields: Option, - pub serde: SerdeMeta, -} - -#[derive(Default)] -pub struct PartialAttr { - meta: Vec, -} - -impl ToTokens for PartialAttr { - 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 PartialAttr { - fn from_list(items: &[NestedMeta]) -> darling::Result { - Ok(Self { - meta: items.to_vec(), - }) - } -} - -pub struct Macro<'l> { - pub args: MacroArgs, - pub serde_args: ContainerSerdeArgs, - pub attrs: Vec<&'l Attribute>, - pub casing_format: String, - pub name: &'l Ident, - pub type_of: Container<'l>, - pub generics: &'l Generics, -} - -impl<'l> Macro<'l> { - pub fn from(input: &'l DeriveInput) -> Self { - let args = MacroArgs::from_derive_input(input).unwrap_or_default(); - let serde_args = ContainerSerdeArgs::from_derive_input(input).unwrap_or_default(); - - let base_casing_format = args - .rename_all - .as_deref() - .or(serde_args.rename_all.as_deref()); - - #[allow(unused_assignments)] - let mut casing_format = String::new(); - - let config_type = match &input.data { - Data::Struct(data) => match &data.fields { - Fields::Named(fields) => { - base_casing_format - .unwrap_or("camelCase") - .clone_into(&mut casing_format); - - Container::NamedStruct { - fields: fields - .named - .iter() - .map(|f| { - let mut field = Field::from(f); - field.serde_args.inherit_from_container(&serde_args); - field.casing_format.clone_from(&casing_format); - #[cfg(feature = "env")] - field.env_prefix.clone_from(&args.env_prefix); - field - }) - .collect::>(), - } - } - Fields::Unnamed(fields) => { - base_casing_format - .unwrap_or("camelCase") - .clone_into(&mut casing_format); - - Container::UnnamedStruct { - fields: fields - .unnamed - .iter() - .enumerate() - .map(|(index, f)| { - let mut field = Field::from(f); - field.index = index; - field.serde_args.inherit_from_container(&serde_args); - field.casing_format.clone_from(&casing_format); - #[cfg(feature = "env")] - field.env_prefix.clone_from(&args.env_prefix); - field - }) - .collect::>(), - } - } - Fields::Unit => { - panic!("Unit structs are not supported."); - } - }, - Data::Enum(data) => { - args.rename_all_fields - .as_deref() - .or(serde_args.rename_all_fields.as_deref()) - .or(base_casing_format) - .unwrap_or("kebab-case") - .clone_into(&mut casing_format); - - let tagged_format = { - if args.serde.untagged || serde_args.untagged { - TaggedFormat::Untagged - } else { - match ( - args.serde.tag.as_ref().or(serde_args.tag.as_ref()), - args.serde.content.as_ref().or(serde_args.content.as_ref()), - ) { - (Some(tag), Some(content)) => { - TaggedFormat::Adjacent(tag.to_owned(), content.to_owned()) - } - (Some(tag), None) => TaggedFormat::Internal(tag.to_owned()), - _ => TaggedFormat::External, - } - } - }; - - Container::Enum { - variants: data - .variants - .iter() - .map(|variant| { - if matches!(variant.fields, Fields::Named(_)) { - panic!("Named enum variants are not supported."); - } - - let mut field = Variant::from(variant); - field.casing_format.clone_from(&casing_format); - field.tagged_format.clone_from(&tagged_format); - field - }) - .collect(), - } - } - Data::Union(_) => { - panic!("Unions are not supported."); - } - }; - - Self { - args, - serde_args, - attrs: extract_common_attrs(&input.attrs), - name: &input.ident, - type_of: config_type, - casing_format, - generics: &input.generics, - } - } - - #[cfg(feature = "schema")] - pub fn get_name(&self) -> String { - match &self.args.rename { - Some(local) => local.to_owned(), - _ => { - if let Some(serde) = &self.serde_args.rename { - serde.to_owned() - } else { - self.name.to_string() - } - } - } - } - - pub fn get_serde_meta(&self) -> TokenStream { - let mut meta = vec![]; - - match &self.type_of { - Container::NamedStruct { .. } => { - meta.push(quote! { default }); - - if self.serde_args.deny_unknown_fields || !self.args.allow_unknown_fields { - meta.push(quote! { deny_unknown_fields }); - } - } - Container::UnnamedStruct { .. } => { - meta.push(quote! { default }); - } - Container::Enum { .. } => { - match &self.args.serde.content { - Some(content) => { - meta.push(quote! { content = #content }); - } - _ => { - if let Some(content) = &self.serde_args.content { - meta.push(quote! { content = #content }); - } - } - } - - match &self.args.serde.tag { - Some(tag) => { - meta.push(quote! { tag = #tag }); - } - _ => { - if let Some(tag) = &self.serde_args.tag { - meta.push(quote! { tag = #tag }); - } - } - } - - if self.args.serde.untagged || self.serde_args.untagged { - meta.push(quote! { untagged }); - } - } - }; - - match &self.args.serde.expecting { - Some(expecting) => { - meta.push(quote! { expecting = #expecting }); - } - _ => { - if let Some(expecting) = &self.serde_args.expecting { - meta.push(quote! { expecting = #expecting }); - } - } - } - - match &self.args.rename { - Some(rename) => { - meta.push(quote! { rename = #rename }); - } - _ => { - if let Some(rename) = &self.serde_args.rename { - meta.push(quote! { rename = #rename }); - } - } - } - - let rename_all = &self.casing_format; - - meta.push(quote! { rename_all = #rename_all }); - - quote! { - #(#meta),* - } - } - - pub fn get_partial_attrs(&self) -> Vec { - let serde_meta = self.get_serde_meta(); - let mut attrs = vec![quote! { #[serde(#serde_meta) ]}]; - - for attr in &self.attrs { - attrs.push(quote! { #attr }); - } - let partial = &self.args.partial; - attrs.push(quote! { #partial }); - - attrs - } - - pub fn is_untagged(&self) -> bool { - self.args.serde.untagged || self.serde_args.untagged - } -} diff --git a/crates/macros/src/common/mod.rs b/crates/macros/src/common/mod.rs deleted file mode 100644 index e283477c..00000000 --- a/crates/macros/src/common/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod container; -mod field; -mod field_value; -mod macros; -mod variant; - -pub use container::*; -pub use field::*; -pub use field_value::*; -pub use macros::*; -pub use variant::*; - -#[derive(darling::FromMeta, Default)] -#[darling(default)] -pub struct SerdeMeta { - pub content: Option, - pub expecting: Option, - pub tag: Option, - pub untagged: bool, -} diff --git a/crates/macros/src/common/variant.rs b/crates/macros/src/common/variant.rs deleted file mode 100644 index 292c68e1..00000000 --- a/crates/macros/src/common/variant.rs +++ /dev/null @@ -1,307 +0,0 @@ -use crate::common::FieldSerdeArgs; -use crate::utils::{extract_common_attrs, format_case}; -use darling::FromAttributes; -use proc_macro2::{Ident, TokenStream}; -use quote::{ToTokens, quote}; -use syn::{Attribute, ExprPath, Fields, Variant as NativeVariant}; - -#[derive(Clone)] -pub enum TaggedFormat { - Untagged, - External, - #[cfg_attr(not(feature = "schema"), allow(dead_code))] - Internal(String), - #[cfg_attr(not(feature = "schema"), allow(dead_code))] - Adjacent(String, String), - // Special case for unit only enums - Unit, -} - -// #[setting()], #[schema()] -#[derive(FromAttributes, Default)] -#[darling(default, attributes(setting, schema))] -pub struct VariantArgs { - pub exclude: bool, - pub null: bool, - - // config - pub default: bool, - pub merge: Option, - pub nested: bool, - pub required: bool, - #[cfg(feature = "validate")] - pub validate: Option, - - // serde - pub rename: Option, - pub skip: bool, - pub skip_deserializing: bool, - pub skip_serializing: bool, - pub untagged: bool, -} - -pub struct Variant<'l> { - pub args: VariantArgs, - pub casing_format: String, - pub tagged_format: TaggedFormat, - pub serde_args: FieldSerdeArgs, - pub attrs: Vec<&'l Attribute>, - pub name: &'l Ident, - pub value: &'l NativeVariant, -} - -impl Variant<'_> { - pub fn from(var: &NativeVariant) -> Variant<'_> { - Variant { - args: VariantArgs::from_attributes(&var.attrs).unwrap_or_default(), - serde_args: FieldSerdeArgs::from_attributes(&var.attrs).unwrap_or_default(), - attrs: extract_common_attrs(&var.attrs), - casing_format: String::new(), - tagged_format: TaggedFormat::Unit, - name: &var.ident, - value: var, - } - } - - pub fn is_default(&self) -> bool { - self.args.default - } - - #[cfg(feature = "schema")] - pub fn is_excluded(&self) -> bool { - self.args.exclude - } - - pub fn is_nested(&self) -> bool { - self.args.nested - } - - pub fn is_required(&self) -> bool { - self.args.required - } - - pub fn get_name(&self, casing_format: Option<&str>) -> String { - match &self.args.rename { - Some(local) => local.to_owned(), - _ => { - if let Some(serde) = &self.serde_args.rename { - serde.to_owned() - } else if let Some(format) = casing_format { - format_case(format, &self.name.to_string(), true) - } else { - self.name.to_string() - } - } - } - } - - pub fn get_serde_meta(&self) -> Option { - let mut meta = vec![]; - - if let Some(alias) = &self.serde_args.alias { - meta.push(quote! { alias = #alias }); - } - - match &self.args.rename { - Some(rename) => { - meta.push(quote! { rename = #rename }); - } - _ => { - if let Some(rename) = &self.serde_args.rename { - meta.push(quote! { rename = #rename }); - } - } - } - - let mut skipped = false; - - if self.args.skip || self.serde_args.skip { - meta.push(quote! { skip }); - skipped = true; - } - - if !skipped { - if self.args.skip_serializing || self.serde_args.skip_serializing { - meta.push(quote! { skip_serializing }); - } - - if self.args.skip_deserializing || self.serde_args.skip_deserializing { - meta.push(quote! { skip_deserializing }); - } - } - - if meta.is_empty() { - return None; - } - - Some(quote! { - #(#meta),* - }) - } - - #[cfg(feature = "schema")] - pub fn generate_schema_type(&self, all_unit: bool) -> TokenStream { - let name = self.get_name(Some(&self.casing_format)); - let tagged_format = if all_unit { - &TaggedFormat::Unit - } else { - &self.tagged_format - }; - - let untagged = matches!(tagged_format, TaggedFormat::Untagged) - || self.args.untagged - || self.serde_args.untagged; - let partial = self.is_nested(); - - let inner = match &self.value.fields { - Fields::Named(_) => unreachable!(), - Fields::Unnamed(fields) => { - if self.args.null { - panic!("Only unit variants can be marked as `null`."); - } - - let fields = fields - .unnamed - .iter() - .map(|field| { - let ty = &field.ty; - - if partial { - quote! { schema.infer_as_nested::<#ty>() } - } else { - quote! { schema.infer::<#ty>() } - } - }) - .collect::>(); - - if fields.len() == 1 { - let inner = &fields[0]; - - quote! { #inner } - } else { - quote! { - Schema::tuple(TupleType::new([ - #(#fields),* - ])) - } - } - } - Fields::Unit => { - if self.args.null || untagged { - quote! { - Schema::null() - } - } else { - quote! { - Schema::literal_value(LiteralValue::String(#name.into())) - } - } - } - }; - - let partial_statement = if partial { - quote! { item.partialize(); } - } else { - quote! {} - }; - - match tagged_format { - TaggedFormat::Unit => { - quote! { - Schema { - name: Some(#name.into()), - ty: #inner.ty, - ..Default::default() - } - } - } - TaggedFormat::Untagged => inner, - TaggedFormat::External => { - let outer = quote! { - Schema::structure(StructType::new([ - (#name.into(), #inner), - ])) - }; - - if partial { - quote! { - { - let mut item = #outer; - #partial_statement - item - } - } - } else { - outer - } - } - TaggedFormat::Internal(tag) => { - quote! { - { - let mut item = #inner; - item.add_field( - #tag, - Schema::literal_value(LiteralValue::String(#name.into())), - ); - #partial_statement - item - } - } - } - TaggedFormat::Adjacent(tag, content) => { - quote! { - Schema::structure(StructType::new([ - (#tag.into(), Schema::literal_value(LiteralValue::String(#name.into()))), - (#content.into(), #inner), - ])) - } - } - } - } -} - -impl ToTokens for Variant<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { - let name = self.name; - - // Gather all attributes - let mut attrs = vec![]; - - if let Some(serde_meta) = self.get_serde_meta() { - attrs.push(quote! { #[serde(#serde_meta)] }); - } - - for attr in &self.attrs { - attrs.push(quote! { #attr }); - } - - tokens.extend(match &self.value.fields { - Fields::Named(_) => unreachable!(), - Fields::Unnamed(fields) => { - let fields = fields - .unnamed - .iter() - .map(|field| { - let vis = &field.vis; - let ty = &field.ty; - - if self.is_nested() { - quote! { #vis <#ty as schematic::Config>::Partial } - } else { - quote! { #vis #ty } - } - }) - .collect::>(); - - quote! { - #(#attrs)* - #name(#(#fields),*), - } - } - Fields::Unit => quote! { - #(#attrs)* - #name, - }, - }); - } -} diff --git a/crates/macros/src/config/container.rs b/crates/macros/src/config/container.rs deleted file mode 100644 index 799d53f0..00000000 --- a/crates/macros/src/config/container.rs +++ /dev/null @@ -1,576 +0,0 @@ -use crate::common::Container; -use proc_macro2::{Ident, TokenStream}; -use quote::quote; - -impl Container<'_> { - pub fn generate_default_values(&self) -> TokenStream { - match self { - Self::NamedStruct { - fields: settings, .. - } => { - let mut setting_names = vec![]; - let mut default_values = vec![]; - - for setting in settings { - setting_names.push(setting.name); - default_values.push(setting.generate_default_value()); - } - - quote! { - Ok(Some(Self { - #(#setting_names: #default_values),* - })) - } - } - Self::UnnamedStruct { - fields: settings, .. - } => { - let mut default_values = vec![]; - - for setting in settings { - default_values.push(setting.generate_default_value()); - } - - quote! { - Ok(Some(Self( - #(#default_values),* - ))) - } - } - Self::Enum { variants } => { - let default_variant = variants.iter().find(|v| v.is_default()); - - if let Some(variant) = default_variant { - let default_value = variant.generate_default_value(); - - quote! { - Ok(Some(Self::#default_value)) - } - } else { - quote! { - Ok(None) - } - } - } - } - } - - pub fn generate_env_values(&self) -> TokenStream { - match self { - Self::NamedStruct { - fields: settings, .. - } - | Self::UnnamedStruct { - fields: settings, .. - } => { - let env_stmts = settings - .iter() - .filter_map(|s| s.generate_env_statement()) - .collect::>(); - - if env_stmts.is_empty() { - quote! { - Ok(None) - } - } else { - quote! { - let mut tracker = std::collections::HashSet::new(); - let mut partial = Self::default(); - #(#env_stmts)* - Ok(if tracker.is_empty() { - None - } else { - Some(partial) - }) - } - } - } - Self::Enum { .. } => { - quote! { - Ok(None) - } - } - } - } - - pub fn generate_extends_from(&self) -> TokenStream { - #[cfg(feature = "extends")] - match self { - Self::NamedStruct { - fields: settings, .. - } => { - use quote::ToTokens; - - // Validate only 1 setting is using it - let mut names = vec![]; - - for setting in settings { - if setting.is_extendable() { - names.push(setting.get_name_raw().to_string()); - } - } - - if names.len() > 1 { - panic!( - "Only 1 setting may use `extend`, found: {}", - names.join(", ") - ); - } - - // Loop again and generate the necessary code - for setting in settings { - if !setting.is_extendable() { - continue; - } - - if let Some(inner_type) = setting.value_type.get_inner_type() { - let name = setting.get_name_raw(); - let value = format!("{}", inner_type.to_token_stream()); - - // Janky but works! - match value.as_str() { - "String" => { - return quote! { - self.#name - .as_ref() - .map(|inner| schematic::ExtendsFrom::String(inner.to_owned())) - }; - } - "Vec" | "Vec < String >" => { - return quote! { - self.#name - .as_ref() - .map(|inner| schematic::ExtendsFrom::List(inner.to_owned())) - }; - } - "ExtendsFrom" - | "schematic::ExtendsFrom" - | "schematic :: ExtendsFrom" => { - return quote! { - self.#name.clone() - }; - } - inner => { - let inner = inner.to_string(); - - panic!( - "Only `String`, `Vec`, or `ExtendsFrom` are supported when using `extend` for {name}. Received `{inner}`." - ); - } - }; - } - } - - quote! { None } - } - Self::UnnamedStruct { .. } | Self::Enum { .. } => { - quote! { None } - } - } - - #[cfg(not(feature = "extends"))] - quote! { None } - } - - pub fn generate_finalize(&self) -> TokenStream { - match self { - Self::NamedStruct { - fields: settings, .. - } - | Self::UnnamedStruct { - fields: settings, .. - } => { - let finalize_stmts = settings - .iter() - .map(|s| s.generate_finalize_statement()) - .collect::>(); - - let env_statement = if cfg!(feature = "env") { - quote! { - if let Some(data) = Self::env_values()? { - partial.merge(context, data)?; - } - } - } else { - quote! {} - }; - - quote! { - let mut partial = Self::default(); - - if let Some(data) = Self::default_values(context)? { - partial.merge(context, data)?; - } - - partial.merge(context, self)?; - - #env_statement - - #(#finalize_stmts)* - - Ok(partial) - } - } - Self::Enum { variants } => { - if self.has_nested() { - let finalize_stmts = variants - .iter() - .flat_map(|s| s.generate_finalize_statement()) - .collect::>(); - - quote! { - Ok(match self { - #(#finalize_stmts)* - _ => self - }) - } - } else { - quote! { - Ok(self) - } - } - } - } - } - - pub fn generate_merge(&self) -> TokenStream { - match self { - Self::NamedStruct { - fields: settings, .. - } - | Self::UnnamedStruct { - fields: settings, .. - } => { - let merge_stmts = settings - .iter() - .map(|s| s.generate_merge_statement()) - .collect::>(); - - quote! { - #(#merge_stmts)* - Ok(()) - } - } - Self::Enum { variants } => { - let merge_stmts = variants - .iter() - .filter_map(|s| s.generate_merge_statement()) - .collect::>(); - - if merge_stmts.is_empty() { - quote! { - *self = next; - Ok(()) - } - } else { - quote! { - match self { - #(#merge_stmts)* - _ => { - *self = next; - } - }; - Ok(()) - } - } - } - } - } - - pub fn generate_validate(&self) -> TokenStream { - match self { - Self::NamedStruct { - fields: settings, .. - } - | Self::UnnamedStruct { - fields: settings, .. - } => { - let validate_stmts = settings - .iter() - .map(|s| s.generate_validate_statement()) - .collect::>(); - - quote! { - #(#validate_stmts)* - } - } - Self::Enum { variants } => { - let validate_stmts = variants - .iter() - .filter_map(|s| s.generate_validate_statement()) - .collect::>(); - - if validate_stmts.is_empty() { - quote! {} - } else { - quote! { - match self { - #(#validate_stmts)* - _ => {} - }; - } - } - } - } - } - - pub fn generate_from_partial(&self, partial_name: &Ident) -> TokenStream { - match self { - Self::NamedStruct { - fields: settings, .. - } => { - let mut setting_names = vec![]; - let mut from_partial_values = vec![]; - - for setting in settings { - setting_names.push(setting.name); - from_partial_values.push(setting.generate_from_partial_value()); - } - - quote! { - Self { - #(#setting_names: #from_partial_values),* - } - } - } - Self::UnnamedStruct { - fields: settings, .. - } => { - let mut from_partial_values = vec![]; - - for setting in settings { - from_partial_values.push(setting.generate_from_partial_value()); - } - - quote! { - Self( - #(#from_partial_values),* - ) - } - } - Self::Enum { variants } => { - let from_partial_values = variants - .iter() - .map(|s| s.generate_from_partial_value(partial_name)) - .collect::>(); - - quote! { - match partial { - #(#from_partial_values)* - } - } - } - } - } - - pub fn generate_partial( - &self, - partial_name: &Ident, - partial_attrs: &[TokenStream], - is_untagged: bool, - ) -> TokenStream { - match self { - Self::NamedStruct { - fields: settings, .. - } => { - quote! { - #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] - #(#partial_attrs)* - pub struct #partial_name { - #(#settings)* - } - } - } - Self::UnnamedStruct { - fields: settings, .. - } => { - quote! { - #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)] - #(#partial_attrs)* - pub struct #partial_name( - #(#settings)* - ); - } - } - Self::Enum { variants } => { - let default_variant = variants - .iter() - .find(|v| v.is_default()) - .or_else(|| variants.first()); - - let default_impl = if let Some(default) = default_variant { - let value = default.generate_default_value(); - - quote! { Self::#value } - } else { - quote! { panic!("No variant has been marked as default!"); } - }; - - if is_untagged { - // For untagged enums, generate custom Deserialize that collects all errors - let deserialize_impl = - self.generate_untagged_deserialize(partial_name, variants); - - quote! { - #[derive(Clone, Debug, PartialEq, serde::Serialize)] - #(#partial_attrs)* - pub enum #partial_name { - #(#variants)* - } - - impl Default for #partial_name { - fn default() -> Self { - #default_impl - } - } - - #deserialize_impl - } - } else { - quote! { - #[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)] - #(#partial_attrs)* - pub enum #partial_name { - #(#variants)* - } - - impl Default for #partial_name { - fn default() -> Self { - #default_impl - } - } - } - } - } - } - } - - fn generate_untagged_deserialize( - &self, - partial_name: &Ident, - variants: &[crate::common::Variant<'_>], - ) -> TokenStream { - use syn::Fields; - - // Generate a deserialize attempt for each variant - let mut variant_attempts = vec![]; - - for variant in variants { - let name = &variant.name; - let variant_name_str = variant.get_name(Some(&variant.casing_format)); - - match &variant.value.fields { - Fields::Named(_) => unreachable!(), - Fields::Unnamed(fields) => { - let field_types: Vec<_> = fields.unnamed.iter().map(|f| &f.ty).collect(); - - if field_types.len() == 1 { - let ty = &field_types[0]; - // Check if this is a nested variant - let inner_ty = if variant.is_nested() { - quote! { <#ty as schematic::Config>::Partial } - } else { - quote! { #ty } - }; - - variant_attempts.push(quote! { - { - let deserializer = schematic::serde_content::Deserializer::new(content.clone()) - .coerce_numbers() - .human_readable(); - match <#inner_ty as serde::Deserialize>::deserialize(deserializer) { - Ok(value) => return Ok(#partial_name::#name(value)), - Err(e) => errors.push((#variant_name_str, e.to_string())), - } - } - }); - } else { - // Tuple variant with multiple fields - let inner_types: Vec<_> = field_types - .iter() - .map(|ty| { - if variant.is_nested() { - quote! { <#ty as schematic::Config>::Partial } - } else { - quote! { #ty } - } - }) - .collect(); - - // Generate field accessors: value.0, value.1, value.2, etc. - let field_accessors: Vec<_> = (0..field_types.len()) - .map(|i| { - let idx = syn::Index::from(i); - quote! { value.#idx } - }) - .collect(); - - variant_attempts.push(quote! { - { - let deserializer = schematic::serde_content::Deserializer::new(content.clone()) - .coerce_numbers() - .human_readable(); - match <(#(#inner_types),*) as serde::Deserialize>::deserialize(deserializer) { - Ok(value) => return Ok(#partial_name::#name(#(#field_accessors),*)), - Err(e) => errors.push((#variant_name_str, e.to_string())), - } - } - }); - } - } - Fields::Unit => { - // Unit variants in untagged enums are serialized as null - variant_attempts.push(quote! { - { - let deserializer = schematic::serde_content::Deserializer::new(content.clone()) - .coerce_numbers() - .human_readable(); - match <() as serde::Deserialize>::deserialize(deserializer) { - Ok(_) => return Ok(#partial_name::#name), - Err(e) => errors.push((#variant_name_str, e.to_string())), - } - } - }); - } - } - } - - quote! { - impl<'de> serde::Deserialize<'de> for #partial_name { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - use serde::de::Error as _; - - // Buffer the content so we can try deserializing it multiple ways - let content = deserializer.deserialize_any(schematic::serde_content::ValueVisitor)?; - - let mut errors: Vec<(&str, String)> = Vec::new(); - - #(#variant_attempts)* - - // All variants failed, build the combined error message - let mut error_msg = format!("failed to parse as any variant of {}:", stringify!(#partial_name)); - for (variant_name, error) in &errors { - error_msg.push_str(&format!("\n- {}: {}", variant_name, error)); - } - - Err(D::Error::custom(error_msg)) - } - } - } - } - - #[cfg(feature = "schema")] - pub fn generate_partial_schema( - &self, - config_name: &Ident, - _partial_name: &Ident, - ) -> TokenStream { - quote! { - let mut schema = #config_name::build_schema(schema); - schematic::internal::partialize_schema(&mut schema, true); - } - } -} diff --git a/crates/macros/src/config/field.rs b/crates/macros/src/config/field.rs deleted file mode 100644 index 70001931..00000000 --- a/crates/macros/src/config/field.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::common::{Field, FieldValue}; -use proc_macro2::{Literal, TokenStream}; -use quote::{ToTokens, TokenStreamExt, quote}; - -impl Field<'_> { - pub fn generate_default_value(&self) -> TokenStream { - self.value_type - .generate_default_value(&self.args, self.is_nullable()) - } - - #[cfg(not(feature = "env"))] - pub fn generate_env_statement(&self) -> Option { - None - } - - #[cfg(feature = "env")] - pub fn generate_env_statement(&self) -> Option { - let key = self.get_field_key(); - - if self.is_nested() { - return self - .value_type - .generate_env_value(&self.args, "") - .map(|value| quote! { partial.#key = #value; }); - } - - let Some(env_key) = self.get_env_var() else { - if self.args.parse_env.is_some() { - panic!("Cannot use `parse_env` without `env` or a parent `env_prefix`."); - } - - return None; - }; - - self.value_type - .generate_env_value(&self.args, &env_key) - .map(|value| quote! { partial.#key = #value; }) - } - - pub fn generate_finalize_statement(&self) -> TokenStream { - let key = self.get_field_key(); - - match (self.value_type.get_finalize_value(), &self.args.transform) { - (Some(value), Some(func)) => { - quote! { - if let Some(data) = partial.#key { - partial.#key = Some(#func(#value, context)?); - } - } - } - (Some(value), None) => { - quote! { - if let Some(data) = partial.#key { - partial.#key = Some(#value); - } - } - } - (None, Some(func)) => { - quote! { - if let Some(data) = partial.#key { - partial.#key = Some(#func(data, context)?); - } - } - } - _ => quote! {}, - } - } - - pub fn generate_from_partial_value(&self) -> TokenStream { - let key = self.get_field_key(); - - #[allow(clippy::collapsible_else_if)] - if matches!(self.value_type, FieldValue::Value { .. }) { - // Reset extendable values since we don't have the entire resolved list - #[cfg(feature = "extends")] - if self.args.extend { - return quote! { Default::default() }; - } - - if self.value_type.is_outer_boxed() { - let mut value = quote! { Box::new(partial.#key.unwrap_or_default()) }; - - if self.is_nullable() { - value = quote! { Some(#value) }; - } - - value - } else { - if self.is_nullable() { - // Use optional values as-is as they're already wrapped in `Option` - quote! { partial.#key } - } else { - // Otherwise unwrap the resolved value or use the type default - quote! { partial.#key.unwrap_or_default() } - } - } - } else { - let mut value = self.value_type.get_from_partial_value(); - - if self.value_type.is_outer_boxed() { - value = quote! { Box::new(#value) }; - } - - if self.is_nullable() { - quote! { - if let Some(data) = partial.#key { - Some(#value) - } else { - None - } - } - } else { - quote! { - { - let data = partial.#key.unwrap_or_default(); - #value - } - } - } - } - } - - pub fn generate_merge_statement(&self) -> TokenStream { - self.value_type - .get_merge_statement(self.get_field_key(), &self.args) - } - - pub fn generate_validate_statement(&self) -> TokenStream { - let key = self.get_field_key(); - let key_quoted = self.get_field_key_string(); - let mut stmts = vec![]; - - #[cfg(feature = "validate")] - if let Some(expr) = self.args.validate.as_ref() { - use syn::Expr; - - let func = match expr { - // func(arg)() - Expr::Call(func) => quote! { #func }, - // func() - Expr::Path(func) => quote! { #func }, - _ => { - panic!("Unsupported `validate` syntax."); - } - }; - - stmts.push(quote! { - if let Err(mut error) = #func(setting, self, context, finalize) { - errors.push(error.prepend_path(path.join_key(#key_quoted))); - } - }); - } - - if let Some(validator) = self.value_type.get_validate_statement(&key_quoted) { - stmts.push(validator); - } - - let first = if stmts.is_empty() { - quote! {} - } else { - quote! { - if let Some(setting) = self.#key.as_ref() { - #(#stmts)* - } - } - }; - - let second = if self.is_required() { - quote! { - if finalize && self.#key.is_none() { - errors.push(schematic::ValidateError::required().prepend_path( - path.join_key(#key_quoted) - )); - } - } - } else { - quote! {} - }; - - quote! { - #first - #second - } - } - - fn get_field_key(&self) -> TokenStream { - self.name - .as_ref() - .map(|name| quote! { #name }) - .unwrap_or_else(|| { - let index = Index(self.index); - - quote! { #index } - }) - } - - fn get_field_key_string(&self) -> String { - self.name - .as_ref() - .map(|name| name.to_string()) - .unwrap_or_else(|| self.index.to_string()) - } -} - -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/macros/src/config/field_value.rs b/crates/macros/src/config/field_value.rs deleted file mode 100644 index e681973c..00000000 --- a/crates/macros/src/config/field_value.rs +++ /dev/null @@ -1,227 +0,0 @@ -use crate::common::{FieldArgs, FieldValue, TypeInfo}; -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; -use syn::{Expr, Lit}; - -impl FieldValue<'_> { - pub fn generate_default_value(&self, args: &FieldArgs, nullable: bool) -> TokenStream { - match self { - Self::NestedList { .. } | Self::NestedMap { .. } => { - if nullable { - quote! { None } - } else { - quote! { Some(Default::default()) } - } - } - Self::NestedValue { info, .. } => { - if nullable { - quote! { None } - } else { - let partial_name = format_ident!("Partial{}", info.config.as_ref().unwrap()); - - quote! { #partial_name::default_values(context)? } - } - } - Self::Value { value, .. } => match args.default.as_ref() { - Some(expr) => match expr { - Expr::Array(_) | Expr::Call(_) | Expr::Macro(_) | Expr::Tuple(_) => { - quote! { Some(#expr) } - } - Expr::Path(func) => { - quote! { handle_default_result(#func(context))? } - } - Expr::Lit(lit) => match &lit.lit { - Lit::Str(string) => quote! { - Some(handle_default_result(#value::try_from(#string))?) - }, - other => quote! { Some(#other) }, - }, - invalid => { - let info = format!("{invalid:?}"); - - panic!( - "Unsupported default value ({info}). May only provide literals, primitives, arrays, or tuples." - ); - } - }, - _ => { - if nullable { - quote! { None } - } else { - quote! { Some(Default::default()) } - } - } - }, - } - } - - pub fn generate_env_value(&self, args: &FieldArgs, env_key: &str) -> Option { - match self { - Self::NestedValue { info, .. } => { - let partial_name = format_ident!("Partial{}", info.config.as_ref().unwrap()); - - Some(quote! { track_env(#partial_name::env_values()?, &mut tracker) }) - } - Self::Value { .. } => Some(match &args.parse_env { - Some(parse_env) => { - quote! { - track_env(parse_env_value(#env_key, #parse_env)?, &mut tracker) - } - } - _ => { - quote! { - track_env(default_env_value(#env_key)?, &mut tracker) - } - } - }), - _ => None, - } - } - - pub fn get_finalize_value(&self) -> Option { - match self { - Self::NestedList { .. } | Self::NestedMap { .. } => { - Some(self.map_data(quote! { value.finalize(context)? })) - } - Self::NestedValue { .. } => Some(self.map_data(quote! { data.finalize(context)? })), - Self::Value { .. } => None, - } - } - - pub fn get_from_partial_value(&self) -> TokenStream { - match self { - Self::NestedList { - item, item_info, .. - } => self.map_data_with_info( - quote! { - #item::from_partial(value) - }, - item_info, - ), - Self::NestedMap { - value, value_info, .. - } => self.map_data_with_info( - quote! { - #value::from_partial(value) - }, - value_info, - ), - Self::NestedValue { info, .. } => { - let config = info.config.as_ref(); - - quote! { - #config::from_partial(data) - } - } - Self::Value { .. } => quote! { data }, - } - } - - pub fn get_merge_statement(&self, key: TokenStream, args: &FieldArgs) -> TokenStream { - if let Self::NestedValue { .. } = self { - if args.merge.is_some() { - panic!("Nested configs do not support `merge` unless wrapped in a collection."); - } - - return quote! { - self.#key = merge_nested_setting( - self.#key.take(), - next.#key.take(), - context, - )?; - }; - }; - - match args.merge.as_ref() { - Some(func) => { - quote! { - self.#key = merge_setting( - self.#key.take(), - next.#key.take(), - context, - #func, - )?; - } - } - _ => { - quote! { - if next.#key.is_some() { - self.#key = next.#key; - } - } - } - } - } - - pub fn get_validate_statement(&self, key: &str) -> Option { - match self { - Self::NestedList { .. } => Some(quote! { - for (i, item) in setting.iter().enumerate() { - if let Err(nested_errors) = item.validate_with_path(context, finalize, path.join_key(#key).join_index(i)) { - errors.extend(nested_errors); - } - } - }), - Self::NestedMap { .. } => Some(quote! { - for (key, value) in setting { - if let Err(nested_errors) = value.validate_with_path(context, finalize, path.join_key(#key).join_key(key)) { - errors.extend(nested_errors); - } - } - }), - Self::NestedValue { .. } => Some(quote! { - if let Err(nested_errors) = setting.validate_with_path(context, finalize, path.join_key(#key)) { - errors.extend(nested_errors); - } - }), - Self::Value { .. } => { - // Handled in parent struct - None - } - } - } - - pub fn map_data(&self, mapped_data: TokenStream) -> TokenStream { - match self { - Self::NestedList { collection, .. } => { - quote! { - { - let mut result = #collection::default(); - for value in data { - result.push(#mapped_data); - } - result - } - } - } - Self::NestedMap { collection, .. } => { - quote! { - { - let mut result = #collection::default(); - for (key, value) in data { - result.insert(key, #mapped_data); - } - result - } - } - } - Self::NestedValue { .. } | Self::Value { .. } => { - quote! { #mapped_data } - } - } - } - - pub fn map_data_with_info(&self, mapped_data: TokenStream, info: &TypeInfo) -> TokenStream { - let mut data = mapped_data; - - if info.boxed { - data = quote! { Box::new(#data) }; - } - - if info.optional { - data = quote! { Some(#data) }; - } - - self.map_data(data) - } -} diff --git a/crates/macros/src/config/mod.rs b/crates/macros/src/config/mod.rs deleted file mode 100644 index 9aa277a1..00000000 --- a/crates/macros/src/config/mod.rs +++ /dev/null @@ -1,193 +0,0 @@ -pub mod container; -pub mod field; -pub mod field_value; -pub mod variant; - -use crate::common::Macro; -use proc_macro2::TokenStream; -use quote::{ToTokens, format_ident, quote}; - -pub struct ConfigMacro<'l>(pub Macro<'l>); - -impl ToTokens for ConfigMacro<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { - let cfg = &self.0; - let name = cfg.name; - - // Generate the partial implementation - let partial_name = format_ident!("Partial{}", cfg.name); - let partial_attrs = cfg.get_partial_attrs(); - let is_untagged = cfg.is_untagged(); - let partial = cfg - .type_of - .generate_partial(&partial_name, &partial_attrs, is_untagged); - - tokens.extend(quote! { - #partial - }); - - // Generate implementations - let default_values = cfg.type_of.generate_default_values(); - let finalize = cfg.type_of.generate_finalize(); - let merge = cfg.type_of.generate_merge(); - let from_partial = cfg.type_of.generate_from_partial(&partial_name); - let settings_metadata = cfg.type_of.generate_settings_metadata(); - - let context = match cfg.args.context.as_ref() { - Some(ctx) => quote! { #ctx }, - None => quote! { () }, - }; - - let env_method = if cfg!(feature = "env") { - let env_values = cfg.type_of.generate_env_values(); - - quote! { - fn env_values() -> std::result::Result, schematic::ConfigError> { - use schematic::internal::*; - #env_values - } - } - } else { - quote! {} - }; - - let extends_method = if cfg!(feature = "extends") { - let extends_from = cfg.type_of.generate_extends_from(); - - quote! { - fn extends_from(&self) -> Option { - #extends_from - } - } - } else { - quote! {} - }; - - let validate_method = if cfg!(feature = "validate") { - let validate = cfg.type_of.generate_validate(); - - quote! { - fn validate_with_path( - &self, - context: &Self::Context, - finalize: bool, - path: schematic::Path - ) -> std::result::Result<(), Vec> { - let mut errors = vec![]; - - #validate - - if !errors.is_empty() { - return Err(errors); - } - - Ok(()) - } - } - } else { - quote! {} - }; - - tokens.extend(quote! { - #[automatically_derived] - impl schematic::PartialConfig for #partial_name { - type Context = #context; - - fn default_values(context: &Self::Context) -> std::result::Result, schematic::ConfigError> { - use schematic::internal::*; - #default_values - } - - #env_method - - #extends_method - - fn finalize(self, context: &Self::Context) -> std::result::Result { - #finalize - } - - fn merge( - &mut self, - context: &Self::Context, - mut next: Self, - ) -> std::result::Result<(), schematic::ConfigError> { - use schematic::internal::*; - #merge - } - - #validate_method - } - - #[automatically_derived] - impl Default for #name { - fn default() -> Self { - let context = <::Partial as schematic::PartialConfig>::Context::default(); - - let defaults = <::Partial as schematic::PartialConfig>::default_values(&context).unwrap().unwrap_or_default(); - - ::from_partial(defaults) - } - } - - #[automatically_derived] - impl schematic::Config for #name { - type Partial = #partial_name; - - fn from_partial(partial: Self::Partial) -> Self { - #from_partial - } - - fn settings() -> schematic::ConfigSettingMap { - #settings_metadata - } - } - }); - - #[cfg(feature = "schema")] - { - let schema_name = cfg.get_name(); - let schema_impl = cfg.type_of.generate_schema(&cfg.attrs); - - let partial_schema_name = partial_name.to_string(); - let partial_schema_impl = cfg.type_of.generate_partial_schema(name, &partial_name); - - tokens.extend(quote! { - #[automatically_derived] - impl schematic::Schematic for #name { - fn schema_name() -> Option { - Some(#schema_name.into()) - } - - fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { - use schematic::schema::*; - - #schema_impl - } - } - - #[automatically_derived] - impl schematic::Schematic for #partial_name { - fn schema_name() -> Option { - Some(#partial_schema_name.into()) - } - - fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { - #partial_schema_impl - schema - } - } - }); - } - - #[cfg(not(feature = "schema"))] - { - tokens.extend(quote! { - #[automatically_derived] - impl schematic::Schematic for #name {} - - #[automatically_derived] - impl schematic::Schematic for #partial_name {} - }); - } - } -} diff --git a/crates/macros/src/config/variant.rs b/crates/macros/src/config/variant.rs deleted file mode 100644 index bae45c58..00000000 --- a/crates/macros/src/config/variant.rs +++ /dev/null @@ -1,292 +0,0 @@ -use crate::common::Variant; -use proc_macro2::{Ident, TokenStream}; -use quote::{format_ident, quote}; -use syn::{Fields, FieldsUnnamed}; - -impl Variant<'_> { - pub fn generate_default_value(&self) -> TokenStream { - let name = &self.name; - - match &self.value.fields { - Fields::Named(_) => unreachable!(), - Fields::Unnamed(fields) => { - let fields = fields - .unnamed - .iter() - .map(|_| { - quote! { Default::default() } - }) - .collect::>(); - - quote! { #name(#(#fields),*) } - } - Fields::Unit => quote! { #name }, - } - } - - pub fn generate_finalize_statement(&self) -> Option { - let name = &self.name; - - match &self.value.fields { - Fields::Named(_) => unreachable!(), - Fields::Unnamed(fields) => { - if !self.is_nested() { - return None; - } - - Some(self.map_unnamed_match(self.name, fields, |outer_names, _| { - let stmts = outer_names - .iter() - .map(|o| { - quote! { #o.finalize(context)? } - }) - .collect::>(); - - quote! { - Self::#name(#(#stmts),*) - } - })) - } - Fields::Unit => None, - } - } - - pub fn generate_merge_statement(&self) -> Option { - let name = &self.name; - let args = &self.args; - - match &self.value.fields { - Fields::Named(_) => unreachable!(), - Fields::Unnamed(fields) => { - if self.is_nested() { - if args.merge.is_some() { - panic!("Nested variants do not support `merge`."); - } - - return Some(self.map_unnamed_match( - self.name, - fields, - |outer_names, inner_names| { - let merge_stmts = 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 { - #(#merge_stmts)* - } else { - *self = next; - } - } - }, - )); - } - - if let Some(func) = args.merge.as_ref() { - return Some(self.map_unnamed_match( - self.name, - fields, - |outer_names, inner_names| { - if outer_names.len() == 1 { - quote! { - if let Self::#name(ai) = next { - *self = Self::#name( - #func(ao.to_owned(), ai, context)?.unwrap_or_default(), - ); - } else { - *self = next; - } - } - } else { - let defaults = outer_names - .iter() - .map(|_| { - quote! { Default::default() } - }) - .collect::>(); - - quote! { - if let Self::#name(#(#inner_names),*) = next { - if let Some((#(#outer_names),*)) = #func( - (#(#outer_names.to_owned()),*), - (#(#inner_names),*), - context, - )? { - *self = Self::#name(#(#outer_names),*); - } else { - *self = Self::#name(#(#defaults),*); - } - } else { - *self = next; - } - } - } - }, - )); - } - - None - } - Fields::Unit => { - if args.merge.is_some() { - panic!("Unit variants do not support `merge`.") - } - - None - } - } - } - - pub fn generate_validate_statement(&self) -> Option { - let Fields::Unnamed(fields) = &self.value.fields else { - return None; - }; - - Some(self.map_unnamed_match(self.name, fields, |outer_names, _| { - let mut stmts = vec![]; - let name = self.get_name(Some(&self.casing_format)); - - #[cfg(feature = "validate")] - if let Some(expr) = self.args.validate.as_ref() { - use syn::Expr; - - let func = match expr { - // func(arg)() - Expr::Call(func) => quote! { #func }, - // func() - Expr::Path(func) => quote! { #func }, - _ => { - panic!("Unsupported `validate` syntax."); - } - }; - - let value = if outer_names.len() == 1 { - quote! { #(#outer_names),* } - } else { - quote! { (#(#outer_names),*) } - }; - - stmts.push(quote! { - if let Err(mut error) = #func(#value, self, context, finalize) { - errors.push(error.prepend_path(path.join_key(#name))); - } - }); - } - - if self.is_required() { - stmts.push(quote! { - if finalize && [#(#outer_names),*].iter().any(|v| v.is_none()) { - errors.push(schematic::ValidateError::required().prepend_path( - path.join_key(#name) - )); - } - }); - } - - if self.is_nested() { - stmts.extend(outer_names - .iter() - .enumerate() - .map(|(index, o)| { - quote! { - if let Err(nested_errors) = #o.validate_with_path(context, finalize, path.join_index(#index)) { - errors.extend(nested_errors); - } - } - }) - .collect::>()); - } - - quote! { - #(#stmts)* - } - })) - } - - pub fn generate_from_partial_value(&self, partial_name: &Ident) -> TokenStream { - let name = &self.name; - - match &self.value.fields { - Fields::Named(_) => unreachable!(), - Fields::Unnamed(fields) => { - self.map_unnamed_match_custom(self.name, partial_name, fields, |outer_names, _| { - let stmts = outer_names - .iter() - .enumerate() - .map(|(index, o)| { - if self.is_nested() { - let ty = &fields.unnamed[index].ty; - - quote! { #ty::from_partial(#o) } - } else { - quote! { #o } - } - }) - .collect::>(); - - quote! { - Self::#name(#(#stmts),*) - } - }) - } - Fields::Unit => { - quote! { - #partial_name::#name => Self::#name, - } - } - } - } - - 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) - } - - #[allow(clippy::explicit_counter_loop)] - 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![]; - let mut merge_stmts = vec![]; - - for _ in &fields.unnamed { - let outer_name = format_ident!("{}o", count as char); - let inner_name = format_ident!("{}i", count as char); - - merge_stmts.push(quote! { - #outer_name.merge(context, #inner_name)?; - }); - - 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/macros/src/config_enum/mod.rs b/crates/macros/src/config_enum/mod.rs deleted file mode 100644 index a25d68a3..00000000 --- a/crates/macros/src/config_enum/mod.rs +++ /dev/null @@ -1,229 +0,0 @@ -mod variant; - -use crate::common::ContainerSerdeArgs; -use crate::config_enum::variant::Variant; -use darling::FromDeriveInput; -use proc_macro::TokenStream; -use quote::quote; -use syn::{Data, DeriveInput, parse_macro_input}; - -// #[config()] -#[derive(FromDeriveInput, Default)] -#[darling(default, attributes(config), supports(enum_unit, enum_tuple))] -pub struct ConfigEnumArgs { - before_parse: Option, - - // serde - rename: Option, - rename_all: Option, - rename_all_fields: Option, -} - -// #[derive(ConfigEnum)] -pub fn macro_impl(item: TokenStream) -> TokenStream { - let input: DeriveInput = parse_macro_input!(item); - let args = ConfigEnumArgs::from_derive_input(&input).expect("Failed to parse arguments."); - let serde_args = ContainerSerdeArgs::from_derive_input(&input).unwrap_or_default(); - - let Data::Enum(data) = input.data else { - panic!("Only unit enums are supported."); - }; - - #[cfg(feature = "schema")] - let attrs = crate::utils::extract_common_attrs(&input.attrs); - - let enum_name = &input.ident; - - #[cfg(feature = "schema")] - let meta_name = args - .rename - .as_deref() - .or(serde_args.rename.as_deref()) - .map(|n| n.to_owned()) - .unwrap_or(enum_name.to_string()); - - let casing_format = args - .rename_all_fields - .as_deref() - .or(args.rename_all.as_deref()) - .or(serde_args.rename_all_fields.as_deref()) - .or(serde_args.rename_all.as_deref()) - .unwrap_or("kebab-case"); - - // Extract unit variants - let variants = data - .variants - .iter() - .map(|v| Variant::from(v, casing_format)) - .collect::>(); - - // Render variants to tokens - let mut unit_names = vec![]; - let mut display_stmts = vec![]; - let mut from_stmts = vec![]; - let mut schema_types = vec![]; - let mut has_fallback = false; - #[cfg(feature = "schema")] - let mut default_index = None; - - #[cfg_attr(not(feature = "schema"), allow(unused_variables))] - for (index, variant) in variants.into_iter().enumerate() { - unit_names.push(variant.get_unit_name()); - display_stmts.push(variant.get_display_fmt()); - from_stmts.push(variant.get_from_str()); - schema_types.push(variant.get_schema_type()); - - #[cfg(feature = "schema")] - if variant.default { - default_index = Some(index); - } - - if variant.args.fallback { - if has_fallback { - panic!("Only 1 fallback variant is supported.") - } - - has_fallback = true; - } - } - - let before_parse = if let Some(parser) = &args.before_parse { - if parser == "lowercase" { - quote! { - let value = value.to_lowercase(); - let value = value.as_str(); - } - } else if parser == "UPPERCASE" { - quote! { - let value = value.to_uppercase(); - let value = value.as_str(); - } - } else { - panic!("Unknown `before_parse` value {parser}"); - } - } else { - quote! {} - }; - - let from_fallback = if has_fallback { - quote! {} - } else { - quote! { - unknown => return Err(schematic::ConfigError::EnumUnknownVariant(unknown.to_owned())), - } - }; - - let mut impls = vec![]; - - impls.push(quote! { - #[automatically_derived] - impl schematic::ConfigEnum for #enum_name { - fn variants() -> Vec<#enum_name> { - vec![ - #(#unit_names),* - ] - } - } - - #[automatically_derived] - impl std::str::FromStr for #enum_name { - type Err = schematic::ConfigError; - - fn from_str(value: &str) -> std::result::Result { - #before_parse - Ok(match value { - #(#from_stmts)* - #from_fallback - }) - } - } - - #[automatically_derived] - impl std::convert::TryFrom for #enum_name { - 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 #enum_name { - 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 #enum_name { - 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 #enum_name { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", match self { - #(#display_stmts)* - }) - } - } - }); - - #[cfg(feature = "schema")] - { - use crate::utils::{extract_comment, extract_deprecated, map_option_argument_quote}; - - let default_index = map_option_argument_quote(default_index); - - let deprecated = if let Some(comment) = extract_deprecated(&attrs) { - quote! { schema.set_deprecated(#comment); } - } else { - quote! {} - }; - let description = if let Some(comment) = extract_comment(&attrs) { - quote! { schema.set_description(#comment); } - } else { - quote! {} - }; - - impls.push(quote! { - #[automatically_derived] - impl schematic::Schematic for #enum_name { - fn schema_name() -> Option { - Some(#meta_name.into()) - } - - fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { - use schematic::schema::*; - #deprecated - #description - schema.enumerable(EnumType::from_fields( - [ - #(#schema_types),* - ], - #default_index, - )) - } - } - }); - } - - #[cfg(not(feature = "schema"))] - { - impls.push(quote! { - #[automatically_derived] - impl schematic::Schematic for #enum_name {} - }); - } - - quote! { - #(#impls)* - } - .into() -} diff --git a/crates/macros/src/config_enum/variant.rs b/crates/macros/src/config_enum/variant.rs deleted file mode 100644 index 2a84ea8f..00000000 --- a/crates/macros/src/config_enum/variant.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::common::FieldSerdeArgs; -use crate::utils::{ - extract_comment, extract_common_attrs, extract_deprecated, format_case, get_meta_path, - map_option_field_quote, -}; -use darling::FromAttributes; -use proc_macro2::{Ident, TokenStream}; -use quote::quote; -use syn::{Attribute, Fields, Variant as NativeVariant}; - -// #[variant()] -#[derive(FromAttributes, Default)] -#[darling(default, attributes(variant))] -pub struct VariantArgs { - pub fallback: bool, - pub value: Option, -} - -pub struct Variant<'l> { - pub args: VariantArgs, - #[cfg_attr(not(feature = "schema"), allow(dead_code))] - pub default: bool, - pub serde_args: FieldSerdeArgs, - pub attrs: Vec<&'l Attribute>, - pub name: &'l Ident, - pub value: String, -} - -impl Variant<'_> { - pub fn from<'n>(variant: &'n NativeVariant, format: &str) -> Variant<'n> { - let args = VariantArgs::from_attributes(&variant.attrs).unwrap_or_default(); - let serde_args = FieldSerdeArgs::from_attributes(&variant.attrs).unwrap_or_default(); - - if args.fallback { - match &variant.fields { - Fields::Unnamed(fields) => { - if fields.unnamed.len() != 1 { - panic!("Only 1 unnamed field is supported for `fallback`."); - } - } - _ => { - panic!("Only unnamed tuple variants are supported for `fallback`."); - } - } - - if args.value.is_some() { - panic!("`value` is not supported for `fallback`."); - } - } else if !matches!(variant.fields, Fields::Unit) { - panic!("Only unit variants are supported."); - } - - let value = if args.fallback { - String::new() - } else if let Some(v) = &args.value { - v.to_owned() - } else if let Some(v) = &serde_args.rename { - v.to_owned() - } else { - format_case(format, variant.ident.to_string().as_str(), true) - }; - - let attrs = extract_common_attrs(&variant.attrs); - - Variant { - default: attrs - .iter() - .any(|v| get_meta_path(&v.meta).is_ident("default")), - attrs, - name: &variant.ident, - value, - args, - serde_args, - } - } - - pub fn get_display_fmt(&self) -> TokenStream { - let name = &self.name; - let value = &self.value; - - if self.args.fallback { - quote! { - Self::#name(fallback) => fallback, - } - } else { - quote! { - Self::#name => #value, - } - } - } - - pub fn get_from_str(&self) -> TokenStream { - let name = &self.name; - let value = &self.value; - - if self.args.fallback { - quote! { - fallback => Self::#name( - fallback.try_into().map_err(|_| { - schematic::ConfigError::EnumInvalidFallback(fallback.to_string()) - })? - ), - } - } else if let Some(alias) = &self.serde_args.alias { - quote! { - #value | #alias => Self::#name, - } - } else { - quote! { - #value => Self::#name, - } - } - } - - pub fn get_schema_type(&self) -> TokenStream { - let name = self.name.to_string(); - let comment = map_option_field_quote("comment", extract_comment(&self.attrs)); - let deprecated = map_option_field_quote("deprecated", extract_deprecated(&self.attrs)); - - let inner_schema = if self.args.fallback { - quote! { - Schema::string(StringType::default()) - } - } else { - let value = &self.value; - - quote! { - Schema::literal_value(LiteralValue::String(#value.into())) - } - }; - - if comment.is_none() && deprecated.is_none() { - quote! { - (#name.into(), SchemaField::new(#inner_schema)) - } - } else { - quote! { - (#name.into(), { - let mut field = SchemaField::new(#inner_schema); - #comment - #deprecated - field - }) - } - } - } - - pub fn get_unit_name(&self) -> TokenStream { - let name = &self.name; - - if self.args.fallback { - quote! { - Self::#name(Default::default()) - } - } else { - quote! { - Self::#name - } - } - } -} diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 2bfea68c..d2f34860 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -1,41 +1,38 @@ -mod common; -mod utils; +#![allow(unused)] -#[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 schematic_core::container::{Container, ContainerMacro}; use syn::{DeriveInput, parse_macro_input}; // #[derive(Config)] #[cfg(feature = "config")] -#[proc_macro_derive(Config, attributes(config, setting))] +#[proc_macro_derive(Config, attributes(config, serde, setting))] pub fn config(item: TokenStream) -> TokenStream { let input: DeriveInput = parse_macro_input!(item); - let output = config::ConfigMacro(Macro::from(&input)); + let output = Container::from(input); quote! { #output }.into() } // #[derive(ConfigEnum)] #[cfg(feature = "config")] -#[proc_macro_derive(ConfigEnum, attributes(config, variant))] +#[proc_macro_derive(ConfigEnum, attributes(config, serde, variant))] pub fn config_enum(item: TokenStream) -> TokenStream { - config_enum::macro_impl(item) + let input: DeriveInput = parse_macro_input!(item); + let mut output = Container::from(input); + output.macro_type = ContainerMacro::ConfigUnitEnum; + + quote! { #output }.into() } // #[derive(Schematic)] #[cfg(feature = "schema")] -#[proc_macro_derive(Schematic, attributes(schematic, schema))] +#[proc_macro_derive(Schematic, attributes(schema, schematic, serde))] pub fn schematic(item: TokenStream) -> TokenStream { let input: DeriveInput = parse_macro_input!(item); - let output = schematic::SchematicMacro(Macro::from(&input)); + let mut output = Container::from(input); + output.macro_type = ContainerMacro::Schematic; quote! { #output }.into() } diff --git a/crates/macros/src/schematic/mod.rs b/crates/macros/src/schematic/mod.rs deleted file mode 100644 index 29c8d22a..00000000 --- a/crates/macros/src/schematic/mod.rs +++ /dev/null @@ -1,31 +0,0 @@ -use crate::common::Macro; -use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; - -pub struct SchematicMacro<'l>(pub Macro<'l>); - -impl ToTokens for SchematicMacro<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) { - let cfg = &self.0; - let name = cfg.name; - - let schema_name = cfg.get_name(); - let schema_impl = cfg.type_of.generate_schema(&cfg.attrs); - let (impl_generics, ty_generics, where_clause) = cfg.generics.split_for_impl(); - - tokens.extend(quote! { - #[automatically_derived] - impl #impl_generics schematic::Schematic for #name #ty_generics #where_clause { - fn schema_name() -> Option { - Some(#schema_name.into()) - } - - fn build_schema(mut schema: schematic::SchemaBuilder) -> schematic::Schema { - use schematic::schema::*; - - #schema_impl - } - } - }); - } -} diff --git a/crates/macros/src/utils.rs b/crates/macros/src/utils.rs deleted file mode 100644 index c4beeae3..00000000 --- a/crates/macros/src/utils.rs +++ /dev/null @@ -1,195 +0,0 @@ -use convert_case::{Boundary, Case, Casing}; -use quote::{ToTokens, format_ident, quote}; -use syn::{Attribute, Expr, ExprLit, Lit, Meta, Path}; - -pub fn format_case(format: &str, value: &str, is_variant: bool) -> String { - let case = match format { - "lowercase" => { - return value.to_lowercase(); - } - "UPPERCASE" => { - return value.to_uppercase(); - } - "PascalCase" => Case::Pascal, - "camelCase" => Case::Camel, - "snake_case" => Case::Snake, - "SCREAMING_SNAKE_CASE" => Case::UpperSnake, - "SCREAMING-KEBAB-CASE" => Case::UpperKebab, - _ => Case::Kebab, - }; - - value - .from_case(if is_variant { - Case::Pascal - } else { - Case::Snake - }) - .remove_boundaries(&[Boundary::UpperDigit, Boundary::LowerDigit]) - .to_case(case) -} - -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 get_meta_path(meta: &Meta) -> &Path { - match meta { - Meta::Path(path) => path, - Meta::List(list) => &list.path, - Meta::NameValue(nv) => &nv.path, - } -} - -pub fn extract_common_attrs(attrs: &[Attribute]) -> Vec<&Attribute> { - let preserve = ["allow", "default", "deprecated", "doc", "warn"]; - - attrs - .iter() - .filter(|a| { - let path = get_meta_path(&a.meta); - - preserve.iter().any(|n| path.is_ident(n)) - }) - .collect() -} - -pub fn extract_comment(attrs: &[&Attribute]) -> Option { - let mut lines = vec![]; - - for attr in attrs { - if let Meta::NameValue(meta) = &attr.meta - && meta.path.is_ident("doc") - && let Expr::Lit(ExprLit { - lit: Lit::Str(value), - .. - }) = &meta.value - { - for mut line in value.value().split('\n') { - line = line.trim(); - - if line.starts_with("* ") { - line = &line[2..]; - } else if line.starts_with(" * ") { - line = &line[3..]; - } - - lines.push(line.to_owned()); - } - } - } - - if lines.is_empty() { - None - } else { - Some(lines.join("\n")) - } -} - -pub fn extract_deprecated(attrs: &[&Attribute]) -> Option { - for attr in attrs { - match &attr.meta { - Meta::NameValue(meta) => { - if meta.path.is_ident("deprecated") - && let Expr::Lit(lit) = &meta.value - { - match &lit.lit { - Lit::Bool(value) => { - if value.value() { - return Some(String::new()); // No message, handle in renderer - } - } - Lit::Str(value) => { - return Some(value.value().trim().to_owned()); - } - _ => {} - }; - } - } - Meta::Path(_) if get_meta_path(&attr.meta).is_ident("deprecated") => { - return Some(String::new()); // No message, handle in renderer - } - _ => {} - } - } - - None -} - -#[cfg(feature = "schema")] -pub fn map_bool_field_quote(name: &str, value: bool) -> Option { - if value { - let id = format_ident!("{}", name); - - Some(quote! { - field.#id = true; - - }) - } else { - None - } -} - -#[cfg(feature = "schema")] -pub fn map_vec_field_quote(name: &str, value: Vec) -> Option { - if !value.is_empty() { - let id = format_ident!("{}", name); - - Some(quote! { - field.#id = [ - #(#value),* - ].into_iter().map(|v| v.to_string()).collect::>(); - - }) - } else { - None - } -} - -pub fn map_option_field_quote( - name: &str, - value: Option, -) -> Option { - match value { - Some(value) => { - let id = format_ident!("{}", name); - - Some(quote! { - field.#id = Some(#value.into()); - - }) - } - _ => None, - } -} - -// pub fn map_option_variant_quote( -// name: &str, -// value: Option, -// ) -> Option { -// if let Some(value) = value { -// let id = format_ident!("{}", name); - -// Some(quote! { -// #id: Some(#value.into()), - -// }) -// } else { -// None -// } -// } - -#[cfg(feature = "schema")] -pub fn map_option_argument_quote(value: Option) -> proc_macro2::TokenStream { - match value { - Some(value) => { - quote! { Some(#value.into()) } - } - _ => { - quote! { None } - } - } -} diff --git a/crates/macros-next/tests/config_enum_test.rs b/crates/macros/tests/config_enum_test.rs similarity index 99% rename from crates/macros-next/tests/config_enum_test.rs rename to crates/macros/tests/config_enum_test.rs index 3a4255fd..c0575795 100644 --- a/crates/macros-next/tests/config_enum_test.rs +++ b/crates/macros/tests/config_enum_test.rs @@ -5,7 +5,7 @@ #![allow(dead_code)] use schematic::{ConfigEnum as ConfigEnumTrait, ConfigError, SchemaBuilder, SchemaType}; -use schematic_macros_next::ConfigEnum; +use schematic_macros::ConfigEnum; use std::str::FromStr; mod basic { diff --git a/crates/macros-next/tests/config_test.rs b/crates/macros/tests/config_test.rs similarity index 99% rename from crates/macros-next/tests/config_test.rs rename to crates/macros/tests/config_test.rs index 91ecd25b..feea54ee 100644 --- a/crates/macros-next/tests/config_test.rs +++ b/crates/macros/tests/config_test.rs @@ -8,7 +8,7 @@ // imported by name (the trait aliased out of the way) and the derives come // from this crate. use schematic::{Config as ConfigTrait, ConfigLoader, PartialConfig}; -use schematic_macros_next::Config; +use schematic_macros::Config; use serde::Serialize; use std::collections::HashMap; diff --git a/crates/macros-next/tests/schematic_test.rs b/crates/macros/tests/schematic_test.rs similarity index 99% rename from crates/macros-next/tests/schematic_test.rs rename to crates/macros/tests/schematic_test.rs index dfd8c0b3..43bbac98 100644 --- a/crates/macros-next/tests/schematic_test.rs +++ b/crates/macros/tests/schematic_test.rs @@ -3,7 +3,7 @@ #![allow(dead_code)] use schematic::{Schema, SchemaBuilder, SchemaType}; -use schematic_macros_next::Schematic; +use schematic_macros::Schematic; use std::collections::HashMap; fn build() -> Schema { diff --git a/crates/schematic/examples/show_error.rs b/crates/schematic/examples/show_error.rs index cc769d39..8535add6 100644 --- a/crates/schematic/examples/show_error.rs +++ b/crates/schematic/examples/show_error.rs @@ -9,7 +9,7 @@ pub struct SomeConfig { } #[derive(Config)] -#[config(serde(untagged))] +#[serde(untagged)] enum Untagged { Foo, Bar(bool), diff --git a/crates/schematic/src/config/loader.rs b/crates/schematic/src/config/loader.rs index 370c4725..fbad4034 100644 --- a/crates/schematic/src/config/loader.rs +++ b/crates/schematic/src/config/loader.rs @@ -55,6 +55,8 @@ impl Default for ConfigLoader { impl ConfigLoader { /// Create a new config loader and auto-register formats based on enables features. pub fn new() -> Self { + // Only mutated when a format feature registers one below + #[allow(unused_mut)] let mut loader = ConfigLoader::default(); #[cfg(feature = "json")] diff --git a/crates/schematic/src/config/mod.rs b/crates/schematic/src/config/mod.rs index 6fedc25d..dbc336ea 100644 --- a/crates/schematic/src/config/mod.rs +++ b/crates/schematic/src/config/mod.rs @@ -9,6 +9,7 @@ mod loader; mod merger; mod parser; mod path; +#[cfg(any(feature = "type_regex", feature = "type_semver"))] mod settings; mod source; #[cfg(feature = "validate")] @@ -25,6 +26,7 @@ pub use loader::*; pub use merger::*; pub use parser::*; pub use path::*; +#[cfg(any(feature = "type_regex", feature = "type_semver"))] pub use settings::*; pub use source::*; #[cfg(feature = "validate")] diff --git a/crates/schematic/src/config/settings/mod.rs b/crates/schematic/src/config/settings/mod.rs index 917715c6..1472301b 100644 --- a/crates/schematic/src/config/settings/mod.rs +++ b/crates/schematic/src/config/settings/mod.rs @@ -1,3 +1,15 @@ +//! Newtypes for third-party types that cannot be used as a setting as-is. +//! +//! A setting that isn't wrapped in `Option` falls back to `Default::default()` +//! when the config provides no value and the field declares no +//! `#[setting(default)]`, so every required setting has to implement `Default`. +//! The types wrapped here don't, and being foreign, can't have it implemented +//! for them directly. +//! +//! Each wrapper picks a sensible default, derefs to the inner type so it reads +//! like the original, and (de)serializes through a string, which also frees it +//! from the upstream crate's `serde` feature. + #[cfg(feature = "type_regex")] mod regex; #[cfg(feature = "type_semver")] diff --git a/crates/schematic/src/config/settings/regex.rs b/crates/schematic/src/config/settings/regex.rs index 6a38c4be..b6626347 100644 --- a/crates/schematic/src/config/settings/regex.rs +++ b/crates/schematic/src/config/settings/regex.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "schema")] use crate::schema::{Schema, SchemaBuilder, Schematic}; use regex::{Error, Regex}; use serde::{Deserialize, Serialize}; @@ -5,22 +6,34 @@ use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::str::FromStr; +/// A [`Regex`] that can be used as a setting. +/// +/// [`Regex`] implements none of `Default`, `PartialEq`, `Serialize`, or +/// `Deserialize`, all of which a required setting needs, and cannot be given +/// them from here as it is a foreign type. This wrapper provides them, along +/// with `Eq` and `Hash` for convenience, and compares by pattern string since +/// a compiled regex has no equality of its own. +/// +/// Defaults to `.`, which matches any single character. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(try_from = "String", into = "String")] pub struct RegexSetting(pub Regex); impl RegexSetting { + /// Compile a regex from a pattern string. pub fn new(value: impl AsRef) -> Result { Ok(Self(Regex::new(value.as_ref())?)) } } +/// Defaults to `.`, which matches any single character. impl Default for RegexSetting { fn default() -> Self { Self(Regex::new(".").unwrap()) } } +/// Derefs to the inner [`Regex`], so its methods can be called directly. impl Deref for RegexSetting { type Target = Regex; @@ -60,6 +73,7 @@ impl Into for RegexSetting { } } +/// Compares the pattern strings, as a compiled [`Regex`] is not comparable. impl PartialEq for RegexSetting { fn eq(&self, other: &RegexSetting) -> bool { self.as_str() == other.as_str() @@ -74,6 +88,7 @@ impl Hash for RegexSetting { } } +#[cfg(feature = "schema")] impl Schematic for RegexSetting { fn build_schema(_: SchemaBuilder) -> Schema { SchemaBuilder::generate::() diff --git a/crates/schematic/src/config/settings/semver.rs b/crates/schematic/src/config/settings/semver.rs index 2576042f..615cd49d 100644 --- a/crates/schematic/src/config/settings/semver.rs +++ b/crates/schematic/src/config/settings/semver.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "schema")] use crate::schema::{Schema, SchemaBuilder, Schematic}; use semver::{Error, Version}; use serde::{Deserialize, Serialize}; @@ -5,22 +6,35 @@ use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::str::FromStr; +/// A [`Version`] that can be used as a setting. +/// +/// [`Version`] does not implement `Default`, which a required setting needs, +/// and cannot be given it from here as it is a foreign type. Its serde support +/// also sits behind a `semver` feature that `schematic` does not enable, so +/// this wrapper (de)serializes from the version string instead. +/// +/// Defaults to `0.0.0`. +/// +/// `VersionReq` needs no equivalent, as it already defaults to `*`. #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(try_from = "String", into = "String")] pub struct VersionSetting(pub Version); impl VersionSetting { + /// Parse a version from a string. pub fn new(value: impl AsRef) -> Result { Ok(Self(Version::parse(value.as_ref())?)) } } +/// Defaults to `0.0.0`. impl Default for VersionSetting { fn default() -> Self { Self(Version::new(0, 0, 0)) } } +/// Derefs to the inner [`Version`], so its methods can be called directly. impl Deref for VersionSetting { type Target = Version; @@ -74,6 +88,7 @@ impl Hash for VersionSetting { } } +#[cfg(feature = "schema")] impl Schematic for VersionSetting { fn build_schema(_: SchemaBuilder) -> Schema { SchemaBuilder::generate::() diff --git a/crates/schematic/src/internal/env.rs b/crates/schematic/src/internal/env.rs index db6f85c7..5a2a9752 100644 --- a/crates/schematic/src/internal/env.rs +++ b/crates/schematic/src/internal/env.rs @@ -72,7 +72,7 @@ impl EnvManager { } }) .map_err(|error| { - HandlerError(format!("Invalid environment variable {key}: {error}")) + HandlerError(format!("Invalid environment variable {key}. {error}")) }); } diff --git a/crates/schematic/src/internal/mod.rs b/crates/schematic/src/internal/mod.rs index e4ab76c8..299f03cb 100644 --- a/crates/schematic/src/internal/mod.rs +++ b/crates/schematic/src/internal/mod.rs @@ -1,8 +1,10 @@ +#[cfg(feature = "env")] mod env; mod merge; #[cfg(feature = "validate")] mod validate; +#[cfg(feature = "env")] pub use env::*; pub use merge::*; #[cfg(feature = "validate")] diff --git a/crates/schematic/src/internal/validate.rs b/crates/schematic/src/internal/validate.rs index 8726f80c..dc277d04 100644 --- a/crates/schematic/src/internal/validate.rs +++ b/crates/schematic/src/internal/validate.rs @@ -1,4 +1,4 @@ -use crate::config::{PartialConfig, Path, ValidateError, Validator}; +use crate::config::{PartialConfig, Path, ValidateError, ValidateResult}; pub struct ValidateManager<'a, Ctx> { context: &'a Ctx, @@ -18,21 +18,39 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { } } - pub fn check(&mut self, key: &str, value: &V, data: &D, validator: Validator) { + pub fn check( + &mut self, + key: &str, + value: V, + data: &D, + validator: impl FnOnce(V, &D, &Ctx, bool) -> ValidateResult, + ) { self.check_at(self.path.join_key(key), value, data, validator); } pub fn check_variant( &mut self, variant: &str, - value: &V, + value: V, data: &D, - validator: Validator, + validator: impl FnOnce(V, &D, &Ctx, bool) -> ValidateResult, ) { self.check_at(self.path.join_variant(variant), value, data, validator); } - fn check_at(&mut self, path: Path, value: &V, data: &D, validator: Validator) { + // The value is taken as-is rather than by reference, because a variant of + // several values passes a tuple of references, not a reference to a tuple. + // + // The validator is an opaque callable rather than a `Validator` box, so + // that the generated closure can deref coerce the value on its way in. + // A `String` setting reaches a `&str` validator, a `Vec` a `&[T]` one. + fn check_at( + &mut self, + path: Path, + value: V, + data: &D, + validator: impl FnOnce(V, &D, &Ctx, bool) -> ValidateResult, + ) { if let Err(error) = validator(value, data, self.context, self.finalizing) { self.errors.push(error.prepend_path(path)); } @@ -76,7 +94,11 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { self.path.join_variant(variant).join_index(index) } - pub fn nested_list<'v, I: IntoIterator, S: PartialConfig + 'v>( + pub fn nested_list< + 'v, + I: IntoIterator>, + S: PartialConfig + 'v, + >( &mut self, key: &str, list: I, @@ -86,7 +108,7 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { pub fn nested_variant_list< 'v, - I: IntoIterator, + I: IntoIterator>, S: PartialConfig + 'v, >( &mut self, @@ -97,12 +119,23 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { self.nested_list_at(self.variant_path(variant, index), list); } - fn nested_list_at<'v, I: IntoIterator, S: PartialConfig + 'v>( + // Items arrive as `Option`s so that a `Vec>` works like a + // `Vec`. A missing item is skipped but still consumes its index, + // otherwise every later item would be reported at the wrong position. + fn nested_list_at< + 'v, + I: IntoIterator>, + S: PartialConfig + 'v, + >( &mut self, path: Path, list: I, ) { for (i, item) in list.into_iter().enumerate() { + let Some(item) = item else { + continue; + }; + if let Err(errors) = item.validate_with_path(self.context, self.finalizing, path.join_index(i)) { @@ -113,7 +146,8 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { pub fn nested_map< 'v, - I: IntoIterator, + I: IntoIterator)>, + K: std::fmt::Display + 'v, S: PartialConfig + 'v, >( &mut self, @@ -125,7 +159,8 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { pub fn nested_variant_map< 'v, - I: IntoIterator, + I: IntoIterator)>, + K: std::fmt::Display + 'v, S: PartialConfig + 'v, >( &mut self, @@ -136,9 +171,12 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { self.nested_map_at(self.variant_path(variant, index), map); } + // A map key is only required to be `Display`, as that is all a path + // segment needs, so keys like `usize` work the same as `String` fn nested_map_at< 'v, - I: IntoIterator, + I: IntoIterator)>, + K: std::fmt::Display + 'v, S: PartialConfig + 'v, >( &mut self, @@ -146,6 +184,10 @@ impl<'a, Ctx> ValidateManager<'a, Ctx> { map: I, ) { for (sub_key, value) in map.into_iter() { + let Some(value) = value else { + continue; + }; + if let Err(errors) = value.validate_with_path(self.context, self.finalizing, path.join_key(sub_key)) { diff --git a/crates/schematic/src/lib.rs b/crates/schematic/src/lib.rs index b3f8cc87..19cb100f 100644 --- a/crates/schematic/src/lib.rs +++ b/crates/schematic/src/lib.rs @@ -32,6 +32,9 @@ pub use starbase_styles::color; #[cfg(feature = "config")] pub use config::*; +// The derives are feature gated in the macro crate: `Config` and +// `ConfigEnum` behind `config`, `Schematic` behind `schema` +#[cfg(any(feature = "config", feature = "schema"))] pub use schematic_macros::*; pub use schematic_types::{Schema, SchemaBuilder, SchemaType, Schematic}; diff --git a/crates/schematic/src/schema/renderer.rs b/crates/schematic/src/schema/renderer.rs index c33cba50..f0069818 100644 --- a/crates/schematic/src/schema/renderer.rs +++ b/crates/schematic/src/schema/renderer.rs @@ -4,7 +4,7 @@ use schematic_types::*; pub type RenderResult = miette::Result; /// Renders [`SchemaType`]s to a distinct format (derived from generic `O`) -/// for use within a [`SchemaGenerator`]. +/// for use within a [`SchemaGenerator`](super::SchemaGenerator). pub trait SchemaRenderer { /// Return true of the provided name is a referenced type. fn is_reference(&self, name: &str) -> bool; @@ -64,7 +64,7 @@ pub trait SchemaRenderer { self.render_schema_without_reference(schema) } - /// Like [`render_schema`] but does not check for references. + /// Like [`Self::render_schema`] but does not check for references. fn render_schema_without_reference(&mut self, schema: &Schema) -> RenderResult { match &schema.ty { SchemaType::Null => self.render_null(schema), diff --git a/crates/schematic/tests/env_test.rs b/crates/schematic/tests/env_test.rs index 37c09756..deec4929 100644 --- a/crates/schematic/tests/env_test.rs +++ b/crates/schematic/tests/env_test.rs @@ -174,7 +174,7 @@ fn loads_env_vars_for_optional_nested_when_valued() { unsafe { env::set_var("ENV_STRING", "foo") }; let result = ConfigLoader::::new() - .code("optNested:\n string: bar", "code.yaml") + .code("opt_nested:\n string: bar", "code.yaml") .unwrap() .load() .unwrap(); diff --git a/crates/schematic/tests/generator_test.rs b/crates/schematic/tests/generator_test.rs index 563b6f83..bb5b0326 100644 --- a/crates/schematic/tests/generator_test.rs +++ b/crates/schematic/tests/generator_test.rs @@ -162,10 +162,10 @@ fn create_template_options() -> TemplateOptions { TemplateOptions { comment_fields: vec!["float32".into(), "map".into()], expand_fields: vec![ - "expandArray".into(), - "expandArrayPrimitive".into(), - "expandObject".into(), - "expandObjectPrimitive".into(), + "expand_array".into(), + "expand_array_primitive".into(), + "expand_object".into(), + "expand_object_primitive".into(), ], hide_fields: vec!["skipped".into(), "one.two.skipped".into()], ..TemplateOptions::default() diff --git a/crates/schematic/tests/macro_enum_test.rs b/crates/schematic/tests/macro_enum_test.rs index 8c6c0756..f80a7e97 100644 --- a/crates/schematic/tests/macro_enum_test.rs +++ b/crates/schematic/tests/macro_enum_test.rs @@ -107,7 +107,7 @@ enum WithComments { } #[derive(Config)] -#[config(serde(untagged, expecting = "something"))] +#[serde(untagged, expecting = "something")] enum Untagged { Unit, OneTuple(bool), @@ -128,7 +128,7 @@ enum ExternalTagged { } #[derive(Config)] -#[config(serde(tag = "type"))] +#[serde(tag = "type")] enum InternalTagged { Foo, Bar(bool), @@ -139,7 +139,7 @@ enum InternalTagged { } #[derive(Config)] -#[config(serde(tag = "type", content = "content"))] +#[serde(tag = "type", content = "content")] enum AdjacentTagged { Foo, Bar(bool), @@ -227,13 +227,13 @@ fn untagged_enum_deserialize_error_shows_all_variants() { // Verify that variant names are listed in the error assert!( - error.contains("unit"), - "Error should mention 'unit' variant, got: {}", + error.contains("Unit"), + "Error should mention 'Unit' variant, got: {}", error ); assert!( - error.contains("one-tuple"), - "Error should mention 'one-tuple' variant, got: {}", + error.contains("OneTuple"), + "Error should mention 'OneTuple' variant, got: {}", error ); assert!( @@ -242,8 +242,8 @@ fn untagged_enum_deserialize_error_shows_all_variants() { error ); assert!( - error.contains("tuple-of-struct"), - "Error should mention 'tuple-of-struct' variant, got: {}", + error.contains("TupleOfStruct"), + "Error should mention 'TupleOfStruct' variant, got: {}", error ); } diff --git a/crates/schematic/tests/macros_test.rs b/crates/schematic/tests/macros_test.rs index b86930b0..c4e4628c 100644 --- a/crates/schematic/tests/macros_test.rs +++ b/crates/schematic/tests/macros_test.rs @@ -251,7 +251,7 @@ enum CustomFormatEnum { Foo, #[serde(rename = "bAr")] Bar, - #[variant(value = "b-a-z")] + #[variant(rename = "b-a-z")] Baz, } diff --git a/crates/schematic/tests/merge_test.rs b/crates/schematic/tests/merge_test.rs index c5778d66..518dee52 100644 --- a/crates/schematic/tests/merge_test.rs +++ b/crates/schematic/tests/merge_test.rs @@ -138,7 +138,7 @@ fn loads_defaults_for_optional_nested() { let result = ConfigLoader::::new() .code( r" -optNested: +opt_nested: string: hij", "code.yaml", ) diff --git a/crates/schematic/tests/partialize_test.rs b/crates/schematic/tests/partialize_test.rs index 9f983260..fbc8b30d 100644 --- a/crates/schematic/tests/partialize_test.rs +++ b/crates/schematic/tests/partialize_test.rs @@ -160,7 +160,7 @@ fn nested_map() { } #[derive(Config)] -#[config(serde(untagged))] +#[serde(untagged)] enum Untagged { Foo, Bar(bool), @@ -189,7 +189,7 @@ fn enum_external() { } #[derive(Config)] -#[config(serde(tag = "type"))] +#[serde(tag = "type")] enum InternalTagged { Foo, Bar(bool), @@ -204,10 +204,8 @@ fn enum_internal() { } #[derive(Config)] -#[config( - serde(tag = "type", content = "content"), - partial(derive(derive_more::TryInto)) -)] +#[serde(tag = "type", content = "content")] +#[config(partial(derive(derive_more::TryInto)))] enum AdjacentTagged { Foo, Bar(bool), diff --git a/crates/schematic/tests/settings_test.rs b/crates/schematic/tests/settings_test.rs index bcab215a..2f18a5d5 100644 --- a/crates/schematic/tests/settings_test.rs +++ b/crates/schematic/tests/settings_test.rs @@ -108,7 +108,7 @@ mod nested { #[serial] fn applies_defaults_for_optional() { let config = ConfigLoader::::new() - .code(r#"{ "nestedOpt": { "req": "xyz" } }"#, "code.json") + .code(r#"{ "nested_opt": { "req": "xyz" } }"#, "code.json") .unwrap() .load() .unwrap() @@ -129,7 +129,7 @@ mod nested { unsafe { env::set_var("OPT_ENV", "env") }; let config = ConfigLoader::::new() - .code(r#"{ "nestedOpt": { "req": "xyz" } }"#, "code.json") + .code(r#"{ "nested_opt": { "req": "xyz" } }"#, "code.json") .unwrap() .load() .unwrap() @@ -145,7 +145,7 @@ mod nested { #[serial] fn validates_all() { let error = ConfigLoader::::new() - .code(r#"{ "nestedOpt": { "req": "xyz" } }"#, "code.json") + .code(r#"{ "nested_opt": { "req": "xyz" } }"#, "code.json") .unwrap() .load_with_context(&Context { fail: true }) .err() @@ -187,8 +187,8 @@ mod nested_vec { .code( r#" { - "nestedReq": [{ "req": "xyz" }], - "nestedOpt": [{ "opt": "hij" }] + "nested_req": [{ "req": "xyz" }], + "nested_opt": [{ "opt": "hij" }] }"#, "code.json", ) @@ -225,8 +225,8 @@ mod nested_vec { .code( r#" { - "nestedReq": [{ "req": "xyz" }], - "nestedOpt": [{ "opt": "hij" }] + "nested_req": [{ "req": "xyz" }], + "nested_opt": [{ "opt": "hij" }] }"#, "code.json", ) @@ -248,8 +248,8 @@ mod nested_vec { .code( r#" { - "nestedReq": [{ "req": "1" }, { "req": "2" }], - "nestedOpt": [{ "opt": "3" }] + "nested_req": [{ "req": "1" }, { "req": "2" }], + "nested_opt": [{ "opt": "3" }] }"#, "code.json", ) @@ -297,8 +297,8 @@ mod nested_map { .code( r#" { - "nestedReq": { "key": { "req": "xyz" } }, - "nestedOpt": { "key": { "opt": "hij" } } + "nested_req": { "key": { "req": "xyz" } }, + "nested_opt": { "key": { "opt": "hij" } } }"#, "code.json", ) @@ -341,8 +341,8 @@ mod nested_map { .code( r#" { - "nestedReq": { "key": { "req": "xyz" } }, - "nestedOpt": { "key": { "opt": "hij" } } + "nested_req": { "key": { "req": "xyz" } }, + "nested_opt": { "key": { "opt": "hij" } } }"#, "code.json", ) @@ -370,8 +370,8 @@ mod nested_map { .code( r#" { - "nestedReq": { "key1": { "req": "xyz" } }, - "nestedOpt": { "key2": { "opt": "hij" }, "key3": { "opt": "abc" } } + "nested_req": { "key1": { "req": "xyz" } }, + "nested_opt": { "key2": { "opt": "hij" }, "key3": { "opt": "abc" } } }"#, "code.json", ) diff --git a/crates/schematic/tests/snapshots/code_sources_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/code_sources_test__generates_json_schema.snap index 3a326b44..8a05a6d2 100644 --- a/crates/schematic/tests/snapshots/code_sources_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/code_sources_test__generates_json_schema.snap @@ -35,4 +35,3 @@ expression: "std::fs::read_to_string(file).unwrap()" }, "additionalProperties": false } - diff --git a/crates/schematic/tests/snapshots/defaults_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/defaults_test__generates_json_schema.snap index d1ce4cd8..6e083ee8 100644 --- a/crates/schematic/tests/snapshots/defaults_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/defaults_test__generates_json_schema.snap @@ -8,15 +8,15 @@ expression: "std::fs::read_to_string(file).unwrap()" "type": "object", "required": [ "nested", - "nestedBoxed", - "nestedMap", - "nestedMapBoxed", - "nestedMapOptBoxed", - "nestedOpt", - "nestedOptBoxed", - "nestedVec", - "nestedVecBoxed", - "nestedVecOptBoxed" + "nested_boxed", + "nested_map", + "nested_map_boxed", + "nested_map_opt_boxed", + "nested_opt", + "nested_opt_boxed", + "nested_vec", + "nested_vec_boxed", + "nested_vec_opt_boxed" ], "properties": { "nested": { @@ -26,7 +26,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "nestedBoxed": { + "nested_boxed": { "type": "object", "required": [ "boolean", @@ -65,7 +65,7 @@ expression: "std::fs::read_to_string(file).unwrap()" }, "additionalProperties": false }, - "nestedMap": { + "nested_map": { "type": "object", "additionalProperties": { "$ref": "#/definitions/NativeDefaults" @@ -74,7 +74,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "type": "string" } }, - "nestedMapBoxed": { + "nested_map_boxed": { "type": "object", "additionalProperties": { "type": "object", @@ -119,7 +119,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "type": "string" } }, - "nestedMapOptBoxed": { + "nested_map_opt_boxed": { "type": "object", "additionalProperties": { "anyOf": [ @@ -171,7 +171,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "type": "string" } }, - "nestedOpt": { + "nested_opt": { "anyOf": [ { "$ref": "#/definitions/NativeDefaults" @@ -181,7 +181,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "nestedOptBoxed": { + "nested_opt_boxed": { "anyOf": [ { "type": "object", @@ -227,13 +227,13 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "nestedVec": { + "nested_vec": { "type": "array", "items": { "$ref": "#/definitions/NativeDefaults" } }, - "nestedVecBoxed": { + "nested_vec_boxed": { "type": "array", "items": { "type": "object", @@ -275,7 +275,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "additionalProperties": false } }, - "nestedVecOptBoxed": { + "nested_vec_opt_boxed": { "type": "array", "items": { "anyOf": [ @@ -428,7 +428,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "optionalWithDefault": { + "optional_with_default": { "default": 123, "anyOf": [ { @@ -439,7 +439,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "optionalWithDefaultFn": { + "optional_with_default_fn": { "anyOf": [ { "type": "number" @@ -452,11 +452,11 @@ expression: "std::fs::read_to_string(file).unwrap()" "required": { "type": "number" }, - "requiredWithDefault": { + "required_with_default": { "default": 123, "type": "number" }, - "requiredWithDefaultFn": { + "required_with_default_fn": { "type": "number" } }, diff --git a/crates/schematic/tests/snapshots/defaults_test__generates_typescript.snap b/crates/schematic/tests/snapshots/defaults_test__generates_typescript.snap index 4420832b..81bbc492 100644 --- a/crates/schematic/tests/snapshots/defaults_test__generates_typescript.snap +++ b/crates/schematic/tests/snapshots/defaults_test__generates_typescript.snap @@ -21,12 +21,12 @@ export interface CustomDefaults { export interface ReqOptDefaults { optional: number | null; /** @default 123 */ - optionalWithDefault?: number | null; - optionalWithDefaultFn?: number | null; + optional_with_default?: number | null; + optional_with_default_fn?: number | null; required: number; /** @default 123 */ - requiredWithDefault?: number; - requiredWithDefaultFn?: number; + required_with_default?: number; + required_with_default_fn?: number; } export interface ContextDefaults { @@ -46,7 +46,7 @@ export interface NativeDefaults { export interface NestedDefaults { nested: NativeDefaults; - nestedBoxed: { + nested_boxed: { boolean: boolean; boxed: string; float32: number; @@ -55,8 +55,8 @@ export interface NestedDefaults { string: string; vector: string[]; }; - nestedMap: Record; - nestedMapBoxed: Record; + nested_map_boxed: Record; - nestedMapOptBoxed: Record; - nestedOpt: NativeDefaults | null; - nestedOptBoxed: { + nested_opt: NativeDefaults | null; + nested_opt_boxed: { boolean: boolean; boxed: string; float32: number; @@ -84,8 +84,8 @@ export interface NestedDefaults { string: string; vector: string[]; } | null; - nestedVec: NativeDefaults[]; - nestedVecBoxed: { + nested_vec: NativeDefaults[]; + nested_vec_boxed: { boolean: boolean; boxed: string; float32: number; @@ -94,7 +94,7 @@ export interface NestedDefaults { string: string; vector: string[]; }[]; - nestedVecOptBoxed: ({ + nested_vec_opt_boxed: ({ boolean: boolean; boxed: string; float32: number; diff --git a/crates/schematic/tests/snapshots/env_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/env_test__generates_json_schema.snap index 464256f2..985556d3 100644 --- a/crates/schematic/tests/snapshots/env_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/env_test__generates_json_schema.snap @@ -66,4 +66,3 @@ expression: "std::fs::read_to_string(file).unwrap()" } } } - diff --git a/crates/schematic/tests/snapshots/env_test__generates_typescript.snap b/crates/schematic/tests/snapshots/env_test__generates_typescript.snap index 1acc3348..11ba6206 100644 --- a/crates/schematic/tests/snapshots/env_test__generates_typescript.snap +++ b/crates/schematic/tests/snapshots/env_test__generates_typescript.snap @@ -12,17 +12,11 @@ export interface EnvVarsNested { } export interface EnvVarsPrefixed { - /** @env ENV_BOOL */ bool: boolean; - /** @env ENV_LIST1 */ list1: string[]; - /** @env ENV_LIST2 */ list2: number[]; nested: EnvVarsNested; - /** @env ENV_NUMBER */ number: number; - /** @env ENV_PATH */ path: string; - /** @env ENV_STRING */ string: string; } diff --git a/crates/schematic/tests/snapshots/extends_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/extends_test__generates_json_schema.snap index 57d5feef..b19e8b98 100644 --- a/crates/schematic/tests/snapshots/extends_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/extends_test__generates_json_schema.snap @@ -113,4 +113,3 @@ expression: "std::fs::read_to_string(file).unwrap()" } } } - diff --git a/crates/schematic/tests/snapshots/extends_test__generates_typescript.snap b/crates/schematic/tests/snapshots/extends_test__generates_typescript.snap index 3ce31af6..16e69b6e 100644 --- a/crates/schematic/tests/snapshots/extends_test__generates_typescript.snap +++ b/crates/schematic/tests/snapshots/extends_test__generates_typescript.snap @@ -1,5 +1,5 @@ --- -source: crates/config/tests/extends_test.rs +source: crates/schematic/tests/extends_test.rs expression: "std::fs::read_to_string(file).unwrap()" --- // Automatically generated by schematic. DO NOT MODIFY! @@ -27,4 +27,3 @@ export interface ExtendsEnum { extends: ExtendsFrom; value: number[]; } - diff --git a/crates/schematic/tests/snapshots/generator_test__json_schema__defaults.snap b/crates/schematic/tests/snapshots/generator_test__json_schema__defaults.snap index 17c6a258..c30e8911 100644 --- a/crates/schematic/tests/snapshots/generator_test__json_schema__defaults.snap +++ b/crates/schematic/tests/snapshots/generator_test__json_schema__defaults.snap @@ -13,28 +13,28 @@ expression: "fs::read_to_string(file).unwrap()" "datetime", "decimal", "enums", - "fallbackEnum", + "fallback_enum", "float32", "float64", "indexmap", "indexset", - "jsonValue", + "json_value", "map", "nested", "number", "path", "regex", - "relPath", + "rel_path", "string", "time", - "tomlValue", + "toml_value", "url", "uuid", "vector", "version", "version2", - "versionReq", - "yamlValue" + "version_req", + "yaml_value" ], "properties": { "boolean": { @@ -61,7 +61,7 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "fallbackEnum": { + "fallback_enum": { "default": "foo", "allOf": [ { @@ -97,7 +97,7 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "jsonValue": { + "json_value": { "type": [ "boolean", "object", @@ -135,7 +135,7 @@ expression: "fs::read_to_string(file).unwrap()" "type": "string", "format": "regex" }, - "relPath": { + "rel_path": { "type": "string", "format": "path" }, @@ -146,7 +146,7 @@ expression: "fs::read_to_string(file).unwrap()" "type": "string", "format": "time" }, - "tomlValue": { + "toml_value": { "anyOf": [ { "type": [ @@ -198,10 +198,10 @@ expression: "fs::read_to_string(file).unwrap()" "version2": { "type": "string" }, - "versionReq": { + "version_req": { "type": "string" }, - "yamlValue": { + "yaml_value": { "type": [ "boolean", "object", diff --git a/crates/schematic/tests/snapshots/generator_test__json_schema__not_required.snap b/crates/schematic/tests/snapshots/generator_test__json_schema__not_required.snap index 0a2de298..ea6799d7 100644 --- a/crates/schematic/tests/snapshots/generator_test__json_schema__not_required.snap +++ b/crates/schematic/tests/snapshots/generator_test__json_schema__not_required.snap @@ -32,7 +32,7 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "fallbackEnum": { + "fallback_enum": { "default": "foo", "allOf": [ { @@ -68,7 +68,7 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "jsonValue": { + "json_value": { "type": [ "boolean", "object", @@ -106,7 +106,7 @@ expression: "fs::read_to_string(file).unwrap()" "type": "string", "format": "regex" }, - "relPath": { + "rel_path": { "type": "string", "format": "path" }, @@ -117,7 +117,7 @@ expression: "fs::read_to_string(file).unwrap()" "type": "string", "format": "time" }, - "tomlValue": { + "toml_value": { "anyOf": [ { "type": [ @@ -169,10 +169,10 @@ expression: "fs::read_to_string(file).unwrap()" "version2": { "type": "string" }, - "versionReq": { + "version_req": { "type": "string" }, - "yamlValue": { + "yaml_value": { "type": [ "boolean", "object", diff --git a/crates/schematic/tests/snapshots/generator_test__json_schema__partials.snap b/crates/schematic/tests/snapshots/generator_test__json_schema__partials.snap index 551ee697..2431503d 100644 --- a/crates/schematic/tests/snapshots/generator_test__json_schema__partials.snap +++ b/crates/schematic/tests/snapshots/generator_test__json_schema__partials.snap @@ -63,7 +63,7 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "fallbackEnum": { + "fallback_enum": { "default": "foo", "anyOf": [ { @@ -123,7 +123,7 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "jsonValue": { + "json_value": { "anyOf": [ { "type": [ @@ -199,7 +199,7 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "relPath": { + "rel_path": { "anyOf": [ { "type": "string", @@ -231,7 +231,7 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "tomlValue": { + "toml_value": { "anyOf": [ { "type": [ @@ -304,7 +304,7 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "versionReq": { + "version_req": { "anyOf": [ { "type": "string" @@ -314,7 +314,7 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "yamlValue": { + "yaml_value": { "anyOf": [ { "type": [ @@ -415,28 +415,28 @@ expression: "fs::read_to_string(file).unwrap()" "datetime", "decimal", "enums", - "fallbackEnum", + "fallback_enum", "float32", "float64", "indexmap", "indexset", - "jsonValue", + "json_value", "map", "nested", "number", "path", "regex", - "relPath", + "rel_path", "string", "time", - "tomlValue", + "toml_value", "url", "uuid", "vector", "version", "version2", - "versionReq", - "yamlValue" + "version_req", + "yaml_value" ], "properties": { "boolean": { @@ -463,7 +463,7 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "fallbackEnum": { + "fallback_enum": { "default": "foo", "allOf": [ { @@ -499,7 +499,7 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "jsonValue": { + "json_value": { "type": [ "boolean", "object", @@ -537,7 +537,7 @@ expression: "fs::read_to_string(file).unwrap()" "type": "string", "format": "regex" }, - "relPath": { + "rel_path": { "type": "string", "format": "path" }, @@ -548,7 +548,7 @@ expression: "fs::read_to_string(file).unwrap()" "type": "string", "format": "time" }, - "tomlValue": { + "toml_value": { "anyOf": [ { "type": [ @@ -600,10 +600,10 @@ expression: "fs::read_to_string(file).unwrap()" "version2": { "type": "string" }, - "versionReq": { + "version_req": { "type": "string" }, - "yamlValue": { + "yaml_value": { "type": [ "boolean", "object", diff --git a/crates/schematic/tests/snapshots/generator_test__json_schema__with_markdown_descs.snap b/crates/schematic/tests/snapshots/generator_test__json_schema__with_markdown_descs.snap index 7fe258d5..8efa625f 100644 --- a/crates/schematic/tests/snapshots/generator_test__json_schema__with_markdown_descs.snap +++ b/crates/schematic/tests/snapshots/generator_test__json_schema__with_markdown_descs.snap @@ -13,28 +13,28 @@ expression: "fs::read_to_string(file).unwrap()" "datetime", "decimal", "enums", - "fallbackEnum", + "fallback_enum", "float32", "float64", "indexmap", "indexset", - "jsonValue", + "json_value", "map", "nested", "number", "path", "regex", - "relPath", + "rel_path", "string", "time", - "tomlValue", + "toml_value", "url", "uuid", "vector", "version", "version2", - "versionReq", - "yamlValue" + "version_req", + "yaml_value" ], "properties": { "boolean": { @@ -62,7 +62,7 @@ expression: "fs::read_to_string(file).unwrap()" ], "markdownDescription": "This is a list of `enumerable` values." }, - "fallbackEnum": { + "fallback_enum": { "default": "foo", "allOf": [ { @@ -98,7 +98,7 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "jsonValue": { + "json_value": { "type": [ "boolean", "object", @@ -137,7 +137,7 @@ expression: "fs::read_to_string(file).unwrap()" "type": "string", "format": "regex" }, - "relPath": { + "rel_path": { "type": "string", "format": "path" }, @@ -148,7 +148,7 @@ expression: "fs::read_to_string(file).unwrap()" "type": "string", "format": "time" }, - "tomlValue": { + "toml_value": { "anyOf": [ { "type": [ @@ -200,10 +200,10 @@ expression: "fs::read_to_string(file).unwrap()" "version2": { "type": "string" }, - "versionReq": { + "version_req": { "type": "string" }, - "yamlValue": { + "yaml_value": { "type": [ "boolean", "object", diff --git a/crates/schematic/tests/snapshots/generator_test__json_schema__with_titles.snap b/crates/schematic/tests/snapshots/generator_test__json_schema__with_titles.snap index 5496511b..d0957b65 100644 --- a/crates/schematic/tests/snapshots/generator_test__json_schema__with_titles.snap +++ b/crates/schematic/tests/snapshots/generator_test__json_schema__with_titles.snap @@ -12,28 +12,28 @@ expression: "fs::read_to_string(file).unwrap()" "datetime", "decimal", "enums", - "fallbackEnum", + "fallback_enum", "float32", "float64", "indexmap", "indexset", - "jsonValue", + "json_value", "map", "nested", "number", "path", "regex", - "relPath", + "rel_path", "string", "time", - "tomlValue", + "toml_value", "url", "uuid", "vector", "version", "version2", - "versionReq", - "yamlValue" + "version_req", + "yaml_value" ], "properties": { "boolean": { @@ -65,8 +65,8 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "fallbackEnum": { - "title": "fallbackEnum", + "fallback_enum": { + "title": "fallback_enum", "default": "foo", "allOf": [ { @@ -106,8 +106,8 @@ expression: "fs::read_to_string(file).unwrap()" } ] }, - "jsonValue": { - "title": "jsonValue", + "json_value": { + "title": "json_value", "type": [ "boolean", "object", @@ -150,8 +150,8 @@ expression: "fs::read_to_string(file).unwrap()" "type": "string", "format": "regex" }, - "relPath": { - "title": "relPath", + "rel_path": { + "title": "rel_path", "type": "string", "format": "path" }, @@ -164,8 +164,8 @@ expression: "fs::read_to_string(file).unwrap()" "type": "string", "format": "time" }, - "tomlValue": { - "title": "tomlValue", + "toml_value": { + "title": "toml_value", "anyOf": [ { "type": [ @@ -222,12 +222,12 @@ expression: "fs::read_to_string(file).unwrap()" "title": "version2", "type": "string" }, - "versionReq": { - "title": "versionReq", + "version_req": { + "title": "version_req", "type": "string" }, - "yamlValue": { - "title": "yamlValue", + "yaml_value": { + "title": "yaml_value", "type": [ "boolean", "object", @@ -297,19 +297,19 @@ expression: "fs::read_to_string(file).unwrap()" "FallbackEnum": { "anyOf": [ { - "title": "Foo", + "title": "foo", "const": "foo" }, { - "title": "Bar", + "title": "bar", "const": "bar" }, { - "title": "Baz", + "title": "baz", "const": "baz" }, { - "title": "Other", + "title": "other", "type": "string" } ] diff --git a/crates/schematic/tests/snapshots/generator_test__template_json__custom_values.snap b/crates/schematic/tests/snapshots/generator_test__template_json__custom_values.snap index fa932d03..a35317bd 100644 --- a/crates/schematic/tests/snapshots/generator_test__template_json__custom_values.snap +++ b/crates/schematic/tests/snapshots/generator_test__template_json__custom_values.snap @@ -7,9 +7,9 @@ expression: "fs::read_to_string(file).unwrap()" // @env TEMPLATE_BOOLEAN "boolean": false, - "emptyArray": [], + "empty_array": [], - "emptyObject": {}, + "empty_object": {}, // This is an enum with a medium length description and deprecated. // @deprecated Dont use enums! @@ -17,7 +17,7 @@ expression: "fs::read_to_string(file).unwrap()" "enums": "foo", // This field is testing array expansion. - "expandArray": [ + "expand_array": [ { // An optional enum. "enums": "foo", @@ -27,12 +27,12 @@ expression: "fs::read_to_string(file).unwrap()" } ], - "expandArrayPrimitive": [ - 123456 + "expand_array_primitive": [ + 0 ], // This field is testing object expansion. - "expandObject": { + "expand_object": { "example": { // An optional enum. "enums": "foo", @@ -42,12 +42,12 @@ expression: "fs::read_to_string(file).unwrap()" } }, - "expandObjectPrimitive": { + "expand_object_primitive": { "example": 0 }, // @values "foo" | "bar" | "baz" - "fallbackEnum": "foo", + "fallback_enum": "foo", // This is a float thats deprecated. // @deprecated @@ -68,8 +68,7 @@ expression: "fs::read_to_string(file).unwrap()" "opt": "custom nested value" }, - // This is a number with a long description. - // This is a number with a long description. + // This is a number with a long description. This is a number with a long description. "number": 0, // This is a nested struct with its own fields. @@ -77,7 +76,6 @@ expression: "fs::read_to_string(file).unwrap()" // This is another nested field. "two": { // An optional string. - // @env ENV_PREFIX_OPT "opt": "", } }, diff --git a/crates/schematic/tests/snapshots/generator_test__template_json__defaults.snap b/crates/schematic/tests/snapshots/generator_test__template_json__defaults.snap index db208051..31c18505 100644 --- a/crates/schematic/tests/snapshots/generator_test__template_json__defaults.snap +++ b/crates/schematic/tests/snapshots/generator_test__template_json__defaults.snap @@ -7,9 +7,9 @@ expression: "fs::read_to_string(file).unwrap()" // @env TEMPLATE_BOOLEAN "boolean": false, - "emptyArray": [], + "empty_array": [], - "emptyObject": {}, + "empty_object": {}, // This is an enum with a medium length description and deprecated. // @deprecated Dont use enums! @@ -17,7 +17,7 @@ expression: "fs::read_to_string(file).unwrap()" "enums": "foo", // This field is testing array expansion. - "expandArray": [ + "expand_array": [ { // An optional enum. "enums": "foo", @@ -27,12 +27,12 @@ expression: "fs::read_to_string(file).unwrap()" } ], - "expandArrayPrimitive": [ + "expand_array_primitive": [ 0 ], // This field is testing object expansion. - "expandObject": { + "expand_object": { "example": { // An optional enum. "enums": "foo", @@ -42,12 +42,12 @@ expression: "fs::read_to_string(file).unwrap()" } }, - "expandObjectPrimitive": { + "expand_object_primitive": { "example": 0 }, // @values "foo" | "bar" | "baz" - "fallbackEnum": "foo", + "fallback_enum": "foo", // This is a float thats deprecated. // @deprecated @@ -68,8 +68,7 @@ expression: "fs::read_to_string(file).unwrap()" "opt": "" }, - // This is a number with a long description. - // This is a number with a long description. + // This is a number with a long description. This is a number with a long description. "number": 0, // This is a nested struct with its own fields. @@ -77,7 +76,6 @@ expression: "fs::read_to_string(file).unwrap()" // This is another nested field. "two": { // An optional string. - // @env ENV_PREFIX_OPT "opt": "", } }, diff --git a/crates/schematic/tests/snapshots/generator_test__template_json__without_comments.snap b/crates/schematic/tests/snapshots/generator_test__template_json__without_comments.snap index 41073c18..f70945d7 100644 --- a/crates/schematic/tests/snapshots/generator_test__template_json__without_comments.snap +++ b/crates/schematic/tests/snapshots/generator_test__template_json__without_comments.snap @@ -4,28 +4,28 @@ expression: "fs::read_to_string(file).unwrap()" --- { "boolean": false, - "emptyArray": [], - "emptyObject": {}, + "empty_array": [], + "empty_object": {}, "enums": "foo", - "expandArray": [ + "expand_array": [ { "enums": "foo", "opt": "" } ], - "expandArrayPrimitive": [ + "expand_array_primitive": [ 0 ], - "expandObject": { + "expand_object": { "example": { "enums": "foo", "opt": "" } }, - "expandObjectPrimitive": { + "expand_object_primitive": { "example": 0 }, - "fallbackEnum": "foo", + "fallback_enum": "foo", "float64": 1.23, "nested": { "enums": "foo", diff --git a/crates/schematic/tests/snapshots/generator_test__template_pkl__defaults.snap b/crates/schematic/tests/snapshots/generator_test__template_pkl__defaults.snap index 9032928c..3a64416c 100644 --- a/crates/schematic/tests/snapshots/generator_test__template_pkl__defaults.snap +++ b/crates/schematic/tests/snapshots/generator_test__template_pkl__defaults.snap @@ -7,9 +7,9 @@ expression: "fs::read_to_string(file).unwrap()" /// @env TEMPLATE_BOOLEAN boolean = false - emptyArray = List() + empty_array = List() - emptyObject = Map() + empty_object = Map() /// This is an enum with a medium length description and deprecated. /// @deprecated Dont use enums! @@ -17,7 +17,7 @@ expression: "fs::read_to_string(file).unwrap()" enums = "foo" /// This field is testing array expansion. - expandArray = new Listing { + expand_array = new Listing { { /// An optional enum. enums = "foo" @@ -27,12 +27,12 @@ expression: "fs::read_to_string(file).unwrap()" } } - expandArrayPrimitive = new Listing { + expand_array_primitive = new Listing { 0 } /// This field is testing object expansion. - expandObject = new Mapping { + expand_object = new Mapping { ["example"] { /// An optional enum. enums = "foo" @@ -42,12 +42,12 @@ expression: "fs::read_to_string(file).unwrap()" } } - expandObjectPrimitive = new Mapping { + expand_object_primitive = new Mapping { ["example"] = 0 } /// @values "foo" | "bar" | "baz" - fallbackEnum = "foo" + fallback_enum = "foo" /// This is a float thats deprecated. /// @deprecated @@ -68,8 +68,7 @@ expression: "fs::read_to_string(file).unwrap()" opt = "" } - /// This is a number with a long description. - /// This is a number with a long description. + /// This is a number with a long description. This is a number with a long description. number = 0 /// This is a nested struct with its own fields. @@ -77,7 +76,6 @@ expression: "fs::read_to_string(file).unwrap()" /// This is another nested field. two { /// An optional string. - /// @env ENV_PREFIX_OPT opt = "" } } diff --git a/crates/schematic/tests/snapshots/generator_test__template_toml__defaults.snap b/crates/schematic/tests/snapshots/generator_test__template_toml__defaults.snap index db0484ec..bce6f08b 100644 --- a/crates/schematic/tests/snapshots/generator_test__template_toml__defaults.snap +++ b/crates/schematic/tests/snapshots/generator_test__template_toml__defaults.snap @@ -6,24 +6,24 @@ expression: "fs::read_to_string(file).unwrap()" # @env TEMPLATE_BOOLEAN boolean = false -emptyArray = [] +empty_array = [] -emptyObject = {} +empty_object = {} # This is an enum with a medium length description and deprecated. # @deprecated Dont use enums! # @values "foo" | "bar" | "baz" enums = "foo" -expandArrayPrimitive = [0] +expand_array_primitive = [0] # This field is testing object expansion. -expandObject = {} +expand_object = {} -expandObjectPrimitive = { example = 0 } +expand_object_primitive = { example = 0 } # @values "foo" | "bar" | "baz" -fallbackEnum = "foo" +fallback_enum = "foo" # This is a float thats deprecated. # @deprecated @@ -35,8 +35,7 @@ float64 = 1.23 # This is a map of numbers. # map = {} -# This is a number with a long description. -# This is a number with a long description. +# This is a number with a long description. This is a number with a long description. number = 0 # This is a string. @@ -46,7 +45,7 @@ string = "abc" vector = [] # This field is testing array expansion. -[[expandArray]] +[[expand_array]] # An optional enum. enums = "foo" @@ -64,5 +63,4 @@ opt = "" # This is another nested field. [one.two] # An optional string. -# @env ENV_PREFIX_OPT opt = "" diff --git a/crates/schematic/tests/snapshots/generator_test__template_yaml__defaults.snap b/crates/schematic/tests/snapshots/generator_test__template_yaml__defaults.snap index bd898259..e5252f6d 100644 --- a/crates/schematic/tests/snapshots/generator_test__template_yaml__defaults.snap +++ b/crates/schematic/tests/snapshots/generator_test__template_yaml__defaults.snap @@ -6,9 +6,9 @@ expression: "fs::read_to_string(file).unwrap()" # @env TEMPLATE_BOOLEAN boolean: false -emptyArray: [] +empty_array: [] -emptyObject: {} +empty_object: {} # This is an enum with a medium length description and deprecated. # @deprecated Dont use enums! @@ -16,17 +16,17 @@ emptyObject: {} enums: "foo" # This field is testing array expansion. -expandArray: +expand_array: - # An optional enum. enums: "foo" # An optional string. opt: "" -expandArrayPrimitive: [0] +expand_array_primitive: [0] # This field is testing object expansion. -expandObject: +expand_object: example: # An optional enum. enums: "foo" @@ -34,10 +34,10 @@ expandObject: # An optional string. opt: "" -expandObjectPrimitive: {} +expand_object_primitive: {} # @values "foo" | "bar" | "baz" -fallbackEnum: "foo" +fallback_enum: "foo" # This is a float thats deprecated. # @deprecated @@ -57,8 +57,7 @@ nested: # An optional string. opt: "" -# This is a number with a long description. -# This is a number with a long description. +# This is a number with a long description. This is a number with a long description. number: 0 # This is a nested struct with its own fields. @@ -66,7 +65,6 @@ one: # This is another nested field. two: # An optional string. - # @env ENV_PREFIX_OPT opt: "" # This is a string. diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__const_enums.snap b/crates/schematic/tests/snapshots/generator_test__typescript__const_enums.snap index dc53d09a..e3f44fe9 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__const_enums.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__const_enums.snap @@ -8,9 +8,9 @@ expression: "generate(TypeScriptOptions\n{\n const_enum: true, enum_format: E /** Docblock comment. */ export const enum BasicEnum { - Foo, - Bar, - Baz, + foo, + bar, + baz, } export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; @@ -46,30 +46,30 @@ export interface GenConfigBase { * @default 'foo' * @type {'foo' | 'bar' | 'baz' | string} */ - fallbackEnum: FallbackEnum; + fallback_enum: FallbackEnum; float32: number; float64: number; indexmap: Record; indexset: string[] | null; - jsonValue: unknown; + json_value: unknown; map: Record; /** **Nested** field. */ nested: AnotherConfig; number: number; path: string; regex: string; - relPath: string; + rel_path: string; string: string; time: string; - tomlValue: unknown | null; + toml_value: unknown | null; url: string | null; uuid: string; /** This is a list of strings. */ vector: string[]; version: string | null; version2: string; - versionReq: string; - yamlValue: unknown; + version_req: string; + yaml_value: unknown; } /** @deprecated */ diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__defaults.snap b/crates/schematic/tests/snapshots/generator_test__typescript__defaults.snap index 871f68fd..dc9b588f 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__defaults.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__defaults.snap @@ -42,30 +42,30 @@ export interface GenConfigBase { * @default 'foo' * @type {'foo' | 'bar' | 'baz' | string} */ - fallbackEnum: FallbackEnum; + fallback_enum: FallbackEnum; float32: number; float64: number; indexmap: Record; indexset: string[] | null; - jsonValue: unknown; + json_value: unknown; map: Record; /** **Nested** field. */ nested: AnotherConfig; number: number; path: string; regex: string; - relPath: string; + rel_path: string; string: string; time: string; - tomlValue: unknown | null; + toml_value: unknown | null; url: string | null; uuid: string; /** This is a list of strings. */ vector: string[]; version: string | null; version2: string; - versionReq: string; - yamlValue: unknown; + version_req: string; + yaml_value: unknown; } /** @deprecated */ diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__enums.snap b/crates/schematic/tests/snapshots/generator_test__typescript__enums.snap index 5cf39bdf..2611d950 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__enums.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__enums.snap @@ -8,9 +8,9 @@ expression: "generate(TypeScriptOptions\n{ enum_format: EnumFormat::Enum, ..Type /** Docblock comment. */ export enum BasicEnum { - Foo, - Bar, - Baz, + foo, + bar, + baz, } export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; @@ -46,30 +46,30 @@ export interface GenConfigBase { * @default 'foo' * @type {'foo' | 'bar' | 'baz' | string} */ - fallbackEnum: FallbackEnum; + fallback_enum: FallbackEnum; float32: number; float64: number; indexmap: Record; indexset: string[] | null; - jsonValue: unknown; + json_value: unknown; map: Record; /** **Nested** field. */ nested: AnotherConfig; number: number; path: string; regex: string; - relPath: string; + rel_path: string; string: string; time: string; - tomlValue: unknown | null; + toml_value: unknown | null; url: string | null; uuid: string; /** This is a list of strings. */ vector: string[]; version: string | null; version2: string; - versionReq: string; - yamlValue: unknown; + version_req: string; + yaml_value: unknown; } /** @deprecated */ diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__exclude_refs.snap b/crates/schematic/tests/snapshots/generator_test__typescript__exclude_refs.snap index 5a61e7a2..a9e0d1e8 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__exclude_refs.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__exclude_refs.snap @@ -39,30 +39,30 @@ export interface GenConfigBase { * @default 'foo' * @type {'foo' | 'bar' | 'baz' | string} */ - fallbackEnum: FallbackEnum; + fallback_enum: FallbackEnum; float32: number; float64: number; indexmap: Record; indexset: string[] | null; - jsonValue: unknown; + json_value: unknown; map: Record; /** **Nested** field. */ nested: AnotherConfig; number: number; path: string; regex: string; - relPath: string; + rel_path: string; string: string; time: string; - tomlValue: unknown | null; + toml_value: unknown | null; url: string | null; uuid: string; /** This is a list of strings. */ vector: string[]; version: string | null; version2: string; - versionReq: string; - yamlValue: unknown; + version_req: string; + yaml_value: unknown; } /** @deprecated */ diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__external_types.snap b/crates/schematic/tests/snapshots/generator_test__typescript__external_types.snap index 838dc4dd..92515b33 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__external_types.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__external_types.snap @@ -44,30 +44,30 @@ export interface GenConfigBase { * @default 'foo' * @type {'foo' | 'bar' | 'baz' | string} */ - fallbackEnum: FallbackEnum; + fallback_enum: FallbackEnum; float32: number; float64: number; indexmap: Record; indexset: string[] | null; - jsonValue: unknown; + json_value: unknown; map: Record; /** **Nested** field. */ nested: AnotherConfig; number: number; path: string; regex: string; - relPath: string; + rel_path: string; string: string; time: string; - tomlValue: unknown | null; + toml_value: unknown | null; url: string | null; uuid: string; /** This is a list of strings. */ vector: string[]; version: string | null; version2: string; - versionReq: string; - yamlValue: unknown; + version_req: string; + yaml_value: unknown; } /** @deprecated */ diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__no_refs.snap b/crates/schematic/tests/snapshots/generator_test__typescript__no_refs.snap index c5ce3f3f..b9d16054 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__no_refs.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__no_refs.snap @@ -42,12 +42,12 @@ export interface GenConfigBase { * @default 'foo' * @type {'foo' | 'bar' | 'baz' | string} */ - fallbackEnum: 'foo' | 'bar' | 'baz' | string; + fallback_enum: 'foo' | 'bar' | 'baz' | string; float32: number; float64: number; indexmap: Record; indexset: string[] | null; - jsonValue: unknown; + json_value: unknown; map: Record; /** **Nested** field. */ nested: { @@ -63,18 +63,18 @@ export interface GenConfigBase { number: number; path: string; regex: string; - relPath: string; + rel_path: string; string: string; time: string; - tomlValue: unknown | null; + toml_value: unknown | null; url: string | null; uuid: string; /** This is a list of strings. */ vector: string[]; version: string | null; version2: string; - versionReq: string; - yamlValue: unknown; + version_req: string; + yaml_value: unknown; } /** @deprecated */ diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__object_aliases.snap b/crates/schematic/tests/snapshots/generator_test__typescript__object_aliases.snap index be62a54d..bcee26d1 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__object_aliases.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__object_aliases.snap @@ -42,30 +42,30 @@ export type GenConfigBase = { * @default 'foo' * @type {'foo' | 'bar' | 'baz' | string} */ - fallbackEnum: FallbackEnum, + fallback_enum: FallbackEnum, float32: number, float64: number, indexmap: Record, indexset: string[] | null, - jsonValue: unknown, + json_value: unknown, map: Record, /** **Nested** field. */ nested: AnotherConfig, number: number, path: string, regex: string, - relPath: string, + rel_path: string, string: string, time: string, - tomlValue: unknown | null, + toml_value: unknown | null, url: string | null, uuid: string, /** This is a list of strings. */ vector: string[], version: string | null, version2: string, - versionReq: string, - yamlValue: unknown, + version_req: string, + yaml_value: unknown, }; /** @deprecated */ diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__partials.snap b/crates/schematic/tests/snapshots/generator_test__typescript__partials.snap index b04f3791..ce541433 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__partials.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__partials.snap @@ -42,30 +42,30 @@ export interface GenConfigBase { * @default 'foo' * @type {'foo' | 'bar' | 'baz' | string} */ - fallbackEnum: FallbackEnum; + fallback_enum: FallbackEnum; float32: number; float64: number; indexmap: Record; indexset: string[] | null; - jsonValue: unknown; + json_value: unknown; map: Record; /** **Nested** field. */ nested: AnotherConfig; number: number; path: string; regex: string; - relPath: string; + rel_path: string; string: string; time: string; - tomlValue: unknown | null; + toml_value: unknown | null; url: string | null; uuid: string; /** This is a list of strings. */ vector: string[]; version: string | null; version2: string; - versionReq: string; - yamlValue: unknown; + version_req: string; + yaml_value: unknown; } /** @deprecated */ @@ -98,30 +98,30 @@ export interface PartialGenConfigBase { */ enums?: BasicEnum | null; /** @default 'foo' */ - fallbackEnum?: FallbackEnum | null; + fallback_enum?: FallbackEnum | null; float32?: number | null; float64?: number | null; indexmap?: Record | null; indexset?: string[] | null; - jsonValue?: unknown | null; + json_value?: unknown | null; map?: Record | null; /** **Nested** field. */ nested?: PartialAnotherConfig | null; number?: number | null; path?: string | null; regex?: string | null; - relPath?: string | null; + rel_path?: string | null; string?: string | null; time?: string | null; - tomlValue?: unknown | null; + toml_value?: unknown | null; url?: string | null; uuid?: string | null; /** This is a list of strings. */ vector?: string[] | null; version?: string | null; version2?: string | null; - versionReq?: string | null; - yamlValue?: unknown | null; + version_req?: string | null; + yaml_value?: unknown | null; } /** @deprecated */ diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__props_optional.snap b/crates/schematic/tests/snapshots/generator_test__typescript__props_optional.snap index 589ae53c..0e954ea4 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__props_optional.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__props_optional.snap @@ -42,30 +42,30 @@ export interface GenConfigBase { * @default 'foo' * @type {'foo' | 'bar' | 'baz' | string} */ - fallbackEnum?: FallbackEnum; + fallback_enum?: FallbackEnum; float32?: number; float64?: number; indexmap?: Record; indexset?: string[] | null; - jsonValue?: unknown; + json_value?: unknown; map?: Record; /** **Nested** field. */ nested?: AnotherConfig; number?: number; path?: string; regex?: string; - relPath?: string; + rel_path?: string; string?: string; time?: string; - tomlValue?: unknown | null; + toml_value?: unknown | null; url?: string | null; uuid?: string; /** This is a list of strings. */ vector?: string[]; version?: string | null; version2?: string; - versionReq?: string; - yamlValue?: unknown; + version_req?: string; + yaml_value?: unknown; } /** @deprecated */ diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__props_optional_undefined.snap b/crates/schematic/tests/snapshots/generator_test__typescript__props_optional_undefined.snap index 3773e68f..0d1c64cb 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__props_optional_undefined.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__props_optional_undefined.snap @@ -42,30 +42,30 @@ export interface GenConfigBase { * @default 'foo' * @type {'foo' | 'bar' | 'baz' | string} */ - fallbackEnum?: FallbackEnum | undefined; + fallback_enum?: FallbackEnum | undefined; float32?: number | undefined; float64?: number | undefined; indexmap?: Record | undefined; indexset?: string[] | null | undefined; - jsonValue?: unknown | undefined; + json_value?: unknown | undefined; map?: Record | undefined; /** **Nested** field. */ nested?: AnotherConfig | undefined; number?: number | undefined; path?: string | undefined; regex?: string | undefined; - relPath?: string | undefined; + rel_path?: string | undefined; string?: string | undefined; time?: string | undefined; - tomlValue?: unknown | null | undefined; + toml_value?: unknown | null | undefined; url?: string | null | undefined; uuid?: string | undefined; /** This is a list of strings. */ vector?: string[] | undefined; version?: string | null | undefined; version2?: string | undefined; - versionReq?: string | undefined; - yamlValue?: unknown | undefined; + version_req?: string | undefined; + yaml_value?: unknown | undefined; } /** @deprecated */ diff --git a/crates/schematic/tests/snapshots/generator_test__typescript__value_enums.snap b/crates/schematic/tests/snapshots/generator_test__typescript__value_enums.snap index 635d569c..7dd2a3be 100644 --- a/crates/schematic/tests/snapshots/generator_test__typescript__value_enums.snap +++ b/crates/schematic/tests/snapshots/generator_test__typescript__value_enums.snap @@ -8,9 +8,9 @@ expression: "generate(TypeScriptOptions\n{ enum_format: EnumFormat::ValuedEnum, /** Docblock comment. */ export enum BasicEnum { - Foo = 'foo', - Bar = 'bar', - Baz = 'baz', + foo = 'foo', + bar = 'bar', + baz = 'baz', } export type FallbackEnum = 'foo' | 'bar' | 'baz' | string; @@ -46,30 +46,30 @@ export interface GenConfigBase { * @default 'foo' * @type {'foo' | 'bar' | 'baz' | string} */ - fallbackEnum: FallbackEnum; + fallback_enum: FallbackEnum; float32: number; float64: number; indexmap: Record; indexset: string[] | null; - jsonValue: unknown; + json_value: unknown; map: Record; /** **Nested** field. */ nested: AnotherConfig; number: number; path: string; regex: string; - relPath: string; + rel_path: string; string: string; time: string; - tomlValue: unknown | null; + toml_value: unknown | null; url: string | null; uuid: string; /** This is a list of strings. */ vector: string[]; version: string | null; version2: string; - versionReq: string; - yamlValue: unknown; + version_req: string; + yaml_value: unknown; } /** @deprecated */ diff --git a/crates/schematic/tests/snapshots/macro_enum_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/macro_enum_test__generates_json_schema.snap index 90d9ba01..92b8eaa8 100644 --- a/crates/schematic/tests/snapshots/macro_enum_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/macro_enum_test__generates_json_schema.snap @@ -9,15 +9,11 @@ expression: "std::fs::read_to_string(file).unwrap()" { "type": "object", "required": [ - "content", "type" ], "properties": { - "content": { - "const": "foo" - }, "type": { - "const": "foo" + "const": "Foo" } }, "additionalProperties": false @@ -33,7 +29,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "type": "boolean" }, "type": { - "const": "bar" + "const": "Bar" } }, "additionalProperties": false @@ -69,7 +65,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ] }, "type": { - "const": "qux" + "const": "Qux" } }, "additionalProperties": false @@ -82,15 +78,11 @@ expression: "std::fs::read_to_string(file).unwrap()" { "type": "object", "required": [ - "content", "type" ], "properties": { - "content": { - "const": "foo" - }, "type": { - "const": "foo" + "const": "Foo" } }, "additionalProperties": false @@ -106,7 +98,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "type": "boolean" }, "type": { - "const": "bar" + "const": "Bar" } }, "additionalProperties": false @@ -142,7 +134,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ] }, "type": { - "const": "qux" + "const": "Qux" } }, "additionalProperties": false @@ -153,9 +145,9 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "AllUnit", "type": "string", "enum": [ - "foo", - "bar", - "baz" + "Foo", + "Bar", + "Baz" ] }, "AllUnnamed": { @@ -164,10 +156,10 @@ expression: "std::fs::read_to_string(file).unwrap()" { "type": "object", "required": [ - "foo" + "Foo" ], "properties": { - "foo": { + "Foo": { "type": "string" } }, @@ -176,10 +168,10 @@ expression: "std::fs::read_to_string(file).unwrap()" { "type": "object", "required": [ - "bar" + "Bar" ], "properties": { - "bar": { + "Bar": { "type": "boolean" } }, @@ -188,10 +180,10 @@ expression: "std::fs::read_to_string(file).unwrap()" { "type": "object", "required": [ - "baz" + "Baz" ], "properties": { - "baz": { + "Baz": { "type": "number" } }, @@ -203,24 +195,15 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "ExternalTagged", "anyOf": [ { - "type": "object", - "required": [ - "foo" - ], - "properties": { - "foo": { - "const": "foo" - } - }, - "additionalProperties": false + "const": "Foo" }, { "type": "object", "required": [ - "bar" + "Bar" ], "properties": { - "bar": { + "Bar": { "type": "boolean" } }, @@ -241,10 +224,10 @@ expression: "std::fs::read_to_string(file).unwrap()" { "type": "object", "required": [ - "qux" + "Qux" ], "properties": { - "qux": { + "Qux": { "allOf": [ { "$ref": "#/definitions/SomeConfig" @@ -260,7 +243,16 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "InternalTagged", "anyOf": [ { - "const": "foo" + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "const": "Foo" + } + }, + "additionalProperties": false }, { "type": "boolean" @@ -279,10 +271,10 @@ expression: "std::fs::read_to_string(file).unwrap()" { "type": "object", "required": [ - "string" + "String" ], "properties": { - "string": { + "String": { "type": "string" } }, @@ -291,10 +283,10 @@ expression: "std::fs::read_to_string(file).unwrap()" { "type": "object", "required": [ - "object" + "Object" ], "properties": { - "object": { + "Object": { "allOf": [ { "$ref": "#/definitions/SomeConfig" @@ -307,10 +299,10 @@ expression: "std::fs::read_to_string(file).unwrap()" { "type": "object", "required": [ - "objects" + "Objects" ], "properties": { - "objects": { + "Objects": { "type": "array", "items": [ { @@ -332,24 +324,15 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "OfBothTypes", "anyOf": [ { - "type": "object", - "required": [ - "foo" - ], - "properties": { - "foo": { - "type": "null" - } - }, - "additionalProperties": false + "type": "null" }, { "type": "object", "required": [ - "bar" + "Bar" ], "properties": { - "bar": { + "Bar": { "type": "array", "items": [ { @@ -371,9 +354,9 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "PartialAllUnit", "type": "string", "enum": [ - "foo", - "bar", - "baz" + "Foo", + "Bar", + "Baz" ] }, "PartialAllUnnamed": { @@ -382,10 +365,10 @@ expression: "std::fs::read_to_string(file).unwrap()" { "type": "object", "required": [ - "foo" + "Foo" ], "properties": { - "foo": { + "Foo": { "type": "string" } }, @@ -394,10 +377,10 @@ expression: "std::fs::read_to_string(file).unwrap()" { "type": "object", "required": [ - "bar" + "Bar" ], "properties": { - "bar": { + "Bar": { "type": "boolean" } }, @@ -406,10 +389,10 @@ expression: "std::fs::read_to_string(file).unwrap()" { "type": "object", "required": [ - "baz" + "Baz" ], "properties": { - "baz": { + "Baz": { "type": "number" } }, @@ -421,24 +404,15 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "PartialExternalTagged", "anyOf": [ { - "type": "object", - "required": [ - "foo" - ], - "properties": { - "foo": { - "const": "foo" - } - }, - "additionalProperties": false + "const": "Foo" }, { "type": "object", "required": [ - "bar" + "Bar" ], "properties": { - "bar": { + "Bar": { "type": "boolean" } }, @@ -458,14 +432,14 @@ expression: "std::fs::read_to_string(file).unwrap()" }, { "type": "object", + "required": [ + "Qux" + ], "properties": { - "qux": { - "anyOf": [ + "Qux": { + "allOf": [ { "$ref": "#/definitions/PartialSomeConfig" - }, - { - "type": "null" } ] } @@ -478,7 +452,16 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "PartialInternalTagged", "anyOf": [ { - "const": "foo" + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "const": "Foo" + } + }, + "additionalProperties": false }, { "type": "boolean" @@ -497,10 +480,10 @@ expression: "std::fs::read_to_string(file).unwrap()" { "type": "object", "required": [ - "string" + "String" ], "properties": { - "string": { + "String": { "type": "string" } }, @@ -508,14 +491,14 @@ expression: "std::fs::read_to_string(file).unwrap()" }, { "type": "object", + "required": [ + "Object" + ], "properties": { - "object": { - "anyOf": [ + "Object": { + "allOf": [ { "$ref": "#/definitions/PartialSomeConfig" - }, - { - "type": "null" } ] } @@ -524,26 +507,22 @@ expression: "std::fs::read_to_string(file).unwrap()" }, { "type": "object", + "required": [ + "Objects" + ], "properties": { - "objects": { - "anyOf": [ + "Objects": { + "type": "array", + "items": [ { - "type": "array", - "items": [ - { - "$ref": "#/definitions/PartialSomeConfig" - }, - { - "$ref": "#/definitions/PartialSomeConfig" - } - ], - "maxItems": 2, - "minItems": 2 + "$ref": "#/definitions/PartialSomeConfig" }, { - "type": "null" + "$ref": "#/definitions/PartialSomeConfig" } - ] + ], + "maxItems": 2, + "minItems": 2 } }, "additionalProperties": false @@ -554,24 +533,15 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "PartialOfBothTypes", "anyOf": [ { - "type": "object", - "required": [ - "foo" - ], - "properties": { - "foo": { - "type": "null" - } - }, - "additionalProperties": false + "type": "null" }, { "type": "object", "required": [ - "bar" + "Bar" ], "properties": { - "bar": { + "Bar": { "type": "array", "items": [ { @@ -648,9 +618,9 @@ expression: "std::fs::read_to_string(file).unwrap()" "description": "Container", "type": "string", "enum": [ - "foo", - "bar", - "baz" + "Foo", + "Bar", + "Baz" ] }, "PartialWithSerde": { @@ -716,9 +686,9 @@ expression: "std::fs::read_to_string(file).unwrap()" "description": "Container", "type": "string", "enum": [ - "foo", - "bar", - "baz" + "Foo", + "Bar", + "Baz" ] }, "WithSerde": { diff --git a/crates/schematic/tests/snapshots/macro_enum_test__generates_typescript.snap b/crates/schematic/tests/snapshots/macro_enum_test__generates_typescript.snap index 2d2f5b7a..89162b48 100644 --- a/crates/schematic/tests/snapshots/macro_enum_test__generates_typescript.snap +++ b/crates/schematic/tests/snapshots/macro_enum_test__generates_typescript.snap @@ -6,20 +6,18 @@ expression: "std::fs::read_to_string(file).unwrap()" /* eslint-disable */ -export type AllUnit = 'foo' | 'bar' | 'baz'; +export type AllUnit = 'Foo' | 'Bar' | 'Baz'; export type AllUnnamed = { - foo: string; + Foo: string; } | { - bar: boolean; + Bar: boolean; } | { - baz: number; + Baz: number; }; -export type OfBothTypes = { - foo: null; -} | { - bar: [boolean, number]; +export type OfBothTypes = null | { + Bar: [boolean, number]; }; export interface SomeConfig { @@ -28,60 +26,57 @@ export interface SomeConfig { } export type NestedConfigs = { - string: string; + String: string; } | { - object: SomeConfig; + Object: SomeConfig; } | { - objects: [SomeConfig, SomeConfig]; + Objects: [SomeConfig, SomeConfig]; }; export type WithSerde = string | boolean | number; /** Container */ -export type WithComments = 'foo' | 'bar' | 'baz'; +export type WithComments = 'Foo' | 'Bar' | 'Baz'; export type Untagged = null | boolean | [number, string] | SomeConfig; -export type ExternalTagged = { - foo: 'foo'; -} | { - bar: boolean; +export type ExternalTagged = 'Foo' | { + Bar: boolean; } | { bazzer: number; } | { - qux: SomeConfig; + Qux: SomeConfig; }; -export type InternalTagged = 'foo' | boolean | number | SomeConfig; +export type InternalTagged = { + type: 'Foo'; +} | boolean | number | SomeConfig; export type AdjacentTagged = { - content: 'foo'; - type: 'foo'; + type: 'Foo'; } | { content: boolean; - type: 'bar'; + type: 'Bar'; } | { content: number; type: 'bazzer'; } | { content: SomeConfig; - type: 'qux'; + type: 'Qux'; }; -export type PartialAllUnit = 'foo' | 'bar' | 'baz'; +export type PartialAllUnit = 'Foo' | 'Bar' | 'Baz'; export type PartialAllUnnamed = { - foo: string; + Foo: string; } | { - bar: boolean; + Bar: boolean; } | { - baz: number; + Baz: number; }; -export type PartialOfBothTypes = { - foo: null; -} | { - bar: [boolean, number]; +export type PartialOfBothTypes = null | { + Bar: [boolean, number]; }; export interface PartialSomeConfig { @@ -90,42 +85,41 @@ export interface PartialSomeConfig { } export type PartialNestedConfigs = { - string: string; + String: string; } | { - object?: PartialSomeConfig | null; + Object: PartialSomeConfig; } | { - objects?: [PartialSomeConfig, PartialSomeConfig] | null; + Objects: [PartialSomeConfig, PartialSomeConfig]; }; export type PartialWithSerde = string | boolean | number; /** Container */ -export type PartialWithComments = 'foo' | 'bar' | 'baz'; +export type PartialWithComments = 'Foo' | 'Bar' | 'Baz'; export type PartialUntagged = null | boolean | [number, string] | PartialSomeConfig; -export type PartialExternalTagged = { - foo: 'foo'; -} | { - bar: boolean; +export type PartialExternalTagged = 'Foo' | { + Bar: boolean; } | { bazzer: number; } | { - qux?: PartialSomeConfig | null; + Qux: PartialSomeConfig; }; -export type PartialInternalTagged = 'foo' | boolean | number | PartialSomeConfig; +export type PartialInternalTagged = { + type: 'Foo'; +} | boolean | number | PartialSomeConfig; export type PartialAdjacentTagged = { - content: 'foo'; - type: 'foo'; + type: 'Foo'; } | { content: boolean; - type: 'bar'; + type: 'Bar'; } | { content: number; type: 'bazzer'; } | { content: PartialSomeConfig; - type: 'qux'; + type: 'Qux'; }; diff --git a/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap index 0fa5d853..1a31717a 100644 --- a/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap @@ -64,30 +64,31 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "Aliased", "type": "string", "enum": [ - "foo", - "bar", - "baz" + "Foo", + "Bar", + "Baz" ] }, "BasicEnum": { "title": "BasicEnum", "type": "string", "enum": [ - "foo", - "bar", - "baz" + "Foo", + "Bar", + "Baz" ] }, "Comments": { "title": "Comments", "description": "Container comment.", + "deprecated": true, "type": "object", "required": [ "block", - "blockLong", + "block_long", "docs", - "docsLong", - "inlineBlock", + "docs_long", + "inline_block", "normal" ], "properties": { @@ -96,7 +97,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "deprecated": true, "type": "boolean" }, - "blockLong": { + "block_long": { "description": "Block with a super long comment that will span multiple lines. Block with a super long comment that will span multiple lines.", "type": "boolean" }, @@ -105,11 +106,11 @@ expression: "std::fs::read_to_string(file).unwrap()" "deprecated": true, "type": "boolean" }, - "docsLong": { + "docs_long": { "description": "Docs with a super long comment that will span multiple lines. It also **contains** some _markdown_ [stuff](.).", "type": "boolean" }, - "inlineBlock": { + "inline_block": { "type": "boolean" }, "normal": { @@ -122,10 +123,10 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "DefaultValues", "type": "object", "required": [ - "arrayOpt", + "array_opt", "enums", - "pathStringBox", - "tupleOpt" + "path_string_box", + "tuple_opt" ], "properties": { "array": { @@ -136,7 +137,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "maxItems": 4, "minItems": 4 }, - "arrayOpt": { + "array_opt": { "anyOf": [ { "type": "array", @@ -155,7 +156,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "default": true, "type": "boolean" }, - "booleanFn": { + "boolean_fn": { "type": "boolean" }, "enums": { @@ -166,7 +167,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "fileString": { + "file_string": { "default": "foo.json", "type": "string" }, @@ -178,7 +179,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "default": 1.64, "type": "number" }, - "longString": { + "long_string": { "default": "foo with. many values!", "type": "string" }, @@ -186,12 +187,12 @@ expression: "std::fs::read_to_string(file).unwrap()" "default": 123, "type": "number" }, - "pathString": { + "path_string": { "default": "foo/bar", "type": "string", "format": "path" }, - "pathStringBox": { + "path_string_box": { "type": "string", "format": "path" }, @@ -199,7 +200,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "default": "foo", "type": "string" }, - "stringFn": { + "string_fn": { "type": "string" }, "tuple": { @@ -221,7 +222,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "maxItems": 4, "minItems": 4 }, - "tupleOpt": { + "tuple_opt": { "anyOf": [ { "type": "array", @@ -413,14 +414,13 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "OtherEnum", "anyOf": [ { - "const": "foo" + "const": "Foo" }, { - "deprecated": true, - "const": "bar" + "const": "Bar" }, { - "const": "baz" + "const": "Baz" }, { "type": "string" @@ -446,7 +446,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "arrayOpt": { + "array_opt": { "anyOf": [ { "type": "array", @@ -472,7 +472,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "booleanFn": { + "boolean_fn": { "anyOf": [ { "type": "boolean" @@ -493,7 +493,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "fileString": { + "file_string": { "default": "foo.json", "anyOf": [ { @@ -526,7 +526,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "longString": { + "long_string": { "default": "foo with. many values!", "anyOf": [ { @@ -548,7 +548,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "pathString": { + "path_string": { "default": "foo/bar", "anyOf": [ { @@ -560,7 +560,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "pathStringBox": { + "path_string_box": { "anyOf": [ { "type": "string", @@ -582,7 +582,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "stringFn": { + "string_fn": { "anyOf": [ { "type": "string" @@ -618,7 +618,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "tupleOpt": { + "tuple_opt": { "anyOf": [ { "type": "array", diff --git a/crates/schematic/tests/snapshots/macros_test__generates_typescript-2.snap b/crates/schematic/tests/snapshots/macros_test__generates_typescript-2.snap index b56984ac..f29b257e 100644 --- a/crates/schematic/tests/snapshots/macros_test__generates_typescript-2.snap +++ b/crates/schematic/tests/snapshots/macros_test__generates_typescript-2.snap @@ -7,25 +7,24 @@ expression: "std::fs::read_to_string(file).unwrap()" /* eslint-disable */ export const enum SomeEnum { - A, - B, - C, + a, + b, + c, } export const enum BasicEnum { Foo, - /** Comment */ Bar, Baz, } export const enum Test { - Foo, - Bar, - Baz, + FOO, + bAr, + b-a-z, } -export type OtherEnum = 'foo' | 'bar' | 'baz' | string; +export type OtherEnum = 'Foo' | 'Bar' | 'Baz' | string; export const enum Aliased { Foo, @@ -58,33 +57,33 @@ export type OptionalValues = { export type DefaultValues = { array?: number[], - arrayOpt: number[] | null, + array_opt: number[] | null, /** @default true */ boolean?: boolean, - booleanFn?: boolean, + boolean_fn?: boolean, /** * @default 'a' * @type {'a' | 'b' | 'c'} */ enums: SomeEnum, /** @default 'foo.json' */ - fileString?: string, + file_string?: string, /** @default 1.32 */ float32?: number, /** @default 1.64 */ float64?: number, /** @default 'foo with. many values!' */ - longString?: string, + long_string?: string, /** @default 123 */ number?: number, /** @default 'foo/bar' */ - pathString?: string, - pathStringBox: string, + path_string?: string, + path_string_box: string, /** @default 'foo' */ string?: string, - stringFn?: string, + string_fn?: string, tuple?: [number, number, number, number], - tupleOpt: [number, number, number, number] | null, + tuple_opt: [number, number, number, number] | null, vector?: number[], }; @@ -138,7 +137,11 @@ export type Validations = { required: string | null, }; -/** Container comment. */ +/** + * Container comment. + * + * @deprecated Invalid + */ export type Comments = { /** * Block @@ -146,23 +149,17 @@ export type Comments = { * @deprecated Bye */ block: boolean, - /** - * Block with a super long comment that will span multiple lines. - * Block with a super long comment that will span multiple lines. - */ - blockLong: boolean, + /** Block with a super long comment that will span multiple lines. Block with a super long comment that will span multiple lines. */ + block_long: boolean, /** * Docs * * @deprecated */ docs: boolean, - /** - * Docs with a super long comment that will span multiple lines. - * It also **contains** some _markdown_ [stuff](.). - */ - docsLong: boolean, - inlineBlock: boolean, + /** Docs with a super long comment that will span multiple lines. It also **contains** some _markdown_ [stuff](.). */ + docs_long: boolean, + inline_block: boolean, normal: boolean, }; @@ -170,30 +167,30 @@ export type UnnamedMultiple = [string, number | null, boolean]; export type PartialDefaultValues = { array?: number[] | null, - arrayOpt?: number[] | null, + array_opt?: number[] | null, /** @default true */ boolean?: boolean | null, - booleanFn?: boolean | null, + boolean_fn?: boolean | null, /** @default 'a' */ enums?: SomeEnum | null, /** @default 'foo.json' */ - fileString?: string | null, + file_string?: string | null, /** @default 1.32 */ float32?: number | null, /** @default 1.64 */ float64?: number | null, /** @default 'foo with. many values!' */ - longString?: string | null, + long_string?: string | null, /** @default 123 */ number?: number | null, /** @default 'foo/bar' */ - pathString?: string | null, - pathStringBox?: string | null, + path_string?: string | null, + path_string_box?: string | null, /** @default 'foo' */ string?: string | null, - stringFn?: string | null, + string_fn?: string | null, tuple?: [number, number, number, number] | null, - tupleOpt?: [number, number, number, number] | null, + tuple_opt?: [number, number, number, number] | null, vector?: number[] | null, }; diff --git a/crates/schematic/tests/snapshots/macros_test__generates_typescript.snap b/crates/schematic/tests/snapshots/macros_test__generates_typescript.snap index a22dc1ff..5b57ed30 100644 --- a/crates/schematic/tests/snapshots/macros_test__generates_typescript.snap +++ b/crates/schematic/tests/snapshots/macros_test__generates_typescript.snap @@ -8,13 +8,13 @@ expression: "std::fs::read_to_string(&file).unwrap()" export type SomeEnum = 'a' | 'b' | 'c'; -export type BasicEnum = 'foo' | 'bar' | 'baz'; +export type BasicEnum = 'Foo' | 'Bar' | 'Baz'; export type Test = 'FOO' | 'bAr' | 'b-a-z'; -export type OtherEnum = 'foo' | 'bar' | 'baz' | string; +export type OtherEnum = 'Foo' | 'Bar' | 'Baz' | string; -export type Aliased = 'foo' | 'bar' | 'baz'; +export type Aliased = 'Foo' | 'Bar' | 'Baz'; export type ValueTypesRest = Record; @@ -41,33 +41,33 @@ export interface OptionalValues { export interface DefaultValues { array?: number[]; - arrayOpt: number[] | null; + array_opt: number[] | null; /** @default true */ boolean?: boolean; - booleanFn?: boolean; + boolean_fn?: boolean; /** * @default 'a' * @type {'a' | 'b' | 'c'} */ enums: SomeEnum; /** @default 'foo.json' */ - fileString?: string; + file_string?: string; /** @default 1.32 */ float32?: number; /** @default 1.64 */ float64?: number; /** @default 'foo with. many values!' */ - longString?: string; + long_string?: string; /** @default 123 */ number?: number; /** @default 'foo/bar' */ - pathString?: string; - pathStringBox: string; + path_string?: string; + path_string_box: string; /** @default 'foo' */ string?: string; - stringFn?: string; + string_fn?: string; tuple?: [number, number, number, number]; - tupleOpt: [number, number, number, number] | null; + tuple_opt: [number, number, number, number] | null; vector?: number[]; } @@ -121,7 +121,11 @@ export interface Validations { required: string | null; } -/** Container comment. */ +/** + * Container comment. + * + * @deprecated Invalid + */ export interface Comments { /** * Block @@ -129,23 +133,17 @@ export interface Comments { * @deprecated Bye */ block: boolean; - /** - * Block with a super long comment that will span multiple lines. - * Block with a super long comment that will span multiple lines. - */ - blockLong: boolean; + /** Block with a super long comment that will span multiple lines. Block with a super long comment that will span multiple lines. */ + block_long: boolean; /** * Docs * * @deprecated */ docs: boolean; - /** - * Docs with a super long comment that will span multiple lines. - * It also **contains** some _markdown_ [stuff](.). - */ - docsLong: boolean; - inlineBlock: boolean; + /** Docs with a super long comment that will span multiple lines. It also **contains** some _markdown_ [stuff](.). */ + docs_long: boolean; + inline_block: boolean; normal: boolean; } @@ -153,30 +151,30 @@ export type UnnamedMultiple = [string, number | null, boolean]; export interface PartialDefaultValues { array?: number[] | null; - arrayOpt?: number[] | null; + array_opt?: number[] | null; /** @default true */ boolean?: boolean | null; - booleanFn?: boolean | null; + boolean_fn?: boolean | null; /** @default 'a' */ enums?: SomeEnum | null; /** @default 'foo.json' */ - fileString?: string | null; + file_string?: string | null; /** @default 1.32 */ float32?: number | null; /** @default 1.64 */ float64?: number | null; /** @default 'foo with. many values!' */ - longString?: string | null; + long_string?: string | null; /** @default 123 */ number?: number | null; /** @default 'foo/bar' */ - pathString?: string | null; - pathStringBox?: string | null; + path_string?: string | null; + path_string_box?: string | null; /** @default 'foo' */ string?: string | null; - stringFn?: string | null; + string_fn?: string | null; tuple?: [number, number, number, number] | null; - tupleOpt?: [number, number, number, number] | null; + tuple_opt?: [number, number, number, number] | null; vector?: number[] | null; } diff --git a/crates/schematic/tests/snapshots/partialize_test__compounds.snap b/crates/schematic/tests/snapshots/partialize_test__compounds.snap index 044b5e24..cef4366f 100644 --- a/crates/schematic/tests/snapshots/partialize_test__compounds.snap +++ b/crates/schematic/tests/snapshots/partialize_test__compounds.snap @@ -102,7 +102,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "listOpt": SchemaField { +⬛️ "list_opt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -279,7 +279,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "mapOpt": SchemaField { +⬛️ "map_opt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { diff --git a/crates/schematic/tests/snapshots/partialize_test__enum_adjacent.snap b/crates/schematic/tests/snapshots/partialize_test__enum_adjacent.snap index ecf07d00..e8938455 100644 --- a/crates/schematic/tests/snapshots/partialize_test__enum_adjacent.snap +++ b/crates/schematic/tests/snapshots/partialize_test__enum_adjacent.snap @@ -35,32 +35,7 @@ expression: "create_diff::()" ⬛️ ty: Literal( ⬛️ LiteralType { ⬛️ value: String( -⬛️ "foo", -⬛️ ), -⬛️ }, -⬛️ ), -⬛️ }, -⬛️ deprecated: None, -⬛️ env_var: None, -⬛️ flatten: false, -⬛️ hidden: false, -⬛️ nullable: false, -⬛️ optional: false, -⬛️ read_only: false, -⬛️ write_only: false, -⬛️ }, -⬛️ "content": SchemaField { -⬛️ aliases: [], -⬛️ comment: None, -⬛️ schema: Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: Literal( -⬛️ LiteralType { -⬛️ value: String( -⬛️ "foo", +⬛️ "Foo", ⬛️ ), ⬛️ }, ⬛️ ), @@ -99,7 +74,7 @@ expression: "create_diff::()" ⬛️ ty: Literal( ⬛️ LiteralType { ⬛️ value: String( -⬛️ "bar", +⬛️ "Bar", ⬛️ ), ⬛️ }, ⬛️ ), @@ -161,7 +136,7 @@ expression: "create_diff::()" ⬛️ ty: Literal( ⬛️ LiteralType { ⬛️ value: String( -⬛️ "baz", +⬛️ "Baz", ⬛️ ), ⬛️ }, ⬛️ ), @@ -231,7 +206,7 @@ expression: "create_diff::()" ⬛️ ty: Literal( ⬛️ LiteralType { ⬛️ value: String( -⬛️ "qux", +⬛️ "Qux", ⬛️ ), ⬛️ }, ⬛️ ), diff --git a/crates/schematic/tests/snapshots/partialize_test__enum_external.snap b/crates/schematic/tests/snapshots/partialize_test__enum_external.snap index 2e9e995c..5964fa6e 100644 --- a/crates/schematic/tests/snapshots/partialize_test__enum_external.snap +++ b/crates/schematic/tests/snapshots/partialize_test__enum_external.snap @@ -21,37 +21,11 @@ expression: "create_diff::()" ⬛️ description: None, ⬛️ name: None, ⬛️ nullable: false, -⬛️ ty: Struct( -⬛️ StructType { -⬛️ fields: { -⬛️ "foo": SchemaField { -⬛️ aliases: [], -⬛️ comment: None, -⬛️ schema: Schema { -⬛️ deprecated: None, -⬛️ description: None, -⬛️ name: None, -⬛️ nullable: false, -⬛️ ty: Literal( -⬛️ LiteralType { -⬛️ value: String( -⬛️ "foo", -⬛️ ), -⬛️ }, -⬛️ ), -⬛️ }, -⬛️ deprecated: None, -⬛️ env_var: None, -⬛️ flatten: false, -⬛️ hidden: false, -⬛️ nullable: false, -⬛️ optional: false, -⬛️ read_only: false, -⬛️ write_only: false, -⬛️ }, -⬛️ }, -⬛️ partial: false, -⬛️ required: None, +⬛️ ty: Literal( +⬛️ LiteralType { +⬛️ value: String( +⬛️ "Foo", +⬛️ ), ⬛️ }, ⬛️ ), ⬛️ }, @@ -63,7 +37,7 @@ expression: "create_diff::()" ⬛️ ty: Struct( ⬛️ StructType { ⬛️ fields: { -⬛️ "bar": SchemaField { +⬛️ "Bar": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -100,7 +74,7 @@ expression: "create_diff::()" ⬛️ ty: Struct( ⬛️ StructType { ⬛️ fields: { -⬛️ "baz": SchemaField { +⬛️ "Baz": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -145,26 +119,27 @@ expression: "create_diff::()" ⬛️ ty: Struct( ⬛️ StructType { ⬛️ fields: { -⬛️ "qux": SchemaField { +⬛️ "Qux": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { ⬛️ deprecated: None, ⬛️ description: None, -🟥 name: Some( +⬛️ name: Some( 🟥 "Basic", -🟥 ), -🟥 nullable: false, -🟥 ty: Struct( -🟥 StructType { -🟥 fields: { -🟥 "field": SchemaField { -🟥 aliases: [], -🟥 comment: None, -🟥 schema: Schema { -🟥 deprecated: None, -🟥 description: None, -🟥 name: None, +🟩 "PartialBasic", +⬛️ ), +⬛️ nullable: false, +⬛️ ty: Struct( +⬛️ StructType { +⬛️ fields: { +⬛️ "field": SchemaField { +⬛️ aliases: [], +⬛️ comment: None, +⬛️ schema: Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, 🟥 nullable: false, 🟥 ty: String( 🟥 StringType { @@ -174,99 +149,54 @@ expression: "create_diff::()" 🟥 max_length: None, 🟥 min_length: None, 🟥 pattern: None, -🟩 name: None, -🟩 nullable: true, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, -🟩 partial: false, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: Some( -🟩 "PartialBasic", -🟩 ), -🟩 nullable: false, -🟩 ty: Struct( -🟩 StructType { -🟩 fields: { -🟩 "field": SchemaField { -🟩 aliases: [], -🟩 comment: None, -🟩 schema: Schema { +🟩 nullable: true, +🟩 ty: Union( +🟩 UnionType { +🟩 default_index: None, +🟩 partial: false, +🟩 operator: AnyOf, +🟩 variants_types: [ +🟩 Schema { 🟩 deprecated: None, 🟩 description: None, 🟩 name: None, -🟩 nullable: true, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, -🟩 partial: false, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: String( -🟩 StringType { -🟩 default: None, -🟩 enum_values: None, -🟩 format: None, -🟩 max_length: None, -🟩 min_length: None, -🟩 pattern: None, -🟩 }, -🟩 ), -🟩 }, -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Null, -🟩 }, -🟩 ], +🟩 nullable: false, +🟩 ty: String( +🟩 StringType { +🟩 default: None, +🟩 enum_values: None, +🟩 format: None, +🟩 max_length: None, +🟩 min_length: None, +🟩 pattern: None, 🟩 }, 🟩 ), 🟩 }, -🟩 deprecated: None, -🟩 env_var: None, -🟩 flatten: false, -🟩 hidden: false, -🟩 nullable: true, -🟩 optional: true, -🟩 read_only: false, -🟩 write_only: false, -🟩 }, +🟩 Schema { +🟩 deprecated: None, +🟩 description: None, +🟩 name: None, +🟩 nullable: false, +🟩 ty: Null, +🟩 }, +🟩 ], ⬛️ }, -🟥 ), -🟥 }, -🟩 partial: true, -🟩 required: None, -🟩 }, -🟩 ), -🟩 }, -🟩 Schema { +⬛️ ), +⬛️ }, ⬛️ deprecated: None, -🟥 env_var: None, -🟥 flatten: false, -🟥 hidden: false, -🟩 description: None, -🟩 name: None, -⬛️ nullable: false, +⬛️ env_var: None, +⬛️ flatten: false, +⬛️ hidden: false, +🟥 nullable: false, 🟥 optional: false, -🟥 read_only: false, -🟥 write_only: false, -🟩 ty: Null, +🟩 nullable: true, +🟩 optional: true, +⬛️ read_only: false, +⬛️ write_only: false, ⬛️ }, -🟥 }, -🟥 partial: true, -🟥 required: None, -🟩 ], +⬛️ }, +⬛️ partial: true, +⬛️ required: None, ⬛️ }, ⬛️ ), ⬛️ }, @@ -274,15 +204,13 @@ expression: "create_diff::()" ⬛️ env_var: None, ⬛️ flatten: false, ⬛️ hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟩 nullable: true, -🟩 optional: true, +⬛️ nullable: false, +⬛️ optional: false, ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, ⬛️ }, -⬛️ partial: true, +⬛️ partial: false, ⬛️ required: None, ⬛️ }, ⬛️ ), diff --git a/crates/schematic/tests/snapshots/partialize_test__enum_internal.snap b/crates/schematic/tests/snapshots/partialize_test__enum_internal.snap index 69e0902d..707721e4 100644 --- a/crates/schematic/tests/snapshots/partialize_test__enum_internal.snap +++ b/crates/schematic/tests/snapshots/partialize_test__enum_internal.snap @@ -21,11 +21,37 @@ expression: "create_diff::()" ⬛️ description: None, ⬛️ name: None, ⬛️ nullable: false, -⬛️ ty: Literal( -⬛️ LiteralType { -⬛️ value: String( -⬛️ "foo", -⬛️ ), +⬛️ ty: Struct( +⬛️ StructType { +⬛️ fields: { +⬛️ "type": SchemaField { +⬛️ aliases: [], +⬛️ comment: None, +⬛️ schema: Schema { +⬛️ deprecated: None, +⬛️ description: None, +⬛️ name: None, +⬛️ nullable: false, +⬛️ ty: Literal( +⬛️ LiteralType { +⬛️ value: String( +⬛️ "Foo", +⬛️ ), +⬛️ }, +⬛️ ), +⬛️ }, +⬛️ deprecated: None, +⬛️ env_var: None, +⬛️ flatten: false, +⬛️ hidden: false, +⬛️ nullable: false, +⬛️ optional: false, +⬛️ read_only: false, +⬛️ write_only: false, +⬛️ }, +⬛️ }, +⬛️ partial: false, +⬛️ required: None, ⬛️ }, ⬛️ ), ⬛️ }, @@ -142,7 +168,7 @@ expression: "create_diff::()" 🟥 ty: Literal( 🟥 LiteralType { 🟥 value: String( -🟥 "qux", +🟥 "Qux", 🟥 ), 🟩 nullable: true, 🟩 ty: Union( @@ -159,7 +185,7 @@ expression: "create_diff::()" 🟩 ty: Literal( 🟩 LiteralType { 🟩 value: String( -🟩 "qux", +🟩 "Qux", 🟩 ), 🟩 }, 🟩 ), diff --git a/crates/schematic/tests/snapshots/partialize_test__enums.snap b/crates/schematic/tests/snapshots/partialize_test__enums.snap index 2908f6a3..7c91c57d 100644 --- a/crates/schematic/tests/snapshots/partialize_test__enums.snap +++ b/crates/schematic/tests/snapshots/partialize_test__enums.snap @@ -41,7 +41,7 @@ expression: "create_diff::()" 🟥 ], 🟥 variants: Some( 🟥 { -🟥 "A": SchemaField { +🟥 "a": SchemaField { 🟥 aliases: [], 🟥 comment: None, 🟥 schema: Schema { @@ -85,7 +85,7 @@ expression: "create_diff::()" 🟥 read_only: false, 🟥 write_only: false, 🟥 }, -🟥 "B": SchemaField { +🟥 "b": SchemaField { 🟥 aliases: [], 🟥 comment: None, 🟥 schema: Schema { @@ -111,7 +111,7 @@ expression: "create_diff::()" 🟩 ], 🟩 variants: Some( 🟩 { -🟩 "A": SchemaField { +🟩 "a": SchemaField { 🟩 aliases: [], 🟩 comment: None, 🟩 schema: Schema { @@ -136,7 +136,7 @@ expression: "create_diff::()" 🟩 read_only: false, 🟩 write_only: false, 🟩 }, -🟩 "B": SchemaField { +🟩 "b": SchemaField { 🟩 aliases: [], 🟩 comment: None, 🟩 schema: Schema { @@ -161,7 +161,7 @@ expression: "create_diff::()" 🟩 read_only: false, 🟩 write_only: false, 🟩 }, -🟩 "C": SchemaField { +🟩 "c": SchemaField { 🟩 aliases: [], 🟩 comment: None, 🟩 schema: Schema { @@ -198,7 +198,7 @@ expression: "create_diff::()" 🟥 read_only: false, 🟥 write_only: false, 🟥 }, -🟥 "C": SchemaField { +🟥 "c": SchemaField { 🟥 aliases: [], 🟥 comment: None, 🟥 schema: Schema { @@ -224,15 +224,15 @@ expression: "create_diff::()" 🟥 write_only: false, 🟥 }, 🟩 ), -🟩 }, +⬛️ }, +🟥 ), 🟩 Schema { 🟩 deprecated: None, 🟩 description: None, 🟩 name: None, 🟩 nullable: false, 🟩 ty: Null, -⬛️ }, -🟥 ), +🟩 }, 🟩 ], ⬛️ }, ⬛️ ), @@ -248,7 +248,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "unitOpt": SchemaField { +⬛️ "unit_opt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -287,7 +287,7 @@ expression: "create_diff::()" ⬛️ ], ⬛️ variants: Some( ⬛️ { -⬛️ "A": SchemaField { +⬛️ "a": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -312,7 +312,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "B": SchemaField { +⬛️ "b": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -337,7 +337,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "C": SchemaField { +⬛️ "c": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -416,7 +416,7 @@ expression: "create_diff::()" 🟥 ], 🟥 variants: Some( 🟥 { -🟥 "A": SchemaField { +🟥 "a": SchemaField { 🟥 aliases: [], 🟥 comment: None, 🟥 schema: Schema { @@ -460,7 +460,7 @@ expression: "create_diff::()" 🟥 read_only: false, 🟥 write_only: false, 🟥 }, -🟥 "B": SchemaField { +🟥 "b": SchemaField { 🟥 aliases: [], 🟥 comment: None, 🟥 schema: Schema { @@ -473,31 +473,6 @@ expression: "create_diff::()" 🟥 value: String( 🟥 "b", 🟥 ), -🟥 }, -🟥 ), -🟥 }, -🟥 deprecated: None, -🟥 env_var: None, -🟥 flatten: false, -🟥 hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟥 read_only: false, -🟥 write_only: false, -🟥 }, -🟥 "C": SchemaField { -🟥 aliases: [], -🟥 comment: None, -🟥 schema: Schema { -🟥 deprecated: None, -🟥 description: None, -🟥 name: None, -🟥 nullable: false, -🟥 ty: Literal( -🟥 LiteralType { -🟥 value: String( -🟥 "c", -🟥 ), 🟩 values: [ 🟩 String( 🟩 "a", @@ -511,7 +486,7 @@ expression: "create_diff::()" 🟩 ], 🟩 variants: Some( 🟩 { -🟩 "A": SchemaField { +🟩 "a": SchemaField { 🟩 aliases: [], 🟩 comment: None, 🟩 schema: Schema { @@ -536,7 +511,7 @@ expression: "create_diff::()" 🟩 read_only: false, 🟩 write_only: false, 🟩 }, -🟩 "B": SchemaField { +🟩 "b": SchemaField { 🟩 aliases: [], 🟩 comment: None, 🟩 schema: Schema { @@ -561,7 +536,7 @@ expression: "create_diff::()" 🟩 read_only: false, 🟩 write_only: false, 🟩 }, -🟩 "C": SchemaField { +🟩 "c": SchemaField { 🟩 aliases: [], 🟩 comment: None, 🟩 schema: Schema { @@ -586,7 +561,7 @@ expression: "create_diff::()" 🟩 read_only: false, 🟩 write_only: false, 🟩 }, -🟩 "Other": SchemaField { +🟩 "other": SchemaField { 🟩 aliases: [], 🟩 comment: None, 🟩 schema: Schema { @@ -626,7 +601,32 @@ expression: "create_diff::()" 🟥 read_only: false, 🟥 write_only: false, 🟥 }, -🟥 "Other": SchemaField { +🟥 "c": SchemaField { +🟥 aliases: [], +🟥 comment: None, +🟥 schema: Schema { +🟥 deprecated: None, +🟥 description: None, +🟥 name: None, +🟥 nullable: false, +🟥 ty: Literal( +🟥 LiteralType { +🟥 value: String( +🟥 "c", +🟥 ), +🟥 }, +🟥 ), +🟥 }, +🟥 deprecated: None, +🟥 env_var: None, +🟥 flatten: false, +🟥 hidden: false, +🟥 nullable: false, +🟥 optional: false, +🟥 read_only: false, +🟥 write_only: false, +🟥 }, +🟥 "other": SchemaField { 🟥 aliases: [], 🟥 comment: None, 🟥 schema: Schema { @@ -679,7 +679,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "fallbackOpt": SchemaField { +⬛️ "fallback_opt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -718,7 +718,7 @@ expression: "create_diff::()" ⬛️ ], ⬛️ variants: Some( ⬛️ { -⬛️ "A": SchemaField { +⬛️ "a": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -743,7 +743,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "B": SchemaField { +⬛️ "b": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -768,7 +768,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "C": SchemaField { +⬛️ "c": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -793,7 +793,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "Other": SchemaField { +⬛️ "other": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -877,7 +877,7 @@ expression: "create_diff::()" 🟥 ty: Struct( 🟥 StructType { 🟥 fields: { -🟥 "a": SchemaField { +🟥 "A": SchemaField { 🟥 aliases: [], 🟥 comment: None, 🟥 schema: Schema { @@ -906,43 +906,21 @@ expression: "create_diff::()" 🟩 ty: Struct( 🟩 StructType { 🟩 fields: { -🟩 "a": SchemaField { +🟩 "A": SchemaField { 🟩 aliases: [], 🟩 comment: None, 🟩 schema: Schema { 🟩 deprecated: None, 🟩 description: None, -🟩 name: None, -🟩 nullable: true, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, -🟩 partial: false, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: Some( -🟩 "PartialEmpty", -🟩 ), -🟩 nullable: false, -🟩 ty: Struct( -🟩 StructType { -🟩 fields: {}, -🟩 partial: true, -🟩 required: None, -🟩 }, -🟩 ), -🟩 }, -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Null, -🟩 }, -🟩 ], +🟩 name: Some( +🟩 "PartialEmpty", +🟩 ), +🟩 nullable: false, +🟩 ty: Struct( +🟩 StructType { +🟩 fields: {}, +🟩 partial: true, +🟩 required: None, 🟩 }, 🟩 ), 🟩 }, @@ -950,15 +928,15 @@ expression: "create_diff::()" 🟩 env_var: None, 🟩 flatten: false, 🟩 hidden: false, -🟩 nullable: true, -🟩 optional: true, +🟩 nullable: false, +🟩 optional: false, 🟩 read_only: false, 🟩 write_only: false, 🟩 }, ⬛️ }, 🟥 ), 🟥 }, -🟩 partial: true, +🟩 partial: false, 🟩 required: None, 🟩 }, 🟩 ), @@ -977,43 +955,21 @@ expression: "create_diff::()" 🟩 ty: Struct( 🟩 StructType { 🟩 fields: { -🟩 "b": SchemaField { +🟩 "B": SchemaField { 🟩 aliases: [], 🟩 comment: None, 🟩 schema: Schema { 🟩 deprecated: None, 🟩 description: None, -🟩 name: None, -🟩 nullable: true, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, -🟩 partial: false, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: Some( -🟩 "PartialEmpty", -🟩 ), -🟩 nullable: false, -🟩 ty: Struct( -🟩 StructType { -🟩 fields: {}, -🟩 partial: true, -🟩 required: None, -🟩 }, -🟩 ), -🟩 }, -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Null, -🟩 }, -🟩 ], +🟩 name: Some( +🟩 "PartialEmpty", +🟩 ), +🟩 nullable: false, +🟩 ty: Struct( +🟩 StructType { +🟩 fields: {}, +🟩 partial: true, +🟩 required: None, 🟩 }, 🟩 ), 🟩 }, @@ -1021,19 +977,19 @@ expression: "create_diff::()" 🟩 env_var: None, 🟩 flatten: false, 🟩 hidden: false, -🟩 nullable: true, -🟩 optional: true, +🟩 nullable: false, +🟩 optional: false, 🟩 read_only: false, 🟩 write_only: false, 🟩 }, 🟩 }, -🟩 partial: true, +🟩 partial: false, 🟩 required: None, 🟩 }, 🟩 ), ⬛️ }, 🟥 }, -🟥 partial: true, +🟥 partial: false, 🟥 required: None, 🟩 ], ⬛️ }, @@ -1047,7 +1003,7 @@ expression: "create_diff::()" 🟥 ty: Struct( 🟥 StructType { 🟥 fields: { -🟥 "b": SchemaField { +🟥 "B": SchemaField { 🟥 aliases: [], 🟥 comment: None, 🟥 schema: Schema { @@ -1075,7 +1031,7 @@ expression: "create_diff::()" 🟥 write_only: false, 🟥 }, 🟥 }, -🟥 partial: true, +🟥 partial: false, 🟥 required: None, 🟥 }, 🟥 ), @@ -1096,7 +1052,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "tupleOpt": SchemaField { +⬛️ "tuple_opt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -1132,52 +1088,22 @@ expression: "create_diff::()" ⬛️ ty: Struct( ⬛️ StructType { ⬛️ fields: { -⬛️ "a": SchemaField { +⬛️ "A": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { ⬛️ deprecated: None, ⬛️ description: None, -🟥 name: Some( +⬛️ name: Some( 🟥 "Empty", -🟥 ), -🟥 nullable: false, -🟥 ty: Struct( -🟥 StructType { -🟥 fields: {}, -🟥 partial: true, -🟥 required: None, -🟩 name: None, -🟩 nullable: true, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, -🟩 partial: false, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: Some( -🟩 "PartialEmpty", -🟩 ), -🟩 nullable: false, -🟩 ty: Struct( -🟩 StructType { -🟩 fields: {}, -🟩 partial: true, -🟩 required: None, -🟩 }, -🟩 ), -🟩 }, -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Null, -🟩 }, -🟩 ], +🟩 "PartialEmpty", +⬛️ ), +⬛️ nullable: false, +⬛️ ty: Struct( +⬛️ StructType { +⬛️ fields: {}, +⬛️ partial: true, +⬛️ required: None, ⬛️ }, ⬛️ ), ⬛️ }, @@ -1185,15 +1111,13 @@ expression: "create_diff::()" ⬛️ env_var: None, ⬛️ flatten: false, ⬛️ hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟩 nullable: true, -🟩 optional: true, +⬛️ nullable: false, +⬛️ optional: false, ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, ⬛️ }, -⬛️ partial: true, +⬛️ partial: false, ⬛️ required: None, ⬛️ }, ⬛️ ), @@ -1206,52 +1130,22 @@ expression: "create_diff::()" ⬛️ ty: Struct( ⬛️ StructType { ⬛️ fields: { -⬛️ "b": SchemaField { +⬛️ "B": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { ⬛️ deprecated: None, ⬛️ description: None, -🟥 name: Some( +⬛️ name: Some( 🟥 "Empty", -🟥 ), -🟥 nullable: false, -🟥 ty: Struct( -🟥 StructType { -🟥 fields: {}, -🟥 partial: true, -🟥 required: None, -🟩 name: None, -🟩 nullable: true, -🟩 ty: Union( -🟩 UnionType { -🟩 default_index: None, -🟩 partial: false, -🟩 operator: AnyOf, -🟩 variants_types: [ -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: Some( -🟩 "PartialEmpty", -🟩 ), -🟩 nullable: false, -🟩 ty: Struct( -🟩 StructType { -🟩 fields: {}, -🟩 partial: true, -🟩 required: None, -🟩 }, -🟩 ), -🟩 }, -🟩 Schema { -🟩 deprecated: None, -🟩 description: None, -🟩 name: None, -🟩 nullable: false, -🟩 ty: Null, -🟩 }, -🟩 ], +🟩 "PartialEmpty", +⬛️ ), +⬛️ nullable: false, +⬛️ ty: Struct( +⬛️ StructType { +⬛️ fields: {}, +⬛️ partial: true, +⬛️ required: None, ⬛️ }, ⬛️ ), ⬛️ }, @@ -1259,15 +1153,13 @@ expression: "create_diff::()" ⬛️ env_var: None, ⬛️ flatten: false, ⬛️ hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟩 nullable: true, -🟩 optional: true, +⬛️ nullable: false, +⬛️ optional: false, ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, ⬛️ }, -⬛️ partial: true, +⬛️ partial: false, ⬛️ required: None, ⬛️ }, ⬛️ ), diff --git a/crates/schematic/tests/snapshots/partialize_test__nested.snap b/crates/schematic/tests/snapshots/partialize_test__nested.snap index 28834667..68b3585c 100644 --- a/crates/schematic/tests/snapshots/partialize_test__nested.snap +++ b/crates/schematic/tests/snapshots/partialize_test__nested.snap @@ -149,7 +149,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "fieldOpt": SchemaField { +⬛️ "field_opt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { diff --git a/crates/schematic/tests/snapshots/partialize_test__nested_list.snap b/crates/schematic/tests/snapshots/partialize_test__nested_list.snap index 9609d301..f86e6dd8 100644 --- a/crates/schematic/tests/snapshots/partialize_test__nested_list.snap +++ b/crates/schematic/tests/snapshots/partialize_test__nested_list.snap @@ -176,7 +176,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "fieldOpt": SchemaField { +⬛️ "field_opt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { diff --git a/crates/schematic/tests/snapshots/partialize_test__nested_map.snap b/crates/schematic/tests/snapshots/partialize_test__nested_map.snap index 14c2c6bc..7860a95f 100644 --- a/crates/schematic/tests/snapshots/partialize_test__nested_map.snap +++ b/crates/schematic/tests/snapshots/partialize_test__nested_map.snap @@ -206,7 +206,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "fieldOpt": SchemaField { +⬛️ "field_opt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { diff --git a/crates/schematic/tests/snapshots/partialize_test__primitives.snap b/crates/schematic/tests/snapshots/partialize_test__primitives.snap index f0277f23..3f7729bd 100644 --- a/crates/schematic/tests/snapshots/partialize_test__primitives.snap +++ b/crates/schematic/tests/snapshots/partialize_test__primitives.snap @@ -259,7 +259,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "stringOpt": SchemaField { +⬛️ "string_opt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -310,7 +310,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "numberOpt": SchemaField { +⬛️ "number_opt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -364,7 +364,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "booleanOpt": SchemaField { +⬛️ "boolean_opt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { @@ -410,7 +410,7 @@ expression: "create_diff::()" ⬛️ read_only: false, ⬛️ write_only: false, ⬛️ }, -⬛️ "floatOpt": SchemaField { +⬛️ "float_opt": SchemaField { ⬛️ aliases: [], ⬛️ comment: None, ⬛️ schema: Schema { diff --git a/crates/schematic/tests/snapshots/settings_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/settings_test__generates_json_schema.snap index 6ff3c8d7..f6d3563d 100644 --- a/crates/schematic/tests/snapshots/settings_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/settings_test__generates_json_schema.snap @@ -7,11 +7,11 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "NestedMapSettings", "type": "object", "required": [ - "nestedOpt", - "nestedReq" + "nested_opt", + "nested_req" ], "properties": { - "nestedOpt": { + "nested_opt": { "anyOf": [ { "type": "object", @@ -27,7 +27,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "nestedReq": { + "nested_req": { "type": "object", "additionalProperties": { "$ref": "#/definitions/StandardSettings" @@ -43,11 +43,11 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "NestedSettings", "type": "object", "required": [ - "nestedOpt", - "nestedReq" + "nested_opt", + "nested_req" ], "properties": { - "nestedOpt": { + "nested_opt": { "anyOf": [ { "$ref": "#/definitions/StandardSettings" @@ -57,7 +57,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "nestedReq": { + "nested_req": { "allOf": [ { "$ref": "#/definitions/StandardSettings" @@ -71,11 +71,11 @@ expression: "std::fs::read_to_string(file).unwrap()" "title": "NestedVecSettings", "type": "object", "required": [ - "nestedOpt", - "nestedReq" + "nested_opt", + "nested_req" ], "properties": { - "nestedOpt": { + "nested_opt": { "anyOf": [ { "type": "array", @@ -88,7 +88,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } ] }, - "nestedReq": { + "nested_req": { "type": "array", "items": { "$ref": "#/definitions/StandardSettings" @@ -118,7 +118,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "req": { "type": "string" }, - "reqDefault": { + "req_default": { "default": "abc", "type": "string" } diff --git a/crates/schematic/tests/snapshots/settings_test__generates_typescript.snap b/crates/schematic/tests/snapshots/settings_test__generates_typescript.snap index 7866a0b3..c0b9b940 100644 --- a/crates/schematic/tests/snapshots/settings_test__generates_typescript.snap +++ b/crates/schematic/tests/snapshots/settings_test__generates_typescript.snap @@ -11,20 +11,20 @@ export interface StandardSettings { opt: string | null; req: string; /** @default 'abc' */ - reqDefault?: string; + req_default?: string; } export interface NestedSettings { - nestedOpt: StandardSettings | null; - nestedReq: StandardSettings; + nested_opt: StandardSettings | null; + nested_req: StandardSettings; } export interface NestedVecSettings { - nestedOpt: StandardSettings[] | null; - nestedReq: StandardSettings[]; + nested_opt: StandardSettings[] | null; + nested_req: StandardSettings[]; } export interface NestedMapSettings { - nestedOpt: Record | null; - nestedReq: Record; + nested_opt: Record | null; + nested_req: Record; } diff --git a/crates/schematic/tests/snapshots/variants_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/variants_test__generates_json_schema.snap index 07f00240..17f3340e 100644 --- a/crates/schematic/tests/snapshots/variants_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/variants_test__generates_json_schema.snap @@ -1,5 +1,5 @@ --- -source: crates/config/tests/variants_test.rs +source: crates/schematic/tests/variants_test.rs expression: "std::fs::read_to_string(file).unwrap()" --- { @@ -71,4 +71,3 @@ expression: "std::fs::read_to_string(file).unwrap()" } } } - diff --git a/crates/schematic/tests/snapshots/variants_test__generates_typescript.snap b/crates/schematic/tests/snapshots/variants_test__generates_typescript.snap index be7410ce..8223e443 100644 --- a/crates/schematic/tests/snapshots/variants_test__generates_typescript.snap +++ b/crates/schematic/tests/snapshots/variants_test__generates_typescript.snap @@ -1,5 +1,5 @@ --- -source: crates/config/tests/variants_test.rs +source: crates/schematic/tests/variants_test.rs expression: "std::fs::read_to_string(file).unwrap()" --- // Automatically generated by schematic. DO NOT MODIFY! @@ -16,4 +16,3 @@ export type Projects = ProjectsConfig | string[] | Record; export interface StandardSettings { projects: Projects; } - diff --git a/crates/schematic/tests/validate_test.rs b/crates/schematic/tests/validate_test.rs index dc8bd6ec..0358a758 100644 --- a/crates/schematic/tests/validate_test.rs +++ b/crates/schematic/tests/validate_test.rs @@ -198,14 +198,14 @@ fn doesnt_error_if_required_and_notempty() { assert!(result.is_ok()); let result = ConfigLoader::::new() - .code(r#"{ "required": "abc" }"#, "code.json") + .code(r#"{ "Required": "abc" }"#, "code.json") .unwrap() .load(); assert!(result.is_ok()); let result = ConfigLoader::::new() - .code(r#"{ "optional": null }"#, "code.json") + .code(r#"{ "Optional": null }"#, "code.json") .unwrap() .load(); @@ -227,7 +227,7 @@ fn errors_if_required_and_empty() { ); let error = ConfigLoader::::new() - .code(r#"{ "required": null }"#, "code.json") + .code(r#"{ "Required": null }"#, "code.json") .unwrap() .load() .err() @@ -235,6 +235,6 @@ fn errors_if_required_and_empty() { assert_eq!( error.to_full_string(), - "Failed to validate ValidateEnumRequired. \n required: this setting is required" + "Failed to validate ValidateEnumRequired. \n Required: this setting is required" ); } diff --git a/crates/schematic/tests/variants_test.rs b/crates/schematic/tests/variants_test.rs index 3c677e98..c74ba3f3 100644 --- a/crates/schematic/tests/variants_test.rs +++ b/crates/schematic/tests/variants_test.rs @@ -52,7 +52,8 @@ pub struct ProjectsConfig { } #[derive(Debug, Config, Eq, PartialEq)] -#[config(context = Context, serde(untagged))] +#[config(context = Context)] +#[serde(untagged)] pub enum Projects { #[setting(nested, validate = test_cfg)] Config(ProjectsConfig), diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 67586ade..bf9f7f43 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] profile = "default" -channel = "1.97.0" +channel = "1.98.0" From b7df3f899d984b29efb3a7aa3e8a4f5a035306b0 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Thu, 20 Aug 2026 14:52:37 -0700 Subject: [PATCH 16/18] breaking: Replace `reqwest` with `ureq`. (#200) --- CHANGELOG.md | 8 + Cargo.lock | 543 +++------------------ Cargo.toml | 2 +- book/src/config/index.md | 2 +- crates/schematic/Cargo.toml | 7 +- crates/schematic/src/config/error.rs | 2 +- crates/schematic/src/config/loader.rs | 12 +- crates/schematic/tests/url_sources_test.rs | 31 ++ 8 files changed, 133 insertions(+), 474 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93dcabc9..aad8ef99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ while the schema types have been updated to be more flexible and composable. 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. +- Replaced `reqwest` with `ureq` for the `url` feature. `ureq` is blocking but builds no runtime of + its own, so loading a URL from inside an async runtime now blocks the thread instead of panicking. + `ConfigError::ReadUrlFailed` carries a `ureq::Error` instead of a `reqwest::Error`. +- Changed a non-2xx response to a URL source into a `ConfigError::ReadUrlFailed`. It previously + passed the response body to the parser, so a 404 page surfaced as a parse error. - Changed doc comments to render as a single flowing paragraph instead of one line per source line. Markdown list items are still kept on their own line. - Changed `#[setting(env_prefix)]` keys to no longer appear as `@env` annotations in generated @@ -139,6 +144,9 @@ while the schema types have been updated to be more flexible and composable. - Fixed a block doc comment (`/** ... */`) keeping its leading `*` continuation markers, which rendered them as stray markdown list items. - Fixed the `config` feature failing to compile without `env`. +- Fixed the `url` feature being unable to request any HTTPS URL. `reqwest` was declared without + a TLS backend, so it only worked in this repository, where a dev-dependency happened to enable + one. `ureq` enables rustls by default. - Fixed the `type_regex` and `type_semver` features failing to compile alongside `config` without `schema`. Their `Schematic` implementations are now gated, so the setting types themselves no longer require the schema layer. diff --git a/Cargo.lock b/Cargo.lock index d7e0d523..35a4b2ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,41 +87,12 @@ dependencies = [ "tempfile", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "aws-lc-rs" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", - "pkg-config", -] - [[package]] name = "backtrace" version = "0.3.76" @@ -148,9 +119,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.22.1" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "bitflags" @@ -258,8 +229,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -275,17 +244,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" -[[package]] -name = "chacha20" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" -dependencies = [ - "cfg-if", - "cpufeatures", - "rand_core 0.10.1", -] - [[package]] name = "chrono" version = "0.4.45" @@ -300,15 +258,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "combine" version = "4.6.7" @@ -370,12 +319,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "cpufeatures" -version = "0.3.0" +name = "crc32fast" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "libc", + "cfg-if", ] [[package]] @@ -548,6 +497,16 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "float-cmp" version = "0.10.0" @@ -566,28 +525,12 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "funty" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -[[package]] -name = "futures-channel" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" -dependencies = [ - "futures-core", - "futures-sink", -] - [[package]] name = "futures-core" version = "0.3.34" @@ -605,18 +548,6 @@ dependencies = [ "futures-util", ] -[[package]] -name = "futures-io" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" - -[[package]] -name = "futures-sink" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" - [[package]] name = "futures-task" version = "0.3.34" @@ -630,10 +561,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", - "futures-io", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "slab", ] @@ -672,10 +600,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -685,11 +611,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi", - "rand_core 0.10.1", - "wasm-bindgen", ] [[package]] @@ -747,93 +670,12 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "hyper" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - [[package]] name = "iana-time-zone" version = "0.1.65" @@ -1008,12 +850,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "ipnet" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" - [[package]] name = "is_ci" version = "1.2.0" @@ -1075,16 +911,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "jobserver" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" -dependencies = [ - "getrandom 0.4.3", - "libc", -] - [[package]] name = "js-sys" version = "0.3.104" @@ -1147,12 +973,6 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "markdown" version = "1.0.0" @@ -1205,17 +1025,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", -] - -[[package]] -name = "mio" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", + "simd-adler32", ] [[package]] @@ -1301,12 +1111,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pkg-config" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" - [[package]] name = "potential_utf" version = "0.1.6" @@ -1413,63 +1217,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "quinn" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.4.3", - "lru-slab", - "rand 0.10.2", - "rand_pcg", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.61.2", -] - [[package]] name = "quote" version = "1.0.47" @@ -1499,18 +1246,7 @@ checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core 0.10.1", + "rand_core", ] [[package]] @@ -1520,7 +1256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -1532,21 +1268,6 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "rand_pcg" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" -dependencies = [ - "rand_core 0.10.1", -] - [[package]] name = "redox_syscall" version = "0.5.18" @@ -1626,43 +1347,6 @@ dependencies = [ "bytecheck", ] -[[package]] -name = "reqwest" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "ring" version = "0.17.14" @@ -1770,7 +1454,7 @@ dependencies = [ "borsh", "bytes", "num-traits", - "rand 0.8.7", + "rand", "rkyv", "serde", "serde_json", @@ -1783,12 +1467,6 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - [[package]] name = "rustc_version" version = "0.4.1" @@ -1817,8 +1495,9 @@ version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ - "aws-lc-rs", + "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", @@ -1843,7 +1522,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ - "web-time", "zeroize", ] @@ -1865,7 +1543,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1880,7 +1558,6 @@ version = "0.103.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1941,7 +1618,6 @@ dependencies = [ "miette", "regex", "relative-path", - "reqwest", "ron", "rpkl", "rust_decimal", @@ -1961,8 +1637,10 @@ dependencies = [ "starbase_styles", "strip-ansi-escapes", "thiserror", + "tokio", "toml", "tracing", + "ureq", "url", "uuid", ] @@ -2194,6 +1872,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "simd_cesu8" version = "1.2.0" @@ -2237,16 +1921,6 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" -[[package]] -name = "socket2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2372,15 +2046,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -2502,22 +2167,19 @@ version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ - "bytes", - "libc", - "mio", "pin-project-lite", - "socket2", - "windows-sys 0.61.2", + "tokio-macros", ] [[package]] -name = "tokio-rustls" -version = "0.26.4" +name = "tokio-macros" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ - "rustls", - "tokio", + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] @@ -2571,51 +2233,6 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - [[package]] name = "tracing" version = "0.1.44" @@ -2647,12 +2264,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - [[package]] name = "typeid" version = "1.0.3" @@ -2713,6 +2324,36 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -2726,6 +2367,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -2777,15 +2424,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2806,16 +2444,6 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.127" @@ -2849,30 +2477,19 @@ dependencies = [ ] [[package]] -name = "web-sys" -version = "0.3.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" +name = "webpki-root-certs" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ - "js-sys", - "wasm-bindgen", + "rustls-pki-types", ] [[package]] -name = "webpki-root-certs" +name = "webpki-roots" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" dependencies = [ "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index cbc86f9d..f1130200 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ proc-macro2 = "1.0.107" quote = "1.0.47" regex = "1.13.1" relative-path = "2.0.1" -reqwest = { version = "0.13.4", default-features = false } ron = "0.12.2" rpkl = "0.8.0" rust_decimal = "1.42.1" @@ -25,5 +24,6 @@ starbase_sandbox = "0.12.0" syn = "3.0.3" toml = "1.1.4" tracing = "0.1.44" +ureq = { version = "3.4.0", features = ["rustls", "platform-verifier"] } url = "2.5.8" uuid = "1.24.1" diff --git a/book/src/config/index.md b/book/src/config/index.md index cf11d488..c3d8fdc2 100644 --- a/book/src/config/index.md +++ b/book/src/config/index.md @@ -77,7 +77,7 @@ or [`ConfigLoader::file_optional()`](https://docs.rs/schematic/latest/schematic/struct.ConfigLoader.html#method.file_optional) methods. For URLs, use the [`ConfigLoader::url()`](https://docs.rs/schematic/latest/schematic/struct.ConfigLoader.html#method.url) -method (requires the `url` Cargo feature, which is on by default). For inline code, use the +method (requires the `url` Cargo feature, which is _not_ enabled by default). For inline code, use the [`ConfigLoader::code()`](https://docs.rs/schematic/latest/schematic/struct.ConfigLoader.html#method.code) method, which requires an explicit format. diff --git a/crates/schematic/Cargo.toml b/crates/schematic/Cargo.toml index 6bcaa7e3..2c0ef932 100644 --- a/crates/schematic/Cargo.toml +++ b/crates/schematic/Cargo.toml @@ -64,7 +64,7 @@ toml = { workspace = true, optional = true } serde_norway = { workspace = true, optional = true } # url -reqwest = { workspace = true, optional = true, features = ["blocking"] } +ureq = { workspace = true, optional = true } [features] default = ["config", "env", "extends", "validate"] @@ -80,7 +80,7 @@ schema_serde = ["schema", "schematic_types/serde"] # Features env = ["schematic_macros/env"] extends = ["schematic_macros/extends"] -url = ["dep:reqwest"] +url = ["dep:ureq"] validate = ["dep:garde", "schematic_macros/validate"] # Formats @@ -141,8 +141,9 @@ schematic = { path = ".", features = [ "validate_url", "yaml", ] } -reqwest = { workspace = true, features = ["blocking", "rustls"] } serial_test = "4.0.1" +# Only to prove loading works from inside an async runtime +tokio = { version = "1.53.1", features = ["macros", "rt"] } similar = "3.1.2" starbase_sandbox = { workspace = true } strip-ansi-escapes = "0.2.1" diff --git a/crates/schematic/src/config/error.rs b/crates/schematic/src/config/error.rs index 272364af..cf8ce842 100644 --- a/crates/schematic/src/config/error.rs +++ b/crates/schematic/src/config/error.rs @@ -82,7 +82,7 @@ pub enum ConfigError { ReadUrlFailed { url: String, #[source] - error: Box, + error: Box, }, #[cfg(feature = "json")] diff --git a/crates/schematic/src/config/loader.rs b/crates/schematic/src/config/loader.rs index fbad4034..441a9555 100644 --- a/crates/schematic/src/config/loader.rs +++ b/crates/schematic/src/config/loader.rs @@ -342,7 +342,7 @@ impl ConfigLoader { let mut cacher = self.cacher.lock().unwrap(); - let handle_reqwest_error = |error: reqwest::Error| ConfigError::ReadUrlFailed { + let handle_http_error = |error: ureq::Error| ConfigError::ReadUrlFailed { url: url.to_owned(), error: Box::new(error), }; @@ -350,10 +350,12 @@ impl ConfigLoader { let content = if let Some(cache) = cacher.read(url)? { cache } else { - let body = reqwest::blocking::get(url) - .map_err(handle_reqwest_error)? - .text() - .map_err(handle_reqwest_error)?; + let body = ureq::get(url) + .call() + .map_err(handle_http_error)? + .body_mut() + .read_to_string() + .map_err(handle_http_error)?; cacher.write(url, &body)?; diff --git a/crates/schematic/tests/url_sources_test.rs b/crates/schematic/tests/url_sources_test.rs index b7aec981..0560798d 100644 --- a/crates/schematic/tests/url_sources_test.rs +++ b/crates/schematic/tests/url_sources_test.rs @@ -49,6 +49,37 @@ fn errors_on_www() { .unwrap(); } +// A non-2xx response is an error, rather than an error page handed to the +// parser as if it were config +#[test] +fn errors_on_a_missing_url() { + let error = ConfigLoader::::new() + .url(get_url("yaml/does-not-exist.yml")) + .unwrap() + .load() + .err() + .unwrap(); + + assert!( + matches!(error, ConfigError::ReadUrlFailed { .. }), + "expected a read failure, got {error:?}" + ); +} + +// `ureq` is blocking but never builds a runtime of its own, so loading from +// inside one blocks the thread instead of panicking the way a client with a +// private runtime would +#[tokio::test] +async fn loads_from_within_an_async_runtime() { + let result = ConfigLoader::::new() + .url(get_url("yaml/one.yml")) + .unwrap() + .load() + .unwrap(); + + assert_eq!(result.config.string, "foo"); +} + #[cfg(feature = "json")] #[test] fn loads_json_files() { From 71234e253f5e77af55ebb7210ebf84d97fa183f0 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Thu, 20 Aug 2026 20:48:12 -0700 Subject: [PATCH 17/18] internal: Upgrade `schemars`. (#201) --- CHANGELOG.md | 11 + Cargo.lock | 41 +- book/src/schema/generator/json-schema.md | 10 +- crates/schematic/Cargo.toml | 4 +- .../src/schema/renderers/json_schema.rs | 654 +++++++++++------- .../defaults_test__generates_json_schema.snap | 6 +- .../env_test__generates_json_schema.snap | 6 +- .../extends_test__generates_json_schema.snap | 6 +- ...acro_enum_test__generates_json_schema.snap | 36 +- .../macros_test__generates_json_schema.snap | 12 +- .../snapshots/partialize_test__enums.snap | 156 ++--- .../settings_test__generates_json_schema.snap | 6 +- .../variants_test__generates_json_schema.snap | 6 +- 13 files changed, 534 insertions(+), 420 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aad8ef99..7f27cc68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,17 @@ while the schema types have been updated to be more flexible and composable. ##### Schema +- Updated `schemars` to v1, which replaces its typed schema model with plain JSON. This reshapes + `JsonSchemaOptions`: + - `visitors: Vec>` is now `transforms: Vec>`. + - `option_nullable` and `option_add_null_type` were removed. Schemars no longer has them, and this + renderer never read them. + - `definitions_path` keeps its meaning as the `$ref` prefix, and is no longer inherited from + schemars, whose field of that name is now a JSON pointer. + - `JsonSchemaRenderer` implements `SchemaRenderer` rather than + `SchemaRenderer`. +- Changed a `$ref` with no sibling keys to render on its own, instead of inside a single-element + `allOf`. The two are equivalent, and a `$ref` that does have siblings is still wrapped. - Changed `SchemaType::Reference` from a newtype into a struct variant. Serde is unable to internally tag a newtype whose value isn't a map, so any schema containing a reference (which is how cycles are represented) previously failed to serialize. diff --git a/Cargo.lock b/Cargo.lock index 35a4b2ca..152f4ac3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,6 +1288,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "reflink-copy" version = "0.1.30" @@ -1543,7 +1563,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1595,11 +1615,12 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.22" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "dyn-clone", + "ref-cast", "serde", "serde_json", ] @@ -1632,7 +1653,7 @@ dependencies = [ "serde_norway", "serde_path_to_error", "serial_test", - "similar 3.1.2", + "similar 3.2.0", "starbase_sandbox", "starbase_styles", "strip-ansi-escapes", @@ -1902,9 +1923,9 @@ checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" [[package]] name = "similar" -version = "3.1.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ee016af5d736b69fc89e19254540fa4b5f5492853fb5503920f084011c78b6" +checksum = "4f66ca1f7aca2474dc10c942eb22feffc897735f54cd1db90138c2fddb490987" dependencies = [ "bstr", ] @@ -2808,9 +2829,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b5c6b5976d66c1d703c4fd17d3f5e43c8cedaacf604961b171adc7130896d8" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -2819,9 +2840,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62" +checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9" dependencies = [ "proc-macro2", "quote", diff --git a/book/src/schema/generator/json-schema.md b/book/src/schema/generator/json-schema.md index 7690a538..d086bd9d 100644 --- a/book/src/schema/generator/json-schema.md +++ b/book/src/schema/generator/json-schema.md @@ -69,9 +69,13 @@ JsonSchemaRenderer::new(JsonSchemaOptions { }); ``` -> This type also contains all fields from the upstream -> [`SchemaSettings`](https://docs.rs/schemars/latest/schemars/gen/struct.SchemaSettings.html) type -> from the `schemars` crate. Refer to their documentation for more information. +> This type also carries some fields from the upstream +> [`SchemaSettings`](https://docs.rs/schemars/latest/schemars/generate/struct.SchemaSettings.html) +> type in the `schemars` crate — `meta_schema`, `transforms`, and `inline_subschemas`. Refer to +> their documentation for more information. + +> `definitions_path` is the prefix each `$ref` is built from, such as `#/definitions/`. Schemars +> models its own field of that name as a JSON pointer instead, so the two are not interchangeable. ### Markdown descriptions diff --git a/crates/schematic/Cargo.toml b/crates/schematic/Cargo.toml index 2c0ef932..8d65b7eb 100644 --- a/crates/schematic/Cargo.toml +++ b/crates/schematic/Cargo.toml @@ -49,7 +49,7 @@ serde_json = { workspace = true, optional = true, features = [ # json schema markdown = { version = "1.0.0", optional = true } -schemars = { version = "0.8.22", optional = true, default-features = false } +schemars = { version = "1.2.2", optional = true, default-features = false } # pkl rpkl = { workspace = true, optional = true } @@ -144,7 +144,7 @@ schematic = { path = ".", features = [ serial_test = "4.0.1" # Only to prove loading works from inside an async runtime tokio = { version = "1.53.1", features = ["macros", "rt"] } -similar = "3.1.2" +similar = "3.2.0" starbase_sandbox = { workspace = true } strip-ansi-escapes = "0.2.1" derive_more = { version = "2.1.1", features = ["try_into", "as_ref"] } diff --git a/crates/schematic/src/schema/renderers/json_schema.rs b/crates/schematic/src/schema/renderers/json_schema.rs index 42037386..2e67d92d 100644 --- a/crates/schematic/src/schema/renderers/json_schema.rs +++ b/crates/schematic/src/schema/renderers/json_schema.rs @@ -1,16 +1,26 @@ use crate::schema::{RenderResult, SchemaRenderer}; use indexmap::IndexMap; use miette::IntoDiagnostic; -use schemars::r#gen::{GenVisitor, SchemaSettings}; -use schemars::schema::{ - ArrayValidation, InstanceType, Metadata, NumberValidation, ObjectValidation, RootSchema, - Schema as JsonSchema, SchemaObject, SingleOrVec, StringValidation, SubschemaValidation, -}; +use schemars::generate::{GenTransform, SchemaSettings}; use schematic_types::*; -use serde_json::{Number, Value}; +use serde_json::{Map, Number, Value}; use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::mem; +/// A schema document, which schemars models as JSON rather than as typed nodes. +type JsonSchema = schemars::Schema; + +/// Keys that describe a schema rather than constrain it. A field replaces these +/// wholesale with its own, so they are dropped from what the type contributed. +const METADATA_KEYS: [&str; 6] = [ + "title", + "description", + "default", + "deprecated", + "readOnly", + "writeOnly", +]; + pub struct JsonSchemaOptions { /// Allows newlines in descriptions, otherwise strips them. pub allow_newlines_in_description: bool, @@ -28,12 +38,12 @@ pub struct JsonSchemaOptions { /// This overrides any `title` manually defined by a type. pub set_field_name_as_title: bool, - // Inherited from schemars. - pub option_nullable: bool, - pub option_add_null_type: bool, + /// Prefix that `$ref` values are built from. pub definitions_path: String, + + // Inherited from schemars. pub meta_schema: Option, - pub visitors: Vec>, + pub transforms: Vec>, pub inline_subschemas: bool, } @@ -47,11 +57,11 @@ impl Default for JsonSchemaOptions { markdown_description: false, mark_struct_fields_required: true, set_field_name_as_title: false, - option_nullable: settings.option_nullable, - option_add_null_type: settings.option_add_null_type, - definitions_path: settings.definitions_path, - meta_schema: settings.meta_schema, - visitors: settings.visitors, + // Schemars models this as a JSON pointer, while we concatenate it + // onto a name to form a `$ref`, so it is not inherited + definitions_path: "#/definitions/".into(), + meta_schema: settings.meta_schema.map(|schema| schema.to_string()), + transforms: settings.transforms, inline_subschemas: settings.inline_subschemas, } } @@ -139,6 +149,22 @@ fn lit_to_value(lit: &LiteralValue) -> Value { } } +/// Insert a key only when there is a value for it, so that an absent setting +/// leaves no trace in the document. +fn insert_some(map: &mut Map, key: &str, value: Option) { + if let Some(value) = value { + map.insert(key.to_owned(), value); + } +} + +fn number(value: f64) -> Option { + Number::from_f64(value).map(Value::Number) +} + +fn count(value: usize) -> Value { + Value::Number(Number::from(value)) +} + impl JsonSchemaRenderer { pub fn new(options: JsonSchemaOptions) -> Self { Self { @@ -147,20 +173,33 @@ impl JsonSchemaRenderer { } } - fn create_metadata_from_schema(&self, schema: &Schema) -> Metadata { - Metadata { - title: if self.options.set_field_name_as_title { - None - } else { - schema.name.clone() - }, - deprecated: schema.deprecated.is_some(), - description: schema - .description - .clone() - .map(|desc| clean_comment(desc, self.options.allow_newlines_in_description)), - ..Default::default() + /// Start a schema document with the metadata a type carries. Keys are + /// inserted in the order schemars used to serialize them, so that upgrading + /// does not reshuffle every generated file. + fn create_metadata_from_schema(&self, schema: &Schema) -> Map { + let mut map = Map::new(); + + if !self.options.set_field_name_as_title + && let Some(name) = &schema.name + { + map.insert("title".into(), Value::String(name.to_owned())); + } + + if let Some(description) = &schema.description { + map.insert( + "description".into(), + Value::String(clean_comment( + description.to_owned(), + self.options.allow_newlines_in_description, + )), + ); } + + if schema.deprecated.is_some() { + map.insert("deprecated".into(), Value::Bool(true)); + } + + map } fn create_field_from_schema( @@ -168,33 +207,53 @@ impl JsonSchemaRenderer { name: &str, field: &SchemaField, ) -> RenderResult { - let mut schema = self.render_schema(&field.schema)?; - - if let JsonSchema::Object(ref mut inner) = schema { - let mut metadata = Metadata { - title: if self.options.set_field_name_as_title && !name.is_empty() { - Some(name.to_owned()) - } else { - None - }, - description: field - .comment - .clone() - .map(|desc| clean_comment(desc, self.options.allow_newlines_in_description)), - deprecated: field.deprecated.is_some(), - read_only: field.read_only, - write_only: field.write_only, - ..Default::default() - }; + let rendered = self.render_schema(&field.schema)?; - if let Some(default) = field.schema.get_default() { - metadata.default = Some(lit_to_value(default)); - } + let Some(object) = rendered.as_object() else { + return Ok(rendered); + }; + + // The field describes itself, so its metadata replaces whatever the + // type contributed rather than merging with it + let mut map = Map::new(); - inner.metadata = Some(Box::new(metadata)); + if self.options.set_field_name_as_title && !name.is_empty() { + map.insert("title".into(), Value::String(name.to_owned())); } - Ok(schema) + if let Some(comment) = &field.comment { + map.insert( + "description".into(), + Value::String(clean_comment( + comment.to_owned(), + self.options.allow_newlines_in_description, + )), + ); + } + + if let Some(default) = field.schema.get_default() { + map.insert("default".into(), lit_to_value(default)); + } + + if field.deprecated.is_some() { + map.insert("deprecated".into(), Value::Bool(true)); + } + + if field.read_only { + map.insert("readOnly".into(), Value::Bool(true)); + } + + if field.write_only { + map.insert("writeOnly".into(), Value::Bool(true)); + } + + for (key, value) in object { + if !METADATA_KEYS.contains(&key.as_str()) { + map.insert(key.to_owned(), value.to_owned()); + } + } + + Ok(JsonSchema::from(map)) } } @@ -204,30 +263,25 @@ impl SchemaRenderer for JsonSchemaRenderer { } fn render_array(&mut self, array: &ArrayType, schema: &Schema) -> RenderResult { + let mut map = self.create_metadata_from_schema(schema); + + map.insert("type".into(), Value::String("array".into())); + map.insert( + "items".into(), + self.render_schema(&array.items_type)?.to_value(), + ); + + insert_some(&mut map, "maxItems", array.max_length.map(count)); + insert_some(&mut map, "minItems", array.min_length.map(count)); + insert_some(&mut map, "uniqueItems", array.unique.map(Value::Bool)); + // `contains` constrains the array as a whole, and `items` every entry, // so the two are independent and both are rendered when present. - let contains = match &array.contains { - Some(inner) => Some(Box::new(self.render_schema(inner)?)), - None => None, - }; - - let data = SchemaObject { - metadata: Some(Box::new(self.create_metadata_from_schema(schema))), - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))), - array: Some(Box::new(ArrayValidation { - contains, - items: Some(SingleOrVec::Single(Box::new( - self.render_schema(&array.items_type)?, - ))), - max_items: array.max_length.map(|i| i as u32), - min_items: array.min_length.map(|i| i as u32), - unique_items: array.unique, - ..Default::default() - })), - ..Default::default() - }; + if let Some(inner) = &array.contains { + map.insert("contains".into(), self.render_schema(inner)?.to_value()); + } - Ok(JsonSchema::Object(data)) + Ok(JsonSchema::from(map)) } fn render_boolean( @@ -235,15 +289,15 @@ impl SchemaRenderer for JsonSchemaRenderer { _boolean: &BooleanType, schema: &Schema, ) -> RenderResult { - Ok(JsonSchema::Object(SchemaObject { - metadata: Some(Box::new(self.create_metadata_from_schema(schema))), - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Boolean))), - ..Default::default() - })) + let mut map = self.create_metadata_from_schema(schema); + + map.insert("type".into(), Value::String("boolean".into())); + + Ok(JsonSchema::from(map)) } fn render_enum(&mut self, enu: &EnumType, schema: &Schema) -> RenderResult { - let metadata = self.create_metadata_from_schema(schema); + let mut map = self.create_metadata_from_schema(schema); // Unit enum with a fallback variant if enu @@ -255,83 +309,83 @@ impl SchemaRenderer for JsonSchemaRenderer { for (name, field) in enu.variants.as_ref().unwrap() { if !field.hidden { - any_of.push(self.create_field_from_schema(name, field)?); + any_of.push(self.create_field_from_schema(name, field)?.to_value()); } } - return Ok(JsonSchema::Object(SchemaObject { - metadata: Some(Box::new(metadata)), - subschemas: Some(Box::new(SubschemaValidation { - any_of: Some(any_of), - ..Default::default() - })), - ..Default::default() - })); + map.insert("anyOf".into(), Value::Array(any_of)); + + return Ok(JsonSchema::from(map)); } // Unit enum with no fallback variant - let mut instance_type = InstanceType::String; + let mut instance_type = "string"; let mut enum_values = vec![]; for value in &enu.values { match value { LiteralValue::Bool(v) => { - instance_type = InstanceType::Boolean; + instance_type = "boolean"; enum_values.push(Value::Bool(*v)); } LiteralValue::F32(v) => { - instance_type = InstanceType::Number; + instance_type = "number"; enum_values.push(Value::Number(Number::from_f64(*v as f64).unwrap())); } LiteralValue::F64(v) => { - instance_type = InstanceType::Number; + instance_type = "number"; enum_values.push(Value::Number(Number::from_f64(*v).unwrap())); } LiteralValue::Int(v) => { - instance_type = InstanceType::Number; + instance_type = "number"; enum_values.push(Value::Number(Number::from(*v))); } LiteralValue::UInt(v) => { - instance_type = InstanceType::Number; + instance_type = "number"; enum_values.push(Value::Number(Number::from(*v))); } LiteralValue::String(v) => { - instance_type = InstanceType::String; + instance_type = "string"; enum_values.push(Value::String(v.to_owned())); } }; } - Ok(JsonSchema::Object(SchemaObject { - metadata: Some(Box::new(metadata)), - instance_type: Some(SingleOrVec::Single(Box::new(instance_type))), - enum_values: Some(enum_values), - ..Default::default() - })) + map.insert("type".into(), Value::String(instance_type.into())); + map.insert("enum".into(), Value::Array(enum_values)); + + Ok(JsonSchema::from(map)) } fn render_float(&mut self, float: &FloatType, schema: &Schema) -> RenderResult { - let data = SchemaObject { - metadata: Some(Box::new(self.create_metadata_from_schema(schema))), - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Number))), - enum_values: float.enum_values.clone().map(|values| { - values - .into_iter() - .map(|v| Value::Number(Number::from_f64(v).unwrap())) - .collect() - }), - format: float.format.clone(), - number: Some(Box::new(NumberValidation { - exclusive_maximum: float.max_exclusive, - exclusive_minimum: float.min_exclusive, - maximum: float.max, - minimum: float.min, - multiple_of: float.multiple_of, - })), - ..Default::default() - }; + let mut map = self.create_metadata_from_schema(schema); + + map.insert("type".into(), Value::String("number".into())); + + insert_some(&mut map, "format", float.format.clone().map(Value::String)); + + if let Some(values) = &float.enum_values { + map.insert( + "enum".into(), + Value::Array(values.iter().filter_map(|v| number(*v)).collect()), + ); + } - Ok(JsonSchema::Object(data)) + insert_some(&mut map, "multipleOf", float.multiple_of.and_then(number)); + insert_some(&mut map, "maximum", float.max.and_then(number)); + insert_some( + &mut map, + "exclusiveMaximum", + float.max_exclusive.and_then(number), + ); + insert_some(&mut map, "minimum", float.min.and_then(number)); + insert_some( + &mut map, + "exclusiveMinimum", + float.min_exclusive.and_then(number), + ); + + Ok(JsonSchema::from(map)) } fn render_integer( @@ -339,27 +393,55 @@ impl SchemaRenderer for JsonSchemaRenderer { integer: &IntegerType, schema: &Schema, ) -> RenderResult { - let data = SchemaObject { - metadata: Some(Box::new(self.create_metadata_from_schema(schema))), - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Number))), - enum_values: integer.enum_values.clone().map(|values| { - values - .into_iter() - .map(|v| Value::Number(Number::from(v))) - .collect() - }), - format: integer.format.clone(), - number: Some(Box::new(NumberValidation { - exclusive_maximum: integer.max_exclusive.map(|i| i as f64), - exclusive_minimum: integer.min_exclusive.map(|i| i as f64), - maximum: integer.max.map(|i| i as f64), - minimum: integer.min.map(|i| i as f64), - multiple_of: integer.multiple_of.map(|i| i as f64), - })), - ..Default::default() - }; + let mut map = self.create_metadata_from_schema(schema); + + map.insert("type".into(), Value::String("number".into())); + + insert_some( + &mut map, + "format", + integer.format.clone().map(Value::String), + ); + + if let Some(values) = &integer.enum_values { + map.insert( + "enum".into(), + Value::Array( + values + .iter() + .map(|v| Value::Number(Number::from(*v))) + .collect(), + ), + ); + } - Ok(JsonSchema::Object(data)) + insert_some( + &mut map, + "multipleOf", + integer.multiple_of.and_then(|i| number(i as f64)), + ); + insert_some( + &mut map, + "maximum", + integer.max.and_then(|i| number(i as f64)), + ); + insert_some( + &mut map, + "exclusiveMaximum", + integer.max_exclusive.and_then(|i| number(i as f64)), + ); + insert_some( + &mut map, + "minimum", + integer.min.and_then(|i| number(i as f64)), + ); + insert_some( + &mut map, + "exclusiveMinimum", + integer.min_exclusive.and_then(|i| number(i as f64)), + ); + + Ok(JsonSchema::from(map)) } fn render_literal( @@ -367,65 +449,81 @@ impl SchemaRenderer for JsonSchemaRenderer { literal: &LiteralType, schema: &Schema, ) -> RenderResult { - Ok(JsonSchema::Object(SchemaObject { - metadata: Some(Box::new(self.create_metadata_from_schema(schema))), - const_value: Some(lit_to_value(&literal.value)), - ..Default::default() - })) + let mut map = self.create_metadata_from_schema(schema); + + map.insert("const".into(), lit_to_value(&literal.value)); + + Ok(JsonSchema::from(map)) } fn render_null(&mut self, schema: &Schema) -> RenderResult { - Ok(JsonSchema::Object(SchemaObject { - metadata: Some(Box::new(self.create_metadata_from_schema(schema))), - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))), - ..Default::default() - })) + let mut map = self.create_metadata_from_schema(schema); + + map.insert("type".into(), Value::String("null".into())); + + Ok(JsonSchema::from(map)) } fn render_object(&mut self, object: &ObjectType, schema: &Schema) -> RenderResult { - let data = SchemaObject { - metadata: Some(Box::new(self.create_metadata_from_schema(schema))), - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))), - object: Some(Box::new(ObjectValidation { - max_properties: object.max_length.map(|i| i as u32), - min_properties: object.min_length.map(|i| i as u32), - required: BTreeSet::from_iter(object.required.clone().unwrap_or_default()), - additional_properties: Some(Box::new(self.render_schema(&object.value_type)?)), - property_names: Some(Box::new(self.render_schema(&object.key_type)?)), - ..Default::default() - })), - ..Default::default() - }; + let mut map = self.create_metadata_from_schema(schema); + + map.insert("type".into(), Value::String("object".into())); + + insert_some(&mut map, "maxProperties", object.max_length.map(count)); + insert_some(&mut map, "minProperties", object.min_length.map(count)); + + let required = BTreeSet::from_iter(object.required.clone().unwrap_or_default()); + + if !required.is_empty() { + map.insert( + "required".into(), + Value::Array(required.into_iter().map(Value::String).collect()), + ); + } + + map.insert( + "additionalProperties".into(), + self.render_schema(&object.value_type)?.to_value(), + ); + map.insert( + "propertyNames".into(), + self.render_schema(&object.key_type)?.to_value(), + ); - Ok(JsonSchema::Object(data)) + Ok(JsonSchema::from(map)) } fn render_reference(&mut self, reference: &str, _schema: &Schema) -> RenderResult { - Ok(JsonSchema::Object(SchemaObject { - // Note: Don't add metadata as it causes nested schema references! - reference: Some(format!("{}{}", self.options.definitions_path, reference)), - ..Default::default() - })) + // Note: Don't add metadata as it causes nested schema references! + Ok(JsonSchema::new_ref(format!( + "{}{}", + self.options.definitions_path, reference + ))) } fn render_string(&mut self, string: &StringType, schema: &Schema) -> RenderResult { - let data = SchemaObject { - metadata: Some(Box::new(self.create_metadata_from_schema(schema))), - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))), - enum_values: string - .enum_values - .clone() - .map(|values| values.into_iter().map(Value::String).collect()), - format: string.format.clone(), - string: Some(Box::new(StringValidation { - max_length: string.max_length.map(|i| i as u32), - min_length: string.min_length.map(|i| i as u32), - pattern: string.pattern.clone(), - })), - ..Default::default() - }; + let mut map = self.create_metadata_from_schema(schema); + + map.insert("type".into(), Value::String("string".into())); - Ok(JsonSchema::Object(data)) + insert_some(&mut map, "format", string.format.clone().map(Value::String)); + + if let Some(values) = &string.enum_values { + map.insert( + "enum".into(), + Value::Array(values.iter().cloned().map(Value::String).collect()), + ); + } + + insert_some(&mut map, "maxLength", string.max_length.map(count)); + insert_some(&mut map, "minLength", string.min_length.map(count)); + insert_some( + &mut map, + "pattern", + string.pattern.clone().map(Value::String), + ); + + Ok(JsonSchema::from(map)) } fn render_struct( @@ -435,7 +533,7 @@ impl SchemaRenderer for JsonSchemaRenderer { ) -> RenderResult { let mut properties = BTreeMap::new(); let mut required = BTreeSet::from_iter(structure.required.clone().unwrap_or_default()); - let mut additional_properties = Some(Box::new(JsonSchema::Bool(false))); + let mut additional_properties = Some(Value::Bool(false)); let exclude_aliases = self.options.exclude_aliases; for (name, field) in structure.sorted_fields() { @@ -448,12 +546,12 @@ impl SchemaRenderer for JsonSchemaRenderer { let flattened = self.render_schema_without_reference(schema)?; if matches!(schema.ty, SchemaType::Object(_)) - && let JsonSchema::Object(inner) = flattened.clone() - && let Some(object) = inner.object + && let Some(inner) = flattened.as_object() + && let Some(value) = inner.get("additionalProperties") { - additional_properties = object.additional_properties; + additional_properties = Some(value.to_owned()); } else { - additional_properties = Some(Box::new(flattened)); + additional_properties = Some(flattened.to_value()); } } @@ -468,125 +566,157 @@ impl SchemaRenderer for JsonSchemaRenderer { for alias in &field.aliases { properties.insert( alias.to_owned(), - self.create_field_from_schema(alias, field)?, + self.create_field_from_schema(alias, field)?.to_value(), ); } } - properties.insert(name.to_owned(), self.create_field_from_schema(name, field)?); + properties.insert( + name.to_owned(), + self.create_field_from_schema(name, field)?.to_value(), + ); } - let data = SchemaObject { - metadata: Some(Box::new(self.create_metadata_from_schema(schema))), - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Object))), - object: Some(Box::new(ObjectValidation { - additional_properties, - required, - properties, - ..Default::default() - })), - ..Default::default() - }; + let mut map = self.create_metadata_from_schema(schema); - Ok(JsonSchema::Object(data)) + map.insert("type".into(), Value::String("object".into())); + + if !required.is_empty() { + map.insert( + "required".into(), + Value::Array(required.into_iter().map(Value::String).collect()), + ); + } + + map.insert( + "properties".into(), + Value::Object(properties.into_iter().collect()), + ); + + insert_some(&mut map, "additionalProperties", additional_properties); + + Ok(JsonSchema::from(map)) } fn render_tuple(&mut self, tuple: &TupleType, schema: &Schema) -> RenderResult { let mut items = vec![]; for item in &tuple.items_types { - items.push(self.render_schema(item)?); - } - - let data = SchemaObject { - metadata: Some(Box::new(self.create_metadata_from_schema(schema))), - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))), - array: Some(Box::new(ArrayValidation { - items: Some(SingleOrVec::Vec(items)), - max_items: Some(tuple.items_types.len() as u32), - min_items: Some(tuple.items_types.len() as u32), - ..Default::default() - })), - ..Default::default() - }; + items.push(self.render_schema(item)?.to_value()); + } + + let mut map = self.create_metadata_from_schema(schema); - Ok(JsonSchema::Object(data)) + map.insert("type".into(), Value::String("array".into())); + map.insert("items".into(), Value::Array(items)); + map.insert("maxItems".into(), count(tuple.items_types.len())); + map.insert("minItems".into(), count(tuple.items_types.len())); + + Ok(JsonSchema::from(map)) } fn render_union(&mut self, uni: &UnionType, schema: &Schema) -> RenderResult { let mut items = vec![]; - let mut metadata = self.create_metadata_from_schema(schema); + let mut default = None; for item in &uni.variants_types { - items.push(self.render_schema(item)?); + items.push(self.render_schema(item)?.to_value()); + + if default.is_none() + && let Some(def) = item.get_default() + { + default = Some(lit_to_value(def)); + } + } + + let mut map = self.create_metadata_from_schema(schema); - if metadata.default.is_none() { - if let Some(def) = item.get_default() { - metadata.default = Some(lit_to_value(def)); + // `default` sits with the other metadata keys, ahead of the subschemas + if let Some(default) = default { + let rest = mem::take(&mut map); + + for (key, value) in rest { + map.insert(key.clone(), value); + + if key == "description" || (key == "title" && !map.contains_key("description")) { + map.insert("default".into(), default.clone()); } } + + if !map.contains_key("default") { + map.insert("default".into(), default); + } } - let subschema = match uni.operator { - UnionOperator::AnyOf => SubschemaValidation { - any_of: Some(items), - ..Default::default() + map.insert( + match uni.operator { + UnionOperator::AnyOf => "anyOf".into(), + UnionOperator::OneOf => "oneOf".into(), }, - UnionOperator::OneOf => SubschemaValidation { - one_of: Some(items), - ..Default::default() - }, - }; + Value::Array(items), + ); - Ok(JsonSchema::Object(SchemaObject { - metadata: Some(Box::new(metadata)), - subschemas: Some(Box::new(subschema)), - ..Default::default() - })) + Ok(JsonSchema::from(map)) } fn render_unknown(&mut self, schema: &Schema) -> RenderResult { - Ok(JsonSchema::Object(SchemaObject { - metadata: Some(Box::new(self.create_metadata_from_schema(schema))), - instance_type: Some(SingleOrVec::Vec(vec![ - InstanceType::Boolean, - InstanceType::Object, - InstanceType::Array, - InstanceType::Number, - InstanceType::String, - InstanceType::Integer, - ])), - ..Default::default() - })) + let mut map = self.create_metadata_from_schema(schema); + + map.insert( + "type".into(), + Value::Array( + ["boolean", "object", "array", "number", "string", "integer"] + .into_iter() + .map(|ty| Value::String(ty.into())) + .collect(), + ), + ); + + Ok(JsonSchema::from(map)) } fn render(&mut self, schemas: IndexMap) -> RenderResult { self.references = HashSet::from_iter(schemas.keys().cloned()); - let mut root_schema = RootSchema { - meta_schema: self.options.meta_schema.clone(), - ..RootSchema::default() - }; + let mut root = Map::new(); + + if let Some(meta_schema) = &self.options.meta_schema { + root.insert("$schema".into(), Value::String(meta_schema.to_owned())); + } + + let mut definitions = BTreeMap::new(); for (i, (name, schema)) in schemas.iter().enumerate() { + let rendered = self.render_schema_without_reference(schema)?; + // The last schema in the generator is the root schema if i == schemas.len() - 1 { - root_schema.schema = self.render_schema_without_reference(schema)?.into_object(); + if let Some(object) = rendered.as_object() { + for (key, value) in object { + root.insert(key.to_owned(), value.to_owned()); + } + } // Otherwise the others are all ref definitions } else { - root_schema.definitions.insert( - name.to_owned(), - self.render_schema_without_reference(schema)?, - ); + definitions.insert(name.to_owned(), rendered.to_value()); } } - for visitor in &mut self.options.visitors { - visitor.visit_root_schema(&mut root_schema) + if !definitions.is_empty() { + root.insert( + "definitions".into(), + Value::Object(definitions.into_iter().collect()), + ); + } + + let mut root_schema = JsonSchema::from(root); + + for transform in &mut self.options.transforms { + transform.transform(&mut root_schema); } - let mut json = serde_json::to_value(&root_schema).into_diagnostic()?; + let mut json = root_schema.to_value(); if self.options.markdown_description { inject_markdown_descriptions(&mut json)?; diff --git a/crates/schematic/tests/snapshots/defaults_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/defaults_test__generates_json_schema.snap index 6e083ee8..778dfd0e 100644 --- a/crates/schematic/tests/snapshots/defaults_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/defaults_test__generates_json_schema.snap @@ -20,11 +20,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ], "properties": { "nested": { - "allOf": [ - { - "$ref": "#/definitions/NativeDefaults" - } - ] + "$ref": "#/definitions/NativeDefaults" }, "nested_boxed": { "type": "object", diff --git a/crates/schematic/tests/snapshots/env_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/env_test__generates_json_schema.snap index 985556d3..7ce6732f 100644 --- a/crates/schematic/tests/snapshots/env_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/env_test__generates_json_schema.snap @@ -32,11 +32,7 @@ expression: "std::fs::read_to_string(file).unwrap()" } }, "nested": { - "allOf": [ - { - "$ref": "#/definitions/EnvVarsNested" - } - ] + "$ref": "#/definitions/EnvVarsNested" }, "number": { "type": "number" diff --git a/crates/schematic/tests/snapshots/extends_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/extends_test__generates_json_schema.snap index b19e8b98..6a662135 100644 --- a/crates/schematic/tests/snapshots/extends_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/extends_test__generates_json_schema.snap @@ -12,11 +12,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ], "properties": { "extends": { - "allOf": [ - { - "$ref": "#/definitions/ExtendsFrom" - } - ] + "$ref": "#/definitions/ExtendsFrom" }, "value": { "type": "array", diff --git a/crates/schematic/tests/snapshots/macro_enum_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/macro_enum_test__generates_json_schema.snap index 92b8eaa8..0bc604b3 100644 --- a/crates/schematic/tests/snapshots/macro_enum_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/macro_enum_test__generates_json_schema.snap @@ -58,11 +58,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ], "properties": { "content": { - "allOf": [ - { - "$ref": "#/definitions/PartialSomeConfig" - } - ] + "$ref": "#/definitions/PartialSomeConfig" }, "type": { "const": "Qux" @@ -127,11 +123,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ], "properties": { "content": { - "allOf": [ - { - "$ref": "#/definitions/SomeConfig" - } - ] + "$ref": "#/definitions/SomeConfig" }, "type": { "const": "Qux" @@ -228,11 +220,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ], "properties": { "Qux": { - "allOf": [ - { - "$ref": "#/definitions/SomeConfig" - } - ] + "$ref": "#/definitions/SomeConfig" } }, "additionalProperties": false @@ -287,11 +275,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ], "properties": { "Object": { - "allOf": [ - { - "$ref": "#/definitions/SomeConfig" - } - ] + "$ref": "#/definitions/SomeConfig" } }, "additionalProperties": false @@ -437,11 +421,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ], "properties": { "Qux": { - "allOf": [ - { - "$ref": "#/definitions/PartialSomeConfig" - } - ] + "$ref": "#/definitions/PartialSomeConfig" } }, "additionalProperties": false @@ -496,11 +476,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ], "properties": { "Object": { - "allOf": [ - { - "$ref": "#/definitions/PartialSomeConfig" - } - ] + "$ref": "#/definitions/PartialSomeConfig" } }, "additionalProperties": false diff --git a/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap index 1a31717a..e70c3e77 100644 --- a/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/macros_test__generates_json_schema.snap @@ -285,11 +285,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ], "properties": { "extends": { - "allOf": [ - { - "$ref": "#/definitions/ExtendsFrom" - } - ] + "$ref": "#/definitions/ExtendsFrom" } }, "additionalProperties": false @@ -931,11 +927,7 @@ expression: "std::fs::read_to_string(file).unwrap()" "type": "string" }, "nested": { - "allOf": [ - { - "$ref": "#/definitions/NestedValidations" - } - ] + "$ref": "#/definitions/NestedValidations" }, "nested2": { "anyOf": [ diff --git a/crates/schematic/tests/snapshots/partialize_test__enums.snap b/crates/schematic/tests/snapshots/partialize_test__enums.snap index 7c91c57d..1aba4347 100644 --- a/crates/schematic/tests/snapshots/partialize_test__enums.snap +++ b/crates/schematic/tests/snapshots/partialize_test__enums.snap @@ -98,6 +98,31 @@ expression: "create_diff::()" 🟥 value: String( 🟥 "b", 🟥 ), +🟥 }, +🟥 ), +🟥 }, +🟥 deprecated: None, +🟥 env_var: None, +🟥 flatten: false, +🟥 hidden: false, +🟥 nullable: false, +🟥 optional: false, +🟥 read_only: false, +🟥 write_only: false, +🟥 }, +🟥 "c": SchemaField { +🟥 aliases: [], +🟥 comment: None, +🟥 schema: Schema { +🟥 deprecated: None, +🟥 description: None, +🟥 name: None, +🟥 nullable: false, +🟥 ty: Literal( +🟥 LiteralType { +🟥 value: String( +🟥 "c", +🟥 ), 🟩 values: [ 🟩 String( 🟩 "a", @@ -198,31 +223,6 @@ expression: "create_diff::()" 🟥 read_only: false, 🟥 write_only: false, 🟥 }, -🟥 "c": SchemaField { -🟥 aliases: [], -🟥 comment: None, -🟥 schema: Schema { -🟥 deprecated: None, -🟥 description: None, -🟥 name: None, -🟥 nullable: false, -🟥 ty: Literal( -🟥 LiteralType { -🟥 value: String( -🟥 "c", -🟥 ), -🟥 }, -🟥 ), -🟥 }, -🟥 deprecated: None, -🟥 env_var: None, -🟥 flatten: false, -🟥 hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟥 read_only: false, -🟥 write_only: false, -🟥 }, 🟩 ), ⬛️ }, 🟥 ), @@ -473,6 +473,59 @@ expression: "create_diff::()" 🟥 value: String( 🟥 "b", 🟥 ), +🟥 }, +🟥 ), +🟥 }, +🟥 deprecated: None, +🟥 env_var: None, +🟥 flatten: false, +🟥 hidden: false, +🟥 nullable: false, +🟥 optional: false, +🟥 read_only: false, +🟥 write_only: false, +🟥 }, +🟥 "c": SchemaField { +🟥 aliases: [], +🟥 comment: None, +🟥 schema: Schema { +🟥 deprecated: None, +🟥 description: None, +🟥 name: None, +🟥 nullable: false, +🟥 ty: Literal( +🟥 LiteralType { +🟥 value: String( +🟥 "c", +🟥 ), +🟥 }, +🟥 ), +🟥 }, +🟥 deprecated: None, +🟥 env_var: None, +🟥 flatten: false, +🟥 hidden: false, +🟥 nullable: false, +🟥 optional: false, +🟥 read_only: false, +🟥 write_only: false, +🟥 }, +🟥 "other": SchemaField { +🟥 aliases: [], +🟥 comment: None, +🟥 schema: Schema { +🟥 deprecated: None, +🟥 description: None, +🟥 name: None, +🟥 nullable: false, +🟥 ty: String( +🟥 StringType { +🟥 default: None, +🟥 enum_values: None, +🟥 format: None, +🟥 max_length: None, +🟥 min_length: None, +🟥 pattern: None, 🟩 values: [ 🟩 String( 🟩 "a", @@ -601,59 +654,6 @@ expression: "create_diff::()" 🟥 read_only: false, 🟥 write_only: false, 🟥 }, -🟥 "c": SchemaField { -🟥 aliases: [], -🟥 comment: None, -🟥 schema: Schema { -🟥 deprecated: None, -🟥 description: None, -🟥 name: None, -🟥 nullable: false, -🟥 ty: Literal( -🟥 LiteralType { -🟥 value: String( -🟥 "c", -🟥 ), -🟥 }, -🟥 ), -🟥 }, -🟥 deprecated: None, -🟥 env_var: None, -🟥 flatten: false, -🟥 hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟥 read_only: false, -🟥 write_only: false, -🟥 }, -🟥 "other": SchemaField { -🟥 aliases: [], -🟥 comment: None, -🟥 schema: Schema { -🟥 deprecated: None, -🟥 description: None, -🟥 name: None, -🟥 nullable: false, -🟥 ty: String( -🟥 StringType { -🟥 default: None, -🟥 enum_values: None, -🟥 format: None, -🟥 max_length: None, -🟥 min_length: None, -🟥 pattern: None, -🟥 }, -🟥 ), -🟥 }, -🟥 deprecated: None, -🟥 env_var: None, -🟥 flatten: false, -🟥 hidden: false, -🟥 nullable: false, -🟥 optional: false, -🟥 read_only: false, -🟥 write_only: false, -🟥 }, 🟩 ), 🟩 }, 🟩 Schema { diff --git a/crates/schematic/tests/snapshots/settings_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/settings_test__generates_json_schema.snap index f6d3563d..9fb4e346 100644 --- a/crates/schematic/tests/snapshots/settings_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/settings_test__generates_json_schema.snap @@ -58,11 +58,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ] }, "nested_req": { - "allOf": [ - { - "$ref": "#/definitions/StandardSettings" - } - ] + "$ref": "#/definitions/StandardSettings" } }, "additionalProperties": false diff --git a/crates/schematic/tests/snapshots/variants_test__generates_json_schema.snap b/crates/schematic/tests/snapshots/variants_test__generates_json_schema.snap index 17f3340e..711b3ea6 100644 --- a/crates/schematic/tests/snapshots/variants_test__generates_json_schema.snap +++ b/crates/schematic/tests/snapshots/variants_test__generates_json_schema.snap @@ -11,11 +11,7 @@ expression: "std::fs::read_to_string(file).unwrap()" ], "properties": { "projects": { - "allOf": [ - { - "$ref": "#/definitions/Projects" - } - ] + "$ref": "#/definitions/Projects" } }, "additionalProperties": false, From f92fb42c5a08354e18275b1e59cbac53b3407ae7 Mon Sep 17 00:00:00 2001 From: Miles Johnson Date: Thu, 20 Aug 2026 22:13:12 -0700 Subject: [PATCH 18/18] new: New features. (#202) --- AGENTS.md | 5 + CHANGELOG.md | 7 + book/src/config/struct/default.md | 29 +++- crates/core/src/field_value.rs | 28 +++- crates/core/tests/setting_default_test.rs | 55 +++++++ ...ing_default__supports_struct_literals.snap | 16 ++ ...setting_default__supports_value_paths.snap | 16 ++ ..._default__wraps_value_paths_in_layers.snap | 15 ++ crates/macros/tests/config_test.rs | 108 +++++++++++++ crates/schematic/src/validate/mod.rs | 2 + crates/schematic/src/validate/uuid.rs | 144 ++++++++++++++++++ crates/schematic/tests/validate_test.rs | 4 +- 12 files changed, 421 insertions(+), 8 deletions(-) create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__supports_struct_literals.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__supports_value_paths.snap create mode 100644 crates/core/tests/snapshots/setting_default_test__setting_default__wraps_value_paths_in_layers.snap create mode 100644 crates/schematic/src/validate/uuid.rs diff --git a/AGENTS.md b/AGENTS.md index 1ed9361f..552ce056 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -291,6 +291,11 @@ These were deliberated and settled. If something looks wrong, it probably isn't. `Result`, and silently returning wrong values is worse. - **Validation error paths use serde names** (renames honored) for fields and variants; variants use `PathSegment::Variant` + index, rendering `Many[0].inner`. +- **A `#[setting(default)]` path is told apart by its casing.** `LevelFilter::Debug` names a value, + `find_unused_port` names a handler function, and both arrive as a bare `Expr::Path` that nothing at + derive time can resolve. `path_names_a_value` goes by the last segment's first character, which + works because a name breaking Rust's conventions would already be earning a lint. Non-literal + defaults still never reach the schema — `impl_schema_type` only reads `Expr::Lit`. - **Derive-time panics for wrong attribute usage are intentional.** The maintainer wants loud failure over silent no-ops. `#[setting(nested)]` on a primitive panics. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f27cc68..8ff14e88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,13 @@ while the schema types have been updated to be more flexible and composable. required an explicit `#[setting(env)]` and panicked at derive time otherwise. - Added support for `#[deprecated(since = "...", note = "...")]`, whose note is now used as the deprecation message. Only `#[deprecated]` and `#[deprecated = "..."]` were recognized before. +- Added support for struct literals and paths that name a value in `#[setting(default)]`, so an enum + variant or constant can be written directly: `#[setting(default = LevelFilter::Debug)]`. A path + whose last segment starts uppercase is a value, and one starting lowercase is still a handler + function to call. ([#173](https://github.com/moonrepo/schematic/issues/173)) +- Added a `validate::uuid` function, which validates a string is a UUID in the canonical hyphenated + form. It checks the shape only, so the nil UUID and unknown versions are both accepted, and it + needs no Cargo feature. ([#156](https://github.com/moonrepo/schematic/issues/156)) - 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/book/src/config/struct/default.md b/book/src/config/struct/default.md index 7ec5ad45..30937c97 100644 --- a/book/src/config/struct/default.md +++ b/book/src/config/struct/default.md @@ -7,9 +7,9 @@ In Schematic, there are 2 forms of default values: - The second is on the [final configuration](../index.md) itself, and uses the `Default` trait to generate the final value if none was provided. This acts more like a fallback. -To define a default value, use the `#[setting(default)]` attribute. The `default` attribute field is -used for declaring primitive values, like numbers, strings, and booleans, but can also be used for -array and tuple literals, as well as function (mainly for `from()`) and macros calls. +To define a default value, use the `#[setting(default)]` attribute. The `default` attribute field +accepts primitive values, like numbers, strings, and booleans, as well as array, tuple, and struct +literals, paths that name a value, and function and macro calls. ```rust #[derive(Config)] @@ -25,9 +25,32 @@ struct AppConfig { #[setting(default = vec!["localhost".into()])] pub allowed_hosts: Vec, + + #[setting(default = LevelFilter::Debug)] + pub log_level: LevelFilter, + + #[setting(default = Retry { attempts: 3, backoff: 500 })] + pub retry: Retry, } ``` +### Paths: value or function? + +A bare path is ambiguous — `LevelFilter::Debug` names a value, while `find_unused_port` names a +[handler function](#handler-function) to call. Nothing at compile time can tell them apart, so +Schematic goes by Rust's naming conventions: + +| Last segment of the path | Treated as | Example | +| ------------------------------- | ---------------- | ------------------------ | +| Starts uppercase | A value | `LevelFilter::Debug` | +| Starts lowercase | A handler function | `find_unused_port` | + +That covers enum variants, unit structs, and constants on one side, and functions on the other, +since a name breaking those conventions would already be earning a compiler lint. + +> A handler function that must keep a non-conventional name can be reached through a `snake_case` +> wrapper, or called directly with `default = my_wrapper()` when it needs no context. + For enums, the `default` field takes no value, and simply marks which variant to use as the default. ```rust diff --git a/crates/core/src/field_value.rs b/crates/core/src/field_value.rs index ef408408..b5c7262c 100644 --- a/crates/core/src/field_value.rs +++ b/crates/core/src/field_value.rs @@ -5,11 +5,26 @@ use crate::value::{Layer, Value}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use std::ops::Deref; -use syn::{Expr, Lit, Type}; +use syn::{Expr, Lit, Path, Type}; #[derive(Debug)] pub struct FieldValue(Value); +/// Whether a path names a value -- an enum variant, unit struct, or constant -- +/// rather than a function to hand the context to. +/// +/// Both arrive as a bare `Expr::Path`, and nothing at derive time can resolve +/// which one it is, so this leans on Rust's naming conventions: a function is +/// `snake_case`, while everything that names a value is `PascalCase` or +/// `SCREAMING_SNAKE_CASE`. A handler that ignored that would already be earning +/// a `non_snake_case` warning of its own. +fn path_names_a_value(path: &Path) -> bool { + path.segments + .last() + .and_then(|segment| segment.ident.to_string().chars().next()) + .is_some_and(|char| char.is_uppercase()) +} + fn wrap_layer(layer: &Layer, value: TokenStream) -> TokenStream { match layer { Layer::Arc => quote! { Arc::new(#value) }, @@ -102,7 +117,7 @@ impl FieldValue { match field_args.default.as_ref() { // Handler functions return the entire value - Some(Expr::Path(func)) => { + Some(Expr::Path(func)) if !path_names_a_value(&func.path) => { res.requires_internal = true; res.value = quote! { handle_default_result(#func(context))? }; } @@ -110,7 +125,12 @@ impl FieldValue { // collection, so only wrap with the layers outside of it Some(expr) => { let mut value = match expr { - Expr::Array(_) | Expr::Call(_) | Expr::Macro(_) | Expr::Tuple(_) => { + Expr::Array(_) + | Expr::Call(_) + | Expr::Macro(_) + | Expr::Path(_) + | Expr::Struct(_) + | Expr::Tuple(_) => { quote! { #expr } } Expr::Lit(lit) => match &lit.lit { @@ -126,7 +146,7 @@ impl FieldValue { }, invalid => { panic!( - "Unsupported default value ({invalid:?}). May only provide literals, primitives, arrays, or tuples." + "Unsupported default value ({invalid:?}). May only provide literals, primitives, arrays, tuples, structs, paths, or calls." ); } }; diff --git a/crates/core/tests/setting_default_test.rs b/crates/core/tests/setting_default_test.rs index a53f821d..b910c617 100644 --- a/crates/core/tests/setting_default_test.rs +++ b/crates/core/tests/setting_default_test.rs @@ -112,6 +112,61 @@ mod setting_default { assert_snapshot!(pretty(container.impl_partial_default_values())); } + // A path that names a value rather than a function, told apart by Rust's + // naming conventions, so that an enum variant does not compile as a call + #[test] + fn supports_value_paths() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = LevelFilter::Debug)] + a: LevelFilter, + #[setting(default = MAX)] + b: usize, + #[setting(default = limits::MAX)] + c: usize, + #[setting(default = Point::ORIGIN)] + d: Point, + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + + #[test] + fn supports_struct_literals() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = Point { x: 1, y: 2 })] + a: Point, + #[setting(default = Wrapper { inner: Point { x: 0, y: 0 } })] + b: Wrapper, + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + + // An `Option` setting still gets its value wrapped, and a collection still + // takes the whole literal + #[test] + fn wraps_value_paths_in_layers() { + let container = Container::from(parse_quote! { + #[derive(Config)] + struct Example { + #[setting(default = LevelFilter::Debug)] + a: Option, + #[setting(default = LevelFilter::Debug)] + b: Box, + #[setting(default = vec![LevelFilter::Debug])] + c: Vec, + } + }); + + assert_snapshot!(pretty(container.impl_partial_default_values())); + } + mod named_struct { use super::*; diff --git a/crates/core/tests/snapshots/setting_default_test__setting_default__supports_struct_literals.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__supports_struct_literals.snap new file mode 100644 index 00000000..0f153761 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__supports_struct_literals.snap @@ -0,0 +1,16 @@ +--- +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(Point { x: 1, y: 2 }), + b: Some(Wrapper { + inner: Point { x: 0, y: 0 }, + }), + }), + ) +} diff --git a/crates/core/tests/snapshots/setting_default_test__setting_default__supports_value_paths.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__supports_value_paths.snap new file mode 100644 index 00000000..a9354a73 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__supports_value_paths.snap @@ -0,0 +1,16 @@ +--- +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(LevelFilter::Debug), + b: Some(MAX), + c: Some(limits::MAX), + d: Some(Point::ORIGIN), + }), + ) +} diff --git a/crates/core/tests/snapshots/setting_default_test__setting_default__wraps_value_paths_in_layers.snap b/crates/core/tests/snapshots/setting_default_test__setting_default__wraps_value_paths_in_layers.snap new file mode 100644 index 00000000..b53e8103 --- /dev/null +++ b/crates/core/tests/snapshots/setting_default_test__setting_default__wraps_value_paths_in_layers.snap @@ -0,0 +1,15 @@ +--- +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(LevelFilter::Debug), + b: Some(LevelFilter::Debug), + c: Some(vec![LevelFilter::Debug]), + }), + ) +} diff --git a/crates/macros/tests/config_test.rs b/crates/macros/tests/config_test.rs index feea54ee..c08ea11f 100644 --- a/crates/macros/tests/config_test.rs +++ b/crates/macros/tests/config_test.rs @@ -96,6 +96,114 @@ mod named_struct { } } +// https://github.com/moonrepo/schematic/issues/173 +mod default_values { + use super::*; + use schematic::DefaultValueResult; + use serde::Deserialize; + + #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, schematic::Schematic)] + pub enum LevelFilter { + #[default] + Off, + Debug, + } + + #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, schematic::Schematic)] + pub struct Point { + pub x: usize, + pub y: usize, + } + + impl Point { + pub const ORIGIN: Point = Point { x: 0, y: 0 }; + } + + pub mod limits { + pub const MAX: usize = 99; + } + + fn pick_level(_: &C) -> DefaultValueResult { + Ok(Some(LevelFilter::Debug)) + } + + #[derive(Debug, Config)] + pub struct Values { + #[setting(default = LevelFilter::Debug)] + variant: LevelFilter, + #[setting(default = Point::ORIGIN)] + assoc_const: Point, + #[setting(default = limits::MAX)] + path_const: usize, + #[setting(default = Point { x: 1, y: 2 })] + literal: Point, + #[setting(default = Point { x: 3, ..Point::ORIGIN })] + literal_with_rest: Point, + } + + #[test] + fn uses_a_path_that_names_a_value() { + let config = Values::default(); + + assert_eq!(config.variant, LevelFilter::Debug); + assert_eq!(config.assoc_const, Point { x: 0, y: 0 }); + assert_eq!(config.path_const, 99); + } + + #[test] + fn uses_a_struct_literal() { + let config = Values::default(); + + assert_eq!(config.literal, Point { x: 1, y: 2 }); + assert_eq!(config.literal_with_rest, Point { x: 3, y: 0 }); + } + + #[derive(Debug, Config)] + pub struct Handlers { + #[setting(default = pick_level)] + via_fn: LevelFilter, + } + + // A `snake_case` path is still called with the context, which is what + // tells the two apart + #[test] + fn still_calls_a_handler_function() { + assert_eq!(Handlers::default().via_fn, LevelFilter::Debug); + } + + #[derive(Debug, Config)] + pub struct Layered { + #[setting(default = LevelFilter::Debug)] + optional: Option, + #[setting(default = LevelFilter::Debug)] + boxed: Box, + #[setting(default = vec![LevelFilter::Debug])] + list: Vec, + } + + #[test] + fn wraps_a_value_in_its_layers() { + let config = Layered::default(); + + assert_eq!(config.optional, Some(LevelFilter::Debug)); + assert_eq!(*config.boxed, LevelFilter::Debug); + assert_eq!(config.list, vec![LevelFilter::Debug]); + } + + // A default is the lowest layer, so a configured value still wins + #[test] + fn a_source_still_overrides_it() { + let result = ConfigLoader::::new() + .code(r#"{"variant": "Off"}"#, "code.json") + .unwrap() + .load() + .unwrap(); + + assert_eq!(result.config.variant, LevelFilter::Off); + assert_eq!(result.config.literal, Point { x: 1, y: 2 }); + } +} + mod merging { use super::*; diff --git a/crates/schematic/src/validate/mod.rs b/crates/schematic/src/validate/mod.rs index 7a2ed13a..f27eab7a 100644 --- a/crates/schematic/src/validate/mod.rs +++ b/crates/schematic/src/validate/mod.rs @@ -8,6 +8,7 @@ mod number; mod string; #[cfg(feature = "validate_url")] mod url; +mod uuid; pub use crate::config::{ValidateError, ValidateResult, Validator}; #[cfg(feature = "validate_email")] @@ -20,6 +21,7 @@ pub use number::*; pub use string::*; #[cfg(feature = "validate_url")] pub use url::*; +pub use uuid::*; pub(crate) fn map_err(error: garde::Error) -> ValidateError { ValidateError::new(error.to_string()) diff --git a/crates/schematic/src/validate/uuid.rs b/crates/schematic/src/validate/uuid.rs new file mode 100644 index 00000000..f3f4fda4 --- /dev/null +++ b/crates/schematic/src/validate/uuid.rs @@ -0,0 +1,144 @@ +use super::{ValidateError, ValidateResult}; + +/// Length of a UUID in its canonical form: 32 hexadecimal digits plus the +/// 4 hyphens that group them. +const LENGTH: usize = 36; + +/// How many digits each hyphen separated group holds. +const GROUPS: [usize; 5] = [8, 4, 4, 4, 12]; + +/// Validate a string is a UUID in the canonical hyphenated form, such as +/// `67e55044-10b1-426f-9247-bb680e5fe0c8`. Digits may be upper or lower case. +/// +/// Only the shape is checked, not the version and variant bits, so the nil +/// UUID and any non-standard version are both accepted. +pub fn uuid, D, C>( + value: &T, + _data: &D, + _context: &C, + _finalize: bool, +) -> ValidateResult { + let value = value.as_ref(); + let length = value.chars().count(); + + if length != LENGTH { + return Err(ValidateError::new(format!( + "not a valid UUID: expected {LENGTH} characters, found {length}" + ))); + } + + let grouping = || { + ValidateError::new("not a valid UUID: expected 5 groups of 8-4-4-4-12 hexadecimal digits") + }; + let mut groups = value.split('-'); + + for expected in GROUPS { + let Some(group) = groups.next() else { + return Err(grouping()); + }; + + // Characters are checked before the size so that a stray one is named, + // rather than being reported as a group of the wrong length + if let Some(char) = group.chars().find(|char| !char.is_ascii_hexdigit()) { + return Err(ValidateError::new(format!( + "not a valid UUID: invalid character `{char}`" + ))); + } + + // Every character is now known to be ASCII, so bytes count as characters + if group.len() != expected { + return Err(grouping()); + } + } + + // Five groups of the right size, plus their hyphens, account for all 36 + // characters, so there can be no sixth + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn check(value: &str) -> ValidateResult { + uuid(&value, &(), &(), false) + } + + // `Display` prefixes the setting path, which is empty here + fn message(value: &str) -> String { + check(value).unwrap_err().message + } + + #[test] + fn accepts_canonical_uuids() { + assert!(check("67e55044-10b1-426f-9247-bb680e5fe0c8").is_ok()); + // v1 through v8, and versions we don't know about, are all shapes + assert!(check("2c5ea4c0-4067-11e9-8bad-9b1deb4d3b7d").is_ok()); + assert!(check("01890a5d-ac96-774b-bcce-b302099a8057").is_ok()); + } + + #[test] + fn accepts_either_case() { + assert!(check("67E55044-10B1-426F-9247-BB680E5FE0C8").is_ok()); + assert!(check("67e55044-10B1-426f-9247-BB680e5fe0c8").is_ok()); + } + + #[test] + fn accepts_the_nil_and_max_uuids() { + assert!(check("00000000-0000-0000-0000-000000000000").is_ok()); + assert!(check("ffffffff-ffff-ffff-ffff-ffffffffffff").is_ok()); + } + + #[test] + fn rejects_the_wrong_length() { + assert_eq!( + message(""), + "not a valid UUID: expected 36 characters, found 0" + ); + // One digit short + assert_eq!( + message("67e55044-10b1-426f-9247-bb680e5fe0c"), + "not a valid UUID: expected 36 characters, found 35" + ); + // Counted in characters, not bytes + assert_eq!( + message("éé7e55044-10b1-426f-9247-bb680e5fe0c8"), + "not a valid UUID: expected 36 characters, found 37" + ); + } + + #[test] + fn rejects_the_wrong_grouping() { + let expected = "not a valid UUID: expected 5 groups of 8-4-4-4-12 hexadecimal digits"; + + // Hyphens in the wrong places + assert_eq!(message("67e5504-410b1-426f-9247-bb680e5fe0c8"), expected); + // No hyphens at all, padded to the right length + assert_eq!(message("67e5504410b1426f9247bb680e5fe0c8----"), expected); + // An extra group, at the cost of a digit + assert_eq!(message("67e55044-10b1-426f-9247-bb680e5fe0-8"), expected); + } + + #[test] + fn rejects_non_hexadecimal_digits() { + assert_eq!( + message("g7e55044-10b1-426f-9247-bb680e5fe0c8"), + "not a valid UUID: invalid character `g`" + ); + // A multi byte character is 1 character, so the length still matches + assert_eq!( + message("é7e55044-10b1-426f-9247-bb680e5fe0c8"), + "not a valid UUID: invalid character `é`" + ); + } + + #[test] + fn rejects_other_common_forms() { + // Braced + assert!(check("{67e55044-10b1-426f-9247-bb680e5fe0c8}").is_err()); + // URN + assert!(check("urn:uuid:67e55044-10b1-426f-9247-bb680e5fe0c8").is_err()); + // Simple, without hyphens + assert!(check("67e5504410b1426f9247bb680e5fe0c8").is_err()); + } +} diff --git a/crates/schematic/tests/validate_test.rs b/crates/schematic/tests/validate_test.rs index 0358a758..1efac7a6 100644 --- a/crates/schematic/tests/validate_test.rs +++ b/crates/schematic/tests/validate_test.rs @@ -93,6 +93,8 @@ pub struct ValidateFuncs { url: String, #[setting(validate = validate::url_secure)] url_secure: String, + #[setting(validate = validate::uuid)] + uuid: String, #[setting(validate = validate::in_range(1, 5))] range: i32, #[setting(validate = validate::extends_string)] @@ -114,7 +116,7 @@ fn runs_the_validator_funcs() { assert_eq!( error.to_full_string(), - "Failed to validate ValidateFuncs. \n contains: does not contain \"foo\"\n email: not a valid email: value is empty\n ip: not a valid IP address\n ip_v4: not a valid IPv4 address\n ip_v6: not a valid IPv6 address\n regex: does not match pattern /^foo$/\n min: length is lower than 1\n len: length is lower than 1\n url: not a valid url: relative URL without a base\n url_secure: not a valid url: relative URL without a base\n range: lower than 1\n ext_str: only file paths and URLs can be extended" + "Failed to validate ValidateFuncs. \n contains: does not contain \"foo\"\n email: not a valid email: value is empty\n ip: not a valid IP address\n ip_v4: not a valid IPv4 address\n ip_v6: not a valid IPv6 address\n regex: does not match pattern /^foo$/\n min: length is lower than 1\n len: length is lower than 1\n url: not a valid url: relative URL without a base\n url_secure: not a valid url: relative URL without a base\n uuid: not a valid UUID: expected 36 characters, found 0\n range: lower than 1\n ext_str: only file paths and URLs can be extended" ) }