Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto collect tuple responses schema references #1071

Merged
merged 1 commit into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion utoipa-gen/src/component/schema/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1005,7 +1005,7 @@ where

/// `RefOrOwned` is simple `Cow` like type to wrap either `ref` or owned value. This allows passing
/// either owned or referenced values as if they were owned like the `Cow` does but this works with
/// non clonable types. Thus values cannot be modified but they can be passed down as re-referenced
/// non cloneable types. Thus values cannot be modified but they can be passed down as re-referenced
/// values by dereffing the original value. `Roo::Ref(original.deref())`.
#[cfg_attr(feature = "debug", derive(Debug))]
pub enum Roo<'t, T> {
Expand Down
59 changes: 19 additions & 40 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1041,15 +1041,11 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// that free form _`ref`_ is accessible via OpenAPI doc or Swagger UI, users are responsible for making
/// these guarantees.
///
/// * `content_type = "..."` or `content_type = [...]` Can be used to override the default behavior of auto resolving the content type
/// from the `body` attribute. If defined the value should be valid content type such as
/// _`application/json`_. By default the content type is _`text/plain`_ for
/// [primitive Rust types][primitive], `application/octet-stream` for _`[u8]`_ and
/// _`application/json`_ for struct and mixed 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.
/// * `content_type = "..."` Can be used to override the default behavior
/// of auto resolving the content type from the `body` attribute. If defined the value should be valid
/// content type such as _`application/json`_ . By default the content type is _`text/plain`_
/// for [primitive Rust types][primitive], `application/octet-stream` for _`[u8]`_ and _`application/json`_
/// for struct and mixed enum types.
///
/// * `headers(...)` Slice of response headers that are returned back to a caller.
///
Expand All @@ -1059,10 +1055,8 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// * `response = ...` Type what implements [`ToResponse`][to_response_trait] trait. This can alternatively be used to
/// define response attributes. _`response`_ attribute cannot co-exist with other than _`status`_ attribute.
///
/// * `content((...), (...))` Can be used to define multiple return types for single response status. Supported format for single
/// _content_ is `(content_type = response_body, example = "...", examples(...))`. _`example`_
/// and _`examples`_ are optional arguments. Examples attribute behaves exactly same way as in
/// the response and is mutually exclusive with the example attribute.
/// * `content((...), (...))` Can be used to define multiple return types for single response status. Supports same syntax as
/// [multiple request body content][`macro@path#multiple-request-body-content`].
///
/// * `examples(...)` Define multiple examples for single response. This attribute is mutually
/// exclusive to the _`example`_ attribute and if both are defined this will override the _`example`_.
Expand Down Expand Up @@ -1159,13 +1153,6 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// )
/// ```
///
/// **Response with multiple response content types:**
/// ```text
/// responses(
/// (status = 200, description = "Success response", body = Pet, content_type = ["application/json", "text/xml"])
/// )
/// ```
///
/// **Multiple response return types with _`content(...)`_ attribute:**
///
/// _**Define multiple response return types for single response status with their own example.**_
Expand Down Expand Up @@ -1628,8 +1615,8 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// path = "/user",
/// responses(
/// (status = 200, content(
/// ("application/vnd.user.v1+json" = User1, example = json!({"id": "id".to_string()})),
/// ("application/vnd.user.v2+json" = User2, example = json!({"id": 2}))
/// (User1 = "application/vnd.user.v1+json", example = json!({"id": "id".to_string()})),
/// (User2 = "application/vnd.user.v2+json", example = json!({"id": 2}))
/// )
/// )
/// )
Expand Down Expand Up @@ -2525,15 +2512,11 @@ pub fn into_params(input: TokenStream) -> TokenStream {
/// * `description = "..."` Define description for the response as str. This can be used to
/// override the default description resolved from doc comments if present.
///
/// * `content_type = "..." | content_type = [...]` Can be used to override the default behavior of auto resolving the content type
/// from the `body` attribute. If defined the value should be valid content type such as
/// _`application/json`_. By default the content type is _`text/plain`_ for
/// [primitive Rust types][primitive], `application/octet-stream` for _`[u8]`_ and
/// _`application/json`_ for struct and mixed 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.
/// * `content_type = "..."` Can be used to override the default behavior
/// of auto resolving the content type from the `body` attribute. If defined the value should be valid
/// content type such as _`application/json`_ . By default the content type is _`text/plain`_
/// for [primitive Rust types][primitive], `application/octet-stream` for _`[u8]`_ and _`application/json`_
/// for struct and mixed enum types.
///
/// * `headers(...)` Slice of response headers that are returned back to a caller.
///
Expand Down Expand Up @@ -2692,15 +2675,11 @@ pub fn to_response(input: TokenStream) -> TokenStream {
/// * `description = "..."` Define description for the response as str. This can be used to
/// override the default description resolved from doc comments if present.
///
/// * `content_type = "..." | content_type = [...]` Can be used to override the default behavior of auto resolving the content type
/// from the `body` attribute. If defined the value should be valid content type such as
/// _`application/json`_. By default the content type is _`text/plain`_ for
/// [primitive Rust types][primitive], `application/octet-stream` for _`[u8]`_ and
/// _`application/json`_ for struct and mixed 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.
/// * `content_type = "..."` Can be used to override the default behavior
/// of auto resolving the content type from the `body` attribute. If defined the value should be valid
/// content type such as _`application/json`_ . By default the content type is _`text/plain`_
/// for [primitive Rust types][primitive], `application/octet-stream` for _`[u8]`_ and _`application/json`_
/// for struct and mixed enum types.
///
/// * `headers(...)` Slice of response headers that are returned back to a caller.
///
Expand Down
147 changes: 33 additions & 114 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use quote::{quote, quote_spanned, ToTokens};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::{Comma, Paren};
use syn::token::Comma;
use syn::{parenthesized, parse::Parse, Token};
use syn::{Expr, ExprLit, Lit, LitStr, Type};
use syn::{Expr, ExprLit, Lit, LitStr};

use crate::component::{GenericType, TypeTree};
use crate::component::{ComponentSchema, GenericType, TypeTree};
use crate::{
as_tokens_or_diagnostics, parse_utils, Deprecated, Diagnostics, OptionExt, ToTokensDiagnostics,
};
Expand Down Expand Up @@ -472,25 +472,40 @@ impl<'p> ToTokensDiagnostics for Path<'p> {
};
let operation = as_tokens_or_diagnostics!(&operation);

fn to_schema_references(
mut schemas: TokenStream2,
component_schema: ComponentSchema,
) -> TokenStream2 {
for reference in component_schema.schema_references {
let name = &reference.name;
let tokens = &reference.tokens;
let references = &reference.references;

schemas.extend(quote!( schemas.push((#name, #tokens)); ));
schemas.extend(quote!( #references; ));
}

schemas
}

let response_schemas = self
.path_attr
.responses
.iter()
.map(|response| response.get_component_schemas())
.collect::<Result<Vec<_>, Diagnostics>>()?
.into_iter()
.flatten()
.fold(TokenStream2::new(), to_schema_references);

let schemas = self
.path_attr
.request_body
.as_ref()
.map_try(|request_body| request_body.get_component_schemas())?
.into_iter()
.flatten()
.fold(TokenStream2::new(), |mut schemas, component_schema| {
for reference in component_schema.schema_references {
let name = &reference.name;
let tokens = &reference.tokens;
let references = &reference.references;

schemas.extend(quote!( schemas.push((#name, #tokens)); ));
schemas.extend(quote!( #references; ));
}

schemas
});
.fold(TokenStream2::new(), to_schema_references);

let mut tags = self.path_attr.tags.clone();
if let Some(tag) = self.path_attr.tag.as_ref() {
Expand Down Expand Up @@ -538,6 +553,7 @@ impl<'p> ToTokensDiagnostics for Path<'p> {
impl utoipa::__dev::SchemaReferences for #impl_for {
fn schemas(schemas: &mut Vec<(String, utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>)>) {
#schemas
#response_schemas
}
}

Expand Down Expand Up @@ -651,82 +667,10 @@ impl ToTokens for Summary<'_> {
}
}

/// Represents either `ref("...")` or `Type` that can be optionally inlined with `inline(Type)`.
#[cfg_attr(feature = "debug", derive(Debug))]
enum PathType<'p> {
Ref(String),
MediaType(InlineType<'p>),
InlineSchema(TokenStream2, Type),
}

impl Parse for PathType<'_> {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let fork = input.fork();
let is_ref = if (fork.parse::<Option<Token![ref]>>()?).is_some() {
fork.peek(Paren)
} else {
false
};

if is_ref {
input.parse::<Token![ref]>()?;
let ref_stream;
parenthesized!(ref_stream in input);
Ok(Self::Ref(ref_stream.parse::<LitStr>()?.value()))
} else {
Ok(Self::MediaType(input.parse()?))
}
}
}

// inline(syn::Type) | syn::Type
#[cfg_attr(feature = "debug", derive(Debug))]
struct InlineType<'i> {
ty: Cow<'i, Type>,
is_inline: bool,
}

impl InlineType<'_> {
/// Get's the underlying [`syn::Type`] as [`TypeTree`].
fn as_type_tree(&self) -> Result<TypeTree, Diagnostics> {
TypeTree::from_type(&self.ty)
}
}

impl Parse for InlineType<'_> {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let fork = input.fork();
let is_inline = if let Some(ident) = fork.parse::<Option<Ident>>()? {
ident == "inline" && fork.peek(Paren)
} else {
false
};

let ty = if is_inline {
input.parse::<Ident>()?;
let inlined;
parenthesized!(inlined in input);

inlined.parse::<Type>()?
} else {
input.parse::<Type>()?
};

Ok(InlineType {
ty: Cow::Owned(ty),
is_inline,
})
}
}

pub trait PathTypeTree {
/// Resolve default content type based on current [`Type`].
fn get_default_content_type(&self) -> Cow<'static, str>;

#[allow(unused)]
/// Check whether [`TypeTree`] an option
fn is_option(&self) -> bool;

/// Check whether [`TypeTree`] is a Vec, slice, array or other supported array type
fn is_array(&self) -> bool;
}
Expand Down Expand Up @@ -769,11 +713,6 @@ impl<'p> PathTypeTree for TypeTree<'p> {
}
}

/// Check whether [`TypeTree`] an option
fn is_option(&self) -> bool {
matches!(self.generic_type, Some(GenericType::Option))
}

/// Check whether [`TypeTree`] is a Vec, slice, array or other supported array type
fn is_array(&self) -> bool {
match self.generic_type {
Expand All @@ -792,8 +731,8 @@ impl<'p> PathTypeTree for TypeTree<'p> {
mod parse {
use syn::parse::ParseStream;
use syn::punctuated::Punctuated;
use syn::token::{Bracket, Comma};
use syn::{bracketed, Result};
use syn::token::Comma;
use syn::Result;

use crate::path::example::Example;
use crate::{parse_utils, AnyValue};
Expand All @@ -803,26 +742,6 @@ mod parse {
parse_utils::parse_next_literal_str_or_expr(input)
}

#[inline]
pub(super) fn content_type(input: ParseStream) -> Result<Vec<parse_utils::LitStrOrExpr>> {
parse_utils::parse_next(input, || {
let look_content_type = input.lookahead1();
if look_content_type.peek(Bracket) {
let content_types;
bracketed!(content_types in input);
Ok(
Punctuated::<parse_utils::LitStrOrExpr, Comma>::parse_terminated(
&content_types,
)?
.into_iter()
.collect(),
)
} else {
Ok(vec![input.parse::<parse_utils::LitStrOrExpr>()?])
}
})
}

#[inline]
pub(super) fn example(input: ParseStream) -> Result<AnyValue> {
parse_utils::parse_next(input, || AnyValue::parse_lit_str_or_json(input))
Expand Down
Loading
Loading