From 11b7a09c93acaf75c62a38fb918addfaa88ad0ab Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Thu, 30 Apr 2020 14:21:04 +0100 Subject: [PATCH] Enable deriving JsonSchema on adjacent tagged enums Issue #4 --- schemars/tests/enum.rs | 16 ++ .../enum_adjacent_tagged-untagged.json | 150 ++++++++++++++++++ schemars_derive/src/attr/schemars_to_serde.rs | 3 +- schemars_derive/src/lib.rs | 69 +++++++- 4 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 schemars/tests/expected/enum_adjacent_tagged-untagged.json diff --git a/schemars/tests/enum.rs b/schemars/tests/enum.rs index f0c0b9b..fd9a8e2 100644 --- a/schemars/tests/enum.rs +++ b/schemars/tests/enum.rs @@ -59,3 +59,19 @@ pub enum Untagged { fn enum_untagged() -> TestResult { test_default_generated_schema::("enum-untagged") } + +#[derive(Debug, JsonSchema)] +#[schemars(tag = "t", content = "c")] +pub enum Adjacent { + UnitOne, + StringMap(Map), + UnitStructNewType(UnitStruct), + StructNewType(Struct), + Struct { foo: i32, bar: bool }, + Tuple(i32, bool), +} + +#[test] +fn enum_adjacent_tagged() -> TestResult { + test_default_generated_schema::("enum_adjacent_tagged-untagged") +} diff --git a/schemars/tests/expected/enum_adjacent_tagged-untagged.json b/schemars/tests/expected/enum_adjacent_tagged-untagged.json new file mode 100644 index 0000000..196ddb0 --- /dev/null +++ b/schemars/tests/expected/enum_adjacent_tagged-untagged.json @@ -0,0 +1,150 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Adjacent", + "anyOf": [ + { + "type": "object", + "required": [ + "t" + ], + "properties": { + "t": { + "type": "string", + "enum": [ + "UnitOne" + ] + } + } + }, + { + "type": "object", + "required": [ + "c", + "t" + ], + "properties": { + "c": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "t": { + "type": "string", + "enum": [ + "StringMap" + ] + } + } + }, + { + "type": "object", + "required": [ + "c", + "t" + ], + "properties": { + "c": { + "type": "null" + }, + "t": { + "type": "string", + "enum": [ + "UnitStructNewType" + ] + } + } + }, + { + "type": "object", + "required": [ + "c", + "t" + ], + "properties": { + "c": { + "type": "object", + "required": [ + "bar", + "foo" + ], + "properties": { + "bar": { + "type": "boolean" + }, + "foo": { + "type": "integer", + "format": "int32" + } + } + }, + "t": { + "type": "string", + "enum": [ + "StructNewType" + ] + } + } + }, + { + "type": "object", + "required": [ + "c", + "t" + ], + "properties": { + "c": { + "type": "object", + "required": [ + "bar", + "foo" + ], + "properties": { + "bar": { + "type": "boolean" + }, + "foo": { + "type": "integer", + "format": "int32" + } + } + }, + "t": { + "type": "string", + "enum": [ + "Struct" + ] + } + } + }, + { + "type": "object", + "required": [ + "c", + "t" + ], + "properties": { + "c": { + "type": "array", + "items": [ + { + "type": "integer", + "format": "int32" + }, + { + "type": "boolean" + } + ], + "maxItems": 2, + "minItems": 2 + }, + "t": { + "type": "string", + "enum": [ + "Tuple" + ] + } + } + } + ] +} \ No newline at end of file diff --git a/schemars_derive/src/attr/schemars_to_serde.rs b/schemars_derive/src/attr/schemars_to_serde.rs index 28a6672..22042d2 100644 --- a/schemars_derive/src/attr/schemars_to_serde.rs +++ b/schemars_derive/src/attr/schemars_to_serde.rs @@ -11,8 +11,7 @@ static SERDE_KEYWORDS: &[&str] = &[ // TODO: for structs with `deny_unknown_fields`, set schema's `additionalProperties` to false. // "deny_unknown_fields", "tag", - // TODO: support adjecently tagged enums (https://github.com/GREsau/schemars/issues/4) - // "content", + "content", "untagged", "default", "skip", diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs index 7f51732..92ced97 100644 --- a/schemars_derive/src/lib.rs +++ b/schemars_derive/src/lib.rs @@ -131,7 +131,9 @@ fn schema_for_enum(variants: &[Variant], cattrs: &serde_attr::Container) -> Toke TagType::External => schema_for_external_tagged_enum(variants), TagType::None => schema_for_untagged_enum(variants), TagType::Internal { tag } => schema_for_internal_tagged_enum(variants, tag), - TagType::Adjacent { .. } => unimplemented!("Adjacent tagged enums not yet supported."), + TagType::Adjacent { tag, content } => { + schema_for_adjacent_tagged_enum(variants, tag, content) + } } } @@ -268,6 +270,71 @@ fn schema_for_untagged_enum_variant(variant: &Variant) -> TokenStream { } } +fn schema_for_adjacent_tagged_enum<'a>( + variants: impl Iterator>, + tag_name: &str, + content_name: &str, +) -> TokenStream { + let schemas = variants.map(|variant| { + let content_schema = match variant.style { + Style::Unit => None, + Style::Newtype => { + let field = &variant.fields[0]; + let ty = get_json_schema_type(field); + Some(quote_spanned! {field.original.span()=> + <#ty>::json_schema(gen) + }) + } + Style::Struct => Some(schema_for_struct(&variant.fields, None)), + Style::Tuple => Some(schema_for_tuple_struct(&variant.fields)), + }; + + let (add_content_property, add_content_required) = content_schema + .map(|content_schema| { + ( + quote!(props.insert(#content_name.to_owned(), #content_schema);), + quote!(required.insert(#content_name.to_owned());), + ) + }) + .unwrap_or_default(); + + let name = variant.attrs.name().deserialize_name(); + let tag_schema = wrap_schema_fields(quote! { + instance_type: Some(schemars::schema::InstanceType::String.into()), + enum_values: Some(vec![#name.into()]), + }); + + let outer_schema = wrap_schema_fields(quote! { + instance_type: Some(schemars::schema::InstanceType::Object.into()), + object: Some(Box::new(schemars::schema::ObjectValidation { + properties: { + let mut props = schemars::Map::new(); + props.insert(#tag_name.to_owned(), #tag_schema); + #add_content_property + props + }, + required: { + let mut required = schemars::Set::new(); + required.insert(#tag_name.to_owned()); + #add_content_required + required + }, + ..Default::default() + })), + }); + + let doc_metadata = SchemaMetadata::from_doc_attrs(&variant.original.attrs); + doc_metadata.apply_to_schema(outer_schema) + }); + + wrap_schema_fields(quote! { + subschemas: Some(Box::new(schemars::schema::SubschemaValidation { + any_of: Some(vec![#(#schemas),*]), + ..Default::default() + })), + }) +} + fn schema_for_unit_struct() -> TokenStream { quote! { gen.subschema_for::<()>()