use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt::{self, Display};
use std::io;
use std::error::Error;
use serde::Serialize;
use serde_json::Value;
use super::*;
pub struct ApiErrorBuilder {
pub status: StatusCode,
pub title: Option<String>,
pub message: Option<String>,
pub type_url: Option<String>,
pub instance: Option<String>,
pub fields: HashMap<String, Value>,
pub source: Option<Box<dyn Error + Send + Sync + 'static>>,
}
impl ApiErrorBuilder {
pub fn status<T: Into<StatusCode>>(mut self, status: T) -> Self {
self.status = status.into();
self
}
pub fn try_status<T: TryInto<StatusCode>>(self, status: T) -> Result<Self, InvalidStatusCode>
where
T::Error: Into<InvalidStatusCode>,
{
let status = status.try_into().map_err(|e| e.into())?;
Ok(self.status(status))
}
pub fn title<T: Display>(mut self, title: T) -> Self {
self.title = Some(title.to_string());
self
}
pub fn message<M: Display>(mut self, message: M) -> Self {
self.message = Some(message.to_string());
self
}
pub fn type_url<U: Display>(mut self, type_url: U) -> Self {
self.type_url = Some(type_url.to_string());
self
}
pub fn instance<T: Display>(mut self, instance: T) -> Self {
self.instance = Some(instance.to_string());
self
}
pub fn field<T: Into<String>, V: Serialize>(mut self, name: T, value: V) -> Self {
if let Ok(value) = serde_json::to_value(value) {
self.fields.insert(name.into(), value);
}
self
}
pub fn source<E: Error + Send + Sync + 'static>(self, source: E) -> Self {
self.source_in_a_box(Box::new(source))
}
pub fn source_in_a_box<E: Into<Box<dyn Error + Send + Sync + 'static>>>(
mut self,
source: E,
) -> Self {
self.source = Some(source.into());
self
}
pub fn finish(self) -> ApiError {
ApiError {
status: self.status,
title: self.title,
message: self.message,
type_url: self.type_url,
instance: self.instance,
fields: self.fields,
source: self.source,
}
}
}
#[derive(Debug)]
pub struct ApiError {
status: StatusCode,
title: Option<String>,
message: Option<String>,
instance: Option<String>,
type_url: Option<String>,
fields: HashMap<String, Value>,
source: Option<Box<dyn Error + Send + Sync + 'static>>,
}
impl ApiError {
pub fn builder<T: Into<StatusCode>>(status: T) -> ApiErrorBuilder {
ApiErrorBuilder {
status: status.into(),
title: None,
message: None,
type_url: None,
instance: None,
fields: HashMap::default(),
source: None,
}
}
pub fn try_builder<S: TryInto<StatusCode>>(
status: S,
) -> Result<ApiErrorBuilder, InvalidStatusCode>
where
S::Error: Into<InvalidStatusCode>,
{
let status = status.try_into().map_err(|e| e.into())?;
Ok(Self::builder(status))
}
pub fn new<T: Into<StatusCode>>(status: T) -> Self {
Self {
status: status.into(),
title: None,
message: None,
type_url: None,
instance: None,
fields: HashMap::new(),
source: None,
}
}
pub fn try_new<S: TryInto<StatusCode>>(status: S) -> Result<Self, InvalidStatusCode>
where
S::Error: Into<InvalidStatusCode>,
{
let status = status.try_into().map_err(|e| e.into())?;
Ok(Self::new(status))
}
pub fn set_status<T: Into<StatusCode>>(&mut self, status: T) {
self.status = status.into();
}
pub fn status<T: Into<StatusCode>>(&self) -> StatusCode {
self.status
}
pub fn set_title<T: Display>(&mut self, title: T) {
self.title = Some(title.to_string())
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn set_message<T: Display>(&mut self, message: T) {
self.message = Some(message.to_string())
}
pub fn message(&self) -> Option<&str> {
self.message.as_deref()
}
pub fn set_type_url<T: Display>(&mut self, type_url: T) {
self.type_url = Some(type_url.to_string())
}
pub fn type_url(&self) -> Option<&str> {
self.type_url.as_deref()
}
pub fn set_instance<T: Display>(&mut self, instance: T) {
self.instance = Some(instance.to_string())
}
pub fn instance(&self) -> Option<&str> {
self.instance.as_deref()
}
pub fn set_source<E: Error + Send + Sync + 'static>(&mut self, source: E) {
self.set_source_in_a_box(Box::new(source))
}
pub fn set_source_in_a_box<E: Into<Box<dyn Error + Send + Sync + 'static>>>(
&mut self,
source: E,
) {
self.source = Some(source.into());
}
pub fn add_field<T: Into<String>, V: Serialize>(&mut self, name: T, value: V) -> bool {
self.try_add_field(name, value).is_ok()
}
pub fn try_add_field<T: Into<String>, V: Serialize>(
&mut self,
name: T,
value: V,
) -> Result<(), Box<dyn Error + 'static>> {
match serde_json::to_value(value) {
Ok(value) => {
self.fields.insert(name.into(), value);
Ok(())
}
Err(err) => Err(Box::new(err)),
}
}
pub fn to_http_api_problem(&self) -> HttpApiProblem {
let mut problem = HttpApiProblem::with_title_and_type(self.status);
problem.title = self.title.clone();
if let Some(message) = self.detail_message() {
problem.detail = Some(message.into())
}
problem.type_url = self.type_url.clone();
problem.instance = self.instance.clone();
if self.status != StatusCode::UNAUTHORIZED {
for (key, value) in self.fields.iter() {
let _ = problem.set_value(key.to_string(), value);
}
}
problem
}
pub fn into_http_api_problem(self) -> HttpApiProblem {
let mut problem = HttpApiProblem::with_title_and_type(self.status);
if let Some(title) = self.title.as_ref() {
problem.title = Some(title.to_owned());
}
if let Some(message) = self.detail_message() {
problem.detail = Some(message.into())
}
if let Some(type_url) = self.type_url.as_ref() {
problem.type_url = Some(type_url.to_owned())
}
if let Some(instance) = self.instance.as_ref() {
problem.instance = Some(instance.to_owned())
}
if self.status != StatusCode::UNAUTHORIZED {
for (key, value) in self.fields.iter() {
let _ = problem.set_value(key.to_string(), value);
}
}
problem
}
pub fn detail_message(&self) -> Option<Cow<str>> {
if let Some(message) = self.message.as_ref() {
return Some(Cow::Borrowed(message));
}
if let Some(source) = self.source() {
return Some(Cow::Owned(source.to_string()));
}
None
}
#[cfg(feature = "hyper")]
pub fn into_hyper_response(self) -> hyper::Response<hyper::Body> {
let problem = self.into_http_api_problem();
problem.to_hyper_response()
}
#[cfg(feature = "actix-web")]
pub fn into_actix_web_response(self) -> actix_web::HttpResponse {
let problem = self.into_http_api_problem();
problem.into()
}
#[cfg(feature = "salvo")]
pub fn into_salvo_response(self) -> salvo::Response {
let problem = self.into_http_api_problem();
problem.to_salvo_response()
}
#[cfg(feature = "tide")]
pub fn into_tide_response(self) -> tide::Response {
let problem = self.into_http_api_problem();
problem.to_tide_response()
}
}
impl Error for ApiError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source.as_ref().map(|e| &**e as _)
}
}
impl Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.status)?;
match (self.title.as_ref(), self.detail_message()) {
(Some(title), Some(detail)) => return write!(f, " - {} - {}", title, detail),
(Some(title), None) => return write!(f, " - {}", title),
(None, Some(detail)) => return write!(f, " - {}", detail),
(None, None) => (),
}
if let Some(type_url) = self.type_url.as_ref() {
return write!(f, " of type {}", type_url);
}
if let Some(instance) = self.instance.as_ref() {
return write!(f, " on {}", instance);
}
Ok(())
}
}
impl From<StatusCode> for ApiError {
fn from(s: StatusCode) -> Self {
Self::new(s)
}
}
impl From<ApiErrorBuilder> for ApiError {
fn from(builder: ApiErrorBuilder) -> Self {
builder.finish()
}
}
impl From<ApiError> for HttpApiProblem {
fn from(error: ApiError) -> Self {
error.into_http_api_problem()
}
}
impl From<io::Error> for ApiError {
fn from(error: io::Error) -> Self {
ApiError::builder(StatusCode::INTERNAL_SERVER_ERROR)
.title("An IO error occurred")
.source(error)
.finish()
}
}
#[cfg(feature = "hyper")]
impl From<hyper::Error> for ApiError {
fn from(error: hyper::Error) -> Self {
ApiError::builder(StatusCode::INTERNAL_SERVER_ERROR)
.source(error)
.finish()
}
}
#[cfg(feature = "hyper")]
impl From<ApiError> for hyper::Response<hyper::Body> {
fn from(error: ApiError) -> hyper::Response<hyper::Body> {
error.into_hyper_response()
}
}
#[cfg(feature = "actix-web")]
impl From<actix::prelude::MailboxError> for ApiError {
fn from(error: actix::prelude::MailboxError) -> Self {
ApiError::builder(StatusCode::INTERNAL_SERVER_ERROR)
.source(error)
.finish()
}
}
#[cfg(feature = "actix-web")]
impl From<ApiError> for actix_web::HttpResponse {
fn from(error: ApiError) -> Self {
error.into_actix_web_response()
}
}
#[cfg(feature = "actix-web")]
impl actix_web::error::ResponseError for ApiError {
fn error_response(&self) -> actix_web::HttpResponse {
let json = self.to_http_api_problem().json_bytes();
let actix_status = actix_web::http::StatusCode::from_u16(self.status.as_u16())
.unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR);
actix_web::HttpResponse::build(actix_status)
.header(
actix_web::http::header::CONTENT_TYPE,
PROBLEM_JSON_MEDIA_TYPE,
)
.body(json)
}
}
#[cfg(feature = "warp")]
impl warp::reject::Reject for ApiError {}
#[cfg(feature = "salvo")]
impl From<salvo::Error> for ApiError {
fn from(error: salvo::Error) -> Self {
ApiError::builder(StatusCode::INTERNAL_SERVER_ERROR)
.source(error)
.finish()
}
}
#[cfg(feature = "salvo")]
impl From<ApiError> for salvo::Response {
fn from(error: ApiError) -> salvo::Response {
error.into_salvo_response()
}
}
#[cfg(feature = "tide")]
impl From<tide::Error> for ApiError {
fn from(error: tide::Error) -> Self {
let status: StatusCode = u16::from(error.status())
.try_into()
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
ApiError::builder(status)
.source_in_a_box(error.into_inner())
.finish()
}
}
#[cfg(feature = "tide")]
impl From<ApiError> for tide::Response {
fn from(error: ApiError) -> tide::Response {
error.into_tide_response()
}
}