garde icon indicating copy to clipboard operation
garde copied to clipboard

Validate correlated sibling fields

Open kamilglod opened this issue 2 years ago • 6 comments

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"));
        }
    }
}
  1. allow to pass self values to custom validator like:
#[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.

kamilglod avatar Oct 27 '23 10:10 kamilglod

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,
}

jprochazk avatar Oct 27 '23 15:10 jprochazk

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: &()) {}

kamilglod avatar Oct 27 '23 17:10 kamilglod

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!()
}

jprochazk avatar Oct 27 '23 19:10 jprochazk

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

kamilglod avatar Feb 13 '24 14:02 kamilglod

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.

jprochazk avatar Mar 29 '24 23:03 jprochazk

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

Rolv-Apneseth avatar Sep 08 '24 15:09 Rolv-Apneseth