diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs index fd425eab..0f5cf25f 100644 --- a/utoipa-gen/src/component.rs +++ b/utoipa-gen/src/component.rs @@ -121,6 +121,7 @@ impl<'a, T> Iterator for TypeTreeValueIter<'a, T> { #[derive(Clone)] pub struct TypeTree<'t> { pub path: Option>, + #[allow(unused)] pub span: Option, pub value_type: ValueType, pub generic_type: Option, diff --git a/utoipa-gen/src/ext.rs b/utoipa-gen/src/ext.rs index d95e7532..0b239d42 100644 --- a/utoipa-gen/src/ext.rs +++ b/utoipa-gen/src/ext.rs @@ -1,9 +1,9 @@ use std::borrow::Cow; use proc_macro2::TokenStream; -use quote::{quote, quote_spanned}; +use quote::ToTokens; use syn::spanned::Spanned; -use syn::{parse_quote, Generics}; +use syn::Generics; use syn::{punctuated::Punctuated, token::Comma, ItemFn}; use crate::component::{ComponentSchema, ComponentSchemaProps, Container, TypeTree}; @@ -96,103 +96,80 @@ pub enum ArgumentIn { } #[cfg_attr(feature = "debug", derive(Debug))] -pub struct RequestBody<'r> { - type_tree: TypeTree<'r>, -} - -impl RequestBody<'_> { - #[cfg(any( - feature = "actix_extras", - feature = "rocket_extras", - feature = "axum_extras" - ))] - pub fn get_component_schema(&self) -> Result, Diagnostics> { - use crate::OptionExt; +pub struct ExtSchema<'a>(TypeTree<'a>); - let type_tree = &self.type_tree; - let actual_body_type = get_actual_body_type(type_tree); +impl<'t> From> for ExtSchema<'t> { + fn from(value: TypeTree<'t>) -> ExtSchema<'t> { + Self(value) + } +} - actual_body_type.and_then_try(|body_type| { - if let Some(component_schema) = body_type.get_component_schema()? { - Result::, Diagnostics>::Ok(Some(component_schema)) +impl ExtSchema<'_> { + fn get_actual_body(&self) -> Cow<'_, TypeTree<'_>> { + let actual_body_type = get_actual_body_type(&self.0); + + actual_body_type.map(|actual_body| { + if let Some(option_type) = find_option_type_tree(actual_body) { + let path = option_type.path.clone(); + Cow::Owned(TypeTree { + children: Some(vec![actual_body.clone()]), + generic_type: Some(crate::component::GenericType::Option), + value_type: crate::component::ValueType::Object, + span: Some(path.span()), + path, + }) } else { - Ok(None) + Cow::Borrowed(actual_body) } - }) + }).expect("ExtSchema must have actual request body resoved from TypeTree of handler fn argument") } -} -impl<'t> From> for RequestBody<'t> { - fn from(value: TypeTree<'t>) -> RequestBody<'t> { - Self { type_tree: value } + pub fn get_type_tree(&self) -> Result>>, Diagnostics> { + Ok(Some(Cow::Borrowed(&self.0))) } -} -impl<'r> MediaTypePathExt<'r> for RequestBody<'r> { - fn get_component_schema(&self) -> Result, Diagnostics> { - self.type_tree.get_component_schema() - } -} + pub fn get_default_content_type(&self) -> Result, Diagnostics> { + let type_tree = &self.0; -impl ToTokensDiagnostics for RequestBody<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { - let mut actual_body = get_actual_body_type(&self.type_tree) - .expect("should have found actual request body TypeTree") - .clone(); - - if let Some(option) = find_option_type_tree(&self.type_tree) { - let path = option.path.clone(); - actual_body = TypeTree { - children: Some(vec![actual_body]), - generic_type: Some(crate::component::GenericType::Option), - value_type: crate::component::ValueType::Object, - span: Some(path.span()), - path, - } - }; - - let required = if actual_body.is_option() { - quote!(utoipa::openapi::Required::False) + let content_type = if type_tree.is("Bytes") { + Cow::Borrowed("application/octet-stream") + } else if type_tree.is("Form") { + Cow::Borrowed("application/x-www-form-urlencoded") } else { - quote!(utoipa::openapi::Required::True) - }; + let get_actual_body = self.get_actual_body(); + let actual_body = get_actual_body.as_ref(); - let mut create_body_tokens = - |content_type: &str, actual_body: &TypeTree| -> Result<(), Diagnostics> { - let schema = &ComponentSchema::new(ComponentSchemaProps { - type_tree: actual_body, - features: Vec::new(), - description: None, - container: &Container { - generics: &Generics::default(), - }, - })?; - - tokens.extend(quote_spanned! {actual_body.span.unwrap()=> - utoipa::openapi::request_body::RequestBodyBuilder::new() - .content(#content_type, - utoipa::openapi::content::Content::new(Some(#schema)) - ) - .required(Some(#required)) - .description(Some("")) - .build() - }); - Ok(()) - }; - - if self.type_tree.is("Bytes") { - let bytes_as_bytes_vec = parse_quote!(Vec); - let ty = TypeTree::from_type(&bytes_as_bytes_vec)?; - create_body_tokens("application/octet-stream", &ty)?; - } else if self.type_tree.is("Form") { - create_body_tokens("application/x-www-form-urlencoded", &actual_body)?; - } else { - create_body_tokens( - actual_body.get_default_content_type().as_ref(), - &actual_body, - )?; + actual_body.get_default_content_type() }; + Ok(content_type) + } + + pub fn get_component_schema(&self) -> Result, Diagnostics> { + use crate::OptionExt; + + let type_tree = &self.0; + let actual_body_type = get_actual_body_type(type_tree); + + actual_body_type.and_then_try(|body_type| body_type.get_component_schema()) + } +} + +impl ToTokensDiagnostics for ExtSchema<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) -> Result<(), Diagnostics> { + let get_actual_body = self.get_actual_body(); + let type_tree = get_actual_body.as_ref(); + + let component_tokens = ComponentSchema::new(ComponentSchemaProps { + type_tree, + features: Vec::new(), + description: None, + container: &Container { + generics: &Generics::default(), + }, + })?; + component_tokens.to_tokens(tokens); + Ok(()) } } @@ -300,7 +277,7 @@ pub struct ResolvedOperation { pub type Arguments<'a> = ( Option>>, Option>>, - Option>, + Option>, ); #[allow(unused)] diff --git a/utoipa-gen/src/path.rs b/utoipa-gen/src/path.rs index 589bd0b3..209f5666 100644 --- a/utoipa-gen/src/path.rs +++ b/utoipa-gen/src/path.rs @@ -11,7 +11,6 @@ use syn::{parenthesized, parse::Parse, Token}; use syn::{Expr, ExprLit, Lit, LitStr, Type}; use crate::component::{GenericType, TypeTree}; -use crate::path::request_body::RequestBody; use crate::{ as_tokens_or_diagnostics, parse_utils, Deprecated, Diagnostics, OptionExt, ToTokensDiagnostics, }; @@ -44,7 +43,7 @@ pub fn format_path_ident(fn_name: Cow<'_, Ident>) -> Cow<'_, Ident> { #[cfg_attr(feature = "debug", derive(Debug))] pub struct PathAttr<'p> { methods: Vec, - request_body: Option>, + request_body: Option>, responses: Vec>, pub(super) path: Option, operation_id: Option, @@ -70,13 +69,12 @@ impl<'p> PathAttr<'p> { feature = "rocket_extras", feature = "axum_extras" ))] - pub fn update_request_body(&mut self, request_body: Option>) { - use std::mem; - + pub fn update_request_body(&mut self, schema: Option>) { + use self::media_type::Schema; if self.request_body.is_none() { - self.request_body = request_body - .map(RequestBody::Ext) - .or(mem::take(&mut self.request_body)); + if let Some(schema) = schema { + self.request_body = Some(RequestBodyAttr::from_schema(Schema::Ext(schema))); + } } } @@ -138,8 +136,7 @@ impl Parse for PathAttr<'_> { path_attr.path = Some(parse_utils::parse_next_literal_str_or_expr(input)?); } "request_body" => { - path_attr.request_body = - Some(RequestBody::Parsed(input.parse::()?)); + path_attr.request_body = Some(input.parse::()?); } "responses" => { let responses; @@ -557,7 +554,7 @@ struct Operation<'a> { description: Option>, deprecated: bool, parameters: &'a Vec>, - request_body: Option<&'a RequestBody<'a>>, + request_body: Option<&'a RequestBodyAttr<'a>>, responses: &'a Vec>, security: Option<&'a Array<'a, SecurityRequirementsAttr>>, } diff --git a/utoipa-gen/src/path/media_type.rs b/utoipa-gen/src/path/media_type.rs index 64acd1f2..fb613112 100644 --- a/utoipa-gen/src/path/media_type.rs +++ b/utoipa-gen/src/path/media_type.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; -use std::ops::Deref; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; @@ -11,6 +10,7 @@ use syn::{Error, Generics, Ident, Token, Type}; use crate::component::features::attributes::Inline; use crate::component::features::Feature; use crate::component::{ComponentSchema, ComponentSchemaProps, Container, TypeTree, ValueType}; +use crate::ext::ExtSchema; use crate::{parse_utils, AnyValue, Array, Diagnostics, ToTokensDiagnostics}; use super::example::Example; @@ -23,9 +23,9 @@ use super::PathTypeTree; /// ( "content/type", example = ..., examples(..., ...), encoding(...) ) #[derive(Default)] #[cfg_attr(feature = "debug", derive(Debug))] -pub struct MediaTypeAttr<'a> { +pub struct MediaTypeAttr<'m> { pub content_type: Option, // if none, true guess - pub schema: Schema>, + pub schema: Schema<'m>, pub example: Option, pub examples: Punctuated, // econding: String, // TODO parse encoding @@ -38,7 +38,7 @@ impl Parse for MediaTypeAttr<'_> { let fork = input.fork(); let is_schema = fork.parse::().is_ok(); if is_schema { - let schema = input.parse::>()?; + let schema = input.parse::()?; let content_type = if input.parse::>()?.is_some() { Some( @@ -56,7 +56,7 @@ impl Parse for MediaTypeAttr<'_> { } else { None }; - media_type.schema = schema; + media_type.schema = Schema::Default(schema); media_type.content_type = content_type; } else { // if schema, the content type is required @@ -85,10 +85,8 @@ impl Parse for MediaTypeAttr<'_> { } impl<'m> MediaTypeAttr<'m> { - pub fn parse_schema(input: ParseStream) -> syn::Result>> { - Ok(Schema { - inner: input.parse()?, - }) + pub fn parse_schema(input: ParseStream) -> syn::Result> { + input.parse() } pub fn parse_named_attributes( @@ -153,54 +151,55 @@ impl ToTokensDiagnostics for MediaTypeAttr<'_> { } } -#[cfg_attr(feature = "debug", derive(Debug))] -#[derive(Default)] -pub struct Schema { - inner: T, +pub trait MediaTypePathExt<'a> { + fn get_component_schema(&self) -> Result, Diagnostics>; } -impl Deref for Schema -where - T: Parse + Default, -{ - type Target = T; +#[cfg_attr(feature = "debug", derive(Debug))] +#[allow(unused)] +pub enum Schema<'a> { + Default(DefaultSchema<'a>), + Ext(ExtSchema<'a>), +} - fn deref(&self) -> &Self::Target { - &self.inner +impl Default for Schema<'_> { + fn default() -> Self { + Self::Default(DefaultSchema::None) } } -impl Parse for Schema -where - T: Parse + Default, -{ - fn parse(input: syn::parse::ParseStream) -> syn::Result { - Ok(Self { - inner: input.parse()?, - }) +impl Schema<'_> { + pub fn get_type_tree(&self) -> Result>>, Diagnostics> { + match self { + Self::Default(def) => def.get_type_tree(), + Self::Ext(ext) => ext.get_type_tree(), + } } -} -impl ToTokens for Schema -where - T: ToTokens + Parse + Default, -{ - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - self.inner.to_tokens(tokens); + pub fn get_default_content_type(&self) -> Result, Diagnostics> { + match self { + Self::Default(def) => def.get_default_content_type(), + Self::Ext(ext) => ext.get_default_content_type(), + } } -} -impl AsRef for Schema -where - T: Parse + Default, -{ - fn as_ref(&self) -> &T { - &self.inner + pub fn get_component_schema(&self) -> Result, Diagnostics> { + match self { + Self::Default(def) => def.get_component_schema(), + Self::Ext(ext) => ext.get_component_schema(), + } } } -pub trait MediaTypePathExt<'a> { - fn get_component_schema(&self) -> Result, Diagnostics>; +impl ToTokensDiagnostics for Schema<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { + match self { + Self::Default(def) => def.to_tokens(tokens)?, + Self::Ext(ext) => ext.to_tokens(tokens)?, + } + + Ok(()) + } } #[cfg_attr(feature = "debug", derive(Debug))] @@ -288,9 +287,11 @@ impl DefaultSchema<'_> { } } - pub fn get_type_tree(&self) -> Result>, Diagnostics> { + pub fn get_type_tree(&self) -> Result>>, Diagnostics> { match self { - Self::TypePath(path) => path.to_type_tree().map(Some), + Self::TypePath(path) => path + .to_type_tree() + .map(|type_tree| Some(Cow::Owned(type_tree))), _ => Ok(None), } } diff --git a/utoipa-gen/src/path/request_body.rs b/utoipa-gen/src/path/request_body.rs index 719e80dc..970e5b5b 100644 --- a/utoipa-gen/src/path/request_body.rs +++ b/utoipa-gen/src/path/request_body.rs @@ -8,40 +8,9 @@ use syn::{parse::Parse, Error, Token}; use crate::component::ComponentSchema; use crate::{parse_utils, Diagnostics, Required, ToTokensDiagnostics}; -use super::media_type::MediaTypeAttr; +use super::media_type::{MediaTypeAttr, Schema}; use super::parse; -#[cfg_attr(feature = "debug", derive(Debug))] -pub enum RequestBody<'r> { - Parsed(RequestBodyAttr<'r>), - #[cfg(any( - feature = "actix_extras", - feature = "rocket_extras", - feature = "axum_extras" - ))] - Ext(crate::ext::RequestBody<'r>), -} - -impl RequestBody<'_> { - pub fn get_component_schemas( - &self, - ) -> Result, Diagnostics> { - match self { - Self::Parsed(parsed) => parsed - .get_component_schemas() - .map(|iter| ComponentSchemaIter::Iter(Box::new(iter))), - #[cfg(any( - feature = "actix_extras", - feature = "rocket_extras", - feature = "axum_extras" - ))] - Self::Ext(ext) => Ok(ComponentSchemaIter::Option( - ext.get_component_schema()?.into_iter(), - )), - } - } -} - #[allow(unused)] enum ComponentSchemaIter { Iter(Box>), @@ -66,22 +35,6 @@ impl Iterator for ComponentSchemaIter { } } -impl ToTokensDiagnostics for RequestBody<'_> { - fn to_tokens(&self, tokens: &mut TokenStream) -> Result<(), Diagnostics> { - match self { - Self::Parsed(parsed) => ToTokensDiagnostics::to_tokens(parsed, tokens)?, - #[cfg(any( - feature = "actix_extras", - feature = "rocket_extras", - feature = "axum_extras" - ))] - Self::Ext(ext) => ToTokensDiagnostics::to_tokens(ext, tokens)?, - }; - - Ok(()) - } -} - /// Parsed information related to request body of path. /// /// Supported configuration options: @@ -135,7 +88,7 @@ pub struct RequestBodyAttr<'r> { media_type: Vec>, } -impl RequestBodyAttr<'_> { +impl<'r> RequestBodyAttr<'r> { fn new() -> Self { Self { description: Default::default(), @@ -143,6 +96,21 @@ impl RequestBodyAttr<'_> { } } + #[cfg(any( + feature = "actix_extras", + feature = "rocket_extras", + feature = "axum_extras" + ))] + pub fn from_schema(schema: Schema<'r>) -> RequestBodyAttr<'r> { + Self { + media_type: vec![MediaTypeAttr { + schema, + ..Default::default() + }], + ..Self::new() + } + } + pub fn get_component_schemas( &self, ) -> Result, Diagnostics> { @@ -179,7 +147,7 @@ impl Parse for RequestBodyAttr<'_> { group.parse::()?; let schema = MediaTypeAttr::parse_schema(&group)?; if let Some(media_type) = request_body_attr.media_type.get_mut(0) { - media_type.schema = schema; + media_type.schema = Schema::Default(schema); } } else if group.peek(Paren) { fn group_parser<'a>( @@ -240,7 +208,7 @@ impl Parse for RequestBodyAttr<'_> { input.parse::()?; let media_type = MediaTypeAttr { - schema: MediaTypeAttr::parse_schema(input)?, + schema: Schema::Default(MediaTypeAttr::parse_schema(input)?), content_type: None, example: None, examples: Punctuated::default(), diff --git a/utoipa-gen/tests/path_derive.rs b/utoipa-gen/tests/path_derive.rs index fb66127c..b072192a 100644 --- a/utoipa-gen/tests/path_derive.rs +++ b/utoipa-gen/tests/path_derive.rs @@ -5,7 +5,7 @@ use paste::paste; use serde::Serialize; use serde_json::{json, Value}; use std::collections::HashMap; -use utoipa::openapi::{self, Components, RefOr, Schema}; +use utoipa::openapi::RefOr; use utoipa::openapi::{Object, ObjectBuilder}; use utoipa::{ openapi::{Response, ResponseBuilder, ResponsesBuilder}, diff --git a/utoipa-gen/tests/path_derive_actix.rs b/utoipa-gen/tests/path_derive_actix.rs index 087ee94b..5bd8da58 100644 --- a/utoipa-gen/tests/path_derive_actix.rs +++ b/utoipa-gen/tests/path_derive_actix.rs @@ -879,7 +879,6 @@ fn path_with_all_args() { assert_json_eq!( &operation.pointer("/requestBody"), json!({ - "description": "", "content": { "application/json": { "schema": { @@ -940,7 +939,6 @@ fn path_with_all_args_using_uuid() { assert_json_eq!( &operation.pointer("/requestBody"), json!({ - "description": "", "content": { "application/json": { "schema": { @@ -1019,7 +1017,6 @@ fn path_with_all_args_using_custom_uuid() { assert_json_eq!( &operation.pointer("/requestBody"), json!({ - "description": "", "content": { "application/json": { "schema": { @@ -1114,7 +1111,6 @@ fn path_derive_custom_generic_wrapper() { assert_json_eq!( &operation.pointer("/requestBody"), json!({ - "description": "", "content": { "application/json": { "schema": { diff --git a/utoipa-gen/tests/path_derive_axum_test.rs b/utoipa-gen/tests/path_derive_axum_test.rs index b5e808c3..66048738 100644 --- a/utoipa-gen/tests/path_derive_axum_test.rs +++ b/utoipa-gen/tests/path_derive_axum_test.rs @@ -447,7 +447,6 @@ fn path_with_path_query_body_resolved() { assert_json_eq!( &operation.pointer("/requestBody"), json!({ - "description": "", "content": { "application/json": { "schema": { diff --git a/utoipa-gen/tests/path_derive_rocket.rs b/utoipa-gen/tests/path_derive_rocket.rs index dc2f8410..51a76200 100644 --- a/utoipa-gen/tests/path_derive_rocket.rs +++ b/utoipa-gen/tests/path_derive_rocket.rs @@ -532,7 +532,6 @@ fn path_with_all_args_and_body() { } } }, - "description": "", "required": true }) );