use std::convert::TryInto;
use std::error::Error;
use std::fmt;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[cfg(feature = "api-error")]
mod api_error;
#[cfg(feature = "api-error")]
pub use api_error::*;
#[cfg(feature = "json-schema")]
use schemars::JsonSchema;
#[cfg(feature = "actix-web")]
use actix_web_crate as actix_web;
#[cfg(feature = "axum")]
use axum_core;
pub use http::status::{InvalidStatusCode, StatusCode};
pub static PROBLEM_JSON_MEDIA_TYPE: &str = "application/problem+json";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
#[cfg_attr(
feature = "json-schema",
schemars(
description = "Description of a problem that can be returned by an HTTP API based on [RFC7807](https://tools.ietf.org/html/rfc7807)"
)
)]
pub struct HttpApiProblem {
#[serde(rename = "type")]
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(
feature = "json-schema",
schemars(
description = "A [RFC3986 URI reference](https://tools.ietf.org/html/rfc3986) that identifies the problem type. When dereferenced, it may provide human-readable documentation for the problem type."
)
)]
pub type_url: Option<String>,
#[serde(default)]
#[serde(with = "custom_http_status_serialization")]
#[cfg_attr(feature = "json-schema", schemars(with = "u16"))]
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<StatusCode>,
#[cfg_attr(
feature = "json-schema",
schemars(
description = "A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence of the problem."
)
)]
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instance: Option<String>,
#[serde(flatten)]
additional_fields: HashMap<String, serde_json::Value>,
}
impl HttpApiProblem {
pub fn new<T: Into<StatusCode>>(status: T) -> Self {
Self::empty().status(status)
}
pub fn try_new<T: TryInto<StatusCode>>(status: T) -> Result<Self, InvalidStatusCode>
where
T::Error: Into<InvalidStatusCode>,
{
let status = status.try_into().map_err(|e| e.into())?;
Ok(Self::new(status))
}
pub fn with_title<T: Into<StatusCode>>(status: T) -> Self {
let status = status.into();
Self::new(status).title(
status
.canonical_reason()
.unwrap_or("<unknown status code>")
.to_string(),
)
}
pub fn try_with_title<T: TryInto<StatusCode>>(status: T) -> Result<Self, InvalidStatusCode>
where
T::Error: Into<InvalidStatusCode>,
{
let status = status.try_into().map_err(|e| e.into())?;
Ok(Self::with_title(status))
}
pub fn with_title_and_type<T: Into<StatusCode>>(status: T) -> Self {
let status = status.into();
Self::with_title(status).type_url(format!("https://httpstatuses.com/{}", status.as_u16()))
}
pub fn try_with_title_and_type<T: TryInto<StatusCode>>(
status: T,
) -> Result<Self, InvalidStatusCode>
where
T::Error: Into<InvalidStatusCode>,
{
let status = status.try_into().map_err(|e| e.into())?;
Ok(Self::with_title_and_type(status))
}
pub fn empty() -> Self {
HttpApiProblem {
type_url: None,
status: None,
title: None,
detail: None,
instance: None,
additional_fields: Default::default(),
}
}
pub fn status<T: Into<StatusCode>>(mut self, status: T) -> Self {
self.status = Some(status.into());
self
}
pub fn type_url<T: Into<String>>(mut self, type_url: T) -> Self {
self.type_url = Some(type_url.into());
self
}
pub fn try_status<T: TryInto<StatusCode>>(
mut self,
status: T,
) -> Result<Self, InvalidStatusCode>
where
T::Error: Into<InvalidStatusCode>,
{
self.status = Some(status.try_into().map_err(|e| e.into())?);
Ok(self)
}
pub fn title<T: Into<String>>(mut self, title: T) -> Self {
self.title = Some(title.into());
self
}
pub fn detail<T: Into<String>>(mut self, detail: T) -> HttpApiProblem {
self.detail = Some(detail.into());
self
}
pub fn instance<T: Into<String>>(mut self, instance: T) -> HttpApiProblem {
self.instance = Some(instance.into());
self
}
pub fn try_value<K, V>(
mut self,
key: K,
value: &V,
) -> Result<Self, Box<dyn Error + Send + Sync + 'static>>
where
V: Serialize,
K: Into<String>,
{
self.try_set_value(key, value)?;
Ok(self)
}
pub fn value<K, V>(mut self, key: K, value: &V) -> Self
where
V: Serialize,
K: Into<String>,
{
self.set_value(key, value);
self
}
pub fn set_value<K, V>(&mut self, key: K, value: &V)
where
V: Serialize,
K: Into<String>,
{
let _ = self.try_set_value(key, value);
}
pub fn get_value<K, V>(&self, key: &str) -> Option<V>
where
V: DeserializeOwned,
{
self.json_value(key)
.and_then(|v| serde_json::from_value(v.clone()).ok())
}
pub fn try_set_value<K, V>(
&mut self,
key: K,
value: &V,
) -> Result<(), Box<dyn Error + Send + Sync + 'static>>
where
V: Serialize,
K: Into<String>,
{
let key: String = key.into();
match key.as_ref() {
"type" => return Err("'type' is a reserved field name".into()),
"status" => return Err("'status' is a reserved field name".into()),
"title" => return Err("'title' is a reserved field name".into()),
"detail" => return Err("'detail' is a reserved field name".into()),
"instance" => return Err("'instance' is a reserved field name".into()),
"additional_fields" => {
return Err("'additional_fields' is a reserved field name".into());
}
_ => (),
}
let serialized = serde_json::to_value(value).map_err(|err| err.to_string())?;
self.additional_fields.insert(key, serialized);
Ok(())
}
pub fn additional_fields(&self) -> &HashMap<String, Value> {
&self.additional_fields
}
pub fn additional_fields_mut(&mut self) -> &mut HashMap<String, Value> {
&mut self.additional_fields
}
pub fn keys<K, V>(&self) -> impl Iterator<Item = &String>
where
V: DeserializeOwned,
{
self.additional_fields.keys()
}
pub fn json_value(&self, key: &str) -> Option<&serde_json::Value> {
self.additional_fields.get(key)
}
pub fn json_bytes(&self) -> Vec<u8> {
serde_json::to_vec(self).unwrap()
}
pub fn json_string(&self) -> String {
serde_json::to_string_pretty(self).unwrap()
}
#[cfg(feature = "hyper")]
pub fn to_hyper_response(&self) -> hyper::Response<String> {
use hyper::header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE};
use hyper::*;
let json = self.json_string();
let length = json.len() as u64;
let (mut parts, body) = Response::new(json).into_parts();
parts.headers.insert(
CONTENT_TYPE,
HeaderValue::from_static(PROBLEM_JSON_MEDIA_TYPE),
);
parts.headers.insert(
CONTENT_LENGTH,
HeaderValue::from_str(&length.to_string()).unwrap(),
);
parts.status = self
.status_or_internal_server_error()
.as_u16()
.try_into()
.unwrap_or(hyper::StatusCode::INTERNAL_SERVER_ERROR);
Response::from_parts(parts, body)
}
#[cfg(feature = "axum")]
pub fn to_axum_response(&self) -> axum_core::response::Response {
use axum_core::response::IntoResponse;
use http::header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE};
let json = self.json_bytes();
let length = json.len() as u64;
let status = self.status_or_internal_server_error();
let mut response = (status, json).into_response();
*response.status_mut() = self.status_or_internal_server_error();
response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static(PROBLEM_JSON_MEDIA_TYPE),
);
response.headers_mut().insert(
CONTENT_LENGTH,
HeaderValue::from_str(&length.to_string()).unwrap(),
);
response
}
#[cfg(feature = "actix-web")]
pub fn to_actix_response(&self) -> actix_web::HttpResponse {
let effective_status = self.status_or_internal_server_error();
let actix_status = actix_web::http::StatusCode::from_u16(effective_status.as_u16())
.unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR);
let json = self.json_bytes();
actix_web::HttpResponse::build(actix_status)
.append_header((
actix_web::http::header::CONTENT_TYPE,
PROBLEM_JSON_MEDIA_TYPE,
))
.body(json)
}
#[cfg(feature = "rocket")]
pub fn to_rocket_response(&self) -> rocket::Response<'static> {
use rocket::http::ContentType;
use rocket::http::Status;
use rocket::Response;
use std::io::Cursor;
let content_type: ContentType = PROBLEM_JSON_MEDIA_TYPE.parse().unwrap();
let json = self.json_bytes();
let response = Response::build()
.status(Status {
code: self.status_code_or_internal_server_error(),
})
.sized_body(json.len(), Cursor::new(json))
.header(content_type)
.finalize();
response
}
#[cfg(feature = "salvo")]
pub fn to_salvo_response(&self) -> salvo::Response {
use salvo::hyper::header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE};
use salvo::hyper::*;
let json = self.json_string();
let length = json.len() as u64;
let (mut parts, body) = Response::new(json).into_parts();
parts.headers.insert(
CONTENT_TYPE,
HeaderValue::from_static(PROBLEM_JSON_MEDIA_TYPE),
);
parts.headers.insert(
CONTENT_LENGTH,
HeaderValue::from_str(&length.to_string()).unwrap(),
);
parts.status = self
.status_or_internal_server_error()
.as_u16()
.try_into()
.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
Response::from_parts(parts, body).into()
}
#[cfg(feature = "tide")]
pub fn to_tide_response(&self) -> tide::Response {
let json = self.json_bytes();
let length = json.len() as u64;
tide::Response::builder(self.status_code_or_internal_server_error())
.body(json)
.header("Content-Length", length.to_string())
.content_type(PROBLEM_JSON_MEDIA_TYPE)
.build()
}
#[allow(dead_code)]
fn status_or_internal_server_error(&self) -> StatusCode {
self.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
}
#[allow(dead_code)]
fn status_code_or_internal_server_error(&self) -> u16 {
self.status_or_internal_server_error().as_u16()
}
#[deprecated(since = "0.50.0", note = "please use `with_title` instead")]
pub fn with_title_from_status<T: Into<StatusCode>>(status: T) -> Self {
Self::with_title(status)
}
#[deprecated(since = "0.50.0", note = "please use `with_title_and_type` instead")]
pub fn with_title_and_type_from_status<T: Into<StatusCode>>(status: T) -> Self {
Self::with_title_and_type(status)
}
#[deprecated(since = "0.50.0", note = "please use `status` instead")]
pub fn set_status<T: Into<StatusCode>>(self, status: T) -> Self {
self.status(status)
}
#[deprecated(since = "0.50.0", note = "please use `title` instead")]
pub fn set_title<T: Into<String>>(self, title: T) -> Self {
self.title(title)
}
#[deprecated(since = "0.50.0", note = "please use `detail` instead")]
pub fn set_detail<T: Into<String>>(self, detail: T) -> Self {
self.detail(detail)
}
#[deprecated(since = "0.50.0", note = "please use `type_url` instead")]
pub fn set_type_url<T: Into<String>>(self, type_url: T) -> Self {
self.type_url(type_url)
}
#[deprecated(since = "0.50.0", note = "please use `instance` instead")]
pub fn set_instance<T: Into<String>>(self, instance: T) -> Self {
self.instance(instance)
}
}
impl fmt::Display for HttpApiProblem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(status) = self.status {
write!(f, "{}", status)?;
} else {
write!(f, "<no status>")?;
}
match (self.title.as_ref(), self.detail.as_ref()) {
(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, " - {}", type_url);
}
Ok(())
}
}
impl Error for HttpApiProblem {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}
impl From<StatusCode> for HttpApiProblem {
fn from(status: StatusCode) -> HttpApiProblem {
HttpApiProblem::new(status)
}
}
impl From<std::convert::Infallible> for HttpApiProblem {
fn from(error: std::convert::Infallible) -> HttpApiProblem {
match error {}
}
}
#[cfg(feature = "hyper")]
pub fn into_hyper_response<T: Into<HttpApiProblem>>(what: T) -> hyper::Response<String> {
let problem: HttpApiProblem = what.into();
problem.to_hyper_response()
}
#[cfg(feature = "hyper")]
impl From<HttpApiProblem> for hyper::Response<String> {
fn from(problem: HttpApiProblem) -> hyper::Response<String> {
problem.to_hyper_response()
}
}
#[cfg(feature = "axum")]
pub fn into_axum_response<T: Into<HttpApiProblem>>(what: T) -> axum_core::response::Response {
let problem: HttpApiProblem = what.into();
problem.to_axum_response()
}
#[cfg(feature = "axum")]
impl From<HttpApiProblem> for axum_core::response::Response {
fn from(problem: HttpApiProblem) -> axum_core::response::Response {
problem.to_axum_response()
}
}
#[cfg(feature = "axum")]
impl axum_core::response::IntoResponse for HttpApiProblem {
fn into_response(self) -> axum_core::response::Response {
self.into()
}
}
#[cfg(feature = "actix-web")]
pub fn into_actix_response<T: Into<HttpApiProblem>>(what: T) -> actix_web::HttpResponse {
let problem: HttpApiProblem = what.into();
problem.to_actix_response()
}
#[cfg(feature = "actix-web")]
impl From<HttpApiProblem> for actix_web::HttpResponse {
fn from(problem: HttpApiProblem) -> actix_web::HttpResponse {
problem.to_actix_response()
}
}
#[cfg(feature = "rocket")]
pub fn into_rocket_response<T: Into<HttpApiProblem>>(what: T) -> ::rocket::Response<'static> {
let problem: HttpApiProblem = what.into();
problem.to_rocket_response()
}
#[cfg(feature = "rocket")]
impl From<HttpApiProblem> for ::rocket::Response<'static> {
fn from(problem: HttpApiProblem) -> ::rocket::Response<'static> {
problem.to_rocket_response()
}
}
#[cfg(feature = "rocket")]
impl<'r> ::rocket::response::Responder<'r, 'static> for HttpApiProblem {
fn respond_to(self, _request: &::rocket::Request) -> ::rocket::response::Result<'static> {
Ok(self.into())
}
}
#[cfg(feature = "rocket-okapi")]
impl rocket_okapi::response::OpenApiResponderInner for HttpApiProblem {
fn responses(
gen: &mut rocket_okapi::gen::OpenApiGenerator,
) -> rocket_okapi::Result<rocket_okapi::okapi::openapi3::Responses> {
let mut responses = rocket_okapi::okapi::openapi3::Responses::default();
let schema = gen.json_schema::<HttpApiProblem>();
rocket_okapi::util::add_default_response_schema(
&mut responses,
PROBLEM_JSON_MEDIA_TYPE,
schema,
);
Ok(responses)
}
}
#[cfg(feature = "warp")]
impl warp::reject::Reject for HttpApiProblem {}
#[cfg(feature = "salvo")]
pub fn into_salvo_response<T: Into<HttpApiProblem>>(what: T) -> salvo::Response {
let problem: HttpApiProblem = what.into();
problem.to_salvo_response()
}
#[cfg(feature = "salvo")]
impl From<HttpApiProblem> for salvo::Response {
fn from(problem: HttpApiProblem) -> salvo::Response {
problem.to_salvo_response()
}
}
#[cfg(feature = "tide")]
pub fn into_tide_response<T: Into<HttpApiProblem>>(what: T) -> tide::Response {
let problem: HttpApiProblem = what.into();
problem.to_tide_response()
}
#[cfg(feature = "tide")]
impl From<HttpApiProblem> for tide::Response {
fn from(problem: HttpApiProblem) -> tide::Response {
problem.to_tide_response()
}
}
mod custom_http_status_serialization {
use http::StatusCode;
use serde::{Deserialize, Deserializer, Serializer};
use std::convert::TryFrom;
pub fn serialize<S>(status: &Option<StatusCode>, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(ref status_code) = *status {
return s.serialize_u16(status_code.as_u16());
}
s.serialize_none()
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<StatusCode>, D::Error>
where
D: Deserializer<'de>,
{
let s: Option<u16> = Option::deserialize(deserializer)?;
if let Some(numeric_status_code) = s {
let status_code = StatusCode::try_from(numeric_status_code).ok();
return Ok(status_code);
}
Ok(None)
}
}
#[cfg(test)]
mod test;