elif_core/config/
schema.rs

1use crate::config::{ConfigField, ConfigError};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5/// Configuration schema definition
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ConfigurationSchema {
8    pub name: String,
9    pub version: String,
10    pub description: Option<String>,
11    pub sections: Vec<ConfigSection>,
12}
13
14impl ConfigurationSchema {
15    /// Create a new configuration schema
16    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
17        Self {
18            name: name.into(),
19            version: version.into(),
20            description: None,
21            sections: Vec::new(),
22        }
23    }
24    
25    /// Set schema description
26    pub fn with_description(mut self, description: impl Into<String>) -> Self {
27        self.description = Some(description.into());
28        self
29    }
30    
31    /// Add a configuration section
32    pub fn add_section(mut self, section: ConfigSection) -> Self {
33        self.sections.push(section);
34        self
35    }
36    
37    /// Get section by name
38    pub fn get_section(&self, name: &str) -> Option<&ConfigSection> {
39        self.sections.iter().find(|s| s.name == name)
40    }
41    
42    /// Get all fields from all sections
43    pub fn all_fields(&self) -> Vec<&ConfigField> {
44        self.sections.iter().flat_map(|s| &s.fields).collect()
45    }
46    
47    /// Validate configuration against this schema
48    pub fn validate(&self, config: &HashMap<String, serde_json::Value>) -> Result<(), ConfigError> {
49        for section in &self.sections {
50            section.validate_fields(config)?;
51        }
52        Ok(())
53    }
54    
55    /// Generate OpenAPI schema
56    pub fn to_openapi_schema(&self) -> serde_json::Value {
57        let mut properties = serde_json::Map::new();
58        let mut required = Vec::new();
59        
60        for section in &self.sections {
61            for field in &section.fields {
62                properties.insert(
63                    field.name.clone(),
64                    field.to_openapi_property()
65                );
66                
67                if field.required {
68                    required.push(field.name.clone());
69                }
70            }
71        }
72        
73        serde_json::json!({
74            "type": "object",
75            "title": self.name,
76            "description": self.description.as_deref().unwrap_or("Configuration schema"),
77            "properties": properties,
78            "required": required
79        })
80    }
81}
82
83/// Configuration section for grouping related fields
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ConfigSection {
86    pub name: String,
87    pub description: Option<String>,
88    pub fields: Vec<ConfigField>,
89}
90
91impl ConfigSection {
92    /// Create a new configuration section
93    pub fn new(name: impl Into<String>) -> Self {
94        Self {
95            name: name.into(),
96            description: None,
97            fields: Vec::new(),
98        }
99    }
100    
101    /// Set section description
102    pub fn with_description(mut self, description: impl Into<String>) -> Self {
103        self.description = Some(description.into());
104        self
105    }
106    
107    /// Add a field to this section
108    pub fn add_field(mut self, field: ConfigField) -> Self {
109        self.fields.push(field);
110        self
111    }
112    
113    /// Get field by name
114    pub fn get_field(&self, name: &str) -> Option<&ConfigField> {
115        self.fields.iter().find(|f| f.name == name)
116    }
117    
118    /// Validate fields in this section
119    pub fn validate_fields(&self, config: &HashMap<String, serde_json::Value>) -> Result<(), ConfigError> {
120        for field in &self.fields {
121            if field.required && !config.contains_key(&field.name) {
122                return Err(ConfigError::missing_required(
123                    &field.name,
124                    field.description.as_deref().unwrap_or("This field is required"),
125                ));
126            }
127            
128            if let Some(value) = config.get(&field.name) {
129                self.validate_field_value(field, value)?;
130            }
131        }
132        Ok(())
133    }
134    
135    /// Validate a single field value
136    fn validate_field_value(&self, field: &ConfigField, value: &serde_json::Value) -> Result<(), ConfigError> {
137        match field.field_type.as_str() {
138            "string" => {
139                if !value.is_string() {
140                    return Err(ConfigError::invalid_value(&field.name, value.to_string(), "string"));
141                }
142            }
143            "integer" | "int" => {
144                if !value.is_i64() {
145                    return Err(ConfigError::invalid_value(&field.name, value.to_string(), "integer"));
146                }
147            }
148            "number" | "float" => {
149                if !value.is_f64() && !value.is_i64() {
150                    return Err(ConfigError::invalid_value(&field.name, value.to_string(), "number"));
151                }
152            }
153            "boolean" | "bool" => {
154                if !value.is_boolean() {
155                    return Err(ConfigError::invalid_value(&field.name, value.to_string(), "boolean"));
156                }
157            }
158            "array" => {
159                if !value.is_array() {
160                    return Err(ConfigError::invalid_value(&field.name, value.to_string(), "array"));
161                }
162            }
163            "object" => {
164                if !value.is_object() {
165                    return Err(ConfigError::invalid_value(&field.name, value.to_string(), "object"));
166                }
167            }
168            _ => {
169                // Custom type - no validation for now
170            }
171        }
172        
173        Ok(())
174    }
175}
176
177impl ConfigField {
178    /// Convert field to OpenAPI property definition
179    pub fn to_openapi_property(&self) -> serde_json::Value {
180        let mut property = serde_json::Map::new();
181        
182        // Set type
183        let openapi_type = match self.field_type.as_str() {
184            "integer" | "int" => "integer",
185            "number" | "float" => "number",
186            "boolean" | "bool" => "boolean",
187            "array" => "array",
188            "object" => "object",
189            _ => "string",
190        };
191        property.insert("type".to_string(), serde_json::Value::String(openapi_type.to_string()));
192        
193        // Set description
194        if let Some(desc) = &self.description {
195            property.insert("description".to_string(), serde_json::Value::String(desc.clone()));
196        }
197        
198        // Set default value
199        if let Some(default) = &self.default_value {
200            let default_value = match openapi_type {
201                "integer" => serde_json::Value::Number(
202                    serde_json::Number::from(default.parse::<i64>().unwrap_or(0))
203                ),
204                "number" => serde_json::Value::Number(
205                    serde_json::Number::from_f64(default.parse::<f64>().unwrap_or(0.0)).unwrap()
206                ),
207                "boolean" => serde_json::Value::Bool(default.parse::<bool>().unwrap_or(false)),
208                _ => serde_json::Value::String(default.clone()),
209            };
210            property.insert("default".to_string(), default_value);
211        }
212        
213        serde_json::Value::Object(property)
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220    
221    #[test]
222    fn test_configuration_schema() {
223        let schema = ConfigurationSchema::new("app_config", "1.0")
224            .with_description("Application configuration")
225            .add_section(
226                ConfigSection::new("database")
227                    .with_description("Database configuration")
228                    .add_field(ConfigField::new("url", "string").required())
229                    .add_field(ConfigField::new("max_connections", "integer").with_default("10"))
230            );
231        
232        assert_eq!(schema.name, "app_config");
233        assert_eq!(schema.sections.len(), 1);
234        assert_eq!(schema.all_fields().len(), 2);
235    }
236    
237    #[test]
238    fn test_openapi_schema_generation() {
239        let schema = ConfigurationSchema::new("test_config", "1.0")
240            .add_section(
241                ConfigSection::new("general")
242                    .add_field(
243                        ConfigField::new("name", "string")
244                            .required()
245                            .with_description("Application name")
246                    )
247                    .add_field(
248                        ConfigField::new("port", "integer")
249                            .with_default("3000")
250                            .with_description("Server port")
251                    )
252            );
253        
254        let openapi_schema = schema.to_openapi_schema();
255        
256        assert_eq!(openapi_schema["type"], "object");
257        assert_eq!(openapi_schema["title"], "test_config");
258        assert!(openapi_schema["properties"]["name"].is_object());
259        assert!(openapi_schema["properties"]["port"].is_object());
260        assert_eq!(openapi_schema["required"].as_array().unwrap().len(), 1);
261    }
262}