elif_core/config/
schema.rs1use crate::config::{ConfigField, ConfigError};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[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 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 pub fn with_description(mut self, description: impl Into<String>) -> Self {
27 self.description = Some(description.into());
28 self
29 }
30
31 pub fn add_section(mut self, section: ConfigSection) -> Self {
33 self.sections.push(section);
34 self
35 }
36
37 pub fn get_section(&self, name: &str) -> Option<&ConfigSection> {
39 self.sections.iter().find(|s| s.name == name)
40 }
41
42 pub fn all_fields(&self) -> Vec<&ConfigField> {
44 self.sections.iter().flat_map(|s| &s.fields).collect()
45 }
46
47 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 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 §ion.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#[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 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 pub fn with_description(mut self, description: impl Into<String>) -> Self {
103 self.description = Some(description.into());
104 self
105 }
106
107 pub fn add_field(mut self, field: ConfigField) -> Self {
109 self.fields.push(field);
110 self
111 }
112
113 pub fn get_field(&self, name: &str) -> Option<&ConfigField> {
115 self.fields.iter().find(|f| f.name == name)
116 }
117
118 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 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 }
171 }
172
173 Ok(())
174 }
175}
176
177impl ConfigField {
178 pub fn to_openapi_property(&self) -> serde_json::Value {
180 let mut property = serde_json::Map::new();
181
182 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 if let Some(desc) = &self.description {
195 property.insert("description".to_string(), serde_json::Value::String(desc.clone()));
196 }
197
198 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}