elif_core/config/
validation.rs

1use thiserror::Error;
2
3/// Configuration error type
4#[derive(Debug, Error)]
5pub enum ConfigError {
6    #[error("Missing required field: {field}. {hint}")]
7    MissingRequired { field: String, hint: String },
8    
9    #[error("Invalid value for field '{field}': '{value}'. Expected: {expected}")]
10    InvalidValue {
11        field: String,
12        value: String,
13        expected: String,
14    },
15    
16    #[error("Configuration validation failed: {message}")]
17    ValidationFailed { message: String },
18    
19    #[error("Environment variable error: {message}")]
20    EnvironmentError { message: String },
21    
22    #[error("File system error: {message}")]
23    FileSystemError { message: String },
24    
25    #[error("Parsing error: {message}")]
26    ParsingError { message: String },
27    
28    #[error("IO error: {0}")]
29    Io(#[from] std::io::Error),
30    
31    #[error("YAML error: {0}")]
32    Yaml(#[from] serde_yaml::Error),
33    
34    #[error("JSON error: {0}")]
35    Json(#[from] serde_json::Error),
36}
37
38impl ConfigError {
39    /// Create a missing required field error
40    pub fn missing_required(field: impl Into<String>, hint: impl Into<String>) -> Self {
41        Self::MissingRequired {
42            field: field.into(),
43            hint: hint.into(),
44        }
45    }
46    
47    /// Create an invalid value error
48    pub fn invalid_value(
49        field: impl Into<String>,
50        value: impl Into<String>,
51        expected: impl Into<String>,
52    ) -> Self {
53        Self::InvalidValue {
54            field: field.into(),
55            value: value.into(),
56            expected: expected.into(),
57        }
58    }
59    
60    /// Create a validation failed error
61    pub fn validation_failed(message: impl Into<String>) -> Self {
62        Self::ValidationFailed {
63            message: message.into(),
64        }
65    }
66    
67    /// Create an environment error
68    pub fn environment_error(message: impl Into<String>) -> Self {
69        Self::EnvironmentError {
70            message: message.into(),
71        }
72    }
73}
74
75/// Trait for validating configuration values
76pub trait ConfigValidator<T> {
77    /// Validate a configuration value
78    fn validate(&self, value: &T) -> Result<(), ConfigError>;
79}
80
81/// Port number validator
82pub struct PortValidator {
83    pub min: u16,
84    pub max: u16,
85}
86
87impl Default for PortValidator {
88    fn default() -> Self {
89        Self { min: 1, max: 65535 }
90    }
91}
92
93impl ConfigValidator<u16> for PortValidator {
94    fn validate(&self, value: &u16) -> Result<(), ConfigError> {
95        if *value < self.min || *value > self.max {
96            return Err(ConfigError::invalid_value(
97                "port",
98                value.to_string(),
99                format!("port between {} and {}", self.min, self.max),
100            ));
101        }
102        Ok(())
103    }
104}
105
106/// URL validator
107pub struct UrlValidator {
108    pub schemes: Vec<String>,
109    pub require_host: bool,
110}
111
112impl Default for UrlValidator {
113    fn default() -> Self {
114        Self {
115            schemes: vec!["http".to_string(), "https".to_string()],
116            require_host: true,
117        }
118    }
119}
120
121impl ConfigValidator<String> for UrlValidator {
122    fn validate(&self, value: &String) -> Result<(), ConfigError> {
123        // Basic URL validation (in a real implementation, use a proper URL parser)
124        if value.is_empty() {
125            return Err(ConfigError::invalid_value(
126                "url",
127                value.clone(),
128                "non-empty URL",
129            ));
130        }
131        
132        // Check scheme
133        let has_valid_scheme = self.schemes.iter().any(|scheme| {
134            value.starts_with(&format!("{}://", scheme))
135        });
136        
137        if !has_valid_scheme {
138            return Err(ConfigError::invalid_value(
139                "url",
140                value.clone(),
141                format!("URL with scheme: {}", self.schemes.join(", ")),
142            ));
143        }
144        
145        // Check for host if required
146        if self.require_host && !value.contains("://") {
147            return Err(ConfigError::invalid_value(
148                "url",
149                value.clone(),
150                "URL with host",
151            ));
152        }
153        
154        Ok(())
155    }
156}
157
158/// Required field validator
159pub struct RequiredValidator;
160
161impl<T> ConfigValidator<Option<T>> for RequiredValidator {
162    fn validate(&self, value: &Option<T>) -> Result<(), ConfigError> {
163        if value.is_none() {
164            return Err(ConfigError::missing_required(
165                "field",
166                "This field is required",
167            ));
168        }
169        Ok(())
170    }
171}
172
173/// String length validator
174pub struct LengthValidator {
175    pub min_length: usize,
176    pub max_length: Option<usize>,
177}
178
179impl LengthValidator {
180    pub fn min(min_length: usize) -> Self {
181        Self {
182            min_length,
183            max_length: None,
184        }
185    }
186    
187    pub fn range(min_length: usize, max_length: usize) -> Self {
188        Self {
189            min_length,
190            max_length: Some(max_length),
191        }
192    }
193}
194
195impl ConfigValidator<String> for LengthValidator {
196    fn validate(&self, value: &String) -> Result<(), ConfigError> {
197        if value.len() < self.min_length {
198            return Err(ConfigError::invalid_value(
199                "string",
200                value.clone(),
201                format!("string with at least {} characters", self.min_length),
202            ));
203        }
204        
205        if let Some(max_length) = self.max_length {
206            if value.len() > max_length {
207                return Err(ConfigError::invalid_value(
208                    "string",
209                    value.clone(),
210                    format!("string with at most {} characters", max_length),
211                ));
212            }
213        }
214        
215        Ok(())
216    }
217}
218
219/// Composite validator that runs multiple validators
220pub struct CompositeValidator<T> {
221    validators: Vec<Box<dyn ConfigValidator<T>>>,
222}
223
224impl<T> CompositeValidator<T> {
225    pub fn new() -> Self {
226        Self {
227            validators: Vec::new(),
228        }
229    }
230    
231    pub fn add_validator(mut self, validator: Box<dyn ConfigValidator<T>>) -> Self {
232        self.validators.push(validator);
233        self
234    }
235}
236
237impl<T> Default for CompositeValidator<T> {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243impl<T> ConfigValidator<T> for CompositeValidator<T> {
244    fn validate(&self, value: &T) -> Result<(), ConfigError> {
245        for validator in &self.validators {
246            validator.validate(value)?;
247        }
248        Ok(())
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    
256    #[test]
257    fn test_port_validator() {
258        let validator = PortValidator::default();
259        
260        assert!(validator.validate(&80).is_ok());
261        assert!(validator.validate(&443).is_ok());
262        assert!(validator.validate(&65535).is_ok());
263        assert!(validator.validate(&0).is_err());
264    }
265    
266    #[test]
267    fn test_url_validator() {
268        let validator = UrlValidator::default();
269        
270        assert!(validator.validate(&"https://example.com".to_string()).is_ok());
271        assert!(validator.validate(&"http://localhost:3000".to_string()).is_ok());
272        assert!(validator.validate(&"ftp://example.com".to_string()).is_err());
273        assert!(validator.validate(&"not-a-url".to_string()).is_err());
274    }
275    
276    #[test]
277    fn test_length_validator() {
278        let validator = LengthValidator::range(3, 10);
279        
280        assert!(validator.validate(&"hello".to_string()).is_ok());
281        assert!(validator.validate(&"hi".to_string()).is_err()); // Too short
282        assert!(validator.validate(&"this is too long".to_string()).is_err()); // Too long
283    }
284}