Add #[schemars(bound = ...)] attribute
Based on https://github.com/GREsau/schemars/pull/162 Co-authored-by: teozkr <teo@nullable.se>
This commit is contained in:
parent
6ada120cd3
commit
104dccca50
7 changed files with 76 additions and 14 deletions
29
schemars/tests/bound.rs
Normal file
29
schemars/tests/bound.rs
Normal file
|
@ -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
schemars/tests/expected/bound.json
Normal file
17
schemars/tests/expected/bound.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ impl<'a> FromSerde for Container<'a> {
|
||||||
ident: serde.ident,
|
ident: serde.ident,
|
||||||
serde_attrs: serde.attrs,
|
serde_attrs: serde.attrs,
|
||||||
data: Data::from_serde(errors, serde.data)?,
|
data: Data::from_serde(errors, serde.data)?,
|
||||||
generics: serde.generics,
|
generics: serde.generics.clone(),
|
||||||
original: serde.original,
|
original: serde.original,
|
||||||
// FIXME this allows with/schema_with attribute on containers
|
// FIXME this allows with/schema_with attribute on containers
|
||||||
attrs: Attrs::new(&serde.original.attrs, errors),
|
attrs: Attrs::new(&serde.original.attrs, errors),
|
||||||
|
|
|
@ -9,7 +9,7 @@ pub struct Container<'a> {
|
||||||
pub ident: syn::Ident,
|
pub ident: syn::Ident,
|
||||||
pub serde_attrs: serde_derive_internals::attr::Container,
|
pub serde_attrs: serde_derive_internals::attr::Container,
|
||||||
pub data: Data<'a>,
|
pub data: Data<'a>,
|
||||||
pub generics: &'a syn::Generics,
|
pub generics: syn::Generics,
|
||||||
pub original: &'a syn::DeriveInput,
|
pub original: &'a syn::DeriveInput,
|
||||||
pub attrs: Attrs,
|
pub attrs: Attrs,
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ pub struct Attrs {
|
||||||
pub examples: Vec<syn::Path>,
|
pub examples: Vec<syn::Path>,
|
||||||
pub repr: Option<syn::Type>,
|
pub repr: Option<syn::Type>,
|
||||||
pub crate_name: Option<syn::Path>,
|
pub crate_name: Option<syn::Path>,
|
||||||
|
pub is_renamed: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[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" => {
|
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 let Ok(p) = parse_lit_into_path(errors, attr_type, "crate", &m.lit) {
|
||||||
if self.crate_name.is_some() {
|
if self.crate_name.is_some() {
|
||||||
|
@ -196,6 +201,7 @@ impl Attrs {
|
||||||
examples,
|
examples,
|
||||||
repr: None,
|
repr: None,
|
||||||
crate_name: None,
|
crate_name: None,
|
||||||
|
is_renamed: _,
|
||||||
} if examples.is_empty() => true,
|
} if examples.is_empty() => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ pub(crate) static SERDE_KEYWORDS: &[&str] = &[
|
||||||
"flatten",
|
"flatten",
|
||||||
"remote",
|
"remote",
|
||||||
"transparent",
|
"transparent",
|
||||||
|
"bound",
|
||||||
// Special cases - `with`/`serialize_with` are passed to serde but not copied from schemars attrs to serde attrs.
|
// 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
|
// 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
|
// 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<Item = &'a m
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
|
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<_>) =
|
let (serde_attrs, other_attrs): (Vec<_>, Vec<_>) =
|
||||||
attrs.drain(..).partition(|at| at.path.is_ident("serde"));
|
attrs.drain(..).partition(|at| at.path.is_ident("serde"));
|
||||||
|
|
||||||
*attrs = other_attrs;
|
*attrs = other_attrs;
|
||||||
|
|
||||||
let schemars_attrs: Vec<_> = attrs
|
let schemars_attrs: Vec<_> = attrs
|
||||||
|
@ -66,6 +67,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
|
||||||
.filter(|at| at.path.is_ident("schemars"))
|
.filter(|at| at.path.is_ident("schemars"))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Copy appropriate #[schemars(...)] attributes to #[serde(...)] attributes
|
||||||
let (mut serde_meta, mut schemars_meta_names): (Vec<_>, HashSet<_>) = schemars_attrs
|
let (mut serde_meta, mut schemars_meta_names): (Vec<_>, HashSet<_>) = schemars_attrs
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|at| get_meta_items(&ctxt, at))
|
.flat_map(|at| get_meta_items(&ctxt, at))
|
||||||
|
@ -85,6 +87,7 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec<Attribute>) {
|
||||||
schemars_meta_names.insert("skip_deserializing".to_string());
|
schemars_meta_names.insert("skip_deserializing".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-add #[serde(...)] attributes that weren't overridden by #[schemars(...)] attributes
|
||||||
for meta in serde_attrs
|
for meta in serde_attrs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|at| get_meta_items(&ctxt, &at))
|
.flat_map(|at| get_meta_items(&ctxt, &at))
|
||||||
|
|
|
@ -36,11 +36,11 @@ fn derive_json_schema(
|
||||||
mut input: syn::DeriveInput,
|
mut input: syn::DeriveInput,
|
||||||
repr: bool,
|
repr: bool,
|
||||||
) -> Result<TokenStream, Vec<syn::Error>> {
|
) -> Result<TokenStream, Vec<syn::Error>> {
|
||||||
add_trait_bounds(&mut input.generics);
|
|
||||||
|
|
||||||
attr::process_serde_attrs(&mut input)?;
|
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| {
|
let crate_alias = cont.attrs.crate_name.as_ref().map(|path| {
|
||||||
quote_spanned! {path.span()=>
|
quote_spanned! {path.span()=>
|
||||||
use #path as schemars;
|
use #path as schemars;
|
||||||
|
@ -84,9 +84,8 @@ fn derive_json_schema(
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut schema_base_name = cont.name();
|
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(path) = cont.serde_attrs.remote() {
|
||||||
if let Some(segment) = path.segments.last() {
|
if let Some(segment) = path.segments.last() {
|
||||||
schema_base_name = segment.ident.to_string();
|
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 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! {
|
quote! {
|
||||||
#schema_base_name.to_owned()
|
#schema_base_name.to_owned()
|
||||||
}
|
}
|
||||||
} else if schema_is_renamed {
|
} else if cont.attrs.is_renamed {
|
||||||
let mut schema_name_fmt = schema_base_name;
|
let mut schema_name_fmt = schema_base_name;
|
||||||
for tp in &type_params {
|
for tp in &type_params {
|
||||||
schema_name_fmt.push_str(&format!("{{{}:.0}}", tp));
|
schema_name_fmt.push_str(&format!("{{{}:.0}}", tp));
|
||||||
|
@ -141,10 +141,17 @@ fn derive_json_schema(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_trait_bounds(generics: &mut syn::Generics) {
|
fn add_trait_bounds(cont: &mut Container) {
|
||||||
for param in &mut generics.params {
|
if let Some(bounds) = cont.serde_attrs.ser_bound() {
|
||||||
if let syn::GenericParam::Type(ref mut type_param) = *param {
|
let where_clause = cont.generics.make_where_clause();
|
||||||
type_param.bounds.push(parse_quote!(schemars::JsonSchema));
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue