axum_error_sets/lib.rs
1//! Typed, composable HTTP error sets for Axum.
2//!
3//! # Overview
4//! - All status-codes are defined in [`codes`].
5//! - [`ApiResponse`] can be used as the return/error type in an `axum` handlers.
6//! - [`ApiResponse<Tuple>`] can be used to specify multiple type-safe response types for an `axum` handler.
7//! - Provides convenient methods for error propagation through [`ResultStatusExt`].
8//! - Supports automatic openapi generation through [`aide`] or [`utoipa`].
9
10use axum::{
11 http::StatusCode,
12 response::{IntoResponse, Response},
13};
14use type_sets::{Contains, Superset};
15
16/// A convenient alias for a `Result` type where the error is an [`ApiResponse`].
17pub type ApiResult<T, S> = Result<T, ApiResponse<S>>;
18
19/// A response that may be returned from an `axum` handler, with a flexible set of response types.
20///
21/// It implements
22/// - [`IntoResponse`]
23/// - Optionally [`aide::OperationOutput`] when the `aide` feature is enabled, and all responses implement `OperationOutput` as well.
24/// - Optionally [`utoipa::ToResponse`] when the `utoipa` feature is enabled, and all responses implement `ToResponse` as well.
25///
26/// Simply specify the set of response types that this handler may return as a tuple in the `ApiResponse` type:
27/// - `ApiResponse<(BadRequest<String>,)>`
28/// - `ApiResponse<(BadRequest<String>, Internal<String>)>`
29/// - `ApiResponse<(BadRequest<String>, Internal<String>, Forbidden<String>)>`
30///
31/// `ApiResponse<S>` implements `From<T>` for any `T` [contained](`type_sets::Contains`)
32/// in the set `S`, allowing for easy error propagation.
33/// See [`ResultStatusExt`] for convenient methods to help with error propagation.
34///
35/// Be aware that [`IntoResponse`] is called whenever this type is constructed, **not**
36/// when it is returned by the handler. This behaviour differs from standard `axum`.
37///
38/// Normally, this type is used as `Result<T, ApiResponse<S>>` (or the type-alias [`ApiResult`]). It can also be used itself as a direct return type from an `axum` handler.
39///
40/// # Examples
41/// ```rust
42/// # use axum_error_sets::{ApiResponse, codes::*};
43/// // An very basic axum-handler. See the `examples` directory for more complete examples.
44/// async fn handler() -> Result<(), ApiResponse<(NotFound<String>, BadRequest<String>)> { todo!() }
45/// ```
46pub struct ApiResponse<S> {
47 response: Response,
48 code: StatusCode,
49 _marker: std::marker::PhantomData<fn() -> S>,
50}
51
52impl<S> ApiResponse<S> {
53 /// Create a new `ApiResponse` from a status wrapper.
54 pub fn new<T>(wrapper: T) -> Self
55 where
56 S: Contains<T>,
57 T: StatusProvider<Inner: IntoResponse>,
58 {
59 Self::new_unchecked(wrapper.into_inner(), T::STATUS_CODE)
60 }
61
62 /// Create a new `ApiResponse` from a raw response and status code, without checking if it is valid
63 pub fn new_unchecked(response: impl IntoResponse, code: StatusCode) -> Self {
64 Self {
65 response: response.into_response(),
66 code,
67 _marker: std::marker::PhantomData,
68 }
69 }
70
71 /// Decompose the `ApiResponse` into its raw response and status code.
72 pub fn into_parts(self) -> (Response, StatusCode) {
73 (self.response, self.code)
74 }
75
76 /// Convert this `ApiResponse` into a new `ApiResponse` with a superset of the original error types.
77 pub fn into_superset<U>(self) -> ApiResponse<U>
78 where
79 U: Superset<S>,
80 {
81 ApiResponse {
82 response: self.response,
83 code: self.code,
84 _marker: std::marker::PhantomData,
85 }
86 }
87}
88
89impl<S> IntoResponse for ApiResponse<S> {
90 fn into_response(self) -> Response {
91 (self.code, self.response).into_response()
92 }
93}
94
95impl<S> std::fmt::Debug for ApiResponse<S> {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 f.debug_struct("ApiResponse")
98 .field("response", &self.response)
99 .field("code", &self.code)
100 .finish()
101 }
102}
103
104macro_rules! utoipa_aide_impls {
105 ($($n:tt => ($($E:ident),*)),+ $(,)?) => {
106 $(
107 #[allow(unused)]
108 #[cfg(feature = "aide")]
109 impl<$($E),*> aide::OperationOutput for ApiResponse<($($E,)*)>
110 where
111 $(
112 $E: StatusProvider<Inner: aide::OperationOutput>,
113 )*
114 {
115 type Inner = (StatusCode, Response);
116
117 fn operation_response(
118 _ctx: &mut aide::generate::GenContext,
119 _operation: &mut aide::openapi::Operation,
120 ) -> Option<aide::openapi::Response> {
121 None
122 }
123
124 fn inferred_responses(
125 ctx: &mut aide::generate::GenContext,
126 operation: &mut aide::openapi::Operation,
127 ) -> Vec<(Option<u16>, aide::openapi::Response)> {
128 vec![
129 $((
130 Some($E::STATUS_CODE.as_u16()),
131 <$E::Inner as aide::OperationOutput>
132 ::operation_response(ctx, operation)
133 .unwrap_or_default(),
134 )),*
135 ]
136 }
137 }
138 )+
139
140 // $(
141 // #[allow(unused)]
142 // #[cfg(feature = "utoipa")]
143 // impl<$($E),*> utoipa::IntoResponses for ApiResponse<($($E,)*)>
144 // where
145 // $(
146 // $E: StatusProvider<Inner: utoipa::ToSchema>,
147 // )*
148 // {
149 // fn responses() -> std::collections::BTreeMap<
150 // String,
151 // utoipa::openapi::RefOr<utoipa::openapi::Response>,
152 // > {
153 // let mut responses = utoipa::openapi::ResponsesBuilder::new();
154
155 // $({
156 // let name = < $E::Inner as utoipa::ToSchema >::name();
157 // let schema = < $E::Inner as utoipa::PartialSchema >::schema();
158 // let content = utoipa::openapi::ContentBuilder::new()
159 // .schema(Some(schema))
160 // .build();
161
162 // responses = responses.response(
163 // $E::STATUS_CODE.as_u16().to_string(),
164 // utoipa::openapi::response::ResponseBuilder::new()
165 // .description(format!(
166 // "{} response for {}",
167 // $E::STATUS_CODE.as_u16(),
168 // name
169 // ))
170 // .content(
171 // "application/json",
172 // content
173 // )
174 // .build()
175 // );
176
177 // })*
178
179 // responses.build().responses
180 // }
181 // }
182 // )+
183 };
184}
185
186utoipa_aide_impls!(
187 0 => (),
188 1 => (E1),
189 2 => (E1, E2),
190 3 => (E1, E2, E3),
191 4 => (E1, E2, E3, E4),
192 5 => (E1, E2, E3, E4, E5),
193 6 => (E1, E2, E3, E4, E5, E6),
194 7 => (E1, E2, E3, E4, E5, E6, E7),
195 8 => (E1, E2, E3, E4, E5, E6, E7, E8),
196 9 => (E1, E2, E3, E4, E5, E6, E7, E8, E9),
197 10 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10),
198 11 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11),
199 12 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12),
200 13 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13),
201 14 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14),
202 15 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14, E15),
203 16 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14, E15, E16),
204);
205
206/// A [`StatusCode`] that wraps an inner value, and can thus be used for easy error
207/// propagation while keeping track of the status-code in the type-system.
208///
209/// This trait is implemented for all codes in the [`codes`] module.
210pub trait StatusProvider: From<Self::Inner> + Sized {
211 /// The status code associated with type.
212 const STATUS_CODE: StatusCode;
213
214 /// The inner value type that is wrapped by this status wrapper.
215 type Inner;
216
217 type WithInner<T>: StatusProvider<Inner = T>;
218
219 /// Convert this status wrapper into its inner value.
220 fn into_inner(self) -> Self::Inner;
221
222 /// Convert this status wrapper into an [`ApiErrorSet`] with the
223 /// given inner value type. (`into` can be used as well)
224 fn into_set<E>(self) -> ApiResponse<E>
225 where
226 Self::Inner: IntoResponse,
227 E: Contains<Self>,
228 {
229 ApiResponse::new(self)
230 }
231
232 /// Apply a function to the inner value of this status wrapper,
233 /// producing a new wrapper with the transformed inner value.
234 fn map<T>(self, f: impl FnOnce(Self::Inner) -> T) -> Self::WithInner<T> {
235 <Self::WithInner<T> as From<T>>::from(f(self.into_inner()))
236 }
237}
238
239/// Extension trait for `Result` that provides methods to wrap errors with specific HTTP status codes.
240pub trait ResultStatusExt<T, E>: Sized {
241 /// Wraps the error `E` with the gives status `S`.
242 ///
243 /// # Example
244 ///
245 /// ```rust
246 /// # use axum_error_sets::{ApiResultExt as _, codes::BadRequest};
247 ///
248 /// let result: Result<(), String> = Err("error".into());
249 ///
250 /// // Has type `Result<(), BadRequest<String>>``
251 /// let _wrapped = result.with_status::<BadRequest>();
252 /// ```
253 fn with_status<S>(self) -> Result<T, S::WithInner<E>>
254 where
255 S: StatusProvider<Inner = ()>;
256
257 /// Wraps the error `E` with the gives status `S`, while converting the inner error-type using [`Into::into`] to `E2`.
258 ///
259 /// Usually the second generic can be inferred by the compiler.
260 ///
261 /// # Example
262 ///
263 /// ```rust
264 /// # use axum_error_sets::{ApiResultExt as _, codes::BadRequest};
265 ///
266 /// let result: Result<(), &str> = Err("error");
267 ///
268 /// // Has type `Result<(), BadRequest<String>>`
269 /// let _wrapped = result.into_status::<BadRequest, String>();
270 /// ```
271 fn into_status<S, E2>(self) -> Result<T, S::WithInner<E2>>
272 where
273 S: StatusProvider<Inner = ()>,
274 E: Into<E2>;
275
276 /// Changes the status of the error to the given [`StatusProvider`] `S`, preserving the inner error.
277 fn change_status<S>(self) -> Result<T, S::WithInner<E::Inner>>
278 where
279 E: StatusProvider,
280 S: StatusProvider;
281
282 /// Maps the inner state of the given [`StatusProvider`] using the provided function `f`.
283 fn map_status<F, O>(self, f: F) -> Result<T, E::WithInner<O>>
284 where
285 E: StatusProvider,
286 F: FnOnce(E::Inner) -> O;
287
288 /// Maps the inner error of the given [`StatusProvider`] using the [`Into::into`] conversion.
289 fn map_status_into<O>(self) -> Result<T, E::WithInner<O>>
290 where
291 E: StatusProvider,
292 E::Inner: Into<O>,
293 {
294 self.map_status(Into::into)
295 }
296}
297
298impl<T, E> ResultStatusExt<T, E> for Result<T, E> {
299 fn into_status<S, E2>(self) -> Result<T, S::WithInner<E2>>
300 where
301 S: StatusProvider<Inner = ()>,
302 E: Into<E2>,
303 {
304 match self {
305 Ok(val) => Ok(val),
306 Err(e) => Err(S::WithInner::from(e.into())),
307 }
308 }
309
310 fn with_status<S>(self) -> Result<T, S::WithInner<E>>
311 where
312 S: StatusProvider<Inner = ()>,
313 {
314 match self {
315 Ok(val) => Ok(val),
316 Err(e) => Err(S::WithInner::from(e)),
317 }
318 }
319
320 fn map_status<F, O>(self, f: F) -> Result<T, E::WithInner<O>>
321 where
322 E: StatusProvider,
323 F: FnOnce(E::Inner) -> O,
324 {
325 match self {
326 Ok(val) => Ok(val),
327 Err(e) => Err(e.map(f)),
328 }
329 }
330
331 fn change_status<S>(self) -> Result<T, S::WithInner<E::Inner>>
332 where
333 E: StatusProvider,
334 S: StatusProvider,
335 {
336 match self {
337 Ok(val) => Ok(val),
338 Err(e) => Err(S::WithInner::from(e.into_inner())),
339 }
340 }
341}
342
343/// Extension trait for `Result` types with `ApiResponse` errors, providing a method to convert the error type into a superset error type.
344pub trait ApiResultExt<T, S> {
345 /// Converts the error type of the `Result` into a superset error type.
346 fn into_superset<U>(self) -> Result<T, ApiResponse<U>>
347 where
348 U: Superset<S>;
349}
350
351impl<T, S> ApiResultExt<T, S> for Result<T, ApiResponse<S>> {
352 fn into_superset<U>(self) -> Result<T, ApiResponse<U>>
353 where
354 U: Superset<S>,
355 {
356 match self {
357 Ok(val) => Ok(val),
358 Err(e) => Err(e.into_superset()),
359 }
360 }
361}
362
363pub mod codes;