next_web_api_doc/
lib.rs

1//! Rust implementation of Openapi Spec V3.1.
2
3pub use paste::paste;
4pub use utoipa::{
5    openapi, {OpenApi, Path, ToSchema},
6};
7
8/// Wrapper type for [`utoipa::openapi::path::Paths`] and [`axum::routing::MethodRouter`].
9///
10/// This is used with [`OpenApiRouter::routes`] method to register current _`paths`_ to the
11/// [`utoipa::openapi::OpenApi`] of [`OpenApiRouter`] instance.
12///
13/// See [`routes`][routes] for usage.
14///
15/// [routes]: ../macro.routes.html
16pub type UtoipaMethodRouter = (
17    Vec<(
18        String,
19        utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
20    )>,
21    utoipa::openapi::path::Paths,
22);
23
24/// A wrapper struct for [`axum::Router`] and [`utoipa::openapi::OpenApi`] for composing handlers
25/// and services with collecting OpenAPI information from the handlers.
26///
27/// This struct provides pass through implementation for most of the [`axum::Router`] methods and
28/// extends capabilities for few to collect the OpenAPI information. Methods that are not
29/// implemented can be easily called after converting this router to [`axum::Router`] by
30/// [`Into::into`].
31///
32/// # Examples
33///
34/// _**Create new [`OpenApiRouter`] with default values populated from cargo environment variables.**_
35/// ```rust
36/// # use utoipa_axum::router::OpenApiRouter;
37/// let _: OpenApiRouter = OpenApiRouter::new();
38/// ```
39///
40/// _**Instantiate a new [`OpenApiRouter`] with new empty [`utoipa::openapi::OpenApi`].**_
41/// ```rust
42/// # use utoipa_axum::router::OpenApiRouter;
43/// let _: OpenApiRouter = OpenApiRouter::default();
44/// ```
45#[derive(Clone)]
46#[cfg_attr(feature = "debug", derive(Debug))]
47pub struct OpenApiRouter(utoipa::openapi::OpenApi);
48
49impl OpenApiRouter {
50    /// Instantiate a new [`OpenApiRouter`] with default values populated from cargo environment
51    /// variables. This creates an `OpenApi` similar of creating a new `OpenApi` via
52    /// `#[derive(OpenApi)]`
53    ///
54    /// If you want to create [`OpenApiRouter`] with completely empty [`utoipa::openapi::OpenApi`]
55    /// instance, use [`OpenApiRouter::default()`].
56    pub fn new() -> OpenApiRouter {
57        use utoipa::OpenApi;
58        #[derive(OpenApi)]
59        struct Api;
60
61        Self::with_openapi(Api::openapi())
62    }
63
64    /// Instantiates a new [`OpenApiRouter`] with given _`openapi`_ instance.
65    ///
66    /// This function allows using existing [`utoipa::openapi::OpenApi`] as source for this router.
67    ///
68    /// # Examples
69    ///
70    /// _**Use derived [`utoipa::openapi::OpenApi`] as source for [`OpenApiRouter`].**_
71    /// ```rust
72    /// # use utoipa::OpenApi;
73    /// # use utoipa_axum::router::OpenApiRouter;
74    /// #[derive(utoipa::ToSchema)]
75    /// struct Todo {
76    ///     id: i32,
77    /// }
78    /// #[derive(utoipa::OpenApi)]
79    /// #[openapi(components(schemas(Todo)))]
80    /// struct Api;
81    ///
82    /// let mut router: OpenApiRouter = OpenApiRouter::with_openapi(Api::openapi());
83    /// ```
84    pub fn with_openapi(openapi: utoipa::openapi::OpenApi) -> Self {
85        Self(openapi)
86    }
87
88    /// Register [`UtoipaMethodRouter`] content created with [`routes`][routes] macro to `self`.
89    ///
90    /// Paths of the [`UtoipaMethodRouter`] will be extended to [`utoipa::openapi::OpenApi`] and
91    /// [`axum::routing::MethodRouter`] will be added to the [`axum::Router`].
92    ///
93    /// [routes]: ../macro.routes.html
94    pub fn routes(mut self, (schemas, paths): UtoipaMethodRouter) -> Self {
95        // add or merge current paths to the OpenApi
96        for (path, item) in paths.paths {
97            if let Some(it) = self.0.paths.paths.get_mut(&path) {
98                it.merge_operations(item);
99            } else {
100                self.0.paths.paths.insert(path, item);
101            }
102        }
103
104        let components = self
105            .0
106            .components
107            .get_or_insert(utoipa::openapi::Components::new());
108        components.schemas.extend(schemas);
109
110        Self(self.0)
111    }
112
113    /// Nest `router` to `self` under given `path`. Router routes will be nested with
114    /// [`axum::Router::nest`].
115    ///
116    /// This method expects [`OpenApiRouter`] instance in order to nest OpenApi paths and router
117    /// routes. If you wish to use [`axum::Router::nest`] you need to first convert this instance
118    /// to [`axum::Router`] _(`let _: Router = OpenApiRouter::new().into()`)_.
119    ///
120    /// # Examples
121    ///
122    /// _**Nest two routers.**_
123    /// ```rust
124    /// # use utoipa_axum::{routes, PathItemExt, router::OpenApiRouter};
125    /// #[utoipa::path(get, path = "/search")]
126    /// async fn search() {}
127    ///
128    /// let search_router = OpenApiRouter::new()
129    ///     .routes(utoipa_axum::routes!(search));
130    ///
131    /// let router: OpenApiRouter = OpenApiRouter::new()
132    ///     .nest("/api", search_router);
133    /// ```
134    pub fn nest(self, path: &str, router: OpenApiRouter) -> Self {
135        // from axum::routing::path_router::path_for_nested_route
136        // method is private, so we need to replicate it here
137        fn path_for_nested_route(prefix: &str, path: &str) -> String {
138            let path = if path.is_empty() { "/" } else { path };
139            debug_assert!(prefix.starts_with('/'));
140
141            if prefix.ends_with('/') {
142                format!("{prefix}{}", path.trim_start_matches('/'))
143            } else if path == "/" {
144                prefix.into()
145            } else {
146                format!("{prefix}{path}")
147            }
148        }
149
150        let api = self.0.nest_with_path_composer(
151            path_for_nested_route(path, "/"),
152            router.0,
153            path_for_nested_route,
154        );
155
156        Self(api)
157    }
158
159    /// Merge [`utoipa::openapi::path::Paths`] from `router` to `self` and merge [`Router`] routes
160    /// and fallback with [`axum::Router::merge`].
161    ///
162    /// This method expects [`OpenApiRouter`] instance in order to merge OpenApi paths and router
163    /// routes. If you wish to use [`axum::Router::merge`] you need to first convert this instance
164    /// to [`axum::Router`] _(`let _: Router = OpenApiRouter::new().into()`)_.
165    ///
166    /// # Examples
167    ///
168    /// _**Merge two routers.**_
169    /// ```rust
170    /// # use utoipa_axum::{routes, PathItemExt, router::OpenApiRouter};
171    /// #[utoipa::path(get, path = "/search")]
172    /// async fn search() {}
173    ///
174    /// let search_router = OpenApiRouter::new()
175    ///     .routes(utoipa_axum::routes!(search));
176    ///
177    /// let router: OpenApiRouter = OpenApiRouter::new()
178    ///     .merge(search_router);
179    /// ```
180    pub fn merge(mut self, router: OpenApiRouter) -> Self {
181        self.0.merge(router.0);
182
183        Self(self.0)
184    }
185
186    /// Consume `self` returning the [`utoipa::openapi::OpenApi`] instance of the
187    /// [`OpenApiRouter`].
188    pub fn into_openapi(self) -> utoipa::openapi::OpenApi {
189        self.0
190    }
191
192    /// Take the [`utoipa::openapi::OpenApi`] instance without consuming the [`OpenApiRouter`].
193    pub fn to_openapi(&mut self) -> utoipa::openapi::OpenApi {
194        std::mem::take(&mut self.0)
195    }
196
197    /// Get reference to the [`utoipa::openapi::OpenApi`] instance of the router.
198    pub fn get_openapi(&self) -> &utoipa::openapi::OpenApi {
199        &self.0
200    }
201
202    /// Get mutable reference to the [`utoipa::openapi::OpenApi`] instance of the router.
203    pub fn get_openapi_mut(&mut self) -> &mut utoipa::openapi::OpenApi {
204        &mut self.0
205    }
206}
207
208impl Default for OpenApiRouter {
209    fn default() -> Self {
210        Self::with_openapi(utoipa::openapi::OpenApiBuilder::new().build())
211    }
212}
213
214/// not
215#[macro_export]
216macro_rules! routes {
217    ( $handler:path $(, $tail:path)* $(,)? ) => {
218        {
219            let mut paths = utoipa::openapi::path::Paths::new();
220            let mut schemas = Vec::<(String, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>)>::new();
221            let (path, item, types) = $crate::routes!(@resolve_types $handler : schemas);
222
223            paths.add_path_operation(&path, types, item);
224
225            (schemas, paths)
226        }
227    };
228    ( @resolve_types $handler:path : $schemas:tt ) => {
229        {
230            $crate::paste! {
231                let path = $crate::routes!( @path [path()] of $handler );
232                let mut operation = $crate::routes!( @path [operation()] of $handler );
233                let types = $crate::routes!( @path [methods()] of $handler );
234                let tags = $crate::routes!( @path [tags()] of $handler );
235                $crate::routes!( @path [schemas(&mut $schemas)] of $handler );
236                if !tags.is_empty() {
237                    let operation_tags = operation.tags.get_or_insert(Vec::new());
238                    operation_tags.extend(tags.iter().map(ToString::to_string));
239                }
240                (path, operation, types)
241            }
242        }
243    };
244    ( @path $op:tt of $part:ident $( :: $tt:tt )* ) => {
245        $crate::routes!( $op : [ $part $( $tt )*] )
246    };
247    ( $op:tt : [ $first:tt $( $rest:tt )* ] $( $rev:tt )* ) => {
248        $crate::routes!( $op : [ $( $rest )* ] $first $( $rev)* )
249    };
250    ( $op:tt : [] $first:tt $( $rest:tt )* ) => {
251        $crate::routes!( @inverse $op : $first $( $rest )* )
252    };
253    ( @inverse $op:tt : $tt:tt $( $rest:tt )* ) => {
254        $crate::routes!( @rev $op : $tt [$($rest)*] )
255    };
256    ( @rev $op:tt : $tt:tt [ $first:tt $( $rest:tt)* ] $( $reversed:tt )* ) => {
257        $crate::routes!( @rev $op : $tt [ $( $rest )* ] $first $( $reversed )* )
258    };
259    ( @rev [$op:ident $( $args:tt )* ] : $handler:tt [] $($tt:tt)* ) => {
260        {
261            #[allow(unused_imports)]
262            use utoipa::{Path, __dev::{Tags, SchemaReferences}};
263            $crate::paste! {
264                $( $tt :: )* [<__path_ $handler>]::$op $( $args )*
265            }
266        }
267    };
268    ( ) => {};
269}