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}