Skip to main content

forgedb_validation/
http.rs

1//! HTTP-specific validation extensions for Sprint 9
2
3use crate::ValidationError;
4
5/// HTTP validation error that maps to status codes
6#[derive(Debug, Clone)]
7pub struct HttpValidationError {
8    pub status_code: u16,
9    pub errors: Vec<ValidationError>,
10}
11
12impl HttpValidationError {
13    /// Create a bad request error (400)
14    pub fn bad_request(errors: Vec<ValidationError>) -> Self {
15        Self {
16            status_code: 400,
17            errors,
18        }
19    }
20
21    /// Create a not found error (404)
22    pub fn not_found(message: impl Into<String>) -> Self {
23        Self {
24            status_code: 404,
25            errors: vec![ValidationError::new(message)],
26        }
27    }
28
29    /// Create a conflict error (409)
30    pub fn conflict(message: impl Into<String>) -> Self {
31        Self {
32            status_code: 409,
33            errors: vec![ValidationError::new(message)],
34        }
35    }
36
37    /// Create an unprocessable entity error (422)
38    pub fn unprocessable_entity(errors: Vec<ValidationError>) -> Self {
39        Self {
40            status_code: 422,
41            errors,
42        }
43    }
44
45    /// Create an internal server error (500)
46    pub fn internal_error(message: impl Into<String>) -> Self {
47        Self {
48            status_code: 500,
49            errors: vec![ValidationError::new(message)],
50        }
51    }
52
53    /// Check if this is a client error (4xx)
54    pub fn is_client_error(&self) -> bool {
55        self.status_code >= 400 && self.status_code < 500
56    }
57
58    /// Check if this is a server error (5xx)
59    pub fn is_server_error(&self) -> bool {
60        self.status_code >= 500 && self.status_code < 600
61    }
62
63    /// Get the primary error message
64    pub fn message(&self) -> String {
65        if self.errors.is_empty() {
66            "Unknown error".to_string()
67        } else {
68            self.errors[0].message.clone()
69        }
70    }
71}
72
73impl std::fmt::Display for HttpValidationError {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(f, "HTTP {} - {}", self.status_code, self.message())
76    }
77}
78
79impl std::error::Error for HttpValidationError {}
80
81/// Validation rules for HTTP requests
82pub struct HttpValidator;
83
84impl HttpValidator {
85    /// Validate required fields are present
86    pub fn validate_required_fields(
87        fields: &[(&str, Option<&str>)],
88    ) -> Result<(), Vec<ValidationError>> {
89        let errors: Vec<ValidationError> = fields
90            .iter()
91            .filter_map(|(name, value)| {
92                if value.is_none() || value.unwrap().is_empty() {
93                    Some(ValidationError::new(format!(
94                        "Field '{}' is required",
95                        name
96                    )))
97                } else {
98                    None
99                }
100            })
101            .collect();
102
103        if errors.is_empty() {
104            Ok(())
105        } else {
106            Err(errors)
107        }
108    }
109
110    /// Validate email format (basic check)
111    pub fn validate_email(email: &str) -> Result<(), ValidationError> {
112        if email.contains('@') && email.contains('.') && email.len() >= 5 {
113            Ok(())
114        } else {
115            Err(ValidationError::new("Invalid email format")
116                .with_suggestion("Email must contain @ and domain"))
117        }
118    }
119
120    /// Validate string length
121    pub fn validate_length(
122        field_name: &str,
123        value: &str,
124        min: usize,
125        max: usize,
126    ) -> Result<(), ValidationError> {
127        let len = value.len();
128        if len < min {
129            Err(ValidationError::new(format!(
130                "Field '{}' must be at least {} characters",
131                field_name, min
132            )))
133        } else if len > max {
134            Err(ValidationError::new(format!(
135                "Field '{}' must be at most {} characters",
136                field_name, max
137            )))
138        } else {
139            Ok(())
140        }
141    }
142
143    /// Validate numeric range
144    pub fn validate_range<T: PartialOrd + std::fmt::Display>(
145        field_name: &str,
146        value: T,
147        min: T,
148        max: T,
149    ) -> Result<(), ValidationError> {
150        if value < min {
151            Err(ValidationError::new(format!(
152                "Field '{}' must be at least {}",
153                field_name, min
154            )))
155        } else if value > max {
156            Err(ValidationError::new(format!(
157                "Field '{}' must be at most {}",
158                field_name, max
159            )))
160        } else {
161            Ok(())
162        }
163    }
164}
165