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}