1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10#[deprecated(
12 since = "0.1.0-rc.16",
13 note = "use individual fragments (CacheSettings, SessionSettings, etc.) with ProjectSettings instead"
14)]
15#[non_exhaustive]
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct AdvancedSettings {
18 #[serde(default)]
20 pub debug: bool,
21
22 #[serde(default = "default_secret_key")]
24 pub secret_key: String,
25
26 #[serde(default)]
28 pub allowed_hosts: Vec<String>,
29
30 #[serde(default)]
32 pub database: DatabaseSettings,
33
34 #[serde(default)]
36 pub cache: CacheSettings,
37
38 #[serde(default)]
40 pub session: SessionSettings,
41
42 #[serde(default)]
44 pub cors: CorsSettings,
45
46 #[serde(default)]
48 pub static_files: StaticSettings,
49
50 #[serde(default)]
52 pub media: MediaSettings,
53
54 #[serde(default)]
56 pub email: EmailSettings,
57
58 #[serde(default)]
60 pub logging: LoggingSettings,
61
62 #[serde(default)]
64 pub custom: HashMap<String, serde_json::Value>,
65}
66
67#[allow(deprecated)]
68impl Default for AdvancedSettings {
69 fn default() -> Self {
70 Self {
71 debug: false,
72 secret_key: "change-me-in-production".to_string(),
73 allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
74 database: DatabaseSettings::default(),
75 cache: CacheSettings::default(),
76 session: SessionSettings::default(),
77 cors: CorsSettings::default(),
78 static_files: StaticSettings::default(),
79 media: MediaSettings::default(),
80 email: EmailSettings::default(),
81 logging: LoggingSettings::default(),
82 custom: HashMap::new(),
83 }
84 }
85}
86
87#[allow(deprecated)]
88impl AdvancedSettings {
89 pub fn new() -> Self {
91 Self::default()
92 }
93 pub fn validate(&self) -> Result<(), SettingsError> {
96 if self.secret_key == "change-me-in-production" && !self.debug {
97 return Err(SettingsError::ValidationError(
98 "SECRET_KEY must be changed in production".to_string(),
99 ));
100 }
101
102 if self.secret_key.len() < 32 {
103 return Err(SettingsError::ValidationError(
104 "SECRET_KEY must be at least 32 characters".to_string(),
105 ));
106 }
107
108 if self.allowed_hosts.is_empty() && !self.debug {
109 return Err(SettingsError::ValidationError(
110 "ALLOWED_HOSTS must not be empty in production".to_string(),
111 ));
112 }
113
114 Ok(())
115 }
116 pub fn from_env() -> Result<Self, SettingsError> {
119 let mut settings = Self::default();
120
121 if let Ok(debug) = std::env::var("REINHARDT_DEBUG") {
122 settings.debug = debug.to_lowercase() == "true" || debug == "1";
123 }
124
125 if let Ok(secret) = std::env::var("REINHARDT_SECRET_KEY") {
126 settings.secret_key = secret;
127 }
128
129 if let Ok(hosts) = std::env::var("REINHARDT_ALLOWED_HOSTS") {
130 settings.allowed_hosts = hosts.split(',').map(|s| s.trim().to_string()).collect();
131 }
132
133 if let Ok(url) = std::env::var("DATABASE_URL") {
135 settings.database.url = url;
136 }
137
138 if let Ok(backend) = std::env::var("CACHE_BACKEND") {
140 settings.cache.backend = backend;
141 }
142
143 Ok(settings)
144 }
145 pub fn from_file(path: impl Into<PathBuf>) -> Result<Self, SettingsError> {
148 let path = path.into();
149 let contents = std::fs::read_to_string(&path).map_err(|e| {
150 SettingsError::FileError(format!("Failed to read {}: {}", path.display(), e))
151 })?;
152
153 let settings: AdvancedSettings =
154 if path.extension().and_then(|s| s.to_str()) == Some("toml") {
155 toml::from_str(&contents)
156 .map_err(|e| SettingsError::ParseError(format!("TOML parse error: {}", e)))?
157 } else if path.extension().and_then(|s| s.to_str()) == Some("json") {
158 serde_json::from_str(&contents)
159 .map_err(|e| SettingsError::ParseError(format!("JSON parse error: {}", e)))?
160 } else {
161 return Err(SettingsError::UnsupportedFormat(
162 "Supported formats: .toml, .json".to_string(),
163 ));
164 };
165
166 Ok(settings)
167 }
168 pub fn set<T: Serialize>(
171 &mut self,
172 key: impl Into<String>,
173 value: T,
174 ) -> Result<(), SettingsError> {
175 let json_value = serde_json::to_value(value)
176 .map_err(|e| SettingsError::SerializationError(e.to_string()))?;
177 self.custom.insert(key.into(), json_value);
178 Ok(())
179 }
180 pub fn get<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Option<T> {
183 self.custom
184 .get(key)
185 .and_then(|v| serde_json::from_value(v.clone()).ok())
186 }
187}
188
189#[non_exhaustive]
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct DatabaseSettings {
193 pub url: String,
195 pub max_connections: u32,
197 pub min_connections: u32,
199 pub connect_timeout: u64,
201 pub idle_timeout: u64,
203}
204
205impl Default for DatabaseSettings {
206 fn default() -> Self {
207 Self {
208 url: "sqlite::memory:".to_string(),
209 max_connections: 10,
210 min_connections: 1,
211 connect_timeout: 30,
212 idle_timeout: 600,
213 }
214 }
215}
216
217#[non_exhaustive]
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct CacheSettings {
221 pub backend: String,
223 pub location: Option<String>,
225 pub timeout: u64,
227}
228
229impl Default for CacheSettings {
230 fn default() -> Self {
231 Self {
232 backend: "memory".to_string(),
233 location: None,
234 timeout: 300,
235 }
236 }
237}
238
239#[non_exhaustive]
241#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct SessionSettings {
243 pub engine: String,
245 pub cookie_name: String,
247 pub cookie_age: u64,
249 pub cookie_secure: bool,
251 pub cookie_httponly: bool,
253 pub cookie_samesite: String,
255}
256
257impl Default for SessionSettings {
258 fn default() -> Self {
259 Self {
260 engine: "cookie".to_string(),
261 cookie_name: "sessionid".to_string(),
262 cookie_age: 1209600, cookie_secure: false,
264 cookie_httponly: true,
265 cookie_samesite: "lax".to_string(),
266 }
267 }
268}
269
270#[non_exhaustive]
272#[derive(Debug, Clone, Serialize, Deserialize)]
273pub struct CorsSettings {
274 pub allow_origins: Vec<String>,
276 pub allow_methods: Vec<String>,
278 pub allow_headers: Vec<String>,
280 pub allow_credentials: bool,
282 pub max_age: u64,
284}
285
286impl Default for CorsSettings {
287 fn default() -> Self {
288 Self {
289 allow_origins: vec!["*".to_string()],
290 allow_methods: vec![
291 "GET".to_string(),
292 "POST".to_string(),
293 "PUT".to_string(),
294 "PATCH".to_string(),
295 "DELETE".to_string(),
296 ],
297 allow_headers: vec!["*".to_string()],
298 allow_credentials: false,
299 max_age: 3600,
300 }
301 }
302}
303
304#[non_exhaustive]
306#[derive(Debug, Clone, Serialize, Deserialize)]
307pub struct StaticSettings {
308 pub url: String,
310 pub root: PathBuf,
312}
313
314impl Default for StaticSettings {
315 fn default() -> Self {
316 Self {
317 url: "/static/".to_string(),
318 root: PathBuf::from("static"),
319 }
320 }
321}
322
323#[non_exhaustive]
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct MediaSettings {
327 pub url: String,
329 pub root: PathBuf,
331}
332
333impl Default for MediaSettings {
334 fn default() -> Self {
335 Self {
336 url: "/media/".to_string(),
337 root: PathBuf::from("media"),
338 }
339 }
340}
341
342#[non_exhaustive]
344#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct EmailSettings {
346 pub backend: String,
348 pub host: String,
350 pub port: u16,
352 pub username: Option<String>,
354 pub password: Option<String>,
356 pub use_tls: bool,
358 pub use_ssl: bool,
360 pub from_email: String,
362
363 #[serde(default)]
366 pub admins: Vec<(String, String)>,
367
368 #[serde(default)]
371 pub managers: Vec<(String, String)>,
372
373 #[serde(default = "default_server_email")]
375 pub server_email: String,
376
377 #[serde(default)]
379 pub subject_prefix: String,
380
381 pub timeout: Option<u64>,
383
384 pub ssl_certfile: Option<PathBuf>,
386
387 pub ssl_keyfile: Option<PathBuf>,
389
390 #[serde(default)]
393 pub file_path: Option<PathBuf>,
394}
395
396fn default_secret_key() -> String {
397 "change-me-in-production".to_string()
398}
399
400fn default_server_email() -> String {
401 "root@localhost".to_string()
402}
403
404impl Default for EmailSettings {
405 fn default() -> Self {
406 Self {
407 backend: "console".to_string(),
408 host: "localhost".to_string(),
409 port: 25,
410 username: None,
411 password: None,
412 use_tls: false,
413 use_ssl: false,
414 from_email: "noreply@example.com".to_string(),
415 admins: Vec::new(),
416 managers: Vec::new(),
417 server_email: default_server_email(),
418 subject_prefix: String::new(),
419 timeout: None,
420 ssl_certfile: None,
421 ssl_keyfile: None,
422 file_path: None,
423 }
424 }
425}
426
427#[non_exhaustive]
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct LoggingSettings {
431 pub level: String,
433 pub format: String,
435}
436
437impl Default for LoggingSettings {
438 fn default() -> Self {
439 Self {
440 level: "info".to_string(),
441 format: "text".to_string(),
442 }
443 }
444}
445
446#[non_exhaustive]
448#[derive(Debug, thiserror::Error)]
449pub enum SettingsError {
450 #[error("File error: {0}")]
452 FileError(String),
453
454 #[error("Parse error: {0}")]
456 ParseError(String),
457
458 #[error("Validation error: {0}")]
460 ValidationError(String),
461
462 #[error("Unsupported format: {0}")]
464 UnsupportedFormat(String),
465
466 #[error("Serialization error: {0}")]
468 SerializationError(String),
469}
470
471#[cfg(test)]
472#[allow(deprecated)]
473mod tests {
474 use super::*;
475
476 #[test]
477 fn test_default_settings() {
478 let settings = AdvancedSettings::default();
479 assert!(!settings.debug);
480 assert_eq!(settings.database.url, "sqlite::memory:");
481 assert_eq!(settings.cache.backend, "memory");
482 }
483
484 #[test]
485 fn test_settings_validation() {
486 let mut settings = AdvancedSettings::default();
487
488 assert!(settings.validate().is_err());
490
491 settings.secret_key = "a".repeat(32);
493 assert!(settings.validate().is_ok());
494 }
495
496 #[test]
497 fn test_custom_settings() {
498 let mut settings = AdvancedSettings::default();
499
500 settings.set("api_version", "v1").unwrap();
501 settings.set("max_upload_size", 10485760_u64).unwrap();
502
503 let version: String = settings.get("api_version").unwrap();
504 assert_eq!(version, "v1");
505
506 let max_size: u64 = settings.get("max_upload_size").unwrap();
507 assert_eq!(max_size, 10485760);
508 }
509}