From 31f3e9a9d5fefcd92ac19d31324f921e2917ea04 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Sat, 27 Jan 2024 18:45:01 +0100 Subject: [PATCH 1/2] implement `#[garde(adapt)]` --- garde/tests/rules/adapt.rs | 37 ++++++++++ garde/tests/rules/mod.rs | 1 + ...s__rules__adapt__alphanumeric_invalid.snap | 10 +++ garde_derive/src/check.rs | 20 +++--- garde_derive/src/emit.rs | 71 ++++++++++++++----- garde_derive/src/model.rs | 4 +- garde_derive/src/syntax.rs | 1 + 7 files changed, 117 insertions(+), 27 deletions(-) create mode 100644 garde/tests/rules/adapt.rs create mode 100644 garde/tests/rules/snapshots/rules__rules__adapt__alphanumeric_invalid.snap diff --git a/garde/tests/rules/adapt.rs b/garde/tests/rules/adapt.rs new file mode 100644 index 0000000..1a527b8 --- /dev/null +++ b/garde/tests/rules/adapt.rs @@ -0,0 +1,37 @@ +use super::util; + +mod test_adapter { + #![allow(unused_imports)] + + pub use garde::rules::*; + + pub mod length { + pub use garde::rules::length::*; + + pub mod simple { + pub fn apply(v: &str, (min, max): (usize, usize)) -> garde::Result { + if !(min..=max).contains(&v.len()) { + Err(garde::Error::new("my custom error message")) + } else { + Ok(()) + } + } + } + } +} + +#[derive(Debug, garde::Validate)] +struct Test<'a> { + #[garde(adapt(test_adapter), length(min = 1))] + v: &'a str, +} + +#[test] +fn alphanumeric_valid() { + util::check_ok(&[Test { v: "test" }], &()) +} + +#[test] +fn alphanumeric_invalid() { + util::check_fail!(&[Test { v: "" }], &()) +} diff --git a/garde/tests/rules/mod.rs b/garde/tests/rules/mod.rs index fb57932..1f8e6b7 100644 --- a/garde/tests/rules/mod.rs +++ b/garde/tests/rules/mod.rs @@ -1,3 +1,4 @@ +mod adapt; mod allow_unvalidated; mod alphanumeric; mod ascii; diff --git a/garde/tests/rules/snapshots/rules__rules__adapt__alphanumeric_invalid.snap b/garde/tests/rules/snapshots/rules__rules__adapt__alphanumeric_invalid.snap new file mode 100644 index 0000000..b1ff353 --- /dev/null +++ b/garde/tests/rules/snapshots/rules__rules__adapt__alphanumeric_invalid.snap @@ -0,0 +1,10 @@ +--- +source: garde/tests/./rules/adapt.rs +expression: snapshot +--- +Test { + v: "", +} +v: my custom error message + + diff --git a/garde_derive/src/check.rs b/garde_derive/src/check.rs index cd417c4..a91a647 100644 --- a/garde_derive/src/check.rs +++ b/garde_derive/src/check.rs @@ -227,6 +227,7 @@ fn check_field(field: model::Field, options: &model::Options) -> syn::Result syn::Result<()> { macro_rules! apply { - ($is_inner:expr, $field:ident, $name:ident, $value:expr, $span:expr) => {{ - if $is_inner { + ($name:ident = $value:expr, $span:expr) => {{ + if is_inner { return Err(syn::Error::new( $span, concat!("rule `", stringify!($name), "` may not be used in `inner`") )); } - match $field.$name { + match field.$name { Some(_) => { return Err(syn::Error::new( $span, concat!("duplicate rule `", stringify!($name), "`"), )) } - None => $field.$name = Some($value), + None => field.$name = Some($value), } }}; @@ -333,11 +334,12 @@ fn check_rule( let span = raw_rule.span; use model::RawRuleKind::*; match raw_rule.kind { - Skip => apply!(is_inner, field, skip, span, span), - Rename(alias) => apply!(is_inner, field, alias, alias.value, span), - Message(message) => apply!(is_inner, field, message, message, span), - Code(code) => apply!(is_inner, field, code, code.value, span), - Dive => apply!(is_inner, field, dive, span, span), + Skip => apply!(skip = span, span), + Adapt(path) => apply!(adapter = path, span), + Rename(alias) => apply!(alias = alias.value, span), + Message(message) => apply!(message = message, span), + Code(code) => apply!(code = code.value, span), + Dive => apply!(dive = span, span), Custom(custom) => rule_set.custom_rules.push(custom), Required => apply!(Required(), span), Ascii => apply!(Ascii(), span), diff --git a/garde_derive/src/emit.rs b/garde_derive/src/emit.rs index e0c38dd..91d3a45 100644 --- a/garde_derive/src/emit.rs +++ b/garde_derive/src/emit.rs @@ -168,20 +168,32 @@ impl<'a> ToTokens for Tuple<'a> { } } -struct Inner<'a>(&'a model::RuleSet); +struct Inner<'a> { + rules_mod: &'a TokenStream2, + rule_set: &'a model::RuleSet, +} impl<'a> ToTokens for Inner<'a> { fn to_tokens(&self, tokens: &mut TokenStream2) { - let Inner(rule_set) = self; + let Inner { + rules_mod, + rule_set, + } = self; let outer = match rule_set.has_top_level_rules() { true => { - let rules = Rules(rule_set); + let rules = Rules { + rules_mod, + rule_set, + }; Some(quote! {#rules}) } false => None, }; - let inner = rule_set.inner.as_deref().map(Inner); + 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! { @@ -196,7 +208,7 @@ impl<'a> ToTokens for Inner<'a> { }; quote! { - ::garde::rules::inner::apply( + #rules_mod::inner::apply( &*__garde_binding, |__garde_binding, __garde_inner_key| { let mut __garde_path = ::garde::util::nested_path!(__garde_path, __garde_inner_key); @@ -208,7 +220,10 @@ impl<'a> ToTokens for Inner<'a> { } } -struct Rules<'a>(&'a model::RuleSet); +struct Rules<'a> { + rules_mod: &'a TokenStream2, + rule_set: &'a model::RuleSet, +} #[derive(Clone, Copy)] enum Binding<'a> { @@ -227,7 +242,10 @@ impl<'a> ToTokens for Binding<'a> { impl<'a> ToTokens for Rules<'a> { fn to_tokens(&self, tokens: &mut TokenStream2) { - let Rules(rule_set) = self; + let Rules { + rules_mod, + rule_set, + } = self; for custom_rule in rule_set.custom_rules.iter() { quote! { @@ -246,13 +264,13 @@ impl<'a> ToTokens for Rules<'a> { quote!(()) } Ip => { - quote!((::garde::rules::ip::IpKind::Any,)) + quote!((#rules_mod::ip::IpKind::Any,)) } IpV4 => { - quote!((::garde::rules::ip::IpKind::V4,)) + quote!((#rules_mod::ip::IpKind::V4,)) } IpV6 => { - quote!((::garde::rules::ip::IpKind::V6,)) + quote!((#rules_mod::ip::IpKind::V6,)) } LengthSimple(range) | LengthBytes(range) @@ -285,16 +303,16 @@ impl<'a> ToTokens for Rules<'a> { target_arch = "wasm32", target_os = "unknown" )))] - static PATTERN: ::garde::rules::pattern::regex::StaticPattern = - ::garde::rules::pattern::regex::init_pattern!(#s); + static PATTERN: #rules_mod::pattern::regex::StaticPattern = + #rules_mod::pattern::regex::init_pattern!(#s); #[cfg(all( feature = "js-sys", target_arch = "wasm32", target_os = "unknown" ))] - static PATTERN: ::garde::rules::pattern::regex_js_sys::StaticPattern = - ::garde::rules::pattern::regex_js_sys::init_pattern!(#s); + static PATTERN: #rules_mod::pattern::regex_js_sys::StaticPattern = + #rules_mod::pattern::regex_js_sys::init_pattern!(#s); (&PATTERN,) }), @@ -302,7 +320,7 @@ impl<'a> ToTokens for Rules<'a> { }; quote! { - if let Err(__garde_error) = (::garde::rules::#name::apply)(&*__garde_binding, #args) { + if let Err(__garde_error) = (#rules_mod::#name::apply)(&*__garde_binding, #args) { __garde_report.append(__garde_path(), __garde_error); } } @@ -330,8 +348,21 @@ where None => return, }; let fields = fields.filter(|(_, field, _)| field.skip.is_none()); + let default_rules_mod = quote!(::garde::rules); for (binding, field, extra) in fields { - let rules = Rules(&field.rule_set); + let field_adapter = field + .adapter + .as_ref() + .map(|p| p.to_token_stream()) + .unwrap_or_default(); + let rules_mod = match field.adapter.as_ref() { + Some(_) => &field_adapter, + None => &default_rules_mod, + }; + let rules = Rules { + rules_mod, + rule_set: &field.rule_set, + }; let outer = match field.has_top_level_rules() { true => Some(quote! {{#rules}}), false => None, @@ -345,7 +376,13 @@ where __garde_report, ); }), - (None, Some(inner)) => Some(Inner(inner).to_token_stream()), + (None, Some(inner)) => Some( + Inner { + rules_mod, + rule_set: inner, + } + .to_token_stream(), + ), (None, None) => None, // TODO: encode this via the type system instead? _ => unreachable!("`dive` and `inner` are mutually exclusive"), diff --git a/garde_derive/src/model.rs b/garde_derive/src/model.rs index 565f7ad..dc8f704 100644 --- a/garde_derive/src/model.rs +++ b/garde_derive/src/model.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use proc_macro2::{Ident, Span}; -use syn::{Expr, Generics, Type}; +use syn::{Expr, Generics, Path, Type}; pub struct Input { pub ident: Ident, @@ -74,6 +74,7 @@ pub struct RawRule { pub enum RawRuleKind { Skip, + Adapt(Path), Rename(Str), Message(Message), Code(Str), @@ -173,6 +174,7 @@ pub enum ValidateKind { pub struct ValidateField { pub ty: Type, + pub adapter: Option, pub skip: Option, pub alias: Option, pub message: Option, diff --git a/garde_derive/src/syntax.rs b/garde_derive/src/syntax.rs index 61b6822..4ce1d4a 100644 --- a/garde_derive/src/syntax.rs +++ b/garde_derive/src/syntax.rs @@ -274,6 +274,7 @@ impl Parse for model::RawRule { rules! { (input, ident) { "skip" => Skip, + "adapt" => Adapt(content), "rename" => Rename(content), "message" => Message(content), "code" => Code(content), From 240e54eadb29e6e0ba37a1d329799d0df5b74913 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Sat, 27 Jan 2024 18:56:02 +0100 Subject: [PATCH 2/2] add docs --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/README.md b/README.md index 80603b8..e3ac018 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ A Rust validation library - [Context/Self access](#contextself-access) - [Implementing rules](#implementing-rules) - [Implementing `Validate`](#implementing-validate) +- [Rule adapters](#rule-adapters) - [Integration with web frameworks](#integration-with-web-frameworks) - [Feature flags](#feature-flags) - [Why `garde`?](#why-garde) @@ -377,6 +378,55 @@ struct Bar { } ``` +### Rule adapters + +Adapters allow you to implement validation for third-party types without using a newtype. + +An adapter may look like this: +```rust +mod my_str_adapter { + #![allow(unused_imports)] + pub use garde::rules::*; // re-export garde's rules + + pub mod length { + pub use garde::rules::length::*; // re-export `length` rules + + pub mod simple { + // re-implement `simple`, but _only_ for the concrete type &str! + pub fn apply(v: &str, (min, max): (usize, usize)) -> garde::Result { + if !(min..=max).contains(&v.len()) { + Err(garde::Error::new("my custom error message")) + } else { + Ok(()) + } + } + } + } +} +``` + +You create a module, add a public glob re-export of `garde::rules` inside of it, +and then re-implement the specific rule you're interested in. This is a form of +[duck typing](https://en.wikipedia.org/wiki/Duck_typing). Any rule which you have +not re-implemented is simply delegated to `garde`'s impl. + +It's quite verbose, but in exchange it is maximally flexible. To use the adapter, +add an `adapt` attribute to a field: +```rust,ignore +#[derive(garde::Validate)] +struct Stuff<'a> { + #[garde( + adapt(my_str_adapter), + length(min = 1), + ascii, + )] + v: &'a str, +} +``` + +The `length` rule will now use your custom implementation, but the `ascii` rule +will continue to use `garde`'s implementation. + ### Integration with web frameworks - [`axum`](https://crates.io/crates/axum): [`axum_garde`](https://crates.io/crates/axum_garde)