1use crate::config::{ConfigError, ConfigField};
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(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#[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 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 pub fn with_description(mut self, description: impl Into<String>) -> Self {
100 self.description = Some(description.into());
101 self
102 }
103
104 pub fn add_field(mut self, field: ConfigField) -> Self {
106 self.fields.push(field);
107 self
108 }
109
110 pub fn get_field(&self, name: &str) -> Option<&ConfigField> {
112 self.fields.iter().find(|f| f.name == name)
113 }
114
115 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 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 }
202 }
203
204 Ok(())
205 }
206}
207
208impl ConfigField {
209 pub fn to_openapi_property(&self) -> serde_json::Value {
211 let mut property = serde_json::Map::new();
212
213 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 if let Some(desc) = &self.description {
229 property.insert(
230 "description".to_string(),
231 serde_json::Value::String(desc.clone()),
232 );
233 }
234
235 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}