reinhardt_conf/settings/fragment.rs
1//! Composable settings fragment trait
2//!
3//! Defines the [`SettingsFragment`] trait that root settings fragments implement.
4//! Each root fragment maps to a TOML section and can be validated independently.
5//!
6//! # TOML section mapping
7//!
8//! Each fragment's [`SettingsFragment::section()`] determines which TOML section it
9//! deserializes from. For example, `CoreSettings` (section `"core"`) reads from `[core]`,
10//! and `I18nSettings` (section `"i18n"`) reads from `[i18n]`.
11//!
12//! # Nested structs within fragments
13//!
14//! When a fragment contains nested structs (e.g., `CoreSettings.security`), the
15//! nested struct maps to a TOML sub-section named after the field:
16//!
17//! ```toml
18//! [core]
19//! secret_key = "..."
20//! debug = false
21//!
22//! [core.security]
23//! secure_ssl_redirect = true
24//! session_cookie_secure = true
25//! ```
26//!
27//! In the legacy `Settings` format (where `CoreSettings` is flattened at the root),
28//! the sub-section becomes a top-level section:
29//!
30//! ```toml
31//! secret_key = "..."
32//! debug = false
33//!
34//! [security]
35//! secure_ssl_redirect = true
36//! session_cookie_secure = true
37//! ```
38//!
39//! # Typed schema references
40//!
41//! Composed settings expose a typed schema through `ProjectSettings::schema()`.
42//! Embedded settings nodes can be followed with field access, for example
43//! `ProjectSettings::schema().database.default.password`. The rendered path is
44//! composed from the root composition key, the embedded field key, and serde
45//! rename attributes, such as `database.default.db-password`.
46//!
47//! Schema generation peels semantically agnostic wrappers when producing nested
48//! references: `Option<T>`, `Vec<T>`, `HashMap<String, T>`,
49//! `BTreeMap<String, T>`, `IndexMap<String, T>`, and `Box<T>`.
50//! `#[setting(node)]` forces a nested settings node, while `#[setting(leaf)]`
51//! keeps a field as a leaf. Without a shape hint, `*Config` types may infer node
52//! behavior; `*Settings` types should be annotated explicitly unless they are
53//! built-in fragments annotated by the crate.
54//!
55//! Recursive required-field validation reports missing nested required leaves as
56//! [`BuildError::MissingRequiredPath`](super::builder::BuildError::MissingRequiredPath).
57//! A missing direct field on the fragment section remains
58//! [`BuildError::MissingRequiredField`](super::builder::BuildError::MissingRequiredField).
59//!
60//! A struct annotated with `#[settings(fragment = true)]` but no `section = "..."`
61//! is an embedded settings node. It implements
62//! [`SettingsNode`](super::schema::SettingsNode) for recursive schema support,
63//! but not [`SettingsFragment`], because it is not a top-level composition
64//! section.
65//!
66//! # Merge semantics
67//!
68//! When multiple TOML files are merged (e.g., `base.toml` + `local.toml`),
69//! the resulting layout depends on the
70//! [`MergeStrategy`](super::builder::MergeStrategy) chosen on the
71//! [`SettingsBuilder`](super::builder::SettingsBuilder):
72//!
73//! - With [`MergeStrategy::Shallow`](super::builder::MergeStrategy::Shallow)
74//! — the legacy default for
75//! [`build`](super::builder::SettingsBuilder::build) — each top-level
76//! section is replaced as a whole. If `local.toml` defines `[core]`, it
77//! replaces the entire `[core]` section from `base.toml`, so
78//! environment-specific files must be self-contained for any section
79//! they touch.
80//! - With [`MergeStrategy::Deep`](super::builder::MergeStrategy::Deep)
81//! — the default for
82//! [`build_composed`](super::builder::SettingsBuilder::build_composed)
83//! — nested tables are merged recursively. Defining `[core].debug =
84//! true` in `local.toml` no longer erases sibling fields like
85//! `[core].secret_key` or sub-sections like `[core.security]` that were
86//! set in `base.toml`. Arrays and scalars are still replaced
87//! wholesale.
88//!
89//! See [issue #4260](https://github.com/kent8192/reinhardt-web/issues/4260)
90//! for the design discussion.
91
92use super::policy::FieldPolicy;
93use super::profile::Profile;
94use super::validation::ValidationResult;
95use serde::Serialize;
96use serde::de::DeserializeOwned;
97use std::fmt::Debug;
98
99/// Profile-aware validation for settings fragments.
100///
101/// Implement this trait to add custom validation logic to a settings fragment.
102/// The `#[settings(fragment = true)]` macro generates a default (no-op) implementation
103/// automatically. To provide custom validation, use `validate = false` in the macro
104/// and implement this trait manually:
105///
106/// ```ignore
107/// #[settings(fragment = true, section = "security", validate = false)]
108/// struct SecuritySettings { /* ... */ }
109///
110/// impl SettingsValidation for SecuritySettings {
111/// fn validate(&self, profile: &Profile) -> ValidationResult {
112/// // custom validation logic
113/// Ok(())
114/// }
115/// }
116/// ```
117pub trait SettingsValidation {
118 /// Validate this fragment against the given profile.
119 ///
120 /// Default implementation: no-op (always valid).
121 fn validate(&self, _profile: &Profile) -> ValidationResult {
122 Ok(())
123 }
124}
125
126/// A rootable composable unit of configuration.
127///
128/// Each root fragment maps to a TOML section and can be validated independently.
129/// Root fragments are composed into a `ProjectSettings` struct using the
130/// `#[settings(key: Type | Type)]` macro.
131///
132/// # Implementing
133///
134/// Use `#[settings(fragment = true, section = "...")]` to auto-derive this trait,
135/// or implement it manually for custom validation. Omit `section = "..."`
136/// only for embedded settings nodes that should not be composed as root
137/// fragments.
138pub trait SettingsFragment:
139 Clone + Debug + Serialize + DeserializeOwned + Send + Sync + 'static
140{
141 /// The accessor trait for this fragment.
142 ///
143 /// Expresses the type-level association between a settings fragment
144 /// and its `Has*Settings` accessor trait. Use `()` for fragments
145 /// without a dedicated accessor trait.
146 type Accessor: ?Sized;
147
148 /// TOML section name (e.g., `"cache"`, `"core"`).
149 fn section() -> &'static str;
150
151 /// Validate this fragment against the given profile.
152 ///
153 /// Default implementation: no-op (always valid).
154 /// For custom validation, implement [`SettingsValidation`] and override
155 /// this method to delegate to it.
156 fn validate(&self, _profile: &Profile) -> ValidationResult {
157 Ok(())
158 }
159
160 /// Returns the default field policies defined by the library author.
161 ///
162 /// Generated by the `#[settings(fragment = true, section = "...")]` macro
163 /// from `#[setting(...)]` field attributes. Returns empty slice by default
164 /// for backward compatibility with existing fragments.
165 fn field_policies() -> &'static [FieldPolicy] {
166 &[]
167 }
168}
169
170/// Generic accessor trait for settings fragments.
171///
172/// The `#[settings]` macro generates implementations of this trait
173/// using fully-qualified paths, so users do not need to manually
174/// import individual `Has*Settings` traits.
175///
176/// Fragment macros bridge `HasSettings<F>` to the specific
177/// `Has*Settings` traits for each generated fragment.
178pub trait HasSettings<F: SettingsFragment> {
179 /// Returns a reference to the contained fragment.
180 fn get_settings(&self) -> &F;
181}
182
183/// Marker trait bundling the commonly required fragment accessors used by
184/// management commands and the database layer.
185///
186/// A composed settings type that derives `HasCoreSettings` and
187/// `HasContactSettings` (i.e. it includes both `CoreSettings` and
188/// `ContactSettings` fragments) automatically satisfies this trait via
189/// the blanket implementation below.
190///
191/// Consumers that need an erased handle to settings (for example
192/// `CommandContext`) hold an `Arc<dyn HasCommonSettings>` instead of a
193/// concrete type, enabling cross-crate trait-object plumbing without
194/// leaking the legacy `Settings` type.
195pub trait HasCommonSettings:
196 super::core_settings::HasCoreSettings + super::contacts::HasContactSettings + Send + Sync + 'static
197{
198}
199
200impl<T> HasCommonSettings for T where
201 T: super::core_settings::HasCoreSettings
202 + super::contacts::HasContactSettings
203 + Send
204 + Sync
205 + 'static
206{
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::settings::profile::Profile;
213 use rstest::rstest;
214
215 #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
216 struct TestFragment {
217 pub value: String,
218 }
219
220 impl SettingsFragment for TestFragment {
221 type Accessor = ();
222
223 fn section() -> &'static str {
224 "test"
225 }
226 }
227
228 #[rstest]
229 fn test_settings_fragment_section() {
230 // Arrange
231 // (no setup needed)
232
233 // Act
234 let section = TestFragment::section();
235
236 // Assert
237 assert_eq!(section, "test");
238 }
239
240 #[rstest]
241 fn test_settings_fragment_validate_default_ok() {
242 // Arrange
243 let fragment = TestFragment {
244 value: "hello".to_string(),
245 };
246 let profile = Profile::Development;
247
248 // Act
249 let result = fragment.validate(&profile);
250
251 // Assert
252 assert!(result.is_ok());
253 }
254}