Skip to main content

reinhardt_conf/
settings.rs

1//! # Settings Module
2//!
3//! Django-inspired settings system for Reinhardt projects.
4//! This module provides configuration management for Reinhardt applications.
5
6pub mod advanced;
7pub mod builder;
8pub mod cache;
9/// Trait for composed settings structs generated by the `#[settings(...)]` macro.
10pub mod composed;
11pub mod contacts;
12pub mod core_settings;
13pub mod cors;
14pub mod email;
15pub mod env;
16pub mod env_loader;
17pub mod env_parser;
18pub mod fragment;
19pub mod i18n;
20pub mod interpolation;
21pub mod logging;
22pub mod media;
23pub(crate) mod merge;
24/// OpenAPI documentation endpoint configuration.
25pub mod openapi;
26/// Field-level policy types for settings fragments.
27pub mod policy;
28pub mod prelude;
29pub mod profile;
30pub mod secret_types;
31pub mod security;
32pub mod session;
33pub mod sources;
34pub mod static_files;
35pub mod template_settings;
36pub mod typed_deserializer;
37pub mod validation;
38
39// Dynamic settings (async feature required)
40#[cfg(feature = "async")]
41pub mod dynamic;
42
43#[cfg(feature = "async")]
44pub mod backends;
45
46/// Secret management with provider-based storage and rotation support.
47#[cfg(feature = "async")]
48pub mod secrets;
49
50#[cfg(feature = "encryption")]
51pub mod encryption;
52
53#[cfg(feature = "async")]
54pub mod audit;
55
56#[cfg(feature = "hot-reload")]
57pub mod hot_reload;
58
59/// Additional application-level configuration types.
60pub mod config;
61/// Database connection configuration types and helpers.
62pub mod database_config;
63/// Settings documentation and introspection utilities.
64pub mod docs;
65/// Test utilities for settings configuration.
66pub mod testing;
67
68use core_settings::CoreSettings;
69use fragment::HasSettings;
70use reinhardt_utils::staticfiles::storage::StaticFilesConfig;
71use serde::{Deserialize, Serialize};
72use std::collections::HashMap;
73use std::path::PathBuf;
74
75/// Contact information for administrators and managers
76///
77/// Used for error notifications, broken link notifications, etc.
78///
79/// # Examples
80///
81/// ```
82/// use reinhardt_conf::settings::Contact;
83///
84/// let admin = Contact::new("John Doe", "john@example.com");
85/// assert_eq!(admin.name, "John Doe");
86/// assert_eq!(admin.email, "john@example.com");
87/// ```
88#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
89pub struct Contact {
90	/// Person's name
91	pub name: String,
92	/// Email address
93	pub email: String,
94}
95
96impl Contact {
97	/// Create a new contact
98	///
99	/// # Examples
100	///
101	/// ```
102	/// use reinhardt_conf::settings::Contact;
103	///
104	/// let contact = Contact::new("Alice Smith", "alice@example.com");
105	/// assert_eq!(contact.name, "Alice Smith");
106	/// assert_eq!(contact.email, "alice@example.com");
107	/// ```
108	pub fn new(name: impl Into<String>, email: impl Into<String>) -> Self {
109		Self {
110			name: name.into(),
111			email: email.into(),
112		}
113	}
114}
115
116// Re-export from advanced module
117#[allow(deprecated)]
118pub use advanced::{
119	AdvancedSettings, CacheSettings, CorsSettings, DatabaseSettings as AdvancedDatabaseSettings,
120	EmailSettings, LoggingSettings, MediaSettings, SessionSettings, SettingsError, StaticSettings,
121};
122
123/// Main settings structure for a Reinhardt project
124///
125/// **Deprecated since 0.1.0-rc.16**: Use [`CoreSettings`] fragment with
126/// `ProjectSettings` instead. This struct is retained as a migration bridge
127/// and implements `HasCoreSettings` so existing code can be gradually
128/// moved to the composable settings system.
129#[deprecated(
130	since = "0.1.0-rc.16",
131	note = "use CoreSettings fragment with ProjectSettings instead"
132)]
133#[non_exhaustive]
134#[derive(Clone, Debug, Serialize, Deserialize)]
135pub struct Settings {
136	/// Core settings (flattened for backward-compatible TOML deserialization).
137	#[serde(flatten)]
138	pub core: CoreSettings,
139
140	/// Template engine configurations.
141	///
142	/// **Note:** Currently not consumed by the framework. Reserved for future
143	/// template engine integration. Setting this value has no effect on framework
144	/// behavior.
145	pub templates: Vec<TemplateConfig>,
146
147	/// Static files URL prefix.
148	pub static_url: String,
149
150	/// Static files root directory.
151	pub static_root: Option<PathBuf>,
152
153	/// Additional static files directories (STATICFILES_DIRS).
154	pub staticfiles_dirs: Vec<PathBuf>,
155
156	/// Media files URL prefix.
157	pub media_url: String,
158
159	/// Media files root directory.
160	pub media_root: Option<PathBuf>,
161
162	/// Language code for internationalization.
163	///
164	/// **Note:** Currently not consumed by the framework. Reserved for future
165	/// i18n implementation. Setting this value has no effect on framework behavior.
166	pub language_code: String,
167
168	/// Time zone for datetime handling.
169	///
170	/// **Note:** Currently not consumed by the framework. Reserved for future
171	/// timezone support implementation. Setting this value has no effect on
172	/// framework behavior.
173	pub time_zone: String,
174
175	/// Enable internationalization.
176	///
177	/// **Note:** Currently not consumed by the framework. Reserved for future
178	/// i18n implementation. Setting this value has no effect on framework behavior.
179	pub use_i18n: bool,
180
181	/// Use timezone-aware datetimes.
182	///
183	/// **Note:** Currently not consumed by the framework. Reserved for future
184	/// timezone support implementation. Setting this value has no effect on
185	/// framework behavior.
186	pub use_tz: bool,
187
188	/// Default auto field type for models.
189	///
190	/// **Note:** Currently not consumed by the framework. Reserved for future
191	/// auto field configuration. Setting this value has no effect on framework
192	/// behavior.
193	pub default_auto_field: String,
194
195	/// List of administrators who receive error notifications.
196	/// Django equivalent: ADMINS = [('name', 'email'), ...]
197	pub admins: Vec<Contact>,
198
199	/// List of managers who receive broken link notifications, etc.
200	/// Django equivalent: MANAGERS = [('name', 'email'), ...]
201	pub managers: Vec<Contact>,
202}
203
204#[allow(deprecated)] // Internal: Settings is deprecated but we still need to implement methods
205impl Settings {
206	/// Create a new Settings instance with default values
207	///
208	/// # Examples
209	///
210	/// ```
211	/// use reinhardt_conf::settings::Settings;
212	/// use std::path::PathBuf;
213	///
214	/// #[allow(deprecated)]
215	/// let settings = Settings::new(
216	///     PathBuf::from("/app"),
217	///     "my-secret-key-12345".to_string()
218	/// );
219	///
220	/// assert_eq!(settings.core.base_dir, PathBuf::from("/app"));
221	/// assert_eq!(settings.core.secret_key, "my-secret-key-12345");
222	/// assert!(settings.core.debug);
223	/// assert_eq!(settings.time_zone, "UTC");
224	/// assert!(settings.core.installed_apps.is_empty());
225	/// ```
226	pub fn new(base_dir: PathBuf, secret_key: String) -> Self {
227		Self {
228			core: CoreSettings {
229				base_dir,
230				secret_key,
231				debug: true,
232				..Default::default()
233			},
234			templates: vec![TemplateConfig::default()],
235			static_url: "/static/".to_string(),
236			static_root: None,
237			staticfiles_dirs: vec![],
238			media_url: "/media/".to_string(),
239			media_root: None,
240			language_code: "en-us".to_string(),
241			time_zone: "UTC".to_string(),
242			use_i18n: true,
243			use_tz: true,
244			default_auto_field: "reinhardt.db.models.BigAutoField".to_string(),
245			admins: vec![],
246			managers: vec![],
247		}
248	}
249
250	/// Add an installed app
251	///
252	/// **Deprecated since 0.2.0**: Use `installed_apps!` macro with `ApplicationBuilder` instead.
253	///
254	/// # Examples
255	///
256	/// ```
257	/// use reinhardt_conf::settings::Settings;
258	///
259	/// #[allow(deprecated)]
260	/// let mut settings = Settings::default();
261	/// #[allow(deprecated)]
262	/// {
263	///     let initial_count = settings.core.installed_apps.len();
264	///     settings.add_app("myapp");
265	///
266	///     assert_eq!(settings.core.installed_apps.len(), initial_count + 1);
267	///     assert!(settings.core.installed_apps.contains(&"myapp".to_string()));
268	/// }
269	/// ```
270	#[deprecated(
271		since = "0.1.0-rc.16",
272		note = "Use `installed_apps!` macro with `ApplicationBuilder` instead"
273	)]
274	pub fn add_app(&mut self, app: impl Into<String>) {
275		self.core.installed_apps.push(app.into());
276	}
277
278	/// Create settings with a compile-time validated app list
279	///
280	/// This method accepts a function that returns `Vec<String>` generated by the
281	/// `installed_apps!` macro, providing compile-time validation of app names.
282	///
283	/// **Deprecated since 0.2.0**: Use `installed_apps!` macro with `ApplicationBuilder` instead.
284	///
285	/// # Examples
286	///
287	/// ```
288	/// use reinhardt_conf::settings::Settings;
289	///
290	/// #[allow(deprecated)]
291	/// let settings = Settings::default()
292	///     .with_validated_apps(|| vec![
293	///         "reinhardt.contrib.admin".to_string(),
294	///         "myapp".to_string(),
295	///     ]);
296	///
297	/// #[allow(deprecated)]
298	/// {
299	///     assert_eq!(settings.core.installed_apps.len(), 2);
300	///     assert!(settings.core.installed_apps.contains(&"myapp".to_string()));
301	/// }
302	/// ```
303	#[deprecated(
304		since = "0.1.0-rc.16",
305		note = "Use `installed_apps!` macro with `ApplicationBuilder` instead"
306	)]
307	pub fn with_validated_apps<F>(mut self, app_provider: F) -> Self
308	where
309		F: FnOnce() -> Vec<String>,
310	{
311		self.core.installed_apps = app_provider();
312		self
313	}
314
315	/// Add an administrator
316	///
317	/// # Examples
318	///
319	/// ```
320	/// use reinhardt_conf::settings::{Settings, Contact};
321	///
322	/// #[allow(deprecated)]
323	/// let mut settings = Settings::default();
324	/// settings.add_admin("John Doe", "john@example.com");
325	///
326	/// assert_eq!(settings.admins.len(), 1);
327	/// assert_eq!(settings.admins[0].name, "John Doe");
328	/// assert_eq!(settings.admins[0].email, "john@example.com");
329	/// ```
330	pub fn add_admin(&mut self, name: impl Into<String>, email: impl Into<String>) {
331		self.admins.push(Contact::new(name, email));
332	}
333
334	/// Add a manager
335	///
336	/// # Examples
337	///
338	/// ```
339	/// use reinhardt_conf::settings::{Settings, Contact};
340	///
341	/// #[allow(deprecated)]
342	/// let mut settings = Settings::default();
343	/// settings.add_manager("Jane Smith", "jane@example.com");
344	///
345	/// assert_eq!(settings.managers.len(), 1);
346	/// assert_eq!(settings.managers[0].name, "Jane Smith");
347	/// assert_eq!(settings.managers[0].email, "jane@example.com");
348	/// ```
349	pub fn add_manager(&mut self, name: impl Into<String>, email: impl Into<String>) {
350		self.managers.push(Contact::new(name, email));
351	}
352
353	/// Set managers to be the same as administrators
354	///
355	/// This is a common pattern in Django projects where MANAGERS = ADMINS
356	///
357	/// # Examples
358	///
359	/// ```
360	/// use reinhardt_conf::settings::Settings;
361	///
362	/// #[allow(deprecated)]
363	/// let mut settings = Settings::default();
364	/// settings.add_admin("John Doe", "john@example.com");
365	/// settings.add_admin("Jane Smith", "jane@example.com");
366	/// settings.managers_from_admins();
367	///
368	/// assert_eq!(settings.managers.len(), 2);
369	/// assert_eq!(settings.managers, settings.admins);
370	/// ```
371	pub fn managers_from_admins(&mut self) {
372		self.managers = self.admins.clone();
373	}
374
375	/// Set administrators with a fluent API
376	///
377	/// # Examples
378	///
379	/// ```
380	/// use reinhardt_conf::settings::{Settings, Contact};
381	///
382	/// #[allow(deprecated)]
383	/// let settings = Settings::default()
384	///     .with_admins(vec![
385	///         Contact::new("John Doe", "john@example.com"),
386	///         Contact::new("Jane Smith", "jane@example.com"),
387	///     ]);
388	///
389	/// assert_eq!(settings.admins.len(), 2);
390	/// ```
391	pub fn with_admins(mut self, admins: Vec<Contact>) -> Self {
392		self.admins = admins;
393		self
394	}
395
396	/// Set managers with a fluent API
397	///
398	/// # Examples
399	///
400	/// ```
401	/// use reinhardt_conf::settings::{Settings, Contact};
402	///
403	/// #[allow(deprecated)]
404	/// let settings = Settings::default()
405	///     .with_managers(vec![
406	///         Contact::new("Alice Brown", "alice@example.com"),
407	///     ]);
408	///
409	/// assert_eq!(settings.managers.len(), 1);
410	/// ```
411	pub fn with_managers(mut self, managers: Vec<Contact>) -> Self {
412		self.managers = managers;
413		self
414	}
415
416	/// Convert Settings to `StaticFilesConfig`
417	///
418	/// This method extracts static files related configuration from Settings
419	/// and creates a `StaticFilesConfig` instance suitable for use with `CollectStaticCommand`.
420	///
421	/// # Returns
422	///
423	/// Returns `Ok(StaticFilesConfig)` if static_root is configured,
424	/// or `Err` if static_root is None.
425	///
426	/// # Examples
427	///
428	/// ```no_run
429	/// use reinhardt_conf::settings::Settings;
430	/// use std::path::PathBuf;
431	///
432	/// #[allow(deprecated)]
433	/// let settings = Settings::new(
434	///     PathBuf::from("/app"),
435	///     "secret".to_string()
436	/// );
437	///
438	/// let config = settings.get_static_config().unwrap();
439	/// assert_eq!(config.static_url, "/static/");
440	/// ```
441	pub fn get_static_config(&self) -> Result<StaticFilesConfig, String> {
442		let static_root = self
443			.static_root
444			.clone()
445			.ok_or_else(|| "STATIC_ROOT is not configured".to_string())?;
446
447		Ok(StaticFilesConfig {
448			static_root,
449			static_url: self.static_url.clone(),
450			staticfiles_dirs: self.staticfiles_dirs.clone(),
451			media_url: Some(self.media_url.clone()),
452		})
453	}
454}
455
456#[allow(deprecated)] // Internal: Settings is deprecated but we still need to implement Default
457impl Default for Settings {
458	fn default() -> Self {
459		Self::new(
460			PathBuf::from("."),
461			"insecure-change-this-in-production".to_string(),
462		)
463	}
464}
465
466#[allow(deprecated)] // Internal: Settings is deprecated but we still need the HasCoreSettings bridge
467impl HasSettings<CoreSettings> for Settings {
468	fn get_settings(&self) -> &CoreSettings {
469		&self.core
470	}
471}
472
473// Re-export DatabaseConfig from database_config module
474pub use database_config::DatabaseConfig;
475
476// Re-export policy types
477pub use policy::{FieldPolicy, FieldRequirement};
478
479// Re-export ComposedSettings trait
480pub use composed::ComposedSettings;
481
482// Re-export the merge strategy selector for SettingsBuilder. See issue #4260.
483pub use builder::MergeStrategy;
484
485/// Template engine configuration
486#[non_exhaustive]
487#[derive(Clone, Debug, Serialize, Deserialize)]
488pub struct TemplateConfig {
489	/// Template backend/engine
490	pub backend: String,
491
492	/// Directories to search for templates
493	pub dirs: Vec<PathBuf>,
494
495	/// Search for templates in app directories
496	pub app_dirs: bool,
497
498	/// Template engine options
499	pub options: HashMap<String, serde_json::Value>,
500}
501
502impl TemplateConfig {
503	/// Create a new template configuration
504	///
505	/// # Examples
506	///
507	/// ```
508	/// use reinhardt_conf::settings::TemplateConfig;
509	///
510	/// let config = TemplateConfig::new("reinhardt.template.backends.jinja2.Jinja2");
511	///
512	/// assert_eq!(config.backend, "reinhardt.template.backends.jinja2.Jinja2");
513	/// assert!(config.app_dirs);
514	/// assert_eq!(config.dirs.len(), 0);
515	/// ```
516	pub fn new(backend: impl Into<String>) -> Self {
517		Self {
518			backend: backend.into(),
519			dirs: vec![],
520			app_dirs: true,
521			options: HashMap::new(),
522		}
523	}
524	/// Add a template directory
525	///
526	/// # Examples
527	///
528	/// ```
529	/// use reinhardt_conf::settings::TemplateConfig;
530	/// use std::path::PathBuf;
531	///
532	/// let config = TemplateConfig::new("reinhardt.template.backends.jinja2.Jinja2")
533	///     .add_dir("/app/templates");
534	///
535	/// assert_eq!(config.dirs.len(), 1);
536	/// assert_eq!(config.dirs[0], PathBuf::from("/app/templates"));
537	/// ```
538	pub fn add_dir(mut self, dir: impl Into<PathBuf>) -> Self {
539		self.dirs.push(dir.into());
540		self
541	}
542}
543
544impl Default for TemplateConfig {
545	fn default() -> Self {
546		let mut options = HashMap::new();
547		options.insert(
548			"context_processors".to_string(),
549			serde_json::json!([
550				"reinhardt.template.context_processors.request",
551				"reinhardt.contrib.auth.context_processors.auth",
552				"reinhardt.contrib.messages.context_processors.messages",
553			]),
554		);
555
556		Self {
557			backend: "reinhardt.template.backends.jinja2.Jinja2".to_string(),
558			dirs: vec![],
559			app_dirs: true,
560			options,
561		}
562	}
563}
564
565/// Middleware configuration
566#[non_exhaustive]
567#[derive(Clone, Debug, Serialize, Deserialize)]
568pub struct MiddlewareConfig {
569	/// Full path to the middleware class
570	pub path: String,
571
572	/// Middleware options
573	pub options: HashMap<String, serde_json::Value>,
574}
575
576impl MiddlewareConfig {
577	/// Create a new middleware configuration
578	///
579	/// # Examples
580	///
581	/// ```
582	/// use reinhardt_conf::settings::MiddlewareConfig;
583	///
584	/// let middleware = MiddlewareConfig::new("myapp.middleware.CustomMiddleware");
585	///
586	/// assert_eq!(middleware.path, "myapp.middleware.CustomMiddleware");
587	/// assert_eq!(middleware.options.len(), 0);
588	/// ```
589	pub fn new(path: impl Into<String>) -> Self {
590		Self {
591			path: path.into(),
592			options: HashMap::new(),
593		}
594	}
595	/// Add an option to the middleware
596	///
597	/// # Examples
598	///
599	/// ```
600	/// use reinhardt_conf::settings::MiddlewareConfig;
601	///
602	/// let middleware = MiddlewareConfig::new("myapp.middleware.CustomMiddleware")
603	///     .with_option("timeout", serde_json::json!(30));
604	///
605	/// assert_eq!(middleware.options.get("timeout"), Some(&serde_json::json!(30)));
606	/// ```
607	pub fn with_option(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
608		self.options.insert(key.into(), value);
609		self
610	}
611}
612
613#[cfg(test)]
614#[allow(deprecated)] // Tests exercise deprecated Settings for backward-compatibility verification
615mod tests {
616	use super::*;
617	use crate::settings::core_settings::HasCoreSettings;
618	use rstest::rstest;
619
620	#[rstest]
621	fn test_settings_default_unit() {
622		// Arrange / Act
623		let settings = Settings::default();
624
625		// Assert
626		assert!(settings.core.debug);
627		assert_eq!(settings.language_code, "en-us");
628		assert_eq!(settings.time_zone, "UTC");
629	}
630
631	#[rstest]
632	fn test_template_config() {
633		// Arrange / Act
634		let config = TemplateConfig::default();
635
636		// Assert
637		assert!(config.app_dirs);
638		assert_eq!(config.backend, "reinhardt.template.backends.jinja2.Jinja2");
639	}
640
641	#[rstest]
642	fn test_middleware_config() {
643		// Arrange / Act
644		let middleware = MiddlewareConfig::new("reinhardt.middleware.TestMiddleware")
645			.with_option("enabled", serde_json::json!(true));
646
647		// Assert
648		assert_eq!(middleware.path, "reinhardt.middleware.TestMiddleware");
649		assert_eq!(
650			middleware.options.get("enabled"),
651			Some(&serde_json::json!(true))
652		);
653	}
654
655	#[rstest]
656	fn test_contact_creation() {
657		// Arrange / Act
658		let contact = Contact::new("Alice Smith", "alice@example.com");
659
660		// Assert
661		assert_eq!(contact.name, "Alice Smith");
662		assert_eq!(contact.email, "alice@example.com");
663	}
664
665	#[rstest]
666	fn test_settings_admins() {
667		// Arrange
668		let mut settings = Settings::default();
669		assert_eq!(settings.admins.len(), 0);
670
671		// Act
672		settings.add_admin("John Doe", "john@example.com");
673
674		// Assert
675		assert_eq!(settings.admins.len(), 1);
676		assert_eq!(settings.admins[0].name, "John Doe");
677		assert_eq!(settings.admins[0].email, "john@example.com");
678	}
679
680	#[rstest]
681	fn test_settings_managers() {
682		// Arrange
683		let mut settings = Settings::default();
684		assert_eq!(settings.managers.len(), 0);
685
686		// Act
687		settings.add_manager("Jane Smith", "jane@example.com");
688
689		// Assert
690		assert_eq!(settings.managers.len(), 1);
691		assert_eq!(settings.managers[0].name, "Jane Smith");
692		assert_eq!(settings.managers[0].email, "jane@example.com");
693	}
694
695	#[rstest]
696	fn test_managers_from_admins() {
697		// Arrange
698		let mut settings = Settings::default();
699		settings.add_admin("John Doe", "john@example.com");
700		settings.add_admin("Jane Smith", "jane@example.com");
701		assert_eq!(settings.managers.len(), 0);
702
703		// Act
704		settings.managers_from_admins();
705
706		// Assert
707		assert_eq!(settings.managers.len(), 2);
708		assert_eq!(settings.managers, settings.admins);
709	}
710
711	#[rstest]
712	fn test_with_admins_fluent_api() {
713		// Arrange / Act
714		let settings = Settings::default().with_admins(vec![
715			Contact::new("Alice", "alice@example.com"),
716			Contact::new("Bob", "bob@example.com"),
717		]);
718
719		// Assert
720		assert_eq!(settings.admins.len(), 2);
721		assert_eq!(settings.admins[0].name, "Alice");
722		assert_eq!(settings.admins[1].name, "Bob");
723	}
724
725	#[rstest]
726	fn test_with_managers_fluent_api() {
727		// Arrange / Act
728		let settings =
729			Settings::default().with_managers(vec![Contact::new("Charlie", "charlie@example.com")]);
730
731		// Assert
732		assert_eq!(settings.managers.len(), 1);
733		assert_eq!(settings.managers[0].name, "Charlie");
734	}
735
736	#[rstest]
737	fn test_has_core_settings_bridge() {
738		// Arrange
739		let settings = Settings::new(PathBuf::from("/app"), "test-secret".to_string());
740
741		// Act
742		let core = settings.core();
743
744		// Assert
745		assert_eq!(core.base_dir, PathBuf::from("/app"));
746		assert_eq!(core.secret_key, "test-secret");
747		assert!(core.debug);
748	}
749}