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#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct VersionedError {
12 pub error: ErrorInfo,
14 pub api_version: String,
16 pub migration_info: Option<MigrationInfo>,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct ErrorInfo {
23 pub code: String,
25 pub message: String,
27 pub details: Option<String>,
29 pub field_errors: Option<HashMap<String, Vec<String>>>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct MigrationInfo {
36 pub migration_guide_url: Option<String>,
38 pub recommended_version: String,
40 pub deprecation_message: Option<String>,
42 pub sunset_date: Option<String>,
44}
45
46pub 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 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 pub fn status(mut self, status: ElifStatusCode) -> Self {
69 self.status_code = status;
70 self
71 }
72
73 pub fn details(mut self, details: &str) -> Self {
75 self.details = Some(details.to_string());
76 self
77 }
78
79 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 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 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 response = response.json(&versioned_error)?;
125
126 if version_info.is_deprecated {
128 response = response.header("Deprecation", "true")?;
130
131 if let Some(message) = &version_info.api_version.deprecation_message {
133 let warning_value = format!("299 - \"{}\"", message);
134 if warning_value.parse::<axum::http::HeaderValue>().is_ok() {
136 response = response.header("Warning", warning_value)?;
137 }
138 }
139
140 if let Some(sunset) = &version_info.api_version.sunset_date {
142 if sunset.parse::<axum::http::HeaderValue>().is_ok() {
144 response = response.header("Sunset", sunset)?;
145 }
146 }
147 }
148
149 Ok(response)
150 }
151
152 fn get_recommended_version(&self, current_version: &str) -> String {
154 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
171pub trait VersionedErrorExt {
173 fn versioned_bad_request(
175 version_info: &VersionInfo,
176 code: &str,
177 message: &str,
178 ) -> HttpResult<ElifResponse>;
179
180 fn versioned_not_found(version_info: &VersionInfo, resource: &str) -> HttpResult<ElifResponse>;
182
183 fn versioned_validation_error(
185 version_info: &VersionInfo,
186 field_errors: HashMap<String, Vec<String>>,
187 ) -> HttpResult<ElifResponse>;
188
189 fn versioned_internal_error(
191 version_info: &VersionInfo,
192 message: &str,
193 ) -> HttpResult<ElifResponse>;
194
195 fn versioned_unauthorized(
197 version_info: &VersionInfo,
198 message: &str,
199 ) -> HttpResult<ElifResponse>;
200
201 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
262pub 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 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}