garde
garde copied to clipboard
Struct-level custom rule
As far as I understand, it's not possible to derive the validation trait and define a validator at struct level at the same time (for example, to check that different fields are consistent with each other).
You can implement the trait yourself, but then you cannot take advantage of the derive feature if you have other validations in that struct.
It would be nice to have a feature like the one provided by the validator crate (see struct level validation).
for example, to check that different fields are consistent with each other
This would be possible with https://github.com/jprochazk/garde/issues/2
Is there a different use-case for container-level rules besides field matching?
Only other thing I can think of is mutually-exclusive Option fields:
struct Test {
// at least one of `foo`/`bar` must be `Some`
// but not both at the same time
foo: Option<u32>,
bar: Option<u32>,
}
I'm thinking of a more general approach.
In my use case I have two amounts and I need to ensure that one is lower than the other, for example:
struct Test {
principal: i32,
// deposit should be in range 0..principal
deposit: i32,
...
}
So this particular case could also be supported by the range rule if it could refer to other fields in the struct.
Your use-case is already supported, although completely by accident:
#[derive(garde::Validate)]
#[garde(allow_unvalidated)]
struct Test {
principal: i32,
#[garde(range(min=0, max=self.principal))]
deposit: i32,
}
Derive macro expansion
impl ::garde::Validate for Foo {
type Context = ();
#[allow(clippy::needless_borrow)]
fn validate(
&self,
__garde_user_ctx: &Self::Context,
) -> ::core::result::Result<(), ::garde::error::Errors> {
({
let Self { deposit, .. } = self;
::garde::error::Errors::fields(|__garde_errors| {
__garde_errors.insert("deposit", {
let __garde_binding = &*deposit;
::garde::error::Errors::simple(|__garde_errors| {
if let Err(__garde_error) = (::garde::rules::range::apply)(
&*__garde_binding,
(Some(0), Some(self.principal)),
) {
__garde_errors.push(__garde_error)
}
})
});
})
})
.finish()
}
}
You can expect this to continue to work, even if it's not "advertised". I added a test for it specifically here: https://github.com/jprochazk/garde/commit/dfa1fed3e72ec41ac6da83f7953b931ffa296eb8
I think I'm generally against adding container-level validation if the use-cases can be served by more specific rules. The case of "I want custom validation on the container" should only be supported by manually implementing Validate.
Your use-case is already supported
Oh great, thanks!
I think I'm generally against adding container-level validation if the use-cases can be served by more specific rules.
Ok, but in general it could be a nice feature to have, as there could be cases where you cannot perform the check using the existing rules, and if you implement the Validate trait manually, you cannot derive it anymore.
Just an option, what about supporting a custom validation rule at struct level?
#[derive(garde::Validate)]
#[garde(allow_unvalidated)]
#[garde(custom(validate_things)) // the validate_things would receives the whole struct
struct Test {
principal: i32,
#[garde(range(min=0, max=self.principal))]
deposit: i32,
...
}
I don't know if it's feasible, but it seems like a natural approach.
you cannot derive it anymore
The fact that you can't derive parts of that Validate impl seems reasonable to me. Everything in this library is built with extensibility in mind, so there's an expectation that if Garde doesn't support your use-case exactly, then you implement it yourself, or open a feature request (preferably the latter, or both).
I used GitHub code search to find usages of validator's version of this feature. Most of it was for field matching, some for checking mutual exclusivity of Option fields, and a terrifying amount of fully manual implementations of the validation logic, maybe because they wanted early-out semantics. It was definitely not an exhaustive search, but I believe there aren't any use-cases we couldn't support by adding a new rule or two.
I don't know if it's feasible, but it seems like a natural approach.
It is feasible, and it is a natural approach, but in terms of the implementation, it means:
- Supporting an extra kind of error (because there's no way to emit a "container-level" error that isn't attached to a field or array index)
- Adding an extra path to the top-level emit code in the derive macro, which would contribute to a combinatorial explosion of sorts (struct/enum + custom/no-custom), and would require at least doubling the size of the test suite
After thinking about it some more, I'm going to close this as wontfix, but thank you for the suggestion!
I also faced this issue... In my case, I have multiple arrays and hashmaps as properties of the struct. And there should be at least one element in any of them.
struct X {
a: Vec<i32>,
b: MyVec<i32>,
c: HashMap<i32, i32>,
}
It's not pretty, but you can work around the lack of struct-level validation using a newtype and a custom rule on the inner type:
#[derive(garde::Validate)]
#[garde(transparent)]
struct X(#[garde(custom(at_least_1))] Inner);
struct Inner {
a: Vec<i32>,
b: Vec<i32>,
c: Vec<i32>,
}
fn at_least_1(v: &Inner, _: &()) -> garde::Result {
if v.a.is_empty() && v.b.is_empty() && v.c.is_empty() {
Err(garde::Error::new("at least one element must be present"))
} else {
Ok(())
}
}
Now that I think about it, things have changed since this issue was initially opened. It is now straightforward to create a struct-level error, the error path would just be empty. The emit for this should also be pretty simple, call the custom rule here if present:
https://github.com/jprochazk/garde/blob/b5b9d1195aa0132e0de5b60bafa550921d60e46f/garde_derive/src/emit.rs#L36-L37
Is it possible to bypass it similar to this?
#[derive(garde::Validate)]
struct X {
#[garde(custom(at_least_1(&self)))]
a: Vec<i32>,
b: Vec<i32>,
c: Vec<i32>,
}
fn at_least_1(_self: &X) -> impl FnOnce(&X, &()) -> garde::Result + '_ {
move |_self, _ctx| {
Ok(())
}
}
I'm not sure if that would do what you want, the resulting error's path will contain the a field, value.a: at least one element must be present
#[derive(garde::Validate)]
struct X {
#[garde(custom(at_least_1(&self)))]
a: Vec<i32>,
#[garde(skip)]
b: Vec<i32>,
#[garde(skip)]
c: Vec<i32>,
}
fn at_least_1<'a, T>(v: &'a X) -> impl FnOnce(&T, &()) -> garde::Result + 'a {
move |_, _| {
if v.a.is_empty() && v.b.is_empty() && v.c.is_empty() {
Err(garde::Error::new("at least one element must be present"))
} else {
Ok(())
}
}
}
Thanks a lot 🙏🏼 That's better than manually writing a validation function and call two validation methods.
@jprochazk I hope I didn't miss it (I searched throug the issue tracker), but so far you only mentioned mutually exclusive options in a struct.
However, is there built-in support for "at least one of the optional struct fields must not be none"?
Here's another case where I'd like to have struct-level custom rules:
#[derive(Debug, Clone, Deserialize, garde::Validate)]
pub struct Scanners {
/// Map from a device identifier to a scanner configuration
pub devices: HashMap<String, Scanner>,
/// Default device identifier to use if no device is specified
pub default: String,
}
I want to validate that the default value is part of keys in devices. Additionally, I want to use the field-level length validation on devices to ensure that it's not empty. It seems that this isn't currently possible (unless I move up one level and use #[garde(custom(...))] there), right?