utoipa_axum/
router.rs

1//! Implements Router for composing handlers and collecting OpenAPI information.
2use std::convert::Infallible;
3
4use axum::extract::Request;
5use axum::handler::Handler;
6use axum::response::IntoResponse;
7use axum::routing::{MethodRouter, Route, RouterAsService};
8use axum::Router;
9use tower_layer::Layer;
10use tower_service::Service;
11
12/// Wrapper type for [`utoipa::openapi::path::Paths`] and [`axum::routing::MethodRouter`].
13///
14/// This is used with [`OpenApiRouter::routes`] method to register current _`paths`_ to the
15/// [`utoipa::openapi::OpenApi`] of [`OpenApiRouter`] instance.
16///
17/// See [`routes`][routes] for usage.
18///
19/// [routes]: ../macro.routes.html
20pub type UtoipaMethodRouter<S = (), E = Infallible> = (
21    Vec<(
22        String,
23        utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
24    )>,
25    utoipa::openapi::path::Paths,
26    axum::routing::MethodRouter<S, E>,
27);
28
29/// Extension trait for [`UtoipaMethodRouter`] to expose typically used methods of
30/// [`axum::routing::MethodRouter`] and to extend [`UtoipaMethodRouter`] with useful convenience
31/// methods.
32pub trait UtoipaMethodRouterExt<S, E>
33where
34    S: Send + Sync + Clone + 'static,
35{
36    /// Pass through method for [`axum::routing::MethodRouter::layer`].
37    ///
38    /// This method is provided as convenience for defining layers to [`axum::routing::MethodRouter`]
39    /// routes.
40    fn layer<L, NewError>(self, layer: L) -> UtoipaMethodRouter<S, NewError>
41    where
42        L: Layer<Route<E>> + Clone + Send + Sync + 'static,
43        L::Service: Service<Request> + Clone + Send + Sync + 'static,
44        <L::Service as Service<Request>>::Response: IntoResponse + 'static,
45        <L::Service as Service<Request>>::Error: Into<NewError> + 'static,
46        <L::Service as Service<Request>>::Future: Send + 'static,
47        E: 'static,
48        S: 'static,
49        NewError: 'static;
50
51    /// Pass through method for [`axum::routing::MethodRouter::with_state`].
52    ///
53    /// Allows quick state definition for underlying [`axum::routing::MethodRouter`].
54    fn with_state<S2>(self, state: S) -> UtoipaMethodRouter<S2, E>;
55
56    /// Convenience method that allows custom mapping for [`axum::routing::MethodRouter`] via
57    /// methods that not exposed directly through [`UtoipaMethodRouterExt`].
58    ///
59    /// This method could be used to add layers, route layers or fallback handlers for the method
60    /// router.
61    /// ```rust
62    /// # use utoipa_axum::{routes, router::{UtoipaMethodRouter, UtoipaMethodRouterExt}};
63    /// # #[utoipa::path(get, path = "")]
64    /// # async fn search_user() {}
65    /// let _: UtoipaMethodRouter = routes!(search_user).map(|method_router| {
66    ///     // .. implementation here
67    ///     method_router
68    /// });
69    /// ```
70    fn map<NewError>(
71        self,
72        op: impl FnOnce(MethodRouter<S, E>) -> MethodRouter<S, NewError>,
73    ) -> UtoipaMethodRouter<S, NewError>;
74}
75
76impl<S, E> UtoipaMethodRouterExt<S, E> for UtoipaMethodRouter<S, E>
77where
78    S: Send + Sync + Clone + 'static,
79{
80    fn layer<L, NewError>(self, layer: L) -> UtoipaMethodRouter<S, NewError>
81    where
82        L: Layer<Route<E>> + Clone + Send + Sync + 'static,
83        L::Service: Service<Request> + Clone + Send + Sync + 'static,
84        <L::Service as Service<Request>>::Response: IntoResponse + 'static,
85        <L::Service as Service<Request>>::Error: Into<NewError> + 'static,
86        <L::Service as Service<Request>>::Future: Send + 'static,
87        E: 'static,
88        S: 'static,
89        NewError: 'static,
90    {
91        (self.0, self.1, self.2.layer(layer))
92    }
93
94    fn with_state<S2>(self, state: S) -> UtoipaMethodRouter<S2, E> {
95        (self.0, self.1, self.2.with_state(state))
96    }
97
98    fn map<NewError>(
99        self,
100        op: impl FnOnce(MethodRouter<S, E>) -> MethodRouter<S, NewError>,
101    ) -> UtoipaMethodRouter<S, NewError> {
102        (self.0, self.1, op(self.2))
103    }
104}
105
106/// A wrapper struct for [`axum::Router`] and [`utoipa::openapi::OpenApi`] for composing handlers
107/// and services with collecting OpenAPI information from the handlers.
108///
109/// This struct provides pass through implementation for most of the [`axum::Router`] methods and
110/// extends capabilities for few to collect the OpenAPI information. Methods that are not
111/// implemented can be easily called after converting this router to [`axum::Router`] by
112/// [`Into::into`].
113///
114/// # Examples
115///
116/// _**Create new [`OpenApiRouter`] with default values populated from cargo environment variables.**_
117/// ```rust
118/// # use utoipa_axum::router::OpenApiRouter;
119/// let _: OpenApiRouter = OpenApiRouter::new();
120/// ```
121///
122/// _**Instantiate a new [`OpenApiRouter`] with new empty [`utoipa::openapi::OpenApi`].**_
123/// ```rust
124/// # use utoipa_axum::router::OpenApiRouter;
125/// let _: OpenApiRouter = OpenApiRouter::default();
126/// ```
127#[derive(Clone)]
128#[cfg_attr(feature = "debug", derive(Debug))]
129pub struct OpenApiRouter<S = ()>(Router<S>, utoipa::openapi::OpenApi);
130
131impl<S> OpenApiRouter<S>
132where
133    S: Send + Sync + Clone + 'static,
134{
135    /// Instantiate a new [`OpenApiRouter`] with default values populated from cargo environment
136    /// variables. This creates an `OpenApi` similar of creating a new `OpenApi` via
137    /// `#[derive(OpenApi)]`
138    ///
139    /// If you want to create [`OpenApiRouter`] with completely empty [`utoipa::openapi::OpenApi`]
140    /// instance, use [`OpenApiRouter::default()`].
141    pub fn new() -> OpenApiRouter<S> {
142        use utoipa::OpenApi;
143        #[derive(OpenApi)]
144        struct Api;
145
146        Self::with_openapi(Api::openapi())
147    }
148
149    /// Instantiates a new [`OpenApiRouter`] with given _`openapi`_ instance.
150    ///
151    /// This function allows using existing [`utoipa::openapi::OpenApi`] as source for this router.
152    ///
153    /// # Examples
154    ///
155    /// _**Use derived [`utoipa::openapi::OpenApi`] as source for [`OpenApiRouter`].**_
156    /// ```rust
157    /// # use utoipa::OpenApi;
158    /// # use utoipa_axum::router::OpenApiRouter;
159    /// #[derive(utoipa::ToSchema)]
160    /// struct Todo {
161    ///     id: i32,
162    /// }
163    /// #[derive(utoipa::OpenApi)]
164    /// #[openapi(components(schemas(Todo)))]
165    /// struct Api;
166    ///
167    /// let mut router: OpenApiRouter = OpenApiRouter::with_openapi(Api::openapi());
168    /// ```
169    pub fn with_openapi(openapi: utoipa::openapi::OpenApi) -> Self {
170        Self(Router::new(), openapi)
171    }
172
173    /// Pass through method for [`axum::Router::as_service`].
174    pub fn as_service<B>(&mut self) -> RouterAsService<'_, B, S> {
175        self.0.as_service()
176    }
177
178    /// Pass through method for [`axum::Router::fallback`].
179    pub fn fallback<H, T>(self, handler: H) -> Self
180    where
181        H: Handler<T, S>,
182        T: 'static,
183    {
184        Self(self.0.fallback(handler), self.1)
185    }
186
187    /// Pass through method for [`axum::Router::fallback_service`].
188    pub fn fallback_service<T>(self, service: T) -> Self
189    where
190        T: Service<Request, Error = Infallible> + Clone + Send + Sync + 'static,
191        T::Response: IntoResponse,
192        T::Future: Send + 'static,
193    {
194        Self(self.0.fallback_service(service), self.1)
195    }
196
197    /// Pass through method for [`axum::Router::layer`].
198    pub fn layer<L>(self, layer: L) -> Self
199    where
200        L: Layer<Route> + Clone + Send + Sync + 'static,
201        L::Service: Service<Request> + Clone + Send + Sync + 'static,
202        <L::Service as Service<Request>>::Response: IntoResponse + 'static,
203        <L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
204        <L::Service as Service<Request>>::Future: Send + 'static,
205    {
206        Self(self.0.layer(layer), self.1)
207    }
208
209    /// Register [`UtoipaMethodRouter`] content created with [`routes`][routes] macro to `self`.
210    ///
211    /// Paths of the [`UtoipaMethodRouter`] will be extended to [`utoipa::openapi::OpenApi`] and
212    /// [`axum::routing::MethodRouter`] will be added to the [`axum::Router`].
213    ///
214    /// [routes]: ../macro.routes.html
215    pub fn routes(mut self, (schemas, mut paths, method_router): UtoipaMethodRouter<S>) -> Self {
216        let router = if paths.paths.len() == 1 {
217            let first_entry = &paths.paths.first_entry();
218            let path = first_entry.as_ref().map(|path| path.key());
219            let Some(path) = path else {
220                unreachable!("Whoopsie, I thought there was one Path entry");
221            };
222            let path = if path.is_empty() { "/" } else { path };
223
224            self.0.route(path, method_router)
225        } else {
226            paths.paths.iter().fold(self.0, |this, (path, _)| {
227                let path = if path.is_empty() { "/" } else { path };
228                this.route(path, method_router.clone())
229            })
230        };
231
232        // add or merge current paths to the OpenApi
233        for (path, item) in paths.paths {
234            if let Some(it) = self.1.paths.paths.get_mut(&path) {
235                it.merge_operations(item);
236            } else {
237                self.1.paths.paths.insert(path, item);
238            }
239        }
240
241        let components = self
242            .1
243            .components
244            .get_or_insert(utoipa::openapi::Components::new());
245        components.schemas.extend(schemas);
246
247        Self(router, self.1)
248    }
249
250    /// Pass through method for [`axum::Router<S>::route`].
251    pub fn route(self, path: &str, method_router: MethodRouter<S>) -> Self {
252        Self(self.0.route(path, method_router), self.1)
253    }
254
255    /// Pass through method for [`axum::Router::route_layer`].
256    pub fn route_layer<L>(self, layer: L) -> Self
257    where
258        L: Layer<Route> + Clone + Send + Sync + 'static,
259        L::Service: Service<Request> + Clone + Send + Sync + 'static,
260        <L::Service as Service<Request>>::Response: IntoResponse + 'static,
261        <L::Service as Service<Request>>::Error: Into<Infallible> + 'static,
262        <L::Service as Service<Request>>::Future: Send + 'static,
263    {
264        Self(self.0.route_layer(layer), self.1)
265    }
266
267    /// Pass through method for [`axum::Router<S>::route_service`].
268    pub fn route_service<T>(self, path: &str, service: T) -> Self
269    where
270        T: Service<Request, Error = Infallible> + Clone + Send + Sync + 'static,
271        T::Response: IntoResponse,
272        T::Future: Send + 'static,
273    {
274        Self(self.0.route_service(path, service), self.1)
275    }
276
277    /// Nest `router` to `self` under given `path`. Router routes will be nested with
278    /// [`axum::Router::nest`].
279    ///
280    /// This method expects [`OpenApiRouter`] instance in order to nest OpenApi paths and router
281    /// routes. If you wish to use [`axum::Router::nest`] you need to first convert this instance
282    /// to [`axum::Router`] _(`let _: Router = OpenApiRouter::new().into()`)_.
283    ///
284    /// # Examples
285    ///
286    /// _**Nest two routers.**_
287    /// ```rust
288    /// # use utoipa_axum::{routes, PathItemExt, router::OpenApiRouter};
289    /// #[utoipa::path(get, path = "/search")]
290    /// async fn search() {}
291    ///
292    /// let search_router = OpenApiRouter::new()
293    ///     .routes(utoipa_axum::routes!(search));
294    ///
295    /// let router: OpenApiRouter = OpenApiRouter::new()
296    ///     .nest("/api", search_router);
297    /// ```
298    pub fn nest(self, path: &str, router: OpenApiRouter<S>) -> Self {
299        // from axum::routing::path_router::path_for_nested_route
300        // method is private, so we need to replicate it here
301        fn path_for_nested_route(prefix: &str, path: &str) -> String {
302            let path = if path.is_empty() { "/" } else { path };
303            debug_assert!(prefix.starts_with('/'));
304
305            if prefix.ends_with('/') {
306                format!("{prefix}{}", path.trim_start_matches('/'))
307            } else if path == "/" {
308                prefix.into()
309            } else {
310                format!("{prefix}{path}")
311            }
312        }
313
314        let api = self.1.nest_with_path_composer(
315            path_for_nested_route(path, "/"),
316            router.1,
317            path_for_nested_route,
318        );
319        let router = self.0.nest(path, router.0);
320
321        Self(router, api)
322    }
323
324    /// Pass through method for [`axum::Router::nest_service`]. _**This does nothing for OpenApi paths.**_
325    pub fn nest_service<T>(self, path: &str, service: T) -> Self
326    where
327        T: Service<Request, Error = Infallible> + Clone + Send + Sync + 'static,
328        T::Response: IntoResponse,
329        T::Future: Send + 'static,
330    {
331        Self(self.0.nest_service(path, service), self.1)
332    }
333
334    /// Merge [`utoipa::openapi::path::Paths`] from `router` to `self` and merge [`Router`] routes
335    /// and fallback with [`axum::Router::merge`].
336    ///
337    /// This method expects [`OpenApiRouter`] instance in order to merge OpenApi paths and router
338    /// routes. If you wish to use [`axum::Router::merge`] you need to first convert this instance
339    /// to [`axum::Router`] _(`let _: Router = OpenApiRouter::new().into()`)_.
340    ///
341    /// # Examples
342    ///
343    /// _**Merge two routers.**_
344    /// ```rust
345    /// # use utoipa_axum::{routes, PathItemExt, router::OpenApiRouter};
346    /// #[utoipa::path(get, path = "/search")]
347    /// async fn search() {}
348    ///
349    /// let search_router = OpenApiRouter::new()
350    ///     .routes(utoipa_axum::routes!(search));
351    ///
352    /// let router: OpenApiRouter = OpenApiRouter::new()
353    ///     .merge(search_router);
354    /// ```
355    pub fn merge(mut self, router: OpenApiRouter<S>) -> Self {
356        self.1.merge(router.1);
357
358        Self(self.0.merge(router.0), self.1)
359    }
360
361    /// Pass through method for [`axum::Router::with_state`].
362    pub fn with_state<S2>(self, state: S) -> OpenApiRouter<S2> {
363        OpenApiRouter(self.0.with_state(state), self.1)
364    }
365
366    /// Consume `self` returning the [`utoipa::openapi::OpenApi`] instance of the
367    /// [`OpenApiRouter`].
368    pub fn into_openapi(self) -> utoipa::openapi::OpenApi {
369        self.1
370    }
371
372    /// Take the [`utoipa::openapi::OpenApi`] instance without consuming the [`OpenApiRouter`].
373    pub fn to_openapi(&mut self) -> utoipa::openapi::OpenApi {
374        std::mem::take(&mut self.1)
375    }
376
377    /// Get reference to the [`utoipa::openapi::OpenApi`] instance of the router.
378    pub fn get_openapi(&self) -> &utoipa::openapi::OpenApi {
379        &self.1
380    }
381
382    /// Get mutable reference to the [`utoipa::openapi::OpenApi`] instance of the router.
383    pub fn get_openapi_mut(&mut self) -> &mut utoipa::openapi::OpenApi {
384        &mut self.1
385    }
386
387    /// Split the content of the [`OpenApiRouter`] to parts. Method will return a tuple of
388    /// inner [`axum::Router`] and [`utoipa::openapi::OpenApi`].
389    pub fn split_for_parts(self) -> (axum::Router<S>, utoipa::openapi::OpenApi) {
390        (self.0, self.1)
391    }
392}
393
394impl<S> Default for OpenApiRouter<S>
395where
396    S: Send + Sync + Clone + 'static,
397{
398    fn default() -> Self {
399        Self::with_openapi(utoipa::openapi::OpenApiBuilder::new().build())
400    }
401}
402
403impl<S> From<OpenApiRouter<S>> for Router<S> {
404    fn from(value: OpenApiRouter<S>) -> Self {
405        value.0
406    }
407}
408
409impl<S> From<Router<S>> for OpenApiRouter<S> {
410    fn from(value: Router<S>) -> Self {
411        OpenApiRouter(value, utoipa::openapi::OpenApiBuilder::new().build())
412    }
413}