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.description.as_deref().unwrap_or("This field is required"),
94 ));
95 }
96 }
97
98 for (key, value) in config {
100 if let Some(field) = self.get_field(key) {
101 self.validate_field_value(field, value)?;
102 }
103 }
104
105 Ok(())
106 }
107
108 fn validate_field_value(&self, field: &ConfigField, value: &str) -> Result<(), ConfigError> {
110 match field.field_type.as_str() {
112 "integer" | "int" => {
113 value.parse::<i64>().map_err(|_| {
114 ConfigError::invalid_value(&field.name, value, "valid integer")
115 })?;
116 }
117 "float" | "number" => {
118 value.parse::<f64>().map_err(|_| {
119 ConfigError::invalid_value(&field.name, value, "valid number")
120 })?;
121 }
122 "boolean" | "bool" => {
123 value.parse::<bool>().map_err(|_| {
124 ConfigError::invalid_value(&field.name, value, "true or false")
125 })?;
126 }
127 "url" => {
128 if !value.starts_with("http://") && !value.starts_with("https://") {
129 return Err(ConfigError::invalid_value(&field.name, value, "valid URL"));
130 }
131 }
132 _ => {
133 }
135 }
136
137 for rule in &field.validation_rules {
139 self.apply_validation_rule(field, value, rule)?;
140 }
141
142 Ok(())
143 }
144
145 fn apply_validation_rule(&self, field: &ConfigField, value: &str, rule: &str) -> Result<(), ConfigError> {
147 if rule.starts_with("min_length:") {
148 let min_len: usize = rule.strip_prefix("min_length:").unwrap().parse()
149 .map_err(|_| ConfigError::validation_failed("Invalid min_length rule"))?;
150 if value.len() < min_len {
151 return Err(ConfigError::invalid_value(
152 &field.name,
153 value,
154 format!("at least {} characters", min_len)
155 ));
156 }
157 } else if rule.starts_with("max_length:") {
158 let max_len: usize = rule.strip_prefix("max_length:").unwrap().parse()
159 .map_err(|_| ConfigError::validation_failed("Invalid max_length rule"))?;
160 if value.len() > max_len {
161 return Err(ConfigError::invalid_value(
162 &field.name,
163 value,
164 format!("at most {} characters", max_len)
165 ));
166 }
167 } else if rule.starts_with("pattern:") {
168 let pattern = rule.strip_prefix("pattern:").unwrap();
169 if !value.contains(pattern) {
171 return Err(ConfigError::invalid_value(
172 &field.name,
173 value,
174 format!("matching pattern: {}", pattern)
175 ));
176 }
177 }
178
179 Ok(())
180 }
181}
182
183#[builder]
185pub struct ConfigBuilder<T> {
186 #[builder(default)]
187 pub fields: Vec<ConfigField>,
188
189 #[builder(optional)]
190 pub name: Option<String>,
191
192 #[builder(default)]
193 pub _phantom: std::marker::PhantomData<T>,
194}
195
196impl<T> std::fmt::Debug for ConfigBuilder<T> {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 f.debug_struct("ConfigBuilder")
199 .field("fields_count", &self.fields.len())
200 .field("name", &self.name)
201 .finish()
202 }
203}
204
205impl<T> ConfigBuilder<T> {
206 pub fn build_schema(self) -> ConfigSchema {
208 ConfigSchema {
209 name: self.name.unwrap_or_else(|| "DefaultConfig".to_string()),
210 fields: self.fields,
211 }
212 }
213}
214
215impl<T> ConfigBuilderBuilder<T> {
217 pub fn add_field(self, field: ConfigField) -> Self {
219 let mut fields_vec = self.fields.unwrap_or_default();
220 fields_vec.push(field);
221 ConfigBuilderBuilder {
222 fields: Some(fields_vec),
223 name: self.name,
224 _phantom: self._phantom,
225 }
226 }
227
228 pub fn add_string_field(self, name: impl Into<String>) -> Self {
230 self.add_field(ConfigField::new(name, "string"))
231 }
232
233 pub fn add_required_string_field(self, name: impl Into<String>) -> Self {
235 self.add_field(ConfigField::new(name, "string").required())
236 }
237
238 pub fn add_int_field(self, name: impl Into<String>) -> Self {
240 self.add_field(ConfigField::new(name, "integer"))
241 }
242
243 pub fn add_bool_field(self, name: impl Into<String>) -> Self {
245 self.add_field(ConfigField::new(name, "boolean"))
246 }
247
248 pub fn add_url_field(self, name: impl Into<String>) -> Self {
250 self.add_field(ConfigField::new(name, "url"))
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_config_schema() {
260 let schema = ConfigSchema::new("test_config")
261 .add_field(ConfigField::new("name", "string").required())
262 .add_field(ConfigField::new("port", "integer").with_default("3000"))
263 .add_field(ConfigField::new("debug", "boolean").with_default("false"));
264
265 assert_eq!(schema.name, "test_config");
266 assert_eq!(schema.fields.len(), 3);
267 assert_eq!(schema.required_fields().len(), 1);
268 }
269
270 #[test]
271 fn test_config_validation() {
272 let schema = ConfigSchema::new("test_config")
273 .add_field(ConfigField::new("name", "string").required())
274 .add_field(ConfigField::new("port", "integer"));
275
276 let mut config = HashMap::new();
277 config.insert("name".to_string(), "test".to_string());
278 config.insert("port".to_string(), "3000".to_string());
279
280 assert!(schema.validate_config(&config).is_ok());
281
282 config.remove("name");
283 assert!(schema.validate_config(&config).is_err());
284 }
285}