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); }