Skip to main content

reinhardt_conf/settings/
core_settings.rs

1//! Core settings fragment
2//!
3//! Essential configuration shared by all reinhardt applications.
4
5use super::database_config::DatabaseConfig;
6use super::fragment::SettingsValidation;
7use super::profile::Profile;
8use super::security::SecuritySettings;
9use super::validation::{ValidationError, ValidationResult};
10use reinhardt_core::macros::settings;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::path::PathBuf;
14
15/// Core application settings.
16///
17/// Contains essential configuration: base directory, secret key, debug mode,
18/// allowed hosts, database configs, security settings, middleware, and apps.
19#[settings(fragment = true, section = "core", validate = false)]
20#[derive(Clone, Debug, Serialize, Deserialize)]
21pub struct CoreSettings {
22	/// Base directory of the project.
23	#[serde(default = "default_base_dir")]
24	pub base_dir: PathBuf,
25	/// Secret key for cryptographic signing.
26	#[setting(required)]
27	pub secret_key: String,
28	/// Debug mode flag.
29	#[serde(default = "default_debug")]
30	pub debug: bool,
31	/// List of allowed host/domain names.
32	#[serde(default)]
33	pub allowed_hosts: Vec<String>,
34	/// Database configurations keyed by alias.
35	#[serde(default = "default_databases")]
36	pub databases: HashMap<String, DatabaseConfig>,
37	/// Security settings (nested sub-section).
38	///
39	/// In TOML, security fields are placed under a `[core.security]` sub-section
40	/// (composable settings) or `[security]` section (legacy `Settings`):
41	///
42	/// ```toml
43	/// # Composable settings format
44	/// [core]
45	/// secret_key = "..."
46	/// debug = false
47	///
48	/// [core.security]
49	/// secure_ssl_redirect = true
50	/// session_cookie_secure = true
51	/// ```
52	///
53	/// ```toml
54	/// # Legacy Settings format (CoreSettings flattened at root)
55	/// secret_key = "..."
56	/// debug = false
57	///
58	/// [security]
59	/// secure_ssl_redirect = true
60	/// session_cookie_secure = true
61	/// ```
62	#[setting(node)]
63	#[serde(default)]
64	pub security: SecuritySettings,
65	/// Middleware class paths.
66	#[serde(default)]
67	pub middleware: Vec<String>,
68	/// Root URL configuration module.
69	#[serde(default)]
70	pub root_urlconf: String,
71	/// List of installed application paths.
72	#[serde(default)]
73	pub installed_apps: Vec<String>,
74}
75
76fn default_base_dir() -> PathBuf {
77	PathBuf::from(".")
78}
79
80fn default_debug() -> bool {
81	true
82}
83
84fn default_databases() -> HashMap<String, DatabaseConfig> {
85	let mut map = HashMap::new();
86	map.insert("default".to_string(), DatabaseConfig::default());
87	map
88}
89
90impl Default for CoreSettings {
91	fn default() -> Self {
92		Self {
93			base_dir: default_base_dir(),
94			secret_key: String::new(),
95			debug: true,
96			allowed_hosts: Vec::new(),
97			databases: default_databases(),
98			security: SecuritySettings::default(),
99			middleware: Vec::new(),
100			root_urlconf: String::new(),
101			installed_apps: Vec::new(),
102		}
103	}
104}
105
106impl SettingsValidation for CoreSettings {
107	fn validate(&self, profile: &Profile) -> ValidationResult {
108		if self.secret_key.is_empty() {
109			return Err(ValidationError::MissingRequired("secret_key".to_string()));
110		}
111
112		if profile.is_production() {
113			if self.debug {
114				return Err(ValidationError::Security(
115					"debug must be false in production".to_string(),
116				));
117			}
118			if self.allowed_hosts.is_empty() {
119				return Err(ValidationError::MissingRequired(
120					"allowed_hosts".to_string(),
121				));
122			}
123		}
124
125		// Delegate to nested security fragment
126		self.security.validate(profile)?;
127
128		Ok(())
129	}
130}
131
132#[cfg(test)]
133mod tests {
134	use super::{CoreSettings, SecuritySettings};
135	use crate::settings::fragment::SettingsFragment;
136	use crate::settings::profile::Profile;
137	use rstest::rstest;
138
139	#[rstest]
140	fn test_core_settings_section() {
141		// Arrange / Act / Assert
142		assert_eq!(CoreSettings::section(), "core");
143	}
144
145	#[rstest]
146	fn test_core_settings_default() {
147		// Arrange / Act
148		let settings = CoreSettings::default();
149
150		// Assert
151		assert!(settings.debug);
152		assert!(settings.secret_key.is_empty());
153		assert!(settings.allowed_hosts.is_empty());
154		assert!(settings.databases.contains_key("default"));
155		assert!(!settings.security.secure_ssl_redirect);
156	}
157
158	#[rstest]
159	fn test_core_settings_validate_missing_secret_key() {
160		// Arrange
161		let settings = CoreSettings::default();
162
163		// Act
164		let result = settings.validate(&Profile::Development);
165
166		// Assert
167		assert!(result.is_err());
168	}
169
170	#[rstest]
171	fn test_core_settings_validate_development_ok() {
172		// Arrange
173		let settings = CoreSettings {
174			secret_key: "test-secret-key".to_string(),
175			..Default::default()
176		};
177
178		// Act
179		let result = settings.validate(&Profile::Development);
180
181		// Assert
182		assert!(result.is_ok());
183	}
184
185	#[rstest]
186	fn test_core_settings_validate_production_debug_fails() {
187		// Arrange
188		let settings = CoreSettings {
189			secret_key: "production-secret-key".to_string(),
190			debug: true,
191			..Default::default()
192		};
193
194		// Act
195		let result = settings.validate(&Profile::Production);
196
197		// Assert
198		assert!(result.is_err());
199	}
200
201	#[rstest]
202	fn test_core_settings_validate_production_ok() {
203		// Arrange
204		let settings = CoreSettings {
205			secret_key: "production-secret-key".to_string(),
206			debug: false,
207			allowed_hosts: vec!["example.com".to_string()],
208			security: SecuritySettings {
209				secure_ssl_redirect: true,
210				session_cookie_secure: true,
211				csrf_cookie_secure: true,
212				..Default::default()
213			},
214			..Default::default()
215		};
216
217		// Act
218		let result = settings.validate(&Profile::Production);
219
220		// Assert
221		assert!(result.is_ok());
222	}
223
224	#[rstest]
225	fn test_core_settings_delegates_security_validation() {
226		// Arrange - security validation fails in production
227		let settings = CoreSettings {
228			secret_key: "production-secret-key".to_string(),
229			debug: false,
230			allowed_hosts: vec!["example.com".to_string()],
231			security: SecuritySettings::default(), // SSL redirect is false
232			..Default::default()
233		};
234
235		// Act
236		let result = settings.validate(&Profile::Production);
237
238		// Assert
239		assert!(result.is_err());
240	}
241
242	#[rstest]
243	fn test_core_settings_field_policies_secret_key_required() {
244		use crate::settings::policy::FieldRequirement;
245
246		// Arrange / Act
247		let policies = CoreSettings::field_policies();
248
249		// Assert: secret_key must be marked as Required with no default
250		let secret_key_policy = policies.iter().find(|p| p.name == "secret_key");
251		assert!(
252			secret_key_policy.is_some(),
253			"secret_key must have an explicit field policy"
254		);
255		let policy = secret_key_policy.unwrap();
256		assert_eq!(
257			policy.requirement,
258			FieldRequirement::Required,
259			"secret_key must be Required"
260		);
261		assert!(
262			!policy.has_default,
263			"secret_key must not have a default value"
264		);
265	}
266
267	#[rstest]
268	fn test_core_settings_deserialize_nested_security_section() {
269		// Arrange — composable settings format with [core.security] sub-section
270		let toml_str = r#"
271secret_key = "test-secret"
272debug = false
273
274[security]
275secure_ssl_redirect = true
276session_cookie_secure = true
277csrf_cookie_secure = true
278secure_hsts_seconds = 31536000
279secure_hsts_include_subdomains = true
280secure_hsts_preload = true
281"#;
282
283		// Act
284		let settings: CoreSettings = toml::from_str(toml_str).expect("failed to parse TOML");
285
286		// Assert — security fields parsed from nested section
287		assert_eq!(settings.secret_key, "test-secret");
288		assert!(!settings.debug);
289		assert!(settings.security.secure_ssl_redirect);
290		assert!(settings.security.session_cookie_secure);
291		assert!(settings.security.csrf_cookie_secure);
292		assert_eq!(settings.security.secure_hsts_seconds, Some(31536000));
293		assert!(settings.security.secure_hsts_include_subdomains);
294		assert!(settings.security.secure_hsts_preload);
295	}
296
297	#[rstest]
298	fn test_core_settings_deserialize_omitted_security_uses_defaults() {
299		// Arrange — no security section at all
300		let toml_str = r#"
301secret_key = "test-secret"
302debug = true
303"#;
304
305		// Act
306		let settings: CoreSettings = toml::from_str(toml_str).expect("failed to parse TOML");
307
308		// Assert — security defaults applied
309		assert!(!settings.security.secure_ssl_redirect);
310		assert!(!settings.security.session_cookie_secure);
311		assert!(!settings.security.csrf_cookie_secure);
312		assert_eq!(settings.security.secure_hsts_seconds, None);
313	}
314
315	#[rstest]
316	fn test_core_settings_deserialize_partial_security_section() {
317		// Arrange — only some security fields specified
318		let toml_str = r#"
319secret_key = "test-secret"
320
321[security]
322secure_ssl_redirect = true
323"#;
324
325		// Act
326		let settings: CoreSettings = toml::from_str(toml_str).expect("failed to parse TOML");
327
328		// Assert — specified field overridden, others use defaults
329		assert!(settings.security.secure_ssl_redirect);
330		assert!(!settings.security.session_cookie_secure);
331		assert!(!settings.security.csrf_cookie_secure);
332	}
333
334	#[rstest]
335	fn test_core_settings_field_policies_other_fields_optional() {
336		use crate::settings::policy::FieldRequirement;
337
338		// Arrange / Act
339		let policies = CoreSettings::field_policies();
340
341		// Assert: all fields except secret_key are Optional with defaults
342		let optional_fields = [
343			"base_dir",
344			"debug",
345			"allowed_hosts",
346			"databases",
347			"security",
348			"middleware",
349			"root_urlconf",
350			"installed_apps",
351		];
352		for field_name in optional_fields {
353			let policy = policies.iter().find(|p| p.name == field_name);
354			assert!(
355				policy.is_some(),
356				"field '{field_name}' must have a field policy"
357			);
358			let policy = policy.unwrap();
359			assert_eq!(
360				policy.requirement,
361				FieldRequirement::Optional,
362				"field '{field_name}' must be Optional"
363			);
364			assert!(
365				policy.has_default,
366				"field '{field_name}' must have a default value"
367			);
368		}
369	}
370}