use crate::{
errors::{HttpError, HttpResult},
middleware::versioning::VersionInfo,
response::{ElifResponse, ElifStatusCode},
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionedError {
pub error: ErrorInfo,
pub api_version: String,
pub migration_info: Option<MigrationInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorInfo {
pub code: String,
pub message: String,
pub details: Option<String>,
pub field_errors: Option<HashMap<String, Vec<String>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationInfo {
pub migration_guide_url: Option<String>,
pub recommended_version: String,
pub deprecation_message: Option<String>,
pub sunset_date: Option<String>,
}
pub struct VersionedErrorBuilder {
code: String,
message: String,
details: Option<String>,
field_errors: Option<HashMap<String, Vec<String>>>,
status_code: ElifStatusCode,
}
impl VersionedErrorBuilder {
pub fn new(code: &str, message: &str) -> Self {
Self {
code: code.to_string(),
message: message.to_string(),
details: None,
field_errors: None,
status_code: ElifStatusCode::INTERNAL_SERVER_ERROR,
}
}
pub fn status(mut self, status: ElifStatusCode) -> Self {
self.status_code = status;
self
}
pub fn details(mut self, details: &str) -> Self {
self.details = Some(details.to_string());
self
}
pub fn field_errors(mut self, field_errors: HashMap<String, Vec<String>>) -> Self {
self.field_errors = Some(field_errors);
self
}
pub fn field_error(mut self, field: &str, error: &str) -> Self {
self.field_errors
.get_or_insert_with(HashMap::new)
.entry(field.to_string())
.or_default()
.push(error.to_string());
self
}
pub fn build(self, version_info: &VersionInfo) -> HttpResult<ElifResponse> {
let error_info = ErrorInfo {
code: self.code.clone(),
message: self.message.clone(),
details: self.details.clone(),
field_errors: self.field_errors.clone(),
};
let migration_info = if version_info.is_deprecated {
Some(MigrationInfo {
migration_guide_url: Some(format!("/docs/migration/{}", version_info.version)),
recommended_version: self.get_recommended_version(&version_info.version),
deprecation_message: version_info.api_version.deprecation_message.clone(),
sunset_date: version_info.api_version.sunset_date.clone(),
})
} else {
None
};
let versioned_error = VersionedError {
error: error_info,
api_version: version_info.version.clone(),
migration_info,
};
let mut response = ElifResponse::with_status(self.status_code);
response = response.json(&versioned_error)?;
if version_info.is_deprecated {
response = response.header("Deprecation", "true")?;
if let Some(message) = &version_info.api_version.deprecation_message {
let warning_value = format!("299 - \"{}\"", message);
if warning_value.parse::<axum::http::HeaderValue>().is_ok() {
response = response.header("Warning", warning_value)?;
}
}
if let Some(sunset) = &version_info.api_version.sunset_date {
if sunset.parse::<axum::http::HeaderValue>().is_ok() {
response = response.header("Sunset", sunset)?;
}
}
}
Ok(response)
}
fn get_recommended_version(&self, current_version: &str) -> String {
match current_version {
"v1" => "v2".to_string(),
"v2" => "v3".to_string(),
version => {
if let Some(v_pos) = version.find('v') {
if let Ok(num) = version[v_pos + 1..].parse::<u32>() {
return format!("v{}", num + 1);
}
}
"latest".to_string()
}
}
}
}
pub trait VersionedErrorExt {
fn versioned_bad_request(
version_info: &VersionInfo,
code: &str,
message: &str,
) -> HttpResult<ElifResponse>;
fn versioned_not_found(version_info: &VersionInfo, resource: &str) -> HttpResult<ElifResponse>;
fn versioned_validation_error(
version_info: &VersionInfo,
field_errors: HashMap<String, Vec<String>>,
) -> HttpResult<ElifResponse>;
fn versioned_internal_error(
version_info: &VersionInfo,
message: &str,
) -> HttpResult<ElifResponse>;
fn versioned_unauthorized(
version_info: &VersionInfo,
message: &str,
) -> HttpResult<ElifResponse>;
fn versioned_forbidden(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse>;
}
impl VersionedErrorExt for HttpError {
fn versioned_bad_request(
version_info: &VersionInfo,
code: &str,
message: &str,
) -> HttpResult<ElifResponse> {
VersionedErrorBuilder::new(code, message)
.status(ElifStatusCode::BAD_REQUEST)
.build(version_info)
}
fn versioned_not_found(version_info: &VersionInfo, resource: &str) -> HttpResult<ElifResponse> {
VersionedErrorBuilder::new("NOT_FOUND", &format!("{} not found", resource))
.status(ElifStatusCode::NOT_FOUND)
.details(&format!("The requested {} could not be found", resource))
.build(version_info)
}
fn versioned_validation_error(
version_info: &VersionInfo,
field_errors: HashMap<String, Vec<String>>,
) -> HttpResult<ElifResponse> {
VersionedErrorBuilder::new("VALIDATION_ERROR", "Request validation failed")
.status(ElifStatusCode::UNPROCESSABLE_ENTITY)
.details("One or more fields contain invalid values")
.field_errors(field_errors)
.build(version_info)
}
fn versioned_internal_error(
version_info: &VersionInfo,
message: &str,
) -> HttpResult<ElifResponse> {
VersionedErrorBuilder::new("INTERNAL_ERROR", "Internal server error")
.status(ElifStatusCode::INTERNAL_SERVER_ERROR)
.details(message)
.build(version_info)
}
fn versioned_unauthorized(
version_info: &VersionInfo,
message: &str,
) -> HttpResult<ElifResponse> {
VersionedErrorBuilder::new("UNAUTHORIZED", "Authentication required")
.status(ElifStatusCode::UNAUTHORIZED)
.details(message)
.build(version_info)
}
fn versioned_forbidden(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
VersionedErrorBuilder::new("FORBIDDEN", "Access denied")
.status(ElifStatusCode::FORBIDDEN)
.details(message)
.build(version_info)
}
}
pub fn versioned_error(
_version_info: &VersionInfo,
code: &str,
message: &str,
) -> VersionedErrorBuilder {
VersionedErrorBuilder::new(code, message)
}
pub fn bad_request_v(
version_info: &VersionInfo,
code: &str,
message: &str,
) -> HttpResult<ElifResponse> {
HttpError::versioned_bad_request(version_info, code, message)
}
pub fn not_found_v(version_info: &VersionInfo, resource: &str) -> HttpResult<ElifResponse> {
HttpError::versioned_not_found(version_info, resource)
}
pub fn validation_error_v(
version_info: &VersionInfo,
field_errors: HashMap<String, Vec<String>>,
) -> HttpResult<ElifResponse> {
HttpError::versioned_validation_error(version_info, field_errors)
}
pub fn internal_error_v(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
HttpError::versioned_internal_error(version_info, message)
}
pub fn unauthorized_v(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
HttpError::versioned_unauthorized(version_info, message)
}
pub fn forbidden_v(version_info: &VersionInfo, message: &str) -> HttpResult<ElifResponse> {
HttpError::versioned_forbidden(version_info, message)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::middleware::versioning::ApiVersion;
fn create_test_version_info(version: &str, deprecated: bool) -> VersionInfo {
VersionInfo {
version: version.to_string(),
is_deprecated: deprecated,
api_version: ApiVersion {
version: version.to_string(),
deprecated,
deprecation_message: if deprecated {
Some("This version is deprecated".to_string())
} else {
None
},
sunset_date: if deprecated {
Some("2024-12-31".to_string())
} else {
None
},
is_default: false,
},
}
}
#[test]
fn test_versioned_error_builder() {
let version_info = create_test_version_info("v1", false);
let response = VersionedErrorBuilder::new("TEST_ERROR", "Test error message")
.status(ElifStatusCode::BAD_REQUEST)
.details("Additional details")
.build(&version_info)
.unwrap();
assert_eq!(response.status_code(), ElifStatusCode::BAD_REQUEST);
}
#[test]
fn test_deprecated_version_migration_info() {
let version_info = create_test_version_info("v1", true);
let response = VersionedErrorBuilder::new("TEST_ERROR", "Test error")
.status(ElifStatusCode::BAD_REQUEST)
.build(&version_info)
.unwrap();
assert_eq!(response.status_code(), ElifStatusCode::BAD_REQUEST);
}
#[test]
fn test_validation_error_with_fields() {
let version_info = create_test_version_info("v2", false);
let mut field_errors = HashMap::new();
field_errors.insert(
"email".to_string(),
vec!["Invalid email format".to_string()],
);
field_errors.insert("age".to_string(), vec!["Must be positive".to_string()]);
let response = HttpError::versioned_validation_error(&version_info, field_errors).unwrap();
assert_eq!(response.status_code(), ElifStatusCode::UNPROCESSABLE_ENTITY);
}
#[test]
fn test_convenience_functions() {
let version_info = create_test_version_info("v1", false);
let _bad_request = bad_request_v(&version_info, "BAD_INPUT", "Invalid input").unwrap();
let _not_found = not_found_v(&version_info, "User").unwrap();
let _internal = internal_error_v(&version_info, "Something went wrong").unwrap();
let _unauthorized = unauthorized_v(&version_info, "Token expired").unwrap();
let _forbidden = forbidden_v(&version_info, "Insufficient permissions").unwrap();
}
}