Skip to content

Commit

Permalink
Support merge in OpenApi derive
Browse files Browse the repository at this point in the history
  • Loading branch information
mmoreiradj committed Jan 30, 2025
1 parent 88a5842 commit 9981347
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 0 deletions.
101 changes: 101 additions & 0 deletions utoipa-gen/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub struct OpenApiAttr<'o> {
external_docs: Option<ExternalDocs>,
servers: Punctuated<Server, Comma>,
nested: Vec<NestOpenApi>,
merged: Vec<MergeOpenApi>,
}

impl<'o> OpenApiAttr<'o> {
Expand Down Expand Up @@ -130,6 +131,11 @@ impl Parse for OpenApiAttr<'_> {
parenthesized!(nest in input);
openapi.nested = parse_utils::parse_groups_collect(&nest)?;
}
"merge" => {
let merge;
parenthesized!(merge in input);
openapi.merged = parse_utils::parse_groups_collect(&merge)?;
}
_ => {
return Err(Error::new(ident.span(), EXPECTED_ATTRIBUTE));
}
Expand Down Expand Up @@ -469,6 +475,50 @@ impl OpenApi<'_> {
Some(nest_tokens)
}
}

fn merged_tokens(&self) -> Option<TokenStream> {
let merged = self.0.as_ref().map(|openapi| &openapi.merged)?;
let merge_tokens = merged
.iter()
.map(|item| {
let merge_api = &item
.open_api
.as_ref()
.expect("type path of merged api is mandatory");
let merge_api_ident = &merge_api
.path
.segments
.last()
.expect("merge api must have at least one segment")
.ident;
let merge_api_config = format_ident!("{}MergeConfig", merge_api_ident.to_string());

let tags = &item.tags.iter().collect::<Array<_>>();

let span = merge_api.span();
quote_spanned! {span=>
.merge_from({
#[allow(non_camel_case_types)]
struct #merge_api_config;
impl utoipa::__dev::NestedApiConfig for #merge_api_config {
fn config() -> (utoipa::openapi::OpenApi, Vec<&'static str>, &'static str) {
let api = <#merge_api as utoipa::OpenApi>::openapi();

(api, #tags.into(), "")
}
}
<#merge_api_config as utoipa::OpenApi>::openapi()
})
}
})
.collect::<TokenStream>();

if merge_tokens.is_empty() {
None
} else {
Some(merge_tokens)
}
}
}

impl ToTokensDiagnostics for OpenApi<'_> {
Expand Down Expand Up @@ -557,6 +607,11 @@ impl ToTokensDiagnostics for OpenApi<'_> {
let nested_tokens = self
.nested_tokens()
.map(|tokens| quote! {openapi = openapi #tokens;});

let merged_tokens = self
.merged_tokens()
.map(|tokens| quote! {openapi = openapi #tokens;});

tokens.extend(quote! {
impl utoipa::OpenApi for #ident {
fn openapi() -> utoipa::openapi::OpenApi {
Expand All @@ -575,6 +630,7 @@ impl ToTokensDiagnostics for OpenApi<'_> {
#handler_schemas
components.schemas.extend(schemas);
#nested_tokens
#merged_tokens

#modifiers_tokens

Expand Down Expand Up @@ -827,3 +883,48 @@ impl Parse for NestOpenApi {
Ok(nest)
}
}

#[cfg_attr(feature = "debug", derive(Debug))]
#[derive(Default)]
struct MergeOpenApi {
open_api: Option<TypePath>,
tags: Punctuated<parse_utils::LitStrOrExpr, Comma>,
}

impl Parse for MergeOpenApi {
fn parse(input: ParseStream) -> syn::Result<Self> {
const ERROR_MESSAGE: &str = "unexpected identifier, expected any of: api, tags";
let mut merge = MergeOpenApi::default();

while !input.is_empty() {
let ident = input.parse::<Ident>().map_err(|error| {
syn::Error::new(error.span(), format!("{ERROR_MESSAGE}: {error}"))
})?;

match &*ident.to_string() {
"api" => merge.open_api = Some(parse_utils::parse_next(input, || input.parse())?),
"tags" => {
merge.tags = parse_utils::parse_next(input, || {
let tags;
bracketed!(tags in input);
Punctuated::parse_terminated(&tags)
})?;
}
_ => return Err(syn::Error::new(ident.span(), ERROR_MESSAGE)),
}

if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}

if merge.open_api.is_none() {
return Err(syn::Error::new(
input.span(),
"`api = ...` argument is mandatory for merge(...) statement",
));
}

Ok(merge)
}
}
54 changes: 54 additions & 0 deletions utoipa-gen/tests/openapi_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,60 @@ fn derive_nest_openapi_with_tags() {
assert_json_snapshot!(paths);
}

#[test]
fn derive_merge_openapi_with_tags() {
mod one {
use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(paths(api_one_handler))]
pub struct OneApi;

#[utoipa::path(get, path = "/api/v1/one")]
#[allow(dead_code)]
fn api_one_handler() {}
}

mod two {
use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(paths(api_two_handler))]
pub struct TwoApi;

#[utoipa::path(get, path = "/api/v1/two")]
#[allow(dead_code)]
fn api_two_handler() {}
}

mod three {
use utoipa::OpenApi;

#[derive(OpenApi)]
#[openapi(paths(api_three_handler))]
pub struct ThreeApi;

#[utoipa::path(get, path = "/api/v1/three")]
#[allow(dead_code)]
fn api_three_handler() {}
}

#[derive(OpenApi)]
#[openapi(
merge(
(api = one::OneApi, tags = ["one"]),
(api = two::TwoApi, tags = ["two"]),
(api = three::ThreeApi)
)
)]
struct ApiDoc;

let api = serde_json::to_value(ApiDoc::openapi()).expect("should serialize to value");
let paths = api.pointer("/paths");

assert_json_snapshot!(paths);
}

#[test]
fn openapi_schemas_resolve_generic_enum_schema() {
#![allow(dead_code)]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
source: utoipa-gen/tests/openapi_derive.rs
expression: paths
---
{
"/api/v1/one": {
"get": {
"operationId": "api_one_handler",
"responses": {},
"tags": [
"one"
]
}
},
"/api/v1/three": {
"get": {
"operationId": "api_three_handler",
"responses": {},
"tags": []
}
},
"/api/v1/two": {
"get": {
"operationId": "api_two_handler",
"responses": {},
"tags": [
"two"
]
}
}
}

0 comments on commit 9981347

Please sign in to comment.