elif_core/config/
schema.rs

1use crate::config::{ConfigError, ConfigField};
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(field.name.clone(), field.to_openapi_property());
63
64                if field.required {
65                    required.push(field.name.clone());
66                }
67            }
68        }
69
70        serde_json::json!({
71            "type": "object",
72            "title": self.name,
73            "description": self.description.as_deref().unwrap_or("Configuration schema"),
74            "properties": properties,
75            "required": required
76        })
77    }
78}
79
80/// Configuration section for grouping related fields
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct ConfigSection {
83    pub name: String,
84    pub description: Option<String>,
85    pub fields: Vec<ConfigField>,
86}
87
88impl ConfigSection {
89    /// Create a new configuration section
90    pub fn new(name: impl Into<String>) -> Self {
91        Self {
92            name: name.into(),
93            description: None,
94            fields: Vec::new(),
95        }
96    }
97
98    /// Set section description
99    pub fn with_description(mut self, description: impl Into<String>) -> Self {
100        self.description = Some(description.into());
101        self
102    }
103
104    /// Add a field to this section
105    pub fn add_field(mut self, field: ConfigField) -> Self {
106        self.fields.push(field);
107        self
108    }
109
110    /// Get field by name
111    pub fn get_field(&self, name: &str) -> Option<&ConfigField> {
112        self.fields.iter().find(|f| f.name == name)
113    }
114
115    /// Validate fields in this section
116    pub fn validate_fields(
117        &self,
118        config: &HashMap<String, serde_json::Value>,
119    ) -> 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
125                        .description
126                        .as_deref()
127                        .unwrap_or("This field is required"),
128                ));
129            }
130
131            if let Some(value) = config.get(&field.name) {
132                self.validate_field_value(field, value)?;
133            }
134        }
135        Ok(())
136    }
137
138    /// Validate a single field value
139    fn validate_field_value(
140        &self,
141        field: &ConfigField,
142        value: &serde_json::Value,
143    ) -> Result<(), ConfigError> {
144        match field.field_type.as_str() {
145            "string" => {
146                if !value.is_string() {
147                    return Err(ConfigError::invalid_value(
148                        &field.name,
149                        value.to_string(),
150                        "string",
151                    ));
152                }
153            }
154            "integer" | "int" => {
155                if !value.is_i64() {
156                    return Err(ConfigError::invalid_value(
157                        &field.name,
158                        value.to_string(),
159                        "integer",
160                    ));
161                }
162            }
163            "number" | "float" => {
164                if !value.is_f64() && !value.is_i64() {
165                    return Err(ConfigError::invalid_value(
166                        &field.name,
167                        value.to_string(),
168                        "number",
169                    ));
170                }
171            }
172            "boolean" | "bool" => {
173                if !value.is_boolean() {
174                    return Err(ConfigError::invalid_value(
175                        &field.name,
176                        value.to_string(),
177                        "boolean",
178                    ));
179                }
180            }
181            "array" => {
182                if !value.is_array() {
183                    return Err(ConfigError::invalid_value(
184                        &field.name,
185                        value.to_string(),
186                        "array",
187                    ));
188                }
189            }
190            "object" => {
191                if !value.is_object() {
192                    return Err(ConfigError::invalid_value(
193                        &field.name,
194                        value.to_string(),
195                        "object",
196                    ));
197                }
198            }
199            _ => {
200                // Custom type - no validation for now
201            }
202        }
203
204        Ok(())
205    }
206}
207
208impl ConfigField {
209    /// Convert field to OpenAPI property definition
210    pub fn to_openapi_property(&self) -> serde_json::Value {
211        let mut property = serde_json::Map::new();
212
213        // Set type
214        let openapi_type = match self.field_type.as_str() {
215            "integer" | "int" => "integer",
216            "number" | "float" => "number",
217            "boolean" | "bool" => "boolean",
218            "array" => "array",
219            "object" => "object",
220            _ => "string",
221        };
222        property.insert(
223            "type".to_string(),
224            serde_json::Value::String(openapi_type.to_string()),
225        );
226
227        // Set description
228        if let Some(desc) = &self.description {
229            property.insert(
230                "description".to_string(),
231                serde_json::Value::String(desc.clone()),
232            );
233        }
234
235        // Set default value
236        if let Some(default) = &self.default_value {
237            let default_value = match openapi_type {
238                "integer" => serde_json::Value::Number(serde_json::Number::from(
239                    default.parse::<i64>().unwrap_or(0),
240                )),
241                "number" => serde_json::Value::Number(
242                    serde_json::Number::from_f64(default.parse::<f64>().unwrap_or(0.0)).unwrap(),
243                ),
244                "boolean" => serde_json::Value::Bool(default.parse::<bool>().unwrap_or(false)),
245                _ => serde_json::Value::String(default.clone()),
246            };
247            property.insert("default".to_string(), default_value);
248        }
249
250        serde_json::Value::Object(property)
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_configuration_schema() {
260        let schema = ConfigurationSchema::new("app_config", "1.0")
261            .with_description("Application configuration")
262            .add_section(
263                ConfigSection::new("database")
264                    .with_description("Database configuration")
265                    .add_field(ConfigField::new("url", "string").required())
266                    .add_field(ConfigField::new("max_connections", "integer").with_default("10")),
267            );
268
269        assert_eq!(schema.name, "app_config");
270        assert_eq!(schema.sections.len(), 1);
271        assert_eq!(schema.all_fields().len(), 2);
272    }
273
274    #[test]
275    fn test_openapi_schema_generation() {
276        let schema = ConfigurationSchema::new("test_config", "1.0").add_section(
277            ConfigSection::new("general")
278                .add_field(
279                    ConfigField::new("name", "string")
280                        .required()
281                        .with_description("Application name"),
282                )
283                .add_field(
284                    ConfigField::new("port", "integer")
285                        .with_default("3000")
286                        .with_description("Server port"),
287                ),
288        );
289
290        let openapi_schema = schema.to_openapi_schema();
291
292        assert_eq!(openapi_schema["type"], "object");
293        assert_eq!(openapi_schema["title"], "test_config");
294        assert!(openapi_schema["properties"]["name"].is_object());
295        assert!(openapi_schema["properties"]["port"].is_object());
296        assert_eq!(openapi_schema["required"].as_array().unwrap().len(), 1);
297    }
298}