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

Add external ref(...) attribute #409

Merged
merged 1 commit into from
Dec 11, 2022
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
32 changes: 20 additions & 12 deletions utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,16 +588,21 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// # Request Body Attributes
///
/// **Simple format definition by `request_body = ...`**
/// * `request_body = Type` or `request_body = inline(Type)`. The given _`Type`_ can be any
/// Rust type that is JSON parseable. It can be Option, Vec or Map etc. With _`inline(...)`_
/// the schema will be inlined instead of a referenced which is the default for
/// [`ToSchema`][to_schema] types.
/// * _`request_body = Type`_, _`request_body = inline(Type)`_ or _`request_body = ref("...")`_.
/// The given _`Type`_ can be any Rust type that is JSON parseable. It can be Option, Vec or Map etc.
/// With _`inline(...)`_ the schema will be inlined instead of a referenced which is the default for
/// [`ToSchema`][to_schema] types. _`ref("./external.json")`_ can be used to reference external
/// json file for body schema. **Note!** Utoipa does **not** guarantee that free form _`ref`_ is accessbile via
/// OpenAPI doc or Swagger UI, users are eligible to make these guarantees.
///
/// **Advanced format definition by `request_body(...)`**
/// * `content = ...` Can be `content = Type` or `content = inline(Type)`. The given _`Type`_ can be any
/// Rust type that is JSON parseable. It can be Option, Vec or Map etc. With _`inline(...)`_
/// the schema will be inlined instead of a referenced which is the default for
/// [`ToSchema`][to_schema] types.
/// * `content = ...` Can be _`content = Type`_, _`content = inline(Type)`_ or _`content = ref("...")`_. The
/// given _`Type`_ can be any Rust type that is JSON parseable. It can be Option, Vec
/// or Map etc. With _`inline(...)`_ the schema will be inlined instead of a referenced
/// which is the default for [`ToSchema`][to_schema] types. _`ref("./external.json")`_
/// can be used to reference external json file for body schema. **Note!** Utoipa does **not** guarantee
/// that free form _`ref`_ is accessbile via OpenAPI doc or Swagger UI, users are eligible
/// to make these guarantees.
///
/// * `description = "..."` Define the description for the request body object as str.
///
Expand Down Expand Up @@ -631,10 +636,13 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream {
/// * `description = "..."` Define description for the response as str.
///
/// * `body = ...` Optional response body object type. When left empty response does not expect to send any
/// response body. Can be `body = Type` or `body = inline(Type)`. The given _`Type`_ can be any
/// Rust type that is JSON parseable. It can be Option, Vec or Map etc. With _`inline(...)`_
/// the schema will be inlined instead of a referenced which is the default for
/// [`ToSchema`][to_schema] types.
/// response body. Can be _`body = Type`_, _`body = inline(Type)`_, or _`body = ref("...")`_.
/// The given _`Type`_ can be any Rust type that is JSON parseable. It can be Option, Vec or Map etc.
/// With _`inline(...)`_ the schema will be inlined instead of a referenced which is the default for
/// [`ToSchema`][to_schema] types. _`ref("./external.json")`_
/// can be used to reference external json file for body schema. **Note!** Utoipa does **not** guarantee
/// that free form _`ref`_ is accessbile via OpenAPI doc or Swagger UI, users are eligible
/// to make these guarantees.
///
/// * `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
Expand Down
39 changes: 33 additions & 6 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ use proc_macro_error::abort;
use quote::{format_ident, quote, ToTokens};
use syn::punctuated::Punctuated;
use syn::token::Paren;
use syn::Type;
use syn::{parenthesized, parse::Parse, Token};
use syn::{LitStr, Type};

use crate::component::{GenericType, TypeTree};
use crate::{parse_utils, Deprecated};
Expand Down Expand Up @@ -532,21 +532,48 @@ impl ToTokens for Operation<'_> {
}
}

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

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::Type(input.parse()?))
}
}
}

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

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

impl Parse for InlineableType {
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>>()? {
Expand All @@ -565,7 +592,7 @@ impl Parse for InlineableType {
input.parse::<Type>()?
};

Ok(InlineableType { ty, is_inline })
Ok(InlineType { ty, is_inline })
}
}

Expand All @@ -582,7 +609,7 @@ pub trait PathTypeTree {

impl PathTypeTree for TypeTree<'_> {
/// Resolve default content type based on curren [`Type`].
fn get_default_content_type(&self) -> &str {
fn get_default_content_type(&self) -> &'static str {
if self.is_array()
&& self
.children
Expand Down
4 changes: 2 additions & 2 deletions utoipa-gen/src/path/parameter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use syn::{
use crate::ext::{ArgumentIn, ValueArgument};
use crate::{component::TypeTree, parse_utils, AnyValue, Deprecated, Required};

use super::{media_type::MediaTypeSchema, InlineableType, PathTypeTree};
use super::{media_type::MediaTypeSchema, InlineType, PathTypeTree};

/// Parameter of request suchs as in path, header, query or cookie
///
Expand Down Expand Up @@ -104,7 +104,7 @@ pub struct ValueParameter<'a> {
parameter_ext: Option<ParameterExt>,

/// Type only when value parameter is parsed
parsed_type: Option<InlineableType>,
parsed_type: Option<InlineType>,
}

impl<'p> ValueParameter<'p> {
Expand Down
56 changes: 39 additions & 17 deletions utoipa-gen/src/path/request_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{parse_utils, AnyValue, Array, Required};

use super::example::Example;
use super::media_type::MediaTypeSchema;
use super::{InlineableType, PathTypeTree};
use super::{PathType, PathTypeTree};

/// Parsed information related to requst body of path.
///
Expand Down Expand Up @@ -50,7 +50,7 @@ use super::{InlineableType, PathTypeTree};
#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct RequestBodyAttr {
content: Option<InlineableType>,
content: Option<PathType>,
content_type: Option<String>,
description: Option<String>,
example: Option<AnyValue>,
Expand Down Expand Up @@ -135,16 +135,23 @@ impl Parse for RequestBodyAttr {
impl ToTokens for RequestBodyAttr {
fn to_tokens(&self, tokens: &mut TokenStream2) {
if let Some(body_type) = &self.content {
let type_tree = body_type.as_type_tree();
let media_type_schema = MediaTypeSchema {
type_tree: &type_tree,
is_inline: body_type.is_inline,
let media_type_schema = match body_type {
PathType::Ref(ref_type) => quote! {
utoipa::openapi::schema::Ref::new(#ref_type)
},
PathType::Type(body_type) => {
let type_tree = body_type.as_type_tree();
MediaTypeSchema {
type_tree: &type_tree,
is_inline: body_type.is_inline,
}
.to_token_stream()
}
};
let mut content = quote! {
utoipa::openapi::content::ContentBuilder::new()
.schema(#media_type_schema)
};
let content_type = self
.content_type
.as_deref()
.unwrap_or_else(|| type_tree.get_default_content_type());
let mut content = quote! { utoipa::openapi::content::ContentBuilder::new().schema(#media_type_schema) };

if let Some(ref example) = self.example {
content.extend(quote! {
Expand All @@ -164,12 +171,27 @@ impl ToTokens for RequestBodyAttr {
))
}

let required: Required = (!type_tree.is_option()).into();
tokens.extend(quote! {
utoipa::openapi::request_body::RequestBodyBuilder::new()
.content(#content_type, #content.build())
.required(Some(#required))
});
match body_type {
PathType::Ref(_) => {
tokens.extend(quote! {
utoipa::openapi::request_body::RequestBodyBuilder::new()
.content("application/json", #content.build())
});
}
PathType::Type(body_type) => {
let type_tree = body_type.as_type_tree();
let content_type = self
.content_type
.as_deref()
.unwrap_or_else(|| type_tree.get_default_content_type());
let required: Required = (!type_tree.is_option()).into();
tokens.extend(quote! {
utoipa::openapi::request_body::RequestBodyBuilder::new()
.content(#content_type, #content.build())
.required(Some(#required))
});
}
}
}

if let Some(ref description) = self.description {
Expand Down
75 changes: 46 additions & 29 deletions utoipa-gen/src/path/response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ use syn::{
Error, ExprPath, LitInt, LitStr, Token,
};

use crate::{component::TypeTree, parse_utils, AnyValue, Array};
use crate::{parse_utils, AnyValue, Array};

use super::{
example::Example, media_type::MediaTypeSchema, status::STATUS_CODES, InlineableType, PathTypeTree,
example::Example, media_type::MediaTypeSchema, status::STATUS_CODES, InlineType, PathType,
PathTypeTree,
};

#[cfg_attr(feature = "debug", derive(Debug))]
Expand Down Expand Up @@ -62,7 +63,7 @@ impl ResponseTuple {
}

// Use with the `response` attribute, this will fail if an incompatible attribute has already been set
fn set_ref_type(&mut self, span: Span, ty: InlineableType) -> syn::Result<()> {
fn set_ref_type(&mut self, span: Span, ty: InlineType) -> syn::Result<()> {
match &mut self.inner {
None => self.inner = Some(ResponseTupleInner::Ref(ty)),
Some(ResponseTupleInner::Ref(r)) => *r = ty,
Expand All @@ -77,14 +78,14 @@ impl ResponseTuple {
#[cfg_attr(feature = "debug", derive(Debug))]
enum ResponseTupleInner {
Value(ResponseValue),
Ref(InlineableType),
Ref(InlineType),
}

#[derive(Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct ResponseValue {
description: String,
response_type: Option<InlineableType>,
response_type: Option<PathType>,
content_type: Option<Vec<String>>,
headers: Vec<Header>,
example: Option<AnyValue>,
Expand Down Expand Up @@ -197,16 +198,27 @@ impl ToTokens for ResponseTuple {
utoipa::openapi::ResponseBuilder::new().description(#description)
});

let create_content = |type_tree: &TypeTree,
is_inline: bool,
let create_content = |path_type: &PathType,
example: &Option<AnyValue>,
examples: &Option<Punctuated<Example, Comma>>|
-> TokenStream2 {
let media_schema_type = MediaTypeSchema {
type_tree,
is_inline,
let content_schema = match path_type {
PathType::Ref(ref_type) => quote! {
utoipa::openapi::schema::Ref::new(#ref_type)
}
.to_token_stream(),
PathType::Type(ref path_type) => {
let type_tree = path_type.as_type_tree();
MediaTypeSchema {
type_tree: &type_tree,
is_inline: path_type.is_inline,
}
.to_token_stream()
}
};
let mut content = quote! { utoipa::openapi::ContentBuilder::new().schema(#media_schema_type) };

let mut content =
quote! { utoipa::openapi::ContentBuilder::new().schema(#content_schema) };

if let Some(ref example) = example {
content.extend(quote! {
Expand All @@ -226,41 +238,46 @@ impl ToTokens for ResponseTuple {
))
}

content
quote! {
#content.build()
}
};

if let Some(response_type) = &val.response_type {
let type_tree = response_type.as_type_tree();
let content = create_content(
&type_tree,
response_type.is_inline,
&val.example,
&val.examples,
);
let content = create_content(response_type, &val.example, &val.examples);

if let Some(content_types) = val.content_type.as_ref() {
content_types.iter().for_each(|content_type| {
tokens.extend(quote! {
.content(#content_type, #content.build())
.content(#content_type, #content)
})
})
} else {
let default_type = type_tree.get_default_content_type();
tokens.extend(quote! {
.content(#default_type, #content.build())
});
match response_type {
PathType::Ref(_) => {
tokens.extend(quote! {
.content("application/json", #content)
});
}
PathType::Type(path_type) => {
let type_tree = path_type.as_type_tree();
let default_type = type_tree.get_default_content_type();
tokens.extend(quote! {
.content(#default_type, #content)
})
}
}
}
}

val.content
.iter()
.map(|Content(content_type, body, example, examples)| {
let type_tree = body.as_type_tree();
let content = create_content(&type_tree, body.is_inline, example, examples);
let content = create_content(body, example, examples);
(Cow::Borrowed(&**content_type), content)
})
.for_each(|(content_type, content)| {
tokens.extend(quote! { .content(#content_type, #content.build()) })
tokens.extend(quote! { .content(#content_type, #content) })
});

val.headers.iter().for_each(|header| {
Expand Down Expand Up @@ -362,7 +379,7 @@ impl ToTokens for ResponseStatus {
#[cfg_attr(feature = "debug", derive(Debug))]
struct Content(
String,
InlineableType,
PathType,
Option<AnyValue>,
Option<Punctuated<Example, Comma>>,
);
Expand Down Expand Up @@ -496,7 +513,7 @@ impl ToTokens for Responses<'_> {
#[cfg_attr(feature = "debug", derive(Debug))]
struct Header {
name: String,
value_type: Option<InlineableType>,
value_type: Option<InlineType>,
description: Option<String>,
}

Expand Down
Loading