Skip to content

Commit

Permalink
Add #[schemars(bound = ...)] attribute
Browse files Browse the repository at this point in the history
Based on #162

Co-authored-by: teozkr <teo@nullable.se>
  • Loading branch information
GREsau and nightkr committed Oct 2, 2022
1 parent 6ada120 commit 104dccc
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 14 deletions.
29 changes: 29 additions & 0 deletions schemars/tests/bound.rs
Original file line number Diff line number Diff line change
@@ -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")
}
17 changes: 17 additions & 0 deletions schemars/tests/expected/bound.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
2 changes: 1 addition & 1 deletion schemars_derive/src/ast/from_serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion schemars_derive/src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
6 changes: 6 additions & 0 deletions schemars_derive/src/attr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -196,6 +201,7 @@ impl Attrs {
examples,
repr: None,
crate_name: None,
is_renamed: _,
} if examples.is_empty() => true,
_ => false,
}
Expand Down
5 changes: 4 additions & 1 deletion schemars_derive/src/attr/schemars_to_serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -56,16 +57,17 @@ 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
.iter()
.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))
Expand All @@ -85,6 +87,7 @@ 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))
Expand Down
29 changes: 18 additions & 11 deletions schemars_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -84,22 +84,22 @@ 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();
}
}
}

// 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));
Expand Down Expand Up @@ -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));
}
}
}
}
Expand Down

0 comments on commit 104dccc

Please sign in to comment.