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
94                        .description
95                        .as_deref()
96                        .unwrap_or("This field is required"),
97                ));
98            }
99        }
100
101        // Validate field values
102        for (key, value) in config {
103            if let Some(field) = self.get_field(key) {
104                self.validate_field_value(field, value)?;
105            }
106        }
107
108        Ok(())
109    }
110
111    /// Validate a single field value
112    fn validate_field_value(&self, field: &ConfigField, value: &str) -> Result<(), ConfigError> {
113        // Basic type validation
114        match field.field_type.as_str() {
115            "integer" | "int" => {
116                value
117                    .parse::<i64>()
118                    .map_err(|_| ConfigError::invalid_value(&field.name, value, "valid integer"))?;
119            }
120            "float" | "number" => {
121                value
122                    .parse::<f64>()
123                    .map_err(|_| ConfigError::invalid_value(&field.name, value, "valid number"))?;
124            }
125            "boolean" | "bool" => {
126                value
127                    .parse::<bool>()
128                    .map_err(|_| ConfigError::invalid_value(&field.name, value, "true or false"))?;
129            }
130            "url" => {
131                if !value.starts_with("http://") && !value.starts_with("https://") {
132                    return Err(ConfigError::invalid_value(&field.name, value, "valid URL"));
133                }
134            }
135            _ => {
136                // String or custom types - no validation for now
137            }
138        }
139
140        // Apply validation rules
141        for rule in &field.validation_rules {
142            self.apply_validation_rule(field, value, rule)?;
143        }
144
145        Ok(())
146    }
147
148    /// Apply a validation rule to a field value
149    fn apply_validation_rule(
150        &self,
151        field: &ConfigField,
152        value: &str,
153        rule: &str,
154    ) -> Result<(), ConfigError> {
155        if rule.starts_with("min_length:") {
156            let min_len: usize = rule
157                .strip_prefix("min_length:")
158                .unwrap()
159                .parse()
160                .map_err(|_| ConfigError::validation_failed("Invalid min_length rule"))?;
161            if value.len() < min_len {
162                return Err(ConfigError::invalid_value(
163                    &field.name,
164                    value,
165                    format!("at least {} characters", min_len),
166                ));
167            }
168        } else if rule.starts_with("max_length:") {
169            let max_len: usize = rule
170                .strip_prefix("max_length:")
171                .unwrap()
172                .parse()
173                .map_err(|_| ConfigError::validation_failed("Invalid max_length rule"))?;
174            if value.len() > max_len {
175                return Err(ConfigError::invalid_value(
176                    &field.name,
177                    value,
178                    format!("at most {} characters", max_len),
179                ));
180            }
181        } else if rule.starts_with("pattern:") {
182            let pattern = rule.strip_prefix("pattern:").unwrap();
183            // In a real implementation, you would use a regex library
184            if !value.contains(pattern) {
185                return Err(ConfigError::invalid_value(
186                    &field.name,
187                    value,
188                    format!("matching pattern: {}", pattern),
189                ));
190            }
191        }
192
193        Ok(())
194    }
195}
196
197/// Configuration builder for creating configurations programmatically
198#[builder]
199pub struct ConfigBuilder<T> {
200    #[builder(default)]
201    pub fields: Vec<ConfigField>,
202
203    #[builder(optional)]
204    pub name: Option<String>,
205
206    #[builder(default)]
207    pub _phantom: std::marker::PhantomData<T>,
208}
209
210impl<T> std::fmt::Debug for ConfigBuilder<T> {
211    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212        f.debug_struct("ConfigBuilder")
213            .field("fields_count", &self.fields.len())
214            .field("name", &self.name)
215            .finish()
216    }
217}
218
219impl<T> ConfigBuilder<T> {
220    /// Build a ConfigSchema from this builder
221    pub fn build_schema(self) -> ConfigSchema {
222        ConfigSchema {
223            name: self.name.unwrap_or_else(|| "DefaultConfig".to_string()),
224            fields: self.fields,
225        }
226    }
227}
228
229// Add convenience methods to the generated builder
230impl<T> ConfigBuilderBuilder<T> {
231    /// Add a configuration field
232    pub fn add_field(self, field: ConfigField) -> Self {
233        let mut fields_vec = self.fields.unwrap_or_default();
234        fields_vec.push(field);
235        ConfigBuilderBuilder {
236            fields: Some(fields_vec),
237            name: self.name,
238            _phantom: self._phantom,
239        }
240    }
241
242    /// Add a string field
243    pub fn add_string_field(self, name: impl Into<String>) -> Self {
244        self.add_field(ConfigField::new(name, "string"))
245    }
246
247    /// Add a required string field
248    pub fn add_required_string_field(self, name: impl Into<String>) -> Self {
249        self.add_field(ConfigField::new(name, "string").required())
250    }
251
252    /// Add an integer field
253    pub fn add_int_field(self, name: impl Into<String>) -> Self {
254        self.add_field(ConfigField::new(name, "integer"))
255    }
256
257    /// Add a boolean field
258    pub fn add_bool_field(self, name: impl Into<String>) -> Self {
259        self.add_field(ConfigField::new(name, "boolean"))
260    }
261
262    /// Add a URL field
263    pub fn add_url_field(self, name: impl Into<String>) -> Self {
264        self.add_field(ConfigField::new(name, "url"))
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_config_schema() {
274        let schema = ConfigSchema::new("test_config")
275            .add_field(ConfigField::new("name", "string").required())
276            .add_field(ConfigField::new("port", "integer").with_default("3000"))
277            .add_field(ConfigField::new("debug", "boolean").with_default("false"));
278
279        assert_eq!(schema.name, "test_config");
280        assert_eq!(schema.fields.len(), 3);
281        assert_eq!(schema.required_fields().len(), 1);
282    }
283
284    #[test]
285    fn test_config_validation() {
286        let schema = ConfigSchema::new("test_config")
287            .add_field(ConfigField::new("name", "string").required())
288            .add_field(ConfigField::new("port", "integer"));
289
290        let mut config = HashMap::new();
291        config.insert("name".to_string(), "test".to_string());
292        config.insert("port".to_string(), "3000".to_string());
293
294        assert!(schema.validate_config(&config).is_ok());
295
296        config.remove("name");
297        assert!(schema.validate_config(&config).is_err());
298    }
299}