diff --git a/schemars/Cargo.toml b/schemars/Cargo.toml index fb6b06e5..fc882548 100644 --- a/schemars/Cargo.toml +++ b/schemars/Cargo.toml @@ -21,7 +21,9 @@ dyn-clone = "1.0" chrono = { version = "0.4", default-features = false, optional = true } indexmap = { version = "1.2", features = ["serde-1"], optional = true } -indexmap2 = { version = "2.0", features = ["serde"], optional = true, package = "indexmap" } +indexmap2 = { version = "2.0", features = [ + "serde", +], optional = true, package = "indexmap" } either = { version = "1.3", default-features = false, optional = true } uuid08 = { version = "0.8", default-features = false, optional = true, package = "uuid" } uuid1 = { version = "1.0", default-features = false, optional = true, package = "uuid" } diff --git a/schemars/tests/ui/invalid_validation_attrs.stderr b/schemars/tests/ui/invalid_validation_attrs.stderr index 933fd66f..ae936ec3 100644 --- a/schemars/tests/ui/invalid_validation_attrs.stderr +++ b/schemars/tests/ui/invalid_validation_attrs.stderr @@ -1,59 +1,59 @@ error: expected validate regex attribute to be a string: `regex = "..."` - --> $DIR/invalid_validation_attrs.rs:4:39 + --> tests/ui/invalid_validation_attrs.rs:4:39 | 4 | pub struct Struct1(#[validate(regex = 0, foo, length(min = 1, equal = 2, bar))] String); | ^ error: unknown schemars attribute `foo` - --> $DIR/invalid_validation_attrs.rs:7:42 + --> tests/ui/invalid_validation_attrs.rs:7:42 | 7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String); | ^^^ error: expected schemars regex attribute to be a string: `regex = "..."` - --> $DIR/invalid_validation_attrs.rs:7:39 + --> tests/ui/invalid_validation_attrs.rs:7:39 | 7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String); | ^ -error: schemars attribute cannot contain both `equal` and `min` - --> $DIR/invalid_validation_attrs.rs:7:63 +error: schemars attribute cannot contain both `min` and `equal` + --> tests/ui/invalid_validation_attrs.rs:7:63 | 7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String); | ^^^^^ error: unknown item in schemars length attribute - --> $DIR/invalid_validation_attrs.rs:7:74 + --> tests/ui/invalid_validation_attrs.rs:7:74 | 7 | pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String); | ^^^ -error: schemars attribute cannot contain both `contains` and `regex` - --> $DIR/invalid_validation_attrs.rs:26:9 +error: schemars attribute cannot contain both `regex` and `contains` + --> tests/ui/invalid_validation_attrs.rs:26:9 | 26 | contains = "bar", | ^^^^^^^^ error: duplicate schemars attribute `regex` - --> $DIR/invalid_validation_attrs.rs:27:9 + --> tests/ui/invalid_validation_attrs.rs:27:9 | 27 | regex(path = "baz"), | ^^^^^ error: schemars attribute cannot contain both `phone` and `email` - --> $DIR/invalid_validation_attrs.rs:29:9 + --> tests/ui/invalid_validation_attrs.rs:29:9 | 29 | email, | ^^^^^ error: schemars attribute cannot contain both `phone` and `url` - --> $DIR/invalid_validation_attrs.rs:30:9 + --> tests/ui/invalid_validation_attrs.rs:30:9 | 30 | url | ^^^ error[E0425]: cannot find value `foo` in this scope - --> $DIR/invalid_validation_attrs.rs:12:17 + --> tests/ui/invalid_validation_attrs.rs:12:17 | 12 | regex = "foo", | ^^^^^ not found in this scope diff --git a/schemars/tests/validate_inner.rs b/schemars/tests/validate_inner.rs index 535410f1..64166719 100644 --- a/schemars/tests/validate_inner.rs +++ b/schemars/tests/validate_inner.rs @@ -12,7 +12,7 @@ pub struct Struct<'a> { #[schemars(inner(length(min = 5, max = 100)))] array_str_length: [&'a str; 2], #[schemars(inner(contains(pattern = "substring...")))] - slice_str_contains: &'a[&'a str], + slice_str_contains: &'a [&'a str], #[schemars(inner(regex = "STARTS_WITH_HELLO"))] vec_str_regex: Vec, #[schemars(inner(length(min = 1, max = 100)))] diff --git a/schemars_derive/Cargo.toml b/schemars_derive/Cargo.toml index 4535814b..d9f431ae 100644 --- a/schemars_derive/Cargo.toml +++ b/schemars_derive/Cargo.toml @@ -17,8 +17,8 @@ proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" -syn = { version = "1.0", features = ["extra-traits"] } -serde_derive_internals = "0.26.0" +syn = { version = "2.0", features = ["extra-traits"] } +serde_derive_internals = "0.29.0" [dev-dependencies] pretty_assertions = "1.2.1" diff --git a/schemars_derive/src/ast/mod.rs b/schemars_derive/src/ast/mod.rs index 250a3b2d..73623726 100644 --- a/schemars_derive/src/ast/mod.rs +++ b/schemars_derive/src/ast/mod.rs @@ -38,7 +38,7 @@ pub struct Field<'a> { } impl<'a> Container<'a> { - pub fn from_ast(item: &'a syn::DeriveInput) -> Result, Vec> { + pub fn from_ast(item: &'a syn::DeriveInput) -> Result, syn::Error> { let ctxt = Ctxt::new(); let result = serde_ast::Container::from_ast(&ctxt, item, Derive::Deserialize) .ok_or(()) @@ -48,7 +48,7 @@ impl<'a> Container<'a> { .map(|_| result.expect("from_ast set no errors on Ctxt, so should have returned Ok")) } - pub fn name(&self) -> String { + pub fn name(&self) -> &str { self.serde_attrs.name().deserialize_name() } @@ -64,7 +64,7 @@ impl<'a> Container<'a> { } impl<'a> Variant<'a> { - pub fn name(&self) -> String { + pub fn name(&self) -> &str { self.serde_attrs.name().deserialize_name() } @@ -74,7 +74,7 @@ impl<'a> Variant<'a> { } impl<'a> Field<'a> { - pub fn name(&self) -> String { + pub fn name(&self) -> &str { self.serde_attrs.name().deserialize_name() } } diff --git a/schemars_derive/src/attr/doc.rs b/schemars_derive/src/attr/doc.rs index 0827dc01..a24377af 100644 --- a/schemars_derive/src/attr/doc.rs +++ b/schemars_derive/src/attr/doc.rs @@ -1,4 +1,4 @@ -use syn::{Attribute, Lit::Str, Meta::NameValue, MetaNameValue}; +use syn::Attribute; pub fn get_title_and_desc_from_doc(attrs: &[Attribute]) -> (Option, Option) { let doc = match get_doc(attrs) { @@ -35,16 +35,18 @@ fn get_doc(attrs: &[Attribute]) -> Option { let attrs = attrs .iter() .filter_map(|attr| { - if !attr.path.is_ident("doc") { + if !attr.path().is_ident("doc") { return None; } - let meta = attr.parse_meta().ok()?; - if let NameValue(MetaNameValue { lit: Str(s), .. }) = meta { - return Some(s.value()); - } - - None + let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(litstr), + .. + }) = &attr.meta.require_name_value().ok()?.value + else { + return None; + }; + Some(litstr.value()) }) .collect::>(); diff --git a/schemars_derive/src/attr/mod.rs b/schemars_derive/src/attr/mod.rs index f95bc485..9c930b4a 100644 --- a/schemars_derive/src/attr/mod.rs +++ b/schemars_derive/src/attr/mod.rs @@ -1,3 +1,15 @@ +macro_rules! ident_to_string { + ($meta:expr) => {{ + let Some(path_str) = $meta.path.get_ident().map(|i| i.to_string()) else { + return Err(syn::Error::new_spanned($meta.path, "expected valid ident")); + }; + + path_str + }}; +} + +pub(crate) use ident_to_string; + mod doc; mod schemars_to_serde; mod validation; @@ -9,10 +21,8 @@ use crate::metadata::SchemaMetadata; use proc_macro2::{Group, Span, TokenStream, TokenTree}; use quote::ToTokens; use serde_derive_internals::Ctxt; +use syn::meta::ParseNestedMeta; use syn::parse::{self, Parse}; -use syn::Meta::{List, NameValue}; -use syn::MetaNameValue; -use syn::NestedMeta::{Lit, Meta}; // FIXME using the same struct for containers+variants+fields means that // with/schema_with are accepted (but ignored) on containers, and @@ -37,15 +47,13 @@ pub enum WithAttr { } impl Attrs { - pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self { - let mut result = Attrs::default() - .populate(attrs, "schemars", false, errors) - .populate(attrs, "serde", true, errors); + pub fn new(attrs: &[syn::Attribute], cx: &Ctxt) -> Self { + let mut result = Self::populate(attrs, &[("schemars", false), ("serde", true)], cx); - result.deprecated = attrs.iter().any(|a| a.path.is_ident("deprecated")); + result.deprecated = attrs.iter().any(|a| a.path().is_ident("deprecated")); result.repr = attrs .iter() - .find(|a| a.path.is_ident("repr")) + .find(|a| a.path().is_ident("repr")) .and_then(|a| a.parse_args().ok()); let (doc_title, doc_description) = doc::get_title_and_desc_from_doc(attrs); @@ -76,119 +84,154 @@ impl Attrs { } } - fn populate( - mut self, - attrs: &[syn::Attribute], - attr_type: &'static str, - ignore_errors: bool, - errors: &Ctxt, - ) -> Self { - let duplicate_error = |meta: &MetaNameValue| { - if !ignore_errors { - let msg = format!( - "duplicate schemars attribute `{}`", - meta.path.get_ident().unwrap() - ); - errors.error_spanned_by(meta, msg) - } - }; - let mutual_exclusive_error = |meta: &MetaNameValue, other: &str| { - if !ignore_errors { - let msg = format!( - "schemars attribute cannot contain both `{}` and `{}`", - meta.path.get_ident().unwrap(), - other, + fn populate(attrs: &[syn::Attribute], pop: &[(&'static str, bool)], cx: &Ctxt) -> Self { + if attrs.is_empty() { + return Self::default(); + } + + const WITH: Symbol = "with"; + const SCHEMA_WITH: Symbol = "schema_with"; + const TITLE: Symbol = "title"; + const DESCRIPTION: Symbol = "description"; + const RENAME: Symbol = "rename"; + const CRATE: Symbol = "crate"; + const EXAMPLE: Symbol = "example"; + + let mut with = Attr::none(cx, WITH); + let mut schema_with = Attr::none(cx, SCHEMA_WITH); + let mut title = Attr::none(cx, TITLE); + let mut description = Attr::none(cx, DESCRIPTION); + let mut rename = BoolAttr::none(cx, RENAME); + let mut krate = Attr::none(cx, CRATE); + let mut examples = Vec::new(); + + for attr in attrs { + let Some((attr_type, ignore_errors)) = pop + .iter() + .find_map(|(n, i)| attr.path().is_ident(n).then_some((*n, *i))) + else { + continue; + }; + + let is_schema_rs = attr_type == "schemars"; + + let syn::Meta::List(ml) = &attr.meta else { + cx.error_spanned_by( + attr.path(), + format!("expected {attr_type} to be a attribute meta list"), ); - errors.error_spanned_by(meta, msg) + continue; + }; + + if ml.tokens.is_empty() || is_schema_rs && ml.path.is_ident("inner") { + continue; } - }; - for meta_item in get_meta_items(attrs, attr_type, errors, ignore_errors) { - match &meta_item { - Meta(NameValue(m)) if m.path.is_ident("with") => { - if let Ok(ty) = parse_lit_into_ty(errors, attr_type, "with", &m.lit) { - match self.with { - Some(WithAttr::Type(_)) => duplicate_error(m), - Some(WithAttr::Function(_)) => mutual_exclusive_error(m, "schema_with"), - None => self.with = Some(WithAttr::Type(ty)), + let res = attr.parse_nested_meta(|meta| { + let path_str = ident_to_string!(meta); + let mut skip = false; + + match path_str.as_str() { + WITH => { + if let Some(ls) = get_lit_str(cx, attr_type, WITH, &meta)? { + with.set_exclusive( + meta.path, + ls.parse()?, + [schema_with.excl()], + ignore_errors, + ); } } - } - - Meta(NameValue(m)) if m.path.is_ident("schema_with") => { - if let Ok(fun) = parse_lit_into_path(errors, attr_type, "schema_with", &m.lit) { - match self.with { - Some(WithAttr::Function(_)) => duplicate_error(m), - Some(WithAttr::Type(_)) => mutual_exclusive_error(m, "with"), - None => self.with = Some(WithAttr::Function(fun)), + SCHEMA_WITH => { + if let Some(ls) = get_lit_str(cx, attr_type, SCHEMA_WITH, &meta)? { + schema_with.set_exclusive( + meta.path, + ls.parse()?, + [with.excl()], + ignore_errors, + ); } } - } - - Meta(NameValue(m)) if m.path.is_ident("title") => { - if let Ok(title) = get_lit_str(errors, attr_type, "title", &m.lit) { - match self.title { - Some(_) => duplicate_error(m), - None => self.title = Some(title.value()), + TITLE => { + if let Some(ls) = get_lit_str(cx, attr_type, TITLE, &meta)? { + title.set(meta.path, ls.value(), ignore_errors); } } - } - - Meta(NameValue(m)) if m.path.is_ident("description") => { - if let Ok(description) = get_lit_str(errors, attr_type, "description", &m.lit) { - match self.description { - Some(_) => duplicate_error(m), - None => self.description = Some(description.value()), + DESCRIPTION => { + if let Some(ls) = get_lit_str(cx, attr_type, DESCRIPTION, &meta)? { + description.set(meta.path, ls.value(), ignore_errors); } } - } - - Meta(NameValue(m)) if m.path.is_ident("example") => { - if let Ok(fun) = parse_lit_into_path(errors, attr_type, "example", &m.lit) { - self.examples.push(fun) + "example" => { + if let Some(ls) = get_lit_str(cx, attr_type, EXAMPLE, &meta)? { + examples.push(ls.parse()?); + } } - } - - 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() { - duplicate_error(m) - } else { - self.crate_name = Some(p) + "rename" => { + rename.set_true(meta.path, ignore_errors); + skip = true; + } + "crate" if is_schema_rs => { + if let Some(ls) = get_lit_str(cx, attr_type, DESCRIPTION, &meta)? { + krate.set(meta.path, ls.parse()?, ignore_errors); } } - } - - _ if ignore_errors => {} + "inner" => { + meta.parse_nested_meta(|inn| { + if inn.input.peek(syn::token::Paren) { + inn.parse_nested_meta(|inn2| skip_item(inn2.input)) + } else { + skip_item(inn.input) + } + })?; + } + _ => { + if !schemars_to_serde::SERDE_KEYWORDS + .iter() + .chain(validation::VALIDATION_KEYWORDS) + .any(|kw| meta.path.is_ident(&kw)) + { + let path = meta.path.to_token_stream().to_string().replace(' ', ""); + cx.error_spanned_by( + meta.path, + format!("unknown schemars attribute `{path}`"), + ); + return Ok(()); + } - Meta(List(m)) if m.path.is_ident("inner") && attr_type == "schemars" => { - // This will be processed with the validation attributes. - // It's allowed only for the schemars attribute because the - // validator crate doesn't support it yet. - } + if meta.input.peek(syn::token::Paren) { + meta.parse_nested_meta(|inn| skip_item(inn.input))?; + } - Meta(meta_item) => { - if !is_known_serde_or_validation_keyword(meta_item) { - let path = meta_item - .path() - .into_token_stream() - .to_string() - .replace(' ', ""); - errors.error_spanned_by( - meta_item.path(), - format!("unknown schemars attribute `{}`", path), - ); + skip = true; } } - Lit(lit) => { - errors.error_spanned_by(lit, "unexpected literal in schemars attribute"); + if skip { + skip_item(meta.input)?; } + + Ok(()) + }); + + if let Err(res) = res { + cx.syn_error(res); } } - self + + Self { + with: with + .get() + .map(WithAttr::Type) + .or_else(|| schema_with.get().map(WithAttr::Function)), + title: title.get(), + description: description.get(), + deprecated: false, + repr: None, + examples, + crate_name: krate.get(), + is_renamed: rename.get(), + } } pub fn is_default(&self) -> bool { @@ -205,96 +248,167 @@ impl Attrs { } } -fn is_known_serde_or_validation_keyword(meta: &syn::Meta) -> bool { - let mut known_keywords = schemars_to_serde::SERDE_KEYWORDS - .iter() - .chain(validation::VALIDATION_KEYWORDS); - meta.path() - .get_ident() - .map(|i| known_keywords.any(|k| i == k)) - .unwrap_or(false) +type Symbol = &'static str; + +struct Attr<'c, T> { + cx: &'c Ctxt, + name: Symbol, + value: Option, +} + +#[derive(Copy, Clone)] +struct Excl { + name: Symbol, + set: bool, } -fn get_meta_items( - attrs: &[syn::Attribute], - attr_type: &'static str, - errors: &Ctxt, - ignore_errors: bool, -) -> Vec { - attrs.iter().fold(vec![], |mut acc, attr| { - if !attr.path.is_ident(attr_type) { - return acc; +impl<'c, T> Attr<'c, T> { + fn none(cx: &'c Ctxt, name: Symbol) -> Self { + Self { + cx, + name, + value: None, + } + } + + fn set_exclusive( + &mut self, + obj: A, + value: T, + exl: impl IntoIterator, + ie: bool, + ) { + let tokens = obj.into_token_stream(); + + if self.value.is_some() { + if !ie { + self.cx.error_spanned_by( + tokens, + format!("duplicate schemars attribute `{}`", self.name), + ); + } + + return; } - match attr.parse_meta() { - Ok(List(meta)) => acc.extend(meta.nested), - Ok(other) if !ignore_errors => { - errors.error_spanned_by(other, format!("expected #[{}(...)]", attr_type)) + + let non_exclusive = exl.into_iter().fold(false, |acc, excl| { + if excl.set && !ie { + self.cx.error_spanned_by( + tokens.clone(), + format!( + "schemars attribute cannot contain both `{}` and `{}`", + excl.name, self.name + ), + ); } - Err(err) if !ignore_errors => errors.error_spanned_by(attr, err), - _ => (), + + acc || excl.set + }); + + if non_exclusive { + return; + } + + self.value = Some(value); + } + + fn set(&mut self, obj: A, value: T, ie: bool) { + self.set_exclusive(obj, value, [], ie) + } + + fn get(self) -> Option { + self.value + } + + fn excl(&self) -> Excl { + Excl { + name: self.name, + set: self.value.is_some(), } - acc - }) + } } -fn get_lit_str<'a>( - cx: &Ctxt, - attr_type: &'static str, - meta_item_name: &'static str, - lit: &'a syn::Lit, -) -> Result<&'a syn::LitStr, ()> { - if let syn::Lit::Str(lit) = lit { - Ok(lit) - } else { - cx.error_spanned_by( - lit, - format!( - "expected {} {} attribute to be a string: `{} = \"...\"`", - attr_type, meta_item_name, meta_item_name - ), - ); - Err(()) +struct BoolAttr<'c>(Attr<'c, ()>); + +impl<'c> BoolAttr<'c> { + fn none(cx: &'c Ctxt, name: Symbol) -> Self { + Self(Attr::none(cx, name)) + } + + fn set_true(&mut self, obj: A, ie: bool) { + self.0.set(obj, (), ie); + } + + fn set_true_exclusive( + &mut self, + obj: A, + exl: impl IntoIterator, + ie: bool, + ) { + self.0.set_exclusive(obj, (), exl, ie) + } + + fn get(&self) -> bool { + self.0.value.is_some() } } -fn parse_lit_into_ty( - cx: &Ctxt, - attr_type: &'static str, - meta_item_name: &'static str, - lit: &syn::Lit, -) -> Result { - let string = get_lit_str(cx, attr_type, meta_item_name, lit)?; +fn skip_item(input: syn::parse::ParseStream) -> syn::Result<()> { + // Advance past this meta item + if input.peek(syn::Token![=]) { + input.parse::()?; + input.parse::()?; + } else if input.peek(syn::token::Paren) { + let _skip; + syn::parenthesized!(_skip in input); + } - parse_lit_str(string).map_err(|_| { - cx.error_spanned_by( - lit, - format!( - "failed to parse type: `{} = {:?}`", - meta_item_name, - string.value() - ), - ) - }) + Ok(()) } -fn parse_lit_into_path( +fn get_lit_str( cx: &Ctxt, - attr_type: &'static str, - meta_item_name: &'static str, - lit: &syn::Lit, -) -> Result { - let string = get_lit_str(cx, attr_type, meta_item_name, lit)?; + kind: &'static str, + attr_name: &'static str, + meta: &ParseNestedMeta, +) -> syn::Result> { + get_lit_str2(cx, kind, attr_name, attr_name, meta) +} - parse_lit_str(string).map_err(|_| { +fn get_lit_str2( + cx: &Ctxt, + kind: &'static str, + attr_name: &'static str, + meta_item_name: &'static str, + meta: &ParseNestedMeta, +) -> syn::Result> { + let expr: syn::Expr = meta.value()?.parse()?; + let mut value = &expr; + while let syn::Expr::Group(e) = value { + value = &e.expr; + } + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) = value + { + let suffix = lit.suffix(); + if !suffix.is_empty() { + cx.error_spanned_by( + lit, + format!("unexpected suffix `{suffix}` on string literal"), + ); + } + Ok(Some(lit.clone())) + } else { cx.error_spanned_by( - lit, + expr, format!( - "failed to parse path: `{} = {:?}`", - meta_item_name, - string.value() + "expected {kind} {attr_name} attribute to be a string: `{meta_item_name} = \"...\"`" ), - ) - }) + ); + Ok(None) + } } fn parse_lit_str(s: &syn::LitStr) -> parse::Result diff --git a/schemars_derive/src/attr/schemars_to_serde.rs b/schemars_derive/src/attr/schemars_to_serde.rs index 7878a2c1..44138535 100644 --- a/schemars_derive/src/attr/schemars_to_serde.rs +++ b/schemars_derive/src/attr/schemars_to_serde.rs @@ -2,7 +2,7 @@ use quote::ToTokens; use serde_derive_internals::Ctxt; use std::collections::HashSet; use syn::parse::Parser; -use syn::{Attribute, Data, Field, Meta, NestedMeta, Variant}; +use syn::{Attribute, Data, Field, Variant}; // List of keywords that can appear in #[serde(...)]/#[schemars(...)] attributes which we want serde_derive_internals to parse for us. pub(crate) static SERDE_KEYWORDS: &[&str] = &[ @@ -32,7 +32,7 @@ pub(crate) static SERDE_KEYWORDS: &[&str] = &[ // If a struct/variant/field has any #[schemars] attributes, then create copies of them // as #[serde] attributes so that serde_derive_internals will parse them for us. -pub fn process_serde_attrs(input: &mut syn::DeriveInput) -> Result<(), Vec> { +pub fn process_serde_attrs(input: &mut syn::DeriveInput) -> Result<(), syn::Error> { let ctxt = Ctxt::new(); process_attrs(&ctxt, &mut input.attrs); match input.data { @@ -60,26 +60,45 @@ 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.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 + let (mut serde_meta, mut schemars_meta_names): (Vec<_>, HashSet<_>) = attrs .iter() - .flat_map(|at| get_meta_items(ctxt, at)) - .flatten() - .filter_map(|meta| { - let keyword = get_meta_ident(ctxt, &meta).ok()?; - if keyword.ends_with("with") || !SERDE_KEYWORDS.contains(&keyword.as_ref()) { - None - } else { - Some((meta, keyword)) + .filter_map(|at| { + if !at.path().is_ident("schemars") { + return None; } + + match at.meta.require_list() { + Ok(ml) => { + match ml.parse_args_with( + syn::punctuated::Punctuated::::parse_terminated, + ) { + Ok(meta) => Some(meta), + Err(err) => { + ctxt.syn_error(err); + None + } + } + } + Err(_err) => { + ctxt.error_spanned_by(at, "expected #[schemars(...)]"); + None + } + } + }) + .flat_map(|ml| { + ml.into_iter().filter_map(|meta| { + let kw = meta.path().get_ident().map(|i| i.to_string())?; + + if kw.ends_with("with") || !SERDE_KEYWORDS.contains(&kw.as_str()) { + None + } else { + Some((meta, kw)) + } + }) }) .unzip(); @@ -89,19 +108,31 @@ fn process_attrs(ctxt: &Ctxt, attrs: &mut Vec) { } // 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()) - && i != "bound" - { - serde_meta.push(meta); + for attr in serde_attrs { + let ml = match attr.meta.require_list() { + Ok(ml) => ml, + Err(_err) => { + ctxt.error_spanned_by(attr, "expected #[serde(...)]"); + continue; } - } + }; + + let Ok(ml) = ml + .parse_args_with(syn::punctuated::Punctuated::::parse_terminated) + else { + continue; + }; + + serde_meta.extend(ml.into_iter().filter_map(|meta| { + let Some(kw) = meta.path().get_ident().map(|i| i.to_string()) else { + return None; + }; + + (kw != "bound" + && !schemars_meta_names.contains(&kw) + && SERDE_KEYWORDS.contains(&kw.as_ref())) + .then_some(meta) + })); } if !serde_meta.is_empty() { @@ -125,36 +156,6 @@ fn to_tokens(attrs: &[Attribute]) -> impl ToTokens { tokens } -fn get_meta_items(ctxt: &Ctxt, attr: &Attribute) -> Result, ()> { - match attr.parse_meta() { - Ok(Meta::List(meta)) => Ok(meta.nested.into_iter().collect()), - Ok(_) => { - ctxt.error_spanned_by(attr, "expected #[schemars(...)] or #[serde(...)]"); - Err(()) - } - Err(err) => { - ctxt.error_spanned_by(attr, err); - Err(()) - } - } -} - -fn get_meta_ident(ctxt: &Ctxt, meta: &NestedMeta) -> Result { - match meta { - NestedMeta::Meta(m) => m.path().get_ident().map(|i| i.to_string()).ok_or(()), - NestedMeta::Lit(lit) => { - ctxt.error_spanned_by( - meta, - format!( - "unexpected literal in attribute: {}", - lit.into_token_stream() - ), - ); - Err(()) - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -198,7 +199,7 @@ mod tests { }; if let Err(e) = process_serde_attrs(&mut input) { - panic!("process_serde_attrs returned error: {}", e[0]) + panic!("process_serde_attrs returned error: {e}") }; assert_eq!(input, expected); diff --git a/schemars_derive/src/attr/validation.rs b/schemars_derive/src/attr/validation.rs index ec7cc145..5ed683a5 100644 --- a/schemars_derive/src/attr/validation.rs +++ b/schemars_derive/src/attr/validation.rs @@ -1,7 +1,8 @@ -use super::{get_lit_str, get_meta_items, parse_lit_into_path, parse_lit_str}; +use super::{get_lit_str, parse_lit_str, Attr, BoolAttr, Symbol}; use proc_macro2::TokenStream; +use quote::ToTokens; use serde_derive_internals::Ctxt; -use syn::{Expr, ExprLit, ExprPath, Lit, Meta, MetaNameValue, NestedMeta, Path}; +use syn::{meta::ParseNestedMeta, Expr, ExprLit, ExprPath, Lit}; pub(crate) static VALIDATION_KEYWORDS: &[&str] = &[ "range", "regex", "contains", "email", "phone", "url", "length", "required", @@ -15,14 +16,6 @@ enum Format { } impl Format { - fn attr_str(self) -> &'static str { - match self { - Format::Email => "email", - Format::Uri => "url", - Format::Phone => "phone", - } - } - fn schema_str(self) -> &'static str { match self { Format::Email => "email", @@ -43,286 +36,358 @@ pub struct ValidationAttrs { contains: Option, required: bool, format: Option, - inner: Option>, + inner: Option>, } -impl ValidationAttrs { - pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self { - let schemars_items = get_meta_items(attrs, "schemars", errors, false); - let validate_items = get_meta_items(attrs, "validate", errors, true); +const MIN: Symbol = "min"; +const MAX: Symbol = "max"; +const EQUAL: Symbol = "equal"; +const REGEX: Symbol = "regex"; +const CONTAINS: Symbol = "contains"; +const REQUIRED: Symbol = "required"; +const INNER: Symbol = "inner"; +const URL: Symbol = "url"; +const EMAIL: Symbol = "email"; +const PHONE: Symbol = "phone"; +const PATH: Symbol = "path"; +const PATTERN: Symbol = "pattern"; + +enum Regex { + Pattern(syn::LitStr), + Path(syn::Path), +} + +struct Populate<'c> { + length_min: Attr<'c, Expr>, + length_max: Attr<'c, Expr>, + length_equal: Attr<'c, Expr>, + range_min: Attr<'c, Expr>, + range_max: Attr<'c, Expr>, + regex: Attr<'c, Regex>, + contains: Attr<'c, String>, + required: BoolAttr<'c>, + email: BoolAttr<'c>, + url: BoolAttr<'c>, + phone: BoolAttr<'c>, + inner: Attr<'c, ValidationAttrs>, + cx: &'c Ctxt, +} - ValidationAttrs::default() - .populate(schemars_items, "schemars", false, errors) - .populate(validate_items, "validate", true, errors) +impl<'c> From> for ValidationAttrs { + fn from(pop: Populate<'c>) -> Self { + Self { + length_min: pop.length_min.get(), + length_max: pop.length_max.get(), + length_equal: pop.length_equal.get(), + range_min: pop.range_min.get(), + range_max: pop.range_max.get(), + regex: pop.regex.get().map(|rx| match rx { + Regex::Path(path) => Expr::Path(syn::ExprPath { + attrs: Vec::new(), + qself: None, + path, + }), + Regex::Pattern(patt) => Expr::Lit(syn::ExprLit { + attrs: Vec::new(), + lit: syn::Lit::Str(patt), + }), + }), + contains: pop.contains.get(), + required: pop.required.get(), + format: pop + .url + .get() + .then_some(Format::Uri) + .or_else(|| pop.email.get().then_some(Format::Email)) + .or_else(|| pop.phone.get().then_some(Format::Phone)), + inner: pop.inner.get().map(Box::new), + } } +} - pub fn required(&self) -> bool { - self.required +impl<'c> Populate<'c> { + fn new(cx: &'c Ctxt) -> Self { + Self { + length_min: Attr::none(cx, MIN), + length_max: Attr::none(cx, MAX), + length_equal: Attr::none(cx, EQUAL), + range_min: Attr::none(cx, MIN), + range_max: Attr::none(cx, MAX), + regex: Attr::none(cx, REGEX), + contains: Attr::none(cx, CONTAINS), + required: BoolAttr::none(cx, REQUIRED), + url: BoolAttr::none(cx, URL), + email: BoolAttr::none(cx, EMAIL), + phone: BoolAttr::none(cx, PHONE), + inner: Attr::none(cx, INNER), + cx, + } } fn populate( - mut self, - meta_items: Vec, + &mut self, attr_type: &'static str, + meta: ParseNestedMeta, ignore_errors: bool, - errors: &Ctxt, - ) -> Self { - let duplicate_error = |path: &Path| { - if !ignore_errors { - let msg = format!( - "duplicate schemars attribute `{}`", - path.get_ident().unwrap() - ); - errors.error_spanned_by(path, msg) - } - }; - let mutual_exclusive_error = |path: &Path, other: &str| { - if !ignore_errors { - let msg = format!( - "schemars attribute cannot contain both `{}` and `{}`", - path.get_ident().unwrap(), - other, - ); - errors.error_spanned_by(path, msg) - } - }; - let duplicate_format_error = |existing: Format, new: Format, path: &syn::Path| { - if !ignore_errors { - let msg = if existing == new { - format!("duplicate schemars attribute `{}`", existing.attr_str()) - } else { - format!( - "schemars attribute cannot contain both `{}` and `{}`", - existing.attr_str(), - new.attr_str(), - ) - }; - errors.error_spanned_by(path, msg) - } - }; + ) -> syn::Result<()> { + let path_str = super::ident_to_string!(meta); + + // Wrap in a closure so we _always_ consume the set of meta items, otherwise + // we get cascading errors that obscure the root error + let mut inner = |meta: &ParseNestedMeta| { + match path_str.as_str() { + // length(min = n, max = n, equal = n) + "length" => meta.parse_nested_meta(|linner| { + if linner.path.is_ident(MIN) { + str_or_num_to_expr( + &mut self.length_min, + &linner, + [self.length_equal.excl()], + ignore_errors, + ); + } else if linner.path.is_ident(MAX) { + str_or_num_to_expr( + &mut self.length_max, + &linner, + [self.length_equal.excl()], + ignore_errors, + ); + } else if linner.path.is_ident(EQUAL) { + str_or_num_to_expr( + &mut self.length_equal, + &linner, + [self.length_min.excl(), self.length_max.excl()], + ignore_errors, + ); + } else { + if !ignore_errors { + self.cx.error_spanned_by( + linner.path, + "unknown item in schemars length attribute", + ); + } + super::skip_item(linner.input)?; + } - for meta_item in meta_items { - match &meta_item { - NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("length") => { - for nested in meta_list.nested.iter() { - match nested { - NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => { - if self.length_min.is_some() { - duplicate_error(&nv.path) - } else if self.length_equal.is_some() { - mutual_exclusive_error(&nv.path, "equal") - } else { - self.length_min = str_or_num_to_expr(errors, "min", &nv.lit); - } + Ok(()) + })?, + // range(min = n, max = n) + "range" => { + meta.parse_nested_meta(|rinner| { + if rinner.path.is_ident(MIN) { + str_or_num_to_expr(&mut self.range_min, &rinner, None, ignore_errors); + } else if rinner.path.is_ident(MAX) { + str_or_num_to_expr(&mut self.range_max, &rinner, None, ignore_errors); + } else { + if !ignore_errors { + self.cx.error_spanned_by( + rinner.path, + "unknown item in schemars range attribute", + ); } - NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("max") => { - if self.length_max.is_some() { - duplicate_error(&nv.path) - } else if self.length_equal.is_some() { - mutual_exclusive_error(&nv.path, "equal") - } else { - self.length_max = str_or_num_to_expr(errors, "max", &nv.lit); + super::skip_item(rinner.input)?; + } + + Ok(()) + })?; + } + "required" | "required_nested" => { + self.required.set_true(meta.path.clone(), ignore_errors); + } + EMAIL => { + self.email.set_true_exclusive( + meta.path.clone(), + [self.url.0.excl(), self.phone.0.excl()], + ignore_errors, + ); + } + URL => { + self.url.set_true_exclusive( + meta.path.clone(), + [self.phone.0.excl(), self.email.0.excl()], + ignore_errors, + ); + } + PHONE => { + self.phone.set_true_exclusive( + meta.path.clone(), + [self.email.0.excl(), self.url.0.excl()], + ignore_errors, + ); + } + REGEX => { + // (regex = "path") + // + // (regex(path = "path", pattern = "pattern") + let item = &mut self.regex; + let excl = [self.contains.excl()]; + if meta.input.peek(Token![=]) { + if let Some(ls) = get_lit_str(self.cx, attr_type, REGEX, meta)? { + item.set_exclusive( + meta.path.clone(), + Regex::Path(ls.parse::()?), + excl, + ignore_errors, + ); + } + } else { + meta.parse_nested_meta(|rinner| { + if rinner.path.is_ident(PATH) { + if let Some(ls) = get_lit_str(self.cx, attr_type, PATH, &rinner)? { + item.set_exclusive( + meta.path.clone(), + Regex::Path(ls.parse::()?), + excl, + ignore_errors, + ); } - } - NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("equal") => { - if self.length_equal.is_some() { - duplicate_error(&nv.path) - } else if self.length_min.is_some() { - mutual_exclusive_error(&nv.path, "min") - } else if self.length_max.is_some() { - mutual_exclusive_error(&nv.path, "max") - } else { - self.length_equal = - str_or_num_to_expr(errors, "equal", &nv.lit); + } else if rinner.path.is_ident(PATTERN) { + if let Some(patt) = + get_lit_str(self.cx, attr_type, PATTERN, &rinner)? + { + item.set_exclusive( + meta.path.clone(), + Regex::Pattern(patt), + excl, + ignore_errors, + ); } - } - meta => { + } else { if !ignore_errors { - errors.error_spanned_by( - meta, - "unknown item in schemars length attribute".to_string(), + self.cx.error_spanned_by( + rinner.path, + "unknown item in schemars regex attribute", ); } + super::skip_item(rinner.input)?; } - } + + Ok(()) + })?; } } - - NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("range") => { - for nested in meta_list.nested.iter() { - match nested { - NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => { - if self.range_min.is_some() { - duplicate_error(&nv.path) - } else { - self.range_min = str_or_num_to_expr(errors, "min", &nv.lit); - } - } - NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("max") => { - if self.range_max.is_some() { - duplicate_error(&nv.path) - } else { - self.range_max = str_or_num_to_expr(errors, "max", &nv.lit); + CONTAINS => { + // (contains = "pattern") + // + // (contains(pattern = "pattern") + let item = &mut self.contains; + let excl = [self.regex.excl()]; + + if meta.input.peek(Token![=]) { + if let Some(patt) = get_lit_str(self.cx, attr_type, CONTAINS, meta)? { + item.set_exclusive( + meta.path.clone(), + patt.value(), + excl, + ignore_errors, + ); + } + } else { + meta.parse_nested_meta(|rinner| { + if rinner.path.is_ident(PATTERN) { + if let Some(patt) = + get_lit_str(self.cx, attr_type, PATTERN, &rinner)? + { + item.set_exclusive( + meta.path.clone(), + patt.value(), + excl, + ignore_errors, + ); } - } - meta => { + } else { if !ignore_errors { - errors.error_spanned_by( - meta, - "unknown item in schemars range attribute".to_string(), + self.cx.error_spanned_by( + rinner.path, + "unknown item in schemars contains attribute", ); } + + super::skip_item(rinner.input)?; } - } + + Ok(()) + })?; } } + "inner" => { + let mut inner_pop = Populate::new(self.cx); - NestedMeta::Meta(Meta::Path(m)) - if m.is_ident("required") || m.is_ident("required_nested") => - { - self.required = true; - } + meta.parse_nested_meta(|iinner| { + inner_pop.populate(attr_type, iinner, ignore_errors) + })?; - NestedMeta::Meta(Meta::Path(p)) if p.is_ident(Format::Email.attr_str()) => { - match self.format { - Some(f) => duplicate_format_error(f, Format::Email, p), - None => self.format = Some(Format::Email), - } - } - NestedMeta::Meta(Meta::Path(p)) if p.is_ident(Format::Uri.attr_str()) => { - match self.format { - Some(f) => duplicate_format_error(f, Format::Uri, p), - None => self.format = Some(Format::Uri), - } - } - NestedMeta::Meta(Meta::Path(p)) if p.is_ident(Format::Phone.attr_str()) => { - match self.format { - Some(f) => duplicate_format_error(f, Format::Phone, p), - None => self.format = Some(Format::Phone), - } + self.inner + .set(meta.path.clone(), inner_pop.into(), ignore_errors); } + _ => {} + } - NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("regex") => { - match (&self.regex, &self.contains) { - (Some(_), _) => duplicate_error(&nv.path), - (None, Some(_)) => mutual_exclusive_error(&nv.path, "contains"), - (None, None) => { - self.regex = - parse_lit_into_expr_path(errors, attr_type, "regex", &nv.lit).ok() - } - } - } + Ok(()) + }; - NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("regex") => { - match (&self.regex, &self.contains) { - (Some(_), _) => duplicate_error(&meta_list.path), - (None, Some(_)) => mutual_exclusive_error(&meta_list.path, "contains"), - (None, None) => { - for x in meta_list.nested.iter() { - match x { - NestedMeta::Meta(Meta::NameValue(MetaNameValue { - path, - lit, - .. - })) if path.is_ident("path") => { - self.regex = - parse_lit_into_expr_path(errors, attr_type, "path", lit) - .ok() - } - NestedMeta::Meta(Meta::NameValue(MetaNameValue { - path, - lit, - .. - })) if path.is_ident("pattern") => { - self.regex = get_lit_str(errors, attr_type, "pattern", lit) - .ok() - .map(|litstr| { - Expr::Lit(syn::ExprLit { - attrs: Vec::new(), - lit: Lit::Str(litstr.clone()), - }) - }) - } - meta => { - if !ignore_errors { - errors.error_spanned_by( - meta, - "unknown item in schemars regex attribute" - .to_string(), - ); - } - } - } - } - } - } - } + let res = inner(&meta); - NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. })) - if path.is_ident("contains") => - { - match (&self.contains, &self.regex) { - (Some(_), _) => duplicate_error(path), - (None, Some(_)) => mutual_exclusive_error(path, "regex"), - (None, None) => { - self.contains = get_lit_str(errors, attr_type, "contains", lit) - .map(|litstr| litstr.value()) - .ok() - } - } + // We've already validated the contents of the serde/schemars attributes at this point + // so we can just silently skip the items that don't affect validation + super::skip_item(meta.input)?; + + res + } +} + +impl ValidationAttrs { + pub fn new(attrs: &[syn::Attribute], cx: &Ctxt) -> Self { + if attrs.is_empty() { + return ValidationAttrs::default(); + } + + let mut pop = Populate::new(cx); + + // Note that we can't just iterate through the attributes once, to preserve + // the old logic we need to walk through the schemars ones first, then + // validate, as the schemars attributes will give errors and validate ones + // won't, and won't override an attribute if it was already set by schemars + for (name, ignore_errors) in [("schemars", false), ("validate", true)] { + for attr in attrs { + if !attr.path().is_ident(name) { + continue; } - NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("contains") => { - match (&self.contains, &self.regex) { - (Some(_), _) => duplicate_error(&meta_list.path), - (None, Some(_)) => mutual_exclusive_error(&meta_list.path, "regex"), - (None, None) => { - for x in meta_list.nested.iter() { - match x { - NestedMeta::Meta(Meta::NameValue(MetaNameValue { - path, - lit, - .. - })) if path.is_ident("pattern") => { - self.contains = - get_lit_str(errors, attr_type, "contains", lit) - .ok() - .map(|litstr| litstr.value()) - } - meta => { - if !ignore_errors { - errors.error_spanned_by( - meta, - "unknown item in schemars contains attribute" - .to_string(), - ); - } - } - } - } + match &attr.meta { + syn::Meta::Path(p) => { + // Due to how the old code gathered meta items, this would work, but parse_nested_meta + // requires a list or name value pair + if !p.is_ident("validate") { + cx.error_spanned_by(p, "unexpected path item"); } - } - } - NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("inner") => { - match self.inner { - Some(_) => duplicate_error(&meta_list.path), - None => { - let inner_attrs = ValidationAttrs::default().populate( - meta_list.nested.clone().into_iter().collect(), - attr_type, - ignore_errors, - errors, - ); - self.inner = Some(Box::new(inner_attrs)); + continue; + } + syn::Meta::List(ml) => { + if ml.tokens.is_empty() { + continue; } } + syn::Meta::NameValue(mnv) => { + cx.error_spanned_by(mnv, "only meta lists are supported"); + continue; + } } - _ => {} + if let Err(err) = + attr.parse_nested_meta(|meta| pop.populate(name, meta, ignore_errors)) + { + cx.syn_error(err); + } } } - self + + pop.into() + } + + pub fn required(&self) -> bool { + self.required } pub fn apply_to_schema(&self, schema_expr: &mut TokenStream) { @@ -442,21 +507,6 @@ impl ValidationAttrs { } } -fn parse_lit_into_expr_path( - cx: &Ctxt, - attr_type: &'static str, - meta_item_name: &'static str, - lit: &syn::Lit, -) -> Result { - parse_lit_into_path(cx, attr_type, meta_item_name, lit).map(|path| { - Expr::Path(ExprPath { - attrs: Vec::new(), - qself: None, - path, - }) - }) -} - fn wrap_array_validation(v: Vec) -> Option { if v.is_empty() { None @@ -510,22 +560,50 @@ fn wrap_string_validation(v: Vec) -> Option { } } -fn str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, lit: &Lit) -> Option { - match lit { - Lit::Str(s) => parse_lit_str::(s).ok().map(Expr::Path), - Lit::Int(_) | Lit::Float(_) => Some(Expr::Lit(ExprLit { +fn str_or_num_to_expr( + attr: &mut Attr<'_, Expr>, + meta: &syn::meta::ParseNestedMeta<'_>, + excl: impl IntoIterator, + ie: bool, +) { + let val = match meta.value() { + Ok(lit) => lit, + Err(err) => { + attr.cx.syn_error(err); + return; + } + }; + + let lit = match val.parse() { + Ok(l) => l, + Err(err) => { + attr.cx.syn_error(err); + return; + } + }; + + let expr = match lit { + Lit::Str(s) => { + let Some(expr) = parse_lit_str::(&s).ok().map(Expr::Path) else { + return; + }; + expr + } + Lit::Int(_) | Lit::Float(_) => Expr::Lit(ExprLit { attrs: Vec::new(), - lit: lit.clone(), - })), + lit, + }), _ => { - cx.error_spanned_by( + attr.cx.error_spanned_by( lit, format!( "expected `{}` to be a string or number literal", - meta_item_name + meta.path.to_token_stream().to_string().replace(' ', "") ), ); - None + return; } - } + }; + + attr.set_exclusive(meta.path.clone(), expr, excl, ie); } diff --git a/schemars_derive/src/lib.rs b/schemars_derive/src/lib.rs index 3ad10319..3fc0118f 100644 --- a/schemars_derive/src/lib.rs +++ b/schemars_derive/src/lib.rs @@ -32,10 +32,7 @@ pub fn derive_json_schema_repr_wrapper(input: proc_macro::TokenStream) -> proc_m .into() } -fn derive_json_schema( - mut input: syn::DeriveInput, - repr: bool, -) -> Result> { +fn derive_json_schema(mut input: syn::DeriveInput, repr: bool) -> Result { attr::process_serde_attrs(&mut input)?; let mut cont = Container::from_ast(&input)?; @@ -87,7 +84,7 @@ fn derive_json_schema( }); } - let mut schema_base_name = cont.name(); + let mut schema_base_name = cont.name().to_owned(); if !cont.attrs.is_renamed { if let Some(path) = cont.serde_attrs.remote() { @@ -118,7 +115,7 @@ fn derive_json_schema( }, ) } else if cont.attrs.is_renamed { - let mut schema_name_fmt = schema_base_name; + let mut schema_name_fmt = schema_base_name.to_owned(); for tp in ¶ms { schema_name_fmt.push_str(&format!("{{{}:.0}}", tp)); } @@ -141,7 +138,7 @@ fn derive_json_schema( }, ) } else { - let mut schema_name_fmt = schema_base_name; + let mut schema_name_fmt = schema_base_name.to_owned(); schema_name_fmt.push_str("_for_{}"); schema_name_fmt.push_str(&"_and_{}".repeat(params.len() - 1)); ( @@ -165,7 +162,7 @@ fn derive_json_schema( }; let schema_expr = if repr { - schema_exprs::expr_for_repr(&cont).map_err(|e| vec![e])? + schema_exprs::expr_for_repr(&cont)? } else { schema_exprs::expr_for_container(&cont) }; @@ -208,9 +205,6 @@ fn add_trait_bounds(cont: &mut Container) { } } -fn compile_error(errors: Vec) -> TokenStream { - let compile_errors = errors.iter().map(syn::Error::to_compile_error); - quote! { - #(#compile_errors)* - } +fn compile_error(error: syn::Error) -> TokenStream { + error.to_compile_error() } diff --git a/schemars_derive/src/schema_exprs.rs b/schemars_derive/src/schema_exprs.rs index 9bd81325..623fcb55 100644 --- a/schemars_derive/src/schema_exprs.rs +++ b/schemars_derive/src/schema_exprs.rs @@ -155,7 +155,7 @@ fn expr_for_external_tagged_enum<'a>( let mut count = 0; let (unit_variants, complex_variants): (Vec<_>, Vec<_>) = variants .inspect(|v| { - unique_names.insert(v.name()); + unique_names.insert(v.name().into()); count += 1; }) .partition(|v| v.is_unit() && v.attrs.is_default());