Skip to content

Commit

Permalink
Auto collect tuple responses schema references (#1071)
Browse files Browse the repository at this point in the history
Refactor Tuple responses parsing unifying it with request body parsing.
This allows reusing same components when serialized to tokens
making it less error prone and removing duplication.

This also removes the `content_type = [...]` array format from
`ToResponse` and `IntoResponses` derive types as well as from tuple
style responses. Same as with request bodies the multiple content
types need to defined with `content(...)` attribute.

Implement auto collect response schema references from tuple style
responses within `#[utoipa::path(...)]` attribute macro. Schema
references will be collected recursively in same manner as for request
bodies.

Example of supported syntax. The `User` will be automatically collected
to OpenApi when `get_user` path is registered to the `OpenApi`.
```rust
 #[derive(utoipa::ToSchema)]
 struct User {
     name: String,
 }

 #[utoipa::path(
     get,
     path = "/user",
     responses(
         (status = 200, body = User)
     )
 )]
 fn get_user() {}
```
  • Loading branch information
juhaku authored Oct 1, 2024
1 parent 8e5b818 commit 8d5149f
Show file tree
Hide file tree
Showing 11 changed files with 783 additions and 782 deletions.
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

0 comments on commit 8d5149f

Please sign in to comment.