From 3fd316063abb640414e1a02f9e3bebc3b21c9c89 Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Fri, 15 May 2020 17:11:28 +0100 Subject: [PATCH] `schema_with` attribute --- CHANGELOG.md | 4 + schemars/tests/enum.rs | 11 +- .../schema_with-enum-adjacent-tagged.json | 79 ++++++++++++ .../expected/schema_with-enum-external.json | 58 +++++++++ .../expected/schema_with-enum-internal.json | 41 ++++++ .../expected/schema_with-enum-untagged.json | 34 +++++ .../tests/expected/schema_with-newtype.json | 5 + .../tests/expected/schema_with-struct.json | 22 ++++ .../tests/expected/schema_with-tuple.json | 19 +++ schemars/tests/schema_with_enum.rs | 92 ++++++++++++++ schemars/tests/schema_with_struct.rs | 44 +++++++ schemars/tests/struct.rs | 7 +- schemars_derive/src/ast/mod.rs | 10 +- schemars_derive/src/attr/mod.rs | 46 +++++-- schemars_derive/src/schema_exprs.rs | 117 +++++++++++++----- 15 files changed, 538 insertions(+), 51 deletions(-) create mode 100644 schemars/tests/expected/schema_with-enum-adjacent-tagged.json create mode 100644 schemars/tests/expected/schema_with-enum-external.json create mode 100644 schemars/tests/expected/schema_with-enum-internal.json create mode 100644 schemars/tests/expected/schema_with-enum-untagged.json create mode 100644 schemars/tests/expected/schema_with-newtype.json create mode 100644 schemars/tests/expected/schema_with-struct.json create mode 100644 schemars/tests/expected/schema_with-tuple.json create mode 100644 schemars/tests/schema_with_enum.rs create mode 100644 schemars/tests/schema_with_struct.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9deeacf..8f26312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ # Changelog ## In-dev - version TBC +### Added: +- `#[schemars(schema_with = "...")]` attribute can be set on variants and fields. This allows you to specify another function which returns the schema you want, which is particularly useful on fields of types that don't implement the JsonSchema trait (https://github.com/GREsau/schemars/issues/15) + ### Fixed - `#[serde(with = "...")]`/`#[schemars(with = "...")]` attributes on enum variants are now respected +- Some compiler errors generated by schemars_derive should now have more accurate spans ## [0.7.2] - 2020-04-30 ### Added: diff --git a/schemars/tests/enum.rs b/schemars/tests/enum.rs index 774082a..ea34d06 100644 --- a/schemars/tests/enum.rs +++ b/schemars/tests/enum.rs @@ -2,6 +2,9 @@ mod util; use schemars::{JsonSchema, Map}; use util::*; +// Ensure that schemars_derive uses the full path to std::string::String +pub struct String; + #[derive(Debug, JsonSchema)] pub struct UnitStruct; @@ -15,7 +18,7 @@ pub struct Struct { #[schemars(rename_all = "camelCase")] pub enum External { UnitOne, - StringMap(Map), + StringMap(Map<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -37,7 +40,7 @@ fn enum_external_tag() -> TestResult { #[schemars(tag = "typeProperty")] pub enum Internal { UnitOne, - StringMap(Map), + StringMap(Map<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -58,7 +61,7 @@ fn enum_internal_tag() -> TestResult { #[schemars(untagged)] pub enum Untagged { UnitOne, - StringMap(Map), + StringMap(Map<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { @@ -79,7 +82,7 @@ fn enum_untagged() -> TestResult { #[schemars(tag = "t", content = "c")] pub enum Adjacent { UnitOne, - StringMap(Map), + StringMap(Map<&'static str, &'static str>), UnitStructNewType(UnitStruct), StructNewType(Struct), Struct { diff --git a/schemars/tests/expected/schema_with-enum-adjacent-tagged.json b/schemars/tests/expected/schema_with-enum-adjacent-tagged.json new file mode 100644 index 0000000..3ff17f7 --- /dev/null +++ b/schemars/tests/expected/schema_with-enum-adjacent-tagged.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Adjacent", + "anyOf": [ + { + "type": "object", + "required": [ + "c", + "t" + ], + "properties": { + "c": { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "boolean" + } + } + }, + "t": { + "type": "string", + "enum": [ + "Struct" + ] + } + } + }, + { + "type": "object", + "required": [ + "c", + "t" + ], + "properties": { + "c": { + "type": "boolean" + }, + "t": { + "type": "string", + "enum": [ + "NewType" + ] + } + } + }, + { + "type": "object", + "required": [ + "c", + "t" + ], + "properties": { + "c": { + "type": "array", + "items": [ + { + "type": "boolean" + }, + { + "type": "integer", + "format": "int32" + } + ], + "maxItems": 2, + "minItems": 2 + }, + "t": { + "type": "string", + "enum": [ + "Tuple" + ] + } + } + } + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/schema_with-enum-external.json b/schemars/tests/expected/schema_with-enum-external.json new file mode 100644 index 0000000..2e7d415 --- /dev/null +++ b/schemars/tests/expected/schema_with-enum-external.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "External", + "anyOf": [ + { + "type": "object", + "required": [ + "struct" + ], + "properties": { + "struct": { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "boolean" + } + } + } + } + }, + { + "type": "object", + "required": [ + "newType" + ], + "properties": { + "newType": { + "type": "boolean" + } + } + }, + { + "type": "object", + "required": [ + "tuple" + ], + "properties": { + "tuple": { + "type": "array", + "items": [ + { + "type": "boolean" + }, + { + "type": "integer", + "format": "int32" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + } + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/schema_with-enum-internal.json b/schemars/tests/expected/schema_with-enum-internal.json new file mode 100644 index 0000000..2289ef7 --- /dev/null +++ b/schemars/tests/expected/schema_with-enum-internal.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Internal", + "anyOf": [ + { + "type": "object", + "required": [ + "foo", + "typeProperty" + ], + "properties": { + "foo": { + "type": "boolean" + }, + "typeProperty": { + "type": "string", + "enum": [ + "Struct" + ] + } + } + }, + { + "type": [ + "boolean", + "object" + ], + "required": [ + "typeProperty" + ], + "properties": { + "typeProperty": { + "type": "string", + "enum": [ + "NewType" + ] + } + } + } + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/schema_with-enum-untagged.json b/schemars/tests/expected/schema_with-enum-untagged.json new file mode 100644 index 0000000..d22f5a3 --- /dev/null +++ b/schemars/tests/expected/schema_with-enum-untagged.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Untagged", + "anyOf": [ + { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "boolean" + } + } + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": [ + { + "type": "boolean" + }, + { + "type": "integer", + "format": "int32" + } + ], + "maxItems": 2, + "minItems": 2 + } + ] +} \ No newline at end of file diff --git a/schemars/tests/expected/schema_with-newtype.json b/schemars/tests/expected/schema_with-newtype.json new file mode 100644 index 0000000..8b709fb --- /dev/null +++ b/schemars/tests/expected/schema_with-newtype.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Newtype", + "type": "boolean" +} \ No newline at end of file diff --git a/schemars/tests/expected/schema_with-struct.json b/schemars/tests/expected/schema_with-struct.json new file mode 100644 index 0000000..190fdff --- /dev/null +++ b/schemars/tests/expected/schema_with-struct.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Struct", + "type": "object", + "required": [ + "bar", + "baz", + "foo" + ], + "properties": { + "bar": { + "type": "integer", + "format": "int32" + }, + "baz": { + "type": "boolean" + }, + "foo": { + "type": "boolean" + } + } +} \ No newline at end of file diff --git a/schemars/tests/expected/schema_with-tuple.json b/schemars/tests/expected/schema_with-tuple.json new file mode 100644 index 0000000..1a8f8ca --- /dev/null +++ b/schemars/tests/expected/schema_with-tuple.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Tuple", + "type": "array", + "items": [ + { + "type": "boolean" + }, + { + "type": "integer", + "format": "int32" + }, + { + "type": "boolean" + } + ], + "maxItems": 3, + "minItems": 3 +} \ No newline at end of file diff --git a/schemars/tests/schema_with_enum.rs b/schemars/tests/schema_with_enum.rs new file mode 100644 index 0000000..38cef03 --- /dev/null +++ b/schemars/tests/schema_with_enum.rs @@ -0,0 +1,92 @@ +mod util; +use schemars::JsonSchema; +use util::*; + +// FIXME determine whether schema_with should be allowed on unit variants + +fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + ::json_schema(gen) +} + +#[derive(Debug)] +pub struct DoesntImplementJsonSchema; + +#[derive(Debug, JsonSchema)] +#[schemars(rename_all = "camelCase")] +pub enum External { + Struct { + #[schemars(schema_with = "schema_fn")] + foo: DoesntImplementJsonSchema, + }, + NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema), + Tuple( + #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema, + i32, + ), + // #[schemars(schema_with = "schema_fn")] + // Unit, +} + +#[test] +fn enum_external_tag() -> TestResult { + test_default_generated_schema::("schema_with-enum-external") +} + +#[derive(Debug, JsonSchema)] +#[schemars(tag = "typeProperty")] +pub enum Internal { + Struct { + #[schemars(schema_with = "schema_fn")] + foo: DoesntImplementJsonSchema, + }, + NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema), + // #[schemars(schema_with = "schema_fn")] + // Unit, +} + +#[test] +fn enum_internal_tag() -> TestResult { + test_default_generated_schema::("schema_with-enum-internal") +} + +#[derive(Debug, JsonSchema)] +#[schemars(untagged)] +pub enum Untagged { + Struct { + #[schemars(schema_with = "schema_fn")] + foo: DoesntImplementJsonSchema, + }, + NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema), + Tuple( + #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema, + i32, + ), + // #[schemars(schema_with = "schema_fn")] + // Unit, +} + +#[test] +fn enum_untagged() -> TestResult { + test_default_generated_schema::("schema_with-enum-untagged") +} + +#[derive(Debug, JsonSchema)] +#[schemars(tag = "t", content = "c")] +pub enum Adjacent { + Struct { + #[schemars(schema_with = "schema_fn")] + foo: DoesntImplementJsonSchema, + }, + NewType(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema), + Tuple( + #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema, + i32, + ), + // #[schemars(schema_with = "schema_fn")] + // Unit, +} + +#[test] +fn enum_adjacent_tagged() -> TestResult { + test_default_generated_schema::("schema_with-enum-adjacent-tagged") +} diff --git a/schemars/tests/schema_with_struct.rs b/schemars/tests/schema_with_struct.rs new file mode 100644 index 0000000..1e94149 --- /dev/null +++ b/schemars/tests/schema_with_struct.rs @@ -0,0 +1,44 @@ +mod util; +use schemars::JsonSchema; +use util::*; + +fn schema_fn(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + ::json_schema(gen) +} + +#[derive(Debug)] +struct DoesntImplementJsonSchema; + +#[derive(Debug, JsonSchema)] +pub struct Struct { + #[schemars(schema_with = "schema_fn")] + foo: DoesntImplementJsonSchema, + bar: i32, + #[schemars(schema_with = "schema_fn")] + baz: DoesntImplementJsonSchema, +} + +#[test] +fn struct_normal() -> TestResult { + test_default_generated_schema::("schema_with-struct") +} + +#[derive(Debug, JsonSchema)] +pub struct Tuple( + #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema, + i32, + #[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema, +); + +#[test] +fn struct_tuple() -> TestResult { + test_default_generated_schema::("schema_with-tuple") +} + +#[derive(Debug, JsonSchema)] +pub struct Newtype(#[schemars(schema_with = "schema_fn")] DoesntImplementJsonSchema); + +#[test] +fn struct_newtype() -> TestResult { + test_default_generated_schema::("schema_with-newtype") +} diff --git a/schemars/tests/struct.rs b/schemars/tests/struct.rs index e522794..73ce851 100644 --- a/schemars/tests/struct.rs +++ b/schemars/tests/struct.rs @@ -2,11 +2,14 @@ mod util; use schemars::JsonSchema; use util::*; +// Ensure that schemars_derive uses the full path to std::string::String +pub struct String; + #[derive(Debug, JsonSchema)] pub struct Struct { foo: i32, bar: bool, - baz: Option, + baz: Option<&'static str>, } #[test] @@ -15,7 +18,7 @@ fn struct_normal() -> TestResult { } #[derive(Debug, JsonSchema)] -pub struct Tuple(i32, bool, Option); +pub struct Tuple(i32, bool, Option<&'static str>); #[test] fn struct_tuple() -> TestResult { diff --git a/schemars_derive/src/ast/mod.rs b/schemars_derive/src/ast/mod.rs index e0ac3fc..2f2cb98 100644 --- a/schemars_derive/src/ast/mod.rs +++ b/schemars_derive/src/ast/mod.rs @@ -1,6 +1,6 @@ mod from_serde; -use crate::attr::{Attrs, WithAttr}; +use crate::attr::Attrs; use from_serde::FromSerde; use serde_derive_internals::ast as serde_ast; use serde_derive_internals::{Ctxt, Derive}; @@ -68,12 +68,4 @@ impl<'a> Field<'a> { pub fn name(&self) -> String { self.serde_attrs.name().deserialize_name() } - - pub fn type_for_schema(&self) -> &syn::Type { - match &self.attrs.with { - None => self.ty, - Some(WithAttr::Type(ty)) => ty, - Some(WithAttr::_Function(_)) => unimplemented!(), // TODO - } - } } diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs index ebcd7bd..6e36946 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -22,7 +22,7 @@ pub struct Attrs { #[derive(Debug)] pub enum WithAttr { Type(syn::Type), - _Function(syn::Path), + Function(syn::Path), } impl Attrs { @@ -74,14 +74,22 @@ impl Attrs { if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.lit) { match self.with { Some(WithAttr::Type(_)) => duplicate_error(m), - Some(WithAttr::_Function(_)) => { - mutual_exclusive_error(m, "schema_with") - } + Some(WithAttr::Function(_)) => mutual_exclusive_error(m, "schema_with"), None => self.with = Some(WithAttr::Type(ty)), } } } + Meta(NameValue(m)) if m.path.is_ident("schema_with") => { + if let Ok(fun) = parse_lit_into_path(errors, attr_type, "schema_with", &m.lit) { + match self.with { + Some(WithAttr::Function(_)) => duplicate_error(m), + Some(WithAttr::Type(_)) => mutual_exclusive_error(m, "with"), + None => self.with = Some(WithAttr::Function(fun)), + } + } + } + Meta(_meta_item) => { // TODO uncomment this for 0.8.0 (breaking change) // https://github.com/GREsau/schemars/issues/18 @@ -148,8 +156,8 @@ fn get_lit_str<'a>( cx.error_spanned_by( lit, format!( - "expected {} attribute to be a string: `{} = \"...\"`", - attr_type, meta_item_name + "expected {} {} attribute to be a string: `{} = \"...\"`", + attr_type, meta_item_name, meta_item_name ), ); Err(()) @@ -167,7 +175,31 @@ fn parse_lit_into_ty( parse_lit_str(string).map_err(|_| { cx.error_spanned_by( lit, - format!("failed to parse type: {} = {:?}", attr_type, string.value()), + format!( + "failed to parse type: `{} = {:?}`", + meta_item_name, + string.value() + ), + ) + }) +} + +fn parse_lit_into_path( + cx: &Ctxt, + attr_type: &'static str, + meta_item_name: &'static str, + lit: &syn::Lit, +) -> Result { + let string = get_lit_str(cx, attr_type, meta_item_name, lit)?; + + parse_lit_str(string).map_err(|_| { + cx.error_spanned_by( + lit, + format!( + "failed to parse path: `{} = {:?}`", + meta_item_name, + string.value() + ), ) }) } diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs index e6b8728..0ce909f 100644 --- a/schemars_derive/src/schema_exprs.rs +++ b/schemars_derive/src/schema_exprs.rs @@ -17,6 +17,58 @@ pub fn expr_for_container(cont: &Container) -> TokenStream { doc_metadata.apply_to_schema(schema_expr) } +fn expr_for_field(field: &Field, allow_ref: bool) -> TokenStream { + let (ty, type_def) = type_for_schema(field, 0); + let span = field.original.span(); + + if allow_ref { + quote_spanned! {span=> + { + #type_def + gen.subschema_for::<#ty>() + } + } + } else { + quote_spanned! {span=> + { + #type_def + <#ty as schemars::JsonSchema>::json_schema(gen) + } + } + } +} + +fn type_for_schema(field: &Field, local_id: usize) -> (syn::Type, Option) { + match &field.attrs.with { + None => (field.ty.to_owned(), None), + Some(WithAttr::Type(ty)) => (ty.to_owned(), None), + Some(WithAttr::Function(fun)) => { + let ty_name = format_ident!("_SchemarsSchemaWithFunction{}", local_id); + let fn_name = fun.segments.last().unwrap().ident.to_string(); + + let type_def = quote_spanned! {fun.span()=> + struct #ty_name; + + impl schemars::JsonSchema for #ty_name { + fn is_referenceable() -> bool { + false + } + + fn schema_name() -> std::string::String { + #fn_name.to_string() + } + + fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + #fun(gen) + } + } + }; + + (parse_quote!(#ty_name), Some(type_def)) + } + } +} + fn expr_for_enum(variants: &[Variant], cattrs: &serde_attr::Container) -> TokenStream { let variants = variants .iter() @@ -208,7 +260,7 @@ fn expr_for_untagged_enum_variant(variant: &Variant) -> TokenStream { match variant.style { Style::Unit => expr_for_unit_struct(), - Style::Newtype => expr_for_newtype_struct(&variant.fields[0]), + Style::Newtype => expr_for_field(&variant.fields[0], true), Style::Tuple => expr_for_tuple_struct(&variant.fields), Style::Struct => expr_for_struct(&variant.fields, None), } @@ -223,13 +275,7 @@ fn expr_for_untagged_enum_variant_for_flatten(variant: &Variant) -> Option return None, - Style::Newtype => { - let field = &variant.fields[0]; - let ty = field.type_for_schema(); - quote_spanned! {field.original.span()=> - <#ty>::json_schema(gen) - } - } + Style::Newtype => expr_for_field(&variant.fields[0], false), Style::Tuple => expr_for_tuple_struct(&variant.fields), Style::Struct => expr_for_struct(&variant.fields, None), }) @@ -242,19 +288,21 @@ fn expr_for_unit_struct() -> TokenStream { } fn expr_for_newtype_struct(field: &Field) -> TokenStream { - let ty = field.type_for_schema(); - quote_spanned! {field.original.span()=> - gen.subschema_for::<#ty>() - } + expr_for_field(field, true) } fn expr_for_tuple_struct(fields: &[Field]) -> TokenStream { - let types = fields + let (types, type_defs): (Vec<_>, Vec<_>) = fields .iter() .filter(|f| !f.serde_attrs.skip_deserializing()) - .map(Field::type_for_schema); + .enumerate() + .map(|(i, f)| type_for_schema(f, i)) + .unzip(); quote! { - gen.subschema_for::<(#(#types),*)>() + { + #(#type_defs)* + gen.subschema_for::<(#(#types),*)>() + } } } @@ -270,7 +318,9 @@ fn expr_for_struct(fields: &[Field], cattrs: Option<&serde_attr::Container>) -> SerdeDefault::Path(path) => Some(quote!(let container_default = #path();)), }); - let properties = property_fields.iter().map(|field| { + let mut type_defs = Vec::new(); + + let properties: Vec<_> = property_fields.into_iter().map(|field| { let name = field.name(); let default = field_default_expr(field, set_container_default.is_some()); @@ -286,25 +336,34 @@ fn expr_for_struct(fields: &[Field], cattrs: Option<&serde_attr::Container>) -> ..SchemaMetadata::from_doc_attrs(&field.original.attrs) }; - let ty = field.type_for_schema(); - let span = field.original.span(); - - quote_spanned! {span=> - <#ty>::add_schema_as_property(gen, &mut schema_object, #name.to_owned(), #metadata, #required); + let (ty, type_def) = type_for_schema(field, type_defs.len()); + if let Some(type_def) = type_def { + type_defs.push(type_def); } - }); - let flattens = flattened_fields.iter().map(|field| { - let ty = field.type_for_schema(); - let span = field.original.span(); - - quote_spanned! {span=> - .flatten(<#ty>::json_schema_for_flatten(gen)) + quote_spanned! {ty.span()=> + <#ty as schemars::JsonSchema>::add_schema_as_property(gen, &mut schema_object, #name.to_owned(), #metadata, #required); } - }); + + }).collect(); + + let flattens: Vec<_> = flattened_fields + .into_iter() + .map(|field| { + let (ty, type_def) = type_for_schema(field, type_defs.len()); + if let Some(type_def) = type_def { + type_defs.push(type_def); + } + + quote_spanned! {ty.span()=> + .flatten(<#ty as schemars::JsonSchema>::json_schema_for_flatten(gen)) + } + }) + .collect(); quote! { { + #(#type_defs)* #set_container_default let mut schema_object = schemars::schema::SchemaObject { instance_type: Some(schemars::schema::InstanceType::Object.into()),