Emit compilation errors for duplicate validation attributes

This commit is contained in:
Graham Esau 2021-09-17 22:57:51 +01:00
parent af69a8ea11
commit 605db3bba8
7 changed files with 230 additions and 90 deletions

View file

@ -6,4 +6,30 @@ pub struct Struct1(#[validate(regex = 0, foo, length(min = 1, equal = 2, bar))]
#[derive(JsonSchema)]
pub struct Struct2(#[schemars(regex = 0, foo, length(min = 1, equal = 2, bar))] String);
#[derive(JsonSchema)]
pub struct Struct3(
#[validate(
regex = "foo",
contains = "bar",
regex(path = "baz"),
phone,
email,
url
)]
String,
);
#[derive(JsonSchema)]
pub struct Struct4(
#[schemars(
regex = "foo",
contains = "bar",
regex(path = "baz"),
phone,
email,
url
)]
String,
);
fn main() {}

View file

@ -20,10 +20,40 @@ error: schemars attribute cannot contain both `equal` and `min`
--> $DIR/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
|
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
|
26 | contains = "bar",
| ^^^^^^^^
error: duplicate schemars attribute `regex`
--> $DIR/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
|
29 | email,
| ^^^^^
error: schemars attribute cannot contain both `phone` and `url`
--> $DIR/invalid_validation_attrs.rs:30:9
|
30 | url
| ^^^
error[E0425]: cannot find value `foo` in this scope
--> $DIR/invalid_validation_attrs.rs:12:17
|
12 | regex = "foo",
| ^^^^^ not found in this scope

View file

@ -4,4 +4,4 @@ error: JsonSchema_repr: missing #[repr(...)] attribute
3 | #[derive(JsonSchema_repr)]
| ^^^^^^^^^^^^^^^
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
= note: this error originates in the derive macro `JsonSchema_repr` (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -4,4 +4,4 @@ error: This argument to `schema_for!` is not a type - did you mean to use `schem
4 | let _schema = schema_for!(123);
| ^^^^^^^^^^^^^^^^
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
= note: this error originates in the macro `schema_for` (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -62,12 +62,14 @@ pub struct Struct2 {
min_max: f32,
#[schemars(range(min = "MIN", max = "MAX"))]
min_max2: f32,
#[validate(regex = "overridden")]
#[schemars(regex = "STARTS_WITH_HELLO")]
regex_str1: String,
#[schemars(regex(path = "STARTS_WITH_HELLO"))]
regex_str2: String,
#[schemars(regex(pattern = r"^\d+$"))]
regex_str3: String,
#[validate(regex = "overridden")]
#[schemars(contains = "substring...")]
contains_str1: String,
#[schemars(contains(pattern = "substring..."))]

View file

@ -1,23 +1,48 @@
use super::{get_lit_str, get_meta_items, parse_lit_into_path, parse_lit_str};
use proc_macro2::TokenStream;
use serde_derive_internals::Ctxt;
use syn::{Expr, ExprLit, ExprPath, Lit, Meta, MetaNameValue, NestedMeta};
use syn::{Expr, ExprLit, ExprPath, Lit, Meta, MetaNameValue, NestedMeta, Path};
pub(crate) static VALIDATION_KEYWORDS: &[&str] = &[
"range", "regex", "contains", "email", "phone", "url", "length", "required",
];
#[derive(Debug, Clone, Copy, PartialEq)]
enum Format {
Email,
Uri,
Phone,
}
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",
Format::Uri => "uri",
Format::Phone => "phone",
}
}
}
#[derive(Debug, Default)]
pub struct ValidationAttrs {
pub length_min: Option<Expr>,
pub length_max: Option<Expr>,
pub length_equal: Option<Expr>,
pub range_min: Option<Expr>,
pub range_max: Option<Expr>,
pub regex: Option<Expr>,
pub contains: Option<String>,
pub required: bool,
pub format: Option<&'static str>,
length_min: Option<Expr>,
length_max: Option<Expr>,
length_equal: Option<Expr>,
range_min: Option<Expr>,
range_max: Option<Expr>,
regex: Option<Expr>,
contains: Option<String>,
required: bool,
format: Option<Format>,
}
impl ValidationAttrs {
@ -27,6 +52,10 @@ impl ValidationAttrs {
.populate(attrs, "validate", true, errors)
}
pub fn required(&self) -> bool {
self.required
}
fn populate(
mut self,
attrs: &[syn::Attribute],
@ -34,23 +63,37 @@ impl ValidationAttrs {
ignore_errors: bool,
errors: &Ctxt,
) -> Self {
let duplicate_error = |meta: &MetaNameValue| {
let duplicate_error = |path: &Path| {
if !ignore_errors {
let msg = format!(
"duplicate schemars attribute `{}`",
meta.path.get_ident().unwrap()
path.get_ident().unwrap()
);
errors.error_spanned_by(meta, msg)
errors.error_spanned_by(path, msg)
}
};
let mutual_exclusive_error = |meta: &MetaNameValue, other: &str| {
let mutual_exclusive_error = |path: &Path, other: &str| {
if !ignore_errors {
let msg = format!(
"schemars attribute cannot contain both `{}` and `{}`",
meta.path.get_ident().unwrap(),
path.get_ident().unwrap(),
other,
);
errors.error_spanned_by(meta, msg)
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)
}
};
@ -65,29 +108,29 @@ impl ValidationAttrs {
match nested {
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => {
if self.length_min.is_some() {
duplicate_error(nv)
duplicate_error(&nv.path)
} else if self.length_equal.is_some() {
mutual_exclusive_error(nv, "equal")
mutual_exclusive_error(&nv.path, "equal")
} else {
self.length_min = str_or_num_to_expr(&errors, "min", &nv.lit);
}
}
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("max") => {
if self.length_max.is_some() {
duplicate_error(nv)
duplicate_error(&nv.path)
} else if self.length_equal.is_some() {
mutual_exclusive_error(nv, "equal")
mutual_exclusive_error(&nv.path, "equal")
} else {
self.length_max = str_or_num_to_expr(&errors, "max", &nv.lit);
}
}
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("equal") => {
if self.length_equal.is_some() {
duplicate_error(nv)
duplicate_error(&nv.path)
} else if self.length_min.is_some() {
mutual_exclusive_error(nv, "min")
mutual_exclusive_error(&nv.path, "min")
} else if self.length_max.is_some() {
mutual_exclusive_error(nv, "max")
mutual_exclusive_error(&nv.path, "max")
} else {
self.length_equal =
str_or_num_to_expr(&errors, "equal", &nv.lit);
@ -110,14 +153,14 @@ impl ValidationAttrs {
match nested {
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => {
if self.range_min.is_some() {
duplicate_error(nv)
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)
duplicate_error(&nv.path)
} else {
self.range_max = str_or_num_to_expr(&errors, "max", &nv.lit);
}
@ -140,53 +183,74 @@ impl ValidationAttrs {
self.required = true;
}
// TODO cause compile error if format is already Some
// FIXME #[validate(...)] overrides #[schemars(...)] - should be other way around!
NestedMeta::Meta(Meta::Path(m)) if m.is_ident("email") => {
self.format = Some("email");
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(m)) if m.is_ident("url") => {
self.format = Some("uri");
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(m)) if m.is_ident("phone") => {
self.format = Some("phone");
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),
}
}
// TODO cause compile error if regex/contains are specified more than once
// FIXME #[validate(...)] overrides #[schemars(...)] - should be other way around!
NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. }))
if path.is_ident("regex") =>
{
self.regex = parse_lit_into_expr_path(errors, attr_type, "regex", lit).ok()
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()
}
}
}
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("regex") => {
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,
format!("unknown item in schemars regex attribute"),
);
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,
format!("unknown item in schemars regex attribute"),
);
}
}
}
}
}
@ -196,27 +260,44 @@ impl ValidationAttrs {
NestedMeta::Meta(Meta::NameValue(MetaNameValue { path, lit, .. }))
if path.is_ident("contains") =>
{
self.contains = get_lit_str(errors, attr_type, "contains", lit)
.ok()
.map(|litstr| litstr.value())
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()
}
}
}
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("contains") => {
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,
format!("unknown item in schemars contains attribute"),
);
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,
format!(
"unknown item in schemars contains attribute"
),
);
}
}
}
}
}
@ -293,6 +374,7 @@ impl ValidationAttrs {
}
let format = self.format.as_ref().map(|f| {
let f = f.schema_str();
quote! {
schema_object.format = Some(#f.to_string());
}

View file

@ -61,7 +61,7 @@ fn expr_for_field(field: &Field, allow_ref: bool) -> TokenStream {
let span = field.original.span();
let gen = quote!(gen);
let mut schema_expr = if field.validation_attrs.required {
let mut schema_expr = if field.validation_attrs.required() {
quote_spanned! {span=>
<#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#gen)
}
@ -439,7 +439,7 @@ fn expr_for_struct(
let (ty, type_def) = type_for_field_schema(field);
let maybe_insert_required = match (&default, field.validation_attrs.required) {
let maybe_insert_required = match (&default, field.validation_attrs.required()) {
(Some(_), _) => TokenStream::new(),
(None, false) => {
quote! {
@ -461,7 +461,7 @@ fn expr_for_struct(
};
let gen = quote!(gen);
let mut schema_expr = if field.validation_attrs.required {
let mut schema_expr = if field.validation_attrs.required() {
quote_spanned! {ty.span()=>
<#ty as schemars::JsonSchema>::_schemars_private_non_optional_json_schema(#gen)
}
@ -489,7 +489,7 @@ fn expr_for_struct(
.map(|field| {
let (ty, type_def) = type_for_field_schema(field);
let required = field.validation_attrs.required;
let required = field.validation_attrs.required();
let args = quote!(gen, #required);
let mut schema_expr = quote_spanned! {ty.span()=>