Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adapters #93

Merged
merged 2 commits into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading