Skip to content

Commit

Permalink
Add discriminator schema (#290)
Browse files Browse the repository at this point in the history
Add discriminator schema. For now if serde `tag` is defined the
discriminator field will be set to the `tag` attribute value in OneOf
composite object. Update docs regarding new functionality where 
`tag` attribute will work as a discriminator field for enums.
  • Loading branch information
juhaku authored Sep 20, 2022
1 parent e0b383f commit 64a5998
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 30 deletions.
47 changes: 24 additions & 23 deletions utoipa-gen/src/component/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,9 +481,9 @@ impl ToTokens for ReprEnum<'_> {
self,
(self.variants, self.attributes, Some(&self.enum_type)),
tokens,
|tokens| {
|tokens, container_rules| {
self.unit_enum_tokens(
serde::parse_container(self.attributes),
container_rules,
(self.variants, self.attributes, Some(&self.enum_type)),
tokens,
)
Expand Down Expand Up @@ -527,9 +527,9 @@ impl ToTokens for SimpleEnum<'_> {
self,
(self.variants, self.attributes, None),
tokens,
|tokens| {
|tokens, container_rules| {
self.unit_enum_tokens(
serde::parse_container(self.attributes),
container_rules,
(self.variants, self.attributes, None),
tokens,
)
Expand Down Expand Up @@ -557,13 +557,14 @@ trait EnumTokens<'e>: ToTokens {
&self,
content: EnumContent,
tokens: &mut TokenStream,
enum_value_to_tokens: impl FnOnce(&mut TokenStream),
enum_value_to_tokens: impl FnOnce(&mut TokenStream, &Option<SerdeContainer>),
) where
Self: Sized,
{
let (_, attributes, _) = content;

enum_value_to_tokens(tokens);
let container_rules = serde::parse_container(attributes);
enum_value_to_tokens(tokens, &container_rules);

let attrs = attr::parse_schema_attr::<SchemaAttr<Enum>>(attributes);
if let Some(attributes) = attrs {
Expand All @@ -583,15 +584,15 @@ trait EnumTokens<'e>: ToTokens {

fn unit_enum_tokens(
&self,
container_rules: Option<SerdeContainer>,
container_rules: &Option<SerdeContainer>,
content: EnumContent,
tokens: &mut TokenStream,
) {
let (variants, _, enum_type) = content;

let enum_values = variants
.iter()
.filter_map(|variant| self.variant_to_tokens_stream(variant, &container_rules))
.filter_map(|variant| self.variant_to_tokens_stream(variant, container_rules))
.collect::<Array<TokenStream>>();

tokens.extend(match container_rules {
Expand Down Expand Up @@ -742,15 +743,11 @@ impl ToTokens for ComplexEnum<'_> {
self,
(self.variants, self.attributes, None),
tokens,
|tokens| {
let mut container_rules = serde::parse_container(self.attributes);
let tag: Option<String> = if let Some(serde_container) = &mut container_rules {
serde_container.tag.take()
} else {
None
};

|tokens, container_rules| {
let capacity = self.variants.len();
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
Expand All @@ -765,16 +762,13 @@ impl ToTokens for ComplexEnum<'_> {
})
.map(|(variant, mut variant_serde_rules)| {
let variant_name = &*variant.ident.to_string();
let variant_name = rename_variant(
&container_rules,
&mut variant_serde_rules,
variant_name,
)
.unwrap_or_else(|| String::from(variant_name));
let variant_name =
rename_variant(container_rules, &mut variant_serde_rules, variant_name)
.unwrap_or_else(|| String::from(variant_name));
let variant_title =
attr::parse_schema_attr::<SchemaAttr<Title>>(&variant.attrs);

if let Some(tag) = &tag {
if let Some(tag) = tag {
Self::tagged_variant_tokens(tag, variant_name, variant_title, variant)
} else {
Self::variant_tokens(variant_name, variant_title, variant)
Expand All @@ -786,11 +780,18 @@ impl ToTokens for ComplexEnum<'_> {
}
})
.collect();
// for now just use tag as a discriminator
let discriminator = tag.map(|tag| {
quote! {
.discriminator(Some(utoipa::openapi::schema::Discriminator::new(#tag)))
}
});

tokens.extend(
quote! {
Into::<utoipa::openapi::schema::OneOfBuilder>::into(utoipa::openapi::OneOf::with_capacity(#capacity))
#items
#discriminator
}
);
},
Expand Down
3 changes: 2 additions & 1 deletion utoipa-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ use ext::ArgumentResolver;
/// * `rename_all = "..."` Supported in container level.
/// * `rename = "..."` Supported **only** in field or variant level.
/// * `skip = "..."` Supported **only** in field or variant level.
/// * `tag = "..."` Supported in container level.
/// * `tag = "..."` Supported in container level. `tag` attribute also works as a [discriminator field][discriminator] for an enum.
/// * `default` Supported in container level and field level according to [serde attributes].
///
/// Other _`serde`_ attributes works as is but does not have any effect on the generated OpenAPI doc.
Expand Down Expand Up @@ -426,6 +426,7 @@ use ext::ArgumentResolver;
/// [into_params]: derive.IntoParams.html
/// [primitive]: https://doc.rust-lang.org/std/primitive/index.html
/// [serde attributes]: https://serde.rs/attributes.html
/// [discriminator]: openapi/schema/struct.Discriminator.html
/// [enum_schema]: derive.ToSchema.html#enum-optional-configuration-options-for-schema
pub fn derive_to_schema(input: TokenStream) -> TokenStream {
let DeriveInput {
Expand Down
9 changes: 9 additions & 0 deletions utoipa-gen/tests/schema_derive_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,9 @@ fn derive_enum_with_unnamed_primitive_field_with_tag() {
"required": ["tag"]
},
],
"discriminator": {
"propertyName": "tag"
}
})
);
}
Expand Down Expand Up @@ -1228,6 +1231,9 @@ fn derive_complex_enum_serde_tag() {
],
},
],
"discriminator": {
"propertyName": "tag"
}
})
);
}
Expand Down Expand Up @@ -1289,6 +1295,9 @@ fn derive_complex_enum_serde_tag_title() {
],
},
],
"discriminator": {
"propertyName": "tag"
}
})
);
}
Expand Down
4 changes: 2 additions & 2 deletions utoipa/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ pub use self::{
path::{PathItem, PathItemType, Paths, PathsBuilder},
response::{Response, ResponseBuilder, Responses, ResponsesBuilder},
schema::{
Array, ArrayBuilder, Components, ComponentsBuilder, Object, ObjectBuilder, OneOf,
OneOfBuilder, Ref, Schema, SchemaFormat, SchemaType, ToArray,
Array, ArrayBuilder, Components, ComponentsBuilder, Discriminator, Object, ObjectBuilder,
OneOf, OneOfBuilder, Ref, Schema, SchemaFormat, SchemaType, ToArray,
},
security::SecurityRequirement,
server::{Server, ServerBuilder, ServerVariable, ServerVariableBuilder},
Expand Down
50 changes: 46 additions & 4 deletions utoipa/src/openapi/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,12 +237,42 @@ impl Default for Schema {
}
}

/// OneOf [Discriminator Object][discriminator] component holds
/// OpenAPI [Discriminator][discriminator] object which can be optionally used together with
/// [`OneOf`] composite object.
///
/// [discriminator]: https://spec.openapis.org/oas/latest.html#discriminator-object
#[derive(Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct Discriminator {
/// Defines a discriminator property name which must be found within all composite
/// objects.
pub property_name: String,
}

impl Discriminator {
/// Construct a new [`Discriminator`] object with property name.
///
/// # Examples
///
/// Create a new [`Discriminator`] object for `pet_type` property.
/// ```rust
/// # use utoipa::openapi::schema::Discriminator;
/// let discriminator = Discriminator::new("pet_type");
/// ```
pub fn new<I: Into<String>>(property_name: I) -> Self {
Self {
property_name: property_name.into(),
}
}
}

/// OneOf [Composite Object][oneof] component holds
/// multiple components together where API endpoint could return any of them.
///
/// See [`Schema::OneOf`] for more details.
///
/// [discriminator]: https://spec.openapis.org/oas/latest.html#components-object
/// [oneof]: https://spec.openapis.org/oas/latest.html#components-object
#[derive(Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "debug", derive(Debug))]
pub struct OneOf {
Expand Down Expand Up @@ -272,6 +302,11 @@ pub struct OneOf {
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg(not(feature = "serde_json"))]
pub example: Option<String>,

/// 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<Discriminator>
}

impl OneOf {
Expand Down Expand Up @@ -320,6 +355,8 @@ pub struct OneOfBuilder {

#[cfg(not(feature = "serde_json"))]
example: Option<String>,

discriminator: Option<Discriminator>
}

impl OneOfBuilder {
Expand Down Expand Up @@ -363,12 +400,17 @@ impl OneOfBuilder {
set_value!(self example example.map(|example| example.into()))
}

/// Add or change discriminator field of the composite [`OneOf`] type.
pub fn discriminator(mut self, discriminator: Option<Discriminator>) -> Self {
set_value!(self discriminator discriminator)
}

to_array_builder!();

build_fn!(pub OneOf items, description, default, example);
build_fn!(pub OneOf items, description, default, example, discriminator);
}

from!(OneOf OneOfBuilder items, description, default, example);
from!(OneOf OneOfBuilder items, description, default, example, discriminator);

impl From<OneOf> for Schema {
fn from(one_of: OneOf) -> Self {
Expand Down

0 comments on commit 64a5998

Please sign in to comment.