Skip to content

Commit

Permalink
Do not inline non-primitive type parameters by default (#1184)
Browse files Browse the repository at this point in the history
Co-authored-by: Jean-Marc Le Roux <[email protected]>
  • Loading branch information
JMLX42 and Jean-Marc Le Roux authored Nov 14, 2024
1 parent 478b7c9 commit 553142a
Show file tree
Hide file tree
Showing 5 changed files with 255 additions and 7 deletions.
44 changes: 39 additions & 5 deletions utoipa-gen/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1216,15 +1216,39 @@ impl ComponentSchema {
let title_tokens = as_tokens_or_diagnostics!(&title);

if is_inline {
let schema_type = SchemaType {
path: Cow::Borrowed(&rewritten_path),
nullable,
};
let index =
if !schema_type.is_primitive() || type_tree.generic_type.is_none() {
container.generics.get_generic_type_param_index(type_tree)
} else {
None
};

object_schema_reference.is_inline = true;
let items_tokens = if let Some(children) = &type_tree.children {
schema_references.extend(Self::compose_child_references(children)?);

let composed_generics =
Self::compose_generics(children, container.generics)?
.collect::<Array<_>>();
quote_spanned! {type_path.span()=>
<#rewritten_path as utoipa::__dev::ComposeSchema>::compose(#composed_generics.to_vec())

if index.is_some() {
quote_spanned! {type_path.span()=>
let _ = <#rewritten_path as utoipa::PartialSchema>::schema;

if let Some(composed) = generics.get_mut(#index) {
composed.clone()
} else {
<#rewritten_path as utoipa::PartialSchema>::schema()
}
}
} else {
quote_spanned! {type_path.span()=>
<#rewritten_path as utoipa::__dev::ComposeSchema>::compose(#composed_generics.to_vec())
}
}
} else {
quote_spanned! {type_path.span()=>
Expand Down Expand Up @@ -1255,8 +1279,18 @@ impl ComponentSchema {

schema.to_tokens(tokens);
} else {
let index = container.generics.get_generic_type_param_index(type_tree);
// only set schema references tokens for concrete non generic types
let schema_type = SchemaType {
path: Cow::Borrowed(&rewritten_path),
nullable,
};
let index =
if !schema_type.is_primitive() || type_tree.generic_type.is_none() {
container.generics.get_generic_type_param_index(type_tree)
} else {
None
};

// forcibly inline primitive type parameters, otherwise use references
if index.is_none() {
let reference_tokens = if let Some(children) = &type_tree.children {
let composed_generics = Self::compose_generics(
Expand All @@ -1281,7 +1315,7 @@ impl ComponentSchema {
let _ = <#rewritten_path as utoipa::PartialSchema>::schema;

if let Some(composed) = generics.get_mut(#index) {
std::mem::take(composed)
composed.clone()
} else {
#item_tokens.into()
}
Expand Down
58 changes: 58 additions & 0 deletions utoipa-gen/tests/openapi_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,64 @@ fn derive_nest_openapi_with_tags() {
)
}

#[test]
fn openapi_schemas_resolve_generic_enum_schema() {
#![allow(dead_code)]
use utoipa::ToSchema;

#[derive(ToSchema)]
enum Element<T> {
One(T),
Many(Vec<T>),
}

#[derive(OpenApi)]
#[openapi(components(schemas(Element<String>)))]
struct ApiDoc;

let doc = ApiDoc::openapi();

let value = serde_json::to_value(&doc).expect("OpenAPI is JSON serializable");
let schemas = value.pointer("/components/schemas").unwrap();
let json = serde_json::to_string_pretty(&schemas).expect("OpenAPI is json serializable");
println!("{json}");

assert_json_eq!(
schemas,
json!({
"Element_String": {
"oneOf": [
{
"properties": {
"One": {
"type": "string"
}
},
"required": [
"One"
],
"type": "object"
},
{
"properties": {
"Many": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"Many"
],
"type": "object"
}
]
}
})
)
}

#[test]
fn openapi_schemas_resolve_schema_references() {
#![allow(dead_code)]
Expand Down
130 changes: 130 additions & 0 deletions utoipa-gen/tests/schema_generics.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::borrow::Cow;
use std::marker::PhantomData;

use assert_json_diff::assert_json_eq;
use serde::Serialize;
use serde_json::json;
use utoipa::openapi::{Info, RefOr, Schema};
use utoipa::{schema, OpenApi, PartialSchema, ToSchema};

Expand All @@ -23,6 +25,132 @@ fn generic_schema_custom_bound() {
assert_is_to_schema::<Type<NoToSchema>>();
}

#[test]
fn generic_request_body_schema() {
#![allow(unused)]

#[derive(ToSchema)]
#[schema(as = path::MyType<T>)]
struct Type<T> {
#[schema(inline)]
t: T,
}

#[derive(ToSchema)]
struct Person<T: Sized, P> {
field: T,
#[schema(inline)]
t: P,
}

#[utoipa::path(
get,
path = "/handler",
request_body = inline(Person<String, Type<i32>>),
)]
async fn handler() {}

#[derive(OpenApi)]
#[openapi(
components(
schemas(
Person::<String, Type<i32>>,
)
),
paths(
handler
)
)]
struct ApiDoc;

let mut doc = ApiDoc::openapi();
doc.info = Info::new("title", "version");

let actual = serde_json::to_value(&doc).expect("operation is JSON serializable");
let json = serde_json::to_string_pretty(&actual).unwrap();

println!("{json}");

assert_json_eq!(
actual,
json!({
"openapi": "3.1.0",
"info": {
"title": "title",
"version": "version"
},
"paths": {
"/handler": {
"get": {
"tags": [],
"operationId": "handler",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"field",
"t"
],
"properties": {
"field": {
"type": "string"
},
"t": {
"type": "object",
"required": [
"t"
],
"properties": {
"t": {
"type": "integer",
"format": "int32"
}
}
}
}
}
}
},
"required": true
},
"responses": {}
}
}
},
"components": {
"schemas": {
"Person_String_path.MyType_i32": {
"type": "object",
"required": [
"field",
"t"
],
"properties": {
"field": {
"type": "string"
},
"t": {
"type": "object",
"required": [
"t"
],
"properties": {
"t": {
"type": "integer",
"format": "int32"
}
}
}
}
}
}
}
})
);
}

#[test]
fn generic_schema_full_api() {
#![allow(unused)]
Expand Down Expand Up @@ -107,6 +235,7 @@ fn schema_with_non_generic_root() {

#[derive(ToSchema)]
struct Bar<T> {
#[schema(inline)]
value: T,
}

Expand Down Expand Up @@ -231,6 +360,7 @@ fn high_order_types() {

#[derive(ToSchema)]
pub struct High<T> {
#[schema(inline)]
high: T,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"properties": {
"Many": {
"items": {
"type": "object"
"type": "string"
},
"type": "array"
}
Expand Down Expand Up @@ -95,6 +95,32 @@
"properties": {
"Many": {
"items": {
"properties": {
"accounts": {
"items": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/Account"
}
]
},
"type": "array"
},
"foo_bar": {
"$ref": "#/components/schemas/Foobar"
},
"name": {
"type": "string"
}
},
"required": [
"name",
"foo_bar",
"accounts"
],
"type": "object"
},
"type": "array"
Expand Down
2 changes: 1 addition & 1 deletion utoipa-gen/tests/testdata/schema_generics_openapi
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@
"Many": {
"type": "array",
"items": {
"type": "object"
"type": "string"
}
}
}
Expand Down

0 comments on commit 553142a

Please sign in to comment.