1pub mod prelude;
2pub mod router;
3
4use std::borrow::Cow;
5
6use axum::{
7 response::Html,
8 routing::{self, MethodFilter},
9 Router,
10};
11use serde::Serialize;
12use serde_json::Value;
13use utoipa::openapi::{HttpMethod, OpenApi};
14
15const DEFAULT_HTML: &str = include_str!("./assets/swagger.html");
16
17#[derive(Clone)]
18pub struct Swagger<S: Spec> {
19 #[allow(unused)]
20 url: Cow<'static, str>,
21 html: Cow<'static, str>,
22 openapi: S,
23}
24
25impl<S: Spec> Swagger<S> {
26 pub fn new(openapi: S) -> Self {
27 Self {
28 url: Cow::Borrowed(""),
29 html: Cow::Borrowed(DEFAULT_HTML),
30 openapi,
31 }
32 }
33
34 pub fn to_html(&self) -> String {
35 self.html.replace(
36 "$spec",
37 &serde_json::to_string(&self.openapi).expect(
38 "Invalid OpenAPI spec, expected OpenApi, String, &str or serde_json::Value",
39 ),
40 )
41 }
42}
43
44pub trait Spec: Serialize {}
45
46impl Spec for OpenApi {}
47
48impl Spec for String {}
49
50impl Spec for &str {}
51
52impl Spec for Value {}
53
54pub trait Servable<S>
55where
56 S: Spec,
57{
58 fn with_url<U: Into<Cow<'static, str>>>(url: U, openapi: S) -> Self;
63}
64
65impl<S: Spec> Servable<S> for Swagger<S> {
67 fn with_url<U: Into<Cow<'static, str>>>(url: U, openapi: S) -> Self {
68 Self {
69 url: url.into(),
70 openapi,
71 html: Cow::Borrowed(DEFAULT_HTML),
72 }
73 }
74}
75
76impl<S: Spec, R> From<Swagger<S>> for Router<R>
77where
78 R: Clone + Send + Sync + 'static,
79{
80 fn from(value: Swagger<S>) -> Self {
81 let html = value.to_html();
82 Router::<R>::new().route(&value.url.as_ref(), routing::get(|| async { Html(html) }))
83 }
84}
85
86pub trait PathItemExt {
89 fn to_method_filter(&self) -> MethodFilter;
93}
94
95impl PathItemExt for HttpMethod {
96 fn to_method_filter(&self) -> MethodFilter {
97 match self {
98 HttpMethod::Get => MethodFilter::GET,
99 HttpMethod::Put => MethodFilter::PUT,
100 HttpMethod::Post => MethodFilter::POST,
101 HttpMethod::Head => MethodFilter::HEAD,
102 HttpMethod::Patch => MethodFilter::PATCH,
103 HttpMethod::Trace => MethodFilter::TRACE,
104 HttpMethod::Delete => MethodFilter::DELETE,
105 HttpMethod::Options => MethodFilter::OPTIONS,
106 }
107 }
108}
109
110#[doc(hidden)]
112pub use paste::paste;
113
114#[macro_export]
115macro_rules! routes {
116 ( $handler:path $(, $tail:path)* ) => {
117 {
118 use $crate::PathItemExt;
119 let mut paths = utoipa::openapi::path::Paths::new();
120 let mut schemas = Vec::<(String, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>)>::new();
121 let (path, item, types) = $crate::routes!(@resolve_types $handler : schemas);
122 #[allow(unused_mut)]
123 let mut method_router = types.iter().by_ref().fold(axum::routing::MethodRouter::new(), |router, path_type| {
124 router.on(path_type.to_method_filter(), $handler)
125 });
126 paths.add_path_operation(&path, types, item);
127 $( method_router = $crate::routes!( schemas: method_router: paths: $tail ); )*
128 (schemas, paths, method_router)
129 }
130 };
131 ( $schemas:tt: $router:ident: $paths:ident: $handler:path $(, $tail:tt)* ) => {
132 {
133 let (path, item, types) = $crate::routes!(@resolve_types $handler : $schemas);
134 let router = types.iter().by_ref().fold($router, |router, path_type| {
135 router.on(path_type.to_method_filter(), $handler)
136 });
137 $paths.add_path_operation(&path, types, item);
138 router
139 }
140 };
141 ( @resolve_types $handler:path : $schemas:tt ) => {
142 {
143 $crate::paste! {
144 let path = $crate::routes!( @path [path()] of $handler );
145 let mut operation = $crate::routes!( @path [operation()] of $handler );
146 let types = $crate::routes!( @path [methods()] of $handler );
147 let tags = $crate::routes!( @path [tags()] of $handler );
148 $crate::routes!( @path [schemas(&mut $schemas)] of $handler );
149 if !tags.is_empty() {
150 let operation_tags = operation.tags.get_or_insert(Vec::new());
151 operation_tags.extend(tags.iter().map(ToString::to_string));
152 }
153 (path, operation, types)
154 }
155 }
156 };
157 ( @path $op:tt of $part:ident $( :: $tt:tt )* ) => {
158 $crate::routes!( $op : [ $part $( $tt )*] )
159 };
160 ( $op:tt : [ $first:tt $( $rest:tt )* ] $( $rev:tt )* ) => {
161 $crate::routes!( $op : [ $( $rest )* ] $first $( $rev)* )
162 };
163 ( $op:tt : [] $first:tt $( $rest:tt )* ) => {
164 $crate::routes!( @inverse $op : $first $( $rest )* )
165 };
166 ( @inverse $op:tt : $tt:tt $( $rest:tt )* ) => {
167 $crate::routes!( @rev $op : $tt [$($rest)*] )
168 };
169 ( @rev $op:tt : $tt:tt [ $first:tt $( $rest:tt)* ] $( $reversed:tt )* ) => {
170 $crate::routes!( @rev $op : $tt [ $( $rest )* ] $first $( $reversed )* )
171 };
172 ( @rev [$op:ident $( $args:tt )* ] : $handler:tt [] $($tt:tt)* ) => {
173 {
174 #[allow(unused_imports)]
175 use utoipa::{Path, __dev::{Tags, SchemaReferences}};
176 $crate::paste! {
177 $( $tt :: )* [<__path_ $handler>]::$op $( $args )*
178 }
179 }
180 };
181 ( ) => {};
182}