use std::{error::Error, fmt};
use crate::{
custom::{CustomProblem, StatusCode, Uri},
CowStr, Extensions, Problem,
};
#[track_caller]
pub fn bad_request(msg: impl Into<CowStr>) -> Problem {
Problem::from(BadRequest { msg: msg.into() })
}
#[track_caller]
pub fn unauthorized() -> Problem {
Problem::from(Unauthorized { _inner: () })
}
#[track_caller]
pub fn forbidden() -> Problem {
Problem::from(Forbidden { _inner: () })
}
#[track_caller]
pub fn not_found(entity: &'static str, identifier: impl fmt::Display) -> Problem {
Problem::from(NotFound {
entity,
identifier: identifier.to_string(),
})
}
#[track_caller]
pub fn not_found_unknown(entity: &'static str) -> Problem {
not_found(entity, "<unknown>")
}
#[track_caller]
pub fn conflict(msg: impl Into<CowStr>) -> Problem {
Problem::from(Conflict { msg: msg.into() })
}
#[track_caller]
pub fn failed_precondition() -> Problem {
Problem::from(PreconditionFailed { _inner: () })
}
#[track_caller]
pub fn unprocessable(msg: impl Into<CowStr>) -> Problem {
Problem::from(UnprocessableEntity { msg: msg.into() })
}
#[track_caller]
pub fn internal_error<M>(msg: M) -> Problem
where
M: Error + Send + Sync + 'static,
{
Problem::from_status(StatusCode::INTERNAL_SERVER_ERROR)
.with_detail("An unexpected error occurred")
.with_cause(InternalError {
inner: Box::new(msg),
})
}
#[track_caller]
pub fn service_unavailable() -> Problem {
Problem::from(ServiceUnavailable { _inner: () })
}
macro_rules! http_problem_type {
($status: ident) => {
fn problem_type(&self) -> Uri {
crate::blank_type_uri()
}
fn title(&self) -> &'static str {
self.status_code().canonical_reason().unwrap()
}
fn status_code(&self) -> StatusCode {
StatusCode::$status
}
};
($status: ident, display) => {
http_problem_type!($status);
fn details(&self) -> CowStr {
self.to_string().into()
}
};
($status: ident, details: $detail: expr) => {
http_problem_type!($status);
fn details(&self) -> CowStr {
$detail.into()
}
};
($status: ident, details <- $detail: ident) => {
http_problem_type!($status);
fn details(&self) -> CowStr {
self.$detail.to_string().into()
}
};
}
macro_rules! zst_problem_type {
($(#[$doc:meta])+ $name:ident, $status_code:ident, $details:literal) => {
$(#[$doc])+
#[derive(Debug, Copy, Clone)]
pub struct $name {
_inner: (),
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(stringify!($name))
}
}
impl Error for $name {}
impl CustomProblem for $name {
http_problem_type!(
$status_code,
details: $details
);
fn add_extensions(&self, _: &mut Extensions) {}
}
}
}
#[derive(Debug)]
pub struct BadRequest {
msg: CowStr,
}
impl fmt::Display for BadRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.msg)
}
}
impl Error for BadRequest {}
impl CustomProblem for BadRequest {
http_problem_type!(BAD_REQUEST, details <- msg);
fn add_extensions(&self, _: &mut Extensions) {}
}
zst_problem_type!(
Unauthorized,
UNAUTHORIZED,
"You don't have the necessary permissions"
);
zst_problem_type!(
Forbidden,
FORBIDDEN,
"You don't have the necessary permissions"
);
zst_problem_type!(
PreconditionFailed,
PRECONDITION_FAILED,
"Some request precondition could not be satisfied."
);
#[derive(Debug)]
pub struct NotFound {
identifier: String,
entity: &'static str,
}
impl NotFound {
pub const fn entity(&self) -> &'static str {
self.entity
}
}
impl fmt::Display for NotFound {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"The {} identified by '{}' wasn't found",
self.entity, self.identifier
)
}
}
impl Error for NotFound {}
impl CustomProblem for NotFound {
http_problem_type!(NOT_FOUND, display);
fn add_extensions(&self, extensions: &mut Extensions) {
extensions.insert("identifier", &self.identifier);
extensions.insert("entity", self.entity);
}
}
#[derive(Debug, Clone)]
pub struct Conflict {
msg: CowStr,
}
impl fmt::Display for Conflict {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Conflict: {}", self.msg)
}
}
impl Error for Conflict {}
impl CustomProblem for Conflict {
http_problem_type!(CONFLICT, details <- msg);
fn add_extensions(&self, _: &mut Extensions) {}
}
#[derive(Debug)]
pub struct UnprocessableEntity {
msg: CowStr,
}
impl fmt::Display for UnprocessableEntity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Unprocessable Entity: {}", self.msg)
}
}
impl Error for UnprocessableEntity {}
impl CustomProblem for UnprocessableEntity {
http_problem_type!(UNPROCESSABLE_ENTITY, details <- msg);
fn add_extensions(&self, _: &mut Extensions) {}
}
#[derive(Debug)]
pub struct InternalError {
inner: Box<dyn Error + Send + Sync + 'static>,
}
impl fmt::Display for InternalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.fmt(f)
}
}
impl Error for InternalError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&*self.inner)
}
}
zst_problem_type!(
ServiceUnavailable,
SERVICE_UNAVAILABLE,
"The service is currently unavailable, try again after an exponential backoff"
);
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test_constructor {
($test_fn: ident, $constructor: ident, $ty: ty $(,$arg: expr)*) => {
#[test]
fn $test_fn() {
let prd = $constructor($($arg),*);
assert!(prd.is::<$ty>());
}
};
}
test_constructor!(test_bad_request, bad_request, BadRequest, "bla");
test_constructor!(test_unauthorized, unauthorized, Unauthorized);
test_constructor!(test_forbidden, forbidden, Forbidden);
test_constructor!(test_not_found, not_found, NotFound, "bla", "foo");
test_constructor!(test_conflict, conflict, Conflict, "bla");
test_constructor!(
test_failed_precondition,
failed_precondition,
PreconditionFailed
);
test_constructor!(
test_unprocessable,
unprocessable,
UnprocessableEntity,
"bla"
);
test_constructor!(
test_internal_error,
internal_error,
InternalError,
std::io::Error::new(std::io::ErrorKind::Other, "bla")
);
test_constructor!(
test_service_unavailable,
service_unavailable,
ServiceUnavailable
);
}