elif_http/errors/
versioned.rs

1use crate::{
2    errors::{HttpError, HttpResult},
3    middleware::versioning::VersionInfo,
4    response::{ElifResponse, ElifStatusCode},
5};
6use serde::{Deserialize, Serialize};
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_default()
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(
175        version_info: &VersionInfo,
176        code: &str,
177        message: &str,
178    ) -> HttpResult<ElifResponse>;
179
180    /// Create a version-aware not found error
181    fn versioned_not_found(version_info: &VersionInfo, resource: &str) -> HttpResult<ElifResponse>;
182
183    /// Create a version-aware validation error
184    fn versioned_validation_error(
185        version_info: &VersionInfo,
186        field_errors: HashMap<String, Vec<String>>,
187    ) -> HttpResult<ElifResponse>;
188
189    /// Create a version-aware internal server error
190    fn versioned_internal_error(
191        version_info: &VersionInfo,
192        message: &str,
193    ) -> HttpResult<ElifResponse>;
194
195    /// Create a version-aware unauthorized error
196    fn versioned_unauthorized(
197        version_info: &VersionInfo,
198        message: &str,
199    ) -> HttpResult<ElifResponse>;
200
201    /// Create a version-aware forbidden error
202    fn versioned_forbidden(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse>;
203}
204
205impl VersionedErrorExt for HttpError {
206    fn versioned_bad_request(
207        version_info: &VersionInfo,
208        code: &str,
209        message: &str,
210    ) -> HttpResult<ElifResponse> {
211        VersionedErrorBuilder::new(code, message)
212            .status(ElifStatusCode::BAD_REQUEST)
213            .build(version_info)
214    }
215
216    fn versioned_not_found(version_info: &VersionInfo, resource: &str) -> HttpResult<ElifResponse> {
217        VersionedErrorBuilder::new("NOT_FOUND", &format!("{} not found", resource))
218            .status(ElifStatusCode::NOT_FOUND)
219            .details(&format!("The requested {} could not be found", resource))
220            .build(version_info)
221    }
222
223    fn versioned_validation_error(
224        version_info: &VersionInfo,
225        field_errors: HashMap<String, Vec<String>>,
226    ) -> HttpResult<ElifResponse> {
227        VersionedErrorBuilder::new("VALIDATION_ERROR", "Request validation failed")
228            .status(ElifStatusCode::UNPROCESSABLE_ENTITY)
229            .details("One or more fields contain invalid values")
230            .field_errors(field_errors)
231            .build(version_info)
232    }
233
234    fn versioned_internal_error(
235        version_info: &VersionInfo,
236        message: &str,
237    ) -> HttpResult<ElifResponse> {
238        VersionedErrorBuilder::new("INTERNAL_ERROR", "Internal server error")
239            .status(ElifStatusCode::INTERNAL_SERVER_ERROR)
240            .details(message)
241            .build(version_info)
242    }
243
244    fn versioned_unauthorized(
245        version_info: &VersionInfo,
246        message: &str,
247    ) -> HttpResult<ElifResponse> {
248        VersionedErrorBuilder::new("UNAUTHORIZED", "Authentication required")
249            .status(ElifStatusCode::UNAUTHORIZED)
250            .details(message)
251            .build(version_info)
252    }
253
254    fn versioned_forbidden(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
255        VersionedErrorBuilder::new("FORBIDDEN", "Access denied")
256            .status(ElifStatusCode::FORBIDDEN)
257            .details(message)
258            .build(version_info)
259    }
260}
261
262/// Convenience functions for creating versioned errors
263pub fn versioned_error(
264    _version_info: &VersionInfo,
265    code: &str,
266    message: &str,
267) -> VersionedErrorBuilder {
268    VersionedErrorBuilder::new(code, message)
269}
270
271pub fn bad_request_v(
272    version_info: &VersionInfo,
273    code: &str,
274    message: &str,
275) -> HttpResult<ElifResponse> {
276    HttpError::versioned_bad_request(version_info, code, message)
277}
278
279pub fn not_found_v(version_info: &VersionInfo, resource: &str) -> HttpResult<ElifResponse> {
280    HttpError::versioned_not_found(version_info, resource)
281}
282
283pub fn validation_error_v(
284    version_info: &VersionInfo,
285    field_errors: HashMap<String, Vec<String>>,
286) -> HttpResult<ElifResponse> {
287    HttpError::versioned_validation_error(version_info, field_errors)
288}
289
290pub fn internal_error_v(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
291    HttpError::versioned_internal_error(version_info, message)
292}
293
294pub fn unauthorized_v(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
295    HttpError::versioned_unauthorized(version_info, message)
296}
297
298pub fn forbidden_v(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
299    HttpError::versioned_forbidden(version_info, message)
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use crate::middleware::versioning::ApiVersion;
306
307    fn create_test_version_info(version: &str, deprecated: bool) -> VersionInfo {
308        VersionInfo {
309            version: version.to_string(),
310            is_deprecated: deprecated,
311            api_version: ApiVersion {
312                version: version.to_string(),
313                deprecated,
314                deprecation_message: if deprecated {
315                    Some("This version is deprecated".to_string())
316                } else {
317                    None
318                },
319                sunset_date: if deprecated {
320                    Some("2024-12-31".to_string())
321                } else {
322                    None
323                },
324                is_default: false,
325            },
326        }
327    }
328
329    #[test]
330    fn test_versioned_error_builder() {
331        let version_info = create_test_version_info("v1", false);
332
333        let response = VersionedErrorBuilder::new("TEST_ERROR", "Test error message")
334            .status(ElifStatusCode::BAD_REQUEST)
335            .details("Additional details")
336            .build(&version_info)
337            .unwrap();
338
339        assert_eq!(response.status_code(), ElifStatusCode::BAD_REQUEST);
340    }
341
342    #[test]
343    fn test_deprecated_version_migration_info() {
344        let version_info = create_test_version_info("v1", true);
345
346        let response = VersionedErrorBuilder::new("TEST_ERROR", "Test error")
347            .status(ElifStatusCode::BAD_REQUEST)
348            .build(&version_info)
349            .unwrap();
350
351        // Test passes if we can build the response
352        assert_eq!(response.status_code(), ElifStatusCode::BAD_REQUEST);
353    }
354
355    #[test]
356    fn test_validation_error_with_fields() {
357        let version_info = create_test_version_info("v2", false);
358        let mut field_errors = HashMap::new();
359        field_errors.insert(
360            "email".to_string(),
361            vec!["Invalid email format".to_string()],
362        );
363        field_errors.insert("age".to_string(), vec!["Must be positive".to_string()]);
364
365        let response = HttpError::versioned_validation_error(&version_info, field_errors).unwrap();
366
367        assert_eq!(response.status_code(), ElifStatusCode::UNPROCESSABLE_ENTITY);
368    }
369
370    #[test]
371    fn test_convenience_functions() {
372        let version_info = create_test_version_info("v1", false);
373
374        let _bad_request = bad_request_v(&version_info, "BAD_INPUT", "Invalid input").unwrap();
375        let _not_found = not_found_v(&version_info, "User").unwrap();
376        let _internal = internal_error_v(&version_info, "Something went wrong").unwrap();
377        let _unauthorized = unauthorized_v(&version_info, "Token expired").unwrap();
378        let _forbidden = forbidden_v(&version_info, "Insufficient permissions").unwrap();
379    }
380}