elif_http/errors/
versioned.rs

1use crate::{
2    errors::{HttpError, HttpResult},
3    response::{ElifResponse, ElifStatusCode},
4    middleware::versioning::VersionInfo,
5};
6use serde::{Serialize, Deserialize};
7use std::collections::HashMap;
8
9/// Version-aware error response structure
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct VersionedError {
12    /// Error information
13    pub error: ErrorInfo,
14    /// API version that generated this error
15    pub api_version: String,
16    /// Links to migration guides or documentation (if version is deprecated)
17    pub migration_info: Option<MigrationInfo>,
18}
19
20/// Core error information
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ErrorInfo {
23    /// Error code
24    pub code: String,
25    /// Human-readable error message
26    pub message: String,
27    /// Additional details or hints
28    pub details: Option<String>,
29    /// Field-specific errors for validation
30    pub field_errors: Option<HashMap<String, Vec<String>>>,
31}
32
33/// Migration information for deprecated versions
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct MigrationInfo {
36    /// URL to migration guide
37    pub migration_guide_url: Option<String>,
38    /// Recommended version to migrate to
39    pub recommended_version: String,
40    /// Deprecation warning message
41    pub deprecation_message: Option<String>,
42    /// Date when this version will be removed
43    pub sunset_date: Option<String>,
44}
45
46/// Version-aware error builder
47pub struct VersionedErrorBuilder {
48    code: String,
49    message: String,
50    details: Option<String>,
51    field_errors: Option<HashMap<String, Vec<String>>>,
52    status_code: ElifStatusCode,
53}
54
55impl VersionedErrorBuilder {
56    /// Create a new versioned error builder
57    pub fn new(code: &str, message: &str) -> Self {
58        Self {
59            code: code.to_string(),
60            message: message.to_string(),
61            details: None,
62            field_errors: None,
63            status_code: ElifStatusCode::INTERNAL_SERVER_ERROR,
64        }
65    }
66
67    /// Set status code
68    pub fn status(mut self, status: ElifStatusCode) -> Self {
69        self.status_code = status;
70        self
71    }
72
73    /// Add details
74    pub fn details(mut self, details: &str) -> Self {
75        self.details = Some(details.to_string());
76        self
77    }
78
79    /// Add field errors for validation
80    pub fn field_errors(mut self, field_errors: HashMap<String, Vec<String>>) -> Self {
81        self.field_errors = Some(field_errors);
82        self
83    }
84
85    /// Add a single field error
86    pub fn field_error(mut self, field: &str, error: &str) -> Self {
87        self.field_errors
88            .get_or_insert_with(HashMap::new)
89            .entry(field.to_string())
90            .or_insert_with(Vec::new)
91            .push(error.to_string());
92        self
93    }
94
95    /// Build the error response with version information
96    pub fn build(self, version_info: &VersionInfo) -> HttpResult<ElifResponse> {
97        let error_info = ErrorInfo {
98            code: self.code.clone(),
99            message: self.message.clone(),
100            details: self.details.clone(),
101            field_errors: self.field_errors.clone(),
102        };
103
104        let migration_info = if version_info.is_deprecated {
105            Some(MigrationInfo {
106                migration_guide_url: Some(format!("/docs/migration/{}", version_info.version)),
107                recommended_version: self.get_recommended_version(&version_info.version),
108                deprecation_message: version_info.api_version.deprecation_message.clone(),
109                sunset_date: version_info.api_version.sunset_date.clone(),
110            })
111        } else {
112            None
113        };
114
115        let versioned_error = VersionedError {
116            error: error_info,
117            api_version: version_info.version.clone(),
118            migration_info,
119        };
120
121        let mut response = ElifResponse::with_status(self.status_code);
122        
123        // Add JSON body
124        response = response.json(&versioned_error)?;
125        
126        // Add deprecation headers if needed using safe header handling
127        if version_info.is_deprecated {
128            // Use safe header parsing for static values
129            response = response.header("Deprecation", "true")?;
130            
131            // Handle dynamic warning message safely
132            if let Some(message) = &version_info.api_version.deprecation_message {
133                let warning_value = format!("299 - \"{}\"", message);
134                // Only add header if it can be parsed successfully
135                if warning_value.parse::<axum::http::HeaderValue>().is_ok() {
136                    response = response.header("Warning", warning_value)?;
137                }
138            }
139            
140            // Handle dynamic sunset date safely
141            if let Some(sunset) = &version_info.api_version.sunset_date {
142                // Only add header if it can be parsed successfully  
143                if sunset.parse::<axum::http::HeaderValue>().is_ok() {
144                    response = response.header("Sunset", sunset)?;
145                }
146            }
147        }
148
149        Ok(response)
150    }
151
152    /// Get recommended version for migration
153    fn get_recommended_version(&self, current_version: &str) -> String {
154        // Simple logic to recommend next version
155        // In practice, this would be configurable
156        match current_version {
157            "v1" => "v2".to_string(),
158            "v2" => "v3".to_string(),
159            version => {
160                if let Some(v_pos) = version.find('v') {
161                    if let Ok(num) = version[v_pos + 1..].parse::<u32>() {
162                        return format!("v{}", num + 1);
163                    }
164                }
165                "latest".to_string()
166            }
167        }
168    }
169}
170
171/// Extension trait for version-aware error handling
172pub trait VersionedErrorExt {
173    /// Create a version-aware bad request error
174    fn versioned_bad_request(version_info: &VersionInfo, code: &str, message: &str) -> HttpResult<ElifResponse>;
175    
176    /// Create a version-aware not found error
177    fn versioned_not_found(version_info: &VersionInfo, resource: &str) -> HttpResult<ElifResponse>;
178    
179    /// Create a version-aware validation error
180    fn versioned_validation_error(
181        version_info: &VersionInfo, 
182        field_errors: HashMap<String, Vec<String>>
183    ) -> HttpResult<ElifResponse>;
184    
185    /// Create a version-aware internal server error
186    fn versioned_internal_error(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse>;
187    
188    /// Create a version-aware unauthorized error
189    fn versioned_unauthorized(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse>;
190    
191    /// Create a version-aware forbidden error
192    fn versioned_forbidden(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse>;
193}
194
195impl VersionedErrorExt for HttpError {
196    fn versioned_bad_request(version_info: &VersionInfo, code: &str, message: &str) -> HttpResult<ElifResponse> {
197        VersionedErrorBuilder::new(code, message)
198            .status(ElifStatusCode::BAD_REQUEST)
199            .build(version_info)
200    }
201    
202    fn versioned_not_found(version_info: &VersionInfo, resource: &str) -> HttpResult<ElifResponse> {
203        VersionedErrorBuilder::new("NOT_FOUND", &format!("{} not found", resource))
204            .status(ElifStatusCode::NOT_FOUND)
205            .details(&format!("The requested {} could not be found", resource))
206            .build(version_info)
207    }
208    
209    fn versioned_validation_error(
210        version_info: &VersionInfo, 
211        field_errors: HashMap<String, Vec<String>>
212    ) -> HttpResult<ElifResponse> {
213        VersionedErrorBuilder::new("VALIDATION_ERROR", "Request validation failed")
214            .status(ElifStatusCode::UNPROCESSABLE_ENTITY)
215            .details("One or more fields contain invalid values")
216            .field_errors(field_errors)
217            .build(version_info)
218    }
219    
220    fn versioned_internal_error(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
221        VersionedErrorBuilder::new("INTERNAL_ERROR", "Internal server error")
222            .status(ElifStatusCode::INTERNAL_SERVER_ERROR)
223            .details(message)
224            .build(version_info)
225    }
226    
227    fn versioned_unauthorized(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
228        VersionedErrorBuilder::new("UNAUTHORIZED", "Authentication required")
229            .status(ElifStatusCode::UNAUTHORIZED)
230            .details(message)
231            .build(version_info)
232    }
233    
234    fn versioned_forbidden(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
235        VersionedErrorBuilder::new("FORBIDDEN", "Access denied")
236            .status(ElifStatusCode::FORBIDDEN)
237            .details(message)
238            .build(version_info)
239    }
240}
241
242/// Convenience functions for creating versioned errors
243pub fn versioned_error(_version_info: &VersionInfo, code: &str, message: &str) -> VersionedErrorBuilder {
244    VersionedErrorBuilder::new(code, message)
245}
246
247pub fn bad_request_v(version_info: &VersionInfo, code: &str, message: &str) -> HttpResult<ElifResponse> {
248    HttpError::versioned_bad_request(version_info, code, message)
249}
250
251pub fn not_found_v(version_info: &VersionInfo, resource: &str) -> HttpResult<ElifResponse> {
252    HttpError::versioned_not_found(version_info, resource)
253}
254
255pub fn validation_error_v(version_info: &VersionInfo, field_errors: HashMap<String, Vec<String>>) -> HttpResult<ElifResponse> {
256    HttpError::versioned_validation_error(version_info, field_errors)
257}
258
259pub fn internal_error_v(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
260    HttpError::versioned_internal_error(version_info, message)
261}
262
263pub fn unauthorized_v(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
264    HttpError::versioned_unauthorized(version_info, message)
265}
266
267pub fn forbidden_v(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
268    HttpError::versioned_forbidden(version_info, message)
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use crate::middleware::versioning::ApiVersion;
275
276    fn create_test_version_info(version: &str, deprecated: bool) -> VersionInfo {
277        VersionInfo {
278            version: version.to_string(),
279            is_deprecated: deprecated,
280            api_version: ApiVersion {
281                version: version.to_string(),
282                deprecated,
283                deprecation_message: if deprecated {
284                    Some("This version is deprecated".to_string())
285                } else {
286                    None
287                },
288                sunset_date: if deprecated {
289                    Some("2024-12-31".to_string())
290                } else {
291                    None
292                },
293                is_default: false,
294            },
295        }
296    }
297
298    #[test]
299    fn test_versioned_error_builder() {
300        let version_info = create_test_version_info("v1", false);
301        
302        let response = VersionedErrorBuilder::new("TEST_ERROR", "Test error message")
303            .status(ElifStatusCode::BAD_REQUEST)
304            .details("Additional details")
305            .build(&version_info)
306            .unwrap();
307        
308        assert_eq!(response.status_code(), ElifStatusCode::BAD_REQUEST);
309    }
310
311    #[test]
312    fn test_deprecated_version_migration_info() {
313        let version_info = create_test_version_info("v1", true);
314        
315        let response = VersionedErrorBuilder::new("TEST_ERROR", "Test error")
316            .status(ElifStatusCode::BAD_REQUEST)
317            .build(&version_info)
318            .unwrap();
319        
320        // Test passes if we can build the response
321        assert_eq!(response.status_code(), ElifStatusCode::BAD_REQUEST);
322    }
323
324    #[test]
325    fn test_validation_error_with_fields() {
326        let version_info = create_test_version_info("v2", false);
327        let mut field_errors = HashMap::new();
328        field_errors.insert("email".to_string(), vec!["Invalid email format".to_string()]);
329        field_errors.insert("age".to_string(), vec!["Must be positive".to_string()]);
330        
331        let response = HttpError::versioned_validation_error(&version_info, field_errors).unwrap();
332        
333        assert_eq!(response.status_code(), ElifStatusCode::UNPROCESSABLE_ENTITY);
334    }
335
336    #[test]
337    fn test_convenience_functions() {
338        let version_info = create_test_version_info("v1", false);
339        
340        let _bad_request = bad_request_v(&version_info, "BAD_INPUT", "Invalid input").unwrap();
341        let _not_found = not_found_v(&version_info, "User").unwrap();
342        let _internal = internal_error_v(&version_info, "Something went wrong").unwrap();
343        let _unauthorized = unauthorized_v(&version_info, "Token expired").unwrap();
344        let _forbidden = forbidden_v(&version_info, "Insufficient permissions").unwrap();
345    }
346}