Skip to content

Commit

Permalink
oapi: Support #[serde(flatten)] for maps. (#516)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrislearn authored Nov 27, 2023
1 parent 96cc7ba commit cb95e64
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 33 deletions.
23 changes: 13 additions & 10 deletions crates/compression/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ pub enum CompressionLevel {
}

/// CompressionAlgo
#[non_exhaustive]
#[derive(Eq, PartialEq, Clone, Copy, Debug, Hash)]
#[non_exhaustive]
pub enum CompressionAlgo {
#[cfg(feature = "brotli")]
#[cfg_attr(docsrs, doc(cfg(feature = "brotli")))]
Expand All @@ -61,34 +61,37 @@ impl FromStr for CompressionAlgo {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
#[cfg(feature = "brotli")]
"br" => Ok(Self::Brotli),
"br" => Ok(CompressionAlgo::Brotli),
#[cfg(feature = "brotli")]
"brotli" => Ok(Self::Brotli),
"brotli" => Ok(CompressionAlgo::Brotli),

#[cfg(feature = "deflate")]
"deflate" => Ok(Self::Deflate),
"deflate" => Ok(CompressionAlgo::Deflate),

#[cfg(feature = "gzip")]
"gzip" => Ok(Self::Gzip),
"gzip" => Ok(CompressionAlgo::Gzip),

#[cfg(feature = "zstd")]
"zstd" => Ok(Self::Zstd),
"zstd" => Ok(CompressionAlgo::Zstd),
_ => Err(format!("unknown compression algorithm: {s}")),
}
}
}

impl Display for CompressionAlgo {
#[allow(unreachable_patterns)]
#[allow(unused_variables)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(feature = "brotli")]
Self::Brotli => write!(f, "br"),
CompressionAlgo::Brotli => write!(f, "br"),
#[cfg(feature = "deflate")]
Self::Deflate => write!(f, "deflate"),
CompressionAlgo::Deflate => write!(f, "deflate"),
#[cfg(feature = "gzip")]
Self::Gzip => write!(f, "gzip"),
CompressionAlgo::Gzip => write!(f, "gzip"),
#[cfg(feature = "zstd")]
Self::Zstd => write!(f, "zstd"),
CompressionAlgo::Zstd => write!(f, "zstd"),
_ => unreachable!(),
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/oapi-macros/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ impl<'c> ComponentSchema {
}
}

fn get_description(comments: Option<&'c CommentAttributes>) -> Option<TokenStream> {
pub(crate) fn get_description(comments: Option<&'c CommentAttributes>) -> Option<TokenStream> {
comments
.and_then(|comments| {
let comment = CommentAttributes::as_formatted_string(comments);
Expand All @@ -442,7 +442,7 @@ impl<'c> ComponentSchema {
.map(|description| quote! { .description(#description) })
}

fn get_deprecated(deprecated: Option<&'c Deprecated>) -> Option<TokenStream> {
pub(crate) fn get_deprecated(deprecated: Option<&'c Deprecated>) -> Option<TokenStream> {
deprecated.map(|deprecated| quote! { .deprecated(#deprecated) })
}
}
Expand Down
68 changes: 68 additions & 0 deletions crates/oapi-macros/src/schema/flattened_map_schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use crate::feature::{pop_feature, Feature, FeaturesExt};
use crate::{ComponentSchema, ComponentSchemaProps};

#[derive(Debug)]
pub(crate) struct FlattenedMapSchema {
tokens: TokenStream,
}

impl<'c> FlattenedMapSchema {
pub(crate) fn new(
ComponentSchemaProps {
type_tree,
features,
description,
deprecated,
object_name,
type_definition,
}: ComponentSchemaProps,
) -> Self {
let mut tokens = TokenStream::new();
let mut features = features.unwrap_or(Vec::new());
let deprecated_stream = ComponentSchema::get_deprecated(deprecated);
let description_stream = ComponentSchema::get_description(description);

let example = features.pop_by(|feature| matches!(feature, Feature::Example(_)));
let nullable = pop_feature!(features => Feature::Nullable(_));
let default = pop_feature!(features => Feature::Default(_));

// Maps are treated as generic objects with no named properties and
// additionalProperties denoting the type
// maps have 2 child schemas and we are interested the second one of them
// which is used to determine the additional properties
let schema_property = ComponentSchema::new(ComponentSchemaProps {
type_tree: type_tree
.children
.as_ref()
.expect("ComponentSchema Map type should have children")
.iter()
.nth(1)
.expect("ComponentSchema Map type should have 2 child"),
features: Some(features),
description: None,
deprecated: None,
object_name,
type_definition,
});

tokens.extend(quote! {
#schema_property
#description_stream
#deprecated_stream
#default
});

example.to_tokens(&mut tokens);
nullable.to_tokens(&mut tokens);

Self { tokens }
}
}

impl ToTokens for FlattenedMapSchema {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.tokens.to_tokens(tokens)
}
}
19 changes: 12 additions & 7 deletions crates/oapi-macros/src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,18 @@ use syn::{punctuated::Punctuated, Attribute, Data, Fields, FieldsNamed, FieldsUn

use crate::feature::{Inline, Symbol};

mod enum_schemas;
mod enum_variant;
mod feature;
mod flattened_map_schema;
mod struct_schemas;
mod xml;

pub(crate) use self::{
enum_schemas::*,
feature::{FromAttributes, NamedFieldStructFeatures, UnnamedFieldStructFeatures},
struct_schemas::*,
flattened_map_schema::*,
xml::XmlAttr,
};

Expand All @@ -20,11 +28,6 @@ use super::{
ComponentSchema, FieldRename, VariantRename,
};

mod enum_schemas;
mod enum_variant;
mod feature;
mod struct_schemas;
mod xml;

pub(crate) struct ToSchema<'a> {
ident: &'a Ident,
Expand Down Expand Up @@ -222,14 +225,16 @@ struct TypeTuple<'a, T>(T, &'a Ident);
#[derive(Debug)]
enum Property {
Schema(ComponentSchema),
WithSchema(Feature),
SchemaWith(Feature),
FlattenedMap(FlattenedMapSchema),
}

impl ToTokens for Property {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Schema(schema) => schema.to_tokens(tokens),
Self::WithSchema(with_schema) => with_schema.to_tokens(tokens),
Self::FlattenedMap(schema) => schema.to_tokens(tokens),
Self::SchemaWith(with_schema) => with_schema.to_tokens(tokens),
}
}
}
Expand Down
58 changes: 44 additions & 14 deletions crates/oapi-macros/src/schema/struct_schemas.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use std::borrow::Cow;

use proc_macro2::TokenStream;
use proc_macro_error::abort;
use quote::{format_ident, quote, ToTokens};
use syn::{punctuated::Punctuated, Attribute, Field, Generics, Token};
use syn::{punctuated::Punctuated, spanned::Spanned, Attribute, Field, Generics, Token};

use crate::{
component::ComponentSchemaProps,
Expand All @@ -17,7 +18,7 @@ use super::{
feature::{FromAttributes, NamedFieldFeatures},
is_flatten, is_not_skipped,
serde::{self, SerdeContainer},
ComponentSchema, FieldRename, Property,
ComponentSchema, FieldRename, FlattenedMapSchema, Property,
};

#[derive(Debug)]
Expand All @@ -44,6 +45,7 @@ impl NamedStructSchema<'_> {
fn field_to_schema_property<R>(
&self,
field: &Field,
flatten: bool,
container_rules: &Option<SerdeContainer>,
yield_: impl FnOnce(NamedStructFieldOptions<'_>) -> R,
) -> R {
Expand Down Expand Up @@ -93,16 +95,21 @@ impl NamedStructSchema<'_> {

yield_(NamedStructFieldOptions {
property: if let Some(with_schema) = with_schema {
Property::WithSchema(with_schema)
Property::SchemaWith(with_schema)
} else {
Property::Schema(ComponentSchema::new(ComponentSchemaProps {
let cs = ComponentSchemaProps {
type_tree,
features: field_features,
description: Some(&comments),
deprecated: deprecated.as_ref(),
object_name: self.struct_name.as_ref(),
type_definition: true,
}))
};
if flatten && type_tree.is_map() {
Property::FlattenedMap(FlattenedMapSchema::new(cs))
} else {
Property::Schema(ComponentSchema::new(cs))
}
},
rename_field_value: rename_field,
required,
Expand All @@ -116,7 +123,7 @@ impl ToTokens for NamedStructSchema<'_> {
let oapi = crate::oapi_crate();
let container_rules = serde::parse_container(self.attributes);

let object_tokens = self
let mut object_tokens = self
.fields
.iter()
.filter_map(|field| {
Expand All @@ -139,6 +146,7 @@ impl ToTokens for NamedStructSchema<'_> {

self.field_to_schema_property(
field,
false,
&container_rules,
|NamedStructFieldOptions {
property,
Expand Down Expand Up @@ -189,23 +197,45 @@ impl ToTokens for NamedStructSchema<'_> {
.collect();

if !flatten_fields.is_empty() {
tokens.extend(quote! {
#oapi::oapi::schema::AllOf::new()
});
let mut flattened_tokens = TokenStream::new();
let mut flattened_map_field = None;

for field in flatten_fields {
self.field_to_schema_property(
field,
true,
&container_rules,
|NamedStructFieldOptions { property, .. }| {
tokens.extend(quote! { .item(#property) });
|NamedStructFieldOptions { property, .. }| match property {
Property::Schema(_) | Property::SchemaWith(_) => {
flattened_tokens.extend(quote! { .item(#property) })
}
Property::FlattenedMap(_) => match flattened_map_field {
None => {
object_tokens
.extend(quote! { .additional_properties(Some(#property)) });
flattened_map_field = Some(field);
}
Some(flattened_map_field) => {
abort!(self.fields,
"The structure `{}` contains multiple flattened map fields.",
self.struct_name;
note = flattened_map_field.span() => "first flattened map field was declared here as `{}`", flattened_map_field.ident.as_ref().unwrap();
note = field.span() => "second flattened map field was declared here as `{}`", field.ident.as_ref().unwrap());
},
},
},
)
}

tokens.extend(quote! {
.item(#object_tokens)
})
if flattened_tokens.is_empty() {
tokens.extend(object_tokens)
} else {
tokens.extend(quote! {
utoipa::openapi::AllOfBuilder::new()
#flattened_tokens
.item(#object_tokens)
})
}
} else {
tokens.extend(object_tokens)
}
Expand Down
5 changes: 5 additions & 0 deletions crates/oapi-macros/src/type_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ impl<'t> TypeTree<'t> {
pub(crate) fn is_option(&self) -> bool {
matches!(self.generic_type, Some(GenericType::Option))
}

/// Check whether the [`TypeTree`]'s `generic_type` is [`GenericType::Map`]
pub(crate) fn is_map(&self) -> bool {
matches!(self.generic_type, Some(GenericType::Map))
}
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
Expand Down

0 comments on commit cb95e64

Please sign in to comment.