axum_error_sets/lib.rs
1//! Typed, composable HTTP error sets for [axum], with OpenAPI generation through
2//! [aide](https://docs.rs/aide).
3//!
4//! Instead of one large error enum per application, each function lists the exact HTTP status
5//! codes it can return, as a tuple in its return type:
6//!
7//! ```rust
8//! # use axum_error_sets::{ApiResult, codes::{NotFound, Unauthorized}};
9//! async fn get_user() -> ApiResult<String, (Unauthorized, NotFound<String>)> {
10//! # todo!()
11//! // ...
12//! }
13//! ```
14//!
15//! Returning a status code that isn't in the set is a compile error, and with the `aide`
16//! feature every status code in the set appears in the generated OpenAPI documentation.
17//!
18//! # Building blocks
19//! - **Status codes.** Every 4xx and 5xx status code has a wrapper type in [`codes`], such as
20//! [`NotFound<T>`](codes::NotFound). The wrapped value `T` is the response body and must
21//! implement [`IntoResponse`]. It defaults to `()`, which means an empty body.
22//! - **Error sets.** [`ApiResponse<S>`] is an error response whose status code is one of the
23//! codes in the tuple `S`. [`ApiResult<T, S>`] is short for `Result<T, ApiResponse<S>>`.
24//! - **`?` conversion.** Any status code `C` converts into `ApiResponse<S>` when `S` contains
25//! `C`, so `?` works directly. The order of the codes in the tuple doesn't matter.
26//! - **Wrapping errors.** [`ResultStatusExt`] adds methods to every `Result` for giving its
27//! error a status code (`with_status`, `into_status`), changing it (`change_status`), or
28//! changing the body (`map_status`, `map_status_into`).
29//! - **Growing sets.** [`ApiResultExt::into_superset`] turns a result with a small error set
30//! into one with a larger set, so functions with narrow sets can be called from functions
31//! with wider ones.
32//!
33//! # Example
34//! ```rust
35//! use axum::Json;
36//! use axum_error_sets::{
37//! ApiResult, ApiResultExt as _, ResultStatusExt as _,
38//! codes::{Internal, NotFound, Unauthorized},
39//! };
40//!
41//! fn check_token(token: &str) -> Result<(), Unauthorized> {
42//! if token.is_empty() {
43//! return Err(Unauthorized(()));
44//! }
45//! Ok(())
46//! }
47//!
48//! fn find_user(id: u32) -> ApiResult<String, (NotFound<String>,)> {
49//! let name = lookup(id)
50//! .ok_or("no such user")
51//! // `&str` error -> `NotFound<String>`
52//! .into_status::<NotFound, String>()?;
53//! Ok(name)
54//! }
55//!
56//! async fn get_user(
57//! token: String,
58//! id: u32,
59//! ) -> ApiResult<Json<String>, (Unauthorized, NotFound<String>, Internal<String>)> {
60//! // `Unauthorized` is in the set, so `?` converts it.
61//! check_token(&token)?;
62//!
63//! // `(NotFound<String>,)` is a subset of this handler's set.
64//! let name = find_user(id).into_superset()?;
65//!
66//! // A `String` error -> `Internal<String>`
67//! let name = normalize(name).with_status::<Internal>()?;
68//!
69//! Ok(Json(name))
70//! }
71//! # fn lookup(_: u32) -> Option<String> { None }
72//! # fn normalize(name: String) -> Result<String, String> { Ok(name) }
73//! ```
74//!
75//! Returning a status code that isn't in the set doesn't compile:
76//! ```rust,compile_fail
77//! # use axum_error_sets::{ApiResult, codes::{Forbidden, NotFound}};
78//! async fn handler() -> ApiResult<(), (NotFound,)> {
79//! Err(Forbidden(()))?; // error: `(NotFound,): Contains<Forbidden>` is not satisfied
80//! Ok(())
81//! }
82//! ```
83//!
84//! More examples are in the
85//! [`examples`](https://github.com/jvdwrf/axum-error-sets/tree/main/examples) directory.
86//!
87//! # Responses
88//! When an [`ApiResponse`] is returned, the response has the status code of the wrapper it was
89//! created from, even if the body's own response sets a different status. The body's
90//! [`IntoResponse`] runs as soon as the `ApiResponse` is created, not when the handler
91//! returns. Standard axum behaves differently here, which matters if `into_response` has side
92//! effects such as logging.
93//!
94//! # OpenAPI with `aide`
95//! With the `aide` feature enabled, `ApiResponse<S>` implements [`aide::OperationOutput`]
96//! whenever every body type in `S` does. Each status code in the set is then documented as a
97//! response of the operation. This works for sets of up to 16 status codes.
98//!
99//! # Typed routing
100//! [axum-typed-routing](https://docs.rs/axum-typed-routing) is a companion crate for
101//! declaring a route's path and parameters next to its handler. With its `api_route` macro,
102//! the handler's error set shows up in the generated OpenAPI documentation automatically.
103//!
104//! # Feature flags
105//! - `aide`: implements [`aide::OperationOutput`] for [`ApiResponse`].
106
107use axum::{
108 http::StatusCode,
109 response::{IntoResponse, Response},
110};
111use type_sets::{Contains, Superset};
112
113/// Short for `Result<T, ApiResponse<S>>`.
114pub type ApiResult<T, S> = Result<T, ApiResponse<S>>;
115
116/// An error response whose status code is one of the codes in the set `S`.
117///
118/// `S` is a tuple of status codes from [`codes`], for example
119/// `ApiResponse<(NotFound<String>, Internal<Json<String>>)>`. Usually it is the error type of
120/// a handler, written as `Result<T, ApiResponse<S>>` or [`ApiResult<T, S>`].
121///
122/// It implements:
123/// - [`IntoResponse`], so it can be returned from axum handlers;
124/// - `From<C>` for every status code `C` in `S`, so `?` converts status codes into it;
125/// - `aide::OperationOutput`, when the `aide` feature is enabled and every body type in `S`
126/// implements `OperationOutput`.
127///
128/// The body's [`IntoResponse`] runs when the `ApiResponse` is created, not when the handler
129/// returns. See [Responses](crate#responses).
130///
131/// # Example
132/// ```rust
133/// # use axum_error_sets::{ApiResponse, codes::*};
134/// async fn handler() -> Result<(), ApiResponse<(NotFound<String>, BadRequest<String>)>> {
135/// Err(NotFound("no such item".to_string()).into())
136/// }
137/// ```
138pub struct ApiResponse<S> {
139 response: Response,
140 code: StatusCode,
141 _marker: std::marker::PhantomData<fn() -> S>,
142}
143
144impl<S> ApiResponse<S> {
145 /// Creates an `ApiResponse` from a status code in the set `S`.
146 ///
147 /// Usually you don't need this, because `?` and [`Into::into`] do the same.
148 pub fn new<T>(wrapper: T) -> Self
149 where
150 S: Contains<T>,
151 T: StatusProvider<Inner: IntoResponse>,
152 {
153 Self::new_unchecked(wrapper.into_inner(), T::STATUS_CODE)
154 }
155
156 /// Creates an `ApiResponse` from any response and status code, without checking that the
157 /// status code is in the set `S`.
158 pub fn new_unchecked(response: impl IntoResponse, code: StatusCode) -> Self {
159 Self {
160 response: response.into_response(),
161 code,
162 _marker: std::marker::PhantomData,
163 }
164 }
165
166 /// Splits the `ApiResponse` into the body's response and the status code.
167 pub fn into_parts(self) -> (Response, StatusCode) {
168 (self.response, self.code)
169 }
170
171 /// Converts into an `ApiResponse` with a larger set `U`, which must contain every status
172 /// code in `S`. See also [`ApiResultExt::into_superset`].
173 pub fn into_superset<U>(self) -> ApiResponse<U>
174 where
175 U: Superset<S>,
176 {
177 ApiResponse {
178 response: self.response,
179 code: self.code,
180 _marker: std::marker::PhantomData,
181 }
182 }
183}
184
185impl<S> IntoResponse for ApiResponse<S> {
186 fn into_response(self) -> Response {
187 (self.code, self.response).into_response()
188 }
189}
190
191impl<S> std::fmt::Debug for ApiResponse<S> {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 f.debug_struct("ApiResponse")
194 .field("response", &self.response)
195 .field("code", &self.code)
196 .finish()
197 }
198}
199
200macro_rules! utoipa_aide_impls {
201 ($($n:tt => ($($E:ident),*)),+ $(,)?) => {
202 $(
203 #[allow(unused)]
204 #[cfg(feature = "aide")]
205 impl<$($E),*> aide::OperationOutput for ApiResponse<($($E,)*)>
206 where
207 $(
208 $E: StatusProvider<Inner: aide::OperationOutput>,
209 )*
210 {
211 type Inner = (StatusCode, Response);
212
213 fn operation_response(
214 _ctx: &mut aide::generate::GenContext,
215 _operation: &mut aide::openapi::Operation,
216 ) -> Option<aide::openapi::Response> {
217 None
218 }
219
220 fn inferred_responses(
221 ctx: &mut aide::generate::GenContext,
222 operation: &mut aide::openapi::Operation,
223 ) -> Vec<(Option<u16>, aide::openapi::Response)> {
224 vec![
225 $((
226 Some($E::STATUS_CODE.as_u16()),
227 <$E::Inner as aide::OperationOutput>
228 ::operation_response(ctx, operation)
229 .unwrap_or_default(),
230 )),*
231 ]
232 }
233 }
234 )+
235
236 // $(
237 // #[allow(unused)]
238 // #[cfg(feature = "utoipa")]
239 // impl<$($E),*> utoipa::IntoResponses for ApiResponse<($($E,)*)>
240 // where
241 // $(
242 // $E: StatusProvider<Inner: utoipa::ToSchema>,
243 // )*
244 // {
245 // fn responses() -> std::collections::BTreeMap<
246 // String,
247 // utoipa::openapi::RefOr<utoipa::openapi::Response>,
248 // > {
249 // let mut responses = utoipa::openapi::ResponsesBuilder::new();
250
251 // $({
252 // let name = < $E::Inner as utoipa::ToSchema >::name();
253 // let schema = < $E::Inner as utoipa::PartialSchema >::schema();
254 // let content = utoipa::openapi::ContentBuilder::new()
255 // .schema(Some(schema))
256 // .build();
257
258 // responses = responses.response(
259 // $E::STATUS_CODE.as_u16().to_string(),
260 // utoipa::openapi::response::ResponseBuilder::new()
261 // .description(format!(
262 // "{} response for {}",
263 // $E::STATUS_CODE.as_u16(),
264 // name
265 // ))
266 // .content(
267 // "application/json",
268 // content
269 // )
270 // .build()
271 // );
272
273 // })*
274
275 // responses.build().responses
276 // }
277 // }
278 // )+
279 };
280}
281
282utoipa_aide_impls!(
283 0 => (),
284 1 => (E1),
285 2 => (E1, E2),
286 3 => (E1, E2, E3),
287 4 => (E1, E2, E3, E4),
288 5 => (E1, E2, E3, E4, E5),
289 6 => (E1, E2, E3, E4, E5, E6),
290 7 => (E1, E2, E3, E4, E5, E6, E7),
291 8 => (E1, E2, E3, E4, E5, E6, E7, E8),
292 9 => (E1, E2, E3, E4, E5, E6, E7, E8, E9),
293 10 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10),
294 11 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11),
295 12 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12),
296 13 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13),
297 14 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14),
298 15 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14, E15),
299 16 => (E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13, E14, E15, E16),
300);
301
302/// A wrapper type that pairs a body with a fixed HTTP status code.
303///
304/// This trait is implemented for every status code in [`codes`]. The status code is part of
305/// the type, which is what lets [`ApiResponse`] track which codes a handler can return.
306pub trait StatusProvider: From<Self::Inner> + Sized {
307 /// The status code of this wrapper.
308 const STATUS_CODE: StatusCode;
309
310 /// The type of the wrapped body.
311 type Inner;
312
313 /// The same status code with a different body type, for example `NotFound<T>` for
314 /// `NotFound<String>`.
315 type WithInner<T>: StatusProvider<Inner = T>;
316
317 /// Returns the wrapped body.
318 fn into_inner(self) -> Self::Inner;
319
320 /// Converts into an [`ApiResponse`] whose set contains this status code. This is the
321 /// same as calling `into()`.
322 fn into_set<E>(self) -> ApiResponse<E>
323 where
324 Self::Inner: IntoResponse,
325 E: Contains<Self>,
326 {
327 ApiResponse::new(self)
328 }
329
330 /// Applies `f` to the body, keeping the status code.
331 fn map<T>(self, f: impl FnOnce(Self::Inner) -> T) -> Self::WithInner<T> {
332 <Self::WithInner<T> as From<T>>::from(f(self.into_inner()))
333 }
334}
335
336/// Methods on any `Result` for giving its error an HTTP status code, and for changing the
337/// status code or body of an error that already has one.
338///
339/// Each method only changes the `Err` value. `Ok` values pass through unchanged.
340pub trait ResultStatusExt<T, E>: Sized {
341 /// Wraps the error in the status code `S`. The error becomes the body.
342 ///
343 /// # Example
344 ///
345 /// ```rust
346 /// # use axum_error_sets::{ResultStatusExt as _, codes::BadRequest};
347 /// let result: Result<(), String> = Err("error".into());
348 ///
349 /// // Has type `Result<(), BadRequest<String>>`
350 /// let _wrapped = result.with_status::<BadRequest>();
351 /// ```
352 fn with_status<S>(self) -> Result<T, S::WithInner<E>>
353 where
354 S: StatusProvider<Inner = ()>;
355
356 /// Like [`with_status`](ResultStatusExt::with_status), but first converts the error into
357 /// `E2` with [`Into::into`].
358 ///
359 /// `E2` can often be inferred, as in `into_status::<BadRequest, _>()`.
360 ///
361 /// # Example
362 ///
363 /// ```rust
364 /// # use axum_error_sets::{ResultStatusExt as _, codes::BadRequest};
365 /// let result: Result<(), &str> = Err("error");
366 ///
367 /// // Has type `Result<(), BadRequest<String>>`
368 /// let _wrapped = result.into_status::<BadRequest, String>();
369 /// ```
370 fn into_status<S, E2>(self) -> Result<T, S::WithInner<E2>>
371 where
372 S: StatusProvider<Inner = ()>,
373 E: Into<E2>;
374
375 /// Replaces the error's status code with `S`, keeping the body.
376 ///
377 /// # Example
378 ///
379 /// ```rust
380 /// # use axum_error_sets::{ResultStatusExt as _, codes::{Forbidden, NotFound}};
381 /// let result: Result<(), Forbidden<String>> = Err(Forbidden("hidden".into()));
382 ///
383 /// // Has type `Result<(), NotFound<String>>`
384 /// let _changed = result.change_status::<NotFound>();
385 /// ```
386 fn change_status<S>(self) -> Result<T, S::WithInner<E::Inner>>
387 where
388 E: StatusProvider,
389 S: StatusProvider;
390
391 /// Applies `f` to the error's body, keeping the status code.
392 ///
393 /// # Example
394 ///
395 /// ```rust
396 /// # use axum::Json;
397 /// # use axum_error_sets::{ResultStatusExt as _, codes::Internal};
398 /// let result: Result<(), Internal<String>> = Err(Internal("error".into()));
399 ///
400 /// // Has type `Result<(), Internal<Json<String>>>`
401 /// let _mapped = result.map_status(Json);
402 /// ```
403 fn map_status<F, O>(self, f: F) -> Result<T, E::WithInner<O>>
404 where
405 E: StatusProvider,
406 F: FnOnce(E::Inner) -> O;
407
408 /// Converts the error's body with [`Into::into`], keeping the status code.
409 fn map_status_into<O>(self) -> Result<T, E::WithInner<O>>
410 where
411 E: StatusProvider,
412 E::Inner: Into<O>,
413 {
414 self.map_status(Into::into)
415 }
416}
417
418impl<T, E> ResultStatusExt<T, E> for Result<T, E> {
419 fn into_status<S, E2>(self) -> Result<T, S::WithInner<E2>>
420 where
421 S: StatusProvider<Inner = ()>,
422 E: Into<E2>,
423 {
424 match self {
425 Ok(val) => Ok(val),
426 Err(e) => Err(S::WithInner::from(e.into())),
427 }
428 }
429
430 fn with_status<S>(self) -> Result<T, S::WithInner<E>>
431 where
432 S: StatusProvider<Inner = ()>,
433 {
434 match self {
435 Ok(val) => Ok(val),
436 Err(e) => Err(S::WithInner::from(e)),
437 }
438 }
439
440 fn map_status<F, O>(self, f: F) -> Result<T, E::WithInner<O>>
441 where
442 E: StatusProvider,
443 F: FnOnce(E::Inner) -> O,
444 {
445 match self {
446 Ok(val) => Ok(val),
447 Err(e) => Err(e.map(f)),
448 }
449 }
450
451 fn change_status<S>(self) -> Result<T, S::WithInner<E::Inner>>
452 where
453 E: StatusProvider,
454 S: StatusProvider,
455 {
456 match self {
457 Ok(val) => Ok(val),
458 Err(e) => Err(S::WithInner::from(e.into_inner())),
459 }
460 }
461}
462
463/// Methods on `Result<T, ApiResponse<S>>`.
464pub trait ApiResultExt<T, S> {
465 /// Converts the error into an [`ApiResponse`] with a larger set `U`, which must contain
466 /// every status code in `S`.
467 ///
468 /// # Example
469 ///
470 /// ```rust
471 /// # use axum_error_sets::{ApiResult, ApiResultExt as _, codes::{Forbidden, NotFound}};
472 /// fn inner() -> ApiResult<(), (NotFound,)> {
473 /// Ok(())
474 /// }
475 ///
476 /// fn outer() -> ApiResult<(), (Forbidden, NotFound)> {
477 /// inner().into_superset()?;
478 /// Ok(())
479 /// }
480 /// ```
481 fn into_superset<U>(self) -> Result<T, ApiResponse<U>>
482 where
483 U: Superset<S>;
484}
485
486impl<T, S> ApiResultExt<T, S> for Result<T, ApiResponse<S>> {
487 fn into_superset<U>(self) -> Result<T, ApiResponse<U>>
488 where
489 U: Superset<S>,
490 {
491 match self {
492 Ok(val) => Ok(val),
493 Err(e) => Err(e.into_superset()),
494 }
495 }
496}
497
498pub mod codes;