pub use paste::paste;
pub use utoipa::{
openapi, {OpenApi, Path, ToSchema},
};
pub type UtoipaMethodRouter = (
Vec<(
String,
utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
)>,
utoipa::openapi::path::Paths,
);
#[derive(Clone)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct OpenApiRouter(utoipa::openapi::OpenApi);
impl OpenApiRouter {
pub fn new() -> OpenApiRouter {
use utoipa::OpenApi;
#[derive(OpenApi)]
struct Api;
Self::with_openapi(Api::openapi())
}
pub fn with_openapi(openapi: utoipa::openapi::OpenApi) -> Self {
Self(openapi)
}
pub fn routes(mut self, (schemas, paths): UtoipaMethodRouter) -> Self {
for (path, item) in paths.paths {
if let Some(it) = self.0.paths.paths.get_mut(&path) {
it.merge_operations(item);
} else {
self.0.paths.paths.insert(path, item);
}
}
let components = self
.0
.components
.get_or_insert(utoipa::openapi::Components::new());
components.schemas.extend(schemas);
Self(self.0)
}
pub fn nest(self, path: &str, router: OpenApiRouter) -> Self {
fn path_for_nested_route(prefix: &str, path: &str) -> String {
let path = if path.is_empty() { "/" } else { path };
debug_assert!(prefix.starts_with('/'));
if prefix.ends_with('/') {
format!("{prefix}{}", path.trim_start_matches('/'))
} else if path == "/" {
prefix.into()
} else {
format!("{prefix}{path}")
}
}
let api = self.0.nest_with_path_composer(
path_for_nested_route(path, "/"),
router.0,
path_for_nested_route,
);
Self(api)
}
pub fn merge(mut self, router: OpenApiRouter) -> Self {
self.0.merge(router.0);
Self(self.0)
}
pub fn into_openapi(self) -> utoipa::openapi::OpenApi {
self.0
}
pub fn to_openapi(&mut self) -> utoipa::openapi::OpenApi {
std::mem::take(&mut self.0)
}
pub fn get_openapi(&self) -> &utoipa::openapi::OpenApi {
&self.0
}
pub fn get_openapi_mut(&mut self) -> &mut utoipa::openapi::OpenApi {
&mut self.0
}
}
impl Default for OpenApiRouter {
fn default() -> Self {
Self::with_openapi(utoipa::openapi::OpenApiBuilder::new().build())
}
}
#[macro_export]
macro_rules! routes {
( $handler:path $(, $tail:path)* $(,)? ) => {
{
let mut paths = utoipa::openapi::path::Paths::new();
let mut schemas = Vec::<(String, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>)>::new();
let (path, item, types) = $crate::routes!(@resolve_types $handler : schemas);
paths.add_path_operation(&path, types, item);
(schemas, paths)
}
};
( @resolve_types $handler:path : $schemas:tt ) => {
{
$crate::paste! {
let path = $crate::routes!( @path [path()] of $handler );
let mut operation = $crate::routes!( @path [operation()] of $handler );
let types = $crate::routes!( @path [methods()] of $handler );
let tags = $crate::routes!( @path [tags()] of $handler );
$crate::routes!( @path [schemas(&mut $schemas)] of $handler );
if !tags.is_empty() {
let operation_tags = operation.tags.get_or_insert(Vec::new());
operation_tags.extend(tags.iter().map(ToString::to_string));
}
(path, operation, types)
}
}
};
( @path $op:tt of $part:ident $( :: $tt:tt )* ) => {
$crate::routes!( $op : [ $part $( $tt )*] )
};
( $op:tt : [ $first:tt $( $rest:tt )* ] $( $rev:tt )* ) => {
$crate::routes!( $op : [ $( $rest )* ] $first $( $rev)* )
};
( $op:tt : [] $first:tt $( $rest:tt )* ) => {
$crate::routes!( @inverse $op : $first $( $rest )* )
};
( @inverse $op:tt : $tt:tt $( $rest:tt )* ) => {
$crate::routes!( @rev $op : $tt [$($rest)*] )
};
( @rev $op:tt : $tt:tt [ $first:tt $( $rest:tt)* ] $( $reversed:tt )* ) => {
$crate::routes!( @rev $op : $tt [ $( $rest )* ] $first $( $reversed )* )
};
( @rev [$op:ident $( $args:tt )* ] : $handler:tt [] $($tt:tt)* ) => {
{
#[allow(unused_imports)]
use utoipa::{Path, __dev::{Tags, SchemaReferences}};
$crate::paste! {
$( $tt :: )* [<__path_ $handler>]::$op $( $args )*
}
}
};
( ) => {};
}