elif_core/config/
builder.rs

1use crate::config::ConfigError;
2use service_builder::builder;
3use std::collections::HashMap;
4
5/// Configuration field definition
6#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
7pub struct ConfigField {
8    pub name: String,
9    pub field_type: String,
10    pub required: bool,
11    pub default_value: Option<String>,
12    pub description: Option<String>,
13    pub validation_rules: Vec<String>,
14}
15
16impl ConfigField {
17    /// Create a new configuration field
18    pub fn new(name: impl Into<String>, field_type: impl Into<String>) -> Self {
19        Self {
20            name: name.into(),
21            field_type: field_type.into(),
22            required: false,
23            default_value: None,
24            description: None,
25            validation_rules: Vec::new(),
26        }
27    }
28    
29    /// Make field required
30    pub fn required(mut self) -> Self {
31        self.required = true;
32        self
33    }
34    
35    /// Set default value
36    pub fn with_default(mut self, default: impl Into<String>) -> Self {
37        self.default_value = Some(default.into());
38        self
39    }
40    
41    /// Set description
42    pub fn with_description(mut self, description: impl Into<String>) -> Self {
43        self.description = Some(description.into());
44        self
45    }
46    
47    /// Add validation rule
48    pub fn add_validation(mut self, rule: impl Into<String>) -> Self {
49        self.validation_rules.push(rule.into());
50        self
51    }
52}
53
54/// Configuration schema for validation and generation
55#[derive(Debug, Clone)]
56pub struct ConfigSchema {
57    pub name: String,
58    pub fields: Vec<ConfigField>,
59}
60
61impl ConfigSchema {
62    /// Create a new configuration schema
63    pub fn new(name: impl Into<String>) -> Self {
64        Self {
65            name: name.into(),
66            fields: Vec::new(),
67        }
68    }
69    
70    /// Add a field to the schema
71    pub fn add_field(mut self, field: ConfigField) -> Self {
72        self.fields.push(field);
73        self
74    }
75    
76    /// Get field by name
77    pub fn get_field(&self, name: &str) -> Option<&ConfigField> {
78        self.fields.iter().find(|f| f.name == name)
79    }
80    
81    /// Get all required fields
82    pub fn required_fields(&self) -> Vec<&ConfigField> {
83        self.fields.iter().filter(|f| f.required).collect()
84    }
85    
86    /// Validate a configuration map against this schema
87    pub fn validate_config(&self, config: &HashMap<String, String>) -> Result<(), ConfigError> {
88        // Check required fields
89        for field in &self.fields {
90            if field.required && !config.contains_key(&field.name) {
91                return Err(ConfigError::missing_required(
92                    &field.name,
93                    field.description.as_deref().unwrap_or("This field is required"),
94                ));
95            }
96        }
97        
98        // Validate field values
99        for (key, value) in config {
100            if let Some(field) = self.get_field(key) {
101                self.validate_field_value(field, value)?;
102            }
103        }
104        
105        Ok(())
106    }
107    
108    /// Validate a single field value
109    fn validate_field_value(&self, field: &ConfigField, value: &str) -> Result<(), ConfigError> {
110        // Basic type validation
111        match field.field_type.as_str() {
112            "integer" | "int" => {
113                value.parse::<i64>().map_err(|_| {
114                    ConfigError::invalid_value(&field.name, value, "valid integer")
115                })?;
116            }
117            "float" | "number" => {
118                value.parse::<f64>().map_err(|_| {
119                    ConfigError::invalid_value(&field.name, value, "valid number")
120                })?;
121            }
122            "boolean" | "bool" => {
123                value.parse::<bool>().map_err(|_| {
124                    ConfigError::invalid_value(&field.name, value, "true or false")
125                })?;
126            }
127            "url" => {
128                if !value.starts_with("http://") && !value.starts_with("https://") {
129                    return Err(ConfigError::invalid_value(&field.name, value, "valid URL"));
130                }
131            }
132            _ => {
133                // String or custom types - no validation for now
134            }
135        }
136        
137        // Apply validation rules
138        for rule in &field.validation_rules {
139            self.apply_validation_rule(field, value, rule)?;
140        }
141        
142        Ok(())
143    }
144    
145    /// Apply a validation rule to a field value
146    fn apply_validation_rule(&self, field: &ConfigField, value: &str, rule: &str) -> Result<(), ConfigError> {
147        if rule.starts_with("min_length:") {
148            let min_len: usize = rule.strip_prefix("min_length:").unwrap().parse()
149                .map_err(|_| ConfigError::validation_failed("Invalid min_length rule"))?;
150            if value.len() < min_len {
151                return Err(ConfigError::invalid_value(
152                    &field.name, 
153                    value, 
154                    format!("at least {} characters", min_len)
155                ));
156            }
157        } else if rule.starts_with("max_length:") {
158            let max_len: usize = rule.strip_prefix("max_length:").unwrap().parse()
159                .map_err(|_| ConfigError::validation_failed("Invalid max_length rule"))?;
160            if value.len() > max_len {
161                return Err(ConfigError::invalid_value(
162                    &field.name, 
163                    value, 
164                    format!("at most {} characters", max_len)
165                ));
166            }
167        } else if rule.starts_with("pattern:") {
168            let pattern = rule.strip_prefix("pattern:").unwrap();
169            // In a real implementation, you would use a regex library
170            if !value.contains(pattern) {
171                return Err(ConfigError::invalid_value(
172                    &field.name, 
173                    value, 
174                    format!("matching pattern: {}", pattern)
175                ));
176            }
177        }
178        
179        Ok(())
180    }
181}
182
183/// Configuration builder for creating configurations programmatically
184#[builder]
185pub struct ConfigBuilder<T> {
186    #[builder(default)]
187    pub fields: Vec<ConfigField>,
188    
189    #[builder(optional)]
190    pub name: Option<String>,
191    
192    #[builder(default)]
193    pub _phantom: std::marker::PhantomData<T>,
194}
195
196impl<T> std::fmt::Debug for ConfigBuilder<T> {
197    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198        f.debug_struct("ConfigBuilder")
199            .field("fields_count", &self.fields.len())
200            .field("name", &self.name)
201            .finish()
202    }
203}
204
205impl<T> ConfigBuilder<T> {
206    /// Build a ConfigSchema from this builder
207    pub fn build_schema(self) -> ConfigSchema {
208        ConfigSchema {
209            name: self.name.unwrap_or_else(|| "DefaultConfig".to_string()),
210            fields: self.fields,
211        }
212    }
213}
214
215// Add convenience methods to the generated builder
216impl<T> ConfigBuilderBuilder<T> {
217    /// Add a configuration field
218    pub fn add_field(self, field: ConfigField) -> Self {
219        let mut fields_vec = self.fields.unwrap_or_default();
220        fields_vec.push(field);
221        ConfigBuilderBuilder {
222            fields: Some(fields_vec),
223            name: self.name,
224            _phantom: self._phantom,
225        }
226    }
227    
228    /// Add a string field
229    pub fn add_string_field(self, name: impl Into<String>) -> Self {
230        self.add_field(ConfigField::new(name, "string"))
231    }
232    
233    /// Add a required string field
234    pub fn add_required_string_field(self, name: impl Into<String>) -> Self {
235        self.add_field(ConfigField::new(name, "string").required())
236    }
237    
238    /// Add an integer field
239    pub fn add_int_field(self, name: impl Into<String>) -> Self {
240        self.add_field(ConfigField::new(name, "integer"))
241    }
242    
243    /// Add a boolean field
244    pub fn add_bool_field(self, name: impl Into<String>) -> Self {
245        self.add_field(ConfigField::new(name, "boolean"))
246    }
247    
248    /// Add a URL field
249    pub fn add_url_field(self, name: impl Into<String>) -> Self {
250        self.add_field(ConfigField::new(name, "url"))
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    
258    #[test]
259    fn test_config_schema() {
260        let schema = ConfigSchema::new("test_config")
261            .add_field(ConfigField::new("name", "string").required())
262            .add_field(ConfigField::new("port", "integer").with_default("3000"))
263            .add_field(ConfigField::new("debug", "boolean").with_default("false"));
264        
265        assert_eq!(schema.name, "test_config");
266        assert_eq!(schema.fields.len(), 3);
267        assert_eq!(schema.required_fields().len(), 1);
268    }
269    
270    #[test]
271    fn test_config_validation() {
272        let schema = ConfigSchema::new("test_config")
273            .add_field(ConfigField::new("name", "string").required())
274            .add_field(ConfigField::new("port", "integer"));
275        
276        let mut config = HashMap::new();
277        config.insert("name".to_string(), "test".to_string());
278        config.insert("port".to_string(), "3000".to_string());
279        
280        assert!(schema.validate_config(&config).is_ok());
281        
282        config.remove("name");
283        assert!(schema.validate_config(&config).is_err());
284    }
285}