From 553142a613143b13b08556feba6822df2b7716f2 Mon Sep 17 00:00:00 2001 From: Jean-Marc Le Roux Date: Thu, 14 Nov 2024 14:37:09 +0100 Subject: [PATCH] Do not inline non-primitive type parameters by default (#1184) Co-authored-by: Jean-Marc Le Roux --- utoipa-gen/src/component.rs | 44 +++++- utoipa-gen/tests/openapi_derive.rs | 58 ++++++++ utoipa-gen/tests/schema_generics.rs | 130 ++++++++++++++++++ ...pi_schemas_resolve_inner_schema_references | 28 +++- .../tests/testdata/schema_generics_openapi | 2 +- 5 files changed, 255 insertions(+), 7 deletions(-) diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs index 6a0b9b2c..45808ad9 100644 --- a/utoipa-gen/src/component.rs +++ b/utoipa-gen/src/component.rs @@ -1216,6 +1216,17 @@ 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)?); @@ -1223,8 +1234,21 @@ impl ComponentSchema { let composed_generics = Self::compose_generics(children, container.generics)? .collect::>(); - 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()=> @@ -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( @@ -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() } diff --git a/utoipa-gen/tests/openapi_derive.rs b/utoipa-gen/tests/openapi_derive.rs index e34dc570..8702b071 100644 --- a/utoipa-gen/tests/openapi_derive.rs +++ b/utoipa-gen/tests/openapi_derive.rs @@ -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 { + One(T), + Many(Vec), + } + + #[derive(OpenApi)] + #[openapi(components(schemas(Element)))] + 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)] diff --git a/utoipa-gen/tests/schema_generics.rs b/utoipa-gen/tests/schema_generics.rs index cd49a2bd..96b1af64 100644 --- a/utoipa-gen/tests/schema_generics.rs +++ b/utoipa-gen/tests/schema_generics.rs @@ -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}; @@ -23,6 +25,132 @@ fn generic_schema_custom_bound() { assert_is_to_schema::>(); } +#[test] +fn generic_request_body_schema() { + #![allow(unused)] + + #[derive(ToSchema)] + #[schema(as = path::MyType)] + struct Type { + #[schema(inline)] + t: T, + } + + #[derive(ToSchema)] + struct Person { + field: T, + #[schema(inline)] + t: P, + } + + #[utoipa::path( + get, + path = "/handler", + request_body = inline(Person>), + )] + async fn handler() {} + + #[derive(OpenApi)] + #[openapi( + components( + schemas( + Person::>, + ) + ), + 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)] @@ -107,6 +235,7 @@ fn schema_with_non_generic_root() { #[derive(ToSchema)] struct Bar { + #[schema(inline)] value: T, } @@ -231,6 +360,7 @@ fn high_order_types() { #[derive(ToSchema)] pub struct High { + #[schema(inline)] high: T, } diff --git a/utoipa-gen/tests/testdata/openapi_schemas_resolve_inner_schema_references b/utoipa-gen/tests/testdata/openapi_schemas_resolve_inner_schema_references index 7368bc5b..9e0cd742 100644 --- a/utoipa-gen/tests/testdata/openapi_schemas_resolve_inner_schema_references +++ b/utoipa-gen/tests/testdata/openapi_schemas_resolve_inner_schema_references @@ -40,7 +40,7 @@ "properties": { "Many": { "items": { - "type": "object" + "type": "string" }, "type": "array" } @@ -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" diff --git a/utoipa-gen/tests/testdata/schema_generics_openapi b/utoipa-gen/tests/testdata/schema_generics_openapi index 95c429ca..f69eda2f 100644 --- a/utoipa-gen/tests/testdata/schema_generics_openapi +++ b/utoipa-gen/tests/testdata/schema_generics_openapi @@ -193,7 +193,7 @@ "Many": { "type": "array", "items": { - "type": "object" + "type": "string" } } }