Skip to content

Commit 039465d

Browse files
authored
add support for #[serde(flatten)] (#17)
1 parent 5264af5 commit 039465d

File tree

5 files changed

+141
-48
lines changed

5 files changed

+141
-48
lines changed

derive/src/attrs.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ pub(super) struct FieldAttributes {
7272
pub(super) rename: Option<LitStr>,
7373
/// This field can be skipped during either serialization or deserialization.
7474
pub(super) nullable: bool,
75+
/// This field's fields will be flattened into this field.
76+
pub(super) flatten: bool,
7577
/// This field will always be skipped during serialization.
7678
pub(super) skip_serializing: bool,
7779
/// This field will always be skipped during deserialization.
@@ -95,7 +97,10 @@ impl FieldAttributes {
9597
self.nullable = true;
9698
},
9799

98-
// TODO Meta::Path(path) if path.is_ident("flatten")
100+
Meta::Path(path) if path.is_ident("flatten") => {
101+
self.flatten = true;
102+
},
103+
99104
Meta::Path(path) if path.is_ident("skip") => {
100105
self.nullable = true;
101106
self.skip_serializing = true;

derive/src/codegen.rs

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -33,49 +33,65 @@ fn gen_struct(name: Option<&LitStr>, fields: &[ParseDataField]) -> TokenStream {
3333
None => quote!(#option::None)
3434
};
3535

36-
let field_name = fields.iter().map(|f| &f.name);
37-
let field_doc = fields.iter().map(|f| gen_doc_option(&f.doc));
38-
let field_schema = fields.iter().map(|f| match &f.ty {
39-
TypeOrInline::Type(ty) => {
40-
quote!(<#ty as ::openapi_type::OpenapiType>::schema())
41-
},
42-
TypeOrInline::Inline(data) => data.gen_schema()
36+
let fields = fields.iter().map(|f| {
37+
let name = &f.name;
38+
let doc = gen_doc_option(&f.doc);
39+
let schema = match &f.ty {
40+
TypeOrInline::Type(ty) => {
41+
quote!(<#ty as ::openapi_type::OpenapiType>::schema())
42+
},
43+
TypeOrInline::Inline(data) => data.gen_schema()
44+
};
45+
46+
if f.flatten {
47+
quote!({
48+
let field_schema = #schema;
49+
::openapi_type::private::flatten(
50+
&mut dependencies,
51+
&mut properties,
52+
&mut required,
53+
field_schema
54+
);
55+
})
56+
} else {
57+
quote!({
58+
const FIELD_NAME: &::core::primitive::str = #name;
59+
const FIELD_DOC: #option<&'static ::core::primitive::str> = #doc;
60+
61+
let mut field_schema = #schema;
62+
63+
// fields in OpenAPI are nullable by default
64+
match field_schema.nullable {
65+
true => field_schema.nullable = false,
66+
false => required.push(::std::string::String::from(FIELD_NAME))
67+
};
68+
69+
let field_schema = ::openapi_type::private::inline_if_unnamed(
70+
&mut dependencies, field_schema, FIELD_DOC
71+
);
72+
let field_schema = match field_schema {
73+
#openapi::ReferenceOr::Item(schema) => {
74+
#openapi::ReferenceOr::Item(::std::boxed::Box::new(schema))
75+
},
76+
#openapi::ReferenceOr::Reference { reference } => {
77+
#openapi::ReferenceOr::Reference { reference }
78+
}
79+
};
80+
81+
properties.insert(
82+
::std::string::String::from(FIELD_NAME),
83+
field_schema
84+
);
85+
})
86+
}
4387
});
4488

4589
quote! {
4690
{
47-
let mut properties = <::openapi_type::indexmap::IndexMap<
48-
::std::string::String,
49-
#openapi::ReferenceOr<::std::boxed::Box<#openapi::Schema>>
50-
>>::new();
51-
let mut required = <::std::vec::Vec<::std::string::String>>::new();
52-
53-
#({
54-
const FIELD_NAME: &::core::primitive::str = #field_name;
55-
const FIELD_DOC: #option<&'static ::core::primitive::str> = #field_doc;
56-
57-
let mut field_schema = #field_schema;
58-
59-
// fields in OpenAPI are nullable by default
60-
match field_schema.nullable {
61-
true => field_schema.nullable = false,
62-
false => required.push(::std::string::String::from(FIELD_NAME))
63-
};
64-
65-
let field_schema = match ::openapi_type::private::inline_if_unnamed(
66-
&mut dependencies, field_schema, FIELD_DOC
67-
) {
68-
#openapi::ReferenceOr::Item(schema) =>
69-
#openapi::ReferenceOr::Item(::std::boxed::Box::new(schema)),
70-
#openapi::ReferenceOr::Reference { reference } =>
71-
#openapi::ReferenceOr::Reference { reference }
72-
};
73-
74-
properties.insert(
75-
::std::string::String::from(FIELD_NAME),
76-
field_schema
77-
);
78-
})*
91+
let mut properties = ::openapi_type::private::Properties::new();
92+
let mut required = ::openapi_type::private::Required::new();
93+
94+
#(#fields)*
7995

8096
let mut schema = ::openapi_type::OpenapiSchema::new(
8197
#openapi::SchemaKind::Type(

derive/src/parser.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ pub(super) enum TypeOrInline {
1717
pub(super) struct ParseDataField {
1818
pub(super) name: LitStr,
1919
pub(super) doc: Vec<String>,
20-
pub(super) ty: TypeOrInline
20+
pub(super) ty: TypeOrInline,
21+
pub(super) flatten: bool
2122
}
2223

2324
#[allow(dead_code)]
@@ -96,7 +97,8 @@ fn parse_named_fields(named_fields: &FieldsNamed, rename_all: Option<&LitStr>) -
9697
fields.push(ParseDataField {
9798
name,
9899
doc,
99-
ty: TypeOrInline::Type(ty)
100+
ty: TypeOrInline::Type(ty),
101+
flatten: attrs.flatten
100102
});
101103
}
102104
Ok(fields)
@@ -156,7 +158,8 @@ pub(super) fn parse_enum(ident: &Ident, inum: &DataEnum, attrs: &ContainerAttrib
156158
fields: vec![ParseDataField {
157159
name: tag.clone(),
158160
doc: Vec::new(),
159-
ty: TypeOrInline::Inline(ParseData::Enum(strings))
161+
ty: TypeOrInline::Inline(ParseData::Enum(strings)),
162+
flatten: false
160163
}]
161164
}),
162165
// untagged
@@ -183,7 +186,8 @@ pub(super) fn parse_enum(ident: &Ident, inum: &DataEnum, attrs: &ContainerAttrib
183186
fields: vec![ParseDataField {
184187
name,
185188
doc: Vec::new(),
186-
ty: TypeOrInline::Inline(data)
189+
ty: TypeOrInline::Inline(data),
190+
flatten: false
187191
}]
188192
}
189193
},
@@ -193,7 +197,8 @@ pub(super) fn parse_enum(ident: &Ident, inum: &DataEnum, attrs: &ContainerAttrib
193197
ParseData::Struct { fields, .. } => fields.push(ParseDataField {
194198
name: tag.clone(),
195199
doc: Vec::new(),
196-
ty: TypeOrInline::Inline(ParseData::Enum(vec![name]))
200+
ty: TypeOrInline::Inline(ParseData::Enum(vec![name])),
201+
flatten: false
197202
}),
198203
_ => return Err(syn::Error::new(
199204
tag.span(),
@@ -211,12 +216,14 @@ pub(super) fn parse_enum(ident: &Ident, inum: &DataEnum, attrs: &ContainerAttrib
211216
ParseDataField {
212217
name: tag.clone(),
213218
doc: Vec::new(),
214-
ty: TypeOrInline::Inline(ParseData::Enum(vec![name]))
219+
ty: TypeOrInline::Inline(ParseData::Enum(vec![name])),
220+
flatten: false
215221
},
216222
ParseDataField {
217223
name: content.clone(),
218224
doc: Vec::new(),
219-
ty: TypeOrInline::Inline(data)
225+
ty: TypeOrInline::Inline(data),
226+
flatten: false
220227
},
221228
]
222229
}

src/private.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
use crate::OpenapiSchema;
22
use indexmap::IndexMap;
3-
use openapiv3::{ReferenceOr, Schema};
3+
use openapiv3::{ReferenceOr, Schema, SchemaKind, Type};
4+
use std::borrow::Cow;
45

56
pub type Dependencies = IndexMap<String, OpenapiSchema>;
7+
pub type Properties = IndexMap<String, ReferenceOr<Box<Schema>>>;
8+
pub type Required = Vec<String>;
69

710
fn add_dependencies(dependencies: &mut Dependencies, other: &mut Dependencies) {
811
while let Some((dep_name, dep_schema)) = other.pop() {
@@ -36,3 +39,41 @@ pub fn inline_if_unnamed(
3639
}
3740
}
3841
}
42+
43+
struct FlattenError(Cow<'static, str>);
44+
45+
fn flatten_impl(properties: &mut Properties, required: &mut Required, schema: SchemaKind) -> Result<(), FlattenError> {
46+
let mut obj = match schema {
47+
SchemaKind::Type(Type::Object(obj)) => obj,
48+
_ => return Err(FlattenError("Expected object".into()))
49+
};
50+
51+
while let Some((prop_name, prop_schema)) = obj.properties.pop() {
52+
if properties.contains_key(&prop_name) {
53+
return Err(FlattenError("Duplicate property name".into()));
54+
}
55+
properties.insert(prop_name, prop_schema);
56+
}
57+
required.extend(obj.required.into_iter());
58+
59+
Ok(())
60+
}
61+
62+
pub fn flatten(
63+
dependencies: &mut Dependencies,
64+
properties: &mut Properties,
65+
required: &mut Required,
66+
mut schema: OpenapiSchema
67+
) {
68+
add_dependencies(dependencies, &mut schema.dependencies);
69+
match flatten_impl(properties, required, schema.schema) {
70+
Ok(_) => {},
71+
Err(e) => panic!(
72+
concat!(
73+
"Flattening produced an error: {}\n",
74+
"This is likely a bug, please open an issue: https://github.com/msrd0/openapi_type/issues"
75+
),
76+
e.0
77+
)
78+
};
79+
}

tests/custom_types_attrs.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,30 @@ test_type!(FieldRename = {
9393
"required": ["bar"]
9494
});
9595

96+
#[derive(OpenapiType)]
97+
struct FieldFlattenInner {
98+
inner: String
99+
}
100+
#[derive(OpenapiType)]
101+
struct FieldFlatten {
102+
outer: String,
103+
#[openapi(flatten)]
104+
flat: FieldFlattenInner
105+
}
106+
test_type!(FieldFlatten = {
107+
"type": "object",
108+
"title": "FieldFlatten",
109+
"properties": {
110+
"inner": {
111+
"type": "string"
112+
},
113+
"outer": {
114+
"type": "string"
115+
}
116+
},
117+
"required": ["outer", "inner"]
118+
});
119+
96120
#[derive(OpenapiType)]
97121
struct FieldSkip {
98122
#[openapi(skip_serializing, skip_deserializing)]

0 commit comments

Comments
 (0)