Emit compilation errors for duplicate validation attributes
This commit is contained in:
parent
af69a8ea11
commit
605db3bba8
7 changed files with 230 additions and 90 deletions
|
@ -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() {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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..."))]
|
||||
|
|
|
@ -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,37 +183,56 @@ 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(m)) if m.is_ident("phone") => {
|
||||
self.format = Some("phone");
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
// 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") => {
|
||||
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, ..
|
||||
path,
|
||||
lit,
|
||||
..
|
||||
})) if path.is_ident("path") => {
|
||||
self.regex =
|
||||
parse_lit_into_expr_path(errors, attr_type, "path", lit).ok()
|
||||
parse_lit_into_expr_path(errors, attr_type, "path", lit)
|
||||
.ok()
|
||||
}
|
||||
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
|
||||
path, lit, ..
|
||||
path,
|
||||
lit,
|
||||
..
|
||||
})) if path.is_ident("pattern") => {
|
||||
self.regex = get_lit_str(errors, attr_type, "pattern", lit)
|
||||
.ok()
|
||||
|
@ -192,22 +254,37 @@ impl ValidationAttrs {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.ok()
|
||||
.map(|litstr| litstr.value())
|
||||
.ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, ..
|
||||
path,
|
||||
lit,
|
||||
..
|
||||
})) if path.is_ident("pattern") => {
|
||||
self.contains = get_lit_str(errors, attr_type, "contains", lit)
|
||||
self.contains =
|
||||
get_lit_str(errors, attr_type, "contains", lit)
|
||||
.ok()
|
||||
.map(|litstr| litstr.value())
|
||||
}
|
||||
|
@ -215,13 +292,17 @@ impl ValidationAttrs {
|
|||
if !ignore_errors {
|
||||
errors.error_spanned_by(
|
||||
meta,
|
||||
format!("unknown item in schemars contains attribute"),
|
||||
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());
|
||||
}
|
||||
|
|
|
@ -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()=>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue