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
134            .schemes
135            .iter()
136            .any(|scheme| value.starts_with(&format!("{}://", scheme)));
137
138        if !has_valid_scheme {
139            return Err(ConfigError::invalid_value(
140                "url",
141                value.clone(),
142                format!("URL with scheme: {}", self.schemes.join(", ")),
143            ));
144        }
145
146        // Check for host if required
147        if self.require_host && !value.contains("://") {
148            return Err(ConfigError::invalid_value(
149                "url",
150                value.clone(),
151                "URL with host",
152            ));
153        }
154
155        Ok(())
156    }
157}
158
159/// Required field validator
160pub struct RequiredValidator;
161
162impl<T> ConfigValidator<Option<T>> for RequiredValidator {
163    fn validate(&self, value: &Option<T>) -> Result<(), ConfigError> {
164        if value.is_none() {
165            return Err(ConfigError::missing_required(
166                "field",
167                "This field is required",
168            ));
169        }
170        Ok(())
171    }
172}
173
174/// String length validator
175pub struct LengthValidator {
176    pub min_length: usize,
177    pub max_length: Option<usize>,
178}
179
180impl LengthValidator {
181    pub fn min(min_length: usize) -> Self {
182        Self {
183            min_length,
184            max_length: None,
185        }
186    }
187
188    pub fn range(min_length: usize, max_length: usize) -> Self {
189        Self {
190            min_length,
191            max_length: Some(max_length),
192        }
193    }
194}
195
196impl ConfigValidator<String> for LengthValidator {
197    fn validate(&self, value: &String) -> Result<(), ConfigError> {
198        if value.len() < self.min_length {
199            return Err(ConfigError::invalid_value(
200                "string",
201                value.clone(),
202                format!("string with at least {} characters", self.min_length),
203            ));
204        }
205
206        if let Some(max_length) = self.max_length {
207            if value.len() > max_length {
208                return Err(ConfigError::invalid_value(
209                    "string",
210                    value.clone(),
211                    format!("string with at most {} characters", max_length),
212                ));
213            }
214        }
215
216        Ok(())
217    }
218}
219
220/// Composite validator that runs multiple validators
221pub struct CompositeValidator<T> {
222    validators: Vec<Box<dyn ConfigValidator<T>>>,
223}
224
225impl<T> CompositeValidator<T> {
226    pub fn new() -> Self {
227        Self {
228            validators: Vec::new(),
229        }
230    }
231
232    pub fn add_validator(mut self, validator: Box<dyn ConfigValidator<T>>) -> Self {
233        self.validators.push(validator);
234        self
235    }
236}
237
238impl<T> Default for CompositeValidator<T> {
239    fn default() -> Self {
240        Self::new()
241    }
242}
243
244impl<T> ConfigValidator<T> for CompositeValidator<T> {
245    fn validate(&self, value: &T) -> Result<(), ConfigError> {
246        for validator in &self.validators {
247            validator.validate(value)?;
248        }
249        Ok(())
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn test_port_validator() {
259        let validator = PortValidator::default();
260
261        assert!(validator.validate(&80).is_ok());
262        assert!(validator.validate(&443).is_ok());
263        assert!(validator.validate(&65535).is_ok());
264        assert!(validator.validate(&0).is_err());
265    }
266
267    #[test]
268    fn test_url_validator() {
269        let validator = UrlValidator::default();
270
271        assert!(validator
272            .validate(&"https://example.com".to_string())
273            .is_ok());
274        assert!(validator
275            .validate(&"http://localhost:3000".to_string())
276            .is_ok());
277        assert!(validator
278            .validate(&"ftp://example.com".to_string())
279            .is_err());
280        assert!(validator.validate(&"not-a-url".to_string()).is_err());
281    }
282
283    #[test]
284    fn test_length_validator() {
285        let validator = LengthValidator::range(3, 10);
286
287        assert!(validator.validate(&"hello".to_string()).is_ok());
288        assert!(validator.validate(&"hi".to_string()).is_err()); // Too short
289        assert!(validator.validate(&"this is too long".to_string()).is_err()); // Too long
290    }
291}