Skip to content

Commit

Permalink
Merge pull request #93 from jprochazk/adapters
Browse files Browse the repository at this point in the history
Adapters
  • Loading branch information
jprochazk authored Jan 27, 2024
2 parents 291d042 + 240e54e commit 068ac5d
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 27 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
37 changes: 37 additions & 0 deletions garde/tests/rules/adapt.rs
Original file line number Diff line number Diff line change
@@ -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: "" }], &())
}
1 change: 1 addition & 0 deletions garde/tests/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod adapt;
mod allow_unvalidated;
mod alphanumeric;
mod ascii;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
source: garde/tests/./rules/adapt.rs
expression: snapshot
---
Test {
v: "",
}
v: my custom error message


20 changes: 11 additions & 9 deletions garde_derive/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ fn check_field(field: model::Field, options: &model::Options) -> syn::Result<mod

let mut field = model::ValidateField {
ty,
adapter: None,
skip: None,
alias: None,
message: None,
Expand Down Expand Up @@ -303,21 +304,21 @@ fn check_rule(
is_inner: bool,
) -> 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),
}
}};

Expand All @@ -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),
Expand Down
71 changes: 54 additions & 17 deletions garde_derive/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand All @@ -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);
Expand All @@ -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> {
Expand All @@ -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! {
Expand All @@ -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)
Expand Down Expand Up @@ -285,24 +303,24 @@ 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,)
}),
},
};

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);
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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"),
Expand Down
4 changes: 3 additions & 1 deletion garde_derive/src/model.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -74,6 +74,7 @@ pub struct RawRule {

pub enum RawRuleKind {
Skip,
Adapt(Path),
Rename(Str),
Message(Message),
Code(Str),
Expand Down Expand Up @@ -173,6 +174,7 @@ pub enum ValidateKind {
pub struct ValidateField {
pub ty: Type,

pub adapter: Option<Path>,
pub skip: Option<Span>,
pub alias: Option<String>,
pub message: Option<Message>,
Expand Down
1 change: 1 addition & 0 deletions garde_derive/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit 068ac5d

Please sign in to comment.