Turn validators into a trait
Is your feature request related to a problem? Please describe.
I'm trying to create a validator that uses user provided Regex patterns against the input. These patterns come from a settings file, and I'd like to be able to pass the validator around. With the current API of StringValidator, it is possible but very hacky and verbose as it must be turned into a lambda to work.
It would be great to instead be able to use structs that implement a trait. These could then be easier passed around.
Describe the solution you'd like
The validators being a trait like the following for the StringValidator:
trait StringValidator {
fn validate(&self, value: &str) -> Result<(), String>
}
Then it can be implemented for all lambdas, to preserve the previous API:
impl<F> StringValidator for F
where
F: Fn(&str) -> Result<(), String>,
{
fn validate(&self, value: &str) -> Result<(), String> {
(self)(value)
}
}
But in addition, we can have structs with addition state, as well:
struct RegexValidator(Regex);
impl StringValidator for RegexValidator {
fn validate(&self, value: &str) -> Result<(), String> {
match self.0.is_match(value) {
true => Ok(()),
false => Err(format!("value must match pattern `{}`", self.0)),
}
}
}
This is currently still possible with lambdas, but especially when using shared values, it is very difficult to implement. Often forced to put this the same function where it is called.
Here is a very simplified variant, that I used to experiment with this idea: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=fae4884f2c2b872e41a972429a3d32ad
Honestly that looks really good.
A question, would you prefer a reference with a lifetime or a Box as a parameter?
Also, would you be willing to open up a PR? I understand this is a big change though so I can get some time to implement it myself.
A question, would you prefer a reference with a lifetime or a Box as a parameter?
Both works, I guess. But using a Box sounds definitely more portable. Like creating the prompt in some module and passing it around to other parts of the code, possibly re-using it, too.
If we only pass a reference, then we would have the same trouble again, of not being able to have a single owned version of the prompt that can be passed around.
Also, boxing is not really costly (the trait object is way more costly than the boxing), so we should be fine on the performance side too.
We could define the parameter explicitly or internally box:
struct SomePrompt {
validators: Vec<Box<dyn Validator>>,
}
impl SomePrompt {
// explicitly asking for a `Box`.
fn add_validator_one(&mut self, v: Box<dyn StringValidator>) {
self.validators.push(v);
}
// do the boxing ourselves internally.
fn add_validator_two<V: StringValidator + 'static>(&mut self, v: V) {
self.validators.push(Box::new(v));
}
}
Also, would you be willing to open up a PR?
Definitely! Would be happy to implement this.
🎉