graphql_starter/error/
api.rsuse std::collections::HashMap;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use http::{header::IntoHeaderName, HeaderMap, HeaderValue};
use serde::Serialize;
use super::Error;
use crate::axum::extract::Json;
pub type ApiResult<T, E = Box<ApiError>> = std::result::Result<T, E>;
#[derive(Debug, Serialize)]
pub struct ApiError {
title: String,
#[serde(serialize_with = "serialize_status_u16")]
status: StatusCode,
detail: String,
#[serde(skip_serializing_if = "HashMap::is_empty")]
info: HashMap<String, String>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
errors: HashMap<String, serde_json::Value>,
#[serde(skip)]
headers: Option<HeaderMap>,
}
impl ApiError {
pub fn new(status: StatusCode, detail: impl Into<String>) -> Self {
ApiError {
title: status.canonical_reason().unwrap_or("Internal server error").to_owned(),
status,
detail: detail.into(),
info: Default::default(),
errors: Default::default(),
headers: None,
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn with_info(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.info.insert(key.into(), value.into());
self
}
pub fn with_error_info(mut self, field: impl Into<String>, info: serde_json::Value) -> Self {
self.errors.insert(field.into(), info);
self
}
pub fn with_header(mut self, key: impl IntoHeaderName, value: impl TryInto<HeaderValue>) -> Self {
if let Ok(value) = value.try_into() {
let headers = self.headers.get_or_insert_with(Default::default);
headers.append(key, value);
}
self
}
pub fn boxed(self) -> Box<Self> {
Box::new(self)
}
pub fn title(&self) -> &str {
&self.title
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn detail(&self) -> &str {
&self.detail
}
pub fn info(&self) -> &HashMap<String, String> {
&self.info
}
pub fn errors(&self) -> &HashMap<String, serde_json::Value> {
&self.errors
}
pub fn headers(&self) -> &Option<HeaderMap> {
&self.headers
}
}
impl From<Box<Error>> for ApiError {
fn from(err: Box<Error>) -> Self {
(*err).into()
}
}
impl From<Box<Error>> for Box<ApiError> {
fn from(err: Box<Error>) -> Self {
(*err).into()
}
}
impl<T> From<T> for Box<ApiError>
where
T: Into<Error>,
{
fn from(error: T) -> Self {
ApiError::from(error).boxed()
}
}
impl<T> From<T> for ApiError
where
T: Into<Error>,
{
fn from(error: T) -> Self {
let error: Error = error.into();
if error.unexpected {
tracing::error!("{error:#}");
} else if tracing::event_enabled!(tracing::Level::DEBUG) {
tracing::warn!("{error:#}")
} else {
tracing::warn!("{error}")
}
let mut ret = ApiError::new(error.info.status(), error.info.message());
ret = ret.with_info("errorCode", error.info.code());
ret = ret.with_info("rawMessage", error.info.raw_message());
for (key, value) in error.info.fields() {
ret = ret.with_info(key, value);
}
if let Some(properties) = error.properties {
for (key, value) in properties {
ret = ret.with_error_info(key, value);
}
}
ret
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
if let Some(headers) = &self.headers {
(self.status, headers.clone(), Json(self)).into_response()
} else {
(self.status, Json(self)).into_response()
}
}
}
impl IntoResponse for Box<ApiError> {
fn into_response(self) -> Response {
(*self).into_response()
}
}
fn serialize_status_u16<S>(status: &StatusCode, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u16(status.as_u16())
}