use http::{StatusCode, Uri};
use crate::ProblemType;
#[cfg(feature = "json")]
mod json;
#[cfg(feature = "json")]
pub use json::JsonProblemDetails;
#[cfg(feature = "xml")]
mod xml;
#[cfg(feature = "xml")]
pub use xml::XmlProblemDetails;
#[cfg(test)]
mod tests;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(
feature = "utoipa",
schema(description = "RFC 9457 / RFC 7807 problem details")
)]
pub struct ProblemDetails<Ext = ()> {
#[cfg_attr(feature = "serde", serde(default))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg_attr(feature = "utoipa", schema(value_type = String, format = Uri))]
pub r#type: Option<ProblemType>,
#[cfg_attr(feature = "serde", serde(default))]
#[cfg_attr(feature = "serde", serde(with = "crate::serde::status::opt"))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg_attr(feature = "utoipa", schema(value_type = u16))]
pub status: Option<StatusCode>,
#[cfg_attr(feature = "serde", serde(default))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub title: Option<String>,
#[cfg_attr(feature = "serde", serde(default))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub detail: Option<String>,
#[cfg_attr(feature = "serde", serde(default))]
#[cfg_attr(feature = "serde", serde(with = "crate::serde::uri::opt"))]
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[cfg_attr(feature = "utoipa", schema(value_type = String, format = Uri))]
pub instance: Option<Uri>,
#[cfg_attr(feature = "serde", serde(flatten))]
#[cfg_attr(feature = "utoipa", schema(inline))]
pub extensions: Ext,
}
impl ProblemDetails<()> {
#[must_use]
pub fn new() -> Self {
Self {
r#type: None,
status: None,
title: None,
detail: None,
instance: None,
extensions: Default::default(),
}
}
#[must_use]
pub fn from_status_code(status: StatusCode) -> Self {
Self {
r#type: None,
status: Some(status),
title: status.canonical_reason().map(ToOwned::to_owned),
detail: None,
instance: None,
extensions: Default::default(),
}
}
}
impl<Ext> ProblemDetails<Ext> {
#[must_use]
pub fn with_type(mut self, r#type: impl Into<ProblemType>) -> Self {
self.r#type = Some(r#type.into());
self
}
#[must_use]
pub fn with_status(mut self, status: impl Into<StatusCode>) -> Self {
self.status = Some(status.into());
self
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
#[must_use]
pub fn with_instance(mut self, instance: impl Into<Uri>) -> Self {
self.instance = Some(instance.into());
self
}
#[must_use]
pub fn with_extensions<NewExt>(self, extensions: NewExt) -> ProblemDetails<NewExt> {
ProblemDetails::<NewExt> {
r#type: self.r#type,
status: self.status,
title: self.title,
detail: self.detail,
instance: self.instance,
extensions,
}
}
}
impl<Ext> std::fmt::Display for ProblemDetails<Ext> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let default_type = ProblemType::default();
let r#type = self.r#type.as_ref().unwrap_or(&default_type);
write!(f, "[{type}")?;
if let Some(status) = self.status {
write!(f, " {}]", status.as_u16())?;
} else {
write!(f, "]")?;
}
let title = self
.title
.as_deref()
.or(self.status.as_ref().and_then(StatusCode::canonical_reason));
if let Some(title) = title {
write!(f, " {title}")?;
}
if let Some(detail) = self.detail.as_ref() {
if title.is_some() {
write!(f, ":")?;
}
write!(f, " {detail}")?;
}
Ok(())
}
}
impl<Ext> std::error::Error for ProblemDetails<Ext> where Ext: std::fmt::Debug {}