Skip to main content

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}