From 54cfd2ba0e89a0a0d6b30c39823aaf3aafb62769 Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Wed, 7 Aug 2019 08:19:43 +0100 Subject: [PATCH] Support #[serde(flatten)]ed structs --- schemars/src/make_schema.rs | 2 + schemars/src/schema.rs | 63 +++++++++++++++++++++++++++-- schemars/tests/flatten.rs | 11 +++-- schemars/tests/schema-openapi3.json | 12 +++--- schemars/tests/schema.json | 24 ++++------- schemars_derive/src/lib.rs | 28 +++++++++---- 6 files changed, 104 insertions(+), 36 deletions(-) diff --git a/schemars/src/make_schema.rs b/schemars/src/make_schema.rs index 1c2900d..1937714 100644 --- a/schemars/src/make_schema.rs +++ b/schemars/src/make_schema.rs @@ -273,6 +273,8 @@ map_impl!( MakeSchema f ////////// OPTION ////////// +// TODO should a field with a default set also be considered nullable? + impl MakeSchema for Option { no_ref_schema!(); diff --git a/schemars/src/schema.rs b/schemars/src/schema.rs index 4c298bc..26ab123 100644 --- a/schemars/src/schema.rs +++ b/schemars/src/schema.rs @@ -1,5 +1,5 @@ use crate as schemars; -use crate::MakeSchema; +use crate::{MakeSchema, MakeSchemaError, Result}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::BTreeMap as Map; @@ -30,6 +30,63 @@ impl From for Schema { } } +fn extend>(mut a: E, b: impl IntoIterator) -> 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 { + 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)] pub struct SchemaRef { #[serde(rename = "$ref")] @@ -55,8 +112,8 @@ pub struct SchemaObject { pub items: Option>, #[serde(skip_serializing_if = "Map::is_empty")] pub properties: Map, - #[serde(skip_serializing_if = "Option::is_none")] - pub required: Option>, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub required: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub all_of: Option>, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/schemars/tests/flatten.rs b/schemars/tests/flatten.rs index dbcc780..a330302 100644 --- a/schemars/tests/flatten.rs +++ b/schemars/tests/flatten.rs @@ -1,5 +1,5 @@ use pretty_assertions::assert_eq; -use schemars::{schema_for, MakeSchema}; +use schemars::{schema::*, schema_for, MakeSchema}; use serde::{Deserialize, Serialize}; use std::error::Error; @@ -32,8 +32,13 @@ struct Deep3 { } #[test] -#[ignore = "flattening is not yet implemented"] fn flatten_schema() -> Result<(), Box> { - 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(()) } diff --git a/schemars/tests/schema-openapi3.json b/schemars/tests/schema-openapi3.json index a2fdaba..6f2a920 100644 --- a/schemars/tests/schema-openapi3.json +++ b/schemars/tests/schema-openapi3.json @@ -38,6 +38,7 @@ ] }, "SchemaObject": { + "type": "object", "properties": { "$id": { "type": "string", @@ -76,10 +77,6 @@ "items": {}, "nullable": true }, - "extensions": { - "type": "object", - "additionalProperties": true - }, "items": { "anyOf": [ { @@ -125,8 +122,7 @@ "type": "array", "items": { "type": "string" - }, - "nullable": true + } }, "title": { "type": "string", @@ -146,9 +142,11 @@ ], "nullable": true } - } + }, + "additionalProperties": true }, "SchemaRef": { + "type": "object", "properties": { "$ref": { "type": "string" diff --git a/schemars/tests/schema.json b/schemars/tests/schema.json index 4df8ab3..2ed3ad3 100644 --- a/schemars/tests/schema.json +++ b/schemars/tests/schema.json @@ -38,6 +38,7 @@ ] }, "SchemaObject": { + "type": "object", "properties": { "$id": { "anyOf": [ @@ -112,10 +113,6 @@ } ] }, - "extensions": { - "type": "object", - "additionalProperties": true - }, "items": { "anyOf": [ { @@ -156,17 +153,10 @@ } }, "required": { - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] + "type": "array", + "items": { + "type": "string" + } }, "title": { "anyOf": [ @@ -188,9 +178,11 @@ } ] } - } + }, + "additionalProperties": true }, "SchemaRef": { + "type": "object", "properties": { "$ref": { "type": "string" diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs index 235d8ef..dc70561 100644 --- a/schemars_derive/src/lib.rs +++ b/schemars_derive/src/lib.rs @@ -64,11 +64,11 @@ fn add_trait_bounds(generics: &mut Generics) { fn wrap_schema_fields(schema_contents: TokenStream) -> TokenStream { quote! { - Ok(schemars::schema::SchemaObject { + Ok(schemars::schema::Schema::Object( + schemars::schema::SchemaObject { #schema_contents ..Default::default() - } - .into()) + })) } } @@ -118,6 +118,7 @@ fn schema_for_external_tagged_enum(variants: &[Variant]) -> TokenStream { let name = variant.attrs.name().deserialize_name(); let sub_schema = schema_for_untagged_enum_variant(variant); wrap_schema_fields(quote! { + instance_type: Some(schemars::schema::InstanceType::Object.into()), properties: { let mut props = std::collections::BTreeMap::new(); 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 { - 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 ty = f.ty; 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: { let mut props = std::collections::BTreeMap::new(); #(#recurse)* props }, - }) + }); + + let flattens = flat.iter().map(|f| { + let ty = f.ty; + quote_spanned! {f.original.span()=> + ?.flatten(<#ty>::make_schema(gen)?) + } + }); + + quote! { + #schema #(#flattens)* + } }