Skip to content

Commit

Permalink
Fix default tag logic for paths (#1002)
Browse files Browse the repository at this point in the history
The default tag for paths is resolved from the module path of the handler
or `OpenApi` if the type in question is a nested `OpenApi` document. This
was supposed to be only the case when no additional tags are provided for the
paths. However it was resolved in all cases and there was no way to
"not to use" module path as a tag. This commit fixes this and the
module path will be only used if there are no other tags defined.

This commit also changes the old behavior where _`crate`_ was used as
a tag in case there was no module path available. From now on if there is
no module path there will be no default tag added to any of the paths.
This will result the paths to be rendered under _`default`_ tag in a
Swagger UI.

Fixes #978
  • Loading branch information
juhaku authored Aug 24, 2024
1 parent a5695f0 commit f2a7143
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 66 deletions.
31 changes: 15 additions & 16 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ use self::{
/// This attribute requires that a `tag` is present, otherwise serde will trigger a compile-time
/// failure.
/// * `untagged` Supported at the container level. Allows [untagged
/// enum representation](https://serde.rs/enum-representations.html#untagged).
/// enum representation](https://serde.rs/enum-representations.html#untagged).
/// * `default` Supported at the container level and field level according to [serde attributes].
/// * `deny_unknown_fields` Supported at the container level.
/// * `flatten` Supported at the field level.
Expand Down Expand Up @@ -725,11 +725,11 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// **context_path** can become handy to alter the path.
///
/// * `tag = "..."` Can be used to group operations. Operations with same tag are grouped together. By default
/// this is derived from the handler that is given to [`OpenApi`][openapi]. If derive results empty str
/// then default value _`crate`_ is used instead.
/// this is derived from the module path of the handler that is given to [`OpenApi`][openapi].
///
/// * `tags = ["tag1", ...]` Can be used to group operations. Operations with same tag are grouped
/// toghether. Tags attribute can be used to add addtional _tags_ for the operation.
/// toghether. Tags attribute can be used to add addtional _tags_ for the operation. If both
/// _`tag`_ and _`tags`_ are provided then they will be combined to a single _`tags`_ array.
///
/// * `request_body = ... | request_body(...)` Defining request body indicates that the request is expecting request body within
/// the performed request.
Expand Down Expand Up @@ -806,9 +806,9 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// [primitive Rust types][primitive], `application/octet-stream` for _`[u8]`_ and
/// _`application/json`_ for struct and complex enum types.
/// Content type can also be slice of **content_type** values if the endpoint support returning multiple
/// response content types. E.g _`["application/json", "text/xml"]`_ would indicate that endpoint can return both
/// _`json`_ and _`xml`_ formats. **The order** of the content types define the default example show first in
/// the Swagger UI. Swagger UI will use the first _`content_type`_ value as a default example.
/// response content types. E.g _`["application/json", "text/xml"]`_ would indicate that endpoint can return both
/// _`json`_ and _`xml`_ formats. **The order** of the content types define the default example show first in
/// the Swagger UI. Swagger UI will use the first _`content_type`_ value as a default example.
///
/// * `headers(...)` Slice of response headers that are returned back to a caller.
///
Expand Down Expand Up @@ -1141,7 +1141,7 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// 1. It allows users to use tuple style path parameters e.g. _`Path((id, name)): Path<(i32, String)>`_ and resolves
/// parameter names and types from it.
/// 2. It enhances [`IntoParams` derive][into_params_derive] functionality by automatically resolving _`parameter_in`_ from
/// _`Path<...>`_ or _`Query<...>`_ handler function arguments.
/// _`Path<...>`_ or _`Query<...>`_ handler function arguments.
///
/// _**Resole path argument types from tuple style handler arguments.**_
/// ```rust
Expand Down Expand Up @@ -1470,8 +1470,7 @@ pub fn path(attr: TokenStream, item: TokenStream) -> TokenStream {
/// * `components(schemas(...), responses(...))` Takes available _`component`_ configurations. Currently only
/// _`schema`_ and _`response`_ components are supported.
/// * `schemas(...)` List of [`ToSchema`][to_schema]s in OpenAPI schema.
/// * `responses(...)` List of types that implement
/// [`ToResponse`][to_response_trait].
/// * `responses(...)` List of types that implement [`ToResponse`][to_response_trait].
/// * `modifiers(...)` List of items implementing [`Modify`][modify] trait for runtime OpenApi modification.
/// See the [trait documentation][modify] for more details.
/// * `security(...)` List of [`SecurityRequirement`][security]s global to all operations.
Expand Down Expand Up @@ -2182,9 +2181,9 @@ pub fn into_params(input: TokenStream) -> TokenStream {
/// [primitive Rust types][primitive], `application/octet-stream` for _`[u8]`_ and
/// _`application/json`_ for struct and complex enum types.
/// Content type can also be slice of **content_type** values if the endpoint support returning multiple
/// response content types. E.g _`["application/json", "text/xml"]`_ would indicate that endpoint can return both
/// _`json`_ and _`xml`_ formats. **The order** of the content types define the default example show first in
/// the Swagger UI. Swagger UI will use the first _`content_type`_ value as a default example.
/// response content types. E.g _`["application/json", "text/xml"]`_ would indicate that endpoint can return both
/// _`json`_ and _`xml`_ formats. **The order** of the content types define the default example show first in
/// the Swagger UI. Swagger UI will use the first _`content_type`_ value as a default example.
///
/// * `headers(...)` Slice of response headers that are returned back to a caller.
///
Expand Down Expand Up @@ -2349,9 +2348,9 @@ pub fn to_response(input: TokenStream) -> TokenStream {
/// [primitive Rust types][primitive], `application/octet-stream` for _`[u8]`_ and
/// _`application/json`_ for struct and complex enum types.
/// Content type can also be slice of **content_type** values if the endpoint support returning multiple
/// response content types. E.g _`["application/json", "text/xml"]`_ would indicate that endpoint can return both
/// _`json`_ and _`xml`_ formats. **The order** of the content types define the default example show first in
/// the Swagger UI. Swagger UI will use the first _`content_type`_ value as a default example.
/// response content types. E.g _`["application/json", "text/xml"]`_ would indicate that endpoint can return both
/// _`json`_ and _`xml`_ formats. **The order** of the content types define the default example show first in
/// the Swagger UI. Swagger UI will use the first _`content_type`_ value as a default example.
///
/// * `headers(...)` Slice of response headers that are returned back to a caller.
///
Expand Down
11 changes: 4 additions & 7 deletions utoipa-gen/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,16 +429,13 @@ impl OpenApi<'_> {
let span = nest_api.span();
quote_spanned! {span=>
.nest(#path, {
#[allow(non_camel_case_types)]
struct #nest_api_config;
impl utoipa::__dev::NestedApiConfig for #nest_api_config {
fn config() -> (utoipa::openapi::OpenApi, Vec<&'static str>) {
fn config() -> (utoipa::openapi::OpenApi, Vec<&'static str>, &'static str) {
let api = <#nest_api as utoipa::OpenApi>::openapi();
let mut tags: Vec<_> = #tags.into();
if !#module_path.is_empty() {
tags.push(#module_path);
}

(api, tags)
(api, #tags.into(), #module_path)
}
}
<#nest_api_config as utoipa::OpenApi>::openapi()
Expand Down Expand Up @@ -641,7 +638,7 @@ fn impl_paths(handler_paths: &Punctuated<ExprPath, Comma>) -> TokenStream {
let item = #usage::path_item();
let path = #usage::path();
let mut tags = <#usage as utoipa::__dev::Tags>::tags();
if !#tag.is_empty() {
if !#tag.is_empty() && tags.is_empty() {
tags.push(#tag);
}

Expand Down
8 changes: 3 additions & 5 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -445,11 +445,9 @@ impl<'p> ToTokensDiagnostics for Path<'p> {
let operation = as_tokens_or_diagnostics!(&operation);

let mut tags = self.path_attr.tags.clone();
match self.path_attr.tag.as_ref() {
Some(tag) if tags.is_empty() => {
tags.push(tag.clone());
}
_ => (),
if let Some(tag) = self.path_attr.tag.as_ref() {
// if defined tag is the first before the additional tags
tags.insert(0, tag.clone());
}
let tags_list = tags.into_iter().collect::<Array<_>>();

Expand Down
28 changes: 14 additions & 14 deletions utoipa-gen/tests/openapi_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ fn derive_openapi_with_security_requirement() {
))]
struct ApiDoc;

let doc_value = serde_json::to_value(&ApiDoc::openapi()).unwrap();
let doc_value = serde_json::to_value(ApiDoc::openapi()).unwrap();

assert_value! {doc_value=>
"security.[0]" = "{}", "Optional security requirement"
Expand All @@ -45,7 +45,7 @@ fn derive_openapi_tags() {
))]
struct ApiDoc;

let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();
let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();

assert_value! {doc=>
"tags.[0].name" = r###""random::api""###, "Tags random_api name"
Expand All @@ -66,7 +66,7 @@ fn derive_openapi_tags_include_str() {
))]
struct ApiDoc;

let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();
let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();

assert_value! {doc=>
"tags.[0].name" = r###""random::api""###, "Tags random_api name"
Expand All @@ -83,7 +83,7 @@ fn derive_openapi_with_external_docs() {
))]
struct ApiDoc;

let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();
let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();

assert_value! {doc=>
"externalDocs.url" = r###""http://localhost.more.about.api""###, "External docs url"
Expand All @@ -97,7 +97,7 @@ fn derive_openapi_with_external_docs_only_url() {
#[openapi(external_docs(url = "http://localhost.more.about.api"))]
struct ApiDoc;

let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();
let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();

assert_value! {doc=>
"externalDocs.url" = r###""http://localhost.more.about.api""###, "External docs url"
Expand All @@ -121,7 +121,7 @@ fn derive_openapi_with_components_in_different_module() {
#[openapi(components(schemas(custom::Todo)))]
struct ApiDoc;

let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();
let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
let todo = doc.pointer("/components/schemas/Todo").unwrap();

assert_ne!(
Expand Down Expand Up @@ -149,7 +149,7 @@ fn derive_openapi_with_responses() {
#[openapi(components(responses(MyResponse)))]
struct ApiDoc;

let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap();
let doc = serde_json::to_value(ApiDoc::openapi()).unwrap();
let responses = doc.pointer("/components/responses").unwrap();

assert_json_eq!(
Expand Down Expand Up @@ -178,7 +178,7 @@ fn derive_openapi_with_servers() {
)]
struct ApiDoc;

let value = serde_json::to_value(&ApiDoc::openapi()).unwrap();
let value = serde_json::to_value(ApiDoc::openapi()).unwrap();
let servers = value.pointer("/servers");

assert_json_eq!(
Expand Down Expand Up @@ -222,7 +222,7 @@ fn derive_openapi_with_custom_info() {
))]
struct ApiDoc;

let value = serde_json::to_value(&ApiDoc::openapi()).unwrap();
let value = serde_json::to_value(ApiDoc::openapi()).unwrap();
let info = value.pointer("/info");

assert_json_include!(
Expand Down Expand Up @@ -254,7 +254,7 @@ fn derive_openapi_with_include_str_description() {
))]
struct ApiDoc;

let value = serde_json::to_value(&ApiDoc::openapi()).unwrap();
let value = serde_json::to_value(ApiDoc::openapi()).unwrap();
let info = value.pointer("/info");

assert_json_include!(
Expand Down Expand Up @@ -438,7 +438,7 @@ fn derive_nest_openapi_with_tags() {
)]
struct ApiDoc;

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

assert_json_eq!(
Expand All @@ -448,7 +448,7 @@ fn derive_nest_openapi_with_tags() {
"get": {
"operationId": "foobar",
"responses": {},
"tags": [ "yeah", "wowow", "foobarapi" ]
"tags": [ "mytag", "yeah", "wowow", "foobarapi" ]
}
},
"/api/v1/foobar/another": {
Expand All @@ -469,14 +469,14 @@ fn derive_nest_openapi_with_tags() {
"get": {
"operationId": "test_path_status",
"responses": {},
"tags": [ "crate" ]
"tags": []
}
},
"/api/v1/user/test": {
"get": {
"operationId": "user_test_path",
"responses": {},
"tags": [ "user", TAG, "user_api" ]
"tags": [ "user", TAG ]
}
},
"/random": {
Expand Down
Loading

0 comments on commit f2a7143

Please sign in to comment.