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}