Compare commits

...

2 Commits

Author SHA1 Message Date
e7fd8ec7e1 validation builder additions 2024-06-10 12:14:21 -05:00
bf04957370 add validation builder 2024-06-10 12:14:21 -05:00
4 changed files with 167 additions and 13 deletions

View File

@ -11,11 +11,11 @@ use web_sys::FormData;
///
/// This can be useful to use the same validation logic on the front
/// end and backend without duplicating the logic.
pub struct Validator<FD> {
pub struct FormValidator<FD> {
pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>,
}
impl<FD: FormToolData> Validator<FD> {
impl<FD: FormToolData> FormValidator<FD> {
/// Validates the given form data.
///
/// This runs all the validation functions for all the fields
@ -40,8 +40,8 @@ pub struct Form<FD: FormToolData> {
impl<FD: FormToolData> Form<FD> {
/// Gets the [`Validator`] for this form.
pub fn validator(self) -> Validator<FD> {
Validator {
pub fn validator(self) -> FormValidator<FD> {
FormValidator {
validations: self.validations,
}
}
@ -57,10 +57,10 @@ impl<FD: FormToolData> Form<FD> {
}
/// Splits this [`Form`] into it's parts.
pub fn to_parts(self) -> (RwSignal<FD>, Validator<FD>, View) {
pub fn to_parts(self) -> (RwSignal<FD>, FormValidator<FD>, View) {
(
self.fd,
Validator {
FormValidator {
validations: self.validations,
},
self.view,
@ -131,7 +131,7 @@ pub trait FormToolData: Default + Clone + 'static {
///
/// However, the code to render the views are not configured out, it
/// simply doesn't run, so the view needs to compile even on the server.
fn get_validator() -> Validator<Self> {
fn get_validator() -> FormValidator<Self> {
let builder = FormBuilder::new(Self::default(), Self::Style::default());
let builder = Self::build_form(builder);
builder.validator()

View File

@ -4,7 +4,7 @@ use crate::{
FieldSetter, ParseFn, RenderFn, UnparseFn, ValidationCb, ValidationFn,
VanityControlBuilder, VanityControlData,
},
form::{Form, FormToolData, Validator},
form::{Form, FormToolData, FormValidator},
styles::FormStyle,
};
use leptos::{
@ -305,8 +305,8 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
}
}
pub(crate) fn validator(&self) -> Validator<FD> {
Validator {
pub(crate) fn validator(&self) -> FormValidator<FD> {
FormValidator {
validations: self.validations.clone(),
}
}

View File

@ -1,7 +1,9 @@
pub mod controls;
pub mod form;
pub mod form_builder;
mod form;
mod form_builder;
pub mod styles;
mod validation_builder;
pub use form::{Form, FormToolData, Validator};
pub use form::{Form, FormToolData, FormValidator};
pub use form_builder::FormBuilder;
pub use validation_builder::ValidationBuilder;

152
src/validation_builder.rs Normal file
View File

@ -0,0 +1,152 @@
use crate::{controls::ValidationFn, FormToolData};
use std::fmt::Display;
/// A helper builder that allows you to specify a validation function
/// declaritivly
///
/// Using this builder is not required as validation functions can just be
/// closures, but for simple validation function this builder can be helpful
///
/// Validations are run in the order that they are called in the builder.
pub struct ValidationBuilder<FD: FormToolData, T: 'static> {
/// The name of the field, for error messages.
name: String,
/// The getter function for the field to validate.
field_fn: Box<dyn Fn(&FD) -> &T + 'static>,
/// The functions to be called when validating.
functions: Vec<Box<dyn Fn(&str, &T) -> Result<(), String> + 'static>>,
}
impl<FD: FormToolData, T: 'static> ValidationBuilder<FD, T> {
/// Creates a new empty [`ValidationBuilder`] on the given field.
pub fn for_field(field_fn: impl Fn(&FD) -> &T + 'static) -> Self {
ValidationBuilder {
name: String::from("Field"),
field_fn: Box::new(field_fn),
functions: Vec::new(),
}
}
/// The name of the field that is being validated.
///
/// This is the name that will be used for error messages.
pub fn named(mut self, name: impl ToString) -> Self {
self.name = name.to_string();
self
}
/// Adds a custom validation function.
///
/// The function should take the value as an argument and return
/// a [`Result<(), String>`], just like any other validation function.
pub fn custom(mut self, f: impl ValidationFn<T>) -> Self {
self.functions.push(Box::new(move |_name, value| f(value)));
self
}
/// Builds the action validation function.
pub fn build(self) -> impl ValidationFn<FD> {
move |form_data| {
let value = (self.field_fn)(form_data);
for f in self.functions.iter() {
match f(self.name.as_str(), value) {
Ok(()) => {}
err => return err,
}
}
Ok(())
}
}
}
impl<FD: FormToolData> ValidationBuilder<FD, String> {
/// Requires the field to not be empty.
pub fn required(mut self) -> Self {
self.functions.push(Box::new(move |name, value| {
if value.is_empty() {
Err(format!("{} is required", name))
} else {
Ok(())
}
}));
self
}
/// Requires the field's length to be at least `min_len`.
pub fn min_len(mut self, min_len: usize) -> Self {
self.functions.push(Box::new(move |name, value| {
if value.len() < min_len {
Err(format!("{} must be >= {} characters", name, min_len))
} else {
Ok(())
}
}));
self
}
/// Requires the field's length to be less than or equal to `min_len`.
pub fn max_len(mut self, max_len: usize) -> Self {
self.functions.push(Box::new(move |name, value| {
if value.len() > max_len {
Err(format!("{} must be <= {} characters", name, max_len))
} else {
Ok(())
}
}));
self
}
}
impl<FD: FormToolData, T: PartialOrd<T> + Display + 'static> ValidationBuilder<FD, T> {
/// Requires the value to be at least `min_value` according to
/// `PartialOrd`.
pub fn min_value(mut self, min_value: T) -> Self {
self.functions.push(Box::new(move |name, value| {
if value < &min_value {
Err(format!("{} mut be >= {}", name, min_value))
} else {
Ok(())
}
}));
self
}
/// Requires the value to be at most `max_value` according to
/// `PartialOrd`.
pub fn max_value(mut self, max_value: T) -> Self {
self.functions.push(Box::new(move |name, value| {
if value > &max_value {
Err(format!("{} mut be <= {}", name, max_value))
} else {
Ok(())
}
}));
self
}
}
impl<FD: FormToolData, T: PartialEq<T> + Display + 'static> ValidationBuilder<FD, T> {
/// Requires the field to be in the provided whitelist.
pub fn whitelist(mut self, whitelist: Vec<T>) -> Self {
self.functions.push(Box::new(move |name, value| {
if !whitelist.contains(value) {
Err(format!("{} cannot be {}", name, value))
} else {
Ok(())
}
}));
self
}
/// Requires the field to not be in the provided blacklist.
pub fn blacklist(mut self, blacklist: Vec<T>) -> Self {
self.functions.push(Box::new(move |name, value| {
if blacklist.contains(value) {
Err(format!("{} cannot be {}", name, value))
} else {
Ok(())
}
}));
self
}
}