1use crate::app_config::{AppConfigTrait, ConfigError, ConfigSource};
2use std::collections::HashMap;
3
4pub struct ConfigBuilder<T> {
9 _phantom: std::marker::PhantomData<T>,
10}
11
12impl<T> ConfigBuilder<T> {
13 pub fn new() -> Self {
14 Self {
15 _phantom: std::marker::PhantomData,
16 }
17 }
18}
19
20pub struct ConfigField {
22 pub name: String,
23 pub env_var: Option<String>,
24 pub default_value: Option<String>,
25 pub required: bool,
26 pub nested: bool,
27 pub validation: Option<Box<dyn Fn(&str) -> Result<(), ConfigError> + Send + Sync>>,
28}
29
30impl std::fmt::Debug for ConfigField {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 f.debug_struct("ConfigField")
33 .field("name", &self.name)
34 .field("env_var", &self.env_var)
35 .field("default_value", &self.default_value)
36 .field("required", &self.required)
37 .field("nested", &self.nested)
38 .field("validation", &self.validation.is_some())
39 .finish()
40 }
41}
42
43impl Clone for ConfigField {
44 fn clone(&self) -> Self {
45 Self {
46 name: self.name.clone(),
47 env_var: self.env_var.clone(),
48 default_value: self.default_value.clone(),
49 required: self.required,
50 nested: self.nested,
51 validation: None, }
53 }
54}
55
56impl ConfigField {
57 pub fn new(name: impl Into<String>) -> Self {
58 Self {
59 name: name.into(),
60 env_var: None,
61 default_value: None,
62 required: false,
63 nested: false,
64 validation: None,
65 }
66 }
67
68 pub fn env(mut self, env_var: impl Into<String>) -> Self {
69 self.env_var = Some(env_var.into());
70 self
71 }
72
73 pub fn default(mut self, default_value: impl Into<String>) -> Self {
74 self.default_value = Some(default_value.into());
75 self
76 }
77
78 pub fn required(mut self) -> Self {
79 self.required = true;
80 self
81 }
82
83 pub fn nested(mut self) -> Self {
84 self.nested = true;
85 self
86 }
87
88 pub fn validate<F>(mut self, validator: F) -> Self
89 where
90 F: Fn(&str) -> Result<(), ConfigError> + Send + Sync + 'static,
91 {
92 self.validation = Some(Box::new(validator));
93 self
94 }
95}
96
97pub struct ConfigSchema {
99 pub name: String,
100 pub fields: Vec<ConfigField>,
101}
102
103impl ConfigSchema {
104 pub fn new(name: impl Into<String>) -> Self {
105 Self {
106 name: name.into(),
107 fields: Vec::new(),
108 }
109 }
110
111 pub fn field(mut self, field: ConfigField) -> Self {
112 self.fields.push(field);
113 self
114 }
115
116 pub fn load_values(&self) -> Result<HashMap<String, String>, ConfigError> {
118 let mut values = HashMap::new();
119
120 for field in &self.fields {
121 if field.nested {
122 continue;
124 }
125
126 let value = if let Some(env_var) = &field.env_var {
127 match std::env::var(env_var) {
128 Ok(val) => val,
129 Err(_) if field.required => {
130 return Err(ConfigError::MissingEnvVar {
131 var: env_var.clone(),
132 });
133 }
134 Err(_) => {
135 if let Some(default) = &field.default_value {
136 default.clone()
137 } else {
138 continue;
139 }
140 }
141 }
142 } else if let Some(default) = &field.default_value {
143 default.clone()
144 } else if field.required {
145 return Err(ConfigError::MissingEnvVar {
146 var: format!("{}_NOT_SPECIFIED", field.name.to_uppercase()),
147 });
148 } else {
149 continue;
150 };
151
152 if let Some(validator) = &field.validation {
154 validator(&value)?;
155 }
156
157 values.insert(field.name.clone(), value);
158 }
159
160 Ok(values)
161 }
162
163 pub fn get_sources(&self) -> HashMap<String, ConfigSource> {
165 let mut sources = HashMap::new();
166
167 for field in &self.fields {
168 let source = if field.nested {
169 ConfigSource::Nested
170 } else if let Some(env_var) = &field.env_var {
171 ConfigSource::EnvVar(env_var.clone())
172 } else if field.default_value.is_some() {
173 ConfigSource::Default(field.name.clone())
174 } else {
175 ConfigSource::EnvVar(format!("{}_NOT_SPECIFIED", field.name.to_uppercase()))
176 };
177
178 sources.insert(field.name.clone(), source);
179 }
180
181 sources
182 }
183}
184
185pub struct DatabaseConfig {
187 pub host: String,
188 pub port: u16,
189 pub name: String,
190 pub username: String,
191 pub password: Option<String>,
192 pub pool_size: usize,
193}
194
195impl DatabaseConfig {
196 pub fn schema() -> ConfigSchema {
198 ConfigSchema::new("DatabaseConfig")
199 .field(
200 ConfigField::new("host")
201 .env("DB_HOST")
202 .default("localhost")
203 .validate(|val| {
204 if val.is_empty() {
205 Err(ConfigError::ValidationFailed {
206 field: "host".to_string(),
207 reason: "Host cannot be empty".to_string(),
208 })
209 } else {
210 Ok(())
211 }
212 })
213 )
214 .field(
215 ConfigField::new("port")
216 .env("DB_PORT")
217 .default("5432")
218 .validate(|val| {
219 val.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
220 field: "port".to_string(),
221 value: val.to_string(),
222 expected: "valid port number (0-65535)".to_string(),
223 })?;
224 Ok(())
225 })
226 )
227 .field(
228 ConfigField::new("name")
229 .env("DB_NAME")
230 .required()
231 )
232 .field(
233 ConfigField::new("username")
234 .env("DB_USERNAME")
235 .required()
236 )
237 .field(
238 ConfigField::new("password")
239 .env("DB_PASSWORD")
240 )
241 .field(
242 ConfigField::new("pool_size")
243 .env("DB_POOL_SIZE")
244 .default("10")
245 .validate(|val| {
246 let size: usize = val.parse().map_err(|_| ConfigError::InvalidValue {
247 field: "pool_size".to_string(),
248 value: val.to_string(),
249 expected: "valid number".to_string(),
250 })?;
251
252 if size == 0 || size > 100 {
253 Err(ConfigError::ValidationFailed {
254 field: "pool_size".to_string(),
255 reason: "Pool size must be between 1 and 100".to_string(),
256 })
257 } else {
258 Ok(())
259 }
260 })
261 )
262 }
263}
264
265impl AppConfigTrait for DatabaseConfig {
266 fn from_env() -> Result<Self, ConfigError> {
267 let schema = Self::schema();
268 let values = schema.load_values()?;
269
270 let host = values.get("host").unwrap_or(&"localhost".to_string()).clone();
271 let port = values.get("port").unwrap_or(&"5432".to_string())
272 .parse::<u16>()
273 .map_err(|_| ConfigError::InvalidValue {
274 field: "port".to_string(),
275 value: values.get("port").unwrap_or(&"5432".to_string()).clone(),
276 expected: "valid port number".to_string(),
277 })?;
278
279 let name = values.get("name").ok_or_else(|| ConfigError::MissingEnvVar {
280 var: "DB_NAME".to_string(),
281 })?.clone();
282
283 let username = values.get("username").ok_or_else(|| ConfigError::MissingEnvVar {
284 var: "DB_USERNAME".to_string(),
285 })?.clone();
286
287 let password = values.get("password").cloned();
288
289 let pool_size = values.get("pool_size").unwrap_or(&"10".to_string())
290 .parse::<usize>()
291 .map_err(|_| ConfigError::InvalidValue {
292 field: "pool_size".to_string(),
293 value: values.get("pool_size").unwrap_or(&"10".to_string()).clone(),
294 expected: "valid number".to_string(),
295 })?;
296
297 Ok(DatabaseConfig {
298 host,
299 port,
300 name,
301 username,
302 password,
303 pool_size,
304 })
305 }
306
307 fn validate(&self) -> Result<(), ConfigError> {
308 if self.host.is_empty() {
309 return Err(ConfigError::ValidationFailed {
310 field: "host".to_string(),
311 reason: "Host cannot be empty".to_string(),
312 });
313 }
314
315 if self.name.is_empty() {
316 return Err(ConfigError::ValidationFailed {
317 field: "name".to_string(),
318 reason: "Database name cannot be empty".to_string(),
319 });
320 }
321
322 if self.username.is_empty() {
323 return Err(ConfigError::ValidationFailed {
324 field: "username".to_string(),
325 reason: "Username cannot be empty".to_string(),
326 });
327 }
328
329 if self.pool_size == 0 || self.pool_size > 100 {
330 return Err(ConfigError::ValidationFailed {
331 field: "pool_size".to_string(),
332 reason: "Pool size must be between 1 and 100".to_string(),
333 });
334 }
335
336 Ok(())
337 }
338
339 fn config_sources(&self) -> HashMap<String, ConfigSource> {
340 Self::schema().get_sources()
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use std::env;
348 use std::sync::Mutex;
349
350 static TEST_MUTEX: Mutex<()> = Mutex::new(());
352
353 #[test]
354 fn test_config_field_builder() {
355 let field = ConfigField::new("test_field")
356 .env("TEST_VAR")
357 .default("default_value")
358 .required();
359
360 assert_eq!(field.name, "test_field");
361 assert_eq!(field.env_var, Some("TEST_VAR".to_string()));
362 assert_eq!(field.default_value, Some("default_value".to_string()));
363 assert!(field.required);
364 }
365
366 #[test]
367 fn test_config_schema() {
368 let schema = ConfigSchema::new("TestConfig")
369 .field(
370 ConfigField::new("field1")
371 .env("TEST_FIELD1")
372 .default("default1")
373 )
374 .field(
375 ConfigField::new("field2")
376 .env("TEST_FIELD2")
377 .required()
378 );
379
380 assert_eq!(schema.name, "TestConfig");
381 assert_eq!(schema.fields.len(), 2);
382 assert_eq!(schema.fields[0].name, "field1");
383 assert_eq!(schema.fields[1].name, "field2");
384 }
385
386 #[test]
387 fn test_database_config_from_env() {
388 let _guard = TEST_MUTEX.lock().unwrap();
389 env::set_var("DB_HOST", "test-host");
391 env::set_var("DB_PORT", "3306");
392 env::set_var("DB_NAME", "test_db");
393 env::set_var("DB_USERNAME", "test_user");
394 env::set_var("DB_PASSWORD", "test_pass");
395 env::set_var("DB_POOL_SIZE", "5");
396
397 let config = DatabaseConfig::from_env().unwrap();
398
399 assert_eq!(config.host, "test-host");
400 assert_eq!(config.port, 3306);
401 assert_eq!(config.name, "test_db");
402 assert_eq!(config.username, "test_user");
403 assert_eq!(config.password, Some("test_pass".to_string()));
404 assert_eq!(config.pool_size, 5);
405
406 env::remove_var("DB_HOST");
408 env::remove_var("DB_PORT");
409 env::remove_var("DB_NAME");
410 env::remove_var("DB_USERNAME");
411 env::remove_var("DB_PASSWORD");
412 env::remove_var("DB_POOL_SIZE");
413 }
414
415 #[test]
416 fn test_database_config_defaults() {
417 let _guard = TEST_MUTEX.lock().unwrap();
418 env::remove_var("DB_HOST");
420 env::remove_var("DB_PORT");
421 env::remove_var("DB_POOL_SIZE");
422
423 env::set_var("DB_NAME", "test_db");
425 env::set_var("DB_USERNAME", "test_user");
426
427 let config = DatabaseConfig::from_env().unwrap();
428
429 assert_eq!(config.host, "localhost");
430 assert_eq!(config.port, 5432);
431 assert_eq!(config.name, "test_db");
432 assert_eq!(config.username, "test_user");
433 assert_eq!(config.password, None);
434 assert_eq!(config.pool_size, 10);
435
436 env::remove_var("DB_NAME");
438 env::remove_var("DB_USERNAME");
439 }
440
441 #[test]
442 fn test_database_config_validation() {
443 let _guard = TEST_MUTEX.lock().unwrap();
444 env::set_var("DB_HOST", "valid-host");
445 env::set_var("DB_NAME", "valid_db");
446 env::set_var("DB_USERNAME", "valid_user");
447 env::set_var("DB_POOL_SIZE", "5");
448
449 let config = DatabaseConfig::from_env().unwrap();
450 assert!(config.validate().is_ok());
451
452 env::remove_var("DB_HOST");
454 env::remove_var("DB_NAME");
455 env::remove_var("DB_USERNAME");
456 env::remove_var("DB_POOL_SIZE");
457 }
458
459 #[test]
460 fn test_invalid_pool_size() {
461 let _guard = TEST_MUTEX.lock().unwrap();
462 env::set_var("DB_NAME", "test_db");
463 env::set_var("DB_USERNAME", "test_user");
464 env::set_var("DB_POOL_SIZE", "invalid");
465
466 let result = DatabaseConfig::from_env();
467 assert!(result.is_err());
468
469 env::remove_var("DB_NAME");
471 env::remove_var("DB_USERNAME");
472 env::remove_var("DB_POOL_SIZE");
473 }
474}