jprochazk / garde

A powerful validation library for Rust
Apache License 2.0
447 stars 25 forks source link

Validate correlated sibling fields #73

Open kamilglod opened 10 months ago

kamilglod commented 10 months ago

It's quite common to have a logic in validation where we want to validate field based on values in other fields, or ensure that all fields are either filled or empty.

  1. it can be implemented by creating custom validator on schema level, but then I don't see an option to create an error with correct field (python marshmallow have an option to pass field_name to the ValidationError). There would be super helpful to have an access to garde::Report inside custom validator. We can use validate_into but it's not working with #[derive(garde::Validate)]
    
    struct User {
    password: String,
    repeat_password: String,
    }

impl garde::Validate for User { fn validate_into( &self, ctx: &Self::Context, mut parent: &mut dyn FnMut() -> garde::Path, report: &mut garde::Report ) { if self.password != self.repeat_password { let mut path = parent().join("repeate_password"); report.append(path, garde::Error::new("passwords are not equal")); } } }


2. allow to pass `self` values to `custom` validator like:
```rust
#[derive(garde::Validate)]
struct User {
    #[garde(length(min = 1, max = 255))]
    password: String,
    #[garde(length(min = 1, max = 255), custom(validate_equal_passwords, password=self.password))]
    repeat_password: String,
}

fn validate_equal_passwords(value: &str, other: &str) -> garde::Result {
    if value != other {
        return Err(garde::Error::new("passwords are not equal"));
    }
    Ok(())
}
  1. Use closures to get access to self:
pub struct Context {}

#[derive(garde::Validate)]
#[garde(context(Context))]
pub struct User {
    #[garde(length(min = 1, max = 255))]
    password: String,

    #[garde(length(min = 1, max = 255))]
    #[garde(custom(|value: &String, _ctx: &Context| {
        if value != &self.password {
            return Err(garde::Error::new("passwords are not equal"));
        }
        Ok(())
    }))]
    repeat_password: String,
}

Option 3 looks like the best solution but I can't find any confirmation in README that it's supported and recommended way of accessing struct siblings.

jprochazk commented 10 months ago

I can't find any confirmation in README that it's supported and recommended way of accessing struct siblings.

The self.field and ctx.field syntax is definitely part of the public API, and there's a mention of it in the top-level docs and README here, but there's no usage of self in the example, which should fixed.

For equality between two fields, we could add an equals rule that would use PartialEq:

#[derive(garde::Validate)]
struct User {
  #[garde(length(min=1, max=255))]
  password: String,
  #[garde(equals(self.password))]
  password2: String,
}
kamilglod commented 10 months ago

Adding equals() sounds good but it would solve only one use case, it would be good to have more general solution. Using closure with access to self sounds good, but for shared functions we would need to do somehing like:

#[derive(garde::Validate)]
pub struct User {
    #[garde(length(min = 1, max = 255))]
    password: String,

    #[garde(length(min = 1, max = 255))]
    #[garde(custom(|value: &String, _ctx: &()| {
        some_shared_validator(value, self.repeat_password)
    }))]
    repeat_password: String,
}

fn some_shared_validator(val: &String, other: &String) {}

instead of

#[derive(garde::Validate)]
pub struct User {
    #[garde(length(min = 1, max = 255))]
    password: String,

    #[garde(length(min = 1, max = 255))]
    #[garde(custom(some_shared_validator, other = self.repeat_password))]
    repeat_password: String,
}

fn some_shared_validator(val: &String, other: &String, _ctx: &()) {}
jprochazk commented 10 months ago

The general solution is custom. It doesn't always result in the most aesthetically pleasing solution, but you can use it to do pretty much anything you can think of. It accepts any expression that evaluates to impl FnOnce(&T, &Ctx) -> garde::Result, so you can use a higher-order function, or a macro that evaluates to a closure:

#[derive(garde::Validate)]
struct User {
    #[garde(length(min = 1, max = 255))]
    password: String,
    #[garde(custom(some_shared_validator(&self.password2)))]
    password2: String,
}

fn some_shared_validator(other: &str) -> impl FnOnce(&str, &()) -> garde::Result {
    |value, ctx| todo!()
}
kamilglod commented 6 months ago

higher-order function works very well, I think this issue might be close but some extra example in README will be very welcomed.

jprochazk commented 5 months ago

I added an example to the README in https://github.com/jprochazk/garde/commit/5b80e50203dcc91de8ffc2608e91813878107e57, but I'm keeping this open for an equals rule.

Rolv-Apneseth commented 9 hours ago

Perhaps I'm misunderstanding but is the equals described here the same as matches added in #110?