ymir_openapi/
lib.rs

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    /// Construct a new [`Servable`] instance of _`openapi`_ with given _`url`_.
59    ///
60    /// * **url** Must point to location where the [`Servable`] is served.
61    /// * **openapi** Is [`Spec`] that is served via this [`Servable`] from the _**url**_.
62    fn with_url<U: Into<Cow<'static, str>>>(url: U, openapi: S) -> Self;
63}
64
65// #[cfg(any(feature = "axum"))]
66impl<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
86/// Extends [`utoipa::openapi::path::PathItem`] by providing conversion methods to convert this
87/// path item type to a [`axum::routing::MethodFilter`].
88pub trait PathItemExt {
89    /// Convert this path item type to a [`axum::routing::MethodFilter`].
90    ///
91    /// Method filter is used with handler registration on [`axum::routing::MethodRouter`].
92    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/// re-export paste so users do not need to add the dependency.
111#[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}