1use crate::config::ConfigError;
2use service_builder::builder;
3use std::collections::HashMap;
4
5#[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 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 pub fn required(mut self) -> Self {
31 self.required = true;
32 self
33 }
34
35 pub fn with_default(mut self, default: impl Into<String>) -> Self {
37 self.default_value = Some(default.into());
38 self
39 }
40
41 pub fn with_description(mut self, description: impl Into<String>) -> Self {
43 self.description = Some(description.into());
44 self
45 }
46
47 pub fn add_validation(mut self, rule: impl Into<String>) -> Self {
49 self.validation_rules.push(rule.into());
50 self
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct ConfigSchema {
57 pub name: String,
58 pub fields: Vec<ConfigField>,
59}
60
61impl ConfigSchema {
62 pub fn new(name: impl Into<String>) -> Self {
64 Self {
65 name: name.into(),
66 fields: Vec::new(),
67 }
68 }
69
70 pub fn add_field(mut self, field: ConfigField) -> Self {
72 self.fields.push(field);
73 self
74 }
75
76 pub fn get_field(&self, name: &str) -> Option<&ConfigField> {
78 self.fields.iter().find(|f| f.name == name)
79 }
80
81 pub fn required_fields(&self) -> Vec<&ConfigField> {
83 self.fields.iter().filter(|f| f.required).collect()
84 }
85
86 pub fn validate_config(&self, config: &HashMap<String, String>) -> Result<(), ConfigError> {
88 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 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 fn validate_field_value(&self, field: &ConfigField, value: &str) -> Result<(), ConfigError> {
113 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 }
138 }
139
140 for rule in &field.validation_rules {
142 self.apply_validation_rule(field, value, rule)?;
143 }
144
145 Ok(())
146 }
147
148 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 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#[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 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
229impl<T> ConfigBuilderBuilder<T> {
231 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 pub fn add_string_field(self, name: impl Into<String>) -> Self {
244 self.add_field(ConfigField::new(name, "string"))
245 }
246
247 pub fn add_required_string_field(self, name: impl Into<String>) -> Self {
249 self.add_field(ConfigField::new(name, "string").required())
250 }
251
252 pub fn add_int_field(self, name: impl Into<String>) -> Self {
254 self.add_field(ConfigField::new(name, "integer"))
255 }
256
257 pub fn add_bool_field(self, name: impl Into<String>) -> Self {
259 self.add_field(ConfigField::new(name, "boolean"))
260 }
261
262 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}