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#[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_insert_with(Vec::new)
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(version_info: &VersionInfo, code: &str, message: &str) -> HttpResult<ElifResponse>;
175
176 fn versioned_not_found(version_info: &VersionInfo, resource: &str) -> HttpResult<ElifResponse>;
178
179 fn versioned_validation_error(
181 version_info: &VersionInfo,
182 field_errors: HashMap<String, Vec<String>>
183 ) -> HttpResult<ElifResponse>;
184
185 fn versioned_internal_error(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse>;
187
188 fn versioned_unauthorized(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse>;
190
191 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
242pub 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 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}