Skip to content

Commit

Permalink
add support for #[serde(flatten)] (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
msrd0 authored Aug 29, 2021
1 parent 5264af5 commit 039465d
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 48 deletions.
7 changes: 6 additions & 1 deletion derive/src/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ pub(super) struct FieldAttributes {
pub(super) rename: Option<LitStr>,
/// This field can be skipped during either serialization or deserialization.
pub(super) nullable: bool,
/// This field's fields will be flattened into this field.
pub(super) flatten: bool,
/// This field will always be skipped during serialization.
pub(super) skip_serializing: bool,
/// This field will always be skipped during deserialization.
Expand All @@ -95,7 +97,10 @@ impl FieldAttributes {
self.nullable = true;
},

// TODO Meta::Path(path) if path.is_ident("flatten")
Meta::Path(path) if path.is_ident("flatten") => {
self.flatten = true;
},

Meta::Path(path) if path.is_ident("skip") => {
self.nullable = true;
self.skip_serializing = true;
Expand Down
94 changes: 55 additions & 39 deletions derive/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,49 +33,65 @@ fn gen_struct(name: Option<&LitStr>, fields: &[ParseDataField]) -> TokenStream {
None => quote!(#option::None)
};

let field_name = fields.iter().map(|f| &f.name);
let field_doc = fields.iter().map(|f| gen_doc_option(&f.doc));
let field_schema = fields.iter().map(|f| match &f.ty {
TypeOrInline::Type(ty) => {
quote!(<#ty as ::openapi_type::OpenapiType>::schema())
},
TypeOrInline::Inline(data) => data.gen_schema()
let fields = fields.iter().map(|f| {
let name = &f.name;
let doc = gen_doc_option(&f.doc);
let schema = match &f.ty {
TypeOrInline::Type(ty) => {
quote!(<#ty as ::openapi_type::OpenapiType>::schema())
},
TypeOrInline::Inline(data) => data.gen_schema()
};

if f.flatten {
quote!({
let field_schema = #schema;
::openapi_type::private::flatten(
&mut dependencies,
&mut properties,
&mut required,
field_schema
);
})
} else {
quote!({
const FIELD_NAME: &::core::primitive::str = #name;
const FIELD_DOC: #option<&'static ::core::primitive::str> = #doc;

let mut field_schema = #schema;

// fields in OpenAPI are nullable by default
match field_schema.nullable {
true => field_schema.nullable = false,
false => required.push(::std::string::String::from(FIELD_NAME))
};

let field_schema = ::openapi_type::private::inline_if_unnamed(
&mut dependencies, field_schema, FIELD_DOC
);
let field_schema = match field_schema {
#openapi::ReferenceOr::Item(schema) => {
#openapi::ReferenceOr::Item(::std::boxed::Box::new(schema))
},
#openapi::ReferenceOr::Reference { reference } => {
#openapi::ReferenceOr::Reference { reference }
}
};

properties.insert(
::std::string::String::from(FIELD_NAME),
field_schema
);
})
}
});

quote! {
{
let mut properties = <::openapi_type::indexmap::IndexMap<
::std::string::String,
#openapi::ReferenceOr<::std::boxed::Box<#openapi::Schema>>
>>::new();
let mut required = <::std::vec::Vec<::std::string::String>>::new();

#({
const FIELD_NAME: &::core::primitive::str = #field_name;
const FIELD_DOC: #option<&'static ::core::primitive::str> = #field_doc;

let mut field_schema = #field_schema;

// fields in OpenAPI are nullable by default
match field_schema.nullable {
true => field_schema.nullable = false,
false => required.push(::std::string::String::from(FIELD_NAME))
};

let field_schema = match ::openapi_type::private::inline_if_unnamed(
&mut dependencies, field_schema, FIELD_DOC
) {
#openapi::ReferenceOr::Item(schema) =>
#openapi::ReferenceOr::Item(::std::boxed::Box::new(schema)),
#openapi::ReferenceOr::Reference { reference } =>
#openapi::ReferenceOr::Reference { reference }
};

properties.insert(
::std::string::String::from(FIELD_NAME),
field_schema
);
})*
let mut properties = ::openapi_type::private::Properties::new();
let mut required = ::openapi_type::private::Required::new();

#(#fields)*

let mut schema = ::openapi_type::OpenapiSchema::new(
#openapi::SchemaKind::Type(
Expand Down
21 changes: 14 additions & 7 deletions derive/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ pub(super) enum TypeOrInline {
pub(super) struct ParseDataField {
pub(super) name: LitStr,
pub(super) doc: Vec<String>,
pub(super) ty: TypeOrInline
pub(super) ty: TypeOrInline,
pub(super) flatten: bool
}

#[allow(dead_code)]
Expand Down Expand Up @@ -96,7 +97,8 @@ fn parse_named_fields(named_fields: &FieldsNamed, rename_all: Option<&LitStr>) -
fields.push(ParseDataField {
name,
doc,
ty: TypeOrInline::Type(ty)
ty: TypeOrInline::Type(ty),
flatten: attrs.flatten
});
}
Ok(fields)
Expand Down Expand Up @@ -156,7 +158,8 @@ pub(super) fn parse_enum(ident: &Ident, inum: &DataEnum, attrs: &ContainerAttrib
fields: vec![ParseDataField {
name: tag.clone(),
doc: Vec::new(),
ty: TypeOrInline::Inline(ParseData::Enum(strings))
ty: TypeOrInline::Inline(ParseData::Enum(strings)),
flatten: false
}]
}),
// untagged
Expand All @@ -183,7 +186,8 @@ pub(super) fn parse_enum(ident: &Ident, inum: &DataEnum, attrs: &ContainerAttrib
fields: vec![ParseDataField {
name,
doc: Vec::new(),
ty: TypeOrInline::Inline(data)
ty: TypeOrInline::Inline(data),
flatten: false
}]
}
},
Expand All @@ -193,7 +197,8 @@ pub(super) fn parse_enum(ident: &Ident, inum: &DataEnum, attrs: &ContainerAttrib
ParseData::Struct { fields, .. } => fields.push(ParseDataField {
name: tag.clone(),
doc: Vec::new(),
ty: TypeOrInline::Inline(ParseData::Enum(vec![name]))
ty: TypeOrInline::Inline(ParseData::Enum(vec![name])),
flatten: false
}),
_ => return Err(syn::Error::new(
tag.span(),
Expand All @@ -211,12 +216,14 @@ pub(super) fn parse_enum(ident: &Ident, inum: &DataEnum, attrs: &ContainerAttrib
ParseDataField {
name: tag.clone(),
doc: Vec::new(),
ty: TypeOrInline::Inline(ParseData::Enum(vec![name]))
ty: TypeOrInline::Inline(ParseData::Enum(vec![name])),
flatten: false
},
ParseDataField {
name: content.clone(),
doc: Vec::new(),
ty: TypeOrInline::Inline(data)
ty: TypeOrInline::Inline(data),
flatten: false
},
]
}
Expand Down
43 changes: 42 additions & 1 deletion src/private.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use crate::OpenapiSchema;
use indexmap::IndexMap;
use openapiv3::{ReferenceOr, Schema};
use openapiv3::{ReferenceOr, Schema, SchemaKind, Type};
use std::borrow::Cow;

pub type Dependencies = IndexMap<String, OpenapiSchema>;
pub type Properties = IndexMap<String, ReferenceOr<Box<Schema>>>;
pub type Required = Vec<String>;

fn add_dependencies(dependencies: &mut Dependencies, other: &mut Dependencies) {
while let Some((dep_name, dep_schema)) = other.pop() {
Expand Down Expand Up @@ -36,3 +39,41 @@ pub fn inline_if_unnamed(
}
}
}

struct FlattenError(Cow<'static, str>);

fn flatten_impl(properties: &mut Properties, required: &mut Required, schema: SchemaKind) -> Result<(), FlattenError> {
let mut obj = match schema {
SchemaKind::Type(Type::Object(obj)) => obj,
_ => return Err(FlattenError("Expected object".into()))
};

while let Some((prop_name, prop_schema)) = obj.properties.pop() {
if properties.contains_key(&prop_name) {
return Err(FlattenError("Duplicate property name".into()));
}
properties.insert(prop_name, prop_schema);
}
required.extend(obj.required.into_iter());

Ok(())
}

pub fn flatten(
dependencies: &mut Dependencies,
properties: &mut Properties,
required: &mut Required,
mut schema: OpenapiSchema
) {
add_dependencies(dependencies, &mut schema.dependencies);
match flatten_impl(properties, required, schema.schema) {
Ok(_) => {},
Err(e) => panic!(
concat!(
"Flattening produced an error: {}\n",
"This is likely a bug, please open an issue: https://github.com/msrd0/openapi_type/issues"
),
e.0
)
};
}
24 changes: 24 additions & 0 deletions tests/custom_types_attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,30 @@ test_type!(FieldRename = {
"required": ["bar"]
});

#[derive(OpenapiType)]
struct FieldFlattenInner {
inner: String
}
#[derive(OpenapiType)]
struct FieldFlatten {
outer: String,
#[openapi(flatten)]
flat: FieldFlattenInner
}
test_type!(FieldFlatten = {
"type": "object",
"title": "FieldFlatten",
"properties": {
"inner": {
"type": "string"
},
"outer": {
"type": "string"
}
},
"required": ["outer", "inner"]
});

#[derive(OpenapiType)]
struct FieldSkip {
#[openapi(skip_serializing, skip_deserializing)]
Expand Down

0 comments on commit 039465d

Please sign in to comment.