use serde::Serialize;
use std::fmt;
#[derive(Debug)]
pub enum UltimoError {
Http { status: u16, message: String },
Validation {
message: String,
details: Vec<ValidationError>,
},
Unauthorized(String),
Forbidden(String),
NotFound(String),
Internal(String),
BadRequest(String),
Hyper(hyper::Error),
HttpError(hyper::http::Error),
Json(serde_json::Error),
Io(std::io::Error),
}
#[derive(Debug, Clone, Serialize)]
pub struct ValidationError {
pub field: String,
pub message: String,
}
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub error: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<Vec<ValidationError>>,
}
impl UltimoError {
pub fn status_code(&self) -> u16 {
match self {
UltimoError::Http { status, .. } => *status,
UltimoError::Validation { .. } => 400,
UltimoError::Unauthorized(_) => 401,
UltimoError::Forbidden(_) => 403,
UltimoError::NotFound(_) => 404,
UltimoError::BadRequest(_) => 400,
UltimoError::Internal(_) => 500,
UltimoError::Hyper(_) => 500,
UltimoError::HttpError(_) => 500,
UltimoError::Json(_) => 400,
UltimoError::Io(_) => 500,
}
}
pub fn to_error_response(&self) -> ErrorResponse {
match self {
UltimoError::Http { message, .. } => ErrorResponse {
error: "HttpError".to_string(),
message: message.clone(),
details: None,
},
UltimoError::Validation { message, details } => ErrorResponse {
error: "ValidationError".to_string(),
message: message.clone(),
details: Some(details.clone()),
},
UltimoError::Unauthorized(msg) => ErrorResponse {
error: "Unauthorized".to_string(),
message: msg.clone(),
details: None,
},
UltimoError::Forbidden(msg) => ErrorResponse {
error: "Forbidden".to_string(),
message: msg.clone(),
details: None,
},
UltimoError::NotFound(msg) => ErrorResponse {
error: "NotFound".to_string(),
message: msg.clone(),
details: None,
},
UltimoError::BadRequest(msg) => ErrorResponse {
error: "BadRequest".to_string(),
message: msg.clone(),
details: None,
},
UltimoError::Internal(msg) => ErrorResponse {
error: "InternalError".to_string(),
message: msg.clone(),
details: None,
},
UltimoError::Hyper(err) => ErrorResponse {
error: "ServerError".to_string(),
message: format!("HTTP server error: {}", err),
details: None,
},
UltimoError::HttpError(err) => ErrorResponse {
error: "ServerError".to_string(),
message: format!("HTTP error: {}", err),
details: None,
},
UltimoError::Json(err) => ErrorResponse {
error: "JsonError".to_string(),
message: format!("JSON parsing error: {}", err),
details: None,
},
UltimoError::Io(err) => ErrorResponse {
error: "IoError".to_string(),
message: format!("IO error: {}", err),
details: None,
},
}
}
}
impl fmt::Display for UltimoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UltimoError::Http { status, message } => {
write!(f, "HTTP {}: {}", status, message)
}
UltimoError::Validation { message, .. } => write!(f, "Validation error: {}", message),
UltimoError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
UltimoError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
UltimoError::NotFound(msg) => write!(f, "Not found: {}", msg),
UltimoError::BadRequest(msg) => write!(f, "Bad request: {}", msg),
UltimoError::Internal(msg) => write!(f, "Internal error: {}", msg),
UltimoError::Hyper(err) => write!(f, "Hyper error: {}", err),
UltimoError::HttpError(err) => write!(f, "HTTP error: {}", err),
UltimoError::Json(err) => write!(f, "JSON error: {}", err),
UltimoError::Io(err) => write!(f, "IO error: {}", err),
}
}
}
impl std::error::Error for UltimoError {}
impl From<hyper::Error> for UltimoError {
fn from(err: hyper::Error) -> Self {
UltimoError::Hyper(err)
}
}
impl From<hyper::http::Error> for UltimoError {
fn from(err: hyper::http::Error) -> Self {
UltimoError::HttpError(err)
}
}
impl From<serde_json::Error> for UltimoError {
fn from(err: serde_json::Error) -> Self {
UltimoError::Json(err)
}
}
impl From<std::io::Error> for UltimoError {
fn from(err: std::io::Error) -> Self {
UltimoError::Io(err)
}
}
pub type Result<T> = std::result::Result<T, UltimoError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_status_codes() {
assert_eq!(UltimoError::Unauthorized("test".into()).status_code(), 401);
assert_eq!(UltimoError::Forbidden("test".into()).status_code(), 403);
assert_eq!(UltimoError::NotFound("test".into()).status_code(), 404);
assert_eq!(UltimoError::BadRequest("test".into()).status_code(), 400);
assert_eq!(UltimoError::Internal("test".into()).status_code(), 500);
}
#[test]
fn test_error_response_format() {
let err = UltimoError::Validation {
message: "Invalid input".to_string(),
details: vec![ValidationError {
field: "email".to_string(),
message: "Invalid email format".to_string(),
}],
};
let response = err.to_error_response();
assert_eq!(response.error, "ValidationError");
assert_eq!(response.message, "Invalid input");
assert!(response.details.is_some());
assert_eq!(response.details.unwrap().len(), 1);
}
#[test]
fn test_http_error_status_code() {
let err = UltimoError::Http {
status: 418,
message: "I'm a teapot".to_string(),
};
assert_eq!(err.status_code(), 418);
}
#[test]
fn test_validation_error_status_code() {
let err = UltimoError::Validation {
message: "Validation failed".to_string(),
details: vec![],
};
assert_eq!(err.status_code(), 400);
}
#[test]
fn test_error_conversions() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid json");
assert!(json_err.is_err());
let ultimo_err = UltimoError::from(json_err.unwrap_err());
assert_eq!(ultimo_err.status_code(), 400);
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let ultimo_err = UltimoError::from(io_err);
assert_eq!(ultimo_err.status_code(), 500);
}
#[test]
fn test_error_display_formatting() {
let err = UltimoError::NotFound("User not found".to_string());
assert_eq!(format!("{}", err), "Not found: User not found");
let err = UltimoError::Unauthorized("Invalid token".to_string());
assert_eq!(format!("{}", err), "Unauthorized: Invalid token");
let err = UltimoError::Http {
status: 503,
message: "Service unavailable".to_string(),
};
assert_eq!(format!("{}", err), "HTTP 503: Service unavailable");
}
#[test]
fn test_all_error_response_types() {
let err = UltimoError::Http {
status: 500,
message: "Server error".to_string(),
};
let response = err.to_error_response();
assert_eq!(response.error, "HttpError");
assert_eq!(response.message, "Server error");
let err = UltimoError::Unauthorized("No token".to_string());
let response = err.to_error_response();
assert_eq!(response.error, "Unauthorized");
assert_eq!(response.message, "No token");
let err = UltimoError::Forbidden("Access denied".to_string());
let response = err.to_error_response();
assert_eq!(response.error, "Forbidden");
assert_eq!(response.message, "Access denied");
let err = UltimoError::NotFound("Resource not found".to_string());
let response = err.to_error_response();
assert_eq!(response.error, "NotFound");
assert_eq!(response.message, "Resource not found");
let err = UltimoError::BadRequest("Invalid data".to_string());
let response = err.to_error_response();
assert_eq!(response.error, "BadRequest");
assert_eq!(response.message, "Invalid data");
let err = UltimoError::Internal("Database failure".to_string());
let response = err.to_error_response();
assert_eq!(response.error, "InternalError");
assert_eq!(response.message, "Database failure");
}
#[test]
fn test_validation_error_with_multiple_fields() {
let err = UltimoError::Validation {
message: "Multiple validation errors".to_string(),
details: vec![
ValidationError {
field: "email".to_string(),
message: "Invalid format".to_string(),
},
ValidationError {
field: "password".to_string(),
message: "Too short".to_string(),
},
],
};
let response = err.to_error_response();
assert_eq!(response.error, "ValidationError");
let details = response.details.unwrap();
assert_eq!(details.len(), 2);
assert_eq!(details[0].field, "email");
assert_eq!(details[1].field, "password");
}
#[test]
fn test_json_error_conversion_and_response() {
let json_err = serde_json::from_str::<serde_json::Value>("not valid json");
let ultimo_err = UltimoError::from(json_err.unwrap_err());
let response = ultimo_err.to_error_response();
assert_eq!(response.error, "JsonError");
assert!(response.message.contains("JSON parsing error"));
assert!(response.details.is_none());
}
#[test]
fn test_io_error_conversion_and_response() {
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
let ultimo_err = UltimoError::from(io_err);
let response = ultimo_err.to_error_response();
assert_eq!(response.error, "IoError");
assert!(response.message.contains("IO error"));
assert!(response.details.is_none());
}
#[test]
fn test_error_response_serialization() {
let response = ErrorResponse {
error: "TestError".to_string(),
message: "Test message".to_string(),
details: None,
};
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("TestError"));
assert!(json.contains("Test message"));
}
#[test]
fn test_validation_error_serialization() {
let validation_err = ValidationError {
field: "username".to_string(),
message: "Required field".to_string(),
};
let json = serde_json::to_string(&validation_err).unwrap();
assert!(json.contains("username"));
assert!(json.contains("Required field"));
}
#[test]
fn test_error_is_send_sync() {
fn assert_send<T: Send>() {}
assert_send::<UltimoError>();
}
}