Allow setting validation attributes via #[schemars(...)]

This commit is contained in:
Graham Esau 2021-04-18 22:17:53 +01:00
parent c013052f59
commit 7914593d89
17 changed files with 607 additions and 99 deletions

View file

@ -1,8 +1,11 @@
use super::parse_lit_str;
use super::{get_lit_str, get_meta_items, parse_lit_into_path, parse_lit_str};
use proc_macro2::TokenStream;
use syn::ExprLit;
use syn::NestedMeta;
use syn::{Expr, Lit, Meta, MetaNameValue};
use serde_derive_internals::Ctxt;
use syn::{Expr, ExprLit, ExprPath, Lit, Meta, MetaNameValue, NestedMeta};
pub(crate) static VALIDATION_KEYWORDS: &[&str] = &[
"range", "regex", "contains", "email", "phone", "url", "length", "required",
];
#[derive(Debug, Default)]
pub struct ValidationAttrs {
@ -18,16 +21,43 @@ pub struct ValidationAttrs {
}
impl ValidationAttrs {
pub fn new(attrs: &[syn::Attribute]) -> Self {
pub fn new(attrs: &[syn::Attribute], errors: &Ctxt) -> Self {
// TODO allow setting "validate" attributes through #[schemars(...)]
ValidationAttrs::default().populate(attrs)
ValidationAttrs::default()
.populate(attrs, "schemars", false, errors)
.populate(attrs, "validate", true, errors)
}
fn populate(mut self, attrs: &[syn::Attribute]) -> Self {
// TODO don't silently ignore unparseable attributes
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,
);
errors.error_spanned_by(meta, msg)
}
};
for meta_item in attrs
.iter()
.flat_map(|attr| get_meta_items(attr, "validate"))
.flat_map(|attr| get_meta_items(attr, attr_type, errors))
.flatten()
{
match &meta_item {
@ -35,15 +65,43 @@ impl ValidationAttrs {
for nested in meta_list.nested.iter() {
match nested {
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => {
self.length_min = str_or_num_to_expr(&nv.lit);
if self.length_min.is_some() {
duplicate_error(nv)
} else if self.length_equal.is_some() {
mutual_exclusive_error(nv, "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") => {
self.length_max = str_or_num_to_expr(&nv.lit);
if self.length_max.is_some() {
duplicate_error(nv)
} else if self.length_equal.is_some() {
mutual_exclusive_error(nv, "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") => {
self.length_equal = str_or_num_to_expr(&nv.lit);
if self.length_equal.is_some() {
duplicate_error(nv)
} else if self.length_min.is_some() {
mutual_exclusive_error(nv, "min")
} else if self.length_max.is_some() {
mutual_exclusive_error(nv, "max")
} else {
self.length_equal =
str_or_num_to_expr(&errors, "equal", &nv.lit);
}
}
meta => {
if !ignore_errors {
errors.error_spanned_by(
meta,
format!("unknown item in schemars length attribute"),
);
}
}
_ => {}
}
}
}
@ -52,78 +110,118 @@ impl ValidationAttrs {
for nested in meta_list.nested.iter() {
match nested {
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("min") => {
self.range_min = str_or_num_to_expr(&nv.lit);
if self.range_min.is_some() {
duplicate_error(nv)
} else {
self.range_min = str_or_num_to_expr(&errors, "min", &nv.lit);
}
}
NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("max") => {
self.range_max = str_or_num_to_expr(&nv.lit);
if self.range_max.is_some() {
duplicate_error(nv)
} else {
self.range_max = str_or_num_to_expr(&errors, "max", &nv.lit);
}
}
meta => {
if !ignore_errors {
errors.error_spanned_by(
meta,
format!("unknown item in schemars range attribute"),
);
}
}
_ => {}
}
}
}
NestedMeta::Meta(m)
if m.path().is_ident("required") || m.path().is_ident("required_nested") =>
NestedMeta::Meta(Meta::Path(m))
if m.is_ident("required") || m.is_ident("required_nested") =>
{
self.required = true;
}
NestedMeta::Meta(m) if m.path().is_ident("email") => {
// 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(m) if m.path().is_ident("url") => {
NestedMeta::Meta(Meta::Path(m)) if m.is_ident("url") => {
self.format = Some("uri");
}
NestedMeta::Meta(m) if m.path().is_ident("phone") => {
NestedMeta::Meta(Meta::Path(m)) if m.is_ident("phone") => {
self.format = Some("phone");
}
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
path,
lit: Lit::Str(regex),
..
})) if path.is_ident("regex") => {
self.regex = parse_lit_str::<syn::ExprPath>(regex).ok().map(Expr::Path)
// 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::List(meta_list)) if meta_list.path.is_ident("regex") => {
self.regex = meta_list.nested.iter().find_map(|x| match x {
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
path,
lit: Lit::Str(regex),
..
})) if path.is_ident("path") => {
parse_lit_str::<syn::ExprPath>(regex).ok().map(Expr::Path)
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"),
);
}
}
}
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
path,
lit: Lit::Str(regex),
..
})) if path.is_ident("pattern") => Some(Expr::Lit(syn::ExprLit {
attrs: Vec::new(),
lit: Lit::Str(regex.clone()),
})),
_ => None,
});
}
}
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
path,
lit: Lit::Str(contains),
..
})) if path.is_ident("contains") => self.contains = Some(contains.value()),
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())
}
NestedMeta::Meta(Meta::List(meta_list)) if meta_list.path.is_ident("contains") => {
self.contains = meta_list.nested.iter().find_map(|x| match x {
NestedMeta::Meta(Meta::NameValue(MetaNameValue {
path,
lit: Lit::Str(contains),
..
})) if path.is_ident("pattern") => Some(contains.value()),
_ => 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"),
);
}
}
}
}
}
_ => {}
@ -230,6 +328,21 @@ impl ValidationAttrs {
}
}
fn parse_lit_into_expr_path(
cx: &Ctxt,
attr_type: &'static str,
meta_item_name: &'static str,
lit: &syn::Lit,
) -> Result<Expr, ()> {
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<TokenStream>) -> Option<TokenStream> {
if v.is_empty() {
None
@ -283,27 +396,22 @@ fn wrap_string_validation(v: Vec<TokenStream>) -> Option<TokenStream> {
}
}
fn get_meta_items(
attr: &syn::Attribute,
attr_type: &'static str,
) -> Result<Vec<syn::NestedMeta>, ()> {
if !attr.path.is_ident(attr_type) {
return Ok(Vec::new());
}
match attr.parse_meta() {
Ok(Meta::List(meta)) => Ok(meta.nested.into_iter().collect()),
_ => Err(()),
}
}
fn str_or_num_to_expr(lit: &Lit) -> Option<Expr> {
fn str_or_num_to_expr(cx: &Ctxt, meta_item_name: &str, lit: &Lit) -> Option<Expr> {
match lit {
Lit::Str(s) => parse_lit_str::<syn::ExprPath>(s).ok().map(Expr::Path),
Lit::Str(s) => parse_lit_str::<ExprPath>(s).ok().map(Expr::Path),
Lit::Int(_) | Lit::Float(_) => Some(Expr::Lit(ExprLit {
attrs: Vec::new(),
lit: lit.clone(),
})),
_ => None,
_ => {
cx.error_spanned_by(
lit,
format!(
"expected `{}` to be a string or number literal",
meta_item_name
),
);
None
}
}
}