diff --git a/garde/src/rules/inner.rs b/garde/src/rules/inner.rs index 00fbed6..1d35c9c 100644 --- a/garde/src/rules/inner.rs +++ b/garde/src/rules/inner.rs @@ -28,50 +28,87 @@ pub trait Inner { F: FnMut(&T, &Self::Key); } -impl Inner for Vec { - type Key = usize; +impl Inner for Option { + type Key = NoKey; - fn validate_inner(&self, f: F) + fn validate_inner(&self, mut f: F) where F: FnMut(&T, &Self::Key), { - self.as_slice().validate_inner(f) + if let Some(item) = self { + f(item, &NoKey::default()) + } } } -impl Inner for [T; N] { - type Key = usize; +macro_rules! impl_via_iter { + (in $T:ty) => { + impl Inner<$V> for $T { + type Key = usize; - fn validate_inner(&self, f: F) - where - F: FnMut(&T, &Self::Key), - { - self.as_slice().validate_inner(f) - } + fn validate_inner(&self, mut f: F) + where + F: FnMut(&$V, &Self::Key), + { + for (index, item) in self.iter().enumerate() { + f(item, &index); + } + } + } + }; + (in<$($lifetime:lifetime,)? $V:ident $(, $S:ident)?> $T:ty) => { + impl<$($lifetime,)? $V $(, $S)?> Inner<$V> for $T { + type Key = usize; + + fn validate_inner(&self, mut f: F) + where + F: FnMut(&$V, &Self::Key), + { + for (index, item) in self.iter().enumerate() { + f(item, &index); + } + } + } + }; } -impl<'a, T> Inner for &'a [T] { - type Key = usize; +impl_via_iter!(in<'a, T> &'a [T]); +impl_via_iter!(in [T; N]); +impl_via_iter!(in Vec); +impl_via_iter!(in std::collections::VecDeque); +impl_via_iter!(in std::collections::BinaryHeap); +impl_via_iter!(in std::collections::LinkedList); +impl_via_iter!(in std::collections::HashSet); +impl_via_iter!(in std::collections::BTreeSet); + +impl Inner for std::collections::HashMap +where + K: PathComponentKind, +{ + type Key = K; fn validate_inner(&self, mut f: F) where - F: FnMut(&T, &Self::Key), + F: FnMut(&V, &Self::Key), { - for (index, item) in self.iter().enumerate() { - f(item, &index); + for (key, value) in self.iter() { + f(value, key) } } } -impl Inner for Option { - type Key = NoKey; +impl Inner for std::collections::BTreeMap +where + K: PathComponentKind, +{ + type Key = K; fn validate_inner(&self, mut f: F) where - F: FnMut(&T, &Self::Key), + F: FnMut(&V, &Self::Key), { - if let Some(item) = self { - f(item, &NoKey::default()) + for (key, value) in self.iter() { + f(value, key) } } } diff --git a/garde/src/rules/keys.rs b/garde/src/rules/keys.rs new file mode 100644 index 0000000..0ddf894 --- /dev/null +++ b/garde/src/rules/keys.rs @@ -0,0 +1,56 @@ +//! Key validation. +//! +//! ```rust +//! #[derive(garde::Validate)] +//! struct Test { +//! #[garde(keys(length(min=1)))] +//! v: HashMap, +//! } +//! ``` +//! +//! The entrypoint is the [`Keys`] trait. Implementing this trait for a type allows that type to be used with the `#[garde(keys(..))]` rule. + +use crate::error::PathComponentKind; + +pub fn apply(field: &T, f: F) +where + T: Keys, + K: PathComponentKind, + F: FnMut(&K), +{ + field.validate_keys(f) +} + +pub trait Keys { + fn validate_keys(&self, f: F) + where + F: FnMut(&K); +} + +impl Keys for std::collections::HashMap +where + K: PathComponentKind, +{ + fn validate_keys(&self, mut f: F) + where + F: FnMut(&K), + { + for key in self.keys() { + f(key) + } + } +} + +impl Keys for std::collections::BTreeMap +where + K: PathComponentKind, +{ + fn validate_keys(&self, mut f: F) + where + F: FnMut(&K), + { + for key in self.keys() { + f(key) + } + } +} diff --git a/garde/src/rules/length/bytes.rs b/garde/src/rules/length/bytes.rs index 543db5b..402f8dd 100644 --- a/garde/src/rules/length/bytes.rs +++ b/garde/src/rules/length/bytes.rs @@ -42,7 +42,6 @@ macro_rules! impl_via_len { } impl_via_len!(std::string::String); -impl_via_len!(in<'a> &'a std::string::String); impl_via_len!(in<'a> &'a str); impl_via_len!(in<'a> std::borrow::Cow<'a, str>); impl_via_len!(std::rc::Rc); diff --git a/garde/src/rules/length/chars.rs b/garde/src/rules/length/chars.rs index b479366..f5d8793 100644 --- a/garde/src/rules/length/chars.rs +++ b/garde/src/rules/length/chars.rs @@ -42,7 +42,6 @@ macro_rules! impl_via_chars { } impl_via_chars!(std::string::String); -impl_via_chars!(in<'a> &'a std::string::String); impl_via_chars!(in<'a> &'a str); impl_via_chars!(in<'a> std::borrow::Cow<'a, str>); impl_via_chars!(std::rc::Rc); diff --git a/garde/src/rules/length/graphemes.rs b/garde/src/rules/length/graphemes.rs index 28cc839..d1a2bd1 100644 --- a/garde/src/rules/length/graphemes.rs +++ b/garde/src/rules/length/graphemes.rs @@ -44,7 +44,6 @@ macro_rules! impl_str { } impl_str!(std::string::String); -impl_str!(in<'a> &'a std::string::String); impl_str!(in<'a> &'a str); impl_str!(in<'a> std::borrow::Cow<'a, str>); impl_str!(std::rc::Rc); diff --git a/garde/src/rules/length/simple.rs b/garde/src/rules/length/simple.rs index 631cb83..f8f6bb5 100644 --- a/garde/src/rules/length/simple.rs +++ b/garde/src/rules/length/simple.rs @@ -44,7 +44,6 @@ macro_rules! impl_via_bytes { } impl_via_bytes!(std::string::String); -impl_via_bytes!(in<'a> &'a std::string::String); impl_via_bytes!(in<'a> &'a str); impl_via_bytes!(in<'a> std::borrow::Cow<'a, str>); impl_via_bytes!(std::rc::Rc); @@ -83,7 +82,6 @@ macro_rules! impl_via_len { } impl_via_len!(in Vec); -impl_via_len!(in<'a, T> &'a Vec); impl_via_len!(in<'a, T> &'a [T]); impl Simple for [T; N] { @@ -105,10 +103,3 @@ impl_via_len!(in std::collections::BTreeSet); impl_via_len!(in std::collections::VecDeque); impl_via_len!(in std::collections::BinaryHeap); impl_via_len!(in std::collections::LinkedList); -impl_via_len!(in<'a, K, V, S> &'a std::collections::HashMap); -impl_via_len!(in<'a, T, S> &'a std::collections::HashSet); -impl_via_len!(in<'a, K, V> &'a std::collections::BTreeMap); -impl_via_len!(in<'a, T> &'a std::collections::BTreeSet); -impl_via_len!(in<'a, T> &'a std::collections::VecDeque); -impl_via_len!(in<'a, T> &'a std::collections::BinaryHeap); -impl_via_len!(in<'a, T> &'a std::collections::LinkedList); diff --git a/garde/src/rules/length/utf16.rs b/garde/src/rules/length/utf16.rs index 1d5a225..7b3dd3b 100644 --- a/garde/src/rules/length/utf16.rs +++ b/garde/src/rules/length/utf16.rs @@ -40,7 +40,6 @@ macro_rules! impl_str { } impl_str!(std::string::String); -impl_str!(in<'a> &'a std::string::String); impl_str!(in<'a> &'a str); impl_str!(in<'a> std::borrow::Cow<'a, str>); impl_str!(std::rc::Rc); diff --git a/garde/src/rules/mod.rs b/garde/src/rules/mod.rs index a07d979..19d5df2 100644 --- a/garde/src/rules/mod.rs +++ b/garde/src/rules/mod.rs @@ -9,6 +9,7 @@ pub mod credit_card; pub mod email; pub mod inner; pub mod ip; +pub mod keys; pub mod length; pub mod pattern; #[cfg(feature = "phone-number")] diff --git a/garde/tests/rules/keys.rs b/garde/tests/rules/keys.rs new file mode 100644 index 0000000..906b017 --- /dev/null +++ b/garde/tests/rules/keys.rs @@ -0,0 +1,9 @@ +use std::collections::HashMap; + +use super::util; + +#[derive(Debug, garde::Validate)] +struct Key<'a> { + #[garde(inner(length(min = 1)), keys(length(min = 1)))] + inner: HashMap<&'a str, &'a str>, +} diff --git a/garde/tests/rules/mod.rs b/garde/tests/rules/mod.rs index 1f8e6b7..3461ece 100644 --- a/garde/tests/rules/mod.rs +++ b/garde/tests/rules/mod.rs @@ -10,6 +10,7 @@ mod dive_with_rules; mod email; mod inner; mod ip; +mod keys; mod length; mod multi_rule; mod newtype; diff --git a/garde_derive/src/check.rs b/garde_derive/src/check.rs index a91a647..be7237d 100644 --- a/garde_derive/src/check.rs +++ b/garde_derive/src/check.rs @@ -367,11 +367,10 @@ fn check_rule( Suffix(v) => apply!(Suffix(v), span), Pattern(v) => apply!(Pattern(check_regex(v)?), span), Inner(v) => { + let mut error = None; if rule_set.inner.is_none() { rule_set.inner = Some(Box::new(model::RuleSet::empty())); } - - let mut error = None; for raw_rule in v.contents { if let Err(e) = check_rule(field, raw_rule, rule_set.inner.as_mut().unwrap(), true) { @@ -382,6 +381,20 @@ fn check_rule( return Err(error); } } + Keys(v) => { + let mut error = None; + if rule_set.keys.is_none() { + rule_set.keys = Some(Box::new(model::RuleSet::empty())); + } + for raw_rule in v.contents { + if let Err(e) = check_rule(field, raw_rule, rule_set.keys.as_mut().unwrap(), true) { + error.maybe_fold(e); + } + } + if let Some(error) = error { + return Err(error); + } + } }; Ok(()) diff --git a/garde_derive/src/emit.rs b/garde_derive/src/emit.rs index 91d3a45..6b4508f 100644 --- a/garde_derive/src/emit.rs +++ b/garde_derive/src/emit.rs @@ -180,6 +180,14 @@ impl<'a> ToTokens for Inner<'a> { rule_set, } = self; + let key = rule_set.keys.as_deref().map(|rule_set| Keys { + rules_mod, + rule_set, + }); + let inner = rule_set.inner.as_deref().map(|rule_set| Inner { + rules_mod, + rule_set, + }); let outer = match rule_set.has_top_level_rules() { true => { let rules = Rules { @@ -190,29 +198,61 @@ impl<'a> ToTokens for Inner<'a> { } false => None, }; + + quote! { + #rules_mod::inner::apply( + &*__garde_binding, + |__garde_binding, __garde_inner_key| { + let mut __garde_path = ::garde::util::nested_path!(__garde_path, __garde_inner_key); + #key + #inner + #outer + } + ); + } + .to_tokens(tokens) + } +} + +struct Keys<'a> { + rules_mod: &'a TokenStream2, + rule_set: &'a model::RuleSet, +} + +impl<'a> ToTokens for Keys<'a> { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let Keys { + rules_mod, + rule_set, + } = self; + + let keys = rule_set.keys.as_deref().map(|rule_set| Keys { + rules_mod, + rule_set, + }); let inner = rule_set.inner.as_deref().map(|rule_set| Inner { rules_mod, rule_set, }); - - let value = match (outer, inner) { - (Some(outer), Some(inner)) => quote! { - #outer - #inner - }, - (None, Some(inner)) => quote! { - #inner - }, - (Some(outer), None) => outer, - (None, None) => return, + let outer = match rule_set.has_top_level_rules() { + true => { + let rules = Rules { + rules_mod, + rule_set, + }; + Some(quote! {#rules}) + } + false => None, }; quote! { - #rules_mod::inner::apply( + #rules_mod::keys::apply( &*__garde_binding, - |__garde_binding, __garde_inner_key| { - let mut __garde_path = ::garde::util::nested_path!(__garde_path, __garde_inner_key); - #value + |__garde_binding| { + let mut __garde_path = ::garde::util::nested_path!(__garde_path, __garde_binding); + #keys + #inner + #outer } ); } @@ -363,10 +403,10 @@ where rules_mod, rule_set: &field.rule_set, }; - let outer = match field.has_top_level_rules() { - true => Some(quote! {{#rules}}), - false => None, - }; + let keys = field.rule_set.keys.as_ref().map(|rule_set| Keys { + rules_mod, + rule_set, + }); let inner = match (&field.dive, &field.rule_set.inner) { (Some(..), None) => Some(quote! { ::garde::validate::Validate::validate_into( @@ -387,22 +427,16 @@ where // TODO: encode this via the type system instead? _ => unreachable!("`dive` and `inner` are mutually exclusive"), }; + let outer = match field.has_top_level_rules() { + true => Some(quote! {{#rules}}), + false => None, + }; - let value = match (outer, inner) { - (Some(outer), Some(inner)) => quote! { - let __garde_binding = &*#binding; - #inner - #outer - }, - (None, Some(inner)) => quote! { - let __garde_binding = &*#binding; - #inner - }, - (Some(outer), None) => quote! { - let __garde_binding = &*#binding; - #outer - }, - (None, None) => unreachable!("field should already be skipped"), + let value = quote! { + let __garde_binding = &*#binding; + #keys + #inner + #outer }; let add = &self.1; diff --git a/garde_derive/src/model.rs b/garde_derive/src/model.rs index dc8f704..fe2634c 100644 --- a/garde_derive/src/model.rs +++ b/garde_derive/src/model.rs @@ -97,6 +97,7 @@ pub enum RawRuleKind { Pattern(Pattern), Custom(Expr), Inner(List), + Keys(List), } pub struct RawLength { @@ -198,6 +199,7 @@ pub struct RuleSet { pub rules: BTreeSet, pub custom_rules: Vec, pub inner: Option>, + pub keys: Option>, } impl RuleSet { @@ -206,6 +208,7 @@ impl RuleSet { rules: BTreeSet::new(), custom_rules: Vec::new(), inner: None, + keys: None, } } diff --git a/garde_derive/src/syntax.rs b/garde_derive/src/syntax.rs index 4ce1d4a..bc9bc3f 100644 --- a/garde_derive/src/syntax.rs +++ b/garde_derive/src/syntax.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use proc_macro2::{Ident, Span}; +use proc_macro2::{Ident, Span, TokenStream}; use syn::ext::IdentExt; use syn::parse::Parse; use syn::punctuated::Punctuated; @@ -250,54 +250,58 @@ impl Parse for model::RawRule { let ident = Ident::parse_any(input)?; macro_rules! rules { - (($input:ident, $ident:ident) { - $($name:literal => $rule:ident $(($content:ident))?,)* - }) => { - match $ident.to_string().as_str() { + ($($name:literal => $rule:ident $(($content:ident))?,)*) => { + match ident.to_string().as_str() { $( $name => { $( let $content; - syn::parenthesized!($content in $input); + syn::parenthesized!($content in input); )? Ok(model::RawRule { - span: $ident.span(), + span: ident.span(), kind: model::RawRuleKind::$rule $(($content.parse()?))? }) } )* - _ => Err(syn::Error::new($ident.span(), "unrecognized validation rule")), + _ => { + if input.peek(syn::token::Paren) { + let _content; + syn::parenthesized!(_content in input); + let _ = _content.parse::()?; + } + Err(syn::Error::new(ident.span(), "unrecognized validation rule")) + }, } }; } rules! { - (input, ident) { - "skip" => Skip, - "adapt" => Adapt(content), - "rename" => Rename(content), - "message" => Message(content), - "code" => Code(content), - "dive" => Dive, - "required" => Required, - "ascii" => Ascii, - "alphanumeric" => Alphanumeric, - "email" => Email, - "url" => Url, - "ip" => Ip, - "ipv4" => IpV4, - "ipv6" => IpV6, - "credit_card" => CreditCard, - "phone_number" => PhoneNumber, - "length" => Length(content), - "range" => Range(content), - "contains" => Contains(content), - "prefix" => Prefix(content), - "suffix" => Suffix(content), - "pattern" => Pattern(content), - "custom" => Custom(content), - "inner" => Inner(content), - } + "skip" => Skip, + "adapt" => Adapt(content), + "rename" => Rename(content), + "message" => Message(content), + "code" => Code(content), + "dive" => Dive, + "required" => Required, + "ascii" => Ascii, + "alphanumeric" => Alphanumeric, + "email" => Email, + "url" => Url, + "ip" => Ip, + "ipv4" => IpV4, + "ipv6" => IpV6, + "credit_card" => CreditCard, + "phone_number" => PhoneNumber, + "length" => Length(content), + "range" => Range(content), + "contains" => Contains(content), + "prefix" => Prefix(content), + "suffix" => Suffix(content), + "pattern" => Pattern(content), + "custom" => Custom(content), + "inner" => Inner(content), + "keys" => Keys(content), } } }