elif_http/request/
validation.rs

1//! Request validation utilities
2
3use crate::errors::{HttpError, HttpResult};
4
5/// Validation trait for request data
6pub trait Validate {
7    fn validate(&self) -> HttpResult<()>;
8}
9
10/// Helper functions for common validation patterns
11pub fn validate_required<T>(field: &Option<T>, field_name: &str) -> HttpResult<()> {
12    if field.is_none() {
13        return Err(HttpError::bad_request(format!(
14            "{} is required",
15            field_name
16        )));
17    }
18    Ok(())
19}
20
21pub fn validate_min_length(value: &str, min: usize, field_name: &str) -> HttpResult<()> {
22    if value.len() < min {
23        return Err(HttpError::bad_request(format!(
24            "{} must be at least {} characters long",
25            field_name, min
26        )));
27    }
28    Ok(())
29}
30
31pub fn validate_max_length(value: &str, max: usize, field_name: &str) -> HttpResult<()> {
32    if value.len() > max {
33        return Err(HttpError::bad_request(format!(
34            "{} must be at most {} characters long",
35            field_name, max
36        )));
37    }
38    Ok(())
39}
40
41pub fn validate_email(email: &str, field_name: &str) -> HttpResult<()> {
42    // Basic email validation - must have @ and . with content around them
43    if !email.contains('@') || !email.contains('.') {
44        return Err(HttpError::bad_request(format!(
45            "{} must be a valid email address",
46            field_name
47        )));
48    }
49
50    // Must have at least one character before @
51    let at_pos = email.find('@').unwrap();
52    if at_pos == 0 {
53        return Err(HttpError::bad_request(format!(
54            "{} must be a valid email address",
55            field_name
56        )));
57    }
58
59    // Must have content after @ and a dot in the domain part
60    let domain_part = &email[at_pos + 1..];
61    if domain_part.is_empty() || !domain_part.contains('.') {
62        return Err(HttpError::bad_request(format!(
63            "{} must be a valid email address",
64            field_name
65        )));
66    }
67
68    // Domain must have content after the last dot
69    let last_dot_pos = domain_part.rfind('.').unwrap();
70    if last_dot_pos == domain_part.len() - 1 {
71        return Err(HttpError::bad_request(format!(
72            "{} must be a valid email address",
73            field_name
74        )));
75    }
76
77    Ok(())
78}
79
80pub fn validate_range<T: PartialOrd>(value: T, min: T, max: T, field_name: &str) -> HttpResult<()> {
81    if value < min || value > max {
82        return Err(HttpError::bad_request(format!(
83            "{} must be between {} and {}",
84            field_name,
85            std::any::type_name::<T>(),
86            std::any::type_name::<T>()
87        )));
88    }
89    Ok(())
90}
91
92pub fn validate_pattern(value: &str, pattern: &str, field_name: &str) -> HttpResult<()> {
93    if !regex_simple_match(value, pattern) {
94        return Err(HttpError::bad_request(format!(
95            "{} does not match required pattern",
96            field_name
97        )));
98    }
99    Ok(())
100}
101
102// Simple pattern matching without regex dependency - for basic patterns
103fn regex_simple_match(value: &str, pattern: &str) -> bool {
104    match pattern {
105        "alphanumeric" => value.chars().all(char::is_alphanumeric),
106        "numeric" => value.chars().all(char::is_numeric),
107        "alpha" => value.chars().all(char::is_alphabetic),
108        _ => true, // Default to true for unknown patterns
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_validate_required_success() {
118        let value = Some("test");
119        let result = validate_required(&value, "test_field");
120        assert!(result.is_ok());
121    }
122
123    #[test]
124    fn test_validate_required_failure() {
125        let value: Option<String> = None;
126        let result = validate_required(&value, "test_field");
127        assert!(result.is_err());
128
129        if let Err(HttpError::BadRequest { message }) = result {
130            assert!(message.contains("test_field is required"));
131        } else {
132            panic!("Expected BadRequest error");
133        }
134    }
135
136    #[test]
137    fn test_validate_min_length_success() {
138        let result = validate_min_length("hello world", 5, "message");
139        assert!(result.is_ok());
140    }
141
142    #[test]
143    fn test_validate_min_length_failure() {
144        let result = validate_min_length("hi", 5, "message");
145        assert!(result.is_err());
146
147        if let Err(HttpError::BadRequest { message }) = result {
148            assert!(message.contains("message must be at least 5 characters long"));
149        } else {
150            panic!("Expected BadRequest error");
151        }
152    }
153
154    #[test]
155    fn test_validate_min_length_exact() {
156        let result = validate_min_length("exact", 5, "message");
157        assert!(result.is_ok());
158    }
159
160    #[test]
161    fn test_validate_max_length_success() {
162        let result = validate_max_length("short", 10, "message");
163        assert!(result.is_ok());
164    }
165
166    #[test]
167    fn test_validate_max_length_failure() {
168        let result = validate_max_length("this is a very long message", 10, "message");
169        assert!(result.is_err());
170
171        if let Err(HttpError::BadRequest { message }) = result {
172            assert!(message.contains("message must be at most 10 characters long"));
173        } else {
174            panic!("Expected BadRequest error");
175        }
176    }
177
178    #[test]
179    fn test_validate_max_length_exact() {
180        let result = validate_max_length("exactly10c", 10, "message");
181        assert!(result.is_ok());
182    }
183
184    #[test]
185    fn test_validate_email_valid() {
186        let valid_emails = [
187            "user@example.com",
188            "test.email@domain.org",
189            "user+tag@example.co.uk",
190        ];
191
192        for email in valid_emails {
193            let result = validate_email(email, "email");
194            assert!(result.is_ok(), "Failed to validate email: {}", email);
195        }
196    }
197
198    #[test]
199    fn test_validate_email_invalid() {
200        let invalid_emails = [
201            "invalid",
202            "no-at-sign.com",
203            "no-domain@",
204            "@no-user.com",
205            "no.dot@domain",
206        ];
207
208        for email in invalid_emails {
209            let result = validate_email(email, "email");
210            assert!(
211                result.is_err(),
212                "Should have failed to validate email: {}",
213                email
214            );
215
216            if let Err(HttpError::BadRequest { message }) = result {
217                assert!(message.contains("email must be a valid email address"));
218            } else {
219                panic!("Expected BadRequest error");
220            }
221        }
222    }
223
224    #[test]
225    fn test_validate_range_success() {
226        let result = validate_range(5, 1, 10, "number");
227        assert!(result.is_ok());
228
229        let result = validate_range(1, 1, 10, "number");
230        assert!(result.is_ok());
231
232        let result = validate_range(10, 1, 10, "number");
233        assert!(result.is_ok());
234    }
235
236    #[test]
237    fn test_validate_range_failure() {
238        let result = validate_range(0, 1, 10, "number");
239        assert!(result.is_err());
240
241        let result = validate_range(11, 1, 10, "number");
242        assert!(result.is_err());
243    }
244
245    #[test]
246    fn test_validate_pattern_alphanumeric() {
247        let result = validate_pattern("test123", "alphanumeric", "username");
248        assert!(result.is_ok());
249
250        let result = validate_pattern("test-123", "alphanumeric", "username");
251        assert!(result.is_err());
252    }
253
254    #[test]
255    fn test_validate_pattern_numeric() {
256        let result = validate_pattern("12345", "numeric", "id");
257        assert!(result.is_ok());
258
259        let result = validate_pattern("123a5", "numeric", "id");
260        assert!(result.is_err());
261    }
262
263    #[test]
264    fn test_validate_pattern_alpha() {
265        let result = validate_pattern("hello", "alpha", "name");
266        assert!(result.is_ok());
267
268        let result = validate_pattern("hello123", "alpha", "name");
269        assert!(result.is_err());
270    }
271
272    #[test]
273    fn test_validate_pattern_unknown() {
274        // Unknown patterns should default to true
275        let result = validate_pattern("anything", "unknown_pattern", "field");
276        assert!(result.is_ok());
277    }
278
279    #[test]
280    fn test_validate_trait_implementation() {
281        struct TestStruct {
282            name: Option<String>,
283            email: String,
284            age: u32,
285        }
286
287        impl Validate for TestStruct {
288            fn validate(&self) -> HttpResult<()> {
289                validate_required(&self.name, "name")?;
290                validate_email(&self.email, "email")?;
291                validate_range(self.age, 0, 120, "age")?;
292                Ok(())
293            }
294        }
295
296        let valid_struct = TestStruct {
297            name: Some("John".to_string()),
298            email: "john@example.com".to_string(),
299            age: 25,
300        };
301        assert!(valid_struct.validate().is_ok());
302
303        let invalid_struct = TestStruct {
304            name: None,
305            email: "invalid-email".to_string(),
306            age: 150,
307        };
308        assert!(invalid_struct.validate().is_err());
309    }
310
311    #[test]
312    fn test_chained_validations() {
313        fn validate_user_input(name: &Option<String>, email: &str, age: u32) -> HttpResult<()> {
314            validate_required(name, "name")?;
315            if let Some(name_value) = name {
316                validate_min_length(name_value, 2, "name")?;
317                validate_max_length(name_value, 50, "name")?;
318            }
319            validate_email(email, "email")?;
320            validate_range(age, 13, 120, "age")?;
321            Ok(())
322        }
323
324        let result = validate_user_input(&Some("John".to_string()), "john@example.com", 25);
325        assert!(result.is_ok());
326
327        let result = validate_user_input(&None, "john@example.com", 25);
328        assert!(result.is_err());
329
330        let result = validate_user_input(
331            &Some("J".to_string()), // Too short
332            "john@example.com",
333            25,
334        );
335        assert!(result.is_err());
336    }
337}