http_problem/custom.rs
1pub use http::{StatusCode, Uri};
2
3use super::{CowStr, Extensions, Problem};
4
5/// Define a new custom problem type.
6///
7/// Although defining a new custom type requires only implementing
8/// the [`CustomProblem`] trait, this macro simplifies the code,
9/// removing boilerplate from the definition.
10///
11/// # Example
12///
13/// ```
14/// use http_problem::prelude::{StatusCode, Uri};
15/// problem::define_custom_type! {
16/// /// An error that occurs when a transaction cannot be done
17/// /// because one of the accounts doesn't have enough credit.
18/// type OutOfCredit {
19/// type: "https://example.com/probs/out-of-credit",
20/// title: "You do not have enough credit",
21/// status: StatusCode::FORBIDDEN,
22/// detail(p): format!("You current balance is {}, but that costs {}", p.balance, p.cost),
23/// extensions: {
24/// balance: i64,
25/// cost: i64,
26/// accounts: Vec<String>
27/// }
28/// }
29/// }
30///
31/// fn do_transaction() -> problem::Result<()> {
32/// Err(OutOfCredit {
33/// balance: 50,
34/// cost: 30,
35/// accounts: vec!["/account/12345".into(), "/account/67890".into()]
36/// }.into())
37/// }
38///
39/// fn main() {
40/// let problem = do_transaction().unwrap_err();
41/// assert_eq!(problem.type_(), &Uri::from_static("https://example.com/probs/out-of-credit"));
42/// assert_eq!(problem.title(), "You do not have enough credit");
43/// assert_eq!(problem.status(), StatusCode::FORBIDDEN);
44/// assert_eq!(problem.details(), "You current balance is 50, but that costs 30");
45/// assert_eq!(problem.extensions().len(), 3);
46/// }
47/// ```
48#[macro_export]
49macro_rules! define_custom_type {
50 ($(#[$meta: meta])* type $rstyp: ident {
51 type: $typ:literal,
52 title: $title:literal,
53 status: $status: expr,
54 detail($prob: ident): $detail: expr,
55 extensions: {
56 $($field:ident: $field_ty: ty),* $(,)?
57 } $(,)?
58 }) => {
59 $(#[$meta])*
60 #[derive(Debug)]
61 pub struct $rstyp {
62 $(pub $field: $field_ty),*
63 }
64
65 impl ::std::fmt::Display for $rstyp {
66 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
67 writeln!(f, "{}", <Self as $crate::prelude::CustomProblem>::details(self))
68 }
69 }
70
71 impl ::std::error::Error for $rstyp {}
72
73 impl $crate::prelude::CustomProblem for $rstyp {
74 fn problem_type(&self) -> $crate::prelude::Uri {
75 $crate::prelude::Uri::from_static($typ)
76 }
77
78 fn title(&self) -> &'static str {
79 $title
80 }
81
82 fn status_code(&self) -> $crate::prelude::StatusCode {
83 $status
84 }
85
86 fn details(&self) -> $crate::CowStr {
87 let $prob = self;
88 $detail.into()
89 }
90
91 fn add_extensions(
92 &self,
93 _extensions: &mut $crate::Extensions)
94 {
95 $(
96 _extensions.insert(stringify!($field), &self.$field);
97 )*
98 }
99 }
100 };
101}
102
103/// A trait defining custom problem types.
104///
105/// Implementing this trait provides enough information to create a
106/// [`Problem`] instance with the correct values for each field.
107///
108/// There is no need to implement `From<Self> for Problem` if you
109/// implement this trait.
110///
111/// See [`define_custom_type!`] for a convenient way of implementing
112/// this trait.
113pub trait CustomProblem: std::error::Error + Send + Sync + 'static {
114 /// A URI reference that identifies the problem type.
115 ///
116 /// See [`Problem::type_`] more information.
117 fn problem_type(&self) -> Uri;
118
119 /// A short, human-readable summary of the problem type.
120 ///
121 /// See [`Problem::title`] for more information.
122 fn title(&self) -> &'static str;
123
124 /// The HTTP status code for this problem type.
125 ///
126 /// See [`Problem::status`] for more information.
127 fn status_code(&self) -> StatusCode;
128
129 /// A human-readable explanation of the occurrence.
130 ///
131 /// See [`Problem::details`] for more information.
132 fn details(&self) -> CowStr;
133
134 /// Add extensions to the final problem instance.
135 ///
136 /// See [`Problem::with_extension`] for more info.
137 fn add_extensions(&self, extensions: &mut Extensions);
138}
139
140impl<C: CustomProblem> From<C> for Problem {
141 #[track_caller]
142 fn from(custom: C) -> Self {
143 let mut problem = Self::custom(custom.status_code(), custom.problem_type())
144 .with_title(custom.title())
145 .with_detail(custom.details());
146
147 custom.add_extensions(problem.extensions_mut());
148
149 problem.with_cause(custom)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 define_custom_type! {
158 /// An error that occurs when a transaction cannot be done
159 /// because one of the accounts doesn't have enough credit.
160 type OutOfCredit {
161 type: "https://example.com/probs/out-of-credit",
162 title: "You do not have enough credit",
163 status: StatusCode::FORBIDDEN,
164 detail(p): format!("You current balance is {}, but that costs {}", p.balance, p.cost),
165 extensions: {
166 balance: i64,
167 cost: i64,
168 accounts: Vec<String>
169 }
170 }
171 }
172
173 #[test]
174 fn test_macro_output() {
175 let error = OutOfCredit {
176 balance: 30,
177 cost: 50,
178 accounts: vec!["aaa".into(), "bbb".into()],
179 };
180
181 assert_eq!(error.title(), "You do not have enough credit");
182 assert_eq!(error.status_code(), StatusCode::FORBIDDEN);
183 assert_eq!(
184 error.details(),
185 "You current balance is 30, but that costs 50"
186 );
187 }
188
189 #[test]
190 fn test_custom_problem_to_problem() {
191 let error = OutOfCredit {
192 balance: 30,
193 cost: 50,
194 accounts: vec!["aaa".into(), "bbb".into()],
195 };
196
197 let prob: Problem = error.into();
198
199 assert_eq!(prob.title(), "You do not have enough credit");
200 assert_eq!(prob.status(), StatusCode::FORBIDDEN);
201 assert_eq!(
202 prob.details(),
203 "You current balance is 30, but that costs 50"
204 );
205 }
206}