#[cfg(feature = "with-hyper")]
extern crate hyper;
use std::error::Error;
use std::fmt;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::collections::HashMap;
#[cfg(feature = "with-api-error")]
mod api_error;
#[cfg(feature = "with-api-error")]
pub use api_error::*;
pub use http::StatusCode;
pub static PROBLEM_JSON_MEDIA_TYPE: &str = "application/problem+json";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
pub struct HttpApiProblem {
#[serde(rename = "type")]
#[serde(skip_serializing_if = "Option::is_none")]
pub type_url: Option<String>,
#[serde(default)]
#[serde(with = "custom_http_status_serialization")]
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<StatusCode>,
#[serde(default)]
pub title: 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<String>>(title: T) -> HttpApiProblem {
HttpApiProblem {
type_url: None,
status: None,
title: title.into(),
detail: None,
instance: None,
additional_fields: Default::default(),
}
}
pub fn with_title_and_type_from_status<T: Into<StatusCode>>(status: T) -> HttpApiProblem {
let status = status.into();
HttpApiProblem {
type_url: Some(format!("https://httpstatuses.com/{}", status.as_u16())),
status: Some(status),
title: status
.canonical_reason()
.unwrap_or("<unknown status code>")
.to_string(),
detail: None,
instance: None,
additional_fields: Default::default(),
}
}
pub fn with_title_from_status<T: Into<StatusCode>>(status: T) -> HttpApiProblem {
let status = status.into();
HttpApiProblem {
type_url: None,
status: Some(status),
title: status
.canonical_reason()
.unwrap_or("<unknown status code>")
.to_string(),
detail: None,
instance: None,
additional_fields: Default::default(),
}
}
pub fn set_type_url<T: Into<String>>(self, type_url: T) -> HttpApiProblem {
let mut s = self;
s.type_url = Some(type_url.into());
s
}
pub fn set_status<T: Into<StatusCode>>(self, status: T) -> HttpApiProblem {
let status = status.into();
let mut s = self;
s.status = Some(status);
s
}
pub fn set_title<T: Into<String>>(self, title: T) -> HttpApiProblem {
let mut s = self;
s.title = title.into();
s
}
pub fn set_detail<T: Into<String>>(self, detail: T) -> HttpApiProblem {
let mut s = self;
s.detail = Some(detail.into());
s
}
pub fn set_value<K, V>(&mut self, key: K, value: &V) -> Result<(), String>
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 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 json_value(&self, key: &str) -> Option<&serde_json::Value> {
self.additional_fields.get(key)
}
pub fn keys<K, V>(&self) -> impl Iterator<Item = &String>
where
V: DeserializeOwned,
{
self.additional_fields.keys()
}
pub fn set_instance<T: Into<String>>(self, instance: T) -> HttpApiProblem {
let mut s = self;
s.instance = Some(instance.into());
s
}
pub fn json_bytes(&self) -> Vec<u8> {
serde_json::to_vec(self).unwrap()
}
pub fn json_string(&self) -> String {
serde_json::to_string(self).unwrap()
}
pub fn status_or_internal_server_error(&self) -> StatusCode {
self.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
}
pub fn status_code_or_internal_server_error(&self) -> u16 {
self.status_or_internal_server_error().as_u16()
}
#[cfg(feature = "with-hyper")]
pub fn to_hyper_response(&self) -> hyper::Response<hyper::Body> {
use hyper::header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE};
use hyper::*;
let json = self.json_bytes();
let length = json.len() as u64;
let (mut parts, body) = Response::new(json.into()).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();
Response::from_parts(parts, body)
}
#[cfg(feature = "with-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)
.header(
actix_web::http::header::CONTENT_TYPE,
PROBLEM_JSON_MEDIA_TYPE,
)
.body(json)
}
}
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>")?;
}
if let Some(ref detail) = self.detail {
write!(f, " - {}", detail)?;
} else {
write!(f, " - {}", self.title)?;
}
Ok(())
}
}
impl Error for HttpApiProblem {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}
impl From<StatusCode> for HttpApiProblem {
fn from(status: StatusCode) -> HttpApiProblem {
HttpApiProblem::with_title_from_status(status)
}
}
#[cfg(feature = "with-iron")]
impl From<HttpApiProblem> for ::iron::response::Response {
fn from(problem: HttpApiProblem) -> ::iron::response::Response {
problem.to_iron_response()
}
}
#[cfg(feature = "with-hyper")]
pub fn into_hyper_response<T: Into<HttpApiProblem>>(what: T) -> hyper::Response<hyper::Body> {
let problem: HttpApiProblem = what.into();
problem.to_hyper_response()
}
#[cfg(feature = "with-hyper")]
impl From<HttpApiProblem> for hyper::Response<hyper::Body> {
fn from(problem: HttpApiProblem) -> hyper::Response<hyper::Body> {
problem.to_hyper_response()
}
}
#[cfg(feature = "with-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 = "with-actix-web")]
impl From<HttpApiProblem> for actix_web::HttpResponse {
fn from(problem: HttpApiProblem) -> actix_web::HttpResponse {
problem.to_actix_response()
}
}
#[cfg(feature = "with-warp")]
impl warp::reject::Reject for HttpApiProblem {}
mod custom_http_status_serialization {
use http::StatusCode;
use serde::{Deserialize, Deserializer, Serializer};
use std::convert::TryFrom;
pub fn serialize<S>(date: &Option<StatusCode>, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Some(ref status_code) = *date {
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;