Repository: GREsau/schemars
Pull request #167 (2022-08-14 13:27:48)

Add #[schemars(bound = ...)] attribute
Supersedes https://github.com/GREsau/schemars/pull/162

Docs still need updating


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d45c397..9ba2569 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,9 @@
 # Changelog
 
 ## **in-dev**
+### Added:
+- Replace auto-inferred trait bounds with bounds specified in `#[schemars(bound = "...")]` attribute
+
 ### Changed:
 - Derived `JsonSchema` now respects attributes on unit enum variants (https://github.com/GREsau/schemars/pull/152)
 - Minimum supported rust version is now 1.45.0
diff --git a/docs/1.1-attributes.md b/docs/1.1-attributes.md
index 32d6bd7..2904968 100644
--- a/docs/1.1-attributes.md
+++ b/docs/1.1-attributes.md
@@ -35,6 +35,7 @@ TABLE OF CONTENTS
     - [`skip_deserializing`](#skip_deserializing)
     - [`flatten`](#flatten)
     - [`with`](#with)
+    - [`bound`](#bound)
 1. [Supported Validator Attributes](#supported-validator-attributes)
     - [`email` / `phone` / `url`](#email-phone-url)
     - [`length`](#length)
@@ -160,6 +161,15 @@ Set on a newtype struct or a braced struct with one field to make the struct's g
 
 Serde docs: [container](https://serde.rs/container-attrs.html#transparent)
 
+<h3 id="bound">
+
+`#[schemars(bound = "...")]`
+</h3>
+
+Where-clause for the JsonSchema impl. This replaces any trait bounds inferred by schemars. Schemars does **not** use trait bounds from `#[serde(bound)]` attributes.
+
+Serde docs: [container](https://serde.rs/container-attrs.html#bound)
+
 </div>
 
 ## Supported Validator Attributes
diff --git a/docs/_config.yml b/docs/_config.yml
index 933f631..ee946f7 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -29,6 +29,8 @@ permalink: pretty
 # Build settings
 remote_theme: pmarsceill/just-the-docs@v0.3.2
 markdown: CommonMarkGhPages
+commonmark:
+  options: ["UNSAFE", "FOOTNOTES"]
 
 color_scheme: default
 aux_links:
diff --git a/schemars/src/gen.rs b/schemars/src/gen.rs
index 2634e12..8ae1740 100644
--- a/schemars/src/gen.rs
+++ b/schemars/src/gen.rs
@@ -2,7 +2,7 @@
 JSON Schema generator and settings.
 
 This module is useful if you want more control over how the schema generated than the [`schema_for!`] macro gives you.
-There are two main types in this module:two main types in this module:
+There are two main types in this module:
 * [`SchemaSettings`], which defines what JSON Schema features should be used when generating schemas (for example, how `Option`s should be represented).
 * [`SchemaGenerator`], which manages the generation of a schema document.
 */
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<syn::Path>,
     pub repr: Option<syn::Type>,
     pub crate_name: Option<syn::Path>,
+    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..781f5da 100644
--- a/schemars_derive/src/attr/schemars_to_serde.rs
+++ b/schemars_derive/src/attr/schemars_to_serde.rs
@@ -20,6 +20,8 @@ pub(crate) static SERDE_KEYWORDS: &[&str] = &[
     "flatten",
     "remote",
     "transparent",
+    // Special case - `bound` is removed from serde attrs, so is only respected when present in schemars attr.
+    "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 +58,9 @@ fn process_serde_field_attrs<'a>(ctxt: &Ctxt, fields: impl Iterator<Item = &'a m
 }
 
 fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
+    // 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 +68,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
         .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,13 +88,17 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
         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))
         .flatten()
     {
         if let Ok(i) = get_meta_ident(&ctxt, &meta) {
-            if !schemars_meta_names.contains(&i) && SERDE_KEYWORDS.contains(&i.as_ref()) {
+            if !schemars_meta_names.contains(&i)
+                && SERDE_KEYWORDS.contains(&i.as_ref())
+                && i != "bound"
+            {
                 serde_meta.push(meta);
             }
         }
@@ -163,10 +170,10 @@ mod tests {
             #[misc]
             struct MyStruct {
                 /// blah blah blah
-                #[serde(skip_serializing_if = "some_fn")]
+                #[serde(skip_serializing_if = "some_fn", bound = "removed")]
                 field1: i32,
                 #[serde(serialize_with = "se", deserialize_with = "de")]
-                #[schemars(with = "with")]
+                #[schemars(with = "with", bound = "bound")]
                 field2: i32,
                 #[schemars(skip)]
                 #[serde(skip_serializing)]
@@ -181,8 +188,8 @@ mod tests {
                 #[doc = r" blah blah blah"]
                 #[serde(skip_serializing_if = "some_fn")]
                 field1: i32,
-                #[schemars(with = "with")]
-                #[serde(serialize_with = "se")]
+                #[schemars(with = "with", bound = "bound")]
+                #[serde(bound = "bound", serialize_with = "se")]
                 field2: i32,
                 #[schemars(skip)]
                 #[serde(skip)]
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<TokenStream, Vec<syn::Error>> {
-    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));
+            }
         }
     }
 }


Test patch:
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<Self::Item> {
+        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<T> where T: Iterator {
+    pub associated: T::Item,
+    pub generic: PhantomData<T>,
+}
+
+#[test]
+fn manual_bound_set() -> TestResult {
+    test_default_generated_schema::<MyContainer<MyIterator>>("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