use axum::Json;
use axum::http::{HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
struct ErrorData {
sub_category: Option<String>,
code: Option<String>,
exception: Option<bool>,
extra: Option<Vec<String>>,
}
#[derive(Serialize)]
struct ErrorSerialize<'a> {
category: &'a str,
message: &'a str,
#[serde(flatten)]
data: &'a ErrorData,
}
#[derive(Deserialize)]
struct ErrorDeserialize {
#[serde(default)]
category: String,
#[serde(default)]
message: String,
#[serde(flatten)]
data: ErrorData,
}
#[derive(Debug, Clone, Default)]
pub struct Error {
status: u16,
category: String,
message: String,
data: Box<ErrorData>,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for Error {}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
ErrorSerialize {
category: &self.category,
message: &self.message,
data: &self.data,
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Error {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let d = ErrorDeserialize::deserialize(deserializer)?;
Ok(Self {
status: 0,
category: d.category,
message: d.message,
data: Box::new(d.data),
})
}
}
impl Error {
#[must_use]
pub fn new(message: impl ToString) -> Self {
Self {
message: message.to_string(),
..Default::default()
}
}
#[must_use]
pub fn with_category(mut self, category: impl Into<String>) -> Self {
self.category = category.into();
self
}
#[must_use]
pub fn with_sub_category(mut self, sub_category: impl Into<String>) -> Self {
self.data.sub_category = Some(sub_category.into());
self
}
#[must_use]
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.data.code = Some(code.into());
self
}
#[must_use]
pub fn with_status(mut self, status: u16) -> Self {
self.status = status;
self
}
#[must_use]
pub fn with_exception(mut self, exception: bool) -> Self {
self.data.exception = Some(exception);
self
}
#[must_use]
pub fn add_extra(mut self, value: impl Into<String>) -> Self {
self.data
.extra
.get_or_insert_with(Vec::new)
.push(value.into());
self
}
pub fn status(&self) -> u16 {
self.status
}
pub fn category(&self) -> &str {
&self.category
}
pub fn message(&self) -> &str {
&self.message
}
pub fn sub_category(&self) -> Option<&str> {
self.data.sub_category.as_deref()
}
pub fn code(&self) -> Option<&str> {
self.data.code.as_deref()
}
pub fn is_exception(&self) -> bool {
self.data.exception.unwrap_or(false)
}
pub fn extra(&self) -> &[String] {
self.data.extra.as_deref().unwrap_or(&[])
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
let status = StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let mut res = if status.is_server_error() || self.is_exception() {
let redacted = ErrorData {
sub_category: self.data.sub_category.clone(),
code: self.data.code.clone(),
exception: self.data.exception,
extra: None,
};
(
status,
Json(ErrorSerialize {
category: &self.category,
message: "internal server error",
data: &redacted,
}),
)
.into_response()
} else {
(status, Json(&self)).into_response()
};
res.extensions_mut().insert(self);
res.headers_mut()
.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
res
}
}