Support #[serde(flatten)]ed structs

This commit is contained in:
Graham Esau 2019-08-07 08:19:43 +01:00
parent bd750714a0
commit 54cfd2ba0e
6 changed files with 104 additions and 36 deletions

View file

@ -273,6 +273,8 @@ map_impl!(<K: Eq + core::hash::Hash, V, H: core::hash::BuildHasher> MakeSchema f
////////// OPTION ////////// ////////// OPTION //////////
// TODO should a field with a default set also be considered nullable?
impl<T: MakeSchema> MakeSchema for Option<T> { impl<T: MakeSchema> MakeSchema for Option<T> {
no_ref_schema!(); no_ref_schema!();

View file

@ -1,5 +1,5 @@
use crate as schemars; use crate as schemars;
use crate::MakeSchema; use crate::{MakeSchema, MakeSchemaError, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::collections::BTreeMap as Map; use std::collections::BTreeMap as Map;
@ -30,6 +30,63 @@ impl From<SchemaRef> for Schema {
} }
} }
fn extend<A, E: Extend<A>>(mut a: E, b: impl IntoIterator<Item = A>) -> E {
a.extend(b);
a
}
impl Schema {
pub fn flatten(self, other: Self) -> Result {
let s1 = self.ensure_flattenable()?;
let s2 = other.ensure_flattenable()?;
Ok(Schema::Object(SchemaObject {
schema: s1.schema.or(s2.schema),
id: s1.id.or(s2.id),
title: s1.title.or(s2.title),
description: s1.description.or(s2.description),
items: s1.items.or(s2.items),
properties: extend(s1.properties, s2.properties),
required: extend(s1.required, s2.required),
definitions: extend(s1.definitions, s2.definitions),
extensions: extend(s1.extensions, s2.extensions),
// TODO do the following make sense?
instance_type: s1.instance_type.or(s2.instance_type),
enum_values: s1.enum_values.or(s2.enum_values),
all_of: s1.all_of.or(s2.all_of),
any_of: s1.any_of.or(s2.any_of),
one_of: s1.one_of.or(s2.one_of),
not: s1.not.or(s2.not),
}))
}
fn ensure_flattenable(self) -> Result<SchemaObject> {
let s = match self {
Schema::Object(s) => s,
s => {
return Err(MakeSchemaError::new(
"Only schemas with type `object` can be flattened.",
s,
))
}
};
match s.instance_type {
Some(SingleOrVec::Single(ref t)) if **t != InstanceType::Object => {
Err(MakeSchemaError::new(
"Only schemas with type `object` can be flattened.",
s.into(),
))
}
Some(SingleOrVec::Vec(ref t)) if !t.contains(&InstanceType::Object) => {
Err(MakeSchemaError::new(
"Only schemas with type `object` can be flattened.",
s.into(),
))
}
_ => Ok(s),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, MakeSchema)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, MakeSchema)]
pub struct SchemaRef { pub struct SchemaRef {
#[serde(rename = "$ref")] #[serde(rename = "$ref")]
@ -55,8 +112,8 @@ pub struct SchemaObject {
pub items: Option<SingleOrVec<Schema>>, pub items: Option<SingleOrVec<Schema>>,
#[serde(skip_serializing_if = "Map::is_empty")] #[serde(skip_serializing_if = "Map::is_empty")]
pub properties: Map<String, Schema>, pub properties: Map<String, Schema>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Vec::is_empty")]
pub required: Option<Vec<String>>, pub required: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub all_of: Option<Vec<Schema>>, pub all_of: Option<Vec<Schema>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]

View file

@ -1,5 +1,5 @@
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use schemars::{schema_for, MakeSchema}; use schemars::{schema::*, schema_for, MakeSchema};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::error::Error; use std::error::Error;
@ -32,8 +32,13 @@ struct Deep3 {
} }
#[test] #[test]
#[ignore = "flattening is not yet implemented"]
fn flatten_schema() -> Result<(), Box<dyn Error>> { fn flatten_schema() -> Result<(), Box<dyn Error>> {
assert_eq!(schema_for!(Flat)?, schema_for!(Deep1)?); let flat = schema_for!(Flat)?;
let mut deep = schema_for!(Deep1)?;
match deep {
Schema::Object(ref mut o) => o.title = Some("Flat".to_owned()),
_ => assert!(false, "Schema was not object: {:?}", deep),
};
assert_eq!(flat, deep);
Ok(()) Ok(())
} }

View file

@ -38,6 +38,7 @@
] ]
}, },
"SchemaObject": { "SchemaObject": {
"type": "object",
"properties": { "properties": {
"$id": { "$id": {
"type": "string", "type": "string",
@ -76,10 +77,6 @@
"items": {}, "items": {},
"nullable": true "nullable": true
}, },
"extensions": {
"type": "object",
"additionalProperties": true
},
"items": { "items": {
"anyOf": [ "anyOf": [
{ {
@ -125,8 +122,7 @@
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
}, }
"nullable": true
}, },
"title": { "title": {
"type": "string", "type": "string",
@ -146,9 +142,11 @@
], ],
"nullable": true "nullable": true
} }
} },
"additionalProperties": true
}, },
"SchemaRef": { "SchemaRef": {
"type": "object",
"properties": { "properties": {
"$ref": { "$ref": {
"type": "string" "type": "string"

View file

@ -38,6 +38,7 @@
] ]
}, },
"SchemaObject": { "SchemaObject": {
"type": "object",
"properties": { "properties": {
"$id": { "$id": {
"anyOf": [ "anyOf": [
@ -112,10 +113,6 @@
} }
] ]
}, },
"extensions": {
"type": "object",
"additionalProperties": true
},
"items": { "items": {
"anyOf": [ "anyOf": [
{ {
@ -156,18 +153,11 @@
} }
}, },
"required": { "required": {
"anyOf": [
{
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
} }
}, },
{
"type": "null"
}
]
},
"title": { "title": {
"anyOf": [ "anyOf": [
{ {
@ -188,9 +178,11 @@
} }
] ]
} }
} },
"additionalProperties": true
}, },
"SchemaRef": { "SchemaRef": {
"type": "object",
"properties": { "properties": {
"$ref": { "$ref": {
"type": "string" "type": "string"

View file

@ -64,11 +64,11 @@ fn add_trait_bounds(generics: &mut Generics) {
fn wrap_schema_fields(schema_contents: TokenStream) -> TokenStream { fn wrap_schema_fields(schema_contents: TokenStream) -> TokenStream {
quote! { quote! {
Ok(schemars::schema::SchemaObject { Ok(schemars::schema::Schema::Object(
schemars::schema::SchemaObject {
#schema_contents #schema_contents
..Default::default() ..Default::default()
} }))
.into())
} }
} }
@ -118,6 +118,7 @@ fn schema_for_external_tagged_enum(variants: &[Variant]) -> TokenStream {
let name = variant.attrs.name().deserialize_name(); let name = variant.attrs.name().deserialize_name();
let sub_schema = schema_for_untagged_enum_variant(variant); let sub_schema = schema_for_untagged_enum_variant(variant);
wrap_schema_fields(quote! { wrap_schema_fields(quote! {
instance_type: Some(schemars::schema::InstanceType::Object.into()),
properties: { properties: {
let mut props = std::collections::BTreeMap::new(); let mut props = std::collections::BTreeMap::new();
props.insert(#name.to_owned(), #sub_schema); props.insert(#name.to_owned(), #sub_schema);
@ -162,18 +163,31 @@ fn schema_for_untagged_enum_variant(variant: &Variant) -> TokenStream {
} }
fn schema_for_struct(fields: &[Field]) -> TokenStream { fn schema_for_struct(fields: &[Field]) -> TokenStream {
let recurse = fields.into_iter().map(|f| { let (nested, flat): (Vec<_>, Vec<_>) = fields.iter().partition(|f| !f.attrs.flatten());
let recurse = nested.iter().map(|f| {
let name = f.attrs.name().deserialize_name(); let name = f.attrs.name().deserialize_name();
let ty = f.ty; let ty = f.ty;
quote_spanned! {f.original.span()=> quote_spanned! {f.original.span()=>
props.insert(#name.to_owned(), gen.subschema_for::<#ty>()?); props.insert(#name.to_owned(), gen.subschema_for::<#ty>()?);
} }
}); });
wrap_schema_fields(quote! { let schema = wrap_schema_fields(quote! {
instance_type: Some(schemars::schema::InstanceType::Object.into()),
properties: { properties: {
let mut props = std::collections::BTreeMap::new(); let mut props = std::collections::BTreeMap::new();
#(#recurse)* #(#recurse)*
props props
}, },
}) });
let flattens = flat.iter().map(|f| {
let ty = f.ty;
quote_spanned! {f.original.span()=>
?.flatten(<#ty>::make_schema(gen)?)
}
});
quote! {
#schema #(#flattens)*
}
} }