Skip to main content

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;