use http::{Response, StatusCode, header};
use serde::{Deserialize, Serialize};
use std::fmt;
use wae_types::{ErrorCategory, WaeError};
use crate::{Body, full_body};
#[derive(Debug, Clone)]
pub struct HttpError {
inner: WaeError,
}
impl HttpError {
pub fn new(error: WaeError) -> Self {
Self { inner: error }
}
pub fn inner(&self) -> &WaeError {
&self.inner
}
pub fn category(&self) -> ErrorCategory {
self.inner.category()
}
pub fn i18n_key(&self) -> &'static str {
self.inner.i18n_key()
}
pub fn i18n_data(&self) -> serde_json::Value {
self.inner.i18n_data()
}
pub fn invalid_params(param: impl Into<String>, reason: impl Into<String>) -> Self {
Self::new(WaeError::invalid_params(param, reason))
}
pub fn invalid_token(reason: impl Into<String>) -> Self {
Self::new(WaeError::invalid_token(reason))
}
pub fn token_expired() -> Self {
Self::new(WaeError::token_expired())
}
pub fn forbidden(resource: impl Into<String>) -> Self {
Self::new(WaeError::forbidden(resource))
}
pub fn permission_denied(action: impl Into<String>) -> Self {
Self::new(WaeError::permission_denied(action))
}
pub fn not_found(resource_type: impl Into<String>, identifier: impl Into<String>) -> Self {
Self::new(WaeError::not_found(resource_type, identifier))
}
pub fn internal(reason: impl Into<String>) -> Self {
Self::new(WaeError::internal(reason))
}
pub fn invalid_format(field: impl Into<String>, expected: impl Into<String>) -> Self {
Self::new(WaeError::invalid_format(field, expected))
}
pub fn into_response(self) -> Response<Body> {
let status = category_to_status_code(self.category());
let error_response = ErrorResponse::from_error(&self);
let body = serde_json::to_string(&error_response).unwrap_or_else(|_| {
r#"{"success":false,"code":"INTERNAL_ERROR","message":"Failed to serialize error"}"#.to_string()
});
Response::builder().status(status).header(header::CONTENT_TYPE, "application/json").body(full_body(body)).unwrap()
}
}
impl fmt::Display for HttpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.inner)
}
}
impl std::error::Error for HttpError {}
impl From<WaeError> for HttpError {
fn from(error: WaeError) -> Self {
Self::new(error)
}
}
impl From<HttpError> for WaeError {
fn from(error: HttpError) -> Self {
error.inner
}
}
pub type HttpResult<T> = Result<T, HttpError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorResponse {
pub success: bool,
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trace_id: Option<String>,
}
impl ErrorResponse {
pub fn from_error(error: &HttpError) -> Self {
Self {
success: false,
code: error.i18n_key().to_string(),
message: error.to_string(),
details: Some(error.i18n_data()),
trace_id: None,
}
}
pub fn from_wae_error(error: &WaeError) -> Self {
Self {
success: false,
code: error.i18n_key().to_string(),
message: error.to_string(),
details: Some(error.i18n_data()),
trace_id: None,
}
}
pub fn from_error_with_details(error: &HttpError, details: serde_json::Value) -> Self {
let mut base = Self::from_error(error);
base.details = Some(details);
base
}
pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
self.trace_id = Some(trace_id.into());
self
}
pub fn into_response(self) -> Response<Body> {
let status = StatusCode::BAD_REQUEST;
let body = serde_json::to_string(&self).unwrap_or_else(|_| {
r#"{"success":false,"code":"INTERNAL_ERROR","message":"Failed to serialize error"}"#.to_string()
});
Response::builder().status(status).header(header::CONTENT_TYPE, "application/json").body(full_body(body)).unwrap()
}
}
fn category_to_status_code(category: ErrorCategory) -> StatusCode {
match category {
ErrorCategory::Validation => StatusCode::BAD_REQUEST,
ErrorCategory::Auth => StatusCode::UNAUTHORIZED,
ErrorCategory::Permission => StatusCode::FORBIDDEN,
ErrorCategory::NotFound => StatusCode::NOT_FOUND,
ErrorCategory::Conflict => StatusCode::CONFLICT,
ErrorCategory::RateLimited => StatusCode::TOO_MANY_REQUESTS,
ErrorCategory::Network => StatusCode::BAD_GATEWAY,
ErrorCategory::Storage => StatusCode::INTERNAL_SERVER_ERROR,
ErrorCategory::Database => StatusCode::INTERNAL_SERVER_ERROR,
ErrorCategory::Cache => StatusCode::INTERNAL_SERVER_ERROR,
ErrorCategory::Config => StatusCode::INTERNAL_SERVER_ERROR,
ErrorCategory::Timeout => StatusCode::REQUEST_TIMEOUT,
ErrorCategory::Internal => StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub trait ErrorExt<T> {
fn bad_request(self) -> HttpResult<T>;
fn unauthorized(self) -> HttpResult<T>;
fn forbidden(self) -> HttpResult<T>;
fn not_found(self) -> HttpResult<T>;
fn internal_error(self) -> HttpResult<T>;
fn with_http_error(self, error: HttpError) -> HttpResult<T>;
fn map_http_error<F>(self, f: F) -> HttpResult<T>
where
F: FnOnce(String) -> HttpError;
}
impl<T, E: fmt::Display> ErrorExt<T> for Result<T, E> {
fn bad_request(self) -> HttpResult<T> {
self.map_err(|e| HttpError::invalid_params("unknown", e.to_string()))
}
fn unauthorized(self) -> HttpResult<T> {
self.map_err(|e| HttpError::invalid_token(e.to_string()))
}
fn forbidden(self) -> HttpResult<T> {
self.map_err(|e| HttpError::forbidden(e.to_string()))
}
fn not_found(self) -> HttpResult<T> {
self.map_err(|e| HttpError::not_found("resource", e.to_string()))
}
fn internal_error(self) -> HttpResult<T> {
self.map_err(|e| HttpError::internal(e.to_string()))
}
fn with_http_error(self, error: HttpError) -> HttpResult<T> {
self.map_err(|_| error)
}
fn map_http_error<F>(self, f: F) -> HttpResult<T>
where
F: FnOnce(String) -> HttpError,
{
self.map_err(|e| f(e.to_string()))
}
}
pub fn success_response<T: Serialize>(data: T) -> Response<Body> {
let body = serde_json::to_string(&serde_json::json!({
"success": true,
"data": data
}))
.unwrap_or_default();
Response::builder().status(StatusCode::OK).header(header::CONTENT_TYPE, "application/json").body(full_body(body)).unwrap()
}
pub fn paginated_response<T: Serialize>(items: Vec<T>, total: u64, page: u32, page_size: u32) -> Response<Body> {
let total_pages = (total as f64 / page_size as f64).ceil() as u32;
let body = serde_json::to_string(&serde_json::json!({
"success": true,
"data": {
"items": items,
"pagination": {
"total": total,
"page": page,
"page_size": page_size,
"total_pages": total_pages
}
}
}))
.unwrap_or_default();
Response::builder().status(StatusCode::OK).header(header::CONTENT_TYPE, "application/json").body(full_body(body)).unwrap()
}