use serde::{Deserialize, Serialize};
use serde_json::Map;
use std::fmt;
use crate::ValidationItem;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Problem {
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub type_uri: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instance: Option<String>,
#[serde(flatten, skip_serializing_if = "Map::is_empty")]
pub extensions: Map<String, serde_json::Value>,
#[serde(skip)]
cause: Option<InternalCause>,
}
struct InternalCause {
source: Box<dyn std::error::Error + Send + Sync>,
}
impl fmt::Debug for InternalCause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "InternalCause({:?})", self.source.to_string())
}
}
impl Clone for InternalCause {
fn clone(&self) -> Self {
InternalCause {
source: Box::new(StringError(self.source.to_string())),
}
}
}
#[derive(Debug)]
struct StringError(String);
impl fmt::Display for StringError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for StringError {}
impl Problem {
pub fn new(status: u16) -> Self {
Self {
type_uri: None,
title: status_phrase(status).map(String::from),
status: Some(status),
detail: None,
instance: None,
extensions: Map::new(),
cause: None,
}
}
pub fn bad_request() -> Self {
Self::new(400)
}
pub fn unauthorized() -> Self {
Self::new(401)
}
pub fn forbidden() -> Self {
Self::new(403)
}
pub fn not_found() -> Self {
Self::new(404)
}
pub fn conflict() -> Self {
Self::new(409)
}
pub fn validation() -> Self {
Self::new(422)
.type_("validation_error")
.title("Validation failed")
}
pub fn unprocessable_entity() -> Self {
Self::new(422)
}
pub fn too_many_requests() -> Self {
Self::new(429)
}
pub fn internal_server_error() -> Self {
Self::new(500)
.title("Internal Server Error")
.detail("An unexpected error occurred.")
}
}
impl Problem {
pub fn type_(mut self, type_uri: impl Into<String>) -> Self {
self.type_uri = Some(type_uri.into());
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn status(mut self, status: u16) -> Self {
self.status = Some(status);
self
}
pub fn detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
pub fn instance(mut self, instance: impl Into<String>) -> Self {
self.instance = Some(instance.into());
self
}
pub fn code(mut self, code: impl Into<String>) -> Self {
self.extensions
.insert("code".into(), serde_json::Value::String(code.into()));
self
}
pub fn trace_id(mut self, trace_id: impl Into<String>) -> Self {
self.extensions.insert(
"trace_id".into(),
serde_json::Value::String(trace_id.into()),
);
self
}
pub fn request_id(mut self, request_id: impl Into<String>) -> Self {
self.extensions.insert(
"request_id".into(),
serde_json::Value::String(request_id.into()),
);
self
}
pub fn extension(
mut self,
key: impl Into<String>,
value: impl Into<serde_json::Value>,
) -> Self {
self.extensions.insert(key.into(), value.into());
self
}
pub fn push_error(self, field: impl Into<String>, message: impl Into<String>) -> Self {
self.push_validation_item(ValidationItem::new(field, message))
}
pub fn push_error_code(
self,
field: impl Into<String>,
message: impl Into<String>,
code: impl Into<String>,
) -> Self {
self.push_validation_item(ValidationItem::new(field, message).code(code))
}
fn push_validation_item(mut self, item: ValidationItem) -> Self {
let value = serde_json::to_value(&item).expect("ValidationItem is always serializable");
match self.extensions.get_mut("errors") {
Some(serde_json::Value::Array(arr)) => {
arr.push(value);
}
_ => {
self.extensions
.insert("errors".into(), serde_json::Value::Array(vec![value]));
}
}
self
}
pub fn errors(mut self, items: Vec<ValidationItem>) -> Self {
self.extensions.insert(
"errors".into(),
serde_json::to_value(items).expect("ValidationItem is always serializable"),
);
self
}
pub fn with_cause(mut self, err: impl std::error::Error + Send + Sync + 'static) -> Self {
self.cause = Some(InternalCause {
source: Box::new(err),
});
self
}
pub fn with_cause_str(mut self, message: impl Into<String>) -> Self {
self.cause = Some(InternalCause {
source: Box::new(StringError(message.into())),
});
self
}
}
impl Problem {
pub const ABOUT_BLANK: &'static str = "about:blank";
pub fn get_type(&self) -> &str {
self.type_uri.as_deref().unwrap_or(Self::ABOUT_BLANK)
}
pub fn status_code(&self) -> u16 {
self.status.unwrap_or(500)
}
pub fn is_server_error(&self) -> bool {
self.status_code() >= 500
}
pub fn get_code(&self) -> Option<&str> {
self.extensions.get("code").and_then(|v| v.as_str())
}
pub fn get_trace_id(&self) -> Option<&str> {
self.extensions.get("trace_id").and_then(|v| v.as_str())
}
pub fn internal_cause(&self) -> Option<&(dyn std::error::Error + Send + Sync)> {
self.cause.as_ref().map(|c| c.source.as_ref())
}
pub fn to_json_string_pretty(&self) -> String {
serde_json::to_string_pretty(self).expect("Problem is always serializable")
}
}
impl Default for Problem {
fn default() -> Self {
Self {
type_uri: None,
title: None,
status: None,
detail: None,
instance: None,
extensions: Map::new(),
cause: None,
}
}
}
impl fmt::Display for Problem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(title) = &self.title {
write!(f, "{title}")?;
} else {
write!(f, "Problem")?;
}
if let Some(status) = self.status {
write!(f, " ({status})")?;
}
if let Some(detail) = &self.detail {
write!(f, ": {detail}")?;
}
Ok(())
}
}
impl std::error::Error for Problem {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.cause
.as_ref()
.map(|c| c.source.as_ref() as &(dyn std::error::Error + 'static))
}
}
fn status_phrase(status: u16) -> Option<&'static str> {
match status {
400 => Some("Bad Request"),
401 => Some("Unauthorized"),
402 => Some("Payment Required"),
403 => Some("Forbidden"),
404 => Some("Not Found"),
405 => Some("Method Not Allowed"),
406 => Some("Not Acceptable"),
407 => Some("Proxy Authentication Required"),
408 => Some("Request Timeout"),
409 => Some("Conflict"),
410 => Some("Gone"),
411 => Some("Length Required"),
412 => Some("Precondition Failed"),
413 => Some("Content Too Large"),
414 => Some("URI Too Long"),
415 => Some("Unsupported Media Type"),
416 => Some("Range Not Satisfiable"),
417 => Some("Expectation Failed"),
418 => Some("I'm a Teapot"),
421 => Some("Misdirected Request"),
422 => Some("Unprocessable Content"),
423 => Some("Locked"),
424 => Some("Failed Dependency"),
425 => Some("Too Early"),
426 => Some("Upgrade Required"),
428 => Some("Precondition Required"),
429 => Some("Too Many Requests"),
431 => Some("Request Header Fields Too Large"),
451 => Some("Unavailable For Legal Reasons"),
500 => Some("Internal Server Error"),
501 => Some("Not Implemented"),
502 => Some("Bad Gateway"),
503 => Some("Service Unavailable"),
504 => Some("Gateway Timeout"),
505 => Some("HTTP Version Not Supported"),
506 => Some("Variant Also Negotiates"),
507 => Some("Insufficient Storage"),
508 => Some("Loop Detected"),
510 => Some("Not Extended"),
511 => Some("Network Authentication Required"),
_ => None,
}
}