Read #[garde(...)] attributes in addition to #[validate(...)] (#331)

This commit is contained in:
Graham Esau 2024-08-29 17:12:06 +01:00 committed by GitHub
parent 56cdd45c5a
commit 9770301218
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 421 additions and 87 deletions

View file

@ -210,9 +210,10 @@ impl FieldAttrs {
let schemars_cx = &mut AttrCtxt::new(cx, attrs, "schemars");
let serde_cx = &mut AttrCtxt::new(cx, attrs, "serde");
let validate_cx = &mut AttrCtxt::new(cx, attrs, "validate");
let garde_cx = &mut AttrCtxt::new(cx, attrs, "garde");
self.common.populate(attrs, schemars_cx, serde_cx);
self.validation.populate(schemars_cx, validate_cx);
self.validation.populate(schemars_cx, validate_cx, garde_cx);
self.process_attr(schemars_cx);
self.process_attr(serde_cx);
}
@ -277,6 +278,7 @@ impl ContainerAttrs {
None => self.crate_name = parse_name_value_lit_str(meta, cx).ok(),
},
// The actual parsing of `rename` is done by serde
"rename" => self.is_renamed = true,
_ => return Some(meta),

View file

@ -103,7 +103,7 @@ pub fn parse_extensions(
cx: &AttrCtxt,
) -> Result<impl IntoIterator<Item = Extension>, ()> {
let parser = Punctuated::<Extension, Token![,]>::parse_terminated;
parse_meta_list(meta, cx, parser)
parse_meta_list_with(&meta, cx, parser)
}
pub fn parse_length_or_range(outer_meta: Meta, cx: &AttrCtxt) -> Result<LengthOrRange, ()> {
@ -144,6 +144,10 @@ pub fn parse_length_or_range(outer_meta: Meta, cx: &AttrCtxt) -> Result<LengthOr
Ok(result)
}
pub fn parse_pattern(meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
parse_meta_list_with(&meta, cx, Expr::parse)
}
pub fn parse_schemars_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
let mut pattern = None;
@ -200,9 +204,47 @@ pub fn parse_validate_regex(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()>
}
pub fn parse_contains(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
#[derive(Debug)]
enum ContainsFormat {
Metas(Punctuated<Meta, Token![,]>),
Expr(Expr),
}
impl Parse for ContainsFormat {
fn parse(input: ParseStream) -> syn::Result<Self> {
// An imperfect but good-enough heuristic for determining whether it looks more like a
// comma-separated meta list (validator-style), or a single expression (garde-style).
// This heuristic may not generalise well-enough for attributes other than `contains`!
// `foo = bar` => Metas (not Expr::Assign)
// `foo, bar` => Metas
// `foo` => Expr (not Meta::Path)
// `foo(bar)` => Expr (not Meta::List)
if input.peek2(Token![,]) || input.peek2(Token![=]) {
Punctuated::parse_terminated(input).map(Self::Metas)
} else {
input.parse().map(Self::Expr)
}
}
}
let nested_meta_or_expr = match cx.attr_type {
"validate" => parse_meta_list_with(&outer_meta, cx, Punctuated::parse_terminated)
.map(ContainsFormat::Metas),
"garde" => parse_meta_list_with(&outer_meta, cx, Expr::parse).map(ContainsFormat::Expr),
"schemars" => parse_meta_list_with(&outer_meta, cx, ContainsFormat::parse),
wat => {
unreachable!("Unexpected attr type `{wat}` for `contains` item. This is a bug in schemars, please raise an issue!")
}
}?;
let nested_metas = match nested_meta_or_expr {
ContainsFormat::Expr(expr) => return Ok(expr),
ContainsFormat::Metas(m) => m,
};
let mut pattern = None;
for nested_meta in parse_nested_meta(outer_meta.clone(), cx)? {
for nested_meta in nested_metas {
match path_str(nested_meta.path()).as_str() {
"pattern" => match &pattern {
Some(_) => cx.duplicate_error(&nested_meta),
@ -229,10 +271,10 @@ pub fn parse_contains(outer_meta: Meta, cx: &AttrCtxt) -> Result<Expr, ()> {
pub fn parse_nested_meta(meta: Meta, cx: &AttrCtxt) -> Result<impl IntoIterator<Item = Meta>, ()> {
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
parse_meta_list(meta, cx, parser)
parse_meta_list_with(&meta, cx, parser)
}
fn parse_meta_list<F: Parser>(meta: Meta, cx: &AttrCtxt, parser: F) -> Result<F::Output, ()> {
fn parse_meta_list_with<F: Parser>(meta: &Meta, cx: &AttrCtxt, parser: F) -> Result<F::Output, ()> {
let Meta::List(meta_list) = meta else {
let name = path_str(meta.path());
cx.error_spanned_by(

View file

@ -5,8 +5,8 @@ use crate::idents::SCHEMA;
use super::{
parse_meta::{
parse_contains, parse_length_or_range, parse_nested_meta, parse_schemars_regex,
parse_validate_regex, require_path_only, LengthOrRange,
parse_contains, parse_length_or_range, parse_nested_meta, parse_pattern,
parse_schemars_regex, parse_validate_regex, require_path_only, LengthOrRange,
},
AttrCtxt,
};
@ -15,6 +15,9 @@ use super::{
pub enum Format {
Email,
Uri,
Ip,
Ipv4,
Ipv6,
}
impl Format {
@ -22,6 +25,9 @@ impl Format {
match self {
Format::Email => "email",
Format::Uri => "url",
Format::Ip => "ip",
Format::Ipv4 => "ipv4",
Format::Ipv6 => "ipv6",
}
}
@ -29,6 +35,9 @@ impl Format {
match self {
Format::Email => "email",
Format::Uri => "uri",
Format::Ip => "ip",
Format::Ipv4 => "ipv4",
Format::Ipv6 => "ipv6",
}
}
@ -36,6 +45,9 @@ impl Format {
Some(match s {
"email" => Format::Email,
"url" => Format::Uri,
"ip" => Format::Ip,
"ipv4" => Format::Ipv4,
"ipv6" => Format::Ipv6,
_ => return None,
})
}
@ -45,6 +57,7 @@ impl Format {
pub struct ValidationAttrs {
pub length: Option<LengthOrRange>,
pub range: Option<LengthOrRange>,
pub pattern: Option<Expr>,
pub regex: Option<Expr>,
pub contains: Option<Expr>,
pub required: bool,
@ -67,7 +80,7 @@ impl ValidationAttrs {
Self::add_length_or_range(range, mutators, "number", "imum", mut_ref_schema);
}
if let Some(regex) = &self.regex {
if let Some(regex) = self.regex.as_ref().or(self.pattern.as_ref()) {
mutators.push(quote! {
schemars::_private::insert_validation_property(#mut_ref_schema, "string", "pattern", (#regex).to_string());
});
@ -75,7 +88,7 @@ impl ValidationAttrs {
if let Some(contains) = &self.contains {
mutators.push(quote! {
schemars::_private::must_contain(#mut_ref_schema, #contains.to_string());
schemars::_private::must_contain(#mut_ref_schema, &#contains.to_string());
});
}
@ -120,9 +133,15 @@ impl ValidationAttrs {
}
}
pub(super) fn populate(&mut self, schemars_cx: &mut AttrCtxt, validate_cx: &mut AttrCtxt) {
pub(super) fn populate(
&mut self,
schemars_cx: &mut AttrCtxt,
validate_cx: &mut AttrCtxt,
garde_cx: &mut AttrCtxt,
) {
self.process_attr(schemars_cx);
self.process_attr(validate_cx);
self.process_attr(garde_cx);
}
fn process_attr(&mut self, cx: &mut AttrCtxt) {
@ -153,22 +172,36 @@ impl ValidationAttrs {
}
}
"regex" => match (&self.regex, &self.contains, cx.attr_type) {
(Some(_), _, _) => cx.duplicate_error(&meta),
(_, Some(_), _) => cx.mutual_exclusive_error(&meta, "contains"),
(None, None, "schemars") => self.regex = parse_schemars_regex(meta, cx).ok(),
(None, None, "validate") => self.regex = parse_validate_regex(meta, cx).ok(),
(None, None, wat) => {
unreachable!("Unexpected attr type `{wat}` for regex item. This is a bug in schemars, please raise an issue!")
"pattern" if cx.attr_type != "validate" => {
match (&self.pattern, &self.regex, &self.contains) {
(Some(_p), _, _) => cx.duplicate_error(&meta),
(_, Some(_r), _) => cx.mutual_exclusive_error(&meta, "regex"),
(_, _, Some(_c)) => cx.mutual_exclusive_error(&meta, "contains"),
(None, None, None) => self.pattern = parse_pattern(meta, cx).ok(),
}
},
"contains" => match (&self.regex, &self.contains) {
(Some(_), _) => cx.mutual_exclusive_error(&meta, "regex"),
(_, Some(_)) => cx.duplicate_error(&meta),
(None, None) => self.contains = parse_contains(meta, cx).ok(),
}
"regex" if cx.attr_type != "garde" => {
match (&self.pattern, &self.regex, &self.contains) {
(Some(_p), _, _) => cx.mutual_exclusive_error(&meta, "pattern"),
(_, Some(_r), _) => cx.duplicate_error(&meta),
(_, _, Some(_c)) => cx.mutual_exclusive_error(&meta, "contains"),
(None, None, None) => {
if cx.attr_type == "validate" {
self.regex = parse_validate_regex(meta, cx).ok()
} else {
self.regex = parse_schemars_regex(meta, cx).ok()
}
}
}
}
"contains" => match (&self.pattern, &self.regex, &self.contains) {
(Some(_p), _, _) => cx.mutual_exclusive_error(&meta, "pattern"),
(_, Some(_r), _) => cx.mutual_exclusive_error(&meta, "regex"),
(_, _, Some(_c)) => cx.duplicate_error(&meta),
(None, None, None) => self.contains = parse_contains(meta, cx).ok(),
},
"inner" => {
"inner" if cx.attr_type != "validate" => {
if let Ok(nested_meta) = parse_nested_meta(meta, cx) {
let inner = self
.inner

View file

@ -18,7 +18,7 @@ use syn::spanned::Spanned;
#[doc = "Derive macro for `JsonSchema` trait."]
#[cfg_attr(not(doctest), doc = include_str!("../deriving.md"), doc = include_str!("../attributes.md"))]
#[proc_macro_derive(JsonSchema, attributes(schemars, serde, validate))]
#[proc_macro_derive(JsonSchema, attributes(schemars, serde, validate, garde))]
pub fn derive_json_schema_wrapper(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput);
derive_json_schema(input, false)