pub use http::{StatusCode, Uri};
use super::{CowStr, Extensions, Problem};
#[macro_export]
macro_rules! define_custom_type {
($(#[$meta: meta])* type $rstyp: ident {
type: $typ:literal,
title: $title:literal,
status: $status: expr,
detail($prob: ident): $detail: expr,
extensions: {
$($field:ident: $field_ty: ty),* $(,)?
} $(,)?
}) => {
$(#[$meta])*
#[derive(Debug)]
pub struct $rstyp {
$(pub $field: $field_ty),*
}
impl ::std::fmt::Display for $rstyp {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
writeln!(f, "{}", <Self as $crate::prelude::CustomProblem>::details(self))
}
}
impl ::std::error::Error for $rstyp {}
impl $crate::prelude::CustomProblem for $rstyp {
fn problem_type(&self) -> $crate::prelude::Uri {
$crate::prelude::Uri::from_static($typ)
}
fn title(&self) -> &'static str {
$title
}
fn status_code(&self) -> $crate::prelude::StatusCode {
$status
}
fn details(&self) -> $crate::CowStr {
let $prob = self;
$detail.into()
}
fn add_extensions(
&self,
_extensions: &mut $crate::Extensions)
{
$(
_extensions.insert(stringify!($field), &self.$field);
)*
}
}
};
}
pub trait CustomProblem: std::error::Error + Send + Sync + 'static {
fn problem_type(&self) -> Uri;
fn title(&self) -> &'static str;
fn status_code(&self) -> StatusCode;
fn details(&self) -> CowStr;
fn add_extensions(&self, extensions: &mut Extensions);
}
impl<C: CustomProblem> From<C> for Problem {
#[track_caller]
fn from(custom: C) -> Self {
let mut problem = Self::custom(custom.status_code(), custom.problem_type())
.with_title(custom.title())
.with_detail(custom.details());
custom.add_extensions(problem.extensions_mut());
problem.with_cause(custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
define_custom_type! {
type OutOfCredit {
type: "https://example.com/probs/out-of-credit",
title: "You do not have enough credit",
status: StatusCode::FORBIDDEN,
detail(p): format!("You current balance is {}, but that costs {}", p.balance, p.cost),
extensions: {
balance: i64,
cost: i64,
accounts: Vec<String>
}
}
}
#[test]
fn test_macro_output() {
let error = OutOfCredit {
balance: 30,
cost: 50,
accounts: vec!["aaa".into(), "bbb".into()],
};
assert_eq!(error.title(), "You do not have enough credit");
assert_eq!(error.status_code(), StatusCode::FORBIDDEN);
assert_eq!(
error.details(),
"You current balance is 30, but that costs 50"
);
}
#[test]
fn test_custom_problem_to_problem() {
let error = OutOfCredit {
balance: 30,
cost: 50,
accounts: vec!["aaa".into(), "bbb".into()],
};
let prob: Problem = error.into();
assert_eq!(prob.title(), "You do not have enough credit");
assert_eq!(prob.status(), StatusCode::FORBIDDEN);
assert_eq!(
prob.details(),
"You current balance is 30, but that costs 50"
);
}
}