diff --git a/utoipa-gen/src/component/schema.rs b/utoipa-gen/src/component/schema.rs index 174ddf72..02f69602 100644 --- a/utoipa-gen/src/component/schema.rs +++ b/utoipa-gen/src/component/schema.rs @@ -683,19 +683,6 @@ impl ComplexEnum<'_> { } Fields::Unnamed(unnamed_fields) => { if unnamed_fields.unnamed.len() == 1 { - let is_reference = unnamed_fields.unnamed.iter().any(|field| { - let ty = TypeTree::from_type(&field.ty); - - ty.value_type == ValueType::Object - }); - - if is_reference { - abort!( - variant, "Unnamed field enum variant with schema references are not supported with serde `tag = ...` attribute."; - help = "See `https://github.com/juhaku/utoipa/issues/285#issuecomment-1249625860` for more details." - ); - } - let unnamed_enum = UnnamedStructSchema { attributes: &variant.attrs, fields: &unnamed_fields.unnamed, @@ -704,12 +691,31 @@ impl ComplexEnum<'_> { let variant_name_tokens = UnitVariantTokens::to_tokens(&EnumVariantTokens(&variant_name, None)); - quote! { - #unnamed_enum - #variant_title - .schema_type(utoipa::openapi::schema::SchemaType::Object) - .property(#tag, #variant_name_tokens) - .required(#tag) + let is_reference = unnamed_fields.unnamed.iter().any(|field| { + let ty = TypeTree::from_type(&field.ty); + + ty.value_type == ValueType::Object + }); + + if is_reference { + quote! { + utoipa::openapi::schema::AllOfBuilder::new() + #variant_title + .item(#unnamed_enum) + .item(utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(utoipa::openapi::schema::SchemaType::Object) + .property(#tag, #variant_name_tokens) + .required(#tag) + ) + } + } else { + quote! { + #unnamed_enum + #variant_title + .schema_type(utoipa::openapi::schema::SchemaType::Object) + .property(#tag, #variant_name_tokens) + .required(#tag) + } } } else { abort!( @@ -748,6 +754,7 @@ impl ToTokens for ComplexEnum<'_> { let tag = container_rules .as_ref() .and_then(|rules| rules.tag.as_ref()); + // serde, externally tagged format supported by now let items: TokenStream = self .variants diff --git a/utoipa-gen/tests/schema_derive_test.rs b/utoipa-gen/tests/schema_derive_test.rs index 197651ec..ba6d9a28 100644 --- a/utoipa-gen/tests/schema_derive_test.rs +++ b/utoipa-gen/tests/schema_derive_test.rs @@ -835,7 +835,6 @@ fn derive_complex_enum_with_schema_properties() { // TODO fixme https://github.com/juhaku/utoipa/issues/285#issuecomment-1249625860 #[test] -#[ignore = "fix me, see: https://github.com/juhaku/utoipa/issues/285#issuecomment-1249625860"] fn derive_enum_with_unnamed_single_field_with_tag() { #[derive(Serialize)] struct ReferenceValue(String); @@ -844,7 +843,7 @@ fn derive_enum_with_unnamed_single_field_with_tag() { #[derive(Serialize)] #[serde(tag = "enum")] enum EnumWithReference { - Value(String), + Value(ReferenceValue), } }; @@ -853,27 +852,32 @@ fn derive_enum_with_unnamed_single_field_with_tag() { json!({ "oneOf": [ { - "type": "object", - "properties": { - "enum": { - "type": "string", - "enum": ["Value"] - - }, - "Value": { + "allOf": [ + { "$ref": "#/components/schemas/ReferenceValue", }, - }, - "required": ["enum"] - }, + { + "type": "object", + "properties": { + "enum": { + "type": "string", + "enum": ["Value"] + + }, + }, + "required": ["enum"] + }, + ], + } ], + "discriminator": { + "propertyName": "enum" + } }) ); } -// TODO fixme https://github.com/juhaku/utoipa/issues/285#issuecomment-1249625860 #[test] -#[ignore = "fix me, see: https://github.com/juhaku/utoipa/issues/285#issuecomment-1249625860"] fn derive_enum_with_named_fields_with_reference_with_tag() { #[derive(Serialize)] struct ReferenceValue(String); @@ -886,31 +890,73 @@ fn derive_enum_with_named_fields_with_reference_with_tag() { field: ReferenceValue, a: String }, - // UnnamedValue(ReferenceValue), + UnnamedValue(ReferenceValue), UnitValue, } }; - println!("{}", serde_json::to_string_pretty(&value).unwrap()); assert_json_eq!( value, json!({ "oneOf": [ { - "type": "object", "properties": { - "enum": { - "type": "string", - "enum": ["Value"] - + "a": { + "type": "string" }, - "Value": { - "$ref": "#/components/schemas/ReferenceValue", + "enum": { + "enum": [ + "Value" + ], + "type": "string" }, + "field": { + "$ref": "#/components/schemas/ReferenceValue" + } }, - "required": ["enum"] + "required": [ + "field", + "a", + "enum" + ], + "type": "object" }, + { + "allOf": [ + { + "$ref": "#/components/schemas/ReferenceValue", + }, + { + "type": "object", + "properties": { + "enum": { + "type": "string", + "enum": ["UnnamedValue"] + + }, + }, + "required": ["enum"] + } + ], + }, + { + "properties": { + "enum": { + "enum": [ + "UnitValue" + ], + "type": "string" + } + }, + "required": [ + "enum" + ], + "type": "object" + } ], + "discriminator": { + "propertyName": "enum" + } }) ); } diff --git a/utoipa/src/openapi.rs b/utoipa/src/openapi.rs index f704b5ca..bc1fe559 100644 --- a/utoipa/src/openapi.rs +++ b/utoipa/src/openapi.rs @@ -10,8 +10,8 @@ pub use self::{ path::{PathItem, PathItemType, Paths, PathsBuilder}, response::{Response, ResponseBuilder, Responses, ResponsesBuilder}, schema::{ - Array, ArrayBuilder, Components, ComponentsBuilder, Discriminator, Object, ObjectBuilder, - OneOf, OneOfBuilder, Ref, Schema, SchemaFormat, SchemaType, ToArray, + AllOf, AllOfBuilder, Array, ArrayBuilder, Components, ComponentsBuilder, Discriminator, + Object, ObjectBuilder, OneOf, OneOfBuilder, Ref, Schema, SchemaFormat, SchemaType, ToArray, }, security::SecurityRequirement, server::{Server, ServerBuilder, ServerVariable, ServerVariableBuilder}, diff --git a/utoipa/src/openapi/schema.rs b/utoipa/src/openapi/schema.rs index 5f7c24f8..a0dc0bd7 100644 --- a/utoipa/src/openapi/schema.rs +++ b/utoipa/src/openapi/schema.rs @@ -223,12 +223,17 @@ pub enum Schema { /// Defines array schema from another schema. Typically used with /// [`Schema::Object`]. Slice and Vec types are translated to [`Schema::Array`] types. Array(Array), - /// Creates a _OneOf_ type [Discriminator Object][discriminator] schema. This schema + /// Creates a _OneOf_ type [composite Object][composite] schema. This schema /// is used to map multiple schemas together where API endpoint could return any of them. /// [`Schema::OneOf`] is created form complex enum where enum holds other than unit types. /// - /// [discriminator]: https://spec.openapis.org/oas/latest.html#components-object + /// [composite]: https://spec.openapis.org/oas/latest.html#components-object OneOf(OneOf), + + /// Creates a _AnyOf_ type [composite Object][composite] shcema. + /// + /// [composite]: https://spec.openapis.org/oas/latest.html#components-object + AllOf(AllOf) } impl Default for Schema { @@ -280,6 +285,7 @@ pub struct OneOf { #[serde(rename = "oneOf")] pub items: Vec>, + /// Description of the [`OneOf`]. Markdown syntax is supported. #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, @@ -362,9 +368,9 @@ pub struct OneOfBuilder { impl OneOfBuilder { new!(pub OneOfBuilder); - /// Adds a given [`Schema`] to [`OneOf`] [Discriminator Object][discriminator] + /// Adds a given [`Schema`] to [`OneOf`] [Composite Object][composite] /// - /// [discriminator]: https://spec.openapis.org/oas/latest.html#components-object + /// [composite]: https://spec.openapis.org/oas/latest.html#components-object pub fn item>>(mut self, component: I) -> Self { self.items.push(component.into()); @@ -426,6 +432,166 @@ impl From for RefOr { component_from_builder!(OneOfBuilder); +/// AllOf [Composite Object][allof] component holds +/// multiple components together where API endpoint will return a combination of all of them. +/// +/// See [`Schema::AllOf`] for more details. +/// +/// [allof]: https://spec.openapis.org/oas/latest.html#components-object +#[derive(Serialize, Deserialize, Clone, Default)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct AllOf { + /// Components of _AllOf_ component. + #[serde(rename = "allOf")] + pub items: Vec>, + + /// Description of the [`AllOf`]. Markdown syntax is supported. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// Default value which is provided when user has not provided the input in Swagger UI. + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg(feature = "serde_json")] + pub default: Option, + + /// Default value which is provided when user has not provided the input in Swagger UI. + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg(not(feature = "serde_json"))] + pub default: Option, + + /// Example shown in UI of the value for richier documentation. + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg(feature = "serde_json")] + pub example: Option, + + /// Example shown in UI of the value for richier documentation. + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg(not(feature = "serde_json"))] + pub example: Option, + + /// Optional discriminator field can be used to aid deserialization, serialization and validation of a + /// specific schema. + #[serde(skip_serializing_if = "Option::is_none")] + pub discriminator: Option +} + +impl AllOf { + /// Construct a new [`AllOf`] component. + pub fn new() -> Self { + Self { + ..Default::default() + } + } + + /// Construct a new [`AllOf`] component with given capacity. + /// + /// AllOf component is then able to contain number of components without + /// reallocating. + /// + /// # Examples + /// + /// Create [`AllOf`] component with initial capacity of 5. + /// ```rust + /// # use utoipa::openapi::schema::AllOf; + /// let one_of = AllOf::with_capacity(5); + /// ``` + pub fn with_capacity(capacity: usize) -> Self { + Self { + items: Vec::with_capacity(capacity), + ..Default::default() + } + } +} + +/// Builder for [`AllOf`] with chainable configuration methods to create a new [`AllOf`]. +#[derive(Default)] +pub struct AllOfBuilder { + items: Vec>, + + description: Option, + + #[cfg(feature = "serde_json")] + default: Option, + + #[cfg(not(feature = "serde_json"))] + default: Option, + + #[cfg(feature = "serde_json")] + example: Option, + + #[cfg(not(feature = "serde_json"))] + example: Option, + + discriminator: Option +} + +impl AllOfBuilder { + new!(pub AllOfBuilder); + + /// Adds a given [`Schema`] to [`AllOf`] [Composite Object][composite] + /// + /// [composite]: https://spec.openapis.org/oas/latest.html#components-object + pub fn item>>(mut self, component: I) -> Self { + self.items.push(component.into()); + + self + } + + /// Add or change optional description for `AllOf` component. + pub fn description>(mut self, description: Option) -> Self { + set_value!(self description description.map(|description| description.into())) + } + + /// Add or change default value for the object which is provided when user has not provided the input in Swagger UI. + #[cfg(feature = "serde_json")] + pub fn default(mut self, default: Option) -> Self { + set_value!(self default default) + } + + /// Add or change default value for the object which is provided when user has not provided the input in Swagger UI. + #[cfg(not(feature = "serde_json"))] + pub fn default>(mut self, default: Option) -> Self { + set_value!(self default default.map(|default| default.into())) + } + + /// Add or change example shown in UI of the value for richier documentation. + #[cfg(feature = "serde_json")] + pub fn example(mut self, example: Option) -> Self { + set_value!(self example example) + } + + /// Add or change example shown in UI of the value for richier documentation. + #[cfg(not(feature = "serde_json"))] + pub fn example>(mut self, example: Option) -> Self { + set_value!(self example example.map(|example| example.into())) + } + + /// Add or change discriminator field of the composite [`AllOf`] type. + pub fn discriminator(mut self, discriminator: Option) -> Self { + set_value!(self discriminator discriminator) + } + + to_array_builder!(); + + build_fn!(pub AllOf items, description, default, example, discriminator); +} + +from!(AllOf AllOfBuilder items, description, default, example, discriminator); + +impl From for Schema { + fn from(one_of: AllOf) -> Self { + Self::AllOf(one_of) + } +} + +impl From for RefOr { + fn from(one_of: AllOfBuilder) -> Self { + Self::T(Schema::AllOf(one_of.build())) + } +} + +component_from_builder!(AllOfBuilder); + /// Implements subset of [OpenAPI Schema Object][schema] which allows /// adding other [`Schema`]s as **properties** to this [`Schema`]. ///