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.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 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
158pub 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
173pub 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
219pub 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()); assert!(validator.validate(&"this is too long".to_string()).is_err()); }
284}