From b473b9901f16881412ee7f2f173f13880c601236 Mon Sep 17 00:00:00 2001 From: Juha Kukkonen Date: Tue, 3 Sep 2024 14:57:48 +0300 Subject: [PATCH] Make referenced schemas required (#1018) This will affect in all places where schema is being rendered. These are `value_type = ..` `request_body`, `response_body = ...` to name few. This aims to enforce `PartialSchema` implementation for every type that is being used in OpenAPI spec generated by utoipa. The `Schema` trait will be split to `PartialSchema` and `Schema` and `Schema` will extend the `PartialSchema` trait. `PartialSchema` will provide the actual schema and `Schema` will provide name and other data related to the schema itself. This is useful since we already provide `PartialSchema` implementation for many standard Rust types and not all types need the full schema but only the schema definition. This makes schema definition implementation easier by juts allowing users to manually implement `PartialSchema` type for their type if needed. Still as usual the implementation can be automatically derived with `ToSchema` derive trait. Fixes #500 Fixes #801 --- utoipa-gen/src/component.rs | 84 ++++++++++++++---- utoipa-gen/src/component/into_params.rs | 6 +- utoipa-gen/src/component/schema.rs | 36 ++++++-- utoipa-gen/src/ext.rs | 2 + utoipa-gen/src/lib.rs | 27 +++++- utoipa-gen/src/path/parameter.rs | 4 + utoipa-gen/src/path/request_body.rs | 1 + utoipa-gen/src/path/response.rs | 2 + utoipa-gen/src/path/response/derive.rs | 11 ++- utoipa-gen/tests/path_derive_axum_test.rs | 1 + utoipa-gen/tests/request_body_derive_test.rs | 66 +++++++++----- utoipa-gen/tests/response_derive_test.rs | 2 + utoipa-gen/tests/schema_derive_test.rs | 59 ++++++++----- utoipa-gen/tests/utoipa_gen_test.rs | 4 +- utoipa/src/lib.rs | 90 +++++++++++--------- utoipa/src/openapi/schema.rs | 4 +- 16 files changed, 290 insertions(+), 109 deletions(-) diff --git a/utoipa-gen/src/component.rs b/utoipa-gen/src/component.rs index 1a3a5038..ff4115f7 100644 --- a/utoipa-gen/src/component.rs +++ b/utoipa-gen/src/component.rs @@ -367,6 +367,26 @@ impl<'t> TypeTree<'t> { pub fn is_map(&self) -> bool { matches!(self.generic_type, Some(GenericType::Map)) } + + pub fn match_ident(&self, ident: &Ident) -> bool { + let Some(ref path) = self.path else { + return false; + }; + + let matches = path + .segments + .iter() + .last() + .map(|segment| &segment.ident == ident) + .unwrap_or_default(); + + matches + || self + .children + .iter() + .flatten() + .any(|child| child.match_ident(ident)) + } } impl PartialEq for TypeTree<'_> { @@ -478,6 +498,7 @@ pub struct ComponentSchemaProps<'c> { pub(crate) description: Option<&'c ComponentDescription<'c>>, pub(crate) deprecated: Option<&'c Deprecated>, pub object_name: &'c str, + pub is_generics_type_arg: bool, } #[cfg_attr(feature = "debug", derive(Debug))] @@ -520,6 +541,7 @@ impl<'c> ComponentSchema { description, deprecated, object_name, + is_generics_type_arg, }: ComponentSchemaProps, ) -> Result { let mut tokens = TokenStream::new(); @@ -534,6 +556,7 @@ impl<'c> ComponentSchema { object_name, description, deprecated_stream, + is_generics_type_arg, )?, Some(GenericType::Vec | GenericType::LinkedList | GenericType::Set) => { ComponentSchema::vec_to_tokens( @@ -543,6 +566,7 @@ impl<'c> ComponentSchema { object_name, description, deprecated_stream, + is_generics_type_arg, )? } #[cfg(feature = "smallvec")] @@ -553,6 +577,7 @@ impl<'c> ComponentSchema { object_name, description, deprecated_stream, + is_generics_type_arg, )?, Some(GenericType::Option) => { // Add nullable feature if not already exists. Option is always nullable @@ -575,6 +600,7 @@ impl<'c> ComponentSchema { description, deprecated, object_name, + is_generics_type_arg, })? .to_tokens(&mut tokens)?; } @@ -591,6 +617,7 @@ impl<'c> ComponentSchema { description, deprecated, object_name, + is_generics_type_arg, })? .to_tokens(&mut tokens)?; } @@ -608,6 +635,7 @@ impl<'c> ComponentSchema { description, deprecated, object_name, + is_generics_type_arg, })? .to_tokens(&mut tokens)?; } @@ -618,6 +646,7 @@ impl<'c> ComponentSchema { object_name, description, deprecated_stream, + is_generics_type_arg, )?, }; @@ -652,6 +681,7 @@ impl<'c> ComponentSchema { object_name: &str, description_stream: Option<&ComponentDescription<'_>>, deprecated_stream: Option, + is_generics_type_arg: bool, ) -> Result<(), Diagnostics> { let example = features.pop_by(|feature| matches!(feature, Feature::Example(_))); let additional_properties = pop_feature!(features => Feature::AdditionalProperties(_)); @@ -679,6 +709,7 @@ impl<'c> ComponentSchema { description: None, deprecated: None, object_name, + is_generics_type_arg, // TODO check whether this is correct })?; let schema_tokens = as_tokens_or_diagnostics!(&schema_property); @@ -709,6 +740,7 @@ impl<'c> ComponentSchema { object_name: &str, description_stream: Option<&ComponentDescription<'_>>, deprecated_stream: Option, + is_generics_type_arg: bool, ) -> Result<(), Diagnostics> { let example = pop_feature!(features => Feature::Example(_)); let xml = features.extract_vec_xml_feature(type_tree)?; @@ -747,6 +779,7 @@ impl<'c> ComponentSchema { description: None, deprecated: None, object_name, + is_generics_type_arg, })?; let component_schema_tokens = as_tokens_or_diagnostics!(&component_schema); @@ -810,6 +843,7 @@ impl<'c> ComponentSchema { object_name: &str, description_stream: Option<&ComponentDescription<'_>>, deprecated_stream: Option, + is_generics_type_arg: bool, ) -> Result<(), Diagnostics> { let nullable_feat: Option = pop_feature!(features => Feature::Nullable(_)).into_inner(); @@ -898,12 +932,12 @@ impl<'c> ComponentSchema { quote_spanned! {type_path.span()=> utoipa::openapi::schema::AllOfBuilder::new() #nullable_item - .item(<#type_path as utoipa::ToSchema>::schema().1) + .item(<#type_path as utoipa::PartialSchema>::schema()) #default_tokens } } else { quote_spanned! {type_path.span() => - <#type_path as utoipa::ToSchema>::schema().1 + <#type_path as utoipa::PartialSchema>::schema() } }; @@ -913,28 +947,45 @@ impl<'c> ComponentSchema { if name == "Self" && !object_name.is_empty() { name = Cow::Borrowed(object_name); } - let default = pop_feature!(features => Feature::Default(_)); let default_tokens = as_tokens_or_diagnostics!(&default); + // TODO partial schema check cannot be performed for generic type, we + // need to know whether type is generic or not. + let check_type = if !is_generics_type_arg { + Some( + quote_spanned! {type_path.span()=> let _ = <#type_path as utoipa::PartialSchema>::schema;}, + ) + } else { + None + }; + // TODO: refs support `summary` field but currently there is no such field // on schemas more over there is no way to distinct the `summary` from // `description` of the ref. Should we consider supporting the summary? let schema = if default.is_some() || nullable { - quote! { - utoipa::openapi::schema::AllOfBuilder::new() - #nullable_item - .item(utoipa::openapi::schema::RefBuilder::new() - #description_stream - .ref_location_from_schema_name(#name) - ) - #default_tokens + quote_spanned! {type_path.span()=> + { + #check_type + + utoipa::openapi::schema::AllOfBuilder::new() + #nullable_item + .item(utoipa::openapi::schema::RefBuilder::new() + #description_stream + .ref_location_from_schema_name(#name) + ) + #default_tokens + } } } else { - quote! { - utoipa::openapi::schema::RefBuilder::new() - #description_stream - .ref_location_from_schema_name(#name) + quote_spanned! {type_path.span()=> + { + #check_type + + utoipa::openapi::schema::RefBuilder::new() + #description_stream + .ref_location_from_schema_name(#name) + } } }; @@ -962,6 +1013,7 @@ impl<'c> ComponentSchema { description: None, deprecated: None, object_name, + is_generics_type_arg, // TODO check whether this is correct }) { Ok(child) => Ok(as_tokens_or_diagnostics!(&child)), Err(diagnostics) => Err(diagnostics), @@ -1024,6 +1076,7 @@ impl FlattenedMapSchema { description, deprecated, object_name, + is_generics_type_arg, }: ComponentSchemaProps, ) -> Result { let mut tokens = TokenStream::new(); @@ -1050,6 +1103,7 @@ impl FlattenedMapSchema { description: None, deprecated: None, object_name, + is_generics_type_arg, })?; let schema_tokens = as_tokens_or_diagnostics!(&schema_property); diff --git a/utoipa-gen/src/component/into_params.rs b/utoipa-gen/src/component/into_params.rs index ffbef46f..91fe108a 100644 --- a/utoipa-gen/src/component/into_params.rs +++ b/utoipa-gen/src/component/into_params.rs @@ -25,7 +25,7 @@ use crate::{ FieldRename, }, doc_comment::CommentAttributes, - Array, Diagnostics, OptionExt, Required, ToTokensDiagnostics, + Array, Diagnostics, GenericsExt, OptionExt, Required, ToTokensDiagnostics, }; use super::{ @@ -147,6 +147,7 @@ impl ToTokensDiagnostics for IntoParams { name, }, serde_container: &serde_container, + generics: &self.generics }; let mut param_tokens = TokenStream::new(); @@ -300,6 +301,8 @@ struct Param<'a> { container_attributes: FieldParamContainerAttributes<'a>, /// Either serde rename all rule or into_params rename all rule if provided. serde_container: &'a SerdeContainer, + /// Container gnerics + generics: &'a Generics, } impl Param<'_> { @@ -458,6 +461,7 @@ impl ToTokensDiagnostics for Param<'_> { description: None, deprecated: None, object_name: "", + is_generics_type_arg: self.generics.any_match_type_tree(&component), })?; let schema_tokens = crate::as_tokens_or_diagnostics!(&schema); diff --git a/utoipa-gen/src/component/schema.rs b/utoipa-gen/src/component/schema.rs index 4322cbbc..727f3138 100644 --- a/utoipa-gen/src/component/schema.rs +++ b/utoipa-gen/src/component/schema.rs @@ -12,7 +12,7 @@ use crate::{ as_tokens_or_diagnostics, component::features::attributes::{Example, Rename, ValueType}, doc_comment::CommentAttributes, - Array, Deprecated, Diagnostics, OptionExt, ToTokensDiagnostics, + Array, Deprecated, Diagnostics, GenericsExt, OptionExt, ToTokensDiagnostics, }; use self::{ @@ -84,6 +84,7 @@ impl ToTokensDiagnostics for Schema<'_> { self.attributes, ident, None::>, + self.generics, )?; let (_, ty_generics, where_clause) = self.generics.split_for_impl(); @@ -109,6 +110,7 @@ impl ToTokensDiagnostics for Schema<'_> { alias_type_tree? .children .map(|children| children.into_iter().zip(schema_children)), + &Generics::default(), ) .and_then(|variant| { let mut alias_tokens = TokenStream::new(); @@ -169,9 +171,14 @@ impl ToTokensDiagnostics for Schema<'_> { variant.to_tokens(&mut variant_tokens)?; tokens.extend(quote! { + impl #impl_generics utoipa::PartialSchema for #ident #ty_generics #where_clause { + fn schema() -> utoipa::openapi::RefOr { + #variant_tokens.into() + } + } impl #impl_generics utoipa::ToSchema #schema_generics for #ident #ty_generics #where_clause { - fn schema() -> (& #life str, utoipa::openapi::RefOr) { - (#name, #variant_tokens.into()) + fn name() -> std::borrow::Cow<#life, str> { + std::borrow::Cow::Borrowed(#name) } #aliases @@ -197,6 +204,7 @@ impl<'a> SchemaVariant<'a> { attributes: &'a [Attribute], ident: &'a Ident, aliases: Option, + generics: &'a Generics, ) -> Result, Diagnostics> { match data { Data::Struct(content) => match &content.fields { @@ -216,6 +224,7 @@ impl<'a> SchemaVariant<'a> { features: unnamed_features, fields: unnamed, schema_as, + generics, })) } Fields::Named(fields) => { @@ -236,6 +245,7 @@ impl<'a> SchemaVariant<'a> { fields: named, schema_as, aliases: aliases.map(|aliases| aliases.into_iter().collect()), + generics, })) } Fields::Unit => Ok(Self::Unit(UnitStructVariant)), @@ -244,6 +254,7 @@ impl<'a> SchemaVariant<'a> { Cow::Owned(ident.to_string()), &content.variants, attributes, + generics, )?)), _ => Err(Diagnostics::with_span( ident.span(), @@ -297,6 +308,7 @@ pub struct NamedStructSchema<'a> { pub rename_all: Option, pub aliases: Option, &'a TypeTree<'a>)>>, pub schema_as: Option, + pub generics: &'a Generics, } #[cfg_attr(feature = "debug", derive(Debug))] @@ -391,6 +403,7 @@ impl NamedStructSchema<'_> { description: Some(description), deprecated: deprecated.as_ref(), object_name: self.struct_name.as_ref(), + is_generics_type_arg: self.generics.any_match_type_tree(type_tree), }; if is_flatten(field_rules) && type_tree.is_map() { Property::FlattenedMap(FlattenedMapSchema::new(cs)?) @@ -588,6 +601,7 @@ struct UnnamedStructSchema<'a> { attributes: &'a [Attribute], features: Option>, schema_as: Option, + generics: &'a Generics, } impl ToTokensDiagnostics for UnnamedStructSchema<'_> { @@ -646,14 +660,15 @@ impl ToTokensDiagnostics for UnnamedStructSchema<'_> { .as_ref() .map(ComponentDescription::Description) .or(Some(ComponentDescription::CommentAttributes(&comments))); - + let type_tree = override_type_tree.as_ref().unwrap_or(first_part); tokens.extend( ComponentSchema::new(super::ComponentSchemaProps { - type_tree: override_type_tree.as_ref().unwrap_or(first_part), + type_tree, features: unnamed_struct_features, description: description.as_ref(), deprecated: deprecated.as_ref(), object_name: self.struct_name.as_ref(), + is_generics_type_arg: self.generics.any_match_type_tree(type_tree), })? .to_token_stream(), ); @@ -705,6 +720,7 @@ impl<'e> EnumSchema<'e> { enum_name: Cow<'e, str>, variants: &'e Punctuated, attributes: &'e [Attribute], + generics: &'e Generics, ) -> Result { if variants .iter() @@ -820,6 +836,7 @@ impl<'e> EnumSchema<'e> { variants, rename_all, enum_features, + generics, }), schema_as, }) @@ -1063,6 +1080,7 @@ struct ComplexEnum<'a> { enum_name: Cow<'a, str>, enum_features: Vec, rename_all: Option, + generics: &'a Generics, } impl ComplexEnum<'_> { @@ -1109,6 +1127,7 @@ impl ComplexEnum<'_> { fields: &named_fields.named, aliases: None, schema_as: None, + generics: self.generics }), })) } @@ -1142,6 +1161,7 @@ impl ComplexEnum<'_> { features: Some(unnamed_struct_features), fields: &unnamed_fields.unnamed, schema_as: None, + generics: self.generics }), })) } @@ -1210,6 +1230,7 @@ impl ComplexEnum<'_> { fields: &named_fields.named, aliases: None, schema_as: None, + generics: self.generics })) } Fields::Unnamed(unnamed_fields) => { @@ -1226,6 +1247,7 @@ impl ComplexEnum<'_> { features: Some(unnamed_struct_features), fields: &unnamed_fields.unnamed, schema_as: None, + generics: self.generics })) } Fields::Unit => { @@ -1277,6 +1299,7 @@ impl ComplexEnum<'_> { fields: &named_fields.named, aliases: None, schema_as: None, + generics: self.generics, }; let named_enum_tokens = as_tokens_or_diagnostics!(&named_enum); let title = title_features @@ -1318,6 +1341,7 @@ impl ComplexEnum<'_> { features: Some(unnamed_struct_features), fields: &unnamed_fields.unnamed, schema_as: None, + generics: self.generics, }; let unnamed_enum_tokens = as_tokens_or_diagnostics!(&unnamed_enum); @@ -1440,6 +1464,7 @@ impl ComplexEnum<'_> { fields: &named_fields.named, aliases: None, schema_as: None, + generics: self.generics, }; let named_enum_tokens = as_tokens_or_diagnostics!(&named_enum); let title = title_features @@ -1484,6 +1509,7 @@ impl ComplexEnum<'_> { features: Some(unnamed_struct_features), fields: &unnamed_fields.unnamed, schema_as: None, + generics: self.generics, }; let unnamed_enum_tokens = as_tokens_or_diagnostics!(&unnamed_enum); diff --git a/utoipa-gen/src/ext.rs b/utoipa-gen/src/ext.rs index 6bc72252..fdad04ea 100644 --- a/utoipa-gen/src/ext.rs +++ b/utoipa-gen/src/ext.rs @@ -137,6 +137,8 @@ impl ToTokensDiagnostics for RequestBody<'_> { description: None, deprecated: None, object_name: "", + // Currently Request body cannot know about possible generic types + is_generics_type_arg: false, // TODO check whether this is correct })?); tokens.extend(quote_spanned! {actual_body.span.unwrap()=> diff --git a/utoipa-gen/src/lib.rs b/utoipa-gen/src/lib.rs index 1fa4eccf..cd1e732a 100644 --- a/utoipa-gen/src/lib.rs +++ b/utoipa-gen/src/lib.rs @@ -567,7 +567,8 @@ use self::{ /// ```rust /// # use utoipa::ToSchema; /// # mod custom { -/// # struct NewBar; +/// # #[derive(utoipa::ToSchema)] +/// # pub struct NewBar; /// # } /// # /// # struct Bar; @@ -1214,6 +1215,7 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream { /// /// _**More complete example.**_ /// ```rust +/// # #[derive(utoipa::ToSchema)] /// # struct Pet { /// # id: u64, /// # name: String, @@ -1252,6 +1254,7 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream { /// /// _**More minimal example with the defaults.**_ /// ```rust +/// # #[derive(utoipa::ToSchema)] /// # struct Pet { /// # id: u64, /// # name: String, @@ -1318,10 +1321,16 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream { /// _**Example with multiple return types**_ /// ```rust /// # trait User {} +/// # #[derive(utoipa::ToSchema)] /// # struct User1 { /// # id: String /// # } /// # impl User for User1 {} +/// # #[derive(utoipa::ToSchema)] +/// # struct User2 { +/// # id: String +/// # } +/// # impl User for User2 {} /// #[utoipa::path( /// get, /// path = "/user", @@ -1340,7 +1349,7 @@ pub fn derive_to_schema(input: TokenStream) -> TokenStream { /// /// _**Example with multiple examples on single response.**_ /// ```rust -/// # #[derive(serde::Serialize, serde::Deserialize)] +/// # #[derive(serde::Serialize, serde::Deserialize, utoipa::ToSchema)] /// # struct User { /// # name: String /// # } @@ -2581,6 +2590,7 @@ pub fn schema(input: TokenStream) -> TokenStream { deprecated: None, description: None, object_name: "", + is_generics_type_arg: false, // it cannot be generic struct here }); match schema { @@ -2898,6 +2908,19 @@ impl OptionExt for Option { } } +trait GenericsExt { + fn any_match_type_tree(&self, type_tree: &TypeTree) -> bool; +} + +impl<'g> GenericsExt for &'g syn::Generics { + fn any_match_type_tree(&self, type_tree: &TypeTree) -> bool { + self.params.iter().any(|generic| match generic { + syn::GenericParam::Type(generic_type) => type_tree.match_ident(&generic_type.ident), + _ => false, + }) + } +} + trait ToTokensDiagnostics { fn to_tokens(&self, tokens: &mut TokenStream2) -> Result<(), Diagnostics>; diff --git a/utoipa-gen/src/path/parameter.rs b/utoipa-gen/src/path/parameter.rs index 049368bf..636fcf1e 100644 --- a/utoipa-gen/src/path/parameter.rs +++ b/utoipa-gen/src/path/parameter.rs @@ -185,6 +185,8 @@ impl ToTokensDiagnostics for ParameterSchema<'_> { description: None, deprecated: None, object_name: "", + // TODO check whether this is correct + is_generics_type_arg: false } )?), required, @@ -206,6 +208,8 @@ impl ToTokensDiagnostics for ParameterSchema<'_> { description: None, deprecated: None, object_name: "", + // TODO check whether this is correct + is_generics_type_arg: false } )?), required, diff --git a/utoipa-gen/src/path/request_body.rs b/utoipa-gen/src/path/request_body.rs index 74601364..4c963617 100644 --- a/utoipa-gen/src/path/request_body.rs +++ b/utoipa-gen/src/path/request_body.rs @@ -169,6 +169,7 @@ impl ToTokensDiagnostics for RequestBodyAttr<'_> { description: None, deprecated: None, object_name: "", + is_generics_type_arg: false, })? .to_token_stream() } diff --git a/utoipa-gen/src/path/response.rs b/utoipa-gen/src/path/response.rs index c972c84f..11275202 100644 --- a/utoipa-gen/src/path/response.rs +++ b/utoipa-gen/src/path/response.rs @@ -294,6 +294,7 @@ impl ToTokensDiagnostics for ResponseTuple<'_> { description: None, deprecated: None, object_name: "", + is_generics_type_arg: false, })? .to_token_stream() } @@ -866,6 +867,7 @@ impl ToTokensDiagnostics for Header { description: None, deprecated: None, object_name: "", + is_generics_type_arg: false, })? .to_token_stream(); diff --git a/utoipa-gen/src/path/response/derive.rs b/utoipa-gen/src/path/response/derive.rs index 9f45627e..8575bc20 100644 --- a/utoipa-gen/src/path/response/derive.rs +++ b/utoipa-gen/src/path/response/derive.rs @@ -352,6 +352,7 @@ impl NamedStructResponse<'_> { rename_all: None, struct_name: Cow::Owned(ident.to_string()), schema_as: None, + generics: &Generics::default(), }; let ty = Self::to_type(ident); @@ -438,6 +439,7 @@ impl<'p> ToResponseNamedStructResponse<'p> { struct_name: Cow::Owned(ident.to_string()), rename_all: None, schema_as: None, + generics: &Generics::default(), }; let response_type = PathType::InlineSchema(inline_schema.to_token_stream(), ty); @@ -564,8 +566,13 @@ impl<'r> EnumResponse<'r> { description, }); response_value.response_type = if content.is_empty() { - let inline_schema = - EnumSchema::new(Cow::Owned(ident.to_string()), variants, attributes)?; + let generics = Generics::default(); + let inline_schema = EnumSchema::new( + Cow::Owned(ident.to_string()), + variants, + attributes, + &generics, + )?; Some(PathType::InlineSchema( inline_schema.into_token_stream(), diff --git a/utoipa-gen/tests/path_derive_axum_test.rs b/utoipa-gen/tests/path_derive_axum_test.rs index 929f779a..b5e808c3 100644 --- a/utoipa-gen/tests/path_derive_axum_test.rs +++ b/utoipa-gen/tests/path_derive_axum_test.rs @@ -145,6 +145,7 @@ fn get_todo_with_path_tuple() { #[test] fn get_todo_with_extension() { + #[derive(utoipa::ToSchema)] struct Todo { #[allow(unused)] id: i32, diff --git a/utoipa-gen/tests/request_body_derive_test.rs b/utoipa-gen/tests/request_body_derive_test.rs index 57aa37d9..50d2c678 100644 --- a/utoipa-gen/tests/request_body_derive_test.rs +++ b/utoipa-gen/tests/request_body_derive_test.rs @@ -39,7 +39,7 @@ fn derive_path_request_body_simple_success() { #[openapi(paths(derive_request_body_simple::post_foo))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); assert_value! {doc=> "paths.~1foo.post.requestBody.content.application~1json.schema.$ref" = r###""#/components/schemas/Foo""###, "Request body content object type" @@ -60,7 +60,7 @@ fn derive_path_request_body_simple_array_success() { #[openapi(paths(derive_request_body_simple_array::post_foo))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); assert_value! {doc=> "paths.~1foo.post.requestBody.content.application~1json.schema.$ref" = r###"null"###, "Request body content object type" @@ -83,7 +83,7 @@ fn derive_request_body_option_array_success() { #[openapi(paths(derive_request_body_option_array::post_foo))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let body = doc.pointer("/paths/~1foo/post/requestBody").unwrap(); assert_json_eq!( @@ -115,7 +115,7 @@ fn derive_request_body_primitive_simple_success() { #[openapi(paths(derive_request_body_primitive_simple::post_foo))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); assert_value! {doc=> "paths.~1foo.post.requestBody.content.application~1json.schema.$ref" = r###"null"###, "Request body content object type not application/json" @@ -138,7 +138,7 @@ fn derive_request_body_primitive_array_success() { #[openapi(paths(derive_request_body_primitive_simple_array::post_foo))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let content = doc .pointer("/paths/~1foo/post/requestBody/content") .unwrap(); @@ -172,7 +172,7 @@ fn derive_request_body_complex_success() { #[openapi(paths(derive_request_body_complex::post_foo))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let request_body: &Value = doc.pointer("/paths/~1foo/post/requestBody").unwrap(); @@ -203,7 +203,7 @@ fn derive_request_body_complex_multi_content_type_success() { #[openapi(paths(derive_request_body_complex_multi_content_type::post_foo))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let request_body: &Value = doc.pointer("/paths/~1foo/post/requestBody").unwrap(); @@ -239,7 +239,7 @@ fn derive_request_body_complex_success_inline() { #[openapi(paths(derive_request_body_complex_inline::post_foo))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let request_body: &Value = doc.pointer("/paths/~1foo/post/requestBody").unwrap(); @@ -280,7 +280,7 @@ fn derive_request_body_complex_success_array() { #[openapi(paths(derive_request_body_complex_array::post_foo))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let request_body: &Value = doc.pointer("/paths/~1foo/post/requestBody").unwrap(); @@ -314,7 +314,7 @@ fn derive_request_body_complex_success_inline_array() { #[openapi(paths(derive_request_body_complex_inline_array::post_foo))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let request_body: &Value = doc.pointer("/paths/~1foo/post/requestBody").unwrap(); @@ -358,7 +358,7 @@ fn derive_request_body_simple_inline_success() { #[openapi(paths(derive_request_body_simple_inline::post_foo))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let request_body: &Value = doc.pointer("/paths/~1foo/post/requestBody").unwrap(); assert_json_eq!( @@ -397,7 +397,7 @@ fn derive_request_body_complex_required_explicit_false_success() { #[openapi(paths(derive_request_body_complex_required_explicit::post_foo))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let body = doc.pointer("/paths/~1foo/post/requestBody").unwrap(); assert_json_eq!( @@ -434,7 +434,7 @@ fn derive_request_body_complex_primitive_array_success() { #[openapi(paths(derive_request_body_complex_primitive_array::post_foo))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let content = doc .pointer("/paths/~1foo/post/requestBody/content") .unwrap(); @@ -456,9 +456,31 @@ fn derive_request_body_complex_primitive_array_success() { ); } -test_fn! { - module: derive_request_body_ref_path, - body: = path::to::Foo +#[allow(unused)] +mod derive_request_body_ref_path { + #[derive(utoipa::ToSchema)] + #[doc = r" Some struct"] + pub struct Foo { + #[doc = r" Some name"] + name: String, + } + + mod path { + pub mod to { + #[derive(utoipa::ToSchema)] + pub struct Foo; + } + } + + #[utoipa::path( + post, + path = "/foo", + request_body = path::to::Foo, + responses( + (status = 200, description = "success response") + ) + )] + fn post_foo() {} } #[test] @@ -479,7 +501,7 @@ fn derive_request_body_ref_path_success() { )] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let schemas = doc.pointer("/components/schemas").unwrap(); assert!(schemas.get("path.to.Foo").is_some()); @@ -505,7 +527,7 @@ fn unit_type_request_body() { #[openapi(paths(unit_type_test))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let request_body = doc .pointer("/paths/~1unit_type_test/post/requestBody") .unwrap(); @@ -541,7 +563,7 @@ fn request_body_with_example() { #[openapi(components(schemas(Foo)), paths(get_item))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let content = doc .pointer("/paths/~1item/get/requestBody/content") @@ -586,7 +608,7 @@ fn request_body_with_examples() { #[openapi(components(schemas(Foo)), paths(get_item))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let content = doc .pointer("/paths/~1item/get/requestBody/content") @@ -625,7 +647,7 @@ fn request_body_with_binary() { #[openapi(paths(get_item))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let content = doc .pointer("/paths/~1item/get/requestBody/content") @@ -658,7 +680,7 @@ fn request_body_with_external_ref() { #[openapi(paths(get_item))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let content = doc .pointer("/paths/~1item/get/requestBody/content") diff --git a/utoipa-gen/tests/response_derive_test.rs b/utoipa-gen/tests/response_derive_test.rs index c29a8759..92b09b38 100644 --- a/utoipa-gen/tests/response_derive_test.rs +++ b/utoipa-gen/tests/response_derive_test.rs @@ -298,11 +298,13 @@ fn derive_response_multiple_examples() { #[test] fn derive_response_with_enum_contents() { + #[derive(utoipa::ToSchema)] #[allow(unused)] struct Admin { name: String, } #[allow(unused)] + #[derive(utoipa::ToSchema)] struct Moderator { name: String, } diff --git a/utoipa-gen/tests/schema_derive_test.rs b/utoipa-gen/tests/schema_derive_test.rs index c7a35e7a..687e3a08 100644 --- a/utoipa-gen/tests/schema_derive_test.rs +++ b/utoipa-gen/tests/schema_derive_test.rs @@ -21,10 +21,10 @@ macro_rules! api_doc { } }; ( @schema $ident:ident < $($life:lifetime , )? $generic:ident > $($tt:tt)* ) => { - <$ident<$generic> as utoipa::ToSchema>::schema().1 + <$ident<$generic> as utoipa::PartialSchema>::schema() }; ( @schema $ident:ident $($tt:tt)* ) => { - <$ident as utoipa::ToSchema>::schema().1 + <$ident as utoipa::PartialSchema>::schema() }; } @@ -307,6 +307,7 @@ fn derive_struct_with_default_attr() { #[test] fn derive_struct_with_default_attr_field() { + #[derive(ToSchema)] struct Book; let owner = api_doc! { struct Owner { @@ -416,6 +417,7 @@ fn derive_struct_with_serde_default_attr() { #[test] fn derive_struct_with_optional_properties() { + #[derive(ToSchema)] struct Book; let owner = api_doc! { struct Owner { @@ -473,6 +475,7 @@ fn derive_struct_with_optional_properties() { #[test] fn derive_struct_with_comments() { + #[derive(ToSchema)] struct Foobar; let account = api_doc! { /// This is user account dto object @@ -905,6 +908,7 @@ fn derive_struct_with_cow() { #[test] fn derive_with_box_and_refcell() { #[allow(unused)] + #[derive(ToSchema)] struct Foo { name: &'static str, } @@ -1153,7 +1157,7 @@ fn derive_struct_unnamed_field_reference_with_comment() { /// Derive a complex enum with named and unnamed fields. #[test] fn derive_complex_unnamed_field_reference_with_comment() { - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct CommentedReference(String); let value: Value = api_doc! { @@ -1285,7 +1289,7 @@ fn derive_complex_enum_with_schema_properties() { // TODO fixme https://github.com/juhaku/utoipa/issues/285#issuecomment-1249625860 #[test] fn derive_enum_with_unnamed_single_field_with_tag() { - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct ReferenceValue(String); let value: Value = api_doc! { @@ -1328,7 +1332,7 @@ fn derive_enum_with_unnamed_single_field_with_tag() { #[test] fn derive_enum_with_named_fields_with_reference_with_tag() { - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct ReferenceValue(String); let value: Value = api_doc! { @@ -1413,7 +1417,7 @@ fn derive_enum_with_named_fields_with_reference_with_tag() { /// Derive a complex enum with named and unnamed fields. #[test] fn derive_complex_enum() { - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct Foo(String); let value: Value = api_doc! { @@ -1477,7 +1481,7 @@ fn derive_complex_enum() { #[test] fn derive_complex_enum_title() { - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct Foo(String); let value: Value = api_doc! { @@ -1540,7 +1544,7 @@ fn derive_complex_enum_title() { #[test] fn derive_complex_enum_example() { - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct Foo(String); let value: Value = api_doc! { @@ -1605,7 +1609,7 @@ fn derive_complex_enum_example() { #[test] fn derive_complex_enum_serde_rename_all() { - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct Foo(String); let value: Value = api_doc! { @@ -1670,7 +1674,7 @@ fn derive_complex_enum_serde_rename_all() { #[test] fn derive_complex_enum_serde_rename_variant() { - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct Foo(String); let value: Value = api_doc! { @@ -2082,13 +2086,13 @@ fn derive_complex_enum_serde_tag() { #[test] fn derive_serde_flatten() { - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct Metadata { category: String, total: u64, } - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct Record { amount: i64, description: String, @@ -2096,7 +2100,7 @@ fn derive_serde_flatten() { metadata: Metadata, } - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct Pagination { page: i64, next_page: i64, @@ -2210,7 +2214,7 @@ fn derive_complex_enum_serde_untagged() { #[test] fn derive_complex_enum_with_ref_serde_untagged() { - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct Foo { name: String, age: u32, @@ -2403,7 +2407,7 @@ fn derive_complex_enum_serde_adjacently_tagged() { #[test] fn derive_complex_enum_with_ref_serde_adjacently_tagged() { - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct Foo { name: String, age: u32, @@ -3067,6 +3071,12 @@ fn derive_struct_component_field_type_override() { #[test] fn derive_struct_component_field_type_path_override() { + mod path { + pub mod to { + #[derive(utoipa::ToSchema)] + pub struct Foo; + } + } let post = api_doc! { struct Post { id: i32, @@ -3186,13 +3196,14 @@ fn derive_struct_override_type_with_object_type() { #[test] fn derive_struct_override_type_with_a_reference() { mod custom { + #[derive(utoipa::ToSchema)] #[allow(dead_code)] - struct NewBar; + pub struct NewBar; } let value = api_doc! { struct Value { - #[schema(value_type = NewBar)] + #[schema(value_type = custom::NewBar)] field: String, } }; @@ -3203,7 +3214,7 @@ fn derive_struct_override_type_with_a_reference() { "type": "object", "properties": { "field": { - "$ref": "#/components/schemas/NewBar" + "$ref": "#/components/schemas/custom.NewBar" } }, "required": ["field"] @@ -3402,7 +3413,7 @@ fn derive_parse_serde_simple_enum_attributes() { #[test] fn derive_parse_serde_complex_enum() { - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct Foo; let complex_enum = api_doc! { #[derive(Serialize)] @@ -3460,6 +3471,7 @@ fn derive_component_with_generic_types_having_path_expression() { #[test] fn derive_component_with_aliases() { + #[derive(ToSchema)] struct A; #[derive(Debug, OpenApi)] @@ -3484,6 +3496,7 @@ fn derive_component_with_aliases() { #[test] fn derive_complex_enum_as() { + #[derive(ToSchema)] struct Foobar; #[derive(ToSchema)] @@ -3497,7 +3510,7 @@ fn derive_complex_enum_as() { #[openapi(components(schemas(BarBar)))] struct ApiDoc; - let doc = serde_json::to_value(&ApiDoc::openapi()).unwrap(); + let doc = serde_json::to_value(ApiDoc::openapi()).unwrap(); let value = doc .pointer("/components/schemas/named.BarBar") .expect("Should have BarBar named to named.BarBar"); @@ -4063,6 +4076,7 @@ fn derive_struct_with_vec_field_with_example() { #[test] fn derive_struct_field_with_example() { + #[derive(ToSchema)] struct MyStruct; let doc = api_doc! { struct MyValue { @@ -4734,6 +4748,7 @@ fn derive_struct_with_unit_alias() { #[test] fn derive_struct_with_deprecated_fields() { + #[derive(ToSchema)] struct Foobar; let account = api_doc! { struct Account { @@ -4794,6 +4809,7 @@ fn derive_struct_with_deprecated_fields() { #[test] fn derive_struct_with_schema_deprecated_fields() { + #[derive(ToSchema)] struct Foobar; let account = api_doc! { struct AccountA { @@ -5002,7 +5018,7 @@ fn derive_nullable_tuple() { #[test] fn derive_unit_type_untagged_enum() { - #[derive(Serialize)] + #[derive(Serialize, ToSchema)] struct AggregationRequest; let value = api_doc! { @@ -5462,6 +5478,7 @@ fn derive_simple_enum_description_override() { #[test] fn derive_complex_enum_description_override() { #[allow(unused)] + #[derive(ToSchema)] struct User { name: &'static str, } diff --git a/utoipa-gen/tests/utoipa_gen_test.rs b/utoipa-gen/tests/utoipa_gen_test.rs index 652a3fac..fe2f12ae 100644 --- a/utoipa-gen/tests/utoipa_gen_test.rs +++ b/utoipa-gen/tests/utoipa_gen_test.rs @@ -145,10 +145,10 @@ impl Modify for Foo { } } -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, ToSchema)] struct Foo; -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, ToSchema)] struct FooResources; #[test] diff --git a/utoipa/src/lib.rs b/utoipa/src/lib.rs index 7073a169..03350965 100644 --- a/utoipa/src/lib.rs +++ b/utoipa/src/lib.rs @@ -268,6 +268,7 @@ pub mod openapi; +use std::borrow::Cow; use std::collections::BTreeMap; #[cfg(feature = "macros")] @@ -354,46 +355,52 @@ pub trait OpenApi { /// # } /// # /// impl<'__s> utoipa::ToSchema<'__s> for Pet { -/// fn schema() -> (&'__s str, utoipa::openapi::RefOr) { -/// ( -/// "Pet", -/// utoipa::openapi::ObjectBuilder::new() -/// .property( -/// "id", -/// utoipa::openapi::ObjectBuilder::new() -/// .schema_type(utoipa::openapi::schema::Type::Integer) -/// .format(Some(utoipa::openapi::SchemaFormat::KnownFormat( -/// utoipa::openapi::KnownFormat::Int64, -/// ))), -/// ) -/// .required("id") -/// .property( -/// "name", -/// utoipa::openapi::ObjectBuilder::new() -/// .schema_type(utoipa::openapi::schema::Type::String), -/// ) -/// .required("name") -/// .property( -/// "age", -/// utoipa::openapi::ObjectBuilder::new() -/// .schema_type(utoipa::openapi::schema::Type::Integer) -/// .format(Some(utoipa::openapi::SchemaFormat::KnownFormat( -/// utoipa::openapi::KnownFormat::Int32, -/// ))), -/// ) -/// .example(Some(serde_json::json!({ -/// "name":"bob the cat","id":1 -/// }))) -/// .into(), -/// ) } +/// fn name() -> std::borrow::Cow<'__s, str> { +/// std::borrow::Cow::Borrowed("Pet") +/// } +/// } +/// impl utoipa::PartialSchema for Pet { +/// fn schema() -> utoipa::openapi::RefOr { +/// utoipa::openapi::ObjectBuilder::new() +/// .property( +/// "id", +/// utoipa::openapi::ObjectBuilder::new() +/// .schema_type(utoipa::openapi::schema::Type::Integer) +/// .format(Some(utoipa::openapi::SchemaFormat::KnownFormat( +/// utoipa::openapi::KnownFormat::Int64, +/// ))), +/// ) +/// .required("id") +/// .property( +/// "name", +/// utoipa::openapi::ObjectBuilder::new() +/// .schema_type(utoipa::openapi::schema::Type::String), +/// ) +/// .required("name") +/// .property( +/// "age", +/// utoipa::openapi::ObjectBuilder::new() +/// .schema_type(utoipa::openapi::schema::Type::Integer) +/// .format(Some(utoipa::openapi::SchemaFormat::KnownFormat( +/// utoipa::openapi::KnownFormat::Int32, +/// ))), +/// ) +/// .example(Some(serde_json::json!({ +/// "name":"bob the cat","id":1 +/// }))) +/// .into() +/// } /// } /// ``` -pub trait ToSchema<'__s> { +pub trait ToSchema<'__s>: PartialSchema { /// Return a tuple of name and schema or reference to a schema that can be referenced by the /// name or inlined directly to responses, request bodies or parameters. - fn schema() -> (&'__s str, openapi::RefOr); + fn name() -> Cow<'__s, str>; + // /// Return a tuple of name and schema or reference to a schema that can be referenced by the + // /// name or inlined directly to responses, request bodies or parameters. + // fn schema() -> (&'__s str, openapi::RefOr); - /// Optional set of alias schemas for the [`ToSchema::schema`]. + /// Optional set of alias schemas for the [`PartialSchema::schema`]. /// /// Typically there is no need to manually implement this method but it is instead implemented /// by derive [`macro@ToSchema`] when `#[aliases(...)]` attribute is defined. @@ -404,7 +411,7 @@ pub trait ToSchema<'__s> { impl<'__s, T: ToSchema<'__s>> From for openapi::RefOr { fn from(_: T) -> Self { - T::schema().1 + T::schema() } } @@ -413,9 +420,15 @@ impl<'__s, T: ToSchema<'__s>> From for openapi::RefOr openapi::RefOr { + openapi::schema::empty().into() + } +} + impl<'__s> ToSchema<'__s> for TupleUnit { - fn schema() -> (&'__s str, openapi::RefOr) { - ("TupleUnit", openapi::schema::empty().into()) + fn name() -> Cow<'__s, str> { + Cow::Borrowed("TupleUnit") } } @@ -675,6 +688,7 @@ impl<'__s, K: PartialSchema, V: ToSchema<'__s>> PartialSchema /// /// Use `#[utoipa::path(..)]` to implement Path trait /// ```rust +/// # #[derive(utoipa::ToSchema)] /// # struct Pet { /// # id: u64, /// # name: String, diff --git a/utoipa/src/openapi/schema.rs b/utoipa/src/openapi/schema.rs index 0d80fd53..910c78a1 100644 --- a/utoipa/src/openapi/schema.rs +++ b/utoipa/src/openapi/schema.rs @@ -142,7 +142,9 @@ impl ComponentsBuilder { // are created when the main schema is a generic type which should be included in OpenAPI // spec in its generic form. if aliases.is_empty() { - let (name, schema) = I::schema(); + let name = I::name(); + let schema = I::schema(); + // let (name, schema) = I::schema(); self.schemas.insert(name.to_string(), schema); }