diff --git a/src/form.rs b/src/form.rs index 59cdf57..c5ed3d8 100644 --- a/src/form.rs +++ b/src/form.rs @@ -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 { +pub struct FormValidator { pub(crate) validations: Vec>>, } -impl Validator { +impl FormValidator { /// Validates the given form data. /// /// This runs all the validation functions for all the fields @@ -40,8 +40,8 @@ pub struct Form { impl Form { /// Gets the [`Validator`] for this form. - pub fn validator(self) -> Validator { - Validator { + pub fn validator(self) -> FormValidator { + FormValidator { validations: self.validations, } } @@ -57,10 +57,10 @@ impl Form { } /// Splits this [`Form`] into it's parts. - pub fn to_parts(self) -> (RwSignal, Validator, View) { + pub fn to_parts(self) -> (RwSignal, FormValidator, 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 { + fn get_validator() -> FormValidator { let builder = FormBuilder::new(Self::default(), Self::Style::default()); let builder = Self::build_form(builder); builder.validator() diff --git a/src/form_builder.rs b/src/form_builder.rs index 10b952c..49cba10 100644 --- a/src/form_builder.rs +++ b/src/form_builder.rs @@ -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 FormBuilder { } } - pub(crate) fn validator(&self) -> Validator { - Validator { + pub(crate) fn validator(&self) -> FormValidator { + FormValidator { validations: self.validations.clone(), } } diff --git a/src/lib.rs b/src/lib.rs index 8a9c08d..5ba52d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/validation_builder.rs b/src/validation_builder.rs new file mode 100644 index 0000000..569dcf9 --- /dev/null +++ b/src/validation_builder.rs @@ -0,0 +1,140 @@ +use crate::controls::ValidationFn; +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 { + /// The name of the field, for error messages. + name: String, + /// The functions to be called when validating. + functions: Vec Result<(), String> + 'static>>, +} + +impl Default for ValidationBuilder { + fn default() -> Self { + ValidationBuilder { + name: String::from("Field"), + functions: Vec::new(), + } + } +} + +impl ValidationBuilder { + /// 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 + } + + /// Builds the action validation function. + pub fn build(self) -> impl ValidationFn { + move |value| { + for f in self.functions.iter() { + match f(self.name.as_str(), value) { + Ok(()) => {} + err => return err, + } + } + Ok(()) + } + } +} + +impl ValidationBuilder { + /// 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 + Display + 'static> ValidationBuilder { + /// 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 + Display + 'static> ValidationBuilder { + /// Requires the field to be in the provided whitelist. + pub fn whitelist(mut self, whitelist: Vec) -> 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) -> Self { + self.functions.push(Box::new(move |name, value| { + if blacklist.contains(value) { + Err(format!("{} cannot be {}", name, value)) + } else { + Ok(()) + } + })); + self + } +}