elif_core/
config_derive.rs

1use crate::app_config::{AppConfigTrait, ConfigError, ConfigSource};
2use std::collections::HashMap;
3
4/// Attribute-based configuration builder for creating configuration structs
5/// 
6/// This provides a simplified approach to configuration without proc macros,
7/// using builder pattern and attribute-like methods.
8pub 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
20/// Configuration field descriptor for manual configuration building
21pub 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, // Can't clone function pointers
52        }
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
97/// Configuration schema for defining configuration structures
98pub 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    /// Load configuration values based on schema
117    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                // Nested fields are handled separately
123                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            // Apply validation if present
153            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    /// Get configuration sources for debugging
164    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
185/// Example of a manually defined configuration using the schema system
186pub 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    /// Create configuration schema for DatabaseConfig
197    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    // Global test lock to prevent concurrent environment modifications
351    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        // Set test environment
390        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        // Cleanup
407        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        // Clean environment
419        env::remove_var("DB_HOST");
420        env::remove_var("DB_PORT");
421        env::remove_var("DB_POOL_SIZE");
422        
423        // Set required fields
424        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        // Cleanup
437        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        // Cleanup
453        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        // Cleanup
470        env::remove_var("DB_NAME");
471        env::remove_var("DB_USERNAME");
472        env::remove_var("DB_POOL_SIZE");
473    }
474}