1#![cfg_attr(doc_cfg, feature(doc_cfg))]
2#![warn(missing_docs)]
3#![warn(rustdoc::broken_intra_doc_links)]
4
5pub mod router;
48
49use axum::routing::MethodFilter;
50use utoipa::openapi::HttpMethod;
51
52pub trait PathItemExt {
55 fn to_method_filter(&self) -> MethodFilter;
59}
60
61impl PathItemExt for HttpMethod {
62 fn to_method_filter(&self) -> MethodFilter {
63 match self {
64 HttpMethod::Get => MethodFilter::GET,
65 HttpMethod::Put => MethodFilter::PUT,
66 HttpMethod::Post => MethodFilter::POST,
67 HttpMethod::Head => MethodFilter::HEAD,
68 HttpMethod::Patch => MethodFilter::PATCH,
69 HttpMethod::Trace => MethodFilter::TRACE,
70 HttpMethod::Delete => MethodFilter::DELETE,
71 HttpMethod::Options => MethodFilter::OPTIONS,
72 }
73 }
74}
75
76#[doc(hidden)]
78pub use paste::paste;
79
80#[macro_export]
125macro_rules! routes {
126 ( $handler:path $(, $tail:path)* $(,)? ) => {
127 {
128 use $crate::PathItemExt;
129 let mut paths = utoipa::openapi::path::Paths::new();
130 let mut schemas = Vec::<(String, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>)>::new();
131 let (path, item, types) = $crate::routes!(@resolve_types $handler : schemas);
132 #[allow(unused_mut)]
133 let mut method_router = types.iter().by_ref().fold(axum::routing::MethodRouter::new(), |router, path_type| {
134 router.on(path_type.to_method_filter(), $handler)
135 });
136 paths.add_path_operation(&path, types, item);
137 $( method_router = $crate::routes!( schemas: method_router: paths: $tail ); )*
138 (schemas, paths, method_router)
139 }
140 };
141 ( $schemas:tt: $router:ident: $paths:ident: $handler:path $(, $tail:tt)* ) => {
142 {
143 let (path, item, types) = $crate::routes!(@resolve_types $handler : $schemas);
144 let router = types.iter().by_ref().fold($router, |router, path_type| {
145 router.on(path_type.to_method_filter(), $handler)
146 });
147 $paths.add_path_operation(&path, types, item);
148 router
149 }
150 };
151 ( @resolve_types $handler:path : $schemas:tt ) => {
152 {
153 $crate::paste! {
154 let path = $crate::routes!( @path [path()] of $handler );
155 let mut operation = $crate::routes!( @path [operation()] of $handler );
156 let types = $crate::routes!( @path [methods()] of $handler );
157 let tags = $crate::routes!( @path [tags()] of $handler );
158 $crate::routes!( @path [schemas(&mut $schemas)] of $handler );
159 if !tags.is_empty() {
160 let operation_tags = operation.tags.get_or_insert(Vec::new());
161 operation_tags.extend(tags.iter().map(ToString::to_string));
162 }
163 (path, operation, types)
164 }
165 }
166 };
167 ( @path $op:tt of $part:ident $( :: $tt:tt )* ) => {
168 $crate::routes!( $op : [ $part $( $tt )*] )
169 };
170 ( $op:tt : [ $first:tt $( $rest:tt )* ] $( $rev:tt )* ) => {
171 $crate::routes!( $op : [ $( $rest )* ] $first $( $rev)* )
172 };
173 ( $op:tt : [] $first:tt $( $rest:tt )* ) => {
174 $crate::routes!( @inverse $op : $first $( $rest )* )
175 };
176 ( @inverse $op:tt : $tt:tt $( $rest:tt )* ) => {
177 $crate::routes!( @rev $op : $tt [$($rest)*] )
178 };
179 ( @rev $op:tt : $tt:tt [ $first:tt $( $rest:tt)* ] $( $reversed:tt )* ) => {
180 $crate::routes!( @rev $op : $tt [ $( $rest )* ] $first $( $reversed )* )
181 };
182 ( @rev [$op:ident $( $args:tt )* ] : $handler:tt [] $($tt:tt)* ) => {
183 {
184 #[allow(unused_imports)]
185 use utoipa::{Path, __dev::{Tags, SchemaReferences}};
186 $crate::paste! {
187 $( $tt :: )* [<__path_ $handler>]::$op $( $args )*
188 }
189 }
190 };
191 ( ) => {};
192}
193
194#[cfg(test)]
195mod tests {
196 use std::collections::BTreeMap;
197
198 use super::*;
199 use axum::extract::{Path, State};
200 use insta::assert_json_snapshot;
201 use router::*;
202 use tower::util::ServiceExt;
203 use utoipa::openapi::{Content, OpenApi, Ref, ResponseBuilder};
204 use utoipa::PartialSchema;
205
206 #[utoipa::path(get, path = "/")]
207 async fn root() {}
208
209 #[utoipa::path(get, path = "/")]
212 async fn get_user() {}
213
214 #[utoipa::path(post, path = "/")]
215 async fn post_user() {}
216
217 #[utoipa::path(delete, path = "/")]
218 async fn delete_user() {}
219
220 #[utoipa::path(get, path = "/search")]
221 async fn search_user() {}
222
223 #[utoipa::path(get, path = "/")]
226 async fn get_customer() {}
227
228 #[utoipa::path(post, path = "/")]
229 async fn post_customer() {}
230
231 #[utoipa::path(delete, path = "/")]
232 async fn delete_customer() {}
233
234 #[utoipa::path(get, path = "/search")]
236 async fn search_customer(State(_s): State<String>) {}
237
238 #[test]
239 fn axum_router_nest_openapi_routes_compile() {
240 let user_router: OpenApiRouter = OpenApiRouter::new()
241 .routes(routes!(search_user))
242 .routes(routes!(get_user, post_user, delete_user));
243
244 let customer_router: OpenApiRouter = OpenApiRouter::new()
245 .routes(routes!(get_customer, post_customer, delete_customer))
246 .routes(routes!(search_customer))
247 .with_state(String::new());
248
249 let router = OpenApiRouter::new()
250 .nest("/api/user", user_router)
251 .nest("/api/customer", customer_router)
252 .route("/", axum::routing::get(root));
253
254 let _ = router.get_openapi();
255 }
256
257 #[test]
258 fn routes_with_trailing_comma_compiles() {
259 let _: OpenApiRouter =
260 OpenApiRouter::new().routes(routes!(get_user, post_user, delete_user,));
261 }
262
263 #[test]
264 fn openapi_router_with_openapi() {
265 use utoipa::OpenApi;
266
267 #[derive(utoipa::ToSchema)]
268 #[allow(unused)]
269 struct Todo {
270 id: i32,
271 }
272 #[derive(utoipa::OpenApi)]
273 #[openapi(components(schemas(Todo)))]
274 struct Api;
275
276 let mut router: OpenApiRouter = OpenApiRouter::with_openapi(Api::openapi())
277 .routes(routes!(search_user))
278 .routes(routes!(get_user));
279
280 let paths = router.to_openapi().paths;
281 let expected_paths = utoipa::openapi::path::PathsBuilder::new()
282 .path(
283 "/",
284 utoipa::openapi::PathItem::new(
285 utoipa::openapi::path::HttpMethod::Get,
286 utoipa::openapi::path::OperationBuilder::new().operation_id(Some("get_user")),
287 ),
288 )
289 .path(
290 "/search",
291 utoipa::openapi::PathItem::new(
292 utoipa::openapi::path::HttpMethod::Get,
293 utoipa::openapi::path::OperationBuilder::new()
294 .operation_id(Some("search_user")),
295 ),
296 );
297 assert_eq!(expected_paths.build(), paths);
298 }
299
300 #[test]
301 fn openapi_router_nest_openapi() {
302 use utoipa::OpenApi;
303
304 #[derive(utoipa::ToSchema)]
305 #[allow(unused)]
306 struct Todo {
307 id: i32,
308 }
309 #[derive(utoipa::OpenApi)]
310 #[openapi(components(schemas(Todo)))]
311 struct Api;
312
313 let router: router::OpenApiRouter =
314 router::OpenApiRouter::with_openapi(Api::openapi()).routes(routes!(search_user));
315
316 let customer_router: router::OpenApiRouter = router::OpenApiRouter::new()
317 .routes(routes!(get_customer))
318 .with_state(String::new());
319
320 let mut router = router.nest("/api/customer", customer_router);
321 let paths = router.to_openapi().paths;
322 let expected_paths = utoipa::openapi::path::PathsBuilder::new()
323 .path(
324 "/api/customer",
325 utoipa::openapi::PathItem::new(
326 utoipa::openapi::path::HttpMethod::Get,
327 utoipa::openapi::path::OperationBuilder::new()
328 .operation_id(Some("get_customer")),
329 ),
330 )
331 .path(
332 "/search",
333 utoipa::openapi::PathItem::new(
334 utoipa::openapi::path::HttpMethod::Get,
335 utoipa::openapi::path::OperationBuilder::new()
336 .operation_id(Some("search_user")),
337 ),
338 );
339 assert_eq!(expected_paths.build(), paths);
340 }
341
342 #[test]
343 fn openapi_with_auto_collected_schemas() {
344 #[derive(utoipa::ToSchema)]
345 #[allow(unused)]
346 struct Todo {
347 id: i32,
348 }
349
350 #[utoipa::path(get, path = "/todo", responses((status = 200, body = Todo)))]
351 async fn get_todo() {}
352
353 let mut router: router::OpenApiRouter =
354 router::OpenApiRouter::new().routes(routes!(get_todo));
355
356 let openapi = router.to_openapi();
357 let paths = openapi.paths;
358 let schemas = openapi
359 .components
360 .expect("Router must have auto collected schemas")
361 .schemas;
362
363 let expected_paths = utoipa::openapi::path::PathsBuilder::new().path(
364 "/todo",
365 utoipa::openapi::PathItem::new(
366 utoipa::openapi::path::HttpMethod::Get,
367 utoipa::openapi::path::OperationBuilder::new()
368 .operation_id(Some("get_todo"))
369 .response(
370 "200",
371 ResponseBuilder::new().content(
372 "application/json",
373 Content::builder()
374 .schema(Some(Ref::from_schema_name("Todo")))
375 .build(),
376 ),
377 ),
378 ),
379 );
380 let expected_schemas =
381 BTreeMap::from_iter(std::iter::once(("Todo".to_string(), Todo::schema())));
382 assert_eq!(expected_paths.build(), paths);
383 assert_eq!(expected_schemas, schemas);
384 }
385
386 mod pets {
387
388 #[utoipa::path(get, path = "/")]
389 pub async fn get_pet() {}
390
391 #[utoipa::path(post, path = "/")]
392 pub async fn post_pet() {}
393
394 #[utoipa::path(delete, path = "/")]
395 pub async fn delete_pet() {}
396 }
397
398 #[test]
399 fn openapi_routes_from_another_path() {
400 let mut router: OpenApiRouter =
401 OpenApiRouter::new().routes(routes!(pets::get_pet, pets::post_pet, pets::delete_pet));
402 let paths = router.to_openapi().paths;
403
404 let expected_paths = utoipa::openapi::path::PathsBuilder::new()
405 .path(
406 "/",
407 utoipa::openapi::PathItem::new(
408 utoipa::openapi::path::HttpMethod::Get,
409 utoipa::openapi::path::OperationBuilder::new().operation_id(Some("get_pet")),
410 ),
411 )
412 .path(
413 "/",
414 utoipa::openapi::PathItem::new(
415 utoipa::openapi::path::HttpMethod::Post,
416 utoipa::openapi::path::OperationBuilder::new().operation_id(Some("post_pet")),
417 ),
418 )
419 .path(
420 "/",
421 utoipa::openapi::PathItem::new(
422 utoipa::openapi::path::HttpMethod::Delete,
423 utoipa::openapi::path::OperationBuilder::new().operation_id(Some("delete_pet")),
424 ),
425 );
426 assert_eq!(expected_paths.build(), paths);
427 }
428
429 #[tokio::test]
430 async fn test_axum_router() {
431 #[utoipa::path(
432 get,
433 path = "/pet/{pet_id}",
434 params(("pet_id" = u32, Path, description = "ID of pet to return")),
435 )]
436 pub async fn get_pet_by_id(Path(pet_id): Path<u32>) -> String {
437 pet_id.to_string()
438 }
439
440 let (router, openapi) = OpenApiRouter::with_openapi(OpenApi::default())
441 .routes(routes!(get_pet_by_id))
442 .split_for_parts();
443
444 assert_json_snapshot!(openapi);
445
446 let request = http::Request::builder()
447 .uri("/pet/1")
448 .body("".to_string())
449 .unwrap();
450
451 let response = router.clone().oneshot(request).await.unwrap();
452 assert_eq!(response.status(), http::StatusCode::OK);
453
454 let body = response.into_body();
455 let body = axum::body::to_bytes(body, usize::MAX).await.unwrap();
456 assert_eq!(body, "1");
457
458 let request = http::Request::builder()
459 .uri("/pet/foo")
460 .body("".to_string())
461 .unwrap();
462
463 let response = router.oneshot(request).await.unwrap();
464 assert_eq!(response.status(), http::StatusCode::BAD_REQUEST);
465 }
466}