elif_core/config/
validation.rs1use thiserror::Error;
2
3#[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 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 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 pub fn validation_failed(message: impl Into<String>) -> Self {
62 Self::ValidationFailed {
63 message: message.into(),
64 }
65 }
66
67 pub fn environment_error(message: impl Into<String>) -> Self {
69 Self::EnvironmentError {
70 message: message.into(),
71 }
72 }
73}
74
75pub trait ConfigValidator<T> {
77 fn validate(&self, value: &T) -> Result<(), ConfigError>;
79}
80
81pub 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
106pub 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 if value.is_empty() {
125 return Err(ConfigError::invalid_value(
126 "url",
127 value.clone(),
128 "non-empty URL",
129 ));
130 }
131
132 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 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
159pub 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
174pub 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
220pub 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()); assert!(validator.validate(&"this is too long".to_string()).is_err()); }
291}