From 709ba7b62e1aa8e29d550538d91dd605186d365c Mon Sep 17 00:00:00 2001 From: Graham Esau Date: Thu, 12 Sep 2019 18:02:37 +0100 Subject: [PATCH] Enable eriving JsonSchema when fields are in remote crates --- schemars/tests/expected/remote_derive.json | 44 +++++++++++++++++++ schemars/tests/remote_derive.rs | 37 ++++++++++++++++ schemars_derive/src/lib.rs | 51 ++++++++++++++++++---- schemars_derive/src/preprocess.rs | 15 ++++--- 4 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 schemars/tests/expected/remote_derive.json create mode 100644 schemars/tests/remote_derive.rs diff --git a/schemars/tests/expected/remote_derive.json b/schemars/tests/expected/remote_derive.json new file mode 100644 index 0000000..f91ad59 --- /dev/null +++ b/schemars/tests/expected/remote_derive.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Process", + "type": "object", + "properties": { + "command_line": { + "type": "string" + }, + "system_cpu_time": { + "$ref": "#/definitions/DurationDef" + }, + "user_cpu_time": { + "$ref": "#/definitions/DurationDef" + }, + "wall_time": { + "$ref": "#/definitions/DurationDef" + } + }, + "required": [ + "command_line", + "system_cpu_time", + "user_cpu_time", + "wall_time" + ], + "definitions": { + "DurationDef": { + "type": "object", + "properties": { + "nanos": { + "type": "integer", + "format": "int32" + }, + "secs": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "nanos", + "secs" + ] + } + } +} \ No newline at end of file diff --git a/schemars/tests/remote_derive.rs b/schemars/tests/remote_derive.rs new file mode 100644 index 0000000..7397360 --- /dev/null +++ b/schemars/tests/remote_derive.rs @@ -0,0 +1,37 @@ +mod util; + +use other_crate::Duration; +use schemars::JsonSchema; +use util::*; + +mod other_crate { + #[derive(Debug)] + pub struct Duration { + pub secs: i64, + pub nanos: i32, + } +} + +#[derive(Debug, JsonSchema)] +#[serde(remote = "Duration")] +struct DurationDef { + secs: i64, + nanos: i32, +} + +#[derive(Debug, JsonSchema)] +struct Process { + command_line: String, + #[serde(with = "DurationDef")] + wall_time: Duration, + #[serde(with = "DurationDef")] + user_cpu_time: Duration, + #[serde(deserialize_with = "some_serialize_function")] + #[schemars(with = "DurationDef")] + system_cpu_time: Duration, +} + +#[test] +fn remote_derive_json_schema() -> TestResult { + test_default_generated_schema::("remote_derive") +} diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs index 050c506..00a3acf 100644 --- a/schemars_derive/src/lib.rs +++ b/schemars_derive/src/lib.rs @@ -2,21 +2,20 @@ extern crate quote; #[macro_use] extern crate syn; - extern crate proc_macro; mod preprocess; use proc_macro2::{Span, TokenStream}; +use quote::ToTokens; use serde_derive_internals::ast::{Container, Data, Field, Style, Variant}; use serde_derive_internals::attr::{self, Default as SerdeDefault, EnumTag}; use serde_derive_internals::{Ctxt, Derive}; use syn::spanned::Spanned; -use syn::DeriveInput; #[proc_macro_derive(JsonSchema, attributes(schemars, serde))] pub fn derive_json_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let mut input = parse_macro_input!(input as DeriveInput); + let mut input = parse_macro_input!(input as syn::DeriveInput); preprocess::add_trait_bounds(&mut input.generics); if let Err(e) = preprocess::process_serde_attrs(&mut input) { @@ -222,14 +221,14 @@ fn schema_for_unit_struct() -> TokenStream { } fn schema_for_newtype_struct(field: &Field) -> TokenStream { - let ty = field.ty; + let ty = get_json_schema_type(field); quote_spanned! {field.original.span()=> gen.subschema_for::<#ty>()? } } fn schema_for_tuple_struct(fields: &[Field]) -> TokenStream { - let types = fields.iter().map(|f| f.ty); + let types = fields.iter().map(get_json_schema_type); quote! { gen.subschema_for::<(#(#types),*)>()? } @@ -244,7 +243,7 @@ fn schema_for_struct(fields: &[Field], cattrs: &attr::Container) -> TokenStream if !container_has_default && !has_default(field.attrs.default()) { required.push(name.clone()); } - let ty = field.ty; + let ty = get_json_schema_type(field); quote_spanned! {field.original.span()=> props.insert(#name.to_owned(), gen.subschema_for::<#ty>()?); } @@ -264,9 +263,9 @@ fn schema_for_struct(fields: &[Field], cattrs: &attr::Container) -> TokenStream }, }); - let flattens = flat.iter().map(|f| { - let ty = f.ty; - quote_spanned! {f.original.span()=> + let flattens = flat.iter().map(|field| { + let ty = get_json_schema_type(field); + quote_spanned! {field.original.span()=> .flatten(<#ty>::json_schema(gen)?)? } }); @@ -282,3 +281,37 @@ fn has_default(d: &SerdeDefault) -> bool { _ => true, } } + +fn get_json_schema_type(field: &Field) -> Box { + // TODO it would probably be simpler to parse attributes manually here, instead of + // using the serde-parsed attributes + let de_with_segments = without_last_element(field.attrs.deserialize_with(), "deserialize"); + let se_with_segments = without_last_element(field.attrs.serialize_with(), "serialize"); + if de_with_segments == se_with_segments { + if let Some(expr_path) = de_with_segments { + return Box::from(expr_path); + } + } + Box::from(field.ty.clone()) +} + +fn without_last_element(path: Option<&syn::ExprPath>, last: &str) -> Option { + match path { + Some(expr_path) + if expr_path + .path + .segments + .last() + .map(|p| p.value().ident == last) + .unwrap_or(false) => + { + let mut expr_path = expr_path.clone(); + expr_path.path.segments.pop(); + if let Some(segment) = expr_path.path.segments.pop() { + expr_path.path.segments.push(segment.into_value()) + } + Some(expr_path) + } + _ => None, + } +} diff --git a/schemars_derive/src/preprocess.rs b/schemars_derive/src/preprocess.rs index 009ce29..4b6d7c2 100644 --- a/schemars_derive/src/preprocess.rs +++ b/schemars_derive/src/preprocess.rs @@ -58,16 +58,21 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec) { .push(Ident::new("serde", schemars_ident.span()).into()); } - let schemars_meta_names: BTreeSet = attrs + let mut schemars_meta_names: BTreeSet = attrs .iter() .flat_map(|attr| get_meta_items(&ctxt, attr)) .flatten() .flat_map(|m| get_meta_ident(&ctxt, &m)) + .map(|i| i.to_string()) .collect(); + if schemars_meta_names.contains("with") { + schemars_meta_names.insert("serialize_with".to_string()); + schemars_meta_names.insert("deserialize_with".to_string()); + } serde_meta.retain(|m| { get_meta_ident(&ctxt, m) - .map(|i| !schemars_meta_names.contains(&i)) + .map(|i| !schemars_meta_names.contains(&i.to_string())) .unwrap_or(false) }); @@ -129,8 +134,8 @@ mod tests { struct MyStruct { #[serde(field, field2)] field1: i32, - #[serde(field, field2)] - #[schemars(field = "overridden")] + #[serde(field, field2, serialize_with = "se", deserialize_with = "de")] + #[schemars(field = "overridden", with = "with")] field2: i32, #[schemars(field)] field3: i32, @@ -142,7 +147,7 @@ mod tests { struct MyStruct { #[serde(field, field2)] field1: i32, - #[serde(field = "overridden")] + #[serde(field = "overridden", with = "with")] #[serde(field2)] field2: i32, #[serde(field)]