Skip to content

Commit

Permalink
Chore unify request body and ext request body (#1067)
Browse files Browse the repository at this point in the history
This commit unifies to logic for parsed request body and ext request
body from handler function argument. Prior to this commit they had
separate implementations for `ToTokens` but that is changed in this
commit for being overly superfluous and error prone. Now they share same
`ToTokens` logic where only difference is how the `ComponentSchema` is
resolved for the different types of request bodies.
  • Loading branch information
juhaku authored Sep 27, 2024
1 parent 5a06616 commit ac13f48
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 202 deletions.
1 change: 1 addition & 0 deletions utoipa-gen/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ impl<'a, T> Iterator for TypeTreeValueIter<'a, T> {
#[derive(Clone)]
pub struct TypeTree<'t> {
pub path: Option<Cow<'t, Path>>,
#[allow(unused)]
pub span: Option<Span>,
pub value_type: ValueType,
pub generic_type: Option<GenericType>,
Expand Down
151 changes: 64 additions & 87 deletions utoipa-gen/src/ext.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<Option<ComponentSchema>, 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<TypeTree<'t>> 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::<Option<ComponentSchema>, 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<TypeTree<'t>> for RequestBody<'t> {
fn from(value: TypeTree<'t>) -> RequestBody<'t> {
Self { type_tree: value }
pub fn get_type_tree(&self) -> Result<Option<Cow<'_, TypeTree<'_>>>, Diagnostics> {
Ok(Some(Cow::Borrowed(&self.0)))
}
}

impl<'r> MediaTypePathExt<'r> for RequestBody<'r> {
fn get_component_schema(&self) -> Result<Option<ComponentSchema>, Diagnostics> {
self.type_tree.get_component_schema()
}
}
pub fn get_default_content_type(&self) -> Result<Cow<'static, str>, 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<u8>);
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<Option<ComponentSchema>, 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(())
}
}
Expand Down Expand Up @@ -300,7 +277,7 @@ pub struct ResolvedOperation {
pub type Arguments<'a> = (
Option<Vec<ValueArgument<'a>>>,
Option<Vec<IntoParamsType<'a>>>,
Option<RequestBody<'a>>,
Option<ExtSchema<'a>>,
);

#[allow(unused)]
Expand Down
19 changes: 8 additions & 11 deletions utoipa-gen/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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<HttpMethod>,
request_body: Option<RequestBody<'p>>,
request_body: Option<RequestBodyAttr<'p>>,
responses: Vec<Response<'p>>,
pub(super) path: Option<parse_utils::LitStrOrExpr>,
operation_id: Option<Expr>,
Expand All @@ -70,13 +69,12 @@ impl<'p> PathAttr<'p> {
feature = "rocket_extras",
feature = "axum_extras"
))]
pub fn update_request_body(&mut self, request_body: Option<crate::ext::RequestBody<'p>>) {
use std::mem;

pub fn update_request_body(&mut self, schema: Option<crate::ext::ExtSchema<'p>>) {
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)));
}
}
}

Expand Down Expand Up @@ -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::<RequestBodyAttr>()?));
path_attr.request_body = Some(input.parse::<RequestBodyAttr>()?);
}
"responses" => {
let responses;
Expand Down Expand Up @@ -557,7 +554,7 @@ struct Operation<'a> {
description: Option<Description<'a>>,
deprecated: bool,
parameters: &'a Vec<Parameter<'a>>,
request_body: Option<&'a RequestBody<'a>>,
request_body: Option<&'a RequestBodyAttr<'a>>,
responses: &'a Vec<Response<'a>>,
security: Option<&'a Array<'a, SecurityRequirementsAttr>>,
}
Expand Down
Loading

0 comments on commit ac13f48

Please sign in to comment.