aide/axum/
mod.rs

1//! Open API code generation for [`axum`].
2//!
3//! The implementation closely mimics the api of [`axum`] with
4//! extra care taken in order to allow seamless transitions.
5//!
6//! The notable types are [`ApiRouter`] and [`ApiMethodRouter`] that wrap
7//! [`axum::Router`] and [`axum::routing::MethodRouter`] respectively.
8//! Likewise, the top-level methods in [`axum::routing`] have their counterparts
9//! in [`routing`].
10//!
11//! # Examples
12//!
13//! Take the following `axum` example:
14//!
15//! ```no_run
16//! use axum::{response::IntoResponse, routing::post, Json, Router};
17//! use serde::Deserialize;
18//!
19//! #[derive(Deserialize)]
20//! struct User {
21//!     name: String,
22//! }
23//!
24//! async fn hello_user(Json(user): Json<User>) -> impl IntoResponse {
25//!     format!("hello {}", user.name)
26//! }
27//!
28//! #[tokio::main]
29//! async fn main() {
30//!     let app = Router::new().route("/hello", post(hello_user));
31//!
32//!     let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
33//!
34//!     axum::serve(listener, app).await.unwrap();
35//! }
36//! ```
37//!
38//! We can apply the following changes to generate documentation for it:
39//!
40//! ```no_run
41//! // Replace some of the `axum::` types with `aide::axum::` ones.
42//! use aide::{
43//!     axum::{
44//!         routing::{get, post},
45//!         ApiRouter, IntoApiResponse,
46//!     },
47//!     openapi::{Info, OpenApi},
48//! };
49//! use axum::{Extension, Json};
50//! use schemars::JsonSchema;
51//! use serde::Deserialize;
52//!
53//! // We'll need to derive `JsonSchema` for
54//! // all types that appear in the api documentation.
55//! #[derive(Deserialize, JsonSchema)]
56//! struct User {
57//!     name: String,
58//! }
59//!
60//! async fn hello_user(Json(user): Json<User>) -> impl IntoApiResponse {
61//!     format!("hello {}", user.name)
62//! }
63//!
64//! // Note that this clones the document on each request.
65//! // To be more efficient, we could wrap it into an Arc,
66//! // or even store it as a serialized string.
67//! async fn serve_api(Extension(api): Extension<OpenApi>) -> impl IntoApiResponse {
68//!     Json(api)
69//! }
70//!
71//! #[tokio::main]
72//! async fn main() {
73//!     let app = ApiRouter::new()
74//!         // Change `route` to `api_route` for the route
75//!         // we'd like to expose in the documentation.
76//!         .api_route("/hello", post(hello_user))
77//!         // We'll serve our generated document here.
78//!         .route("/api.json", get(serve_api));
79//!
80//!     let mut api = OpenApi {
81//!         info: Info {
82//!             description: Some("an example API".to_string()),
83//!             ..Info::default()
84//!         },
85//!         ..OpenApi::default()
86//!     };
87//!
88//!     let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
89//!
90//!     axum::serve(
91//!         listener,
92//!         app
93//!             // Generate the documentation.
94//!             .finish_api(&mut api)
95//!             // Expose the documentation to the handlers.
96//!             .layer(Extension(api))
97//!             .into_make_service(),
98//!     )
99//!     .await
100//!     .unwrap();
101//! }
102//! ```
103//!
104//! Only routes added via `api_route` are visible in the documentation,
105//! this makes exposed routes explicit and less error-prone.
106//!
107//! ## Adding details.
108//!
109//! The above example includes routes and request parameters but
110//! it's lacking response types and additional metadata such as descriptions,
111//! as these are not possible to infer just via types.
112//!
113//! ### Responses
114//!
115//! Generally we can add information at the following levels:
116//!
117//! - Operation level (e.g. [`get_with`](crate::axum::routing::get_with))
118//! - Route level ([`api_route_with`](crate::axum::ApiRouter::api_route_with))
119//! - API-level ([`finish_api_with`](crate::axum::ApiRouter::finish_api_with))
120//!
121//! All of these are additive and the API-level information will not override
122//! route or operation metadata unless explicitly stated.
123//!
124//! With this being said, we can specify the response status code
125//! and the type for our `hello_user` operation:
126//!
127//! ```ignore
128//! // ...
129//! .api_route(
130//!     "/hello",
131//!     post_with(hello_user, |op| op.response::<200, String>()),
132//! )
133//! // ...
134//! ```
135//!
136//! And on the API-level we define that in every unspecified
137//! case, we return some kind of text:
138//!
139//! ```ignore
140//! // ...
141//! app.finish_api_with(&mut api, |api| api.default_response::<String>())
142//! // ...
143//! ```
144//!
145//! ### Other Metadata
146//!
147//! We can extend our `hello_user` operation with further metadata:
148//!
149//! ```ignore
150//! // ...
151//! .api_route(
152//!     "/hello",
153//!     post_with(hello_user, |o| {
154//!         o.id("helloUser")
155//!             .description("says hello to the given user")
156//!             .response_with::<200, String, _>(|res| {
157//!                 res.description("a simple message saying hello to the user")
158//!                     .example(String::from("hello Tom"))
159//!             })
160//!     }),
161//! )
162//! // ...
163//! ```
164//!
165//! # Composability
166//!
167//! Just like in `axum`, nesting and merging routers is possible,
168//! and the documented routes will be updated as expected.
169//!
170
171use std::{convert::Infallible, future::Future, mem, pin::Pin};
172
173use crate::{
174    generate::{self, in_context},
175    openapi::{OpenApi, PathItem, ReferenceOr, SchemaObject},
176    operation::OperationHandler,
177    util::{merge_paths, path_for_nested_route},
178    OperationInput, OperationOutput,
179};
180#[cfg(feature = "axum-tokio")]
181use axum::extract::connect_info::IntoMakeServiceWithConnectInfo;
182use axum::{
183    body::{Body, Bytes, HttpBody},
184    handler::Handler,
185    http::Request,
186    response::IntoResponse,
187    routing::{IntoMakeService, Route, RouterAsService, RouterIntoService},
188    Router,
189};
190use indexmap::map::Entry;
191use indexmap::IndexMap;
192use tower_layer::Layer;
193use tower_service::Service;
194
195#[cfg(feature = "axum-extra")]
196use axum_extra::routing::RouterExt as _;
197
198use self::routing::ApiMethodRouter;
199use crate::transform::{TransformOpenApi, TransformPathItem};
200
201mod inputs;
202mod outputs;
203
204pub mod routing;
205
206/// A wrapper over [`axum::Router`] that adds
207/// API documentation-specific features.
208#[must_use]
209#[derive(Debug)]
210pub struct ApiRouter<S = ()> {
211    paths: IndexMap<String, PathItem>,
212    router: Router<S>,
213}
214
215impl<S> Clone for ApiRouter<S> {
216    fn clone(&self) -> Self {
217        Self {
218            paths: self.paths.clone(),
219            router: self.router.clone(),
220        }
221    }
222}
223
224impl<B> Service<Request<B>> for ApiRouter<()>
225where
226    B: HttpBody<Data = Bytes> + Send + 'static,
227    B::Error: Into<axum::BoxError>,
228{
229    type Response = axum::response::Response;
230    type Error = Infallible;
231    type Future = axum::routing::future::RouteFuture<Infallible>;
232
233    #[inline]
234    fn poll_ready(
235        &mut self,
236        cx: &mut std::task::Context<'_>,
237    ) -> std::task::Poll<Result<(), Self::Error>> {
238        Service::<Request<B>>::poll_ready(&mut self.router, cx)
239    }
240
241    #[inline]
242    fn call(&mut self, req: Request<B>) -> Self::Future {
243        self.router.call(req)
244    }
245}
246
247#[allow(clippy::mismatching_type_param_order)]
248impl Default for ApiRouter<()> {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254impl<S> ApiRouter<S>
255where
256    S: Clone + Send + Sync + 'static,
257{
258    /// Create a new router.
259    ///
260    /// See [`axum::Router::new`] for details.
261    pub fn new() -> Self {
262        Self {
263            paths: IndexMap::new(),
264            router: Router::new(),
265        }
266    }
267
268    /// Add state to the router.
269    ///
270    /// See [`axum::Router::with_state`] for details.
271    pub fn with_state<S2>(self, state: S) -> ApiRouter<S2> {
272        ApiRouter {
273            paths: self.paths,
274            router: self.router.with_state(state),
275        }
276    }
277
278    /// Transform the contained [`PathItem`]s.
279    ///
280    /// This method accepts a transform function to edit each [`PathItem`] provided by this router.
281    pub fn with_path_items(
282        mut self,
283        mut transform: impl FnMut(TransformPathItem) -> TransformPathItem,
284    ) -> Self {
285        for (_, item) in &mut self.paths {
286            let _ = transform(TransformPathItem::new(item));
287        }
288        self
289    }
290
291    /// Create a route to the given method router and include it in
292    /// the API documentation.
293    ///
294    /// As opposed to [`route`](crate::axum::ApiRouter::route), this method only accepts an [`ApiMethodRouter`].
295    ///
296    /// See [`axum::Router::route`] for details.
297    #[tracing::instrument(skip_all, fields(path = path))]
298    pub fn api_route(mut self, path: &str, mut method_router: ApiMethodRouter<S>) -> Self {
299        in_context(|ctx| {
300            let new_path_item = method_router.take_path_item();
301
302            if let Some(path_item) = self.paths.get_mut(path) {
303                merge_paths(ctx, path, path_item, new_path_item);
304            } else {
305                self.paths.insert(path.into(), new_path_item);
306            }
307        });
308
309        self.router = self.router.route(path, method_router.router);
310        self
311    }
312
313    #[cfg(feature = "axum-extra")]
314    /// Create a route to the given method router with trailing slash removal and include it in
315    /// the API documentation.
316    ///
317    /// As opposed to [`route_with_tsr`](crate::axum::ApiRouter::route_with_tsr), this method only accepts an [`ApiMethodRouter`].
318    ///
319    /// See [`axum_extra::routing::RouterExt::route_with_tsr`] for details.
320    #[tracing::instrument(skip_all, fields(path = path))]
321    pub fn api_route_with_tsr(mut self, path: &str, mut method_router: ApiMethodRouter<S>) -> Self {
322        in_context(|ctx| {
323            let new_path_item = method_router.take_path_item();
324
325            if let Some(path_item) = self.paths.get_mut(path) {
326                merge_paths(ctx, path, path_item, new_path_item);
327            } else {
328                self.paths.insert(path.into(), new_path_item);
329            }
330        });
331
332        self.router = self.router.route_with_tsr(path, method_router.router);
333        self
334    }
335
336    /// Create a route to the given method router and include it in
337    /// the API documentation.
338    ///
339    /// This method accepts a transform function to edit
340    /// the generated API documentation with.
341    ///
342    /// See [`axum::Router::route`] or [`api_route`](crate::axum::ApiRouter::api_route) for details.
343    #[tracing::instrument(skip_all, fields(path = path))]
344    pub fn api_route_with(
345        mut self,
346        path: &str,
347        mut method_router: ApiMethodRouter<S>,
348        transform: impl FnOnce(TransformPathItem) -> TransformPathItem,
349    ) -> Self {
350        in_context(|ctx| {
351            let mut p = method_router.take_path_item();
352            let t = transform(TransformPathItem::new(&mut p));
353
354            if !t.hidden {
355                if let Some(path_item) = self.paths.get_mut(path) {
356                    merge_paths(ctx, path, path_item, p);
357                } else {
358                    self.paths.insert(path.into(), p);
359                }
360            }
361        });
362
363        self.router = self.router.route(path, method_router.router);
364        self
365    }
366
367    #[cfg(feature = "axum-extra")]
368    /// Create a route to the given method router with trailing slash removal and include it in
369    /// the API documentation.
370    ///
371    /// This method accepts a transform function to edit
372    /// the generated API documentation with.
373    ///
374    /// See [`axum_extra::routing::RouterExt::route_with_tsr`] for details.
375    #[tracing::instrument(skip_all, fields(path = path))]
376    pub fn api_route_with_tsr_and(
377        mut self,
378        path: &str,
379        mut method_router: ApiMethodRouter<S>,
380        transform: impl FnOnce(TransformPathItem) -> TransformPathItem,
381    ) -> Self {
382        in_context(|ctx| {
383            let mut p = method_router.take_path_item();
384            let t = transform(TransformPathItem::new(&mut p));
385
386            if !t.hidden {
387                if let Some(path_item) = self.paths.get_mut(path) {
388                    merge_paths(ctx, path, path_item, p);
389                } else {
390                    self.paths.insert(path.into(), p);
391                }
392            }
393        });
394
395        self.router = self.router.route_with_tsr(path, method_router.router);
396        self
397    }
398
399    /// Turn this router into an [`axum::Router`] while merging
400    /// generated documentation into the provided [`OpenApi`].
401    #[tracing::instrument(skip_all)]
402    pub fn finish_api(mut self, api: &mut OpenApi) -> Router<S> {
403        self.merge_api(api);
404        self.router
405    }
406
407    /// Turn this router into an [`axum::Router`] while merging
408    /// generated documentation into the provided [`OpenApi`].
409    ///
410    /// This method accepts a transform function to edit
411    /// the generated API documentation with.
412    #[tracing::instrument(skip_all)]
413    pub fn finish_api_with<F>(mut self, api: &mut OpenApi, transform: F) -> Router<S>
414    where
415        F: FnOnce(TransformOpenApi) -> TransformOpenApi,
416    {
417        self.merge_api_with(api, transform);
418        self.router
419    }
420
421    fn merge_api(&mut self, api: &mut OpenApi) {
422        self.merge_api_with(api, |x| x);
423    }
424
425    fn merge_api_with<F>(&mut self, api: &mut OpenApi, transform: F)
426    where
427        F: FnOnce(TransformOpenApi) -> TransformOpenApi,
428    {
429        if api.paths.is_none() {
430            api.paths = Some(Default::default());
431        }
432
433        let paths = api.paths.as_mut().unwrap();
434
435        paths.paths = mem::take(&mut self.paths)
436            .into_iter()
437            .map(|(route, path)| (route, ReferenceOr::Item(path)))
438            .collect();
439
440        let _ = transform(TransformOpenApi::new(api));
441
442        let needs_reset =
443            in_context(|ctx| {
444                if !ctx.extract_schemas {
445                    return false;
446                }
447
448                let components = api.components.get_or_insert_with(Default::default);
449
450                components
451                    .schemas
452                    .extend(ctx.schema.take_definitions().into_iter().map(
453                        |(name, json_schema)| {
454                            (
455                                name,
456                                SchemaObject {
457                                    json_schema,
458                                    example: None,
459                                    external_docs: None,
460                                },
461                            )
462                        },
463                    ));
464
465                true
466            });
467
468        if needs_reset {
469            generate::reset_context();
470        }
471    }
472}
473
474/// Existing methods extended with api-specifics.
475impl<S> ApiRouter<S>
476where
477    S: Clone + Send + Sync + 'static,
478{
479    /// See [`axum::Router::route`] for details.
480    ///
481    /// This method accepts [`ApiMethodRouter`] but does not generate API documentation.
482    #[tracing::instrument(skip_all)]
483    pub fn route(mut self, path: &str, method_router: impl Into<ApiMethodRouter<S>>) -> Self {
484        self.router = self.router.route(path, method_router.into().router);
485        self
486    }
487
488    /// See [`axum_extra::routing::RouterExt::route_with_tsr`] for details.
489    ///
490    /// This method accepts [`ApiMethodRouter`] but does not generate API documentation.
491    #[cfg(feature = "axum-extra")]
492    #[tracing::instrument(skip_all)]
493    pub fn route_with_tsr(
494        mut self,
495        path: &str,
496        method_router: impl Into<ApiMethodRouter<S>>,
497    ) -> Self {
498        self.router = self.router.route(path, method_router.into().router);
499        self
500    }
501
502    /// See [`axum::Router::route_service`] for details.
503    #[tracing::instrument(skip_all)]
504    pub fn route_service<T>(mut self, path: &str, service: T) -> Self
505    where
506        T: Service<Request<Body>, Error = Infallible> + Clone + Send + Sync + 'static,
507        T::Response: IntoResponse,
508        T::Future: Send + 'static,
509    {
510        self.router = self.router.route_service(path, service);
511        self
512    }
513
514    /// See [`axum_extra::routing::RouterExt::route_service_with_tsr`] for details.
515    #[cfg(feature = "axum-extra")]
516    #[tracing::instrument(skip_all)]
517    pub fn route_service_with_tsr<T>(mut self, path: &str, service: T) -> Self
518    where
519        T: Service<axum::extract::Request, Error = Infallible> + Clone + Send + Sync + 'static,
520        T::Response: IntoResponse,
521        T::Future: Send + 'static,
522        Self: Sized,
523    {
524        self.router = self.router.route_service_with_tsr(path, service);
525        self
526    }
527
528    /// See [`axum::Router::nest`] for details.
529    ///
530    /// The generated documentations are nested as well.
531    #[tracing::instrument(skip_all)]
532    pub fn nest(mut self, path: &str, router: ApiRouter<S>) -> Self {
533        self.router = self.router.nest(path, router.router);
534
535        self.paths.extend(
536            router
537                .paths
538                .into_iter()
539                .map(|(route, path_item)| (path_for_nested_route(path, &route), path_item)),
540        );
541
542        self
543    }
544
545    /// Alternative to [`nest_service`](Self::nest_service) which besides nesting the service nests
546    /// the generated documentation as well.
547    ///
548    /// Due to Rust's limitations, currently this function will not
549    /// accept arbitrary services but only types that can be
550    /// converted into an [`ApiRouter`].
551    ///
552    /// Thus the primary and probably the only use-case
553    /// of this function is nesting routers with different states.
554    pub fn nest_api_service(mut self, path: &str, service: impl Into<ApiRouter<()>>) -> Self {
555        let router: ApiRouter<()> = service.into();
556
557        self.paths.extend(
558            router
559                .paths
560                .into_iter()
561                .map(|(route, path_item)| (path_for_nested_route(path, &route), path_item)),
562        );
563        self.router = self.router.nest_service(path, router.router);
564        self
565    }
566
567    /// See [`axum::Router::nest_service`] for details. Use [`nest_api_service`](Self::nest_api_service())
568    /// to pass on the API documentation from the nested service as well.
569    pub fn nest_service<T>(mut self, path: &str, svc: T) -> Self
570    where
571        T: Service<Request<Body>, Error = Infallible> + Clone + Send + Sync + 'static,
572        T::Response: IntoResponse,
573        T::Future: Send + 'static,
574    {
575        self.router = self.router.nest_service(path, svc);
576
577        self
578    }
579
580    /// See [`axum::Router::merge`] for details.
581    ///
582    /// If an another [`ApiRouter`] is provided, the generated documentations
583    /// are merged as well..
584    pub fn merge<R>(mut self, other: R) -> Self
585    where
586        R: Into<ApiRouter<S>>,
587    {
588        let other: ApiRouter<S> = other.into();
589
590        for (key, path) in other.paths {
591            match self.paths.entry(key) {
592                Entry::Occupied(mut o) => {
593                    o.get_mut().merge_with(path);
594                }
595                Entry::Vacant(v) => {
596                    v.insert(path);
597                }
598            }
599        }
600        self.router = self.router.merge(other.router);
601        self
602    }
603
604    /// See [`axum::Router::layer`] for details.
605    pub fn layer<L>(self, layer: L) -> ApiRouter<S>
606    where
607        L: Layer<Route> + Clone + Send + Sync + 'static,
608        L::Service: Service<Request<Body>> + Clone + Send + Sync + 'static,
609        <L::Service as Service<Request<Body>>>::Response: IntoResponse + 'static,
610        <L::Service as Service<Request<Body>>>::Error: Into<Infallible> + 'static,
611        <L::Service as Service<Request<Body>>>::Future: Send + 'static,
612    {
613        ApiRouter {
614            paths: self.paths,
615            router: self.router.layer(layer),
616        }
617    }
618
619    /// See [`axum::Router::route_layer`] for details.
620    pub fn route_layer<L>(mut self, layer: L) -> Self
621    where
622        L: Layer<Route> + Clone + Send + Sync + 'static,
623        L::Service: Service<Request<Body>> + Clone + Send + Sync + 'static,
624        <L::Service as Service<Request<Body>>>::Response: IntoResponse + 'static,
625        <L::Service as Service<Request<Body>>>::Error: Into<Infallible> + 'static,
626        <L::Service as Service<Request<Body>>>::Future: Send + 'static,
627    {
628        self.router = self.router.route_layer(layer);
629        self
630    }
631
632    /// See [`axum::Router::fallback`] for details.
633    pub fn fallback<H, T>(mut self, handler: H) -> Self
634    where
635        H: Handler<T, S>,
636        T: 'static,
637    {
638        self.router = self.router.fallback(handler);
639        self
640    }
641
642    /// See [`axum::Router::fallback_service`] for details.
643    pub fn fallback_service<T>(mut self, svc: T) -> Self
644    where
645        T: Service<Request<Body>, Error = Infallible> + Clone + Send + Sync + 'static,
646        T::Response: IntoResponse,
647        T::Future: Send + 'static,
648    {
649        self.router = self.router.fallback_service(svc);
650        self
651    }
652
653    /// See [`axum::Router::as_service`] for details.
654    ///
655    /// Using this method will not generate API documentation.
656    #[must_use]
657    pub fn as_service<B>(&mut self) -> RouterAsService<'_, B, S> {
658        self.router.as_service()
659    }
660
661    /// See [`axum::Router::into_service`] for details.
662    ///
663    /// Using this method will not generate API documentation.
664    #[must_use]
665    pub fn into_service<B>(self) -> RouterIntoService<B, S> {
666        self.router.into_service()
667    }
668}
669
670impl ApiRouter<()> {
671    /// See [`axum::Router::into_make_service`] for details.
672    #[tracing::instrument(skip_all)]
673    #[must_use]
674    pub fn into_make_service(self) -> IntoMakeService<Router<()>> {
675        self.router.into_make_service()
676    }
677
678    /// See [`axum::Router::into_make_service_with_connect_info`] for details.
679    #[tracing::instrument(skip_all)]
680    #[must_use]
681    #[cfg(feature = "axum-tokio")]
682    pub fn into_make_service_with_connect_info<C>(
683        self,
684    ) -> IntoMakeServiceWithConnectInfo<Router<()>, C> {
685        self.router.into_make_service_with_connect_info()
686    }
687}
688
689impl<S> From<Router<S>> for ApiRouter<S> {
690    fn from(router: Router<S>) -> Self {
691        ApiRouter {
692            paths: IndexMap::new(),
693            router,
694        }
695    }
696}
697
698impl<S> From<ApiRouter<S>> for Router<S> {
699    fn from(api: ApiRouter<S>) -> Self {
700        api.router
701    }
702}
703
704/// A trait analogous to [`IntoResponse`] that allows writing
705/// `impl IntoApiResponse` for documented handlers.
706/// Axum's `IntoResponse` cannot be used for these handlers
707/// since the return type has to implement [`OperationOutput`].
708///
709/// This trait has a blanket implementation for all types
710/// that implement [`IntoResponse`] and [`OperationOutput`],
711/// it should not be implemented manually.
712pub trait IntoApiResponse: IntoResponse + OperationOutput {}
713
714impl<T> IntoApiResponse for T where T: IntoResponse + OperationOutput {}
715
716/// Convenience extension trait for [`axum::Router`].
717pub trait RouterExt<S>: private::Sealed + Sized {
718    /// Turn the router into an [`ApiRouter`] to enable
719    /// automatic generation of API documentation.
720    fn into_api(self) -> ApiRouter<S>;
721    /// Add an API route, see [`ApiRouter::api_route`](crate::axum::ApiRouter::api_route)
722    /// for details.
723    ///
724    /// This method additionally turns the router into an [`ApiRouter`].
725    fn api_route(self, path: &str, method_router: ApiMethodRouter<S>) -> ApiRouter<S>;
726    #[cfg(feature = "axum-extra")]
727    /// Add an API route, see [`ApiRouter::api_route_with_tsr`](crate::axum::ApiRouter::api_route_with_tsr)
728    fn api_route_with_tsr(self, path: &str, method_router: ApiMethodRouter<S>) -> ApiRouter<S>;
729}
730
731impl<S> RouterExt<S> for Router<S>
732where
733    S: Clone + Send + Sync + 'static,
734{
735    #[tracing::instrument(skip_all)]
736    fn into_api(self) -> ApiRouter<S> {
737        ApiRouter::from(self)
738    }
739
740    #[tracing::instrument(skip_all)]
741    fn api_route(self, path: &str, method_router: ApiMethodRouter<S>) -> ApiRouter<S> {
742        ApiRouter::from(self).api_route(path, method_router)
743    }
744
745    #[cfg(feature = "axum-extra")]
746    #[tracing::instrument(skip_all)]
747    fn api_route_with_tsr(self, path: &str, method_router: ApiMethodRouter<S>) -> ApiRouter<S> {
748        ApiRouter::from(self).api_route_with_tsr(path, method_router)
749    }
750}
751
752impl<S> private::Sealed for Router<S> {}
753
754#[doc(hidden)]
755pub enum ServiceOrApiRouter<T> {
756    Service(T),
757    Router(ApiRouter<()>),
758}
759
760impl<T> From<T> for ServiceOrApiRouter<T>
761where
762    T: Service<Request<Body>, Error = Infallible> + Clone + Send + 'static,
763    T::Response: IntoResponse,
764    T::Future: Send + 'static,
765{
766    fn from(v: T) -> Self {
767        Self::Service(v)
768    }
769}
770
771impl From<ApiRouter<()>> for ServiceOrApiRouter<DefinitelyNotService> {
772    fn from(v: ApiRouter<()>) -> Self {
773        Self::Router(v)
774    }
775}
776
777// To help with type-inference.
778#[derive(Clone)]
779#[doc(hidden)]
780pub enum DefinitelyNotService {}
781
782impl Service<Request<Body>> for DefinitelyNotService {
783    type Response = String;
784
785    type Error = Infallible;
786
787    type Future =
788        Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + Sync + 'static>>;
789
790    fn poll_ready(
791        &mut self,
792        _cx: &mut std::task::Context<'_>,
793    ) -> std::task::Poll<Result<(), Self::Error>> {
794        unreachable!()
795    }
796
797    fn call(&mut self, _req: Request<Body>) -> Self::Future {
798        unreachable!()
799    }
800}
801
802mod private {
803    pub trait Sealed {}
804}
805
806impl<I, O, L, H, T, S> OperationHandler<I, O> for axum::handler::Layered<L, H, T, S>
807where
808    H: OperationHandler<I, O>,
809    I: OperationInput,
810    O: OperationOutput,
811{
812}
813
814/// A trait that extends [`axum::handler::Handler`] with API operation
815/// details.
816///
817/// Just like axum's `Handler`, it is automatically implemented
818/// for the appropriate types.
819pub trait AxumOperationHandler<I, O, T, S>: Handler<T, S> + OperationHandler<I, O>
820where
821    I: OperationInput,
822    O: OperationOutput,
823{
824}
825
826impl<H, I, O, T, S> AxumOperationHandler<I, O, T, S> for H
827where
828    H: Handler<T, S> + OperationHandler<I, O>,
829    I: OperationInput,
830    O: OperationOutput,
831{
832}
833
834#[cfg(test)]
835#[allow(clippy::unused_async)]
836mod tests {
837    use crate::axum::{routing, ApiRouter};
838    use axum::{extract::State, handler::Handler};
839
840    async fn test_handler1(State(_): State<TestState>) {}
841
842    async fn test_handler2(State(_): State<u8>) {}
843
844    async fn test_handler3() {}
845
846    fn nested_route() -> ApiRouter {
847        ApiRouter::new()
848            .api_route_with("/", routing::post(test_handler3), |t| t)
849            .api_route_with("/test1", routing::post(test_handler3), |t| t)
850            .api_route_with("/test2/", routing::post(test_handler3), |t| t)
851    }
852
853    #[derive(Clone, Copy)]
854    struct TestState {
855        field1: u8,
856    }
857
858    #[test]
859    fn test_nesting_with_nondefault_state() {
860        let _app: ApiRouter = ApiRouter::new()
861            .nest_api_service("/home", ApiRouter::new().with_state(1_isize))
862            .with_state(1_usize);
863    }
864
865    #[test]
866    fn test_method_router_with_state() {
867        let app: ApiRouter<TestState> =
868            ApiRouter::new().api_route("/", routing::get(test_handler1));
869        let app_with_state: ApiRouter = app.with_state(TestState { field1: 0 });
870        // Only after state is given `into_make_service()` can be invoked.
871        let _service = app_with_state.into_make_service();
872    }
873
874    #[test]
875    fn test_router_with_different_states() {
876        let state = TestState { field1: 0 };
877        let app: ApiRouter = ApiRouter::new()
878            .api_route("/test1", routing::get(test_handler1))
879            .api_route(
880                "/test2",
881                routing::get(test_handler2).with_state(state.field1),
882            )
883            .with_state(state);
884        let _service = app.into_make_service();
885    }
886
887    #[test]
888    fn test_api_route_with_same_router_different_methods() {
889        let app: ApiRouter = ApiRouter::new()
890            .api_route_with("/test1", routing::post(test_handler3), |t| t)
891            .api_route_with("/test1", routing::get(test_handler3), |t| t);
892
893        let item = app
894            .paths
895            .get("/test1")
896            .expect("should contain handler for /test1");
897
898        assert!(item.get.is_some());
899        assert!(item.post.is_some());
900    }
901
902    #[test]
903    fn test_nested_routing() {
904        let app: ApiRouter = ApiRouter::new().nest("/app", nested_route());
905
906        assert!(app.paths.contains_key("/app"));
907        assert!(!app.paths.contains_key("/app/"));
908        assert!(app.paths.contains_key("/app/test1"));
909        assert!(app.paths.contains_key("/app/test2/"));
910    }
911
912    #[test]
913    fn test_layered_handler() {
914        let _app: ApiRouter = ApiRouter::new().api_route(
915            "/test-route",
916            routing::get(test_handler3.layer(tower_layer::Identity::new())),
917        );
918    }
919}