diff --git a/schemars/tests/bound.rs b/schemars/tests/bound.rs new file mode 100644 index 0000000..8df3e91 --- /dev/null +++ b/schemars/tests/bound.rs @@ -0,0 +1,29 @@ +mod util; +use std::marker::PhantomData; + +use schemars::JsonSchema; +use util::*; + +struct MyIterator; + +impl Iterator for MyIterator { + type Item = String; + + fn next(&mut self) -> Option { + unimplemented!() + } +} + +// The default trait bounds would require T to implement JsonSchema, +// which MyIterator does not. +#[derive(JsonSchema)] +#[schemars(bound = "T::Item: JsonSchema", rename = "MyContainer")] +pub struct MyContainer where T: Iterator { + pub associated: T::Item, + pub generic: PhantomData, +} + +#[test] +fn manual_bound_set() -> TestResult { + test_default_generated_schema::>("bound") +} diff --git a/schemars/tests/expected/bound.json b/schemars/tests/expected/bound.json new file mode 100644 index 0000000..3c022dc --- /dev/null +++ b/schemars/tests/expected/bound.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MyContainer", + "type": "object", + "required": [ + "associated", + "generic" + ], + "properties": { + "associated": { + "type": "string" + }, + "generic": { + "type": "null" + } + } +} \ No newline at end of file diff --git a/schemars_derive/src/ast/from_serde.rs b/schemars_derive/src/ast/from_serde.rs index 83bfcf3..453c247 100644 --- a/schemars_derive/src/ast/from_serde.rs +++ b/schemars_derive/src/ast/from_serde.rs @@ -25,7 +25,7 @@ impl<'a> FromSerde for Container<'a> { ident: serde.ident, serde_attrs: serde.attrs, data: Data::from_serde(errors, serde.data)?, - generics: serde.generics, + generics: serde.generics.clone(), original: serde.original, // FIXME this allows with/schema_with attribute on containers attrs: Attrs::new(&serde.original.attrs, errors), diff --git a/schemars_derive/src/ast/mod.rs b/schemars_derive/src/ast/mod.rs index 99fe188..7b9052e 100644 --- a/schemars_derive/src/ast/mod.rs +++ b/schemars_derive/src/ast/mod.rs @@ -9,7 +9,7 @@ pub struct Container<'a> { pub ident: syn::Ident, pub serde_attrs: serde_derive_internals::attr::Container, pub data: Data<'a>, - pub generics: &'a syn::Generics, + pub generics: syn::Generics, pub original: &'a syn::DeriveInput, pub attrs: Attrs, } diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs index 3fad9e4..26c1f3e 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -27,6 +27,7 @@ pub struct Attrs { pub examples: Vec, pub repr: Option, pub crate_name: Option, + pub is_renamed: bool } #[derive(Debug)] @@ -152,6 +153,10 @@ impl Attrs { } } + Meta(NameValue(m)) if m.path.is_ident("rename") => { + self.is_renamed = true + } + Meta(NameValue(m)) if m.path.is_ident("crate") && attr_type == "schemars" => { if let Ok(p) = parse_lit_into_path(errors, attr_type, "crate", &m.lit) { if self.crate_name.is_some() { @@ -196,6 +201,7 @@ impl Attrs { examples, repr: None, crate_name: None, + is_renamed: _, } if examples.is_empty() => true, _ => false, } diff --git a/schemars_derive/src/attr/schemars_to_serde.rs b/schemars_derive/src/attr/schemars_to_serde.rs index bf8d389..dd871b0 100644 --- a/schemars_derive/src/attr/schemars_to_serde.rs +++ b/schemars_derive/src/attr/schemars_to_serde.rs @@ -20,6 +20,7 @@ pub(crate) static SERDE_KEYWORDS: &[&str] = &[ "flatten", "remote", "transparent", + "bound", // Special cases - `with`/`serialize_with` are passed to serde but not copied from schemars attrs to serde attrs. // This is because we want to preserve any serde attribute's `serialize_with` value to determine whether the field's // default value should be serialized. We also check the `with` value on schemars/serde attrs e.g. to support deriving @@ -56,9 +57,9 @@ fn process_serde_field_attrs<'a>(ctxt: &Ctxt, fields: impl Iterator) { + // Remove #[serde(...)] attributes (some may be re-added later) let (serde_attrs, other_attrs): (Vec<_>, Vec<_>) = attrs.drain(..).partition(|at| at.path.is_ident("serde")); - *attrs = other_attrs; let schemars_attrs: Vec<_> = attrs @@ -66,6 +67,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec) { .filter(|at| at.path.is_ident("schemars")) .collect(); + // Copy appropriate #[schemars(...)] attributes to #[serde(...)] attributes let (mut serde_meta, mut schemars_meta_names): (Vec<_>, HashSet<_>) = schemars_attrs .iter() .flat_map(|at| get_meta_items(&ctxt, at)) @@ -85,6 +87,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec) { schemars_meta_names.insert("skip_deserializing".to_string()); } + // Re-add #[serde(...)] attributes that weren't overridden by #[schemars(...)] attributes for meta in serde_attrs .into_iter() .flat_map(|at| get_meta_items(&ctxt, &at)) diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs index 5fa4ac2..7e7060e 100644 --- a/schemars_derive/src/lib.rs +++ b/schemars_derive/src/lib.rs @@ -36,11 +36,11 @@ fn derive_json_schema( mut input: syn::DeriveInput, repr: bool, ) -> Result> { - add_trait_bounds(&mut input.generics); - attr::process_serde_attrs(&mut input)?; - let cont = Container::from_ast(&input)?; + let mut cont = Container::from_ast(&input)?; + add_trait_bounds(&mut cont); + let crate_alias = cont.attrs.crate_name.as_ref().map(|path| { quote_spanned! {path.span()=> use #path as schemars; @@ -84,9 +84,8 @@ fn derive_json_schema( } let mut schema_base_name = cont.name(); - let schema_is_renamed = *type_name != schema_base_name; - if !schema_is_renamed { + if !cont.attrs.is_renamed { if let Some(path) = cont.serde_attrs.remote() { if let Some(segment) = path.segments.last() { schema_base_name = segment.ident.to_string(); @@ -94,12 +93,13 @@ fn derive_json_schema( } } + // FIXME improve handling of generic type params which may not implement JsonSchema let type_params: Vec<_> = cont.generics.type_params().map(|ty| &ty.ident).collect(); - let schema_name = if type_params.is_empty() { + let schema_name = if type_params.is_empty() || (cont.attrs.is_renamed && !schema_base_name.contains('{')) { quote! { #schema_base_name.to_owned() } - } else if schema_is_renamed { + } else if cont.attrs.is_renamed { let mut schema_name_fmt = schema_base_name; for tp in &type_params { schema_name_fmt.push_str(&format!("{{{}:.0}}", tp)); @@ -141,10 +141,17 @@ fn derive_json_schema( }) } -fn add_trait_bounds(generics: &mut syn::Generics) { - for param in &mut generics.params { - if let syn::GenericParam::Type(ref mut type_param) = *param { - type_param.bounds.push(parse_quote!(schemars::JsonSchema)); +fn add_trait_bounds(cont: &mut Container) { + if let Some(bounds) = cont.serde_attrs.ser_bound() { + let where_clause = cont.generics.make_where_clause(); + where_clause.predicates.extend(bounds.iter().cloned()); + } else { + // No explicit trait bounds specified, assume the Rust convention of adding the trait to each type parameter + // TODO consider also adding trait bound to associated types when used as fields - I think Serde does this? + for param in &mut cont.generics.params { + if let syn::GenericParam::Type(ref mut type_param) = *param { + type_param.bounds.push(parse_quote!(schemars::JsonSchema)); + } } } }