From 5afa60c8c8ecacfe23e0febaa8a1b8fd26d55856 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Tue, 14 Nov 2023 23:36:58 -0500 Subject: [PATCH 01/38] 'StrInput' type - Alternate type for handling validation/filtering for strings. --- inputfilter/src/filter/slug.rs | 380 ++++++++++++------------- inputfilter/src/lib.rs | 38 +-- inputfilter/src/string_input.rs | 410 +++++++++++++++++++++++++++ inputfilter/src/types.rs | 3 +- inputfilter/src/validator/pattern.rs | 58 ++++ src/lib.rs | 15 +- 6 files changed, 687 insertions(+), 217 deletions(-) create mode 100644 inputfilter/src/string_input.rs diff --git a/inputfilter/src/filter/slug.rs b/inputfilter/src/filter/slug.rs index ce8c0fb..171a342 100644 --- a/inputfilter/src/filter/slug.rs +++ b/inputfilter/src/filter/slug.rs @@ -1,190 +1,190 @@ -use std::borrow::Cow; -use std::sync::OnceLock; -use regex::Regex; - -static SLUG_FILTER_REGEX: OnceLock = OnceLock::new(); -static SLUG_FILTER_REGEX_STR: &str = r"(?i)[^\w\-]"; -static DASH_FILTER_REGEX: OnceLock = OnceLock::new(); -static DASH_FILTER_REGEX_STR: &str = r"(?i)\-{2,}"; - -/// Returns the static regex used for filtering a string to slug. -pub fn get_slug_filter_regex() -> &'static Regex { - SLUG_FILTER_REGEX.get_or_init(|| Regex::new(SLUG_FILTER_REGEX_STR).unwrap()) -} - -/// Returns the static regex used for filtering out multiple dashes for one dash. -pub fn get_dash_filter_regex() -> &'static Regex { - DASH_FILTER_REGEX.get_or_init(|| Regex::new(DASH_FILTER_REGEX_STR).unwrap()) -} - -/// Normalizes given string into a slug - e.g., a string matching /^\w[\w\-]{0,198}\w?$/ -/// -/// ```rust -/// use std::borrow::Cow; -/// use walrs_inputfilter::filter::slug::to_slug; -/// -/// assert_eq!(to_slug(Cow::Borrowed("Hello World")), "hello-world"); -/// ``` -pub fn to_slug(xs: Cow) -> Cow { - _to_slug(get_slug_filter_regex(), 200, xs) -} - -/// Same as `to_slug` method but removes duplicate '-' symbols. -/// -/// ```rust -/// use std::borrow::Cow; -/// use walrs_inputfilter::filter::slug::to_pretty_slug; -/// -/// assert_eq!(to_pretty_slug(Cow::Borrowed("%$Hello@#$@#!(World$$")), "hello-world"); -/// ``` -pub fn to_pretty_slug(xs: Cow) -> Cow { - _to_pretty_slug(get_slug_filter_regex(), 200, xs) -} - -fn _to_slug<'a>(pattern: &Regex, max_length: usize, xs: Cow<'a, str>) -> Cow<'a, str> { - let rslt = pattern.replace_all(xs.as_ref(), "-") - .to_lowercase() - .trim_matches('-') - .to_string(); - - if rslt.len() > max_length { - Cow::Owned(rslt[..max_length + 1].to_string()) - } else { - Cow::Owned(rslt) - } -} - -fn _to_pretty_slug<'a>(pattern: &Regex, max_length: usize, xs: Cow<'a, str>) -> Cow<'a, str> { - if xs.is_empty() { return xs; } - - get_dash_filter_regex() - .replace_all(&_to_slug(pattern, max_length, xs), "-") - .to_string() - .into() -} - -/// Configurable version of `to_slug()` - allows for setting the max_length. -#[derive(Clone, Debug, Default, Builder)] -pub struct SlugFilter { - #[builder(setter(into), default = "200")] - pub max_length: usize, - - #[builder(setter(into), default = "true")] - pub allow_duplicate_dashes: bool, -} - -impl SlugFilter { - pub fn new(max_length: usize, allow_duplicate_dashes: bool) -> Self { - SlugFilter { - max_length, - allow_duplicate_dashes, - } - } - - pub fn filter<'a>(&self, xs: Cow<'a, str>) -> Cow<'a, str> { - if self.allow_duplicate_dashes { - _to_slug(get_slug_filter_regex(), self.max_length, xs) - } else { - _to_pretty_slug(get_slug_filter_regex(), self.max_length, xs) - } - } -} - -impl<'a> FnOnce<(Cow<'a, str>, )> for SlugFilter { - type Output = Cow<'a, str>; - - extern "rust-call" fn call_once(self, args: (Cow<'a, str>, )) -> Self::Output { - self.filter(args.0) - } -} - -impl<'a, 'b> Fn<(Cow<'a, str>, )> for SlugFilter { - extern "rust-call" fn call(&self, args: (Cow<'a, str>, )) -> Self::Output { - self.filter(args.0) - } -} - -impl<'a, 'b> FnMut<(Cow<'a, str>, )> for SlugFilter { - extern "rust-call" fn call_mut(&mut self, args: (Cow<'a, str>, )) -> Self::Output { - self.filter(args.0) - } -} - -#[cfg(test)] -mod test { - use std::{borrow::Cow, thread}; - use super::*; - - #[test] - fn test_to_slug_standalone_method() { - for (cow_str, expected) in vec![ - (Cow::Borrowed("Hello World"), "hello-world"), - (Cow::Borrowed("#$@#$Hello World$@#$"), "hello-world"), - (Cow::Borrowed("$Hello'\"@$World$"), "hello----world"), - ] { - assert_eq!(to_slug(cow_str), expected); - } - } - - #[test] - fn test_to_pretty_slug_standalone_method() { - for (cow_str, expected) in vec![ - (Cow::Borrowed("Hello World"), "hello-world"), - (Cow::Borrowed("$Hello World$"), "hello-world"), - (Cow::Borrowed("$Hello'\"@$World$"), "hello-world"), - ] { - assert_eq!(to_pretty_slug(cow_str), expected); - } - } - - #[test] - fn test_slug_filter_constructor() { - for x in vec![0, 1, 2] { - let instance = SlugFilter::new(x, false); - assert_eq!(instance.max_length, x); - assert_eq!(instance.allow_duplicate_dashes, false); - } - } - - #[test] - fn test_slug_filter_builder() { - let instance = SlugFilterBuilder::default().build().unwrap(); - assert_eq!(instance.max_length, 200); - assert_eq!(instance.allow_duplicate_dashes, true); - } - - #[test] - fn test_fn_trait_impls() { - let slug_filter = SlugFilter { max_length: 200, allow_duplicate_dashes: true }; - - assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello-world"); - assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello---world"); - assert_eq!(slug_filter(Cow::Borrowed("$@#$Hello @World@#$@#$")), "hello----world"); - } - - #[test] - fn test_standalone_methods_in_threaded_contexts() { - thread::scope(|scope| { - scope.spawn(move ||{ - assert_eq!(to_slug(Cow::Borrowed("Hello World")), "hello-world"); - assert_eq!(to_slug(Cow::Borrowed("Hello World")), "hello---world"); - assert_eq!(to_pretty_slug(Cow::Borrowed("$@#$Hello@#$@#$World@#$@#$")), "hello-world"); - }); - }); - } - - #[test] - fn test_struct_in_threaded_contexts() { - let slug_filter = SlugFilterBuilder::default() - .allow_duplicate_dashes(false) - .build() - .unwrap(); - - thread::scope(|scope| { - scope.spawn(move ||{ - assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello-world"); - assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello-world"); - }); - }); - } -} +use std::borrow::Cow; +use std::sync::OnceLock; +use regex::Regex; + +static SLUG_FILTER_REGEX: OnceLock = OnceLock::new(); +static SLUG_FILTER_REGEX_STR: &str = r"(?i)[^\w\-]"; +static DASH_FILTER_REGEX: OnceLock = OnceLock::new(); +static DASH_FILTER_REGEX_STR: &str = r"(?i)\-{2,}"; + +/// Returns the static regex used for filtering a string to slug. +pub fn get_slug_filter_regex() -> &'static Regex { + SLUG_FILTER_REGEX.get_or_init(|| Regex::new(SLUG_FILTER_REGEX_STR).unwrap()) +} + +/// Returns the static regex used for filtering out multiple dashes for one dash. +pub fn get_dash_filter_regex() -> &'static Regex { + DASH_FILTER_REGEX.get_or_init(|| Regex::new(DASH_FILTER_REGEX_STR).unwrap()) +} + +/// Normalizes given string into a slug - e.g., a string matching /^\w[\w\-]{0,198}\w?$/ +/// +/// ```rust +/// use std::borrow::Cow; +/// use walrs_inputfilter::filter::slug::to_slug; +/// +/// assert_eq!(to_slug(Cow::Borrowed("Hello World")), "hello-world"); +/// ``` +pub fn to_slug(xs: Cow) -> Cow { + _to_slug(get_slug_filter_regex(), 200, xs) +} + +/// Same as `to_slug` method but removes duplicate '-' symbols. +/// +/// ```rust +/// use std::borrow::Cow; +/// use walrs_inputfilter::filter::slug::to_pretty_slug; +/// +/// assert_eq!(to_pretty_slug(Cow::Borrowed("%$Hello@#$@#!(World$$")), "hello-world"); +/// ``` +pub fn to_pretty_slug(xs: Cow) -> Cow { + _to_pretty_slug(get_slug_filter_regex(), 200, xs) +} + +fn _to_slug<'a>(pattern: &Regex, max_length: usize, xs: Cow<'a, str>) -> Cow<'a, str> { + let rslt = pattern.replace_all(xs.as_ref(), "-") + .to_lowercase() + .trim_matches('-') + .to_string(); + + if rslt.len() > max_length { + Cow::Owned(rslt[..max_length + 1].to_string()) + } else { + Cow::Owned(rslt) + } +} + +fn _to_pretty_slug<'a>(pattern: &Regex, max_length: usize, xs: Cow<'a, str>) -> Cow<'a, str> { + if xs.is_empty() { return xs; } + + get_dash_filter_regex() + .replace_all(&_to_slug(pattern, max_length, xs), "-") + .to_string() + .into() +} + +/// Configurable version of `to_slug()` - allows for setting the max_length. +#[derive(Clone, Debug, Default, Builder)] +pub struct SlugFilter { + #[builder(setter(into), default = "200")] + pub max_length: usize, + + #[builder(setter(into), default = "true")] + pub allow_duplicate_dashes: bool, +} + +impl SlugFilter { + pub fn new(max_length: usize, allow_duplicate_dashes: bool) -> Self { + SlugFilter { + max_length, + allow_duplicate_dashes, + } + } + + pub fn filter<'a>(&self, xs: Cow<'a, str>) -> Cow<'a, str> { + if self.allow_duplicate_dashes { + _to_slug(get_slug_filter_regex(), self.max_length, xs) + } else { + _to_pretty_slug(get_slug_filter_regex(), self.max_length, xs) + } + } +} + +impl<'a> FnOnce<(Cow<'a, str>, )> for SlugFilter { + type Output = Cow<'a, str>; + + extern "rust-call" fn call_once(self, args: (Cow<'a, str>, )) -> Self::Output { + self.filter(args.0) + } +} + +impl<'a> Fn<(Cow<'a, str>, )> for SlugFilter { + extern "rust-call" fn call(&self, args: (Cow<'a, str>, )) -> Self::Output { + self.filter(args.0) + } +} + +impl<'a> FnMut<(Cow<'a, str>, )> for SlugFilter { + extern "rust-call" fn call_mut(&mut self, args: (Cow<'a, str>, )) -> Self::Output { + self.filter(args.0) + } +} + +#[cfg(test)] +mod test { + use std::{borrow::Cow, thread}; + use super::*; + + #[test] + fn test_to_slug_standalone_method() { + for (cow_str, expected) in vec![ + (Cow::Borrowed("Hello World"), "hello-world"), + (Cow::Borrowed("#$@#$Hello World$@#$"), "hello-world"), + (Cow::Borrowed("$Hello'\"@$World$"), "hello----world"), + ] { + assert_eq!(to_slug(cow_str), expected); + } + } + + #[test] + fn test_to_pretty_slug_standalone_method() { + for (cow_str, expected) in vec![ + (Cow::Borrowed("Hello World"), "hello-world"), + (Cow::Borrowed("$Hello World$"), "hello-world"), + (Cow::Borrowed("$Hello'\"@$World$"), "hello-world"), + ] { + assert_eq!(to_pretty_slug(cow_str), expected); + } + } + + #[test] + fn test_slug_filter_constructor() { + for x in vec![0, 1, 2] { + let instance = SlugFilter::new(x, false); + assert_eq!(instance.max_length, x); + assert_eq!(instance.allow_duplicate_dashes, false); + } + } + + #[test] + fn test_slug_filter_builder() { + let instance = SlugFilterBuilder::default().build().unwrap(); + assert_eq!(instance.max_length, 200); + assert_eq!(instance.allow_duplicate_dashes, true); + } + + #[test] + fn test_fn_trait_impls() { + let slug_filter = SlugFilter { max_length: 200, allow_duplicate_dashes: true }; + + assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello-world"); + assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello---world"); + assert_eq!(slug_filter(Cow::Borrowed("$@#$Hello @World@#$@#$")), "hello----world"); + } + + #[test] + fn test_standalone_methods_in_threaded_contexts() { + thread::scope(|scope| { + scope.spawn(move ||{ + assert_eq!(to_slug(Cow::Borrowed("Hello World")), "hello-world"); + assert_eq!(to_slug(Cow::Borrowed("Hello World")), "hello---world"); + assert_eq!(to_pretty_slug(Cow::Borrowed("$@#$Hello@#$@#$World@#$@#$")), "hello-world"); + }); + }); + } + + #[test] + fn test_struct_in_threaded_contexts() { + let slug_filter = SlugFilterBuilder::default() + .allow_duplicate_dashes(false) + .build() + .unwrap(); + + thread::scope(|scope| { + scope.spawn(move ||{ + assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello-world"); + assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello-world"); + }); + }); + } +} diff --git a/inputfilter/src/lib.rs b/inputfilter/src/lib.rs index ae73578..e356d2d 100644 --- a/inputfilter/src/lib.rs +++ b/inputfilter/src/lib.rs @@ -1,18 +1,20 @@ -#![feature(fn_traits)] -#![feature(unboxed_closures)] - -#[macro_use] -extern crate derive_builder; - -pub mod types; -pub mod validator; -pub mod input; -pub mod filter; - -pub use types::*; -pub use validator::*; -pub use input::*; -pub use filter::*; - -// @todo Add 'Builder' for `wal_inputfilter` structs. - +#![feature(fn_traits)] +#![feature(unboxed_closures)] + +#[macro_use] +extern crate derive_builder; + +pub mod types; +pub mod validator; +pub mod input; +pub mod filter; +pub mod string_input; + +pub use types::*; +pub use validator::*; +pub use input::*; +pub use filter::*; +pub use string_input::*; + +// @todo Add 'Builder' for `wal_inputfilter` structs. + diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs new file mode 100644 index 0000000..58fd732 --- /dev/null +++ b/inputfilter/src/string_input.rs @@ -0,0 +1,410 @@ +use std::borrow::Cow; +use std::fmt::{Debug, Display, Formatter}; + +use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; + +pub type StrValueMissingViolationCallback = +dyn Fn(&StrInput) -> ViolationMessage + Send + Sync; + +#[derive(Builder, Clone)] +#[builder(pattern = "owned")] +pub struct StrInput<'a, 'b> +{ + #[builder(default = "true")] + pub break_on_failure: bool, + + /// @todo This should be an `Option>`, for compatibility. + #[builder(setter(into), default = "None")] + pub name: Option<&'a str>, + + #[builder(default = "false")] + pub required: bool, + + #[builder(setter(strip_option), default = "None")] + pub validators: Option>>, + + #[builder(setter(strip_option), default = "None")] + pub filters: Option>>>, + + #[builder(default = "&str_value_missing_msg")] + pub value_missing: &'a (dyn Fn(&StrInput<'a, 'b>) -> ViolationMessage + Send + Sync), + + // @todo Add support for `io_validators` (e.g., validators that return futures). +} + +impl<'a, 'b> StrInput<'a, 'b> +{ + pub fn new(name: Option<&'a str>) -> Self { + StrInput { + break_on_failure: false, + name, + required: false, + validators: None, + filters: None, + value_missing: &str_value_missing_msg, + } + } +} + +impl<'a, 'b> InputConstraints<'a, 'b, str> for StrInput<'a, 'b> { + fn get_should_break_on_failure(&self) -> bool { + self.break_on_failure + } + + fn get_required(&self) -> bool { + self.required + } + + fn get_name(&self) -> Option> { + self.name.map(move |s: &'a str| Cow::Borrowed(s)) + } + + fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self) -> ViolationMessage + Send + Sync) { + self.value_missing + } + + fn get_validators(&self) -> Option<&[&'a Validator<&'b str>]> { + self.validators.as_deref() + } + + fn get_filters(&self) -> Option<&[&'a Filter>]> { + self.filters.as_deref() + } +} + +impl Default for StrInput<'_, '_> { + fn default() -> Self { + Self::new(None) + } +} + +impl Display for StrInput<'_, '_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "StrInput {{ name: {}, required: {}, validators: {}, filters: {} }}", + self.name.unwrap_or("None"), + self.required, + self.validators.as_deref().map(|vs| + format!("Some([Validator; {}])", vs.len()) + ).unwrap_or("None".to_string()), + self.filters.as_deref().map(|fs| + format!("Some([Filter; {}])", fs.len()) + ).unwrap_or("None".to_string()), + ) + } +} + +impl Debug for StrInput<'_, '_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self) + } +} + +pub fn str_value_missing_msg(_: &StrInput) -> String { + "Value is missing.".to_string() +} + +#[cfg(test)] +mod test { + use regex::Regex; + use std::{borrow::Cow, error::Error, sync::Arc, thread}; + use crate::types::{ConstraintViolation, ConstraintViolation::{PatternMismatch, RangeOverflow}, + ValidationResult, InputConstraints}; + use crate::validator::pattern::PatternValidator; + use super::*; + + // Tests setup types + fn less_than_1990_msg(value: &str) -> String { + format!("{} is greater than 1989-12-31", value) + } + + /// Faux validator that checks if the input is less than 1990-01-01. + fn less_than_1990(x: &str) -> ValidationResult { + if x >= "1989-12-31" { + return Err(vec![( + RangeOverflow, + less_than_1990_msg(x) + )]); + } + Ok(()) + } + + fn ymd_mismatch_msg(s: &str, pattern_str: &str) -> String { + format!("{} doesn't match pattern {}", s, pattern_str) + } + + fn ymd_check(s: &str) -> ValidationResult { + // Simplified ISO year-month-date regex + let rx = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap(); + if !rx.is_match(s) { + return Err(vec![(PatternMismatch, ymd_mismatch_msg(s, rx.as_str()))]); + } + Ok(()) + } + + /// Faux filter that returns the last date of the month. + /// **Note:** Assumes that the input is a valid ISO year-month-date. + fn to_last_date_of_month(x: Option>) -> Option> { + x.map(|x| { + let mut xs = x.into_owned(); + xs.replace_range(8..10, "31"); + Cow::Owned(xs) + }) + } + + #[test] + fn test_input_builder() -> Result<(), Box> { + // Simplified ISO year-month-date regex + let ymd_regex = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; + let ymd_regex_2 = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; + let ymd_regex_arc_orig = Arc::new(ymd_regex); + let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); + + let ymd_mismatch_msg = Arc::new(move |s: &str| -> String { + format!("{} doesn't match pattern {}", s, ymd_regex_arc.as_str()) + }); + + let ymd_mismatch_msg_arc = Arc::clone(&ymd_mismatch_msg); + let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); + + let ymd_check = move |s: &str| -> ValidationResult { + if !ymd_regex_arc.is_match(s) { + return Err(vec![(PatternMismatch, ymd_mismatch_msg_arc(s))]); + } + Ok(()) + }; + + // Validator case 1 + let pattern_validator = PatternValidator { + pattern: Cow::Owned(ymd_regex_2), + pattern_mismatch: &|validator, s| { + format!("{} doesn't match pattern {}", s, validator.pattern.as_str()) + }, + }; + + let less_than_1990_input = StrInputBuilder::default() + .validators(vec![&less_than_1990]) + .build()?; + + let yyyy_mm_dd_input = StrInputBuilder::default() + .validators(vec![&ymd_check]) + .build()?; + + let yyyy_mm_dd_input2 = StrInputBuilder::default() + .validators(vec![&pattern_validator]) + .build()?; + + // Missing value check + match less_than_1990_input.validate(None) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + // Mismatch check + let value = "1000-99-999"; + match yyyy_mm_dd_input.validate(Some(&value)) { + Ok(_) => panic!("Expected Err(...); Received Ok(())"), + Err(tuples) => { + assert_eq!(tuples[0].0, PatternMismatch); + assert_eq!(tuples[0].1, ymd_mismatch_msg(value).as_str()); + } + } + + // Valid check + match yyyy_mm_dd_input.validate(None) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + // Valid check 2 + let value = "1000-99-99"; + match yyyy_mm_dd_input.validate(Some(&value)) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + // Valid check + let value = "1000-99-99"; + match yyyy_mm_dd_input2.validate(Some(&value)) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + Ok(()) + } + + #[test] + fn test_thread_safety() -> Result<(), Box> { + let less_than_1990_input = StrInputBuilder::default() + .validators(vec![&less_than_1990]) + .build()?; + + let ymd_input = StrInputBuilder::default() + .validators(vec![&ymd_check]) + .build()?; + + let less_than_input = Arc::new(less_than_1990_input); + let less_than_input_instance = Arc::clone(&less_than_input); + + let str_input = Arc::new(ymd_input); + let str_input_instance = Arc::clone(&str_input); + + let handle = + thread::spawn( + move || match less_than_input_instance.validate(Some("2023-12-31")) { + Err(x) => { + assert_eq!(x[0].1.as_str(), less_than_1990_msg("2023-12-31")); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + let handle2 = + thread::spawn( + move || match str_input_instance.validate(Some(&"")) { + Err(x) => { + assert_eq!( + x[0].1.as_str(), + ymd_mismatch_msg( + "", + Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap().as_str(), + ) + ); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + // @note Conclusion of tests here is that validators can only (easily) be shared between threads if they are function pointers - + // closures are too loose and require over the top value management and planning due to the nature of multi-threaded + // contexts. + + // Contrary to the above, 'scoped threads', will allow variable sharing without requiring them to + // be 'moved' first (as long as rust's lifetime rules are followed - + // @see https://blog.logrocket.com/using-rust-scoped-threads-improve-efficiency-safety/ + // ). + + handle.join().unwrap(); + handle2.join().unwrap(); + + Ok(()) + } + + /// Example showing shared references in `StrInput`, and user-land, controls. + #[test] + fn test_thread_safety_with_scoped_threads_and_closures() -> Result<(), Box> { + let ymd_rx = Arc::new(Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap()); + let ymd_rx_clone = Arc::clone(&ymd_rx); + + let ymd_check = move |s: &str| -> ValidationResult { + // Simplified ISO year-month-date regex + if !ymd_rx_clone.is_match(s) { + return Err(vec![( + PatternMismatch, + ymd_mismatch_msg(s, ymd_rx_clone.as_str()), + )]); + } + Ok(()) + }; + + let less_than_1990_input = StrInputBuilder::default() + .validators(vec![&less_than_1990]) + .filters(vec![&to_last_date_of_month]) + .build()?; + + let ymd_input = StrInputBuilder::default() + .validators(vec![&ymd_check]) + .build()?; + + let less_than_input = Arc::new(less_than_1990_input); + let less_than_input_instance = Arc::clone(&less_than_input); + let ymd_check_input = Arc::new(ymd_input); + let ymd_check_input_instance = Arc::clone(&ymd_check_input); + + thread::scope(|scope| { + scope.spawn( + || match less_than_input_instance.validate(Some("2023-12-31")) { + Err(x) => { + assert_eq!(x[0].1.as_str(), &less_than_1990_msg("2023-12-31")); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + scope.spawn( + || match less_than_input_instance.validate_and_filter(Some("1989-01-01")) { + Err(err) => panic!("Expected `Ok(Some({:#?})`; Received `Err({:#?})`", + Cow::::Owned("1989-01-31".to_string()), err), + Ok(Some(x)) => assert_eq!(x, Cow::::Owned("1989-01-31".to_string())), + _ => panic!("Expected `Ok(Some(Cow::Owned(99 * 2)))`; Received `Ok(None)`"), + }, + ); + + scope.spawn( + || match ymd_check_input_instance.validate(Some(&"")) { + Err(x) => { + assert_eq!(x[0].1.as_str(), ymd_mismatch_msg("", ymd_rx.as_str())); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + scope.spawn( + || if let Err(_err_tuple) = ymd_check_input_instance.validate(Some(&"2013-08-31")) { + panic!("Expected `Ok(()); Received Err(...)`") + }, + ); + }); + + Ok(()) + } + + #[test] + fn test_validate_and_filter() { + let input = StrInputBuilder::default() + .name("hello") + .required(true) + .validators(vec![&less_than_1990]) + .filters(vec![&to_last_date_of_month]) + .build() + .unwrap(); + + assert_eq!(input.validate_and_filter(Some("2023-12-31")), Err(vec![(RangeOverflow, less_than_1990_msg("2023-12-31"))])); + assert_eq!(input.validate_and_filter(Some("1989-01-01")), Ok(Some(Cow::Owned("1989-01-31".to_string())))); + } + + #[test] + fn test_value_type() { + let callback1 = |xs: &str| -> ValidationResult { + if !xs.is_empty() { + Ok(()) + } else { + Err(vec![( + ConstraintViolation::TypeMismatch, + "Error".to_string(), + )]) + } + }; + + let _input = StrInputBuilder::default() + .name("hello") + .validators(vec![&callback1]) + .build() + .unwrap(); + } + + #[test] + fn test_display() { + let input = StrInputBuilder::default() + .name("hello") + .validators(vec![&less_than_1990]) + .build() + .unwrap(); + + assert_eq!( + input.to_string(), + "StrInput { name: hello, required: false, validators: Some([Validator; 1]), filters: None }" + ); + } +} diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index b68eb00..edcab20 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -24,7 +24,6 @@ impl InputValue for f64 {} impl InputValue for bool {} -impl InputValue for str {} impl InputValue for Box {} impl InputValue for String {} impl<'a> InputValue for Cow<'a, str> {} @@ -91,7 +90,7 @@ pub trait ToAttributesList { } } -pub trait InputConstraints<'a, 'call_ctx: 'a, T: InputValue>: Display + Debug + 'a { +pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + PartialEq + PartialOrd + Serialize + ?Sized>: Display + Debug + 'a { fn get_should_break_on_failure(&self) -> bool; fn get_required(&self) -> bool; fn get_name(&self) -> Option>; diff --git a/inputfilter/src/validator/pattern.rs b/inputfilter/src/validator/pattern.rs index 04280bb..a5d0892 100644 --- a/inputfilter/src/validator/pattern.rs +++ b/inputfilter/src/validator/pattern.rs @@ -84,6 +84,64 @@ impl Fn<(&&str, )> for PatternValidator<'_> { } } +impl FnOnce<(Box, )> for PatternValidator<'_> { + type Output = ValidationResult; + + extern "rust-call" fn call_once(self, args: (Box, )) -> Self::Output { + self.validate(&args.0) + } +} + +impl FnMut<(Box, )> for PatternValidator<'_> { + extern "rust-call" fn call_mut(&mut self, args: (Box, )) -> Self::Output { + self.validate(&args.0) + } +} + +impl Fn<(Box, )> for PatternValidator<'_> { + extern "rust-call" fn call(&self, args: (Box, )) -> Self::Output { + self.validate(&args.0) + } +} +impl FnOnce<(&Box, )> for PatternValidator<'_> { + type Output = ValidationResult; + + extern "rust-call" fn call_once(self, args: (&Box, )) -> Self::Output { + self.validate(&args.0) + } +} + +impl FnMut<(&Box, )> for PatternValidator<'_> { + extern "rust-call" fn call_mut(&mut self, args: (&Box, )) -> Self::Output { + self.validate(&args.0) + } +} + +impl Fn<(&Box, )> for PatternValidator<'_> { + extern "rust-call" fn call(&self, args: (&Box, )) -> Self::Output { + self.validate(&args.0) + } +} +impl FnOnce<(&String, )> for PatternValidator<'_> { + type Output = ValidationResult; + + extern "rust-call" fn call_once(self, args: (&String, )) -> Self::Output { + self.validate(&args.0) + } +} + +impl FnMut<(&String, )> for PatternValidator<'_> { + extern "rust-call" fn call_mut(&mut self, args: (&String, )) -> Self::Output { + self.validate(&args.0) + } +} + +impl Fn<(&String, )> for PatternValidator<'_> { + extern "rust-call" fn call(&self, args: (&String, )) -> Self::Output { + self.validate(&args.0) + } +} + impl Display for PatternValidator<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "PatternValidator {{pattern: {}}}", &self.pattern.to_string()) diff --git a/src/lib.rs b/src/lib.rs index f4057a9..bc9ca0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ -#![feature(fn_traits)] -#![feature(unboxed_closures)] - -pub use walrs_inputfilter::validator; -pub use walrs_inputfilter::filter; -pub use walrs_inputfilter::input; -pub use walrs_inputfilter::types; +#![feature(fn_traits)] +#![feature(unboxed_closures)] + +pub use walrs_inputfilter::validator; +pub use walrs_inputfilter::filter; +pub use walrs_inputfilter::input; +pub use walrs_inputfilter::string_input; +pub use walrs_inputfilter::types; From 952316a74155a55d641be87045c1b3e5ae2d9d83 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Wed, 15 Nov 2023 21:49:47 -0500 Subject: [PATCH 02/38] inputfilter - Added 'validate1', and 'validate_and_filter1' method variants, and added .gitattributes '*.rs' crlf entry. --- .gitattributes | 1 + inputfilter/src/types.rs | 390 +++++++++++++++++++++++---------------- 2 files changed, 233 insertions(+), 158 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..da2c02c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.rs eol=crlf diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index edcab20..ddf36ed 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -1,158 +1,232 @@ -use std::ops::{Add, Div, Mul, Rem, Sub}; -use std::borrow::Cow; -use std::fmt::{Debug, Display}; -use serde::Serialize; - -pub trait InputValue: ToOwned + Debug + Display + PartialEq + PartialOrd + Serialize {} - -impl InputValue for i8 {} -impl InputValue for i16 {} -impl InputValue for i32 {} -impl InputValue for i64 {} -impl InputValue for i128 {} -impl InputValue for isize {} - -impl InputValue for u8 {} -impl InputValue for u16 {} -impl InputValue for u32 {} -impl InputValue for u64 {} -impl InputValue for u128 {} -impl InputValue for usize {} - -impl InputValue for f32 {} -impl InputValue for f64 {} - -impl InputValue for bool {} - -impl InputValue for Box {} -impl InputValue for String {} -impl<'a> InputValue for Cow<'a, str> {} -impl InputValue for &'_ str {} -impl InputValue for &&'_ str {} - -pub trait NumberValue: Default + InputValue + Copy + Add + Sub + Mul + Div + Rem {} - -impl NumberValue for i8 {} -impl NumberValue for i16 {} -impl NumberValue for i32 {} -impl NumberValue for i64 {} -impl NumberValue for i128 {} -impl NumberValue for isize {} - -impl NumberValue for u8 {} -impl NumberValue for u16 {} -impl NumberValue for u32 {} -impl NumberValue for u64 {} -impl NumberValue for u128 {} -impl NumberValue for usize {} - -impl NumberValue for f32 {} -impl NumberValue for f64 {} - -#[derive(PartialEq, Debug, Clone, Copy)] -pub enum ConstraintViolation { - CustomError, - PatternMismatch, - RangeOverflow, - RangeUnderflow, - StepMismatch, - TooLong, - TooShort, - NotEqual, - - /// Used to convey an expected string format (not necessarily a `Pattern` format; - /// E.g., invalid email hostname in email pattern, etc.). - TypeMismatch, - ValueMissing, -} - -pub type ViolationMessage = String; - -pub type ValidationError = (ConstraintViolation, ViolationMessage); - -pub type ValidationResult = Result<(), Vec>; - -pub type Filter = dyn Fn(Option) -> Option + Send + Sync; - -pub type Validator = dyn Fn(T) -> ValidationResult + Send + Sync; - -pub trait ValidateValue { - fn validate(&self, value: T) -> ValidationResult; -} - -pub trait FilterValue { - fn filter(&self, value: Option>) -> Option>; -} - -pub trait ToAttributesList { - fn to_attributes_list(&self) -> Option> { - None - } -} - -pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + PartialEq + PartialOrd + Serialize + ?Sized>: Display + Debug + 'a { - fn get_should_break_on_failure(&self) -> bool; - fn get_required(&self) -> bool; - fn get_name(&self) -> Option>; - fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self) -> ViolationMessage + Send + Sync); - fn get_validators(&self) -> Option<&[&'a Validator<&'call_ctx T>]>; - fn get_filters(&self) -> Option<&[&'a Filter>]>; - - fn validate_with_validators(&self, value: &'call_ctx T, validators: Option<&[&'a Validator<&'call_ctx T>]>) -> ValidationResult { - validators.map(|vs| { - - // If not break on failure then capture all validation errors. - if !self.get_should_break_on_failure() { - return vs.iter().fold( - Vec::::new(), - |mut agg, f| match (f)(value) { - Err(mut message_tuples) => { - agg.append(message_tuples.as_mut()); - agg - } - _ => agg, - }); - } - - // Else break on, and capture, first failure. - let mut agg = Vec::::new(); - for f in vs.iter() { - if let Err(mut message_tuples) = (f)(value) { - agg.append(message_tuples.as_mut()); - break; - } - } - agg - }) - .and_then(|messages| if messages.is_empty() { None } else { Some(messages) }) - .map_or(Ok(()), Err) - } - - fn validate(&self, value: Option<&'call_ctx T>) -> ValidationResult { - match value { - None => { - if self.get_required() { - Err(vec![( - ConstraintViolation::ValueMissing, - (self.get_value_missing_handler())(self), - )]) - } else { - Ok(()) - } - } - Some(v) => self.validate_with_validators(v, self.get_validators()), - } - } - - fn filter(&self, value: Option>) -> Option> { - match self.get_filters() { - None => value, - Some(fs) => fs.iter().fold(value, |agg, f| (f)(agg)), - } - } - - fn validate_and_filter(&self, x: Option<&'call_ctx T>) -> Result>, Vec> { - self.validate(x).map(|_| self.filter(x.map(|_x| Cow::Borrowed(_x)))) - } -} - +use std::ops::{Add, Div, Mul, Rem, Sub}; +use std::borrow::Cow; +use std::fmt::{Debug, Display}; +use serde::Serialize; + +pub trait InputValue: ToOwned + Debug + Display + PartialEq + PartialOrd + Serialize {} + +impl InputValue for i8 {} +impl InputValue for i16 {} +impl InputValue for i32 {} +impl InputValue for i64 {} +impl InputValue for i128 {} +impl InputValue for isize {} + +impl InputValue for u8 {} +impl InputValue for u16 {} +impl InputValue for u32 {} +impl InputValue for u64 {} +impl InputValue for u128 {} +impl InputValue for usize {} + +impl InputValue for f32 {} +impl InputValue for f64 {} + +impl InputValue for bool {} + +impl InputValue for Box {} +impl InputValue for String {} +impl<'a> InputValue for Cow<'a, str> {} +impl InputValue for &'_ str {} +impl InputValue for &&'_ str {} + +pub trait NumberValue: Default + InputValue + Copy + Add + Sub + Mul + Div + Rem {} + +impl NumberValue for i8 {} +impl NumberValue for i16 {} +impl NumberValue for i32 {} +impl NumberValue for i64 {} +impl NumberValue for i128 {} +impl NumberValue for isize {} + +impl NumberValue for u8 {} +impl NumberValue for u16 {} +impl NumberValue for u32 {} +impl NumberValue for u64 {} +impl NumberValue for u128 {} +impl NumberValue for usize {} + +impl NumberValue for f32 {} +impl NumberValue for f64 {} + +#[derive(PartialEq, Debug, Clone, Copy)] +pub enum ConstraintViolation { + CustomError, + PatternMismatch, + RangeOverflow, + RangeUnderflow, + StepMismatch, + TooLong, + TooShort, + NotEqual, + + /// Used to convey an expected string format (not necessarily a `Pattern` format; + /// E.g., invalid email hostname in email pattern, etc.). + TypeMismatch, + ValueMissing, +} + +pub type ViolationMessage = String; + +pub type ValidationError = (ConstraintViolation, ViolationMessage); + +pub type ValidationResult = Result<(), Vec>; + +pub type Filter = dyn Fn(Option) -> Option + Send + Sync; + +pub type Validator = dyn Fn(T) -> ValidationResult + Send + Sync; + +pub trait ValidateValue { + fn validate(&self, value: T) -> ValidationResult; +} + +pub trait FilterValue { + fn filter(&self, value: Option>) -> Option>; +} + +pub trait ToAttributesList { + fn to_attributes_list(&self) -> Option> { + None + } +} + +pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + PartialEq + PartialOrd + Serialize + ?Sized>: Display + Debug + 'a { + fn get_should_break_on_failure(&self) -> bool; + fn get_required(&self) -> bool; + fn get_name(&self) -> Option>; + fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self) -> ViolationMessage + Send + Sync); + fn get_validators(&self) -> Option<&[&'a Validator<&'call_ctx T>]>; + fn get_filters(&self) -> Option<&[&'a Filter>]>; + + fn validate_with_validators(&self, value: &'call_ctx T, validators: Option<&[&'a Validator<&'call_ctx T>]>) -> ValidationResult { + validators.map(|vs| { + + // If not break on failure then capture all validation errors. + if !self.get_should_break_on_failure() { + return vs.iter().fold( + Vec::::new(), + |mut agg, f| match (f)(value) { + Err(mut message_tuples) => { + agg.append(message_tuples.as_mut()); + agg + } + _ => agg, + }); + } + + // Else break on, and capture, first failure. + let mut agg = Vec::::new(); + for f in vs.iter() { + if let Err(mut message_tuples) = (f)(value) { + agg.append(message_tuples.as_mut()); + break; + } + } + agg + }) + .and_then(|messages| if messages.is_empty() { None } else { Some(messages) }) + .map_or(Ok(()), Err) + } + + fn validate(&self, value: Option<&'call_ctx T>) -> ValidationResult { + match value { + None => { + if self.get_required() { + Err(vec![( + ConstraintViolation::ValueMissing, + (self.get_value_missing_handler())(self), + )]) + } else { + Ok(()) + } + } + Some(v) => self.validate_with_validators(v, self.get_validators()), + } + } + + /// Special case of `validate` where the error type enums are ignored (in `Err(...)`) result, + /// and only the error messages are returned. + /// + /// ```rust + /// use walrs_inputfilter::*; + /// + /// let input = StrInputBuilder::default() + /// .required(true) + /// .value_missing(&|_| "Value missing".to_string()) + /// .validators(vec![&|x: &str| { + /// if x.len() < 3 { + /// return Err(vec![( + /// ConstraintViolation::TooShort, + /// "Too short".to_string(), + /// )]); + /// } + /// Ok(()) + /// }]) + /// .build() + /// .unwrap() + /// ; + /// + /// assert_eq!(input.validate1(Some(&"ab")), Err(vec!["Too short".to_string()])); + /// assert_eq!(input.validate1(None), Err(vec!["Value missing".to_string()])); + /// ``` + fn validate1(&self, value: Option<&'call_ctx T>) -> Result<(), Vec> { + match self.validate(value) { + Err(messages) => + Err(messages.into_iter().map(|(_, message)| message).collect()), + Ok(_) => Ok(()), + } + } + + fn filter(&self, value: Option>) -> Option> { + match self.get_filters() { + None => value, + Some(fs) => fs.iter().fold(value, |agg, f| (f)(agg)), + } + } + + fn validate_and_filter(&self, x: Option<&'call_ctx T>) -> Result>, Vec> { + self.validate(x).map(|_| self.filter(x.map(|_x| Cow::Borrowed(_x)))) + } + + /// Special case of `validate_and_filter` where the error type enums are ignored (in `Err(...)`) result, + /// and only the error messages are returned, for `Err` case. + /// + /// ```rust + /// use walrs_inputfilter::*; + /// use std::borrow::Cow; + /// + /// let input = StrInputBuilder::default() + /// .required(true) + /// .value_missing(&|_| "Value missing".to_string()) + /// .validators(vec![&|x: &str| { + /// if x.len() < 3 { + /// return Err(vec![( + /// ConstraintViolation::TooShort, + /// "Too short".to_string(), + /// )]); + /// } + /// Ok(()) + /// }]) + /// .filters(vec![&|xs: Option>| { + /// xs.map(|xs| Cow::Owned(xs.to_lowercase())) + /// }]) + /// .build() + /// .unwrap() + /// ; + /// + /// assert_eq!(input.validate_and_filter1(Some(&"ab")), Err(vec!["Too short".to_string()])); + /// assert_eq!(input.validate_and_filter1(Some(&"Abba")), Ok(Some("Abba".to_lowercase().into()))); + /// assert_eq!(input.validate_and_filter1(None), Err(vec!["Value missing".to_string()])); + /// ``` + /// + fn validate_and_filter1(&self, x: Option<&'call_ctx T>) -> Result>, Vec> { + match self.validate_and_filter(x) { + Err(messages) => + Err(messages.into_iter().map(|(_, message)| message).collect()), + Ok(filtered) => Ok(filtered), + } + } + +} + + From 13be699e5aed6a7dc6b66bb0273f14ce644722d9 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Thu, 23 Nov 2023 21:22:24 -0500 Subject: [PATCH 03/38] Updated line endings. --- inputfilter/src/filter/slug.rs | 380 +++++----- inputfilter/src/filter/strip_tags.rs | 332 ++++----- inputfilter/src/filter/xml_entities.rs | 272 +++---- inputfilter/src/input.rs | 938 ++++++++++++------------- inputfilter/src/lib.rs | 40 +- inputfilter/src/string_input.rs | 820 ++++++++++----------- src/lib.rs | 16 +- 7 files changed, 1399 insertions(+), 1399 deletions(-) diff --git a/inputfilter/src/filter/slug.rs b/inputfilter/src/filter/slug.rs index 171a342..e349286 100644 --- a/inputfilter/src/filter/slug.rs +++ b/inputfilter/src/filter/slug.rs @@ -1,190 +1,190 @@ -use std::borrow::Cow; -use std::sync::OnceLock; -use regex::Regex; - -static SLUG_FILTER_REGEX: OnceLock = OnceLock::new(); -static SLUG_FILTER_REGEX_STR: &str = r"(?i)[^\w\-]"; -static DASH_FILTER_REGEX: OnceLock = OnceLock::new(); -static DASH_FILTER_REGEX_STR: &str = r"(?i)\-{2,}"; - -/// Returns the static regex used for filtering a string to slug. -pub fn get_slug_filter_regex() -> &'static Regex { - SLUG_FILTER_REGEX.get_or_init(|| Regex::new(SLUG_FILTER_REGEX_STR).unwrap()) -} - -/// Returns the static regex used for filtering out multiple dashes for one dash. -pub fn get_dash_filter_regex() -> &'static Regex { - DASH_FILTER_REGEX.get_or_init(|| Regex::new(DASH_FILTER_REGEX_STR).unwrap()) -} - -/// Normalizes given string into a slug - e.g., a string matching /^\w[\w\-]{0,198}\w?$/ -/// -/// ```rust -/// use std::borrow::Cow; -/// use walrs_inputfilter::filter::slug::to_slug; -/// -/// assert_eq!(to_slug(Cow::Borrowed("Hello World")), "hello-world"); -/// ``` -pub fn to_slug(xs: Cow) -> Cow { - _to_slug(get_slug_filter_regex(), 200, xs) -} - -/// Same as `to_slug` method but removes duplicate '-' symbols. -/// -/// ```rust -/// use std::borrow::Cow; -/// use walrs_inputfilter::filter::slug::to_pretty_slug; -/// -/// assert_eq!(to_pretty_slug(Cow::Borrowed("%$Hello@#$@#!(World$$")), "hello-world"); -/// ``` -pub fn to_pretty_slug(xs: Cow) -> Cow { - _to_pretty_slug(get_slug_filter_regex(), 200, xs) -} - -fn _to_slug<'a>(pattern: &Regex, max_length: usize, xs: Cow<'a, str>) -> Cow<'a, str> { - let rslt = pattern.replace_all(xs.as_ref(), "-") - .to_lowercase() - .trim_matches('-') - .to_string(); - - if rslt.len() > max_length { - Cow::Owned(rslt[..max_length + 1].to_string()) - } else { - Cow::Owned(rslt) - } -} - -fn _to_pretty_slug<'a>(pattern: &Regex, max_length: usize, xs: Cow<'a, str>) -> Cow<'a, str> { - if xs.is_empty() { return xs; } - - get_dash_filter_regex() - .replace_all(&_to_slug(pattern, max_length, xs), "-") - .to_string() - .into() -} - -/// Configurable version of `to_slug()` - allows for setting the max_length. -#[derive(Clone, Debug, Default, Builder)] -pub struct SlugFilter { - #[builder(setter(into), default = "200")] - pub max_length: usize, - - #[builder(setter(into), default = "true")] - pub allow_duplicate_dashes: bool, -} - -impl SlugFilter { - pub fn new(max_length: usize, allow_duplicate_dashes: bool) -> Self { - SlugFilter { - max_length, - allow_duplicate_dashes, - } - } - - pub fn filter<'a>(&self, xs: Cow<'a, str>) -> Cow<'a, str> { - if self.allow_duplicate_dashes { - _to_slug(get_slug_filter_regex(), self.max_length, xs) - } else { - _to_pretty_slug(get_slug_filter_regex(), self.max_length, xs) - } - } -} - -impl<'a> FnOnce<(Cow<'a, str>, )> for SlugFilter { - type Output = Cow<'a, str>; - - extern "rust-call" fn call_once(self, args: (Cow<'a, str>, )) -> Self::Output { - self.filter(args.0) - } -} - -impl<'a> Fn<(Cow<'a, str>, )> for SlugFilter { - extern "rust-call" fn call(&self, args: (Cow<'a, str>, )) -> Self::Output { - self.filter(args.0) - } -} - -impl<'a> FnMut<(Cow<'a, str>, )> for SlugFilter { - extern "rust-call" fn call_mut(&mut self, args: (Cow<'a, str>, )) -> Self::Output { - self.filter(args.0) - } -} - -#[cfg(test)] -mod test { - use std::{borrow::Cow, thread}; - use super::*; - - #[test] - fn test_to_slug_standalone_method() { - for (cow_str, expected) in vec![ - (Cow::Borrowed("Hello World"), "hello-world"), - (Cow::Borrowed("#$@#$Hello World$@#$"), "hello-world"), - (Cow::Borrowed("$Hello'\"@$World$"), "hello----world"), - ] { - assert_eq!(to_slug(cow_str), expected); - } - } - - #[test] - fn test_to_pretty_slug_standalone_method() { - for (cow_str, expected) in vec![ - (Cow::Borrowed("Hello World"), "hello-world"), - (Cow::Borrowed("$Hello World$"), "hello-world"), - (Cow::Borrowed("$Hello'\"@$World$"), "hello-world"), - ] { - assert_eq!(to_pretty_slug(cow_str), expected); - } - } - - #[test] - fn test_slug_filter_constructor() { - for x in vec![0, 1, 2] { - let instance = SlugFilter::new(x, false); - assert_eq!(instance.max_length, x); - assert_eq!(instance.allow_duplicate_dashes, false); - } - } - - #[test] - fn test_slug_filter_builder() { - let instance = SlugFilterBuilder::default().build().unwrap(); - assert_eq!(instance.max_length, 200); - assert_eq!(instance.allow_duplicate_dashes, true); - } - - #[test] - fn test_fn_trait_impls() { - let slug_filter = SlugFilter { max_length: 200, allow_duplicate_dashes: true }; - - assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello-world"); - assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello---world"); - assert_eq!(slug_filter(Cow::Borrowed("$@#$Hello @World@#$@#$")), "hello----world"); - } - - #[test] - fn test_standalone_methods_in_threaded_contexts() { - thread::scope(|scope| { - scope.spawn(move ||{ - assert_eq!(to_slug(Cow::Borrowed("Hello World")), "hello-world"); - assert_eq!(to_slug(Cow::Borrowed("Hello World")), "hello---world"); - assert_eq!(to_pretty_slug(Cow::Borrowed("$@#$Hello@#$@#$World@#$@#$")), "hello-world"); - }); - }); - } - - #[test] - fn test_struct_in_threaded_contexts() { - let slug_filter = SlugFilterBuilder::default() - .allow_duplicate_dashes(false) - .build() - .unwrap(); - - thread::scope(|scope| { - scope.spawn(move ||{ - assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello-world"); - assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello-world"); - }); - }); - } -} +use std::borrow::Cow; +use std::sync::OnceLock; +use regex::Regex; + +static SLUG_FILTER_REGEX: OnceLock = OnceLock::new(); +static SLUG_FILTER_REGEX_STR: &str = r"(?i)[^\w\-]"; +static DASH_FILTER_REGEX: OnceLock = OnceLock::new(); +static DASH_FILTER_REGEX_STR: &str = r"(?i)\-{2,}"; + +/// Returns the static regex used for filtering a string to slug. +pub fn get_slug_filter_regex() -> &'static Regex { + SLUG_FILTER_REGEX.get_or_init(|| Regex::new(SLUG_FILTER_REGEX_STR).unwrap()) +} + +/// Returns the static regex used for filtering out multiple dashes for one dash. +pub fn get_dash_filter_regex() -> &'static Regex { + DASH_FILTER_REGEX.get_or_init(|| Regex::new(DASH_FILTER_REGEX_STR).unwrap()) +} + +/// Normalizes given string into a slug - e.g., a string matching /^\w[\w\-]{0,198}\w?$/ +/// +/// ```rust +/// use std::borrow::Cow; +/// use walrs_inputfilter::filter::slug::to_slug; +/// +/// assert_eq!(to_slug(Cow::Borrowed("Hello World")), "hello-world"); +/// ``` +pub fn to_slug(xs: Cow) -> Cow { + _to_slug(get_slug_filter_regex(), 200, xs) +} + +/// Same as `to_slug` method but removes duplicate '-' symbols. +/// +/// ```rust +/// use std::borrow::Cow; +/// use walrs_inputfilter::filter::slug::to_pretty_slug; +/// +/// assert_eq!(to_pretty_slug(Cow::Borrowed("%$Hello@#$@#!(World$$")), "hello-world"); +/// ``` +pub fn to_pretty_slug(xs: Cow) -> Cow { + _to_pretty_slug(get_slug_filter_regex(), 200, xs) +} + +fn _to_slug<'a>(pattern: &Regex, max_length: usize, xs: Cow<'a, str>) -> Cow<'a, str> { + let rslt = pattern.replace_all(xs.as_ref(), "-") + .to_lowercase() + .trim_matches('-') + .to_string(); + + if rslt.len() > max_length { + Cow::Owned(rslt[..max_length + 1].to_string()) + } else { + Cow::Owned(rslt) + } +} + +fn _to_pretty_slug<'a>(pattern: &Regex, max_length: usize, xs: Cow<'a, str>) -> Cow<'a, str> { + if xs.is_empty() { return xs; } + + get_dash_filter_regex() + .replace_all(&_to_slug(pattern, max_length, xs), "-") + .to_string() + .into() +} + +/// Configurable version of `to_slug()` - allows for setting the max_length. +#[derive(Clone, Debug, Default, Builder)] +pub struct SlugFilter { + #[builder(setter(into), default = "200")] + pub max_length: usize, + + #[builder(setter(into), default = "true")] + pub allow_duplicate_dashes: bool, +} + +impl SlugFilter { + pub fn new(max_length: usize, allow_duplicate_dashes: bool) -> Self { + SlugFilter { + max_length, + allow_duplicate_dashes, + } + } + + pub fn filter<'a>(&self, xs: Cow<'a, str>) -> Cow<'a, str> { + if self.allow_duplicate_dashes { + _to_slug(get_slug_filter_regex(), self.max_length, xs) + } else { + _to_pretty_slug(get_slug_filter_regex(), self.max_length, xs) + } + } +} + +impl<'a> FnOnce<(Cow<'a, str>, )> for SlugFilter { + type Output = Cow<'a, str>; + + extern "rust-call" fn call_once(self, args: (Cow<'a, str>, )) -> Self::Output { + self.filter(args.0) + } +} + +impl<'a> Fn<(Cow<'a, str>, )> for SlugFilter { + extern "rust-call" fn call(&self, args: (Cow<'a, str>, )) -> Self::Output { + self.filter(args.0) + } +} + +impl<'a> FnMut<(Cow<'a, str>, )> for SlugFilter { + extern "rust-call" fn call_mut(&mut self, args: (Cow<'a, str>, )) -> Self::Output { + self.filter(args.0) + } +} + +#[cfg(test)] +mod test { + use std::{borrow::Cow, thread}; + use super::*; + + #[test] + fn test_to_slug_standalone_method() { + for (cow_str, expected) in vec![ + (Cow::Borrowed("Hello World"), "hello-world"), + (Cow::Borrowed("#$@#$Hello World$@#$"), "hello-world"), + (Cow::Borrowed("$Hello'\"@$World$"), "hello----world"), + ] { + assert_eq!(to_slug(cow_str), expected); + } + } + + #[test] + fn test_to_pretty_slug_standalone_method() { + for (cow_str, expected) in vec![ + (Cow::Borrowed("Hello World"), "hello-world"), + (Cow::Borrowed("$Hello World$"), "hello-world"), + (Cow::Borrowed("$Hello'\"@$World$"), "hello-world"), + ] { + assert_eq!(to_pretty_slug(cow_str), expected); + } + } + + #[test] + fn test_slug_filter_constructor() { + for x in vec![0, 1, 2] { + let instance = SlugFilter::new(x, false); + assert_eq!(instance.max_length, x); + assert_eq!(instance.allow_duplicate_dashes, false); + } + } + + #[test] + fn test_slug_filter_builder() { + let instance = SlugFilterBuilder::default().build().unwrap(); + assert_eq!(instance.max_length, 200); + assert_eq!(instance.allow_duplicate_dashes, true); + } + + #[test] + fn test_fn_trait_impls() { + let slug_filter = SlugFilter { max_length: 200, allow_duplicate_dashes: true }; + + assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello-world"); + assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello---world"); + assert_eq!(slug_filter(Cow::Borrowed("$@#$Hello @World@#$@#$")), "hello----world"); + } + + #[test] + fn test_standalone_methods_in_threaded_contexts() { + thread::scope(|scope| { + scope.spawn(move ||{ + assert_eq!(to_slug(Cow::Borrowed("Hello World")), "hello-world"); + assert_eq!(to_slug(Cow::Borrowed("Hello World")), "hello---world"); + assert_eq!(to_pretty_slug(Cow::Borrowed("$@#$Hello@#$@#$World@#$@#$")), "hello-world"); + }); + }); + } + + #[test] + fn test_struct_in_threaded_contexts() { + let slug_filter = SlugFilterBuilder::default() + .allow_duplicate_dashes(false) + .build() + .unwrap(); + + thread::scope(|scope| { + scope.spawn(move ||{ + assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello-world"); + assert_eq!(slug_filter(Cow::Borrowed("Hello World")), "hello-world"); + }); + }); + } +} diff --git a/inputfilter/src/filter/strip_tags.rs b/inputfilter/src/filter/strip_tags.rs index 7232b42..2e14fd2 100644 --- a/inputfilter/src/filter/strip_tags.rs +++ b/inputfilter/src/filter/strip_tags.rs @@ -1,166 +1,166 @@ -use std::borrow::Cow; -use std::sync::OnceLock; -use ammonia; - -static DEFAULT_AMMONIA_BUILDER: OnceLock = OnceLock::new(); - -/// Sanitizes incoming HTML using the [Ammonia](https://docs.rs/ammonia/1.0.0/ammonia/) crate. -/// -/// ```rust -/// use walrs_inputfilter::filter::StripTags; -/// use std::borrow::Cow; -/// -/// let filter = StripTags::new(); -/// -/// for (i, (incoming_src, expected_src)) in [ -/// ("", ""), -/// ("Socrates'", "Socrates'"), -/// ("\"Hello\"", "\"Hello\""), -/// ("Hello", "Hello"), -/// ("", ""), // Removes `script` tags, by default -/// ("

The quick brown fox

", -/// "

The quick brown fox

"), // Removes `style` tags, by default -/// ("

The quick brown fox", "

The quick brown fox

") // Fixes erroneous markup, by default -/// ] -/// .into_iter().enumerate() { -/// println!("Filter test {}: filter({}) == {}", i, incoming_src, expected_src); -/// let result = filter.filter(incoming_src.into()); -/// -/// assert_eq!(result, expected_src.to_string()); -/// assert_eq!(filter(incoming_src.into()), result); -/// } -/// ``` -/// -pub struct StripTags<'a> { - /// Ammonia builder used to sanitize incoming HTML. - /// - /// If `None`, a default builder is used when `filter`/instance is called. - pub ammonia: Option>, -} - -impl<'a> StripTags<'a> { - /// Constructs a new `StripTags` instance. - pub fn new() -> Self { - Self { - ammonia: None, - } - } - - /// Filters incoming HTML using the contained `ammonia::Builder` instance. - /// If no instance is set gets/(and/or) initializes a new (default, and singleton) instance. - /// - /// ```rust - /// use std::borrow::Cow; - /// use std::sync::OnceLock; - /// use ammonia::Builder as AmmoniaBuilder; - /// use walrs_inputfilter::filter::StripTags; - /// - /// // Using default settings: - /// let filter = StripTags::new(); - /// - /// let subject = r#"

Hello

- /// "#; - /// - /// // Ammonia removes `script`, and `style` tags by default. - /// assert_eq!(filter.filter(subject.into()).trim(), - /// "

Hello

" - /// ); - /// - /// // Using custom settings: - /// // Instantiate a custom sanitizer instance. - /// let mut sanitizer = AmmoniaBuilder::default(); - /// let additional_allowed_tags = vec!["style"]; - /// - /// sanitizer - /// .add_tags(&additional_allowed_tags) // Add 'style' tag to "tags-whitelist" - /// - /// // Remove 'style' tag from "tags-blacklist" - /// .rm_clean_content_tags(&additional_allowed_tags); - /// - /// let filter = StripTags { - /// ammonia: Some(sanitizer) - /// }; - /// - /// // Notice `style` tags are no longer removed. - /// assert_eq!(filter.filter( - /// "".into() - /// ), - /// "" - /// ); - /// ``` - /// - pub fn filter<'b>(&self, input: Cow<'b, str>) -> Cow<'b, str> { - match self.ammonia { - None => Cow::Owned( - DEFAULT_AMMONIA_BUILDER.get_or_init(ammonia::Builder::default) - .clean(&input).to_string() - ), - Some(ref sanitizer) => Cow::Owned( - sanitizer.clean(&input).to_string() - ), - } - } -} - -impl<'a> Default for StripTags<'a> { - fn default() -> Self { - Self::new() - } -} - -impl<'a, 'b> FnOnce<(Cow<'b, str>, )> for StripTags<'a> { - type Output = Cow<'b, str>; - - extern "rust-call" fn call_once(self, args: (Cow<'b, str>, )) -> Self::Output { - self.filter(args.0) - } -} - -impl<'a, 'b> FnMut<(Cow<'b, str>, )> for StripTags<'a> { - extern "rust-call" fn call_mut(&mut self, args: (Cow<'b, str>, )) -> Self::Output { - self.filter(args.0) - } -} - -impl<'a, 'b> Fn<(Cow<'b, str>, )> for StripTags<'a> { - extern "rust-call" fn call(&self, args: (Cow<'b, str>, )) -> Self::Output { - self.filter(args.0) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_construction() { - let _ = StripTags::new(); - let _ = StripTags { - ammonia: Some(ammonia::Builder::default()), - }; - } - - #[test] - fn test_filter() { - let filter = StripTags::new(); - - for (i, (incoming_src, expected_src)) in [ - ("", ""), - ("Socrates'", "Socrates'"), - ("\"Hello\"", "\"Hello\""), - ("Hello", "Hello"), - ("", ""), // Removes `script` tags, by default - ("

The quick brown fox

", - "

The quick brown fox

"), // Removes `style` tags, by default - ("

The quick brown fox", "

The quick brown fox

") // Fixes erroneous markup - ] - .into_iter().enumerate() { - println!("Filter test {}: filter({}) == {}", i, incoming_src, expected_src); - - let result = filter.filter(incoming_src.into()); - - assert_eq!(result, expected_src.to_string()); - assert_eq!(filter(incoming_src.into()), result); - } - } -} +use std::borrow::Cow; +use std::sync::OnceLock; +use ammonia; + +static DEFAULT_AMMONIA_BUILDER: OnceLock = OnceLock::new(); + +/// Sanitizes incoming HTML using the [Ammonia](https://docs.rs/ammonia/1.0.0/ammonia/) crate. +/// +/// ```rust +/// use walrs_inputfilter::filter::StripTags; +/// use std::borrow::Cow; +/// +/// let filter = StripTags::new(); +/// +/// for (i, (incoming_src, expected_src)) in [ +/// ("", ""), +/// ("Socrates'", "Socrates'"), +/// ("\"Hello\"", "\"Hello\""), +/// ("Hello", "Hello"), +/// ("", ""), // Removes `script` tags, by default +/// ("

The quick brown fox

", +/// "

The quick brown fox

"), // Removes `style` tags, by default +/// ("

The quick brown fox", "

The quick brown fox

") // Fixes erroneous markup, by default +/// ] +/// .into_iter().enumerate() { +/// println!("Filter test {}: filter({}) == {}", i, incoming_src, expected_src); +/// let result = filter.filter(incoming_src.into()); +/// +/// assert_eq!(result, expected_src.to_string()); +/// assert_eq!(filter(incoming_src.into()), result); +/// } +/// ``` +/// +pub struct StripTags<'a> { + /// Ammonia builder used to sanitize incoming HTML. + /// + /// If `None`, a default builder is used when `filter`/instance is called. + pub ammonia: Option>, +} + +impl<'a> StripTags<'a> { + /// Constructs a new `StripTags` instance. + pub fn new() -> Self { + Self { + ammonia: None, + } + } + + /// Filters incoming HTML using the contained `ammonia::Builder` instance. + /// If no instance is set gets/(and/or) initializes a new (default, and singleton) instance. + /// + /// ```rust + /// use std::borrow::Cow; + /// use std::sync::OnceLock; + /// use ammonia::Builder as AmmoniaBuilder; + /// use walrs_inputfilter::filter::StripTags; + /// + /// // Using default settings: + /// let filter = StripTags::new(); + /// + /// let subject = r#"

Hello

+ /// "#; + /// + /// // Ammonia removes `script`, and `style` tags by default. + /// assert_eq!(filter.filter(subject.into()).trim(), + /// "

Hello

" + /// ); + /// + /// // Using custom settings: + /// // Instantiate a custom sanitizer instance. + /// let mut sanitizer = AmmoniaBuilder::default(); + /// let additional_allowed_tags = vec!["style"]; + /// + /// sanitizer + /// .add_tags(&additional_allowed_tags) // Add 'style' tag to "tags-whitelist" + /// + /// // Remove 'style' tag from "tags-blacklist" + /// .rm_clean_content_tags(&additional_allowed_tags); + /// + /// let filter = StripTags { + /// ammonia: Some(sanitizer) + /// }; + /// + /// // Notice `style` tags are no longer removed. + /// assert_eq!(filter.filter( + /// "".into() + /// ), + /// "" + /// ); + /// ``` + /// + pub fn filter<'b>(&self, input: Cow<'b, str>) -> Cow<'b, str> { + match self.ammonia { + None => Cow::Owned( + DEFAULT_AMMONIA_BUILDER.get_or_init(ammonia::Builder::default) + .clean(&input).to_string() + ), + Some(ref sanitizer) => Cow::Owned( + sanitizer.clean(&input).to_string() + ), + } + } +} + +impl<'a> Default for StripTags<'a> { + fn default() -> Self { + Self::new() + } +} + +impl<'a, 'b> FnOnce<(Cow<'b, str>, )> for StripTags<'a> { + type Output = Cow<'b, str>; + + extern "rust-call" fn call_once(self, args: (Cow<'b, str>, )) -> Self::Output { + self.filter(args.0) + } +} + +impl<'a, 'b> FnMut<(Cow<'b, str>, )> for StripTags<'a> { + extern "rust-call" fn call_mut(&mut self, args: (Cow<'b, str>, )) -> Self::Output { + self.filter(args.0) + } +} + +impl<'a, 'b> Fn<(Cow<'b, str>, )> for StripTags<'a> { + extern "rust-call" fn call(&self, args: (Cow<'b, str>, )) -> Self::Output { + self.filter(args.0) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_construction() { + let _ = StripTags::new(); + let _ = StripTags { + ammonia: Some(ammonia::Builder::default()), + }; + } + + #[test] + fn test_filter() { + let filter = StripTags::new(); + + for (i, (incoming_src, expected_src)) in [ + ("", ""), + ("Socrates'", "Socrates'"), + ("\"Hello\"", "\"Hello\""), + ("Hello", "Hello"), + ("", ""), // Removes `script` tags, by default + ("

The quick brown fox

", + "

The quick brown fox

"), // Removes `style` tags, by default + ("

The quick brown fox", "

The quick brown fox

") // Fixes erroneous markup + ] + .into_iter().enumerate() { + println!("Filter test {}: filter({}) == {}", i, incoming_src, expected_src); + + let result = filter.filter(incoming_src.into()); + + assert_eq!(result, expected_src.to_string()); + assert_eq!(filter(incoming_src.into()), result); + } + } +} diff --git a/inputfilter/src/filter/xml_entities.rs b/inputfilter/src/filter/xml_entities.rs index cf590d7..47f4bb1 100644 --- a/inputfilter/src/filter/xml_entities.rs +++ b/inputfilter/src/filter/xml_entities.rs @@ -1,136 +1,136 @@ -use std::borrow::Cow; -use std::collections::HashMap; -use std::sync::OnceLock; - -static DEFAULT_CHARS_ASSOC_MAP: OnceLock> = OnceLock::new(); - -/// Encodes >, <, &, ', and " as XML entities. -/// -/// Note: This filter does not skip already (XML) encoded characters; -/// E.g., the `&` in `&` will get encoded as well resulting in the value `&amp;`. -/// -/// @todo Update algorithm to skip over existing XML entity declarations, or use a third-party lib.; -/// E.g., ignore results like `&amp;` for string `&`, etc. -/// -/// ```rust -/// use walrs_inputfilter::filter::XmlEntitiesFilter; -/// -/// let filter = XmlEntitiesFilter::new(); -/// -/// for (incoming_src, expected_src) in [ -/// ("", ""), -/// ("Socrates'", "Socrates'"), -/// ("\"Hello\"", ""Hello""), -/// ("Hello", "Hello"), -/// ("S & P", "S & P"), -/// ("S & P", "S &amp; P"), -/// ("", "<script>alert('hello');</script>"), -/// ] { -/// assert_eq!(filter(incoming_src.into()), expected_src.to_string()); -/// } -/// ``` -pub struct XmlEntitiesFilter<'a> { - pub chars_assoc_map: &'a HashMap, -} - -impl<'a> XmlEntitiesFilter<'a> { - pub fn new() -> Self { - Self { - chars_assoc_map: DEFAULT_CHARS_ASSOC_MAP.get_or_init(|| { - let mut map = HashMap::new(); - map.insert('<', "<"); - map.insert('>', ">"); - map.insert('"', """); - map.insert('\'', "'"); - map.insert('&', "&"); - map - }) - } - } - - /// Uses contained character association map to encode characters matching contained characters as - /// xml entities. - /// - /// ```rust - /// use walrs_inputfilter::filter::XmlEntitiesFilter; - /// - /// let filter = XmlEntitiesFilter::new(); - /// - /// for (incoming_src, expected_src) in [ - /// ("", ""), - /// (" ", " "), - /// ("Socrates'", "Socrates'"), - /// ("\"Hello\"", ""Hello""), - /// ("Hello", "Hello"), - /// ("<", "<"), - /// (">", ">"), - /// ("&", "&"), - /// ("", "<script></script>"), - /// ] { - /// assert_eq!(filter.filter(incoming_src.into()), expected_src.to_string()); - /// } - ///``` - pub fn filter<'b>(&self, input: Cow<'b, str>) -> Cow<'b, str> { - let mut output = String::with_capacity(input.len()); - for c in input.chars() { - match self.chars_assoc_map.get(&c) { - Some(entity) => output.push_str(entity), - None => output.push(c), - } - } - - Cow::Owned(output.to_string()) - } -} - -impl<'a> Default for XmlEntitiesFilter<'a> { - fn default() -> Self { - Self::new() - } -} - -impl<'a, 'b> FnOnce<(Cow<'b, str>, )> for XmlEntitiesFilter<'a> { - type Output = Cow<'b, str>; - - extern "rust-call" fn call_once(self, args: (Cow<'b, str>, )) -> Self::Output { - self.filter(args.0) - } -} - -impl <'a, 'b> FnMut<(Cow<'b, str>, )> for XmlEntitiesFilter<'a> { - extern "rust-call" fn call_mut(&mut self, args: (Cow<'b, str>, )) -> Self::Output { - self.filter(args.0) - } -} - -impl <'a, 'b> Fn<(Cow<'b, str>, )> for XmlEntitiesFilter<'a> { - extern "rust-call" fn call(&self, args: (Cow<'b, str>, )) -> Self::Output { - self.filter(args.0) - } -} - -#[cfg(test)] -mod test { - #[test] - fn test_construction() { - let _ = super::XmlEntitiesFilter::new(); - } - - #[test] - fn test_filter() { - let filter = super::XmlEntitiesFilter::new(); - - for (incoming_src, expected_src) in [ - ("", ""), - ("Socrates'", "Socrates'"), - ("\"Hello\"", ""Hello""), - ("Hello", "Hello"), - ("<", "<"), - (">", ">"), - ("&", "&"), - ("", "<script>alert('hello');</script>"), - ] { - assert_eq!(filter(incoming_src.into()), expected_src.to_string()); - } - } -} +use std::borrow::Cow; +use std::collections::HashMap; +use std::sync::OnceLock; + +static DEFAULT_CHARS_ASSOC_MAP: OnceLock> = OnceLock::new(); + +/// Encodes >, <, &, ', and " as XML entities. +/// +/// Note: This filter does not skip already (XML) encoded characters; +/// E.g., the `&` in `&` will get encoded as well resulting in the value `&amp;`. +/// +/// @todo Update algorithm to skip over existing XML entity declarations, or use a third-party lib.; +/// E.g., ignore results like `&amp;` for string `&`, etc. +/// +/// ```rust +/// use walrs_inputfilter::filter::XmlEntitiesFilter; +/// +/// let filter = XmlEntitiesFilter::new(); +/// +/// for (incoming_src, expected_src) in [ +/// ("", ""), +/// ("Socrates'", "Socrates'"), +/// ("\"Hello\"", ""Hello""), +/// ("Hello", "Hello"), +/// ("S & P", "S & P"), +/// ("S & P", "S &amp; P"), +/// ("", "<script>alert('hello');</script>"), +/// ] { +/// assert_eq!(filter(incoming_src.into()), expected_src.to_string()); +/// } +/// ``` +pub struct XmlEntitiesFilter<'a> { + pub chars_assoc_map: &'a HashMap, +} + +impl<'a> XmlEntitiesFilter<'a> { + pub fn new() -> Self { + Self { + chars_assoc_map: DEFAULT_CHARS_ASSOC_MAP.get_or_init(|| { + let mut map = HashMap::new(); + map.insert('<', "<"); + map.insert('>', ">"); + map.insert('"', """); + map.insert('\'', "'"); + map.insert('&', "&"); + map + }) + } + } + + /// Uses contained character association map to encode characters matching contained characters as + /// xml entities. + /// + /// ```rust + /// use walrs_inputfilter::filter::XmlEntitiesFilter; + /// + /// let filter = XmlEntitiesFilter::new(); + /// + /// for (incoming_src, expected_src) in [ + /// ("", ""), + /// (" ", " "), + /// ("Socrates'", "Socrates'"), + /// ("\"Hello\"", ""Hello""), + /// ("Hello", "Hello"), + /// ("<", "<"), + /// (">", ">"), + /// ("&", "&"), + /// ("", "<script></script>"), + /// ] { + /// assert_eq!(filter.filter(incoming_src.into()), expected_src.to_string()); + /// } + ///``` + pub fn filter<'b>(&self, input: Cow<'b, str>) -> Cow<'b, str> { + let mut output = String::with_capacity(input.len()); + for c in input.chars() { + match self.chars_assoc_map.get(&c) { + Some(entity) => output.push_str(entity), + None => output.push(c), + } + } + + Cow::Owned(output.to_string()) + } +} + +impl<'a> Default for XmlEntitiesFilter<'a> { + fn default() -> Self { + Self::new() + } +} + +impl<'a, 'b> FnOnce<(Cow<'b, str>, )> for XmlEntitiesFilter<'a> { + type Output = Cow<'b, str>; + + extern "rust-call" fn call_once(self, args: (Cow<'b, str>, )) -> Self::Output { + self.filter(args.0) + } +} + +impl <'a, 'b> FnMut<(Cow<'b, str>, )> for XmlEntitiesFilter<'a> { + extern "rust-call" fn call_mut(&mut self, args: (Cow<'b, str>, )) -> Self::Output { + self.filter(args.0) + } +} + +impl <'a, 'b> Fn<(Cow<'b, str>, )> for XmlEntitiesFilter<'a> { + extern "rust-call" fn call(&self, args: (Cow<'b, str>, )) -> Self::Output { + self.filter(args.0) + } +} + +#[cfg(test)] +mod test { + #[test] + fn test_construction() { + let _ = super::XmlEntitiesFilter::new(); + } + + #[test] + fn test_filter() { + let filter = super::XmlEntitiesFilter::new(); + + for (incoming_src, expected_src) in [ + ("", ""), + ("Socrates'", "Socrates'"), + ("\"Hello\"", ""Hello""), + ("Hello", "Hello"), + ("<", "<"), + (">", ">"), + ("&", "&"), + ("", "<script>alert('hello');</script>"), + ] { + assert_eq!(filter(incoming_src.into()), expected_src.to_string()); + } + } +} diff --git a/inputfilter/src/input.rs b/inputfilter/src/input.rs index 773a252..35fef6c 100644 --- a/inputfilter/src/input.rs +++ b/inputfilter/src/input.rs @@ -1,469 +1,469 @@ -use std::borrow::Cow; -use std::fmt::{Debug, Display, Formatter}; - -use crate::types::{Filter, InputConstraints, InputValue, Validator, ViolationMessage}; - -pub type ValueMissingViolationCallback = - dyn Fn(&Input) -> ViolationMessage + Send + Sync; - -#[derive(Builder, Clone)] -#[builder(pattern = "owned")] -pub struct Input<'a, 'b, T> -where - T: InputValue, -{ - #[builder(default = "true")] - pub break_on_failure: bool, - - /// @todo This should be an `Option>`, for compatibility. - #[builder(setter(into), default = "None")] - pub name: Option<&'a str>, - - #[builder(default = "false")] - pub required: bool, - - #[builder(setter(strip_option), default = "None")] - pub validators: Option>>, - - #[builder(setter(strip_option), default = "None")] - pub filters: Option>>>, - - #[builder(default = "&value_missing_msg")] - pub value_missing: &'a (dyn Fn(&Input<'a, 'b, T>) -> ViolationMessage + Send + Sync), - - // @todo Add support for `io_validators` (e.g., validators that return futures). -} - -impl<'a, 'b, T> Input<'a, 'b, T> -where - T: InputValue, -{ - pub fn new(name: Option<&'a str>) -> Self { - Input { - break_on_failure: false, - name, - required: false, - validators: None, - filters: None, - value_missing: &value_missing_msg, - } - } -} - -impl<'a, 'b, T: InputValue> InputConstraints<'a, 'b, T> for Input<'a, 'b, T> { - fn get_should_break_on_failure(&self) -> bool { - self.break_on_failure - } - - fn get_required(&self) -> bool { - self.required - } - - fn get_name(&self) -> Option> { - self.name.map(move |s: &'a str| Cow::Borrowed(s)) - } - - fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self) -> ViolationMessage + Send + Sync) { - self.value_missing - } - - fn get_validators(&self) -> Option<&[&'a Validator<&'b T>]> { - self.validators.as_deref() - } - - fn get_filters(&self) -> Option<&[&'a Filter>]> { - self.filters.as_deref() - } -} - -impl Default for Input<'_, '_, T> { - fn default() -> Self { - Self::new(None) - } -} - -impl Display for Input<'_, '_, T> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Input {{ name: {}, required: {}, validators: {}, filters: {} }}", - self.name.unwrap_or("None"), - self.required, - self.validators.as_deref().map(|vs| - format!("Some([Validator; {}])", vs.len()) - ).unwrap_or("None".to_string()), - self.filters.as_deref().map(|fs| - format!("Some([Filter; {}])", fs.len()) - ).unwrap_or("None".to_string()), - ) - } -} - -impl Debug for Input<'_, '_, T> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", &self) - } -} - -pub fn value_missing_msg(_: &Input) -> String { - "Value is missing.".to_string() -} - -#[cfg(test)] -mod test { - use regex::Regex; - use std::{borrow::Cow, error::Error, sync::Arc, thread}; - - use crate::types::{ConstraintViolation, ConstraintViolation::{PatternMismatch, RangeOverflow}, - ValidationResult, InputConstraints}; - use crate::input::{InputBuilder}; - use crate::number::range_overflow_msg; - use crate::validator::number::{NumberValidatorBuilder, step_mismatch_msg}; - use crate::validator::pattern::PatternValidator; - - // Tests setup types - fn unsized_less_than_100_msg(value: usize) -> String { - format!("{} is greater than 100", value) - } - - fn ymd_mismatch_msg(s: &str, pattern_str: &str) -> String { - format!("{} doesn't match pattern {}", s, pattern_str) - } - - fn unsized_less_100(x: &usize) -> ValidationResult { - if *x >= 100 { - return Err(vec![( - RangeOverflow, - unsized_less_than_100_msg(*x) - )]); - } - Ok(()) - } - - fn times_two(x: Option>) -> Option> { - x.map(|_x| Cow::Owned(*_x * 2)) - } - - #[test] - fn test_input_builder() -> Result<(), Box> { - // Simplified ISO year-month-date regex - let ymd_regex = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; - let ymd_regex_2 = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; - let ymd_regex_arc_orig = Arc::new(ymd_regex); - let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); - - let ymd_mismatch_msg = Arc::new(move |s: &str| -> String { - format!("{} doesn't match pattern {}", s, ymd_regex_arc.as_str()) - }); - - let ymd_mismatch_msg_arc = Arc::clone(&ymd_mismatch_msg); - let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); - - let ymd_check = move |s: &&str| -> ValidationResult { - if !ymd_regex_arc.is_match(s) { - return Err(vec![(PatternMismatch, ymd_mismatch_msg_arc(s))]); - } - Ok(()) - }; - - // Validator case 1 - let pattern_validator = PatternValidator { - pattern: Cow::Owned(ymd_regex_2), - pattern_mismatch: &|validator, s| { - format!("{} doesn't match pattern {}", s, validator.pattern.as_str()) - }, - }; - - let less_than_100_input = InputBuilder::::default() - .validators(vec![&unsized_less_100]) - .build()?; - - let yyyy_mm_dd_input = InputBuilder::<&str>::default() - .validators(vec![&ymd_check]) - .build()?; - - let even_0_to_100 = NumberValidatorBuilder::::default() - .min(0) - .max(100) - .step(2) - .build()?; - - let even_from_0_to_100_input = InputBuilder::::default() - .name("even-0-to-100") - .validators(vec![&even_0_to_100]) - .build()?; - - let yyyy_mm_dd_input2 = InputBuilder::<&str>::default() - .validators(vec![&pattern_validator]) - .build()?; - - // Missing value check - match less_than_100_input.validate(None) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), - } - - // `Rem` (Remainder) trait check - match even_from_0_to_100_input.validate(Some(&3)) { - Err(errs) => errs.iter().for_each(|v_err| { - assert_eq!(v_err.0, ConstraintViolation::StepMismatch); - assert_eq!(v_err.1, step_mismatch_msg(&even_0_to_100, 3)); - }), - _ => panic!("Expected Err(...); Received Ok(())") - } - - // Mismatch check - let value = "1000-99-999"; - match yyyy_mm_dd_input.validate(Some(&value)) { - Ok(_) => panic!("Expected Err(...); Received Ok(())"), - Err(tuples) => { - assert_eq!(tuples[0].0, PatternMismatch); - assert_eq!(tuples[0].1, ymd_mismatch_msg(value).as_str()); - } - } - - // Valid check - match yyyy_mm_dd_input.validate(None) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), - } - - // Valid check 2 - let value = "1000-99-99"; - match yyyy_mm_dd_input.validate(Some(&value)) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), - } - - // Valid check - let value = "1000-99-99"; - match yyyy_mm_dd_input2.validate(Some(&value)) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), - } - - Ok(()) - } - - // #[test] - // fn test_input_exotic_value_types() -> Result<(), Box> { - // todo!("Input control exotic `InputValue` test cases.") - // } - - #[test] - fn test_thread_safety() -> Result<(), Box> { - fn ymd_mismatch_msg(s: &str, pattern_str: &str) -> String { - format!("{} doesn't match pattern {}", s, pattern_str) - } - - fn ymd_check(s: &&str) -> ValidationResult { - // Simplified ISO year-month-date regex - let rx = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap(); - if !rx.is_match(s) { - return Err(vec![(PatternMismatch, ymd_mismatch_msg(s, rx.as_str()))]); - } - Ok(()) - } - - let less_than_100_input = InputBuilder::::default() - .validators(vec![&unsized_less_100]) - .build()?; - - let ymd_input = InputBuilder::<&str>::default() - .validators(vec![&ymd_check]) - .build()?; - - let usize_input = Arc::new(less_than_100_input); - let usize_input_instance = Arc::clone(&usize_input); - - let str_input = Arc::new(ymd_input); - let str_input_instance = Arc::clone(&str_input); - - let handle = - thread::spawn( - move || match usize_input_instance.validate(Some(&101)) { - Err(x) => { - assert_eq!(x[0].1.as_str(), unsized_less_than_100_msg(101)); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - let handle2 = - thread::spawn( - move || match str_input_instance.validate(Some(&"")) { - Err(x) => { - assert_eq!( - x[0].1.as_str(), - ymd_mismatch_msg( - "", - Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap().as_str() - ) - ); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - // @note Conclusion of tests here is that validators can only (easily) be shared between threads if they are function pointers - - // closures are too loose and require over the top value management and planning due to the nature of multi-threaded - // contexts. - - // Contrary to the above, 'scoped threads', will allow variable sharing without requiring them to - // be 'moved' first (as long as rust's lifetime rules are followed - - // @see https://blog.logrocket.com/using-rust-scoped-threads-improve-efficiency-safety/ - // ). - - handle.join().unwrap(); - handle2.join().unwrap(); - - Ok(()) - } - - /// Example showing shared references in `Input`, and user-land, controls. - #[test] - fn test_thread_safety_with_scoped_threads_and_closures() -> Result<(), Box> { - let ymd_rx = Arc::new(Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap()); - let ymd_rx_clone = Arc::clone(&ymd_rx); - - let ymd_check = move |s: &&str| -> ValidationResult { - // Simplified ISO year-month-date regex - if !ymd_rx_clone.is_match(s) { - return Err(vec![( - PatternMismatch, - ymd_mismatch_msg(s, ymd_rx_clone.as_str()), - )]); - } - Ok(()) - }; - let unsized_one_to_one_hundred = NumberValidatorBuilder::::default() - .min(0) - .max(100) - .build()?; - - let less_than_100_input = InputBuilder::::default() - .validators(vec![&unsized_less_100]) - .filters(vec![×_two]) - .build()?; - - let less_than_100_input2 = InputBuilder::::default() - .validators(vec![&unsized_one_to_one_hundred]) - .filters(vec![×_two]) - .build()?; - - let ymd_input = InputBuilder::<&str>::default() - .validators(vec![&ymd_check]) - .build()?; - - let usize_input = Arc::new(less_than_100_input); - let usize_input_instance = Arc::clone(&usize_input); - let usize_input2 = Arc::new(&less_than_100_input2); - let usize_input2_instance = Arc::clone(&usize_input2); - - let str_input = Arc::new(ymd_input); - let str_input_instance = Arc::clone(&str_input); - - thread::scope(|scope| { - scope.spawn( - || match usize_input_instance.validate(Some(&101)) { - Err(x) => { - assert_eq!(x[0].1.as_str(), &unsized_less_than_100_msg(101)); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - scope.spawn( - || match usize_input_instance.validate_and_filter(Some(&99)) { - Err(err) => panic!("Expected `Ok(Some({:#?})`; Received `Err({:#?})`", - Cow::::Owned(99 * 2), err), - Ok(Some(x)) => assert_eq!(x, Cow::::Owned(99 * 2)), - _ => panic!("Expected `Ok(Some(Cow::Owned(99 * 2)))`; Received `Ok(None)`"), - }, - ); - - scope.spawn( - || match usize_input2_instance.validate(Some(&101)) { - Err(x) => { - assert_eq!(x[0].1.as_str(), &range_overflow_msg(&unsized_one_to_one_hundred, 101)); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - scope.spawn( - || match usize_input2_instance.validate_and_filter(Some(&99)) { - Err(err) => panic!("Expected `Ok(Some({:#?})`; Received `Err({:#?})`", - Cow::::Owned(99 * 2), err), - Ok(Some(x)) => assert_eq!(x, Cow::::Owned(99 * 2)), - _ => panic!("Expected `Ok(Some(Cow::Owned(99 * 2)))`; Received `Ok(None)`"), - }, - ); - - scope.spawn( - || match str_input_instance.validate(Some(&"")) { - Err(x) => { - assert_eq!(x[0].1.as_str(), ymd_mismatch_msg("", ymd_rx.as_str())); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - scope.spawn( - || if let Err(_err_tuple) = str_input_instance.validate(Some(&"2013-08-31")) { - panic!("Expected `Ok(()); Received Err(...)`") - }, - ); - }); - - Ok(()) - } - - #[test] - fn test_validate_and_filter() { - let input = InputBuilder::::default() - .name("hello") - .required(true) - .validators(vec![&unsized_less_100]) - .filters(vec![×_two]) - .build() - .unwrap(); - - assert_eq!(input.validate_and_filter(Some(&101)), Err(vec![(RangeOverflow, unsized_less_than_100_msg(101))])); - assert_eq!(input.validate_and_filter(Some(&99)), Ok(Some(Cow::Borrowed(&(99 * 2))))); - } - - #[test] - fn test_value_type() { - let callback1 = |xs: &&str| -> ValidationResult { - if !xs.is_empty() { - Ok(()) - } else { - Err(vec![( - ConstraintViolation::TypeMismatch, - "Error".to_string(), - )]) - } - }; - - let _input = InputBuilder::default() - .name("hello") - .validators(vec![&callback1]) - .build() - .unwrap(); - } - - #[test] - fn test_display() { - let input = InputBuilder::::default() - .name("hello") - .validators(vec![&unsized_less_100]) - .build() - .unwrap(); - - assert_eq!( - input.to_string(), - "Input { name: hello, required: false, validators: Some([Validator; 1]), filters: None }" - ); - } -} +use std::borrow::Cow; +use std::fmt::{Debug, Display, Formatter}; + +use crate::types::{Filter, InputConstraints, InputValue, Validator, ViolationMessage}; + +pub type ValueMissingViolationCallback = + dyn Fn(&Input) -> ViolationMessage + Send + Sync; + +#[derive(Builder, Clone)] +#[builder(pattern = "owned")] +pub struct Input<'a, 'b, T> +where + T: InputValue, +{ + #[builder(default = "true")] + pub break_on_failure: bool, + + /// @todo This should be an `Option>`, for compatibility. + #[builder(setter(into), default = "None")] + pub name: Option<&'a str>, + + #[builder(default = "false")] + pub required: bool, + + #[builder(setter(strip_option), default = "None")] + pub validators: Option>>, + + #[builder(setter(strip_option), default = "None")] + pub filters: Option>>>, + + #[builder(default = "&value_missing_msg")] + pub value_missing: &'a (dyn Fn(&Input<'a, 'b, T>) -> ViolationMessage + Send + Sync), + + // @todo Add support for `io_validators` (e.g., validators that return futures). +} + +impl<'a, 'b, T> Input<'a, 'b, T> +where + T: InputValue, +{ + pub fn new(name: Option<&'a str>) -> Self { + Input { + break_on_failure: false, + name, + required: false, + validators: None, + filters: None, + value_missing: &value_missing_msg, + } + } +} + +impl<'a, 'b, T: InputValue> InputConstraints<'a, 'b, T> for Input<'a, 'b, T> { + fn get_should_break_on_failure(&self) -> bool { + self.break_on_failure + } + + fn get_required(&self) -> bool { + self.required + } + + fn get_name(&self) -> Option> { + self.name.map(move |s: &'a str| Cow::Borrowed(s)) + } + + fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self) -> ViolationMessage + Send + Sync) { + self.value_missing + } + + fn get_validators(&self) -> Option<&[&'a Validator<&'b T>]> { + self.validators.as_deref() + } + + fn get_filters(&self) -> Option<&[&'a Filter>]> { + self.filters.as_deref() + } +} + +impl Default for Input<'_, '_, T> { + fn default() -> Self { + Self::new(None) + } +} + +impl Display for Input<'_, '_, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Input {{ name: {}, required: {}, validators: {}, filters: {} }}", + self.name.unwrap_or("None"), + self.required, + self.validators.as_deref().map(|vs| + format!("Some([Validator; {}])", vs.len()) + ).unwrap_or("None".to_string()), + self.filters.as_deref().map(|fs| + format!("Some([Filter; {}])", fs.len()) + ).unwrap_or("None".to_string()), + ) + } +} + +impl Debug for Input<'_, '_, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self) + } +} + +pub fn value_missing_msg(_: &Input) -> String { + "Value is missing.".to_string() +} + +#[cfg(test)] +mod test { + use regex::Regex; + use std::{borrow::Cow, error::Error, sync::Arc, thread}; + + use crate::types::{ConstraintViolation, ConstraintViolation::{PatternMismatch, RangeOverflow}, + ValidationResult, InputConstraints}; + use crate::input::{InputBuilder}; + use crate::number::range_overflow_msg; + use crate::validator::number::{NumberValidatorBuilder, step_mismatch_msg}; + use crate::validator::pattern::PatternValidator; + + // Tests setup types + fn unsized_less_than_100_msg(value: usize) -> String { + format!("{} is greater than 100", value) + } + + fn ymd_mismatch_msg(s: &str, pattern_str: &str) -> String { + format!("{} doesn't match pattern {}", s, pattern_str) + } + + fn unsized_less_100(x: &usize) -> ValidationResult { + if *x >= 100 { + return Err(vec![( + RangeOverflow, + unsized_less_than_100_msg(*x) + )]); + } + Ok(()) + } + + fn times_two(x: Option>) -> Option> { + x.map(|_x| Cow::Owned(*_x * 2)) + } + + #[test] + fn test_input_builder() -> Result<(), Box> { + // Simplified ISO year-month-date regex + let ymd_regex = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; + let ymd_regex_2 = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; + let ymd_regex_arc_orig = Arc::new(ymd_regex); + let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); + + let ymd_mismatch_msg = Arc::new(move |s: &str| -> String { + format!("{} doesn't match pattern {}", s, ymd_regex_arc.as_str()) + }); + + let ymd_mismatch_msg_arc = Arc::clone(&ymd_mismatch_msg); + let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); + + let ymd_check = move |s: &&str| -> ValidationResult { + if !ymd_regex_arc.is_match(s) { + return Err(vec![(PatternMismatch, ymd_mismatch_msg_arc(s))]); + } + Ok(()) + }; + + // Validator case 1 + let pattern_validator = PatternValidator { + pattern: Cow::Owned(ymd_regex_2), + pattern_mismatch: &|validator, s| { + format!("{} doesn't match pattern {}", s, validator.pattern.as_str()) + }, + }; + + let less_than_100_input = InputBuilder::::default() + .validators(vec![&unsized_less_100]) + .build()?; + + let yyyy_mm_dd_input = InputBuilder::<&str>::default() + .validators(vec![&ymd_check]) + .build()?; + + let even_0_to_100 = NumberValidatorBuilder::::default() + .min(0) + .max(100) + .step(2) + .build()?; + + let even_from_0_to_100_input = InputBuilder::::default() + .name("even-0-to-100") + .validators(vec![&even_0_to_100]) + .build()?; + + let yyyy_mm_dd_input2 = InputBuilder::<&str>::default() + .validators(vec![&pattern_validator]) + .build()?; + + // Missing value check + match less_than_100_input.validate(None) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + // `Rem` (Remainder) trait check + match even_from_0_to_100_input.validate(Some(&3)) { + Err(errs) => errs.iter().for_each(|v_err| { + assert_eq!(v_err.0, ConstraintViolation::StepMismatch); + assert_eq!(v_err.1, step_mismatch_msg(&even_0_to_100, 3)); + }), + _ => panic!("Expected Err(...); Received Ok(())") + } + + // Mismatch check + let value = "1000-99-999"; + match yyyy_mm_dd_input.validate(Some(&value)) { + Ok(_) => panic!("Expected Err(...); Received Ok(())"), + Err(tuples) => { + assert_eq!(tuples[0].0, PatternMismatch); + assert_eq!(tuples[0].1, ymd_mismatch_msg(value).as_str()); + } + } + + // Valid check + match yyyy_mm_dd_input.validate(None) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + // Valid check 2 + let value = "1000-99-99"; + match yyyy_mm_dd_input.validate(Some(&value)) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + // Valid check + let value = "1000-99-99"; + match yyyy_mm_dd_input2.validate(Some(&value)) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + Ok(()) + } + + // #[test] + // fn test_input_exotic_value_types() -> Result<(), Box> { + // todo!("Input control exotic `InputValue` test cases.") + // } + + #[test] + fn test_thread_safety() -> Result<(), Box> { + fn ymd_mismatch_msg(s: &str, pattern_str: &str) -> String { + format!("{} doesn't match pattern {}", s, pattern_str) + } + + fn ymd_check(s: &&str) -> ValidationResult { + // Simplified ISO year-month-date regex + let rx = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap(); + if !rx.is_match(s) { + return Err(vec![(PatternMismatch, ymd_mismatch_msg(s, rx.as_str()))]); + } + Ok(()) + } + + let less_than_100_input = InputBuilder::::default() + .validators(vec![&unsized_less_100]) + .build()?; + + let ymd_input = InputBuilder::<&str>::default() + .validators(vec![&ymd_check]) + .build()?; + + let usize_input = Arc::new(less_than_100_input); + let usize_input_instance = Arc::clone(&usize_input); + + let str_input = Arc::new(ymd_input); + let str_input_instance = Arc::clone(&str_input); + + let handle = + thread::spawn( + move || match usize_input_instance.validate(Some(&101)) { + Err(x) => { + assert_eq!(x[0].1.as_str(), unsized_less_than_100_msg(101)); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + let handle2 = + thread::spawn( + move || match str_input_instance.validate(Some(&"")) { + Err(x) => { + assert_eq!( + x[0].1.as_str(), + ymd_mismatch_msg( + "", + Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap().as_str() + ) + ); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + // @note Conclusion of tests here is that validators can only (easily) be shared between threads if they are function pointers - + // closures are too loose and require over the top value management and planning due to the nature of multi-threaded + // contexts. + + // Contrary to the above, 'scoped threads', will allow variable sharing without requiring them to + // be 'moved' first (as long as rust's lifetime rules are followed - + // @see https://blog.logrocket.com/using-rust-scoped-threads-improve-efficiency-safety/ + // ). + + handle.join().unwrap(); + handle2.join().unwrap(); + + Ok(()) + } + + /// Example showing shared references in `Input`, and user-land, controls. + #[test] + fn test_thread_safety_with_scoped_threads_and_closures() -> Result<(), Box> { + let ymd_rx = Arc::new(Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap()); + let ymd_rx_clone = Arc::clone(&ymd_rx); + + let ymd_check = move |s: &&str| -> ValidationResult { + // Simplified ISO year-month-date regex + if !ymd_rx_clone.is_match(s) { + return Err(vec![( + PatternMismatch, + ymd_mismatch_msg(s, ymd_rx_clone.as_str()), + )]); + } + Ok(()) + }; + let unsized_one_to_one_hundred = NumberValidatorBuilder::::default() + .min(0) + .max(100) + .build()?; + + let less_than_100_input = InputBuilder::::default() + .validators(vec![&unsized_less_100]) + .filters(vec![×_two]) + .build()?; + + let less_than_100_input2 = InputBuilder::::default() + .validators(vec![&unsized_one_to_one_hundred]) + .filters(vec![×_two]) + .build()?; + + let ymd_input = InputBuilder::<&str>::default() + .validators(vec![&ymd_check]) + .build()?; + + let usize_input = Arc::new(less_than_100_input); + let usize_input_instance = Arc::clone(&usize_input); + let usize_input2 = Arc::new(&less_than_100_input2); + let usize_input2_instance = Arc::clone(&usize_input2); + + let str_input = Arc::new(ymd_input); + let str_input_instance = Arc::clone(&str_input); + + thread::scope(|scope| { + scope.spawn( + || match usize_input_instance.validate(Some(&101)) { + Err(x) => { + assert_eq!(x[0].1.as_str(), &unsized_less_than_100_msg(101)); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + scope.spawn( + || match usize_input_instance.validate_and_filter(Some(&99)) { + Err(err) => panic!("Expected `Ok(Some({:#?})`; Received `Err({:#?})`", + Cow::::Owned(99 * 2), err), + Ok(Some(x)) => assert_eq!(x, Cow::::Owned(99 * 2)), + _ => panic!("Expected `Ok(Some(Cow::Owned(99 * 2)))`; Received `Ok(None)`"), + }, + ); + + scope.spawn( + || match usize_input2_instance.validate(Some(&101)) { + Err(x) => { + assert_eq!(x[0].1.as_str(), &range_overflow_msg(&unsized_one_to_one_hundred, 101)); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + scope.spawn( + || match usize_input2_instance.validate_and_filter(Some(&99)) { + Err(err) => panic!("Expected `Ok(Some({:#?})`; Received `Err({:#?})`", + Cow::::Owned(99 * 2), err), + Ok(Some(x)) => assert_eq!(x, Cow::::Owned(99 * 2)), + _ => panic!("Expected `Ok(Some(Cow::Owned(99 * 2)))`; Received `Ok(None)`"), + }, + ); + + scope.spawn( + || match str_input_instance.validate(Some(&"")) { + Err(x) => { + assert_eq!(x[0].1.as_str(), ymd_mismatch_msg("", ymd_rx.as_str())); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + scope.spawn( + || if let Err(_err_tuple) = str_input_instance.validate(Some(&"2013-08-31")) { + panic!("Expected `Ok(()); Received Err(...)`") + }, + ); + }); + + Ok(()) + } + + #[test] + fn test_validate_and_filter() { + let input = InputBuilder::::default() + .name("hello") + .required(true) + .validators(vec![&unsized_less_100]) + .filters(vec![×_two]) + .build() + .unwrap(); + + assert_eq!(input.validate_and_filter(Some(&101)), Err(vec![(RangeOverflow, unsized_less_than_100_msg(101))])); + assert_eq!(input.validate_and_filter(Some(&99)), Ok(Some(Cow::Borrowed(&(99 * 2))))); + } + + #[test] + fn test_value_type() { + let callback1 = |xs: &&str| -> ValidationResult { + if !xs.is_empty() { + Ok(()) + } else { + Err(vec![( + ConstraintViolation::TypeMismatch, + "Error".to_string(), + )]) + } + }; + + let _input = InputBuilder::default() + .name("hello") + .validators(vec![&callback1]) + .build() + .unwrap(); + } + + #[test] + fn test_display() { + let input = InputBuilder::::default() + .name("hello") + .validators(vec![&unsized_less_100]) + .build() + .unwrap(); + + assert_eq!( + input.to_string(), + "Input { name: hello, required: false, validators: Some([Validator; 1]), filters: None }" + ); + } +} diff --git a/inputfilter/src/lib.rs b/inputfilter/src/lib.rs index e356d2d..b498e82 100644 --- a/inputfilter/src/lib.rs +++ b/inputfilter/src/lib.rs @@ -1,20 +1,20 @@ -#![feature(fn_traits)] -#![feature(unboxed_closures)] - -#[macro_use] -extern crate derive_builder; - -pub mod types; -pub mod validator; -pub mod input; -pub mod filter; -pub mod string_input; - -pub use types::*; -pub use validator::*; -pub use input::*; -pub use filter::*; -pub use string_input::*; - -// @todo Add 'Builder' for `wal_inputfilter` structs. - +#![feature(fn_traits)] +#![feature(unboxed_closures)] + +#[macro_use] +extern crate derive_builder; + +pub mod types; +pub mod validator; +pub mod input; +pub mod filter; +pub mod string_input; + +pub use types::*; +pub use validator::*; +pub use input::*; +pub use filter::*; +pub use string_input::*; + +// @todo Add 'Builder' for `wal_inputfilter` structs. + diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 58fd732..33b540a 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -1,410 +1,410 @@ -use std::borrow::Cow; -use std::fmt::{Debug, Display, Formatter}; - -use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; - -pub type StrValueMissingViolationCallback = -dyn Fn(&StrInput) -> ViolationMessage + Send + Sync; - -#[derive(Builder, Clone)] -#[builder(pattern = "owned")] -pub struct StrInput<'a, 'b> -{ - #[builder(default = "true")] - pub break_on_failure: bool, - - /// @todo This should be an `Option>`, for compatibility. - #[builder(setter(into), default = "None")] - pub name: Option<&'a str>, - - #[builder(default = "false")] - pub required: bool, - - #[builder(setter(strip_option), default = "None")] - pub validators: Option>>, - - #[builder(setter(strip_option), default = "None")] - pub filters: Option>>>, - - #[builder(default = "&str_value_missing_msg")] - pub value_missing: &'a (dyn Fn(&StrInput<'a, 'b>) -> ViolationMessage + Send + Sync), - - // @todo Add support for `io_validators` (e.g., validators that return futures). -} - -impl<'a, 'b> StrInput<'a, 'b> -{ - pub fn new(name: Option<&'a str>) -> Self { - StrInput { - break_on_failure: false, - name, - required: false, - validators: None, - filters: None, - value_missing: &str_value_missing_msg, - } - } -} - -impl<'a, 'b> InputConstraints<'a, 'b, str> for StrInput<'a, 'b> { - fn get_should_break_on_failure(&self) -> bool { - self.break_on_failure - } - - fn get_required(&self) -> bool { - self.required - } - - fn get_name(&self) -> Option> { - self.name.map(move |s: &'a str| Cow::Borrowed(s)) - } - - fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self) -> ViolationMessage + Send + Sync) { - self.value_missing - } - - fn get_validators(&self) -> Option<&[&'a Validator<&'b str>]> { - self.validators.as_deref() - } - - fn get_filters(&self) -> Option<&[&'a Filter>]> { - self.filters.as_deref() - } -} - -impl Default for StrInput<'_, '_> { - fn default() -> Self { - Self::new(None) - } -} - -impl Display for StrInput<'_, '_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "StrInput {{ name: {}, required: {}, validators: {}, filters: {} }}", - self.name.unwrap_or("None"), - self.required, - self.validators.as_deref().map(|vs| - format!("Some([Validator; {}])", vs.len()) - ).unwrap_or("None".to_string()), - self.filters.as_deref().map(|fs| - format!("Some([Filter; {}])", fs.len()) - ).unwrap_or("None".to_string()), - ) - } -} - -impl Debug for StrInput<'_, '_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", &self) - } -} - -pub fn str_value_missing_msg(_: &StrInput) -> String { - "Value is missing.".to_string() -} - -#[cfg(test)] -mod test { - use regex::Regex; - use std::{borrow::Cow, error::Error, sync::Arc, thread}; - use crate::types::{ConstraintViolation, ConstraintViolation::{PatternMismatch, RangeOverflow}, - ValidationResult, InputConstraints}; - use crate::validator::pattern::PatternValidator; - use super::*; - - // Tests setup types - fn less_than_1990_msg(value: &str) -> String { - format!("{} is greater than 1989-12-31", value) - } - - /// Faux validator that checks if the input is less than 1990-01-01. - fn less_than_1990(x: &str) -> ValidationResult { - if x >= "1989-12-31" { - return Err(vec![( - RangeOverflow, - less_than_1990_msg(x) - )]); - } - Ok(()) - } - - fn ymd_mismatch_msg(s: &str, pattern_str: &str) -> String { - format!("{} doesn't match pattern {}", s, pattern_str) - } - - fn ymd_check(s: &str) -> ValidationResult { - // Simplified ISO year-month-date regex - let rx = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap(); - if !rx.is_match(s) { - return Err(vec![(PatternMismatch, ymd_mismatch_msg(s, rx.as_str()))]); - } - Ok(()) - } - - /// Faux filter that returns the last date of the month. - /// **Note:** Assumes that the input is a valid ISO year-month-date. - fn to_last_date_of_month(x: Option>) -> Option> { - x.map(|x| { - let mut xs = x.into_owned(); - xs.replace_range(8..10, "31"); - Cow::Owned(xs) - }) - } - - #[test] - fn test_input_builder() -> Result<(), Box> { - // Simplified ISO year-month-date regex - let ymd_regex = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; - let ymd_regex_2 = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; - let ymd_regex_arc_orig = Arc::new(ymd_regex); - let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); - - let ymd_mismatch_msg = Arc::new(move |s: &str| -> String { - format!("{} doesn't match pattern {}", s, ymd_regex_arc.as_str()) - }); - - let ymd_mismatch_msg_arc = Arc::clone(&ymd_mismatch_msg); - let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); - - let ymd_check = move |s: &str| -> ValidationResult { - if !ymd_regex_arc.is_match(s) { - return Err(vec![(PatternMismatch, ymd_mismatch_msg_arc(s))]); - } - Ok(()) - }; - - // Validator case 1 - let pattern_validator = PatternValidator { - pattern: Cow::Owned(ymd_regex_2), - pattern_mismatch: &|validator, s| { - format!("{} doesn't match pattern {}", s, validator.pattern.as_str()) - }, - }; - - let less_than_1990_input = StrInputBuilder::default() - .validators(vec![&less_than_1990]) - .build()?; - - let yyyy_mm_dd_input = StrInputBuilder::default() - .validators(vec![&ymd_check]) - .build()?; - - let yyyy_mm_dd_input2 = StrInputBuilder::default() - .validators(vec![&pattern_validator]) - .build()?; - - // Missing value check - match less_than_1990_input.validate(None) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), - } - - // Mismatch check - let value = "1000-99-999"; - match yyyy_mm_dd_input.validate(Some(&value)) { - Ok(_) => panic!("Expected Err(...); Received Ok(())"), - Err(tuples) => { - assert_eq!(tuples[0].0, PatternMismatch); - assert_eq!(tuples[0].1, ymd_mismatch_msg(value).as_str()); - } - } - - // Valid check - match yyyy_mm_dd_input.validate(None) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), - } - - // Valid check 2 - let value = "1000-99-99"; - match yyyy_mm_dd_input.validate(Some(&value)) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), - } - - // Valid check - let value = "1000-99-99"; - match yyyy_mm_dd_input2.validate(Some(&value)) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), - } - - Ok(()) - } - - #[test] - fn test_thread_safety() -> Result<(), Box> { - let less_than_1990_input = StrInputBuilder::default() - .validators(vec![&less_than_1990]) - .build()?; - - let ymd_input = StrInputBuilder::default() - .validators(vec![&ymd_check]) - .build()?; - - let less_than_input = Arc::new(less_than_1990_input); - let less_than_input_instance = Arc::clone(&less_than_input); - - let str_input = Arc::new(ymd_input); - let str_input_instance = Arc::clone(&str_input); - - let handle = - thread::spawn( - move || match less_than_input_instance.validate(Some("2023-12-31")) { - Err(x) => { - assert_eq!(x[0].1.as_str(), less_than_1990_msg("2023-12-31")); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - let handle2 = - thread::spawn( - move || match str_input_instance.validate(Some(&"")) { - Err(x) => { - assert_eq!( - x[0].1.as_str(), - ymd_mismatch_msg( - "", - Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap().as_str(), - ) - ); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - // @note Conclusion of tests here is that validators can only (easily) be shared between threads if they are function pointers - - // closures are too loose and require over the top value management and planning due to the nature of multi-threaded - // contexts. - - // Contrary to the above, 'scoped threads', will allow variable sharing without requiring them to - // be 'moved' first (as long as rust's lifetime rules are followed - - // @see https://blog.logrocket.com/using-rust-scoped-threads-improve-efficiency-safety/ - // ). - - handle.join().unwrap(); - handle2.join().unwrap(); - - Ok(()) - } - - /// Example showing shared references in `StrInput`, and user-land, controls. - #[test] - fn test_thread_safety_with_scoped_threads_and_closures() -> Result<(), Box> { - let ymd_rx = Arc::new(Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap()); - let ymd_rx_clone = Arc::clone(&ymd_rx); - - let ymd_check = move |s: &str| -> ValidationResult { - // Simplified ISO year-month-date regex - if !ymd_rx_clone.is_match(s) { - return Err(vec![( - PatternMismatch, - ymd_mismatch_msg(s, ymd_rx_clone.as_str()), - )]); - } - Ok(()) - }; - - let less_than_1990_input = StrInputBuilder::default() - .validators(vec![&less_than_1990]) - .filters(vec![&to_last_date_of_month]) - .build()?; - - let ymd_input = StrInputBuilder::default() - .validators(vec![&ymd_check]) - .build()?; - - let less_than_input = Arc::new(less_than_1990_input); - let less_than_input_instance = Arc::clone(&less_than_input); - let ymd_check_input = Arc::new(ymd_input); - let ymd_check_input_instance = Arc::clone(&ymd_check_input); - - thread::scope(|scope| { - scope.spawn( - || match less_than_input_instance.validate(Some("2023-12-31")) { - Err(x) => { - assert_eq!(x[0].1.as_str(), &less_than_1990_msg("2023-12-31")); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - scope.spawn( - || match less_than_input_instance.validate_and_filter(Some("1989-01-01")) { - Err(err) => panic!("Expected `Ok(Some({:#?})`; Received `Err({:#?})`", - Cow::::Owned("1989-01-31".to_string()), err), - Ok(Some(x)) => assert_eq!(x, Cow::::Owned("1989-01-31".to_string())), - _ => panic!("Expected `Ok(Some(Cow::Owned(99 * 2)))`; Received `Ok(None)`"), - }, - ); - - scope.spawn( - || match ymd_check_input_instance.validate(Some(&"")) { - Err(x) => { - assert_eq!(x[0].1.as_str(), ymd_mismatch_msg("", ymd_rx.as_str())); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - scope.spawn( - || if let Err(_err_tuple) = ymd_check_input_instance.validate(Some(&"2013-08-31")) { - panic!("Expected `Ok(()); Received Err(...)`") - }, - ); - }); - - Ok(()) - } - - #[test] - fn test_validate_and_filter() { - let input = StrInputBuilder::default() - .name("hello") - .required(true) - .validators(vec![&less_than_1990]) - .filters(vec![&to_last_date_of_month]) - .build() - .unwrap(); - - assert_eq!(input.validate_and_filter(Some("2023-12-31")), Err(vec![(RangeOverflow, less_than_1990_msg("2023-12-31"))])); - assert_eq!(input.validate_and_filter(Some("1989-01-01")), Ok(Some(Cow::Owned("1989-01-31".to_string())))); - } - - #[test] - fn test_value_type() { - let callback1 = |xs: &str| -> ValidationResult { - if !xs.is_empty() { - Ok(()) - } else { - Err(vec![( - ConstraintViolation::TypeMismatch, - "Error".to_string(), - )]) - } - }; - - let _input = StrInputBuilder::default() - .name("hello") - .validators(vec![&callback1]) - .build() - .unwrap(); - } - - #[test] - fn test_display() { - let input = StrInputBuilder::default() - .name("hello") - .validators(vec![&less_than_1990]) - .build() - .unwrap(); - - assert_eq!( - input.to_string(), - "StrInput { name: hello, required: false, validators: Some([Validator; 1]), filters: None }" - ); - } -} +use std::borrow::Cow; +use std::fmt::{Debug, Display, Formatter}; + +use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; + +pub type StrValueMissingViolationCallback = +dyn Fn(&StrInput) -> ViolationMessage + Send + Sync; + +#[derive(Builder, Clone)] +#[builder(pattern = "owned")] +pub struct StrInput<'a, 'b> +{ + #[builder(default = "true")] + pub break_on_failure: bool, + + /// @todo This should be an `Option>`, for compatibility. + #[builder(setter(into), default = "None")] + pub name: Option<&'a str>, + + #[builder(default = "false")] + pub required: bool, + + #[builder(setter(strip_option), default = "None")] + pub validators: Option>>, + + #[builder(setter(strip_option), default = "None")] + pub filters: Option>>>, + + #[builder(default = "&str_value_missing_msg")] + pub value_missing: &'a (dyn Fn(&StrInput<'a, 'b>) -> ViolationMessage + Send + Sync), + + // @todo Add support for `io_validators` (e.g., validators that return futures). +} + +impl<'a, 'b> StrInput<'a, 'b> +{ + pub fn new(name: Option<&'a str>) -> Self { + StrInput { + break_on_failure: false, + name, + required: false, + validators: None, + filters: None, + value_missing: &str_value_missing_msg, + } + } +} + +impl<'a, 'b> InputConstraints<'a, 'b, str> for StrInput<'a, 'b> { + fn get_should_break_on_failure(&self) -> bool { + self.break_on_failure + } + + fn get_required(&self) -> bool { + self.required + } + + fn get_name(&self) -> Option> { + self.name.map(move |s: &'a str| Cow::Borrowed(s)) + } + + fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self) -> ViolationMessage + Send + Sync) { + self.value_missing + } + + fn get_validators(&self) -> Option<&[&'a Validator<&'b str>]> { + self.validators.as_deref() + } + + fn get_filters(&self) -> Option<&[&'a Filter>]> { + self.filters.as_deref() + } +} + +impl Default for StrInput<'_, '_> { + fn default() -> Self { + Self::new(None) + } +} + +impl Display for StrInput<'_, '_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "StrInput {{ name: {}, required: {}, validators: {}, filters: {} }}", + self.name.unwrap_or("None"), + self.required, + self.validators.as_deref().map(|vs| + format!("Some([Validator; {}])", vs.len()) + ).unwrap_or("None".to_string()), + self.filters.as_deref().map(|fs| + format!("Some([Filter; {}])", fs.len()) + ).unwrap_or("None".to_string()), + ) + } +} + +impl Debug for StrInput<'_, '_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self) + } +} + +pub fn str_value_missing_msg(_: &StrInput) -> String { + "Value is missing.".to_string() +} + +#[cfg(test)] +mod test { + use regex::Regex; + use std::{borrow::Cow, error::Error, sync::Arc, thread}; + use crate::types::{ConstraintViolation, ConstraintViolation::{PatternMismatch, RangeOverflow}, + ValidationResult, InputConstraints}; + use crate::validator::pattern::PatternValidator; + use super::*; + + // Tests setup types + fn less_than_1990_msg(value: &str) -> String { + format!("{} is greater than 1989-12-31", value) + } + + /// Faux validator that checks if the input is less than 1990-01-01. + fn less_than_1990(x: &str) -> ValidationResult { + if x >= "1989-12-31" { + return Err(vec![( + RangeOverflow, + less_than_1990_msg(x) + )]); + } + Ok(()) + } + + fn ymd_mismatch_msg(s: &str, pattern_str: &str) -> String { + format!("{} doesn't match pattern {}", s, pattern_str) + } + + fn ymd_check(s: &str) -> ValidationResult { + // Simplified ISO year-month-date regex + let rx = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap(); + if !rx.is_match(s) { + return Err(vec![(PatternMismatch, ymd_mismatch_msg(s, rx.as_str()))]); + } + Ok(()) + } + + /// Faux filter that returns the last date of the month. + /// **Note:** Assumes that the input is a valid ISO year-month-date. + fn to_last_date_of_month(x: Option>) -> Option> { + x.map(|x| { + let mut xs = x.into_owned(); + xs.replace_range(8..10, "31"); + Cow::Owned(xs) + }) + } + + #[test] + fn test_input_builder() -> Result<(), Box> { + // Simplified ISO year-month-date regex + let ymd_regex = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; + let ymd_regex_2 = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; + let ymd_regex_arc_orig = Arc::new(ymd_regex); + let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); + + let ymd_mismatch_msg = Arc::new(move |s: &str| -> String { + format!("{} doesn't match pattern {}", s, ymd_regex_arc.as_str()) + }); + + let ymd_mismatch_msg_arc = Arc::clone(&ymd_mismatch_msg); + let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); + + let ymd_check = move |s: &str| -> ValidationResult { + if !ymd_regex_arc.is_match(s) { + return Err(vec![(PatternMismatch, ymd_mismatch_msg_arc(s))]); + } + Ok(()) + }; + + // Validator case 1 + let pattern_validator = PatternValidator { + pattern: Cow::Owned(ymd_regex_2), + pattern_mismatch: &|validator, s| { + format!("{} doesn't match pattern {}", s, validator.pattern.as_str()) + }, + }; + + let less_than_1990_input = StrInputBuilder::default() + .validators(vec![&less_than_1990]) + .build()?; + + let yyyy_mm_dd_input = StrInputBuilder::default() + .validators(vec![&ymd_check]) + .build()?; + + let yyyy_mm_dd_input2 = StrInputBuilder::default() + .validators(vec![&pattern_validator]) + .build()?; + + // Missing value check + match less_than_1990_input.validate(None) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + // Mismatch check + let value = "1000-99-999"; + match yyyy_mm_dd_input.validate(Some(&value)) { + Ok(_) => panic!("Expected Err(...); Received Ok(())"), + Err(tuples) => { + assert_eq!(tuples[0].0, PatternMismatch); + assert_eq!(tuples[0].1, ymd_mismatch_msg(value).as_str()); + } + } + + // Valid check + match yyyy_mm_dd_input.validate(None) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + // Valid check 2 + let value = "1000-99-99"; + match yyyy_mm_dd_input.validate(Some(&value)) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + // Valid check + let value = "1000-99-99"; + match yyyy_mm_dd_input2.validate(Some(&value)) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + Ok(()) + } + + #[test] + fn test_thread_safety() -> Result<(), Box> { + let less_than_1990_input = StrInputBuilder::default() + .validators(vec![&less_than_1990]) + .build()?; + + let ymd_input = StrInputBuilder::default() + .validators(vec![&ymd_check]) + .build()?; + + let less_than_input = Arc::new(less_than_1990_input); + let less_than_input_instance = Arc::clone(&less_than_input); + + let str_input = Arc::new(ymd_input); + let str_input_instance = Arc::clone(&str_input); + + let handle = + thread::spawn( + move || match less_than_input_instance.validate(Some("2023-12-31")) { + Err(x) => { + assert_eq!(x[0].1.as_str(), less_than_1990_msg("2023-12-31")); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + let handle2 = + thread::spawn( + move || match str_input_instance.validate(Some(&"")) { + Err(x) => { + assert_eq!( + x[0].1.as_str(), + ymd_mismatch_msg( + "", + Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap().as_str(), + ) + ); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + // @note Conclusion of tests here is that validators can only (easily) be shared between threads if they are function pointers - + // closures are too loose and require over the top value management and planning due to the nature of multi-threaded + // contexts. + + // Contrary to the above, 'scoped threads', will allow variable sharing without requiring them to + // be 'moved' first (as long as rust's lifetime rules are followed - + // @see https://blog.logrocket.com/using-rust-scoped-threads-improve-efficiency-safety/ + // ). + + handle.join().unwrap(); + handle2.join().unwrap(); + + Ok(()) + } + + /// Example showing shared references in `StrInput`, and user-land, controls. + #[test] + fn test_thread_safety_with_scoped_threads_and_closures() -> Result<(), Box> { + let ymd_rx = Arc::new(Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap()); + let ymd_rx_clone = Arc::clone(&ymd_rx); + + let ymd_check = move |s: &str| -> ValidationResult { + // Simplified ISO year-month-date regex + if !ymd_rx_clone.is_match(s) { + return Err(vec![( + PatternMismatch, + ymd_mismatch_msg(s, ymd_rx_clone.as_str()), + )]); + } + Ok(()) + }; + + let less_than_1990_input = StrInputBuilder::default() + .validators(vec![&less_than_1990]) + .filters(vec![&to_last_date_of_month]) + .build()?; + + let ymd_input = StrInputBuilder::default() + .validators(vec![&ymd_check]) + .build()?; + + let less_than_input = Arc::new(less_than_1990_input); + let less_than_input_instance = Arc::clone(&less_than_input); + let ymd_check_input = Arc::new(ymd_input); + let ymd_check_input_instance = Arc::clone(&ymd_check_input); + + thread::scope(|scope| { + scope.spawn( + || match less_than_input_instance.validate(Some("2023-12-31")) { + Err(x) => { + assert_eq!(x[0].1.as_str(), &less_than_1990_msg("2023-12-31")); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + scope.spawn( + || match less_than_input_instance.validate_and_filter(Some("1989-01-01")) { + Err(err) => panic!("Expected `Ok(Some({:#?})`; Received `Err({:#?})`", + Cow::::Owned("1989-01-31".to_string()), err), + Ok(Some(x)) => assert_eq!(x, Cow::::Owned("1989-01-31".to_string())), + _ => panic!("Expected `Ok(Some(Cow::Owned(99 * 2)))`; Received `Ok(None)`"), + }, + ); + + scope.spawn( + || match ymd_check_input_instance.validate(Some(&"")) { + Err(x) => { + assert_eq!(x[0].1.as_str(), ymd_mismatch_msg("", ymd_rx.as_str())); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + scope.spawn( + || if let Err(_err_tuple) = ymd_check_input_instance.validate(Some(&"2013-08-31")) { + panic!("Expected `Ok(()); Received Err(...)`") + }, + ); + }); + + Ok(()) + } + + #[test] + fn test_validate_and_filter() { + let input = StrInputBuilder::default() + .name("hello") + .required(true) + .validators(vec![&less_than_1990]) + .filters(vec![&to_last_date_of_month]) + .build() + .unwrap(); + + assert_eq!(input.validate_and_filter(Some("2023-12-31")), Err(vec![(RangeOverflow, less_than_1990_msg("2023-12-31"))])); + assert_eq!(input.validate_and_filter(Some("1989-01-01")), Ok(Some(Cow::Owned("1989-01-31".to_string())))); + } + + #[test] + fn test_value_type() { + let callback1 = |xs: &str| -> ValidationResult { + if !xs.is_empty() { + Ok(()) + } else { + Err(vec![( + ConstraintViolation::TypeMismatch, + "Error".to_string(), + )]) + } + }; + + let _input = StrInputBuilder::default() + .name("hello") + .validators(vec![&callback1]) + .build() + .unwrap(); + } + + #[test] + fn test_display() { + let input = StrInputBuilder::default() + .name("hello") + .validators(vec![&less_than_1990]) + .build() + .unwrap(); + + assert_eq!( + input.to_string(), + "StrInput { name: hello, required: false, validators: Some([Validator; 1]), filters: None }" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index bc9ca0c..bf195b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,8 @@ -#![feature(fn_traits)] -#![feature(unboxed_closures)] - -pub use walrs_inputfilter::validator; -pub use walrs_inputfilter::filter; -pub use walrs_inputfilter::input; -pub use walrs_inputfilter::string_input; -pub use walrs_inputfilter::types; +#![feature(fn_traits)] +#![feature(unboxed_closures)] + +pub use walrs_inputfilter::validator; +pub use walrs_inputfilter::filter; +pub use walrs_inputfilter::input; +pub use walrs_inputfilter::string_input; +pub use walrs_inputfilter::types; From 60d89680f38f211b5990a8e9bd8717083283908b Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Fri, 24 Nov 2023 02:13:26 -0500 Subject: [PATCH 04/38] inputfilter - Added ability for 'InputConstraints' implementors to have shorthand, via fields, for common validation rules - Makes common validation tasks easier to define (added some doc tests for these as well). - Tweaked 'Builder' options, for 'NumberValidator', to allow rule values to be set without 'into' pattern (ensures types are easily translatable (into underlying (builder's) struct's generic type)). . --- inputfilter/src/input.rs | 11 +- inputfilter/src/string_input.rs | 816 +++++++++++++++------------ inputfilter/src/types.rs | 112 +++- inputfilter/src/validator/number.rs | 9 +- inputfilter/src/validator/pattern.rs | 3 +- 5 files changed, 583 insertions(+), 368 deletions(-) diff --git a/inputfilter/src/input.rs b/inputfilter/src/input.rs index 35fef6c..53a7b9c 100644 --- a/inputfilter/src/input.rs +++ b/inputfilter/src/input.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::fmt::{Debug, Display, Formatter}; use crate::types::{Filter, InputConstraints, InputValue, Validator, ViolationMessage}; +use crate::ValidationResult; pub type ValueMissingViolationCallback = dyn Fn(&Input) -> ViolationMessage + Send + Sync; @@ -29,7 +30,7 @@ where pub filters: Option>>>, #[builder(default = "&value_missing_msg")] - pub value_missing: &'a (dyn Fn(&Input<'a, 'b, T>) -> ViolationMessage + Send + Sync), + pub value_missing: &'a (dyn Fn(&Input<'a, 'b, T>, Option<&T>) -> ViolationMessage + Send + Sync), // @todo Add support for `io_validators` (e.g., validators that return futures). } @@ -63,7 +64,7 @@ impl<'a, 'b, T: InputValue> InputConstraints<'a, 'b, T> for Input<'a, 'b, T> { self.name.map(move |s: &'a str| Cow::Borrowed(s)) } - fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self) -> ViolationMessage + Send + Sync) { + fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self, Option<&T>) -> ViolationMessage + Send + Sync) { self.value_missing } @@ -74,6 +75,10 @@ impl<'a, 'b, T: InputValue> InputConstraints<'a, 'b, T> for Input<'a, 'b, T> { fn get_filters(&self) -> Option<&[&'a Filter>]> { self.filters.as_deref() } + + fn validate_custom(&self, _: &'b T) -> ValidationResult { + Ok(()) + } } impl Default for Input<'_, '_, T> { @@ -105,7 +110,7 @@ impl Debug for Input<'_, '_, T> { } } -pub fn value_missing_msg(_: &Input) -> String { +pub fn value_missing_msg(_: &Input, _: Option<&T>) -> String { "Value is missing.".to_string() } diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 33b540a..dffec75 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -1,410 +1,520 @@ use std::borrow::Cow; use std::fmt::{Debug, Display, Formatter}; +use regex::Regex; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; +use crate::{ConstraintViolation, ValidationResult}; -pub type StrValueMissingViolationCallback = -dyn Fn(&StrInput) -> ViolationMessage + Send + Sync; +pub type StrMissingViolationCallback = dyn Fn(&StrInput, Option<&str>) -> ViolationMessage + Send + Sync; + +pub fn pattern_mismatch_msg(rules: &StrInput, xs: Option<&str>) -> String { + format!( + "`{}` does not match pattern `{}`", + &xs.as_ref().unwrap(), + rules.pattern.as_ref().unwrap() + ) +} + +pub fn too_short_msg(rules: &StrInput, xs: Option<&str>) -> String { + format!( + "Value length `{:}` is less than allowed minimum `{:}`.", + &xs.as_ref().unwrap().len(), + &rules.min_length.unwrap_or(0) + ) +} + +pub fn too_long_msg(rules: &StrInput, xs: Option<&str>) -> String { + format!( + "Value length `{:}` is greater than allowed maximum `{:}`.", + &xs.as_ref().unwrap().len(), + &rules.min_length.unwrap_or(0) + ) +} + +pub fn str_not_equal_msg(rules: &StrInput, _: Option<&str>) -> String { + format!( + "Value is not equal to {}.", + &rules.equal.as_deref().unwrap_or("") + ) +} #[derive(Builder, Clone)] -#[builder(pattern = "owned")] -pub struct StrInput<'a, 'b> -{ - #[builder(default = "true")] - pub break_on_failure: bool, +#[builder(pattern = "owned", setter(strip_option))] +pub struct StrInput<'a, 'b> { + #[builder(default = "true")] + pub break_on_failure: bool, + + /// @todo This should be an `Option>`, for compatibility. + #[builder(setter(into), default = "None")] + pub name: Option<&'a str>, + + #[builder(default = "None")] + pub min_length: Option, + + #[builder(default = "None")] + pub max_length: Option, + + #[builder(default = "None")] + pub pattern: Option, + + #[builder(default = "None")] + pub equal: Option<&'b str>, + + #[builder(default = "false")] + pub required: bool, - /// @todo This should be an `Option>`, for compatibility. - #[builder(setter(into), default = "None")] - pub name: Option<&'a str>, + #[builder(default = "None")] + pub validators: Option>>, - #[builder(default = "false")] - pub required: bool, + // @todo Add support for `io_validators` (e.g., validators that return futures). - #[builder(setter(strip_option), default = "None")] - pub validators: Option>>, + #[builder(default = "None")] + pub filters: Option>>>, - #[builder(setter(strip_option), default = "None")] - pub filters: Option>>>, + #[builder(default = "&too_short_msg")] + pub too_short: &'a StrMissingViolationCallback, - #[builder(default = "&str_value_missing_msg")] - pub value_missing: &'a (dyn Fn(&StrInput<'a, 'b>) -> ViolationMessage + Send + Sync), + #[builder(default = "&too_long_msg")] + pub too_long: &'a StrMissingViolationCallback, - // @todo Add support for `io_validators` (e.g., validators that return futures). + #[builder(default = "&pattern_mismatch_msg")] + pub pattern_mismatch: &'a StrMissingViolationCallback, + + #[builder(default = "&str_not_equal_msg")] + pub not_equal: &'a StrMissingViolationCallback, + + #[builder(default = "&str_missing_msg")] + pub value_missing: &'a StrMissingViolationCallback, } -impl<'a, 'b> StrInput<'a, 'b> -{ - pub fn new(name: Option<&'a str>) -> Self { - StrInput { - break_on_failure: false, - name, - required: false, - validators: None, - filters: None, - value_missing: &str_value_missing_msg, - } +impl<'a, 'b> StrInput<'a, 'b> { + pub fn new(name: Option<&'a str>) -> Self { + StrInput { + break_on_failure: false, + name, + min_length: None, + max_length: None, + pattern: None, + equal: None, + required: false, + validators: None, + filters: None, + too_short: &(too_long_msg), + too_long: &(too_long_msg), + pattern_mismatch: &(pattern_mismatch_msg), + not_equal: &(str_not_equal_msg), + value_missing: &str_missing_msg, } + } } impl<'a, 'b> InputConstraints<'a, 'b, str> for StrInput<'a, 'b> { - fn get_should_break_on_failure(&self) -> bool { - self.break_on_failure + fn get_should_break_on_failure(&self) -> bool { + self.break_on_failure + } + + fn get_required(&self) -> bool { + self.required + } + + fn get_name(&self) -> Option> { + self.name.map(move |s: &'a str| Cow::Borrowed(s)) + } + + fn get_value_missing_handler(&self) -> &'a StrMissingViolationCallback { + self.value_missing + } + + fn get_validators(&self) -> Option<&[&'a Validator<&'b str>]> { + self.validators.as_deref() + } + + fn get_filters(&self) -> Option<&[&'a Filter>]> { + self.filters.as_deref() + } + + fn validate_custom(&self, value: &'b str) -> ValidationResult { + if let Some(min_length) = self.min_length { + if value.len() < min_length { + return Err(vec![( + ConstraintViolation::TooShort, + (self.too_short)(self, Some(value)), + )]); + } } - fn get_required(&self) -> bool { - self.required + if let Some(max_length) = self.max_length { + if value.len() > max_length { + return Err(vec![( + ConstraintViolation::TooLong, + (self.too_long)(self, Some(value)), + )]); + } } - fn get_name(&self) -> Option> { - self.name.map(move |s: &'a str| Cow::Borrowed(s)) + if let Some(pattern) = &self.pattern { + if !pattern.is_match(value) { + return Err(vec![( + ConstraintViolation::PatternMismatch, + (&self.pattern_mismatch)(self, Some(value)), + )]); + } } - fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self) -> ViolationMessage + Send + Sync) { - self.value_missing + if let Some(equal) = &self.equal { + if value != *equal { + return Err(vec![( + ConstraintViolation::NotEqual, + (&self.not_equal)(self, Some(value)), + )]); + } } - fn get_validators(&self) -> Option<&[&'a Validator<&'b str>]> { - self.validators.as_deref() - } - - fn get_filters(&self) -> Option<&[&'a Filter>]> { - self.filters.as_deref() - } + Ok(()) + } } impl Default for StrInput<'_, '_> { - fn default() -> Self { - Self::new(None) - } + fn default() -> Self { + Self::new(None) + } } impl Display for StrInput<'_, '_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "StrInput {{ name: {}, required: {}, validators: {}, filters: {} }}", - self.name.unwrap_or("None"), - self.required, - self.validators.as_deref().map(|vs| - format!("Some([Validator; {}])", vs.len()) - ).unwrap_or("None".to_string()), - self.filters.as_deref().map(|fs| - format!("Some([Filter; {}])", fs.len()) - ).unwrap_or("None".to_string()), - ) - } + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "StrInput {{ name: {}, required: {}, validators: {}, filters: {} }}", + self.name.unwrap_or("None"), + self.required, + self + .validators + .as_deref() + .map(|vs| format!("Some([Validator; {}])", vs.len())) + .unwrap_or("None".to_string()), + self + .filters + .as_deref() + .map(|fs| format!("Some([Filter; {}])", fs.len())) + .unwrap_or("None".to_string()), + ) + } } impl Debug for StrInput<'_, '_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", &self) - } + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self) + } } -pub fn str_value_missing_msg(_: &StrInput) -> String { - "Value is missing.".to_string() +pub fn str_missing_msg(_: &StrInput, _: Option<&str>) -> String { + "Value is missing.".to_string() } #[cfg(test)] mod test { - use regex::Regex; - use std::{borrow::Cow, error::Error, sync::Arc, thread}; - use crate::types::{ConstraintViolation, ConstraintViolation::{PatternMismatch, RangeOverflow}, - ValidationResult, InputConstraints}; - use crate::validator::pattern::PatternValidator; - use super::*; - - // Tests setup types - fn less_than_1990_msg(value: &str) -> String { - format!("{} is greater than 1989-12-31", value) - } - - /// Faux validator that checks if the input is less than 1990-01-01. - fn less_than_1990(x: &str) -> ValidationResult { - if x >= "1989-12-31" { - return Err(vec![( - RangeOverflow, - less_than_1990_msg(x) - )]); - } - Ok(()) - } - - fn ymd_mismatch_msg(s: &str, pattern_str: &str) -> String { - format!("{} doesn't match pattern {}", s, pattern_str) + use super::*; + use crate::types::{ + ConstraintViolation, + ConstraintViolation::{PatternMismatch, RangeOverflow}, + InputConstraints, ValidationResult, + }; + use crate::validator::pattern::PatternValidator; + use regex::Regex; + use std::{borrow::Cow, error::Error, sync::Arc, thread}; + + // Tests setup types + fn less_than_1990_msg(value: &str) -> String { + format!("{} is greater than 1989-12-31", value) + } + + /// Faux validator that checks if the input is less than 1990-01-01. + fn less_than_1990(x: &str) -> ValidationResult { + if x >= "1989-12-31" { + return Err(vec![(RangeOverflow, less_than_1990_msg(x))]); } - - fn ymd_check(s: &str) -> ValidationResult { - // Simplified ISO year-month-date regex - let rx = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap(); - if !rx.is_match(s) { - return Err(vec![(PatternMismatch, ymd_mismatch_msg(s, rx.as_str()))]); - } - Ok(()) + Ok(()) + } + + fn ymd_mismatch_msg(s: &str, pattern_str: &str) -> String { + format!("{} doesn't match pattern {}", s, pattern_str) + } + + fn ymd_check(s: &str) -> ValidationResult { + // Simplified ISO year-month-date regex + let rx = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap(); + if !rx.is_match(s) { + return Err(vec![(PatternMismatch, ymd_mismatch_msg(s, rx.as_str()))]); } - - /// Faux filter that returns the last date of the month. - /// **Note:** Assumes that the input is a valid ISO year-month-date. - fn to_last_date_of_month(x: Option>) -> Option> { - x.map(|x| { - let mut xs = x.into_owned(); - xs.replace_range(8..10, "31"); - Cow::Owned(xs) - }) - } - - #[test] - fn test_input_builder() -> Result<(), Box> { - // Simplified ISO year-month-date regex - let ymd_regex = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; - let ymd_regex_2 = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; - let ymd_regex_arc_orig = Arc::new(ymd_regex); - let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); - - let ymd_mismatch_msg = Arc::new(move |s: &str| -> String { - format!("{} doesn't match pattern {}", s, ymd_regex_arc.as_str()) - }); - - let ymd_mismatch_msg_arc = Arc::clone(&ymd_mismatch_msg); - let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); - - let ymd_check = move |s: &str| -> ValidationResult { - if !ymd_regex_arc.is_match(s) { - return Err(vec![(PatternMismatch, ymd_mismatch_msg_arc(s))]); - } - Ok(()) - }; - - // Validator case 1 - let pattern_validator = PatternValidator { - pattern: Cow::Owned(ymd_regex_2), - pattern_mismatch: &|validator, s| { - format!("{} doesn't match pattern {}", s, validator.pattern.as_str()) - }, - }; - - let less_than_1990_input = StrInputBuilder::default() - .validators(vec![&less_than_1990]) - .build()?; - - let yyyy_mm_dd_input = StrInputBuilder::default() - .validators(vec![&ymd_check]) - .build()?; - - let yyyy_mm_dd_input2 = StrInputBuilder::default() - .validators(vec![&pattern_validator]) - .build()?; - - // Missing value check - match less_than_1990_input.validate(None) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), - } - - // Mismatch check - let value = "1000-99-999"; - match yyyy_mm_dd_input.validate(Some(&value)) { - Ok(_) => panic!("Expected Err(...); Received Ok(())"), - Err(tuples) => { - assert_eq!(tuples[0].0, PatternMismatch); - assert_eq!(tuples[0].1, ymd_mismatch_msg(value).as_str()); - } - } - - // Valid check - match yyyy_mm_dd_input.validate(None) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), - } - - // Valid check 2 - let value = "1000-99-99"; - match yyyy_mm_dd_input.validate(Some(&value)) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), - } - - // Valid check - let value = "1000-99-99"; - match yyyy_mm_dd_input2.validate(Some(&value)) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), - } - - Ok(()) + Ok(()) + } + + /// Faux filter that returns the last date of the month. + /// **Note:** Assumes that the input is a valid ISO year-month-date. + fn to_last_date_of_month(x: Option>) -> Option> { + x.map(|x| { + let mut xs = x.into_owned(); + xs.replace_range(8..10, "31"); + Cow::Owned(xs) + }) + } + + #[test] + fn test_input_builder() -> Result<(), Box> { + // Simplified ISO year-month-date regex + let ymd_regex = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; + let ymd_regex_2 = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; + let ymd_regex_arc_orig = Arc::new(ymd_regex); + let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); + + let ymd_mismatch_msg = Arc::new(move |s: &str| -> String { + format!("{} doesn't match pattern {}", s, ymd_regex_arc.as_str()) + }); + + let ymd_mismatch_msg_arc = Arc::clone(&ymd_mismatch_msg); + let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); + + let ymd_check = move |s: &str| -> ValidationResult { + if !ymd_regex_arc.is_match(s) { + return Err(vec![(PatternMismatch, ymd_mismatch_msg_arc(s))]); + } + Ok(()) + }; + + // Validator case 1 + let pattern_validator = PatternValidator { + pattern: Cow::Owned(ymd_regex_2), + pattern_mismatch: &|validator, s| { + format!("{} doesn't match pattern {}", s, validator.pattern.as_str()) + }, + }; + + let less_than_1990_input = StrInputBuilder::default() + .validators(vec![&less_than_1990]) + .build()?; + + let yyyy_mm_dd_input = StrInputBuilder::default() + .validators(vec![&ymd_check]) + .build()?; + + let yyyy_mm_dd_input2 = StrInputBuilder::default() + .validators(vec![&pattern_validator]) + .build()?; + + // Missing value check + match less_than_1990_input.validate(None) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), } - #[test] - fn test_thread_safety() -> Result<(), Box> { - let less_than_1990_input = StrInputBuilder::default() - .validators(vec![&less_than_1990]) - .build()?; - - let ymd_input = StrInputBuilder::default() - .validators(vec![&ymd_check]) - .build()?; - - let less_than_input = Arc::new(less_than_1990_input); - let less_than_input_instance = Arc::clone(&less_than_input); - - let str_input = Arc::new(ymd_input); - let str_input_instance = Arc::clone(&str_input); - - let handle = - thread::spawn( - move || match less_than_input_instance.validate(Some("2023-12-31")) { - Err(x) => { - assert_eq!(x[0].1.as_str(), less_than_1990_msg("2023-12-31")); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - let handle2 = - thread::spawn( - move || match str_input_instance.validate(Some(&"")) { - Err(x) => { - assert_eq!( - x[0].1.as_str(), - ymd_mismatch_msg( - "", - Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap().as_str(), - ) - ); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - // @note Conclusion of tests here is that validators can only (easily) be shared between threads if they are function pointers - - // closures are too loose and require over the top value management and planning due to the nature of multi-threaded - // contexts. - - // Contrary to the above, 'scoped threads', will allow variable sharing without requiring them to - // be 'moved' first (as long as rust's lifetime rules are followed - - // @see https://blog.logrocket.com/using-rust-scoped-threads-improve-efficiency-safety/ - // ). - - handle.join().unwrap(); - handle2.join().unwrap(); - - Ok(()) + // Mismatch check + let value = "1000-99-999"; + match yyyy_mm_dd_input.validate(Some(&value)) { + Ok(_) => panic!("Expected Err(...); Received Ok(())"), + Err(tuples) => { + assert_eq!(tuples[0].0, PatternMismatch); + assert_eq!(tuples[0].1, ymd_mismatch_msg(value).as_str()); + } } - /// Example showing shared references in `StrInput`, and user-land, controls. - #[test] - fn test_thread_safety_with_scoped_threads_and_closures() -> Result<(), Box> { - let ymd_rx = Arc::new(Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap()); - let ymd_rx_clone = Arc::clone(&ymd_rx); - - let ymd_check = move |s: &str| -> ValidationResult { - // Simplified ISO year-month-date regex - if !ymd_rx_clone.is_match(s) { - return Err(vec![( - PatternMismatch, - ymd_mismatch_msg(s, ymd_rx_clone.as_str()), - )]); - } - Ok(()) - }; - - let less_than_1990_input = StrInputBuilder::default() - .validators(vec![&less_than_1990]) - .filters(vec![&to_last_date_of_month]) - .build()?; - - let ymd_input = StrInputBuilder::default() - .validators(vec![&ymd_check]) - .build()?; - - let less_than_input = Arc::new(less_than_1990_input); - let less_than_input_instance = Arc::clone(&less_than_input); - let ymd_check_input = Arc::new(ymd_input); - let ymd_check_input_instance = Arc::clone(&ymd_check_input); - - thread::scope(|scope| { - scope.spawn( - || match less_than_input_instance.validate(Some("2023-12-31")) { - Err(x) => { - assert_eq!(x[0].1.as_str(), &less_than_1990_msg("2023-12-31")); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - scope.spawn( - || match less_than_input_instance.validate_and_filter(Some("1989-01-01")) { - Err(err) => panic!("Expected `Ok(Some({:#?})`; Received `Err({:#?})`", - Cow::::Owned("1989-01-31".to_string()), err), - Ok(Some(x)) => assert_eq!(x, Cow::::Owned("1989-01-31".to_string())), - _ => panic!("Expected `Ok(Some(Cow::Owned(99 * 2)))`; Received `Ok(None)`"), - }, - ); - - scope.spawn( - || match ymd_check_input_instance.validate(Some(&"")) { - Err(x) => { - assert_eq!(x[0].1.as_str(), ymd_mismatch_msg("", ymd_rx.as_str())); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - scope.spawn( - || if let Err(_err_tuple) = ymd_check_input_instance.validate(Some(&"2013-08-31")) { - panic!("Expected `Ok(()); Received Err(...)`") - }, - ); - }); - - Ok(()) + // Valid check + match yyyy_mm_dd_input.validate(None) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), } - #[test] - fn test_validate_and_filter() { - let input = StrInputBuilder::default() - .name("hello") - .required(true) - .validators(vec![&less_than_1990]) - .filters(vec![&to_last_date_of_month]) - .build() - .unwrap(); - - assert_eq!(input.validate_and_filter(Some("2023-12-31")), Err(vec![(RangeOverflow, less_than_1990_msg("2023-12-31"))])); - assert_eq!(input.validate_and_filter(Some("1989-01-01")), Ok(Some(Cow::Owned("1989-01-31".to_string())))); + // Valid check 2 + let value = "1000-99-99"; + match yyyy_mm_dd_input.validate(Some(&value)) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), } - #[test] - fn test_value_type() { - let callback1 = |xs: &str| -> ValidationResult { - if !xs.is_empty() { - Ok(()) - } else { - Err(vec![( - ConstraintViolation::TypeMismatch, - "Error".to_string(), - )]) - } - }; - - let _input = StrInputBuilder::default() - .name("hello") - .validators(vec![&callback1]) - .build() - .unwrap(); + // Valid check + let value = "1000-99-99"; + match yyyy_mm_dd_input2.validate(Some(&value)) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), } - #[test] - fn test_display() { - let input = StrInputBuilder::default() - .name("hello") - .validators(vec![&less_than_1990]) - .build() - .unwrap(); - + Ok(()) + } + + #[test] + fn test_thread_safety() -> Result<(), Box> { + let less_than_1990_input = StrInputBuilder::default() + .validators(vec![&less_than_1990]) + .build()?; + + let ymd_input = StrInputBuilder::default() + .validators(vec![&ymd_check]) + .build()?; + + let less_than_input = Arc::new(less_than_1990_input); + let less_than_input_instance = Arc::clone(&less_than_input); + + let str_input = Arc::new(ymd_input); + let str_input_instance = Arc::clone(&str_input); + + let handle = + thread::spawn( + move || match less_than_input_instance.validate(Some("2023-12-31")) { + Err(x) => { + assert_eq!(x[0].1.as_str(), less_than_1990_msg("2023-12-31")); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + let handle2 = thread::spawn(move || match str_input_instance.validate(Some(&"")) { + Err(x) => { assert_eq!( - input.to_string(), - "StrInput { name: hello, required: false, validators: Some([Validator; 1]), filters: None }" + x[0].1.as_str(), + ymd_mismatch_msg( + "", + Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap().as_str(), + ) ); - } + } + _ => panic!("Expected `Err(...)`"), + }); + + // @note Conclusion of tests here is that validators can only (easily) be shared between threads if they are function pointers - + // closures are too loose and require over the top value management and planning due to the nature of multi-threaded + // contexts. + + // Contrary to the above, 'scoped threads', will allow variable sharing without requiring them to + // be 'moved' first (as long as rust's lifetime rules are followed - + // @see https://blog.logrocket.com/using-rust-scoped-threads-improve-efficiency-safety/ + // ). + + handle.join().unwrap(); + handle2.join().unwrap(); + + Ok(()) + } + + /// Example showing shared references in `StrInput`, and user-land, controls. + #[test] + fn test_thread_safety_with_scoped_threads_and_closures() -> Result<(), Box> { + let ymd_rx = Arc::new(Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap()); + let ymd_rx_clone = Arc::clone(&ymd_rx); + + let ymd_check = move |s: &str| -> ValidationResult { + // Simplified ISO year-month-date regex + if !ymd_rx_clone.is_match(s) { + return Err(vec![( + PatternMismatch, + ymd_mismatch_msg(s, ymd_rx_clone.as_str()), + )]); + } + Ok(()) + }; + + let less_than_1990_input = StrInputBuilder::default() + .validators(vec![&less_than_1990]) + .filters(vec![&to_last_date_of_month]) + .build()?; + + let ymd_input = StrInputBuilder::default() + .validators(vec![&ymd_check]) + .build()?; + + let less_than_input = Arc::new(less_than_1990_input); + let less_than_input_instance = Arc::clone(&less_than_input); + let ymd_check_input = Arc::new(ymd_input); + let ymd_check_input_instance = Arc::clone(&ymd_check_input); + + thread::scope(|scope| { + scope.spawn( + || match less_than_input_instance.validate(Some("2023-12-31")) { + Err(x) => { + assert_eq!(x[0].1.as_str(), &less_than_1990_msg("2023-12-31")); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + scope.spawn( + || match less_than_input_instance.validate_and_filter(Some("1989-01-01")) { + Err(err) => panic!( + "Expected `Ok(Some({:#?})`; Received `Err({:#?})`", + Cow::::Owned("1989-01-31".to_string()), + err + ), + Ok(Some(x)) => assert_eq!(x, Cow::::Owned("1989-01-31".to_string())), + _ => panic!("Expected `Ok(Some(Cow::Owned(99 * 2)))`; Received `Ok(None)`"), + }, + ); + + scope.spawn(|| match ymd_check_input_instance.validate(Some(&"")) { + Err(x) => { + assert_eq!(x[0].1.as_str(), ymd_mismatch_msg("", ymd_rx.as_str())); + } + _ => panic!("Expected `Err(...)`"), + }); + + scope.spawn(|| { + if let Err(_err_tuple) = ymd_check_input_instance.validate(Some(&"2013-08-31")) { + panic!("Expected `Ok(()); Received Err(...)`") + } + }); + }); + + Ok(()) + } + + #[test] + fn test_validate_and_filter() { + let input = StrInputBuilder::default() + .name("hello") + .required(true) + .validators(vec![&less_than_1990]) + .filters(vec![&to_last_date_of_month]) + .build() + .unwrap(); + + assert_eq!( + input.validate_and_filter(Some("2023-12-31")), + Err(vec![(RangeOverflow, less_than_1990_msg("2023-12-31"))]) + ); + assert_eq!( + input.validate_and_filter(Some("1989-01-01")), + Ok(Some(Cow::Owned("1989-01-31".to_string()))) + ); + } + + #[test] + fn test_value_type() { + let callback1 = |xs: &str| -> ValidationResult { + if !xs.is_empty() { + Ok(()) + } else { + Err(vec![( + ConstraintViolation::TypeMismatch, + "Error".to_string(), + )]) + } + }; + + let _input = StrInputBuilder::default() + .name("hello") + .validators(vec![&callback1]) + .build() + .unwrap(); + } + + #[test] + fn test_display() { + let input = StrInputBuilder::default() + .name("hello") + .validators(vec![&less_than_1990]) + .build() + .unwrap(); + + assert_eq!( + input.to_string(), + "StrInput { name: hello, required: false, validators: Some([Validator; 1]), filters: None }" + ); + } } diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index ddf36ed..ed15cc9 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -94,11 +94,37 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par fn get_should_break_on_failure(&self) -> bool; fn get_required(&self) -> bool; fn get_name(&self) -> Option>; - fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self) -> ViolationMessage + Send + Sync); + fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self, Option<&T>) -> ViolationMessage + Send + Sync); fn get_validators(&self) -> Option<&[&'a Validator<&'call_ctx T>]>; fn get_filters(&self) -> Option<&[&'a Filter>]>; - fn validate_with_validators(&self, value: &'call_ctx T, validators: Option<&[&'a Validator<&'call_ctx T>]>) -> ValidationResult { + /// Validates value using implementing structs own custom validation logic (e.g., using it's own "custom" properties etc.). + /// Note: Gets called in `InputConstraints::validate` method, before any set validators are run. + /// + /// ```rust + /// use walrs_inputfilter::*; + /// + /// let input = StrInputBuilder::default() + /// .required(true) + /// .value_missing(&|_, _| "Value missing".to_string()) + /// .min_length(3usize) + /// .too_short(&|_, _| "Too short".to_string()) + /// .max_length(55usize) + /// .too_long(&|_, _| "Too long".to_string()) + /// .build() + /// .unwrap() + /// ; + /// + /// let too_long_str = &"ab".repeat(30);; + /// + /// assert_eq!(input.validate1(Some(&"ab")), Err(vec!["Too short".to_string()])); + /// assert_eq!(input.validate1(Some(&too_long_str)), Err(vec!["Too long".to_string()])); + /// assert_eq!(input.validate1(None), Err(vec!["Value missing".to_string()])); + /// ``` + fn validate_custom(&self, value: &'call_ctx T) -> Result<(), Vec>; + + /// Validates value against contained validators. + fn validate_with_validators(&self, value: &'call_ctx T, validators: Option<&[&'a Validator<&'call_ctx T>]>) -> Result<(), Vec> { validators.map(|vs| { // If not break on failure then capture all validation errors. @@ -128,19 +154,92 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par .map_or(Ok(()), Err) } + /// Validates value against any own `validate_custom` implementation and any set validators - + /// Runs `validate_custom(...)`, first and then (if it is `Ok(())`) runs + /// `validate_with_validators(...)` method. + /// + /// Additionally, note, `break_on_failure` is only guaranteed to be respected for the + /// the validators list; E.g., It is not guaranteed for `validate_custom()` call (as this is + /// implementing struct specific). + /// + /// ```rust + /// use walrs_inputfilter::*; + /// use walrs_inputfilter::number::{ + /// NumberValidator, + /// NumberValidatorBuilder, + /// range_overflow_msg, + /// range_underflow_msg, + /// step_mismatch_msg + /// }; + /// use walrs_inputfilter::pattern::PatternValidator; + /// + /// let num_validator = NumberValidatorBuilder::::default() + /// .min(-100isize) + /// .max(100isize) + /// .step(5) + /// .build() + /// .unwrap(); + /// + /// let input = InputBuilder::::default() + /// .validators(vec![ + /// &num_validator, + /// // Pretend "not allowed" num. + /// &|x: &isize| -> Result<(), Vec> { + /// if *x == 45 { + /// return Err(vec![(ConstraintViolation::CustomError, "\"45\" not allowed".to_string())]); + /// } + /// Ok(()) + /// } + /// ]) + /// .build() + /// .unwrap(); + /// + /// assert_eq!(input.validate1(Some(&-101)), Err(vec![range_underflow_msg(&num_validator, -101)])); + /// assert_eq!(input.validate1(Some(&101)), Err(vec![range_overflow_msg(&num_validator, 101)])); + /// assert_eq!(input.validate1(Some(&100)), Ok(())); + /// assert_eq!(input.validate1(Some(&-99)), Err(vec![step_mismatch_msg(&num_validator, -99)])); + /// assert_eq!(input.validate1(Some(&95)), Ok(())); + /// assert_eq!(input.validate1(Some(&45)), Err(vec!["\"45\" not allowed".to_string()])); + /// + /// let str_input = StrInputBuilder::default() + /// .required(true) + /// .value_missing(&|_, _| "Value missing".to_string()) + /// .min_length(3usize) + /// .too_short(&|_, _| "Too short".to_string()) + /// .max_length(200usize) // Default violation message callback used here. + /// // Naive email pattern validator (naive for example). + /// .validators(vec![&|x: &str| { + /// if !x.contains('@') { + /// return Err(vec![(ConstraintViolation::TypeMismatch, "Invalid email".to_string())]); + /// } + /// Ok(()) + /// }]) + /// .build() + /// .unwrap(); + /// + /// let too_long_str = &"ab".repeat(201);; + /// + /// assert_eq!(str_input.validate1(None), Err(vec!["Value missing".to_string()])); + /// assert_eq!(str_input.validate1(Some(&"ab")), Err(vec!["Too short".to_string()])); + /// assert_eq!(str_input.validate1(Some(&too_long_str)), Err(vec![too_long_msg(&str_input, Some(&too_long_str))])); + /// assert_eq!(str_input.validate1(Some(&"abc")), Err(vec!["Invalid email".to_string()])); + /// assert_eq!(str_input.validate1(Some(&"abc@def")), Ok(())); + /// ``` fn validate(&self, value: Option<&'call_ctx T>) -> ValidationResult { match value { None => { if self.get_required() { Err(vec![( ConstraintViolation::ValueMissing, - (self.get_value_missing_handler())(self), + (self.get_value_missing_handler())(self, None), )]) } else { Ok(()) } } - Some(v) => self.validate_with_validators(v, self.get_validators()), + // Else if value is populated validate it + Some(v) => self.validate_custom(v) + .and_then(|_| self.validate_with_validators(v, self.get_validators())), } } @@ -152,7 +251,7 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// /// let input = StrInputBuilder::default() /// .required(true) - /// .value_missing(&|_| "Value missing".to_string()) + /// .value_missing(&|_, _| "Value missing".to_string()) /// .validators(vec![&|x: &str| { /// if x.len() < 3 { /// return Err(vec![( @@ -197,7 +296,7 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// /// let input = StrInputBuilder::default() /// .required(true) - /// .value_missing(&|_| "Value missing".to_string()) + /// .value_missing(&|_, _| "Value missing".to_string()) /// .validators(vec![&|x: &str| { /// if x.len() < 3 { /// return Err(vec![( @@ -218,7 +317,6 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// assert_eq!(input.validate_and_filter1(Some(&"Abba")), Ok(Some("Abba".to_lowercase().into()))); /// assert_eq!(input.validate_and_filter1(None), Err(vec!["Value missing".to_string()])); /// ``` - /// fn validate_and_filter1(&self, x: Option<&'call_ctx T>) -> Result>, Vec> { match self.validate_and_filter(x) { Err(messages) => diff --git a/inputfilter/src/validator/number.rs b/inputfilter/src/validator/number.rs index e31e4cc..31bd3e2 100644 --- a/inputfilter/src/validator/number.rs +++ b/inputfilter/src/validator/number.rs @@ -13,17 +13,18 @@ pub type NumberVldrViolationCallback<'a, T> = (dyn Fn(&NumberValidator<'a, T>, T) -> String + Send + Sync); #[derive(Builder, Clone)] +#[builder(setter(strip_option))] pub struct NumberValidator<'a, T: NumberValue> { - #[builder(setter(into), default = "None")] + #[builder(default = "None")] pub min: Option, - #[builder(setter(into), default = "None")] + #[builder(default = "None")] pub max: Option, - #[builder(setter(into), default = "None")] + #[builder(default = "None")] pub step: Option, - #[builder(setter(into), default = "None")] + #[builder(default = "None")] pub equal: Option, #[builder(default = "&range_underflow_msg")] diff --git a/inputfilter/src/validator/pattern.rs b/inputfilter/src/validator/pattern.rs index a5d0892..de545ed 100644 --- a/inputfilter/src/validator/pattern.rs +++ b/inputfilter/src/validator/pattern.rs @@ -155,6 +155,7 @@ pub fn pattern_mismatch_msg(rules: &PatternValidator, xs: &str) -> String { &rules.pattern.to_string() ) } + #[cfg(test)] mod test { use std::borrow::Cow; @@ -205,4 +206,4 @@ mod test { Ok(()) } -} \ No newline at end of file +} From 761d1bc88cef31f0f3a8bd90334904fb6c6fb38a Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Fri, 24 Nov 2023 12:48:16 -0500 Subject: [PATCH 05/38] inputfilter - Added additional support for 'break_on_failure', from 'validate', and 'StrInput::validate_custom' (untested) method calls. --- inputfilter/src/input.rs | 1 + inputfilter/src/string_input.rs | 32 ++++++++++++------ inputfilter/src/types.rs | 57 +++++++++++++++++++++------------ 3 files changed, 60 insertions(+), 30 deletions(-) diff --git a/inputfilter/src/input.rs b/inputfilter/src/input.rs index 53a7b9c..a8f4c23 100644 --- a/inputfilter/src/input.rs +++ b/inputfilter/src/input.rs @@ -7,6 +7,7 @@ use crate::ValidationResult; pub type ValueMissingViolationCallback = dyn Fn(&Input) -> ViolationMessage + Send + Sync; +/// Deprecated - Use `StrInput`, and or `NumInput` instead. #[derive(Builder, Clone)] #[builder(pattern = "owned")] pub struct Input<'a, 'b, T> diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index dffec75..7de0a3d 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -134,43 +134,55 @@ impl<'a, 'b> InputConstraints<'a, 'b, str> for StrInput<'a, 'b> { } fn validate_custom(&self, value: &'b str) -> ValidationResult { + let mut errs = vec![]; + if let Some(min_length) = self.min_length { if value.len() < min_length { - return Err(vec![( + errs.push(( ConstraintViolation::TooShort, (self.too_short)(self, Some(value)), - )]); + )); + + if self.break_on_failure { return Err(errs); } } } if let Some(max_length) = self.max_length { if value.len() > max_length { - return Err(vec![( + errs.push(( ConstraintViolation::TooLong, (self.too_long)(self, Some(value)), - )]); + )); + + if self.break_on_failure { return Err(errs); } } } if let Some(pattern) = &self.pattern { if !pattern.is_match(value) { - return Err(vec![( + errs.push(( ConstraintViolation::PatternMismatch, - (&self.pattern_mismatch)(self, Some(value)), - )]); + (&self. + pattern_mismatch)(self, Some(value)), + )); + + if self.break_on_failure { return Err(errs); } } } if let Some(equal) = &self.equal { if value != *equal { - return Err(vec![( + errs.push(( ConstraintViolation::NotEqual, (&self.not_equal)(self, Some(value)), - )]); + )); + + if self.break_on_failure { return Err(errs); } } } - Ok(()) + if errs.is_empty() { Ok(()) } + else { Err(errs) } } } diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index ed15cc9..be630da 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -155,12 +155,11 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par } /// Validates value against any own `validate_custom` implementation and any set validators - - /// Runs `validate_custom(...)`, first and then (if it is `Ok(())`) runs - /// `validate_with_validators(...)` method. + /// E.g., runs `validate_custom(...)`, then, if it is `Ok`, `validate_with_validators(...)` method. /// /// Additionally, note, `break_on_failure` is only guaranteed to be respected for the - /// the validators list; E.g., It is not guaranteed for `validate_custom()` call (as this is - /// implementing struct specific). + /// the validators list, and input filters defined in the library; E.g., It is not guaranteed for + /// `validate_custom()` call in external libraries (e.g., this is left to implementing struct authors). /// /// ```rust /// use walrs_inputfilter::*; @@ -172,6 +171,10 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// step_mismatch_msg /// }; /// use walrs_inputfilter::pattern::PatternValidator; + /// use walrs_inputfilter::types::ConstraintViolation::{ + /// ValueMissing, TooShort, TooLong, TypeMismatch, CustomError, + /// RangeOverflow, RangeUnderflow, StepMismatch + /// }; /// /// let num_validator = NumberValidatorBuilder::::default() /// .min(-100isize) @@ -183,7 +186,7 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// let input = InputBuilder::::default() /// .validators(vec![ /// &num_validator, - /// // Pretend "not allowed" num. + /// // Pretend "not allowed" case. /// &|x: &isize| -> Result<(), Vec> { /// if *x == 45 { /// return Err(vec![(ConstraintViolation::CustomError, "\"45\" not allowed".to_string())]); @@ -194,12 +197,13 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// .build() /// .unwrap(); /// - /// assert_eq!(input.validate1(Some(&-101)), Err(vec![range_underflow_msg(&num_validator, -101)])); - /// assert_eq!(input.validate1(Some(&101)), Err(vec![range_overflow_msg(&num_validator, 101)])); - /// assert_eq!(input.validate1(Some(&100)), Ok(())); - /// assert_eq!(input.validate1(Some(&-99)), Err(vec![step_mismatch_msg(&num_validator, -99)])); - /// assert_eq!(input.validate1(Some(&95)), Ok(())); - /// assert_eq!(input.validate1(Some(&45)), Err(vec!["\"45\" not allowed".to_string()])); + /// assert_eq!(input.validate(None), Ok(())); + /// assert_eq!(input.validate(Some(&-101)), Err(vec![(RangeUnderflow, range_underflow_msg(&num_validator, -101))])); + /// assert_eq!(input.validate(Some(&101)), Err(vec![(RangeOverflow, range_overflow_msg(&num_validator, 101))])); + /// assert_eq!(input.validate(Some(&100)), Ok(())); + /// assert_eq!(input.validate(Some(&-99)), Err(vec![(StepMismatch, step_mismatch_msg(&num_validator, -99))])); + /// assert_eq!(input.validate(Some(&95)), Ok(())); + /// assert_eq!(input.validate(Some(&45)), Err(vec![(CustomError, "\"45\" not allowed".to_string())])); /// /// let str_input = StrInputBuilder::default() /// .required(true) @@ -207,10 +211,10 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// .min_length(3usize) /// .too_short(&|_, _| "Too short".to_string()) /// .max_length(200usize) // Default violation message callback used here. - /// // Naive email pattern validator (naive for example). + /// // Naive email pattern validator (naive for this example). /// .validators(vec![&|x: &str| { /// if !x.contains('@') { - /// return Err(vec![(ConstraintViolation::TypeMismatch, "Invalid email".to_string())]); + /// return Err(vec![(TypeMismatch, "Invalid email".to_string())]); /// } /// Ok(()) /// }]) @@ -219,11 +223,11 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// /// let too_long_str = &"ab".repeat(201);; /// - /// assert_eq!(str_input.validate1(None), Err(vec!["Value missing".to_string()])); - /// assert_eq!(str_input.validate1(Some(&"ab")), Err(vec!["Too short".to_string()])); - /// assert_eq!(str_input.validate1(Some(&too_long_str)), Err(vec![too_long_msg(&str_input, Some(&too_long_str))])); - /// assert_eq!(str_input.validate1(Some(&"abc")), Err(vec!["Invalid email".to_string()])); - /// assert_eq!(str_input.validate1(Some(&"abc@def")), Ok(())); + /// assert_eq!(str_input.validate(None), Err(vec![ (ValueMissing, "Value missing".to_string()) ])); + /// assert_eq!(str_input.validate(Some(&"ab")), Err(vec![ (TooShort, "Too short".to_string()) ])); + /// assert_eq!(str_input.validate(Some(&too_long_str)), Err(vec![ (TooLong, too_long_msg(&str_input, Some(&too_long_str))) ])); + /// assert_eq!(str_input.validate(Some(&"abc")), Err(vec![ (TypeMismatch, "Invalid email".to_string()) ])); + /// assert_eq!(str_input.validate(Some(&"abc@def")), Ok(())); /// ``` fn validate(&self, value: Option<&'call_ctx T>) -> ValidationResult { match value { @@ -238,8 +242,21 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par } } // Else if value is populated validate it - Some(v) => self.validate_custom(v) - .and_then(|_| self.validate_with_validators(v, self.get_validators())), + Some(v) => match self.validate_custom(v) { + Ok(_) => self.validate_with_validators(v, self.get_validators()), + Err(messages1) => if self.get_should_break_on_failure() { + Err(messages1) + } else { + match self.validate_with_validators(v, self.get_validators()) { + Ok(_) => Ok(()), + Err(mut messages2) => { + let mut agg = messages1; + agg.append(messages2.as_mut()); + Err(agg) + } + } + } + }, } } From 6b35aa8fda7c6116116480f152a045fe2c7faf35 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Fri, 24 Nov 2023 16:55:09 -0500 Subject: [PATCH 06/38] inputfilter - Progress on updating 'InputValue', and 'Input' struct, to support unsized ('?Sized') types. --- inputfilter/src/input.rs | 19 +++++++------------ inputfilter/src/types.rs | 18 ++++++++---------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/inputfilter/src/input.rs b/inputfilter/src/input.rs index a8f4c23..408587a 100644 --- a/inputfilter/src/input.rs +++ b/inputfilter/src/input.rs @@ -12,7 +12,7 @@ pub type ValueMissingViolationCallback = #[builder(pattern = "owned")] pub struct Input<'a, 'b, T> where - T: InputValue, + T: InputValue + ?Sized, { #[builder(default = "true")] pub break_on_failure: bool, @@ -38,7 +38,7 @@ where impl<'a, 'b, T> Input<'a, 'b, T> where - T: InputValue, + T: InputValue + ?Sized, { pub fn new(name: Option<&'a str>) -> Self { Input { @@ -52,7 +52,7 @@ where } } -impl<'a, 'b, T: InputValue> InputConstraints<'a, 'b, T> for Input<'a, 'b, T> { +impl<'a, 'b, T: InputValue + ?Sized> InputConstraints<'a, 'b, T> for Input<'a, 'b, T> { fn get_should_break_on_failure(&self) -> bool { self.break_on_failure } @@ -82,13 +82,13 @@ impl<'a, 'b, T: InputValue> InputConstraints<'a, 'b, T> for Input<'a, 'b, T> { } } -impl Default for Input<'_, '_, T> { +impl Default for Input<'_, '_, T> { fn default() -> Self { Self::new(None) } } -impl Display for Input<'_, '_, T> { +impl Display for Input<'_, '_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, @@ -105,13 +105,13 @@ impl Display for Input<'_, '_, T> { } } -impl Debug for Input<'_, '_, T> { +impl Debug for Input<'_, '_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self) } } -pub fn value_missing_msg(_: &Input, _: Option<&T>) -> String { +pub fn value_missing_msg(_: &Input, _: Option<&T>) -> String { "Value is missing.".to_string() } @@ -251,11 +251,6 @@ mod test { Ok(()) } - // #[test] - // fn test_input_exotic_value_types() -> Result<(), Box> { - // todo!("Input control exotic `InputValue` test cases.") - // } - #[test] fn test_thread_safety() -> Result<(), Box> { fn ymd_mismatch_msg(s: &str, pattern_str: &str) -> String { diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index be630da..1dd9d9f 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -24,11 +24,9 @@ impl InputValue for f64 {} impl InputValue for bool {} -impl InputValue for Box {} -impl InputValue for String {} -impl<'a> InputValue for Cow<'a, str> {} -impl InputValue for &'_ str {} -impl InputValue for &&'_ str {} +impl InputValue for char {} +impl InputValue for str {} +impl InputValue for &str {} pub trait NumberValue: Default + InputValue + Copy + Add + Sub + Mul + Div + Rem {} @@ -76,11 +74,11 @@ pub type Filter = dyn Fn(Option) -> Option + Send + Sync; pub type Validator = dyn Fn(T) -> ValidationResult + Send + Sync; -pub trait ValidateValue { +pub trait ValidateValue { fn validate(&self, value: T) -> ValidationResult; } -pub trait FilterValue { +pub trait FilterValue { fn filter(&self, value: Option>) -> Option>; } @@ -115,7 +113,7 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// .unwrap() /// ; /// - /// let too_long_str = &"ab".repeat(30);; + /// let too_long_str = &"ab".repeat(30); /// /// assert_eq!(input.validate1(Some(&"ab")), Err(vec!["Too short".to_string()])); /// assert_eq!(input.validate1(Some(&too_long_str)), Err(vec!["Too long".to_string()])); @@ -189,7 +187,7 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// // Pretend "not allowed" case. /// &|x: &isize| -> Result<(), Vec> { /// if *x == 45 { - /// return Err(vec![(ConstraintViolation::CustomError, "\"45\" not allowed".to_string())]); + /// return Err(vec![(CustomError, "\"45\" not allowed".to_string())]); /// } /// Ok(()) /// } @@ -221,7 +219,7 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// .build() /// .unwrap(); /// - /// let too_long_str = &"ab".repeat(201);; + /// let too_long_str = &"ab".repeat(201); /// /// assert_eq!(str_input.validate(None), Err(vec![ (ValueMissing, "Value missing".to_string()) ])); /// assert_eq!(str_input.validate(Some(&"ab")), Err(vec![ (TooShort, "Too short".to_string()) ])); From ef1716b5c714b693e83b6409a302bd2e703ae3e5 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Fri, 24 Nov 2023 17:22:23 -0500 Subject: [PATCH 07/38] inputfilter - Renamed 'StrInput' to 'StringInput'. --- inputfilter/src/string_input.rs | 46 ++++++++++++++-------------- inputfilter/src/types.rs | 13 +++++--- inputfilter/src/validator/pattern.rs | 38 ----------------------- 3 files changed, 31 insertions(+), 66 deletions(-) diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 7de0a3d..dce33a4 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -5,9 +5,9 @@ use regex::Regex; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; use crate::{ConstraintViolation, ValidationResult}; -pub type StrMissingViolationCallback = dyn Fn(&StrInput, Option<&str>) -> ViolationMessage + Send + Sync; +pub type StrMissingViolationCallback = dyn Fn(&StringInput, Option<&str>) -> ViolationMessage + Send + Sync; -pub fn pattern_mismatch_msg(rules: &StrInput, xs: Option<&str>) -> String { +pub fn pattern_mismatch_msg(rules: &StringInput, xs: Option<&str>) -> String { format!( "`{}` does not match pattern `{}`", &xs.as_ref().unwrap(), @@ -15,7 +15,7 @@ pub fn pattern_mismatch_msg(rules: &StrInput, xs: Option<&str>) -> String { ) } -pub fn too_short_msg(rules: &StrInput, xs: Option<&str>) -> String { +pub fn too_short_msg(rules: &StringInput, xs: Option<&str>) -> String { format!( "Value length `{:}` is less than allowed minimum `{:}`.", &xs.as_ref().unwrap().len(), @@ -23,7 +23,7 @@ pub fn too_short_msg(rules: &StrInput, xs: Option<&str>) -> String { ) } -pub fn too_long_msg(rules: &StrInput, xs: Option<&str>) -> String { +pub fn too_long_msg(rules: &StringInput, xs: Option<&str>) -> String { format!( "Value length `{:}` is greater than allowed maximum `{:}`.", &xs.as_ref().unwrap().len(), @@ -31,7 +31,7 @@ pub fn too_long_msg(rules: &StrInput, xs: Option<&str>) -> String { ) } -pub fn str_not_equal_msg(rules: &StrInput, _: Option<&str>) -> String { +pub fn str_not_equal_msg(rules: &StringInput, _: Option<&str>) -> String { format!( "Value is not equal to {}.", &rules.equal.as_deref().unwrap_or("") @@ -40,7 +40,7 @@ pub fn str_not_equal_msg(rules: &StrInput, _: Option<&str>) -> String { #[derive(Builder, Clone)] #[builder(pattern = "owned", setter(strip_option))] -pub struct StrInput<'a, 'b> { +pub struct StringInput<'a, 'b> { #[builder(default = "true")] pub break_on_failure: bool, @@ -87,9 +87,9 @@ pub struct StrInput<'a, 'b> { pub value_missing: &'a StrMissingViolationCallback, } -impl<'a, 'b> StrInput<'a, 'b> { +impl<'a, 'b> StringInput<'a, 'b> { pub fn new(name: Option<&'a str>) -> Self { - StrInput { + StringInput { break_on_failure: false, name, min_length: None, @@ -108,7 +108,7 @@ impl<'a, 'b> StrInput<'a, 'b> { } } -impl<'a, 'b> InputConstraints<'a, 'b, str> for StrInput<'a, 'b> { +impl<'a, 'b> InputConstraints<'a, 'b, str> for StringInput<'a, 'b> { fn get_should_break_on_failure(&self) -> bool { self.break_on_failure } @@ -186,13 +186,13 @@ impl<'a, 'b> InputConstraints<'a, 'b, str> for StrInput<'a, 'b> { } } -impl Default for StrInput<'_, '_> { +impl Default for StringInput<'_, '_> { fn default() -> Self { Self::new(None) } } -impl Display for StrInput<'_, '_> { +impl Display for StringInput<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, @@ -213,13 +213,13 @@ impl Display for StrInput<'_, '_> { } } -impl Debug for StrInput<'_, '_> { +impl Debug for StringInput<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self) } } -pub fn str_missing_msg(_: &StrInput, _: Option<&str>) -> String { +pub fn str_missing_msg(_: &StringInput, _: Option<&str>) -> String { "Value is missing.".to_string() } @@ -301,15 +301,15 @@ mod test { }, }; - let less_than_1990_input = StrInputBuilder::default() + let less_than_1990_input = StringInputBuilder::default() .validators(vec![&less_than_1990]) .build()?; - let yyyy_mm_dd_input = StrInputBuilder::default() + let yyyy_mm_dd_input = StringInputBuilder::default() .validators(vec![&ymd_check]) .build()?; - let yyyy_mm_dd_input2 = StrInputBuilder::default() + let yyyy_mm_dd_input2 = StringInputBuilder::default() .validators(vec![&pattern_validator]) .build()?; @@ -354,11 +354,11 @@ mod test { #[test] fn test_thread_safety() -> Result<(), Box> { - let less_than_1990_input = StrInputBuilder::default() + let less_than_1990_input = StringInputBuilder::default() .validators(vec![&less_than_1990]) .build()?; - let ymd_input = StrInputBuilder::default() + let ymd_input = StringInputBuilder::default() .validators(vec![&ymd_check]) .build()?; @@ -423,12 +423,12 @@ mod test { Ok(()) }; - let less_than_1990_input = StrInputBuilder::default() + let less_than_1990_input = StringInputBuilder::default() .validators(vec![&less_than_1990]) .filters(vec![&to_last_date_of_month]) .build()?; - let ymd_input = StrInputBuilder::default() + let ymd_input = StringInputBuilder::default() .validators(vec![&ymd_check]) .build()?; @@ -478,7 +478,7 @@ mod test { #[test] fn test_validate_and_filter() { - let input = StrInputBuilder::default() + let input = StringInputBuilder::default() .name("hello") .required(true) .validators(vec![&less_than_1990]) @@ -509,7 +509,7 @@ mod test { } }; - let _input = StrInputBuilder::default() + let _input = StringInputBuilder::default() .name("hello") .validators(vec![&callback1]) .build() @@ -518,7 +518,7 @@ mod test { #[test] fn test_display() { - let input = StrInputBuilder::default() + let input = StringInputBuilder::default() .name("hello") .validators(vec![&less_than_1990]) .build() diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index 1dd9d9f..9a87350 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -26,9 +26,10 @@ impl InputValue for bool {} impl InputValue for char {} impl InputValue for str {} + impl InputValue for &str {} -pub trait NumberValue: Default + InputValue + Copy + Add + Sub + Mul + Div + Rem {} +pub trait NumberValue: InputValue + Default + Copy + Add + Sub + Mul + Div + Rem {} impl NumberValue for i8 {} impl NumberValue for i16 {} @@ -75,6 +76,8 @@ pub type Filter = dyn Fn(Option) -> Option + Send + Sync; pub type Validator = dyn Fn(T) -> ValidationResult + Send + Sync; pub trait ValidateValue { + /// @todo Should accept `&T`, or `Cow`, here, instead of `T` (will allow overall types + /// to work with (seamlessly) with unsized (`?Sized`) types. fn validate(&self, value: T) -> ValidationResult; } @@ -102,7 +105,7 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// ```rust /// use walrs_inputfilter::*; /// - /// let input = StrInputBuilder::default() + /// let input = StringInputBuilder::default() /// .required(true) /// .value_missing(&|_, _| "Value missing".to_string()) /// .min_length(3usize) @@ -203,7 +206,7 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// assert_eq!(input.validate(Some(&95)), Ok(())); /// assert_eq!(input.validate(Some(&45)), Err(vec![(CustomError, "\"45\" not allowed".to_string())])); /// - /// let str_input = StrInputBuilder::default() + /// let str_input = StringInputBuilder::default() /// .required(true) /// .value_missing(&|_, _| "Value missing".to_string()) /// .min_length(3usize) @@ -264,7 +267,7 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// ```rust /// use walrs_inputfilter::*; /// - /// let input = StrInputBuilder::default() + /// let input = StringInputBuilder::default() /// .required(true) /// .value_missing(&|_, _| "Value missing".to_string()) /// .validators(vec![&|x: &str| { @@ -309,7 +312,7 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// use walrs_inputfilter::*; /// use std::borrow::Cow; /// - /// let input = StrInputBuilder::default() + /// let input = StringInputBuilder::default() /// .required(true) /// .value_missing(&|_, _| "Value missing".to_string()) /// .validators(vec![&|x: &str| { diff --git a/inputfilter/src/validator/pattern.rs b/inputfilter/src/validator/pattern.rs index de545ed..8492edd 100644 --- a/inputfilter/src/validator/pattern.rs +++ b/inputfilter/src/validator/pattern.rs @@ -84,44 +84,6 @@ impl Fn<(&&str, )> for PatternValidator<'_> { } } -impl FnOnce<(Box, )> for PatternValidator<'_> { - type Output = ValidationResult; - - extern "rust-call" fn call_once(self, args: (Box, )) -> Self::Output { - self.validate(&args.0) - } -} - -impl FnMut<(Box, )> for PatternValidator<'_> { - extern "rust-call" fn call_mut(&mut self, args: (Box, )) -> Self::Output { - self.validate(&args.0) - } -} - -impl Fn<(Box, )> for PatternValidator<'_> { - extern "rust-call" fn call(&self, args: (Box, )) -> Self::Output { - self.validate(&args.0) - } -} -impl FnOnce<(&Box, )> for PatternValidator<'_> { - type Output = ValidationResult; - - extern "rust-call" fn call_once(self, args: (&Box, )) -> Self::Output { - self.validate(&args.0) - } -} - -impl FnMut<(&Box, )> for PatternValidator<'_> { - extern "rust-call" fn call_mut(&mut self, args: (&Box, )) -> Self::Output { - self.validate(&args.0) - } -} - -impl Fn<(&Box, )> for PatternValidator<'_> { - extern "rust-call" fn call(&self, args: (&Box, )) -> Self::Output { - self.validate(&args.0) - } -} impl FnOnce<(&String, )> for PatternValidator<'_> { type Output = ValidationResult; From 1532e6ebc6c31c3288edca82f5470fc856e2ec2f Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Fri, 24 Nov 2023 18:25:08 -0500 Subject: [PATCH 08/38] inputfilter - [unstable] Backup on progress on updating 'InputConstraints' trait to support associated types for Validator 'T', and Filter 'T' types. --- inputfilter/src/lib.rs | 2 + inputfilter/src/number_input.rs | 241 ++++++++++++++++++++++++++++++++ inputfilter/src/types.rs | 26 ++-- 3 files changed, 258 insertions(+), 11 deletions(-) create mode 100644 inputfilter/src/number_input.rs diff --git a/inputfilter/src/lib.rs b/inputfilter/src/lib.rs index b498e82..baf1d41 100644 --- a/inputfilter/src/lib.rs +++ b/inputfilter/src/lib.rs @@ -1,5 +1,6 @@ #![feature(fn_traits)] #![feature(unboxed_closures)] +#![feature(associated_type_defaults)] #[macro_use] extern crate derive_builder; @@ -9,6 +10,7 @@ pub mod validator; pub mod input; pub mod filter; pub mod string_input; +pub mod number_input; pub use types::*; pub use validator::*; diff --git a/inputfilter/src/number_input.rs b/inputfilter/src/number_input.rs new file mode 100644 index 0000000..fcb9527 --- /dev/null +++ b/inputfilter/src/number_input.rs @@ -0,0 +1,241 @@ +use std::borrow::Cow; +use std::fmt::{Debug, Display, Formatter}; + +use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; +use crate::{ConstraintViolation, NumberValue, ValidationError, ValidationResult}; + +pub type NumMissingViolationCallback = dyn Fn(&NumberInput, Option) -> ViolationMessage + Send + Sync; + +pub fn range_underflow_msg(rules: &NumberInput, x: Option) -> String { + format!( + "`{:}` is less than minimum `{:}`.", + x.unwrap(), + &rules.min.unwrap() + ) +} + +pub fn range_overflow_msg(rules: &NumberInput, x: Option) -> String { + format!( + "`{:}` is greater than maximum `{:}`.", + x.unwrap(), + &rules.max.unwrap() + ) +} + +pub fn step_mismatch_msg( + rules: &NumberInput, + x: Option, +) -> String { + format!( + "`{:}` is not divisible by `{:}`.", + x.unwrap(), + &rules.step.unwrap() + ) +} + +pub fn num_not_equal_msg( + rules: &NumberInput, + x: Option, +) -> String { + format!( + "`{:}` is not equal to `{:}`.", + x.unwrap(), + &rules.equal.unwrap() + ) +} + +pub fn num_missing_msg(_: &NumberInput, _: Option) -> String { + "Value is missing.".to_string() +} + +#[derive(Builder, Clone)] +#[builder(setter(strip_option))] +pub struct NumberInput<'a, T: NumberValue> { + #[builder(default = "true")] + pub break_on_failure: bool, + + /// @todo This should be an `Option>`, for compatibility. + #[builder(setter(into), default = "None")] + pub name: Option<&'a str>, + + #[builder(default = "None")] + pub min: Option, + + #[builder(default = "None")] + pub max: Option, + + #[builder(default = "None")] + pub step: Option, + + #[builder(default = "None")] + pub equal: Option, + + #[builder(default = "false")] + pub required: bool, + + #[builder(default = "None")] + pub validators: Option>>, + + // @todo Add support for `io_validators` (e.g., validators that return futures). + + #[builder(default = "None")] + pub filters: Option>>, + + #[builder(default = "&range_underflow_msg")] + pub range_underflow: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), + + #[builder(default = "&range_overflow_msg")] + pub range_overflow: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), + + #[builder(default = "&step_mismatch_msg")] + pub step_mismatch: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), + + #[builder(default = "&num_not_equal_msg")] + pub not_equal: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), + + #[builder(default = "&num_missing_msg")] + pub value_missing: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync) +} + +impl<'a, T> NumberInput<'a, T> + where T: NumberValue +{ + pub fn new(name: Option<&'a str>) -> Self { + NumberInput { + break_on_failure: false, + name, + min: None, + max: None, + equal: None, + required: false, + validators: None, + filters: None, + range_underflow: &(range_underflow_msg), + range_overflow: &(range_overflow_msg), + not_equal: &(num_not_equal_msg), + value_missing: &num_missing_msg, + step: None, + step_mismatch: &(step_mismatch_msg), + } + } +} + +impl<'a, 'b: 'a, T> InputConstraints<'a, 'b, T> for NumberInput<'a, T> +where T: NumberValue +{ + fn get_should_break_on_failure(&self) -> bool { + self.break_on_failure + } + + fn get_required(&self) -> bool { + self.required + } + + fn get_name(&self) -> Option> { + self.name.map(move |s: &'a str| Cow::Borrowed(s)) + } + + fn get_value_missing_handler(&self) -> &'a NumMissingViolationCallback { + self.value_missing + } + + fn get_validators(&self) -> Option<&[&'a Validator]> { + self.validators.as_deref() + } + + fn get_filters(&self) -> Option<&[&'a Filter]> { + self.filters.as_deref() + } + + fn validate_custom(&self, value: T) -> Result<(), Vec> { + let mut errs = vec![]; + + // Test lower bound + if let Some(min) = self.min { + if value < min { + errs.push(( + ConstraintViolation::RangeUnderflow, + (&self.range_underflow)(self, Some(value)), + )); + + if self.break_on_failure { return Err(errs); } + } + } + + // Test upper bound + if let Some(max) = self.max { + if value > max { + errs.push(( + ConstraintViolation::TooLong, + (&self.range_overflow)(self, Some(value)), + )); + + if self.break_on_failure { return Err(errs); } + } + } + + // Test equality + if let Some(equal) = self.equal { + if value != equal { + errs.push(( + ConstraintViolation::NotEqual, + (&self.not_equal)(self, Some(value)), + )); + + if self.break_on_failure { return Err(errs); } + } + } + + // Test Step + if let Some(step) = self.step { + if step != Default::default() && value % step != Default::default() { + errs.push(( + ConstraintViolation::StepMismatch, + (&self.step_mismatch)(self, Some(value)) + )); + + if self.break_on_failure { return Err(errs); } + } + } + + if errs.is_empty() { Ok(()) } + else { Err(errs) } + } +} + +impl Default for NumberInput<'_, T> { + fn default() -> Self { + Self::new(None) + } +} + +impl Display for NumberInput<'_, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "StrInput {{ name: {}, required: {}, validators: {}, filters: {} }}", + self.name.unwrap_or("None"), + self.required, + self + .validators + .as_deref() + .map(|vs| format!("Some([Validator; {}])", vs.len())) + .unwrap_or("None".to_string()), + self + .filters + .as_deref() + .map(|fs| format!("Some([Filter; {}])", fs.len())) + .unwrap_or("None".to_string()), + ) + } +} + +impl Debug for NumberInput<'_, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self) + } +} + +#[cfg(test)] +mod test { +} diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index 9a87350..ed08309 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -29,7 +29,7 @@ impl InputValue for str {} impl InputValue for &str {} -pub trait NumberValue: InputValue + Default + Copy + Add + Sub + Mul + Div + Rem {} +pub trait NumberValue: Default + InputValue + Copy + Add + Sub + Mul + Div + Rem {} impl NumberValue for i8 {} impl NumberValue for i16 {} @@ -91,13 +91,17 @@ pub trait ToAttributesList { } } -pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + PartialEq + PartialOrd + Serialize + ?Sized>: Display + Debug + 'a { +pub trait InputConstraints<'a, 'call_ctx: 'a, T>: Display + Debug + 'a + where T: ToOwned + Debug + Display + PartialEq + PartialOrd + Serialize + ?Sized { + type ValidatorT = &'call_ctx T; + type FilterT = Cow<'call_ctx, T>; + fn get_should_break_on_failure(&self) -> bool; fn get_required(&self) -> bool; fn get_name(&self) -> Option>; fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self, Option<&T>) -> ViolationMessage + Send + Sync); - fn get_validators(&self) -> Option<&[&'a Validator<&'call_ctx T>]>; - fn get_filters(&self) -> Option<&[&'a Filter>]>; + fn get_validators(&self) -> Option<&[&'a Validator]>; + fn get_filters(&self) -> Option<&[&'a Filter]>; /// Validates value using implementing structs own custom validation logic (e.g., using it's own "custom" properties etc.). /// Note: Gets called in `InputConstraints::validate` method, before any set validators are run. @@ -122,10 +126,10 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// assert_eq!(input.validate1(Some(&too_long_str)), Err(vec!["Too long".to_string()])); /// assert_eq!(input.validate1(None), Err(vec!["Value missing".to_string()])); /// ``` - fn validate_custom(&self, value: &'call_ctx T) -> Result<(), Vec>; + fn validate_custom(&self, value: Self::ValidatorT) -> Result<(), Vec>; /// Validates value against contained validators. - fn validate_with_validators(&self, value: &'call_ctx T, validators: Option<&[&'a Validator<&'call_ctx T>]>) -> Result<(), Vec> { + fn validate_with_validators(&self, value: Self::ValidatorT, validators: Option<&[&'a Validator]>) -> Result<(), Vec> { validators.map(|vs| { // If not break on failure then capture all validation errors. @@ -230,7 +234,7 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// assert_eq!(str_input.validate(Some(&"abc")), Err(vec![ (TypeMismatch, "Invalid email".to_string()) ])); /// assert_eq!(str_input.validate(Some(&"abc@def")), Ok(())); /// ``` - fn validate(&self, value: Option<&'call_ctx T>) -> ValidationResult { + fn validate(&self, value: Option) -> ValidationResult { match value { None => { if self.get_required() { @@ -286,7 +290,7 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// assert_eq!(input.validate1(Some(&"ab")), Err(vec!["Too short".to_string()])); /// assert_eq!(input.validate1(None), Err(vec!["Value missing".to_string()])); /// ``` - fn validate1(&self, value: Option<&'call_ctx T>) -> Result<(), Vec> { + fn validate1(&self, value: Option) -> Result<(), Vec> { match self.validate(value) { Err(messages) => Err(messages.into_iter().map(|(_, message)| message).collect()), @@ -294,14 +298,14 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par } } - fn filter(&self, value: Option>) -> Option> { + fn filter(&self, value: Option) -> Option { match self.get_filters() { None => value, Some(fs) => fs.iter().fold(value, |agg, f| (f)(agg)), } } - fn validate_and_filter(&self, x: Option<&'call_ctx T>) -> Result>, Vec> { + fn validate_and_filter(&self, x: Option) -> Result, Vec> { self.validate(x).map(|_| self.filter(x.map(|_x| Cow::Borrowed(_x)))) } @@ -335,7 +339,7 @@ pub trait InputConstraints<'a, 'call_ctx: 'a, T: ToOwned + Debug + Display + Par /// assert_eq!(input.validate_and_filter1(Some(&"Abba")), Ok(Some("Abba".to_lowercase().into()))); /// assert_eq!(input.validate_and_filter1(None), Err(vec!["Value missing".to_string()])); /// ``` - fn validate_and_filter1(&self, x: Option<&'call_ctx T>) -> Result>, Vec> { + fn validate_and_filter1(&self, x: Option) -> Result, Vec> { match self.validate_and_filter(x) { Err(messages) => Err(messages.into_iter().map(|(_, message)| message).collect()), From 1eafb797bb62fb7093b7787c68ebec7b835de64a Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Mon, 4 Dec 2023 14:10:50 -0500 Subject: [PATCH 09/38] inputfilter - [backup, unstable] - Backup on resolving methodlogy for inputfilter structs. --- inputfilter/DESIGN.md | 2 +- inputfilter/src/number_input.rs | 4 ++-- inputfilter/src/types.rs | 13 ++++--------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/inputfilter/DESIGN.md b/inputfilter/DESIGN.md index 5b20fcd..cdbaeea 100644 --- a/inputfilter/DESIGN.md +++ b/inputfilter/DESIGN.md @@ -5,7 +5,7 @@ Controls here should: - not be stateful - In the sense of 'changing' state; E.g., should not hold on to/mutate values. -- Should only work with primitive values; E.g., scalars, array, vector, hash_map, etc. (note we can support arbitrary structures (later) via derive macros). +- Should only work with primitive values; E.g., scalars, array, vector, hash_map, etc. (note we can support arbitrary structures (later) via derive macros, etc.). ## Inspiration diff --git a/inputfilter/src/number_input.rs b/inputfilter/src/number_input.rs index fcb9527..3e94b6e 100644 --- a/inputfilter/src/number_input.rs +++ b/inputfilter/src/number_input.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use std::fmt::{Debug, Display, Formatter}; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; -use crate::{ConstraintViolation, NumberValue, ValidationError, ValidationResult}; +use crate::{ConstraintViolation, NumberValue, ValidationError}; pub type NumMissingViolationCallback = dyn Fn(&NumberInput, Option) -> ViolationMessage + Send + Sync; @@ -120,7 +120,7 @@ impl<'a, T> NumberInput<'a, T> } } -impl<'a, 'b: 'a, T> InputConstraints<'a, 'b, T> for NumberInput<'a, T> +impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T> for NumberInput<'a, T> where T: NumberValue { fn get_should_break_on_failure(&self) -> bool { diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index ed08309..06b58c5 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -58,9 +58,6 @@ pub enum ConstraintViolation { TooLong, TooShort, NotEqual, - - /// Used to convey an expected string format (not necessarily a `Pattern` format; - /// E.g., invalid email hostname in email pattern, etc.). TypeMismatch, ValueMissing, } @@ -75,13 +72,13 @@ pub type Filter = dyn Fn(Option) -> Option + Send + Sync; pub type Validator = dyn Fn(T) -> ValidationResult + Send + Sync; -pub trait ValidateValue { +pub trait ValidateValue { /// @todo Should accept `&T`, or `Cow`, here, instead of `T` (will allow overall types /// to work with (seamlessly) with unsized (`?Sized`) types. fn validate(&self, value: T) -> ValidationResult; } -pub trait FilterValue { +pub trait FilterValue { fn filter(&self, value: Option>) -> Option>; } @@ -91,10 +88,8 @@ pub trait ToAttributesList { } } -pub trait InputConstraints<'a, 'call_ctx: 'a, T>: Display + Debug + 'a - where T: ToOwned + Debug + Display + PartialEq + PartialOrd + Serialize + ?Sized { - type ValidatorT = &'call_ctx T; - type FilterT = Cow<'call_ctx, T>; +pub trait InputConstraints<'a, 'call_ctx, T: 'call_ctx>: Display + Debug + where T: ToOwned + Debug + Display + PartialEq + PartialOrd + Serialize { fn get_should_break_on_failure(&self) -> bool; fn get_required(&self) -> bool; From 62ddee11c0ba81158cf3b54667d76e1b0446a4e9 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Mon, 4 Dec 2023 20:59:24 -0500 Subject: [PATCH 10/38] inputfilter - progress on refactor to simplify related traits ('InputConstraints' type), and their usages. --- inputfilter/src/input.rs | 50 +++-- inputfilter/src/number_input.rs | 338 ++++++++++++++++---------------- inputfilter/src/string_input.rs | 54 +++-- inputfilter/src/types.rs | 276 ++------------------------ 4 files changed, 232 insertions(+), 486 deletions(-) diff --git a/inputfilter/src/input.rs b/inputfilter/src/input.rs index 408587a..2e60f6a 100644 --- a/inputfilter/src/input.rs +++ b/inputfilter/src/input.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use std::fmt::{Debug, Display, Formatter}; use crate::types::{Filter, InputConstraints, InputValue, Validator, ViolationMessage}; -use crate::ValidationResult; +use crate::{ValidationErrTuple, ValidationResult}; pub type ValueMissingViolationCallback = dyn Fn(&Input) -> ViolationMessage + Send + Sync; @@ -50,35 +50,31 @@ where value_missing: &value_missing_msg, } } -} - -impl<'a, 'b, T: InputValue + ?Sized> InputConstraints<'a, 'b, T> for Input<'a, 'b, T> { - fn get_should_break_on_failure(&self) -> bool { - self.break_on_failure - } - fn get_required(&self) -> bool { - self.required + fn validate_custom(&self, _: &'b T) -> ValidationResult { + Ok(()) } +} - fn get_name(&self) -> Option> { - self.name.map(move |s: &'a str| Cow::Borrowed(s)) +impl<'a, 'b, T: InputValue + ?Sized, FT> InputConstraints<'a, 'b, T, FT> for Input<'a, 'b, T> { + fn validate(&self, value: Option) -> Result<(), Vec> { + todo!() } - fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self, Option<&T>) -> ViolationMessage + Send + Sync) { - self.value_missing + fn validate1(&self, value: Option) -> Result<(), Vec> { + todo!() } - fn get_validators(&self) -> Option<&[&'a Validator<&'b T>]> { - self.validators.as_deref() + fn filter(&self, value: FT) -> FT { + todo!() } - fn get_filters(&self) -> Option<&[&'a Filter>]> { - self.filters.as_deref() + fn validate_and_filter(&self, x: Option) -> Result, Vec> { + todo!() } - fn validate_custom(&self, _: &'b T) -> ValidationResult { - Ok(()) + fn validate_and_filter1(&self, x: Option) -> Result, Vec> { + todo!() } } @@ -210,7 +206,7 @@ mod test { } // `Rem` (Remainder) trait check - match even_from_0_to_100_input.validate(Some(&3)) { + match even_from_0_to_100_input.validate(Some(3)) { Err(errs) => errs.iter().for_each(|v_err| { assert_eq!(v_err.0, ConstraintViolation::StepMismatch); assert_eq!(v_err.1, step_mismatch_msg(&even_0_to_100, 3)); @@ -282,7 +278,7 @@ mod test { let handle = thread::spawn( - move || match usize_input_instance.validate(Some(&101)) { + move || match usize_input_instance.validate(Some(101)) { Err(x) => { assert_eq!(x[0].1.as_str(), unsized_less_than_100_msg(101)); } @@ -366,7 +362,7 @@ mod test { thread::scope(|scope| { scope.spawn( - || match usize_input_instance.validate(Some(&101)) { + || match usize_input_instance.validate(Some(101)) { Err(x) => { assert_eq!(x[0].1.as_str(), &unsized_less_than_100_msg(101)); } @@ -375,7 +371,7 @@ mod test { ); scope.spawn( - || match usize_input_instance.validate_and_filter(Some(&99)) { + || match usize_input_instance.validate_and_filter(Some(99)) { Err(err) => panic!("Expected `Ok(Some({:#?})`; Received `Err({:#?})`", Cow::::Owned(99 * 2), err), Ok(Some(x)) => assert_eq!(x, Cow::::Owned(99 * 2)), @@ -384,7 +380,7 @@ mod test { ); scope.spawn( - || match usize_input2_instance.validate(Some(&101)) { + || match usize_input2_instance.validate(Some(101)) { Err(x) => { assert_eq!(x[0].1.as_str(), &range_overflow_msg(&unsized_one_to_one_hundred, 101)); } @@ -393,7 +389,7 @@ mod test { ); scope.spawn( - || match usize_input2_instance.validate_and_filter(Some(&99)) { + || match usize_input2_instance.validate_and_filter(Some(99)) { Err(err) => panic!("Expected `Ok(Some({:#?})`; Received `Err({:#?})`", Cow::::Owned(99 * 2), err), Ok(Some(x)) => assert_eq!(x, Cow::::Owned(99 * 2)), @@ -430,8 +426,8 @@ mod test { .build() .unwrap(); - assert_eq!(input.validate_and_filter(Some(&101)), Err(vec![(RangeOverflow, unsized_less_than_100_msg(101))])); - assert_eq!(input.validate_and_filter(Some(&99)), Ok(Some(Cow::Borrowed(&(99 * 2))))); + assert_eq!(input.validate_and_filter(Some(101)), Err(vec![(RangeOverflow, unsized_less_than_100_msg(101))])); + assert_eq!(input.validate_and_filter(Some(99)), Ok(Some(Cow::Borrowed(&(99 * 2))))); } #[test] diff --git a/inputfilter/src/number_input.rs b/inputfilter/src/number_input.rs index 3e94b6e..0e57157 100644 --- a/inputfilter/src/number_input.rs +++ b/inputfilter/src/number_input.rs @@ -2,240 +2,234 @@ use std::borrow::Cow; use std::fmt::{Debug, Display, Formatter}; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; -use crate::{ConstraintViolation, NumberValue, ValidationError}; +use crate::{ConstraintViolation, NumberValue, ValidationErrTuple}; pub type NumMissingViolationCallback = dyn Fn(&NumberInput, Option) -> ViolationMessage + Send + Sync; pub fn range_underflow_msg(rules: &NumberInput, x: Option) -> String { - format!( - "`{:}` is less than minimum `{:}`.", - x.unwrap(), - &rules.min.unwrap() - ) + format!( + "`{:}` is less than minimum `{:}`.", + x.unwrap(), + &rules.min.unwrap() + ) } pub fn range_overflow_msg(rules: &NumberInput, x: Option) -> String { - format!( - "`{:}` is greater than maximum `{:}`.", - x.unwrap(), - &rules.max.unwrap() - ) + format!( + "`{:}` is greater than maximum `{:}`.", + x.unwrap(), + &rules.max.unwrap() + ) } pub fn step_mismatch_msg( - rules: &NumberInput, - x: Option, + rules: &NumberInput, + x: Option, ) -> String { - format!( - "`{:}` is not divisible by `{:}`.", - x.unwrap(), - &rules.step.unwrap() - ) + format!( + "`{:}` is not divisible by `{:}`.", + x.unwrap(), + &rules.step.unwrap() + ) } pub fn num_not_equal_msg( - rules: &NumberInput, - x: Option, + rules: &NumberInput, + x: Option, ) -> String { - format!( - "`{:}` is not equal to `{:}`.", - x.unwrap(), - &rules.equal.unwrap() - ) + format!( + "`{:}` is not equal to `{:}`.", + x.unwrap(), + &rules.equal.unwrap() + ) } pub fn num_missing_msg(_: &NumberInput, _: Option) -> String { - "Value is missing.".to_string() + "Value is missing.".to_string() } #[derive(Builder, Clone)] #[builder(setter(strip_option))] pub struct NumberInput<'a, T: NumberValue> { - #[builder(default = "true")] - pub break_on_failure: bool, + #[builder(default = "true")] + pub break_on_failure: bool, - /// @todo This should be an `Option>`, for compatibility. - #[builder(setter(into), default = "None")] - pub name: Option<&'a str>, + /// @todo This should be an `Option>`, for compatibility. + #[builder(setter(into), default = "None")] + pub name: Option<&'a str>, - #[builder(default = "None")] - pub min: Option, + #[builder(default = "None")] + pub min: Option, - #[builder(default = "None")] - pub max: Option, + #[builder(default = "None")] + pub max: Option, - #[builder(default = "None")] - pub step: Option, + #[builder(default = "None")] + pub step: Option, - #[builder(default = "None")] - pub equal: Option, + #[builder(default = "None")] + pub equal: Option, - #[builder(default = "false")] - pub required: bool, + #[builder(default = "false")] + pub required: bool, - #[builder(default = "None")] - pub validators: Option>>, + #[builder(default = "None")] + pub validators: Option>>, - // @todo Add support for `io_validators` (e.g., validators that return futures). + // @todo Add support for `io_validators` (e.g., validators that return futures). - #[builder(default = "None")] - pub filters: Option>>, + #[builder(default = "None")] + pub filters: Option>>, - #[builder(default = "&range_underflow_msg")] - pub range_underflow: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), + #[builder(default = "&range_underflow_msg")] + pub range_underflow: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), - #[builder(default = "&range_overflow_msg")] - pub range_overflow: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), + #[builder(default = "&range_overflow_msg")] + pub range_overflow: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), - #[builder(default = "&step_mismatch_msg")] - pub step_mismatch: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), + #[builder(default = "&step_mismatch_msg")] + pub step_mismatch: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), - #[builder(default = "&num_not_equal_msg")] - pub not_equal: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), + #[builder(default = "&num_not_equal_msg")] + pub not_equal: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), - #[builder(default = "&num_missing_msg")] - pub value_missing: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync) + #[builder(default = "&num_missing_msg")] + pub value_missing: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), } impl<'a, T> NumberInput<'a, T> - where T: NumberValue + where T: NumberValue { - pub fn new(name: Option<&'a str>) -> Self { - NumberInput { - break_on_failure: false, - name, - min: None, - max: None, - equal: None, - required: false, - validators: None, - filters: None, - range_underflow: &(range_underflow_msg), - range_overflow: &(range_overflow_msg), - not_equal: &(num_not_equal_msg), - value_missing: &num_missing_msg, - step: None, - step_mismatch: &(step_mismatch_msg), + pub fn new(name: Option<&'a str>) -> Self { + NumberInput { + break_on_failure: false, + name, + min: None, + max: None, + equal: None, + required: false, + validators: None, + filters: None, + range_underflow: &(range_underflow_msg), + range_overflow: &(range_overflow_msg), + not_equal: &(num_not_equal_msg), + value_missing: &num_missing_msg, + step: None, + step_mismatch: &(step_mismatch_msg), + } } - } + fn validate_custom(&self, value: T) -> Result<(), Vec> { + let mut errs = vec![]; + + // Test lower bound + if let Some(min) = self.min { + if value < min { + errs.push(( + ConstraintViolation::RangeUnderflow, + (&self.range_underflow)(self, Some(value)), + )); + + if self.break_on_failure { return Err(errs); } + } + } + + // Test upper bound + if let Some(max) = self.max { + if value > max { + errs.push(( + ConstraintViolation::TooLong, + (&self.range_overflow)(self, Some(value)), + )); + + if self.break_on_failure { return Err(errs); } + } + } + + // Test equality + if let Some(equal) = self.equal { + if value != equal { + errs.push(( + ConstraintViolation::NotEqual, + (&self.not_equal)(self, Some(value)), + )); + + if self.break_on_failure { return Err(errs); } + } + } + + // Test Step + if let Some(step) = self.step { + if step != Default::default() && value % step != Default::default() { + errs.push(( + ConstraintViolation::StepMismatch, + (&self.step_mismatch)(self, Some(value)) + )); + + if self.break_on_failure { return Err(errs); } + } + } + + if errs.is_empty() { Ok(()) } else { Err(errs) } + } + } -impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T> for NumberInput<'a, T> -where T: NumberValue +impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for NumberInput<'a, T> + where T: NumberValue { - fn get_should_break_on_failure(&self) -> bool { - self.break_on_failure - } - - fn get_required(&self) -> bool { - self.required - } - - fn get_name(&self) -> Option> { - self.name.map(move |s: &'a str| Cow::Borrowed(s)) - } - - fn get_value_missing_handler(&self) -> &'a NumMissingViolationCallback { - self.value_missing - } - - fn get_validators(&self) -> Option<&[&'a Validator]> { - self.validators.as_deref() - } - - fn get_filters(&self) -> Option<&[&'a Filter]> { - self.filters.as_deref() - } - - fn validate_custom(&self, value: T) -> Result<(), Vec> { - let mut errs = vec![]; - - // Test lower bound - if let Some(min) = self.min { - if value < min { - errs.push(( - ConstraintViolation::RangeUnderflow, - (&self.range_underflow)(self, Some(value)), - )); - - if self.break_on_failure { return Err(errs); } - } + fn validate(&self, value: Option) -> Result<(), Vec> { + todo!() } - // Test upper bound - if let Some(max) = self.max { - if value > max { - errs.push(( - ConstraintViolation::TooLong, - (&self.range_overflow)(self, Some(value)), - )); - - if self.break_on_failure { return Err(errs); } - } + fn validate1(&self, value: Option) -> Result<(), Vec> { + todo!() } - // Test equality - if let Some(equal) = self.equal { - if value != equal { - errs.push(( - ConstraintViolation::NotEqual, - (&self.not_equal)(self, Some(value)), - )); - - if self.break_on_failure { return Err(errs); } - } + fn filter(&self, value: T) -> T { + todo!() } - // Test Step - if let Some(step) = self.step { - if step != Default::default() && value % step != Default::default() { - errs.push(( - ConstraintViolation::StepMismatch, - (&self.step_mismatch)(self, Some(value)) - )); - - if self.break_on_failure { return Err(errs); } - } + fn validate_and_filter(&self, x: Option) -> Result, Vec> { + self.validate(x).map(|_| self.filter(x)) } - if errs.is_empty() { Ok(()) } - else { Err(errs) } - } + fn validate_and_filter1(&self, x: Option) -> Result, Vec> { + todo!() + } } impl Default for NumberInput<'_, T> { - fn default() -> Self { - Self::new(None) - } + fn default() -> Self { + Self::new(None) + } } impl Display for NumberInput<'_, T> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "StrInput {{ name: {}, required: {}, validators: {}, filters: {} }}", - self.name.unwrap_or("None"), - self.required, - self - .validators - .as_deref() - .map(|vs| format!("Some([Validator; {}])", vs.len())) - .unwrap_or("None".to_string()), - self - .filters - .as_deref() - .map(|fs| format!("Some([Filter; {}])", fs.len())) - .unwrap_or("None".to_string()), - ) - } + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "StrInput {{ name: {}, required: {}, validators: {}, filters: {} }}", + self.name.unwrap_or("None"), + self.required, + self + .validators + .as_deref() + .map(|vs| format!("Some([Validator; {}])", vs.len())) + .unwrap_or("None".to_string()), + self + .filters + .as_deref() + .map(|fs| format!("Some([Filter; {}])", fs.len())) + .unwrap_or("None".to_string()), + ) + } } impl Debug for NumberInput<'_, T> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", &self) - } + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self) + } } #[cfg(test)] -mod test { -} +mod test {} diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index dce33a4..3662c27 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Display, Formatter}; use regex::Regex; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; -use crate::{ConstraintViolation, ValidationResult}; +use crate::{ConstraintViolation, ValidationErrTuple, ValidationResult}; pub type StrMissingViolationCallback = dyn Fn(&StringInput, Option<&str>) -> ViolationMessage + Send + Sync; @@ -106,34 +106,8 @@ impl<'a, 'b> StringInput<'a, 'b> { value_missing: &str_missing_msg, } } -} - -impl<'a, 'b> InputConstraints<'a, 'b, str> for StringInput<'a, 'b> { - fn get_should_break_on_failure(&self) -> bool { - self.break_on_failure - } - - fn get_required(&self) -> bool { - self.required - } - - fn get_name(&self) -> Option> { - self.name.map(move |s: &'a str| Cow::Borrowed(s)) - } - - fn get_value_missing_handler(&self) -> &'a StrMissingViolationCallback { - self.value_missing - } - - fn get_validators(&self) -> Option<&[&'a Validator<&'b str>]> { - self.validators.as_deref() - } - - fn get_filters(&self) -> Option<&[&'a Filter>]> { - self.filters.as_deref() - } - fn validate_custom(&self, value: &'b str) -> ValidationResult { + fn _validate_against_self(&self, value: &'b str) -> ValidationResult { let mut errs = vec![]; if let Some(min_length) = self.min_length { @@ -163,7 +137,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, str> for StringInput<'a, 'b> { errs.push(( ConstraintViolation::PatternMismatch, (&self. - pattern_mismatch)(self, Some(value)), + pattern_mismatch)(self, Some(value)), )); if self.break_on_failure { return Err(errs); } @@ -186,6 +160,28 @@ impl<'a, 'b> InputConstraints<'a, 'b, str> for StringInput<'a, 'b> { } } +impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, 'b> { + fn validate(&self, value: Option<&'b str>) -> Result<(), Vec> { + todo!() + } + + fn validate1(&self, value: Option<&'b str>) -> Result<(), Vec> { + todo!() + } + + fn filter(&self, value: Cow<'b, str>) -> Cow<'b, str> { + todo!() + } + + fn validate_and_filter(&self, x: Option<&'b str>) -> Result>, Vec> { + Ok(x.into()) + } + + fn validate_and_filter1(&self, x: Option<&'b str>) -> Result>, Vec> { + todo!() + } +} + impl Default for StringInput<'_, '_> { fn default() -> Self { Self::new(None) diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index 06b58c5..87b5290 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -29,7 +29,7 @@ impl InputValue for str {} impl InputValue for &str {} -pub trait NumberValue: Default + InputValue + Copy + Add + Sub + Mul + Div + Rem {} +pub trait NumberValue: InputValue + Default + Copy + Add + Sub + Mul + Div + Rem {} impl NumberValue for i8 {} impl NumberValue for i16 {} @@ -64,22 +64,26 @@ pub enum ConstraintViolation { pub type ViolationMessage = String; -pub type ValidationError = (ConstraintViolation, ViolationMessage); +pub type ValidationErrTuple = (ConstraintViolation, ViolationMessage); -pub type ValidationResult = Result<(), Vec>; +pub type ValidationResult = Result<(), Vec>; -pub type Filter = dyn Fn(Option) -> Option + Send + Sync; +pub type Filter = dyn Fn(T) -> T + Send + Sync; pub type Validator = dyn Fn(T) -> ValidationResult + Send + Sync; +pub type ValueMissingCallback = dyn Fn(RulesStruct) -> ViolationMessage + Send + Sync; + +pub trait WithName<'a> { + fn get_name() -> Option>; +} + pub trait ValidateValue { - /// @todo Should accept `&T`, or `Cow`, here, instead of `T` (will allow overall types - /// to work with (seamlessly) with unsized (`?Sized`) types. fn validate(&self, value: T) -> ValidationResult; } pub trait FilterValue { - fn filter(&self, value: Option>) -> Option>; + fn filter(&self, value: T) -> T; } pub trait ToAttributesList { @@ -88,260 +92,16 @@ pub trait ToAttributesList { } } -pub trait InputConstraints<'a, 'call_ctx, T: 'call_ctx>: Display + Debug - where T: ToOwned + Debug + Display + PartialEq + PartialOrd + Serialize { +pub trait InputConstraints<'a, 'b, T: 'b, FT: 'b>: Display + Debug + where T: InputValue + ?Sized { - fn get_should_break_on_failure(&self) -> bool; - fn get_required(&self) -> bool; - fn get_name(&self) -> Option>; - fn get_value_missing_handler(&self) -> &'a (dyn Fn(&Self, Option<&T>) -> ViolationMessage + Send + Sync); - fn get_validators(&self) -> Option<&[&'a Validator]>; - fn get_filters(&self) -> Option<&[&'a Filter]>; + fn validate(&self, value: Option) -> Result<(), Vec>; - /// Validates value using implementing structs own custom validation logic (e.g., using it's own "custom" properties etc.). - /// Note: Gets called in `InputConstraints::validate` method, before any set validators are run. - /// - /// ```rust - /// use walrs_inputfilter::*; - /// - /// let input = StringInputBuilder::default() - /// .required(true) - /// .value_missing(&|_, _| "Value missing".to_string()) - /// .min_length(3usize) - /// .too_short(&|_, _| "Too short".to_string()) - /// .max_length(55usize) - /// .too_long(&|_, _| "Too long".to_string()) - /// .build() - /// .unwrap() - /// ; - /// - /// let too_long_str = &"ab".repeat(30); - /// - /// assert_eq!(input.validate1(Some(&"ab")), Err(vec!["Too short".to_string()])); - /// assert_eq!(input.validate1(Some(&too_long_str)), Err(vec!["Too long".to_string()])); - /// assert_eq!(input.validate1(None), Err(vec!["Value missing".to_string()])); - /// ``` - fn validate_custom(&self, value: Self::ValidatorT) -> Result<(), Vec>; + fn validate1(&self, value: Option) -> Result<(), Vec>; - /// Validates value against contained validators. - fn validate_with_validators(&self, value: Self::ValidatorT, validators: Option<&[&'a Validator]>) -> Result<(), Vec> { - validators.map(|vs| { + fn filter(&self, value: FT) -> FT; - // If not break on failure then capture all validation errors. - if !self.get_should_break_on_failure() { - return vs.iter().fold( - Vec::::new(), - |mut agg, f| match (f)(value) { - Err(mut message_tuples) => { - agg.append(message_tuples.as_mut()); - agg - } - _ => agg, - }); - } - - // Else break on, and capture, first failure. - let mut agg = Vec::::new(); - for f in vs.iter() { - if let Err(mut message_tuples) = (f)(value) { - agg.append(message_tuples.as_mut()); - break; - } - } - agg - }) - .and_then(|messages| if messages.is_empty() { None } else { Some(messages) }) - .map_or(Ok(()), Err) - } - - /// Validates value against any own `validate_custom` implementation and any set validators - - /// E.g., runs `validate_custom(...)`, then, if it is `Ok`, `validate_with_validators(...)` method. - /// - /// Additionally, note, `break_on_failure` is only guaranteed to be respected for the - /// the validators list, and input filters defined in the library; E.g., It is not guaranteed for - /// `validate_custom()` call in external libraries (e.g., this is left to implementing struct authors). - /// - /// ```rust - /// use walrs_inputfilter::*; - /// use walrs_inputfilter::number::{ - /// NumberValidator, - /// NumberValidatorBuilder, - /// range_overflow_msg, - /// range_underflow_msg, - /// step_mismatch_msg - /// }; - /// use walrs_inputfilter::pattern::PatternValidator; - /// use walrs_inputfilter::types::ConstraintViolation::{ - /// ValueMissing, TooShort, TooLong, TypeMismatch, CustomError, - /// RangeOverflow, RangeUnderflow, StepMismatch - /// }; - /// - /// let num_validator = NumberValidatorBuilder::::default() - /// .min(-100isize) - /// .max(100isize) - /// .step(5) - /// .build() - /// .unwrap(); - /// - /// let input = InputBuilder::::default() - /// .validators(vec![ - /// &num_validator, - /// // Pretend "not allowed" case. - /// &|x: &isize| -> Result<(), Vec> { - /// if *x == 45 { - /// return Err(vec![(CustomError, "\"45\" not allowed".to_string())]); - /// } - /// Ok(()) - /// } - /// ]) - /// .build() - /// .unwrap(); - /// - /// assert_eq!(input.validate(None), Ok(())); - /// assert_eq!(input.validate(Some(&-101)), Err(vec![(RangeUnderflow, range_underflow_msg(&num_validator, -101))])); - /// assert_eq!(input.validate(Some(&101)), Err(vec![(RangeOverflow, range_overflow_msg(&num_validator, 101))])); - /// assert_eq!(input.validate(Some(&100)), Ok(())); - /// assert_eq!(input.validate(Some(&-99)), Err(vec![(StepMismatch, step_mismatch_msg(&num_validator, -99))])); - /// assert_eq!(input.validate(Some(&95)), Ok(())); - /// assert_eq!(input.validate(Some(&45)), Err(vec![(CustomError, "\"45\" not allowed".to_string())])); - /// - /// let str_input = StringInputBuilder::default() - /// .required(true) - /// .value_missing(&|_, _| "Value missing".to_string()) - /// .min_length(3usize) - /// .too_short(&|_, _| "Too short".to_string()) - /// .max_length(200usize) // Default violation message callback used here. - /// // Naive email pattern validator (naive for this example). - /// .validators(vec![&|x: &str| { - /// if !x.contains('@') { - /// return Err(vec![(TypeMismatch, "Invalid email".to_string())]); - /// } - /// Ok(()) - /// }]) - /// .build() - /// .unwrap(); - /// - /// let too_long_str = &"ab".repeat(201); - /// - /// assert_eq!(str_input.validate(None), Err(vec![ (ValueMissing, "Value missing".to_string()) ])); - /// assert_eq!(str_input.validate(Some(&"ab")), Err(vec![ (TooShort, "Too short".to_string()) ])); - /// assert_eq!(str_input.validate(Some(&too_long_str)), Err(vec![ (TooLong, too_long_msg(&str_input, Some(&too_long_str))) ])); - /// assert_eq!(str_input.validate(Some(&"abc")), Err(vec![ (TypeMismatch, "Invalid email".to_string()) ])); - /// assert_eq!(str_input.validate(Some(&"abc@def")), Ok(())); - /// ``` - fn validate(&self, value: Option) -> ValidationResult { - match value { - None => { - if self.get_required() { - Err(vec![( - ConstraintViolation::ValueMissing, - (self.get_value_missing_handler())(self, None), - )]) - } else { - Ok(()) - } - } - // Else if value is populated validate it - Some(v) => match self.validate_custom(v) { - Ok(_) => self.validate_with_validators(v, self.get_validators()), - Err(messages1) => if self.get_should_break_on_failure() { - Err(messages1) - } else { - match self.validate_with_validators(v, self.get_validators()) { - Ok(_) => Ok(()), - Err(mut messages2) => { - let mut agg = messages1; - agg.append(messages2.as_mut()); - Err(agg) - } - } - } - }, - } - } - - /// Special case of `validate` where the error type enums are ignored (in `Err(...)`) result, - /// and only the error messages are returned. - /// - /// ```rust - /// use walrs_inputfilter::*; - /// - /// let input = StringInputBuilder::default() - /// .required(true) - /// .value_missing(&|_, _| "Value missing".to_string()) - /// .validators(vec![&|x: &str| { - /// if x.len() < 3 { - /// return Err(vec![( - /// ConstraintViolation::TooShort, - /// "Too short".to_string(), - /// )]); - /// } - /// Ok(()) - /// }]) - /// .build() - /// .unwrap() - /// ; - /// - /// assert_eq!(input.validate1(Some(&"ab")), Err(vec!["Too short".to_string()])); - /// assert_eq!(input.validate1(None), Err(vec!["Value missing".to_string()])); - /// ``` - fn validate1(&self, value: Option) -> Result<(), Vec> { - match self.validate(value) { - Err(messages) => - Err(messages.into_iter().map(|(_, message)| message).collect()), - Ok(_) => Ok(()), - } - } - - fn filter(&self, value: Option) -> Option { - match self.get_filters() { - None => value, - Some(fs) => fs.iter().fold(value, |agg, f| (f)(agg)), - } - } - - fn validate_and_filter(&self, x: Option) -> Result, Vec> { - self.validate(x).map(|_| self.filter(x.map(|_x| Cow::Borrowed(_x)))) - } - - /// Special case of `validate_and_filter` where the error type enums are ignored (in `Err(...)`) result, - /// and only the error messages are returned, for `Err` case. - /// - /// ```rust - /// use walrs_inputfilter::*; - /// use std::borrow::Cow; - /// - /// let input = StringInputBuilder::default() - /// .required(true) - /// .value_missing(&|_, _| "Value missing".to_string()) - /// .validators(vec![&|x: &str| { - /// if x.len() < 3 { - /// return Err(vec![( - /// ConstraintViolation::TooShort, - /// "Too short".to_string(), - /// )]); - /// } - /// Ok(()) - /// }]) - /// .filters(vec![&|xs: Option>| { - /// xs.map(|xs| Cow::Owned(xs.to_lowercase())) - /// }]) - /// .build() - /// .unwrap() - /// ; - /// - /// assert_eq!(input.validate_and_filter1(Some(&"ab")), Err(vec!["Too short".to_string()])); - /// assert_eq!(input.validate_and_filter1(Some(&"Abba")), Ok(Some("Abba".to_lowercase().into()))); - /// assert_eq!(input.validate_and_filter1(None), Err(vec!["Value missing".to_string()])); - /// ``` - fn validate_and_filter1(&self, x: Option) -> Result, Vec> { - match self.validate_and_filter(x) { - Err(messages) => - Err(messages.into_iter().map(|(_, message)| message).collect()), - Ok(filtered) => Ok(filtered), - } - } + fn validate_and_filter(&self, x: Option) -> Result, Vec>; + fn validate_and_filter1(&self, x: Option) -> Result, Vec>; } - - From b56084c3f1f289b63b1dae246f402e4cf2fa8de4 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:01:20 -0500 Subject: [PATCH 11/38] inputfilter - progress on updating types to use new 'InputConstraints' type: - Progress on updating 'string_input', and 'scalar_input' modules. --- inputfilter/src/input.rs | 16 +- inputfilter/src/lib.rs | 6 +- .../src/{number_input.rs => scalar_input.rs} | 11 +- inputfilter/src/string_input.rs | 1031 ++++++++++------- inputfilter/src/types.rs | 20 +- 5 files changed, 625 insertions(+), 459 deletions(-) rename inputfilter/src/{number_input.rs => scalar_input.rs} (94%) diff --git a/inputfilter/src/input.rs b/inputfilter/src/input.rs index 2e60f6a..45cced6 100644 --- a/inputfilter/src/input.rs +++ b/inputfilter/src/input.rs @@ -12,7 +12,7 @@ pub type ValueMissingViolationCallback = #[builder(pattern = "owned")] pub struct Input<'a, 'b, T> where - T: InputValue + ?Sized, + T: InputValue, { #[builder(default = "true")] pub break_on_failure: bool, @@ -38,7 +38,7 @@ where impl<'a, 'b, T> Input<'a, 'b, T> where - T: InputValue + ?Sized, + T: InputValue, { pub fn new(name: Option<&'a str>) -> Self { Input { @@ -56,7 +56,7 @@ where } } -impl<'a, 'b, T: InputValue + ?Sized, FT> InputConstraints<'a, 'b, T, FT> for Input<'a, 'b, T> { +impl<'a, 'b, T: InputValue, FT: 'b> InputConstraints<'a, 'b, T, FT> for Input<'a, 'b, T> { fn validate(&self, value: Option) -> Result<(), Vec> { todo!() } @@ -65,7 +65,7 @@ impl<'a, 'b, T: InputValue + ?Sized, FT> InputConstraints<'a, 'b, T, FT> for Inp todo!() } - fn filter(&self, value: FT) -> FT { + fn filter(&self, value: Option) -> Option { todo!() } @@ -78,13 +78,13 @@ impl<'a, 'b, T: InputValue + ?Sized, FT> InputConstraints<'a, 'b, T, FT> for Inp } } -impl Default for Input<'_, '_, T> { +impl Default for Input<'_, '_, T> { fn default() -> Self { Self::new(None) } } -impl Display for Input<'_, '_, T> { +impl Display for Input<'_, '_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, @@ -101,13 +101,13 @@ impl Display for Input<'_, '_, T> { } } -impl Debug for Input<'_, '_, T> { +impl Debug for Input<'_, '_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self) } } -pub fn value_missing_msg(_: &Input, _: Option<&T>) -> String { +pub fn value_missing_msg(_: &Input, _: Option<&T>) -> String { "Value is missing.".to_string() } diff --git a/inputfilter/src/lib.rs b/inputfilter/src/lib.rs index baf1d41..d5e4a41 100644 --- a/inputfilter/src/lib.rs +++ b/inputfilter/src/lib.rs @@ -9,14 +9,12 @@ pub mod types; pub mod validator; pub mod input; pub mod filter; +pub mod scalar_input; pub mod string_input; -pub mod number_input; pub use types::*; pub use validator::*; pub use input::*; pub use filter::*; +pub use scalar_input::*; pub use string_input::*; - -// @todo Add 'Builder' for `wal_inputfilter` structs. - diff --git a/inputfilter/src/number_input.rs b/inputfilter/src/scalar_input.rs similarity index 94% rename from inputfilter/src/number_input.rs rename to inputfilter/src/scalar_input.rs index 0e57157..19cb68c 100644 --- a/inputfilter/src/number_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -4,7 +4,7 @@ use std::fmt::{Debug, Display, Formatter}; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; use crate::{ConstraintViolation, NumberValue, ValidationErrTuple}; -pub type NumMissingViolationCallback = dyn Fn(&NumberInput, Option) -> ViolationMessage + Send + Sync; +pub type NumMissingViolationCallback = dyn Fn(&NumberInput, Option) -> ViolationMessage + Send + Sync; pub fn range_underflow_msg(rules: &NumberInput, x: Option) -> String { format!( @@ -79,7 +79,7 @@ pub struct NumberInput<'a, T: NumberValue> { // @todo Add support for `io_validators` (e.g., validators that return futures). #[builder(default = "None")] - pub filters: Option>>, + pub filters: Option>>>, #[builder(default = "&range_underflow_msg")] pub range_underflow: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), @@ -185,8 +185,11 @@ impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for NumberInput<'a, T> todo!() } - fn filter(&self, value: T) -> T { - todo!() + fn filter(&self, value: Option) -> Option { + match self.filters.as_deref() { + None => value, + Some(fs) => fs.iter().fold(value, |agg, f| (f)(agg)), + } } fn validate_and_filter(&self, x: Option) -> Result, Vec> { diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 3662c27..99407db 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -3,526 +3,691 @@ use std::fmt::{Debug, Display, Formatter}; use regex::Regex; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; -use crate::{ConstraintViolation, ValidationErrTuple, ValidationResult}; +use crate::{ConstraintViolation, ValidationErrTuple, ValidationResult, WithName}; pub type StrMissingViolationCallback = dyn Fn(&StringInput, Option<&str>) -> ViolationMessage + Send + Sync; pub fn pattern_mismatch_msg(rules: &StringInput, xs: Option<&str>) -> String { - format!( - "`{}` does not match pattern `{}`", - &xs.as_ref().unwrap(), - rules.pattern.as_ref().unwrap() - ) + format!( + "`{}` does not match pattern `{}`", + &xs.as_ref().unwrap(), + rules.pattern.as_ref().unwrap() + ) } pub fn too_short_msg(rules: &StringInput, xs: Option<&str>) -> String { - format!( - "Value length `{:}` is less than allowed minimum `{:}`.", - &xs.as_ref().unwrap().len(), - &rules.min_length.unwrap_or(0) - ) + format!( + "Value length `{:}` is less than allowed minimum `{:}`.", + &xs.as_ref().unwrap().len(), + &rules.min_length.unwrap_or(0) + ) } pub fn too_long_msg(rules: &StringInput, xs: Option<&str>) -> String { - format!( - "Value length `{:}` is greater than allowed maximum `{:}`.", - &xs.as_ref().unwrap().len(), - &rules.min_length.unwrap_or(0) - ) + format!( + "Value length `{:}` is greater than allowed maximum `{:}`.", + &xs.as_ref().unwrap().len(), + &rules.min_length.unwrap_or(0) + ) } pub fn str_not_equal_msg(rules: &StringInput, _: Option<&str>) -> String { - format!( - "Value is not equal to {}.", - &rules.equal.as_deref().unwrap_or("") - ) + format!( + "Value is not equal to {}.", + &rules.equal.as_deref().unwrap_or("") + ) } #[derive(Builder, Clone)] #[builder(pattern = "owned", setter(strip_option))] pub struct StringInput<'a, 'b> { - #[builder(default = "true")] - pub break_on_failure: bool, - - /// @todo This should be an `Option>`, for compatibility. - #[builder(setter(into), default = "None")] - pub name: Option<&'a str>, + #[builder(default = "true")] + pub break_on_failure: bool, - #[builder(default = "None")] - pub min_length: Option, + #[builder(setter(into), default = "None")] + pub name: Option<&'a str>, - #[builder(default = "None")] - pub max_length: Option, + #[builder(default = "None")] + pub min_length: Option, - #[builder(default = "None")] - pub pattern: Option, + #[builder(default = "None")] + pub max_length: Option, - #[builder(default = "None")] - pub equal: Option<&'b str>, + #[builder(default = "None")] + pub pattern: Option, - #[builder(default = "false")] - pub required: bool, + #[builder(default = "None")] + pub equal: Option<&'b str>, - #[builder(default = "None")] - pub validators: Option>>, + #[builder(default = "false")] + pub required: bool, - // @todo Add support for `io_validators` (e.g., validators that return futures). + #[builder(default = "None")] + pub validators: Option>>, - #[builder(default = "None")] - pub filters: Option>>>, + #[builder(default = "None")] + pub filters: Option>>>>, - #[builder(default = "&too_short_msg")] - pub too_short: &'a StrMissingViolationCallback, + #[builder(default = "&too_short_msg")] + pub too_short: &'a StrMissingViolationCallback, - #[builder(default = "&too_long_msg")] - pub too_long: &'a StrMissingViolationCallback, + #[builder(default = "&too_long_msg")] + pub too_long: &'a StrMissingViolationCallback, - #[builder(default = "&pattern_mismatch_msg")] - pub pattern_mismatch: &'a StrMissingViolationCallback, + #[builder(default = "&pattern_mismatch_msg")] + pub pattern_mismatch: &'a StrMissingViolationCallback, - #[builder(default = "&str_not_equal_msg")] - pub not_equal: &'a StrMissingViolationCallback, + #[builder(default = "&str_not_equal_msg")] + pub not_equal: &'a StrMissingViolationCallback, - #[builder(default = "&str_missing_msg")] - pub value_missing: &'a StrMissingViolationCallback, + #[builder(default = "&str_missing_msg")] + pub value_missing: &'a StrMissingViolationCallback, } impl<'a, 'b> StringInput<'a, 'b> { - pub fn new(name: Option<&'a str>) -> Self { - StringInput { - break_on_failure: false, - name, - min_length: None, - max_length: None, - pattern: None, - equal: None, - required: false, - validators: None, - filters: None, - too_short: &(too_long_msg), - too_long: &(too_long_msg), - pattern_mismatch: &(pattern_mismatch_msg), - not_equal: &(str_not_equal_msg), - value_missing: &str_missing_msg, + pub fn new(name: Option<&'a str>) -> Self { + StringInput { + break_on_failure: false, + name, + min_length: None, + max_length: None, + pattern: None, + equal: None, + required: false, + validators: None, + filters: None, + too_short: &(too_long_msg), + too_long: &(too_long_msg), + pattern_mismatch: &(pattern_mismatch_msg), + not_equal: &(str_not_equal_msg), + value_missing: &str_missing_msg, + } } - } - fn _validate_against_self(&self, value: &'b str) -> ValidationResult { - let mut errs = vec![]; + fn _validate_against_self(&self, value: &'b str) -> ValidationResult { + let mut errs = vec![]; - if let Some(min_length) = self.min_length { - if value.len() < min_length { - errs.push(( - ConstraintViolation::TooShort, - (self.too_short)(self, Some(value)), - )); + if let Some(min_length) = self.min_length { + if value.len() < min_length { + errs.push(( + ConstraintViolation::TooShort, + (self.too_short)(self, Some(value)), + )); - if self.break_on_failure { return Err(errs); } - } - } + if self.break_on_failure { return Err(errs); } + } + } - if let Some(max_length) = self.max_length { - if value.len() > max_length { - errs.push(( - ConstraintViolation::TooLong, - (self.too_long)(self, Some(value)), - )); + if let Some(max_length) = self.max_length { + if value.len() > max_length { + errs.push(( + ConstraintViolation::TooLong, + (self.too_long)(self, Some(value)), + )); - if self.break_on_failure { return Err(errs); } - } - } + if self.break_on_failure { return Err(errs); } + } + } - if let Some(pattern) = &self.pattern { - if !pattern.is_match(value) { - errs.push(( - ConstraintViolation::PatternMismatch, - (&self. - pattern_mismatch)(self, Some(value)), - )); + if let Some(pattern) = &self.pattern { + if !pattern.is_match(value) { + errs.push(( + ConstraintViolation::PatternMismatch, + (&self. + pattern_mismatch)(self, Some(value)), + )); - if self.break_on_failure { return Err(errs); } - } - } + if self.break_on_failure { return Err(errs); } + } + } - if let Some(equal) = &self.equal { - if value != *equal { - errs.push(( - ConstraintViolation::NotEqual, - (&self.not_equal)(self, Some(value)), - )); + if let Some(equal) = &self.equal { + if value != *equal { + errs.push(( + ConstraintViolation::NotEqual, + (&self.not_equal)(self, Some(value)), + )); + + if self.break_on_failure { return Err(errs); } + } + } - if self.break_on_failure { return Err(errs); } - } + if errs.is_empty() { Ok(()) } else { Err(errs) } } - if errs.is_empty() { Ok(()) } - else { Err(errs) } - } + /// Validates value against contained validators. + fn _validate_against_validators(&self, value: &'b str, validators: Option<&[&'a Validator<&'b str>]>) -> Result<(), Vec> { + validators.map(|vs| { + + // If not break on failure then capture all validation errors. + if !self.break_on_failure { + return vs.iter().fold( + Vec::::new(), + |mut agg, f| match (f)(value) { + Err(mut message_tuples) => { + agg.append(message_tuples.as_mut()); + agg + } + _ => agg, + }); + } + + // Else break on, and capture, first failure. + let mut agg = Vec::::new(); + for f in vs.iter() { + if let Err(mut message_tuples) = (f)(value) { + agg.append(message_tuples.as_mut()); + break; + } + } + agg + }) + .and_then(|messages| if messages.is_empty() { None } else { Some(messages) }) + .map_or(Ok(()), Err) + } +} + +impl<'a, 'b> WithName<'a> for StringInput<'a, 'b> { + fn get_name(&self) -> Option> { + self.name.map(|xs| Cow::Borrowed(xs)) + } } impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, 'b> { - fn validate(&self, value: Option<&'b str>) -> Result<(), Vec> { - todo!() - } + /// Validates value against any own `validate_custom` implementation and any set validators - + /// E.g., runs `validate_custom(...)`, then, if it is `Ok`, `validate_against_validators(...)` method. + /// + /// Additionally, note, `break_on_failure` is only guaranteed to be respected for the + /// the validators list, and input filters defined in the library; E.g., It is not guaranteed for + /// `validate_custom()` call in external libraries (e.g., this is left to implementing struct authors). + /// + /// ```rust + /// use walrs_inputfilter::*; + /// use walrs_inputfilter::pattern::PatternValidator; + /// use walrs_inputfilter::types::ConstraintViolation::{ + /// ValueMissing, TooShort, TooLong, TypeMismatch, CustomError, + /// RangeOverflow, RangeUnderflow, StepMismatch + /// }; + /// + /// let str_input = StringInputBuilder::default() + /// .required(true) + /// .value_missing(&|_, _| "Value missing".to_string()) + /// .min_length(3usize) + /// .too_short(&|_, _| "Too short".to_string()) + /// .max_length(200usize) // Default violation message callback used here. + /// // Naive email pattern validator (naive for this example). + /// .validators(vec![&|x: &str| { + /// if !x.contains('@') { + /// return Err(vec![(TypeMismatch, "Invalid email".to_string())]); + /// } + /// Ok(()) + /// }]) + /// .build() + /// .unwrap(); + /// + /// let too_long_str = &"ab".repeat(201); + /// + /// assert_eq!(str_input.validate(None), Err(vec![ (ValueMissing, "Value missing".to_string()) ])); + /// assert_eq!(str_input.validate(Some(&"ab")), Err(vec![ (TooShort, "Too short".to_string()) ])); + /// assert_eq!(str_input.validate(Some(&too_long_str)), Err(vec![ (TooLong, too_long_msg(&str_input, Some(&too_long_str))) ])); + /// assert_eq!(str_input.validate(Some(&"abc")), Err(vec![ (TypeMismatch, "Invalid email".to_string()) ])); + /// assert_eq!(str_input.validate(Some(&"abc@def")), Ok(())); + /// ``` + fn validate(&self, value: Option<&'b str>) -> Result<(), Vec> { + match value { + None => { + if self.required { + Err(vec![( + ConstraintViolation::ValueMissing, + (&self.value_missing)(self, None), + )]) + } else { + Ok(()) + } + } + // Else if value is populated validate it + Some(v) => match self._validate_against_self(v) { + Ok(_) => self._validate_against_validators(v, self.validators.as_deref()), + Err(messages1) => if self.break_on_failure { + Err(messages1) + } else { + match self._validate_against_validators(v, self.validators.as_deref()) { + Ok(_) => Ok(()), + Err(mut messages2) => { + let mut agg = messages1; + agg.append(messages2.as_mut()); + Err(agg) + } + } + } + }, + } + } - fn validate1(&self, value: Option<&'b str>) -> Result<(), Vec> { - todo!() - } + /// Special case of `validate` where the error type enums are ignored (in `Err(...)`) result, + /// and only the error messages are returned. + /// + /// ```rust + /// use walrs_inputfilter::*; + /// + /// let input = StringInputBuilder::default() + /// .required(true) + /// .value_missing(&|_, _| "Value missing".to_string()) + /// .validators(vec![&|x: &str| { + /// if x.len() < 3 { + /// return Err(vec![( + /// ConstraintViolation::TooShort, + /// "Too short".to_string(), + /// )]); + /// } + /// Ok(()) + /// }]) + /// .build() + /// .unwrap() + /// ; + /// + /// assert_eq!(input.validate1(Some(&"ab")), Err(vec!["Too short".to_string()])); + /// assert_eq!(input.validate1(None), Err(vec!["Value missing".to_string()])); + /// ``` + fn validate1(&self, value: Option<&'b str>) -> Result<(), Vec> { + match self.validate(value) { + Err(messages) => + Err(messages.into_iter().map(|(_, message)| message).collect()), + Ok(_) => Ok(()), + } + } - fn filter(&self, value: Cow<'b, str>) -> Cow<'b, str> { - todo!() - } + fn filter(&self, value: Option>) -> Option> { + match self.filters.as_deref() { + None => value, + Some(fs) => fs.iter().fold(value, |agg, f| f(agg)), + } + } - fn validate_and_filter(&self, x: Option<&'b str>) -> Result>, Vec> { - Ok(x.into()) - } + fn validate_and_filter(&self, x: Option<&'b str>) -> Result>, Vec> { + self.validate(x).map(|_| self.filter(x.map(|_x| Cow::Borrowed(_x)))) + } - fn validate_and_filter1(&self, x: Option<&'b str>) -> Result>, Vec> { - todo!() - } + /// Special case of `validate_and_filter` where the error type enums are ignored (in `Err(...)`) result, + /// and only the error messages are returned, for `Err` case. + /// + /// ```rust + /// use walrs_inputfilter::*; + /// use std::borrow::Cow; + /// + /// let input = StringInputBuilder::default() + /// .required(true) + /// .value_missing(&|_, _| "Value missing".to_string()) + /// .validators(vec![&|x: &str| { + /// if x.len() < 3 { + /// return Err(vec![( + /// ConstraintViolation::TooShort, + /// "Too short".to_string(), + /// )]); + /// } + /// Ok(()) + /// }]) + /// .filters(vec![&|xs: Option>| { + /// xs.map(|xs| Cow::Owned(xs.to_lowercase())) + /// }]) + /// .build() + /// .unwrap() + /// ; + /// + /// assert_eq!(input.validate_and_filter1(Some(&"ab")), Err(vec!["Too short".to_string()])); + /// assert_eq!(input.validate_and_filter1(Some(&"Abba")), Ok(Some("Abba".to_lowercase().into()))); + /// assert_eq!(input.validate_and_filter1(None), Err(vec!["Value missing".to_string()])); + /// ``` + fn validate_and_filter1(&self, x: Option<&'b str>) -> Result>, Vec> { + match self.validate_and_filter(x) { + Err(messages) => + Err(messages.into_iter().map(|(_, message)| message).collect()), + Ok(filtered) => Ok(filtered), + } + } } impl Default for StringInput<'_, '_> { - fn default() -> Self { - Self::new(None) - } + fn default() -> Self { + Self::new(None) + } } impl Display for StringInput<'_, '_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "StrInput {{ name: {}, required: {}, validators: {}, filters: {} }}", - self.name.unwrap_or("None"), - self.required, - self - .validators - .as_deref() - .map(|vs| format!("Some([Validator; {}])", vs.len())) - .unwrap_or("None".to_string()), - self - .filters - .as_deref() - .map(|fs| format!("Some([Filter; {}])", fs.len())) - .unwrap_or("None".to_string()), - ) - } + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "StrInput {{ name: {}, required: {}, validators: {}, filters: {} }}", + self.name.unwrap_or("None"), + self.required, + self + .validators + .as_deref() + .map(|vs| format!("Some([Validator; {}])", vs.len())) + .unwrap_or("None".to_string()), + self + .filters + .as_deref() + .map(|fs| format!("Some([Filter; {}])", fs.len())) + .unwrap_or("None".to_string()), + ) + } } impl Debug for StringInput<'_, '_> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", &self) - } + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self) + } } pub fn str_missing_msg(_: &StringInput, _: Option<&str>) -> String { - "Value is missing.".to_string() + "Value is missing.".to_string() } #[cfg(test)] mod test { - use super::*; - use crate::types::{ - ConstraintViolation, - ConstraintViolation::{PatternMismatch, RangeOverflow}, - InputConstraints, ValidationResult, - }; - use crate::validator::pattern::PatternValidator; - use regex::Regex; - use std::{borrow::Cow, error::Error, sync::Arc, thread}; - - // Tests setup types - fn less_than_1990_msg(value: &str) -> String { - format!("{} is greater than 1989-12-31", value) - } - - /// Faux validator that checks if the input is less than 1990-01-01. - fn less_than_1990(x: &str) -> ValidationResult { - if x >= "1989-12-31" { - return Err(vec![(RangeOverflow, less_than_1990_msg(x))]); - } - Ok(()) - } - - fn ymd_mismatch_msg(s: &str, pattern_str: &str) -> String { - format!("{} doesn't match pattern {}", s, pattern_str) - } - - fn ymd_check(s: &str) -> ValidationResult { - // Simplified ISO year-month-date regex - let rx = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap(); - if !rx.is_match(s) { - return Err(vec![(PatternMismatch, ymd_mismatch_msg(s, rx.as_str()))]); - } - Ok(()) - } - - /// Faux filter that returns the last date of the month. - /// **Note:** Assumes that the input is a valid ISO year-month-date. - fn to_last_date_of_month(x: Option>) -> Option> { - x.map(|x| { - let mut xs = x.into_owned(); - xs.replace_range(8..10, "31"); - Cow::Owned(xs) - }) - } - - #[test] - fn test_input_builder() -> Result<(), Box> { - // Simplified ISO year-month-date regex - let ymd_regex = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; - let ymd_regex_2 = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; - let ymd_regex_arc_orig = Arc::new(ymd_regex); - let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); - - let ymd_mismatch_msg = Arc::new(move |s: &str| -> String { - format!("{} doesn't match pattern {}", s, ymd_regex_arc.as_str()) - }); - - let ymd_mismatch_msg_arc = Arc::clone(&ymd_mismatch_msg); - let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); - - let ymd_check = move |s: &str| -> ValidationResult { - if !ymd_regex_arc.is_match(s) { - return Err(vec![(PatternMismatch, ymd_mismatch_msg_arc(s))]); - } - Ok(()) - }; - - // Validator case 1 - let pattern_validator = PatternValidator { - pattern: Cow::Owned(ymd_regex_2), - pattern_mismatch: &|validator, s| { - format!("{} doesn't match pattern {}", s, validator.pattern.as_str()) - }, + use super::*; + use crate::types::{ + ConstraintViolation, + ConstraintViolation::{PatternMismatch, RangeOverflow}, + InputConstraints, ValidationResult, }; + use crate::validator::pattern::PatternValidator; + use regex::Regex; + use std::{borrow::Cow, error::Error, sync::Arc, thread}; - let less_than_1990_input = StringInputBuilder::default() - .validators(vec![&less_than_1990]) - .build()?; + // Tests setup types + fn less_than_1990_msg(value: &str) -> String { + format!("{} is greater than 1989-12-31", value) + } - let yyyy_mm_dd_input = StringInputBuilder::default() - .validators(vec![&ymd_check]) - .build()?; + /// Faux validator that checks if the input is less than 1990-01-01. + fn less_than_1990(x: &str) -> ValidationResult { + if x >= "1989-12-31" { + return Err(vec![(RangeOverflow, less_than_1990_msg(x))]); + } + Ok(()) + } - let yyyy_mm_dd_input2 = StringInputBuilder::default() - .validators(vec![&pattern_validator]) - .build()?; + fn ymd_mismatch_msg(s: &str, pattern_str: &str) -> String { + format!("{} doesn't match pattern {}", s, pattern_str) + } - // Missing value check - match less_than_1990_input.validate(None) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), + fn ymd_check(s: &str) -> ValidationResult { + // Simplified ISO year-month-date regex + let rx = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap(); + if !rx.is_match(s) { + return Err(vec![(PatternMismatch, ymd_mismatch_msg(s, rx.as_str()))]); + } + Ok(()) } - // Mismatch check - let value = "1000-99-999"; - match yyyy_mm_dd_input.validate(Some(&value)) { - Ok(_) => panic!("Expected Err(...); Received Ok(())"), - Err(tuples) => { - assert_eq!(tuples[0].0, PatternMismatch); - assert_eq!(tuples[0].1, ymd_mismatch_msg(value).as_str()); - } + /// Faux filter that returns the last date of the month. + /// **Note:** Assumes that the input is a valid ISO year-month-date. + fn to_last_date_of_month(x: Option>) -> Option> { + x.map(|x| { + let mut xs = x.into_owned(); + xs.replace_range(8..10, "31"); + Cow::Owned(xs) + }) } - // Valid check - match yyyy_mm_dd_input.validate(None) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), + #[test] + fn test_input_builder() -> Result<(), Box> { + // Simplified ISO year-month-date regex + let ymd_regex = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; + let ymd_regex_2 = Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$")?; + let ymd_regex_arc_orig = Arc::new(ymd_regex); + let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); + + let ymd_mismatch_msg = Arc::new(move |s: &str| -> String { + format!("{} doesn't match pattern {}", s, ymd_regex_arc.as_str()) + }); + + let ymd_mismatch_msg_arc = Arc::clone(&ymd_mismatch_msg); + let ymd_regex_arc = Arc::clone(&ymd_regex_arc_orig); + + let ymd_check = move |s: &str| -> ValidationResult { + if !ymd_regex_arc.is_match(s) { + return Err(vec![(PatternMismatch, ymd_mismatch_msg_arc(s))]); + } + Ok(()) + }; + + // Validator case 1 + let pattern_validator = PatternValidator { + pattern: Cow::Owned(ymd_regex_2), + pattern_mismatch: &|validator, s| { + format!("{} doesn't match pattern {}", s, validator.pattern.as_str()) + }, + }; + + let less_than_1990_input = StringInputBuilder::default() + .validators(vec![&less_than_1990]) + .build()?; + + let yyyy_mm_dd_input = StringInputBuilder::default() + .validators(vec![&ymd_check]) + .build()?; + + let yyyy_mm_dd_input2 = StringInputBuilder::default() + .validators(vec![&pattern_validator]) + .build()?; + + // Missing value check + match less_than_1990_input.validate(None) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + // Mismatch check + let value = "1000-99-999"; + match yyyy_mm_dd_input.validate(Some(&value)) { + Ok(_) => panic!("Expected Err(...); Received Ok(())"), + Err(tuples) => { + assert_eq!(tuples[0].0, PatternMismatch); + assert_eq!(tuples[0].1, ymd_mismatch_msg(value).as_str()); + } + } + + // Valid check + match yyyy_mm_dd_input.validate(None) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + // Valid check 2 + let value = "1000-99-99"; + match yyyy_mm_dd_input.validate(Some(&value)) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + // Valid check + let value = "1000-99-99"; + match yyyy_mm_dd_input2.validate(Some(&value)) { + Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), + Ok(()) => (), + } + + Ok(()) } - // Valid check 2 - let value = "1000-99-99"; - match yyyy_mm_dd_input.validate(Some(&value)) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), + #[test] + fn test_thread_safety() -> Result<(), Box> { + let less_than_1990_input = StringInputBuilder::default() + .validators(vec![&less_than_1990]) + .build()?; + + let ymd_input = StringInputBuilder::default() + .validators(vec![&ymd_check]) + .build()?; + + let less_than_input = Arc::new(less_than_1990_input); + let less_than_input_instance = Arc::clone(&less_than_input); + + let str_input = Arc::new(ymd_input); + let str_input_instance = Arc::clone(&str_input); + + let handle = + thread::spawn( + move || match less_than_input_instance.validate(Some("2023-12-31")) { + Err(x) => { + assert_eq!(x[0].1.as_str(), less_than_1990_msg("2023-12-31")); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + let handle2 = thread::spawn(move || match str_input_instance.validate(Some(&"")) { + Err(x) => { + assert_eq!( + x[0].1.as_str(), + ymd_mismatch_msg( + "", + Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap().as_str(), + ) + ); + } + _ => panic!("Expected `Err(...)`"), + }); + + // @note Conclusion of tests here is that validators can only (easily) be shared between threads if they are function pointers - + // closures are too loose and require over the top value management and planning due to the nature of multi-threaded + // contexts. + + // Contrary to the above, 'scoped threads', will allow variable sharing without requiring them to + // be 'moved' first (as long as rust's lifetime rules are followed - + // @see https://blog.logrocket.com/using-rust-scoped-threads-improve-efficiency-safety/ + // ). + + handle.join().unwrap(); + handle2.join().unwrap(); + + Ok(()) } - // Valid check - let value = "1000-99-99"; - match yyyy_mm_dd_input2.validate(Some(&value)) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), + /// Example showing shared references in `StrInput`, and user-land, controls. + #[test] + fn test_thread_safety_with_scoped_threads_and_closures() -> Result<(), Box> { + let ymd_rx = Arc::new(Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap()); + let ymd_rx_clone = Arc::clone(&ymd_rx); + + let ymd_check = move |s: &str| -> ValidationResult { + // Simplified ISO year-month-date regex + if !ymd_rx_clone.is_match(s) { + return Err(vec![( + PatternMismatch, + ymd_mismatch_msg(s, ymd_rx_clone.as_str()), + )]); + } + Ok(()) + }; + + let less_than_1990_input = StringInputBuilder::default() + .validators(vec![&less_than_1990]) + .filters(vec![&to_last_date_of_month]) + .build()?; + + let ymd_input = StringInputBuilder::default() + .validators(vec![&ymd_check]) + .build()?; + + let less_than_input = Arc::new(less_than_1990_input); + let less_than_input_instance = Arc::clone(&less_than_input); + let ymd_check_input = Arc::new(ymd_input); + let ymd_check_input_instance = Arc::clone(&ymd_check_input); + + thread::scope(|scope| { + scope.spawn( + || match less_than_input_instance.validate(Some("2023-12-31")) { + Err(x) => { + assert_eq!(x[0].1.as_str(), &less_than_1990_msg("2023-12-31")); + } + _ => panic!("Expected `Err(...)`"), + }, + ); + + scope.spawn( + || match less_than_input_instance.validate_and_filter(Some("1989-01-01")) { + Err(err) => panic!( + "Expected `Ok(Some({:#?})`; Received `Err({:#?})`", + Cow::::Owned("1989-01-31".to_string()), + err + ), + Ok(Some(x)) => assert_eq!(x, Cow::::Owned("1989-01-31".to_string())), + _ => panic!("Expected `Ok(Some(Cow::Owned(99 * 2)))`; Received `Ok(None)`"), + }, + ); + + scope.spawn(|| match ymd_check_input_instance.validate(Some(&"")) { + Err(x) => { + assert_eq!(x[0].1.as_str(), ymd_mismatch_msg("", ymd_rx.as_str())); + } + _ => panic!("Expected `Err(...)`"), + }); + + scope.spawn(|| { + if let Err(_err_tuple) = ymd_check_input_instance.validate(Some(&"2013-08-31")) { + panic!("Expected `Ok(()); Received Err(...)`") + } + }); + }); + + Ok(()) } - Ok(()) - } - - #[test] - fn test_thread_safety() -> Result<(), Box> { - let less_than_1990_input = StringInputBuilder::default() - .validators(vec![&less_than_1990]) - .build()?; - - let ymd_input = StringInputBuilder::default() - .validators(vec![&ymd_check]) - .build()?; - - let less_than_input = Arc::new(less_than_1990_input); - let less_than_input_instance = Arc::clone(&less_than_input); - - let str_input = Arc::new(ymd_input); - let str_input_instance = Arc::clone(&str_input); - - let handle = - thread::spawn( - move || match less_than_input_instance.validate(Some("2023-12-31")) { - Err(x) => { - assert_eq!(x[0].1.as_str(), less_than_1990_msg("2023-12-31")); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - let handle2 = thread::spawn(move || match str_input_instance.validate(Some(&"")) { - Err(x) => { + #[test] + fn test_validate_and_filter() { + let input = StringInputBuilder::default() + .name("hello") + .required(true) + .validators(vec![&less_than_1990]) + .filters(vec![&to_last_date_of_month]) + .build() + .unwrap(); + assert_eq!( - x[0].1.as_str(), - ymd_mismatch_msg( - "", - Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap().as_str(), - ) + input.validate_and_filter(Some("2023-12-31")), + Err(vec![(RangeOverflow, less_than_1990_msg("2023-12-31"))]) ); - } - _ => panic!("Expected `Err(...)`"), - }); - - // @note Conclusion of tests here is that validators can only (easily) be shared between threads if they are function pointers - - // closures are too loose and require over the top value management and planning due to the nature of multi-threaded - // contexts. - - // Contrary to the above, 'scoped threads', will allow variable sharing without requiring them to - // be 'moved' first (as long as rust's lifetime rules are followed - - // @see https://blog.logrocket.com/using-rust-scoped-threads-improve-efficiency-safety/ - // ). - - handle.join().unwrap(); - handle2.join().unwrap(); - - Ok(()) - } - - /// Example showing shared references in `StrInput`, and user-land, controls. - #[test] - fn test_thread_safety_with_scoped_threads_and_closures() -> Result<(), Box> { - let ymd_rx = Arc::new(Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap()); - let ymd_rx_clone = Arc::clone(&ymd_rx); - - let ymd_check = move |s: &str| -> ValidationResult { - // Simplified ISO year-month-date regex - if !ymd_rx_clone.is_match(s) { - return Err(vec![( - PatternMismatch, - ymd_mismatch_msg(s, ymd_rx_clone.as_str()), - )]); - } - Ok(()) - }; + assert_eq!( + input.validate_and_filter(Some("1989-01-01")), + Ok(Some(Cow::Owned("1989-01-31".to_string()))) + ); + } - let less_than_1990_input = StringInputBuilder::default() - .validators(vec![&less_than_1990]) - .filters(vec![&to_last_date_of_month]) - .build()?; - - let ymd_input = StringInputBuilder::default() - .validators(vec![&ymd_check]) - .build()?; - - let less_than_input = Arc::new(less_than_1990_input); - let less_than_input_instance = Arc::clone(&less_than_input); - let ymd_check_input = Arc::new(ymd_input); - let ymd_check_input_instance = Arc::clone(&ymd_check_input); - - thread::scope(|scope| { - scope.spawn( - || match less_than_input_instance.validate(Some("2023-12-31")) { - Err(x) => { - assert_eq!(x[0].1.as_str(), &less_than_1990_msg("2023-12-31")); - } - _ => panic!("Expected `Err(...)`"), - }, - ); - - scope.spawn( - || match less_than_input_instance.validate_and_filter(Some("1989-01-01")) { - Err(err) => panic!( - "Expected `Ok(Some({:#?})`; Received `Err({:#?})`", - Cow::::Owned("1989-01-31".to_string()), - err - ), - Ok(Some(x)) => assert_eq!(x, Cow::::Owned("1989-01-31".to_string())), - _ => panic!("Expected `Ok(Some(Cow::Owned(99 * 2)))`; Received `Ok(None)`"), - }, - ); - - scope.spawn(|| match ymd_check_input_instance.validate(Some(&"")) { - Err(x) => { - assert_eq!(x[0].1.as_str(), ymd_mismatch_msg("", ymd_rx.as_str())); - } - _ => panic!("Expected `Err(...)`"), - }); + #[test] + fn test_value_type() { + let callback1 = |xs: &str| -> ValidationResult { + if !xs.is_empty() { + Ok(()) + } else { + Err(vec![( + ConstraintViolation::TypeMismatch, + "Error".to_string(), + )]) + } + }; + + let _input = StringInputBuilder::default() + .name("hello") + .validators(vec![&callback1]) + .build() + .unwrap(); + } - scope.spawn(|| { - if let Err(_err_tuple) = ymd_check_input_instance.validate(Some(&"2013-08-31")) { - panic!("Expected `Ok(()); Received Err(...)`") - } - }); - }); - - Ok(()) - } - - #[test] - fn test_validate_and_filter() { - let input = StringInputBuilder::default() - .name("hello") - .required(true) - .validators(vec![&less_than_1990]) - .filters(vec![&to_last_date_of_month]) - .build() - .unwrap(); - - assert_eq!( - input.validate_and_filter(Some("2023-12-31")), - Err(vec![(RangeOverflow, less_than_1990_msg("2023-12-31"))]) - ); - assert_eq!( - input.validate_and_filter(Some("1989-01-01")), - Ok(Some(Cow::Owned("1989-01-31".to_string()))) - ); - } - - #[test] - fn test_value_type() { - let callback1 = |xs: &str| -> ValidationResult { - if !xs.is_empty() { - Ok(()) - } else { - Err(vec![( - ConstraintViolation::TypeMismatch, - "Error".to_string(), - )]) - } - }; + #[test] + fn test_display() { + let input = StringInputBuilder::default() + .name("hello") + .validators(vec![&less_than_1990]) + .build() + .unwrap(); - let _input = StringInputBuilder::default() - .name("hello") - .validators(vec![&callback1]) - .build() - .unwrap(); - } - - #[test] - fn test_display() { - let input = StringInputBuilder::default() - .name("hello") - .validators(vec![&less_than_1990]) - .build() - .unwrap(); - - assert_eq!( - input.to_string(), - "StrInput { name: hello, required: false, validators: Some([Validator; 1]), filters: None }" - ); - } + assert_eq!( + input.to_string(), + "StrInput { name: hello, required: false, validators: Some([Validator; 1]), filters: None }" + ); + } } diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index 87b5290..ee90e39 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -72,10 +72,8 @@ pub type Filter = dyn Fn(T) -> T + Send + Sync; pub type Validator = dyn Fn(T) -> ValidationResult + Send + Sync; -pub type ValueMissingCallback = dyn Fn(RulesStruct) -> ViolationMessage + Send + Sync; - pub trait WithName<'a> { - fn get_name() -> Option>; + fn get_name(&self) -> Option>; } pub trait ValidateValue { @@ -86,22 +84,24 @@ pub trait FilterValue { fn filter(&self, value: T) -> T; } -pub trait ToAttributesList { - fn to_attributes_list(&self) -> Option> { - None - } -} +pub type ValueMissingCallback = dyn Fn(&dyn WithName) -> ViolationMessage + Send + Sync; pub trait InputConstraints<'a, 'b, T: 'b, FT: 'b>: Display + Debug - where T: InputValue + ?Sized { + where T: InputValue { fn validate(&self, value: Option) -> Result<(), Vec>; fn validate1(&self, value: Option) -> Result<(), Vec>; - fn filter(&self, value: FT) -> FT; + fn filter(&self, value: Option) -> Option; fn validate_and_filter(&self, x: Option) -> Result, Vec>; fn validate_and_filter1(&self, x: Option) -> Result, Vec>; } + +pub trait ToAttributesList { + fn to_attributes_list(&self) -> Option> { + None + } +} From 4b91b6fc534acd0153557feae6e5faadb7f4d111 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:14:57 -0500 Subject: [PATCH 12/38] inputfilter - Progress on types refactor - Added 'default_value' field, to 'StringInput'. --- inputfilter/src/string_input.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 99407db..a7d9286 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -62,6 +62,9 @@ pub struct StringInput<'a, 'b> { #[builder(default = "false")] pub required: bool, + #[builder(default = "None")] + pub default_value: Option, + #[builder(default = "None")] pub validators: Option>>, @@ -94,6 +97,7 @@ impl<'a, 'b> StringInput<'a, 'b> { pattern: None, equal: None, required: false, + default_value: None, validators: None, filters: None, too_short: &(too_long_msg), @@ -298,12 +302,19 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, } fn filter(&self, value: Option>) -> Option> { + let v = match value { + None => self.default_value.clone().map(|x| x.into()), + Some(x) => Some(x) + }; + match self.filters.as_deref() { - None => value, - Some(fs) => fs.iter().fold(value, |agg, f| f(agg)), + None => v, + Some(fs) => fs.iter().fold(v, |agg, f| f(agg)), } } + // @todo consolidate these (`validate_and_filter*`), into just `filter*` ( + // since we really don't want to use filtered values without them being valid/etc.) fn validate_and_filter(&self, x: Option<&'b str>) -> Result>, Vec> { self.validate(x).map(|_| self.filter(x.map(|_x| Cow::Borrowed(_x)))) } From 0946c08756c846636d8f04fe86a905ac70e96f0c Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Mon, 4 Dec 2023 23:09:50 -0500 Subject: [PATCH 13/38] inputfilter - progress on types refactor - Updated 'scalar', and 'string' input types to use global 'ValueMissingCallback' type, and general progress on 'InputConstraints' type integration refactor. --- inputfilter/src/lib.rs | 4 + inputfilter/src/scalar_input.rs | 159 +++++++++++++++++++++----------- inputfilter/src/string_input.rs | 12 +-- inputfilter/src/types.rs | 25 ++++- 4 files changed, 138 insertions(+), 62 deletions(-) diff --git a/inputfilter/src/lib.rs b/inputfilter/src/lib.rs index d5e4a41..b5453d4 100644 --- a/inputfilter/src/lib.rs +++ b/inputfilter/src/lib.rs @@ -18,3 +18,7 @@ pub use input::*; pub use filter::*; pub use scalar_input::*; pub use string_input::*; + +pub fn value_missing_msg(_: &dyn WithName) -> String { + "Value missing".to_string() +} diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index 19cb68c..9db491c 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -2,11 +2,11 @@ use std::borrow::Cow; use std::fmt::{Debug, Display, Formatter}; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; -use crate::{ConstraintViolation, NumberValue, ValidationErrTuple}; +use crate::{ConstraintViolation, ScalarValue, ValidationErrTuple, value_missing_msg, ValueMissingCallback, WithName}; pub type NumMissingViolationCallback = dyn Fn(&NumberInput, Option) -> ViolationMessage + Send + Sync; -pub fn range_underflow_msg(rules: &NumberInput, x: Option) -> String { +pub fn range_underflow_msg(rules: &NumberInput, x: Option) -> String { format!( "`{:}` is less than minimum `{:}`.", x.unwrap(), @@ -14,7 +14,7 @@ pub fn range_underflow_msg(rules: &NumberInput, x: Option) ) } -pub fn range_overflow_msg(rules: &NumberInput, x: Option) -> String { +pub fn range_overflow_msg(rules: &NumberInput, x: Option) -> String { format!( "`{:}` is greater than maximum `{:}`.", x.unwrap(), @@ -22,18 +22,7 @@ pub fn range_overflow_msg(rules: &NumberInput, x: Option) ) } -pub fn step_mismatch_msg( - rules: &NumberInput, - x: Option, -) -> String { - format!( - "`{:}` is not divisible by `{:}`.", - x.unwrap(), - &rules.step.unwrap() - ) -} - -pub fn num_not_equal_msg( +pub fn num_not_equal_msg( rules: &NumberInput, x: Option, ) -> String { @@ -44,13 +33,13 @@ pub fn num_not_equal_msg( ) } -pub fn num_missing_msg(_: &NumberInput, _: Option) -> String { +pub fn num_missing_msg(_: &NumberInput, _: Option) -> String { "Value is missing.".to_string() } #[derive(Builder, Clone)] #[builder(setter(strip_option))] -pub struct NumberInput<'a, T: NumberValue> { +pub struct NumberInput<'a, T: ScalarValue> { #[builder(default = "true")] pub break_on_failure: bool, @@ -64,9 +53,6 @@ pub struct NumberInput<'a, T: NumberValue> { #[builder(default = "None")] pub max: Option, - #[builder(default = "None")] - pub step: Option, - #[builder(default = "None")] pub equal: Option, @@ -74,9 +60,10 @@ pub struct NumberInput<'a, T: NumberValue> { pub required: bool, #[builder(default = "None")] - pub validators: Option>>, + pub default_value: Option, - // @todo Add support for `io_validators` (e.g., validators that return futures). + #[builder(default = "None")] + pub validators: Option>>, #[builder(default = "None")] pub filters: Option>>>, @@ -87,18 +74,15 @@ pub struct NumberInput<'a, T: NumberValue> { #[builder(default = "&range_overflow_msg")] pub range_overflow: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), - #[builder(default = "&step_mismatch_msg")] - pub step_mismatch: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), - #[builder(default = "&num_not_equal_msg")] pub not_equal: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), - #[builder(default = "&num_missing_msg")] - pub value_missing: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), + #[builder(default = "&value_missing_msg")] + pub value_missing: &'a ValueMissingCallback, } impl<'a, T> NumberInput<'a, T> - where T: NumberValue + where T: ScalarValue { pub fn new(name: Option<&'a str>) -> Self { NumberInput { @@ -108,17 +92,17 @@ impl<'a, T> NumberInput<'a, T> max: None, equal: None, required: false, + default_value: None, validators: None, filters: None, range_underflow: &(range_underflow_msg), range_overflow: &(range_overflow_msg), not_equal: &(num_not_equal_msg), - value_missing: &num_missing_msg, - step: None, - step_mismatch: &(step_mismatch_msg), + value_missing: &value_missing_msg, } } - fn validate_custom(&self, value: T) -> Result<(), Vec> { + + fn _validate_against_self(&self, value: T) -> Result<(), Vec> { let mut errs = vec![]; // Test lower bound @@ -157,61 +141,126 @@ impl<'a, T> NumberInput<'a, T> } } - // Test Step - if let Some(step) = self.step { - if step != Default::default() && value % step != Default::default() { - errs.push(( - ConstraintViolation::StepMismatch, - (&self.step_mismatch)(self, Some(value)) - )); + if errs.is_empty() { Ok(()) } else { Err(errs) } + } - if self.break_on_failure { return Err(errs); } + fn _validate_against_validators(&self, value: T, validators: Option<&[&'a Validator]>) -> Result<(), Vec> { + validators.map(|vs| { + + // If not break on failure then capture all validation errors. + if !self.break_on_failure { + return vs.iter().fold( + Vec::::new(), + |mut agg, f| match (f)(value) { + Err(mut message_tuples) => { + agg.append(message_tuples.as_mut()); + agg + } + _ => agg, + }); } - } - if errs.is_empty() { Ok(()) } else { Err(errs) } + // Else break on, and capture, first failure. + let mut agg = Vec::::new(); + for f in vs.iter() { + if let Err(mut message_tuples) = (f)(value) { + agg.append(message_tuples.as_mut()); + break; + } + } + agg + }) + .and_then(|messages| if messages.is_empty() { None } else { Some(messages) }) + .map_or(Ok(()), Err) } - } impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for NumberInput<'a, T> - where T: NumberValue -{ - fn validate(&self, value: Option) -> Result<(), Vec> { - todo!() + where T: ScalarValue + Copy { + fn validate(&self, value: Option) -> Result<(), Vec> { + match value { + None => { + if self.required { + Err(vec![( + ConstraintViolation::ValueMissing, + (&self.value_missing)(self), + )]) + } else { + Ok(()) + } + } + // Else if value is populated validate it + Some(v) => match self._validate_against_self(v) { + Ok(_) => self._validate_against_validators(v, self.validators.as_deref()), + Err(messages1) => if self.break_on_failure { + Err(messages1) + } else { + match self._validate_against_validators(v, self.validators.as_deref()) { + Ok(_) => Ok(()), + Err(mut messages2) => { + let mut agg = messages1; + agg.append(messages2.as_mut()); + Err(agg) + } + } + } + }, + } } fn validate1(&self, value: Option) -> Result<(), Vec> { - todo!() + match self.validate(value) { + // If errors, extract messages and return them + Err(messages) => + Err(messages.into_iter().map(|(_, message)| message).collect()), + Ok(_) => Ok(()), + } } fn filter(&self, value: Option) -> Option { + let v = match value { + None => self.default_value.map(|x| x.into()), + Some(x) => Some(x) + }; + match self.filters.as_deref() { - None => value, - Some(fs) => fs.iter().fold(value, |agg, f| (f)(agg)), + None => v, + Some(fs) => fs.iter().fold(v, |agg, f| f(agg)), } } + // @todo consolidate these (`validate_and_filter*`), into just `filter*` ( + // since we really don't want to use filtered values without them being valid/etc.) fn validate_and_filter(&self, x: Option) -> Result, Vec> { self.validate(x).map(|_| self.filter(x)) } fn validate_and_filter1(&self, x: Option) -> Result, Vec> { - todo!() + match self.validate_and_filter(x) { + Err(messages) => + Err(messages.into_iter().map(|(_, message)| message).collect()), + Ok(filtered) => Ok(filtered), + } + } +} + +impl<'a, T: ScalarValue> WithName<'a> for NumberInput<'a, T> { + fn get_name(&self) -> Option> { + self.name.map(|xs| Cow::Borrowed(xs)) } } -impl Default for NumberInput<'_, T> { +impl Default for NumberInput<'_, T> { fn default() -> Self { Self::new(None) } } -impl Display for NumberInput<'_, T> { +impl Display for NumberInput<'_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "StrInput {{ name: {}, required: {}, validators: {}, filters: {} }}", + "ScalarInput {{ name: {}, required: {}, validators: {}, filters: {} }}", self.name.unwrap_or("None"), self.required, self @@ -228,7 +277,7 @@ impl Display for NumberInput<'_, T> { } } -impl Debug for NumberInput<'_, T> { +impl Debug for NumberInput<'_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self) } diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index a7d9286..6207bbd 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Display, Formatter}; use regex::Regex; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; -use crate::{ConstraintViolation, ValidationErrTuple, ValidationResult, WithName}; +use crate::{ConstraintViolation, ValidationErrTuple, ValidationResult, value_missing_msg, ValueMissingCallback, WithName}; pub type StrMissingViolationCallback = dyn Fn(&StringInput, Option<&str>) -> ViolationMessage + Send + Sync; @@ -83,8 +83,8 @@ pub struct StringInput<'a, 'b> { #[builder(default = "&str_not_equal_msg")] pub not_equal: &'a StrMissingViolationCallback, - #[builder(default = "&str_missing_msg")] - pub value_missing: &'a StrMissingViolationCallback, + #[builder(default = "&value_missing_msg")] + pub value_missing: &'a ValueMissingCallback, } impl<'a, 'b> StringInput<'a, 'b> { @@ -104,7 +104,7 @@ impl<'a, 'b> StringInput<'a, 'b> { too_long: &(too_long_msg), pattern_mismatch: &(pattern_mismatch_msg), not_equal: &(str_not_equal_msg), - value_missing: &str_missing_msg, + value_missing: &value_missing_msg, } } @@ -159,7 +159,6 @@ impl<'a, 'b> StringInput<'a, 'b> { if errs.is_empty() { Ok(()) } else { Err(errs) } } - /// Validates value against contained validators. fn _validate_against_validators(&self, value: &'b str, validators: Option<&[&'a Validator<&'b str>]>) -> Result<(), Vec> { validators.map(|vs| { @@ -243,7 +242,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, if self.required { Err(vec![( ConstraintViolation::ValueMissing, - (&self.value_missing)(self, None), + (&self.value_missing)(self), )]) } else { Ok(()) @@ -295,6 +294,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// ``` fn validate1(&self, value: Option<&'b str>) -> Result<(), Vec> { match self.validate(value) { + // If errors, extract messages and return them Err(messages) => Err(messages.into_iter().map(|(_, message)| message).collect()), Ok(_) => Ok(()), diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index ee90e39..69f1635 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -29,7 +29,30 @@ impl InputValue for str {} impl InputValue for &str {} -pub trait NumberValue: InputValue + Default + Copy + Add + Sub + Mul + Div + Rem {} +pub trait ScalarValue: InputValue + Default + Copy {} + +impl ScalarValue for i8 {} +impl ScalarValue for i16 {} +impl ScalarValue for i32 {} +impl ScalarValue for i64 {} +impl ScalarValue for i128 {} +impl ScalarValue for isize {} + +impl ScalarValue for u8 {} +impl ScalarValue for u16 {} +impl ScalarValue for u32 {} +impl ScalarValue for u64 {} +impl ScalarValue for u128 {} +impl ScalarValue for usize {} + +impl ScalarValue for f32 {} +impl ScalarValue for f64 {} + +impl ScalarValue for bool {} + +impl ScalarValue for char {} + +pub trait NumberValue: ScalarValue + Add + Sub + Mul + Div + Rem {} impl NumberValue for i8 {} impl NumberValue for i16 {} From 8dc9d5ab5dad707481998e939b33b1900130a1c6 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Mon, 4 Dec 2023 23:12:57 -0500 Subject: [PATCH 14/38] inputfilter - progress on types refactor - Ran 'cargo clippy --fix'. --- inputfilter/src/input.rs | 10 +++++----- inputfilter/src/scalar_input.rs | 12 ++++++------ inputfilter/src/string_input.rs | 12 ++++++------ inputfilter/src/validator/pattern.rs | 6 +++--- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/inputfilter/src/input.rs b/inputfilter/src/input.rs index 45cced6..f12ca4c 100644 --- a/inputfilter/src/input.rs +++ b/inputfilter/src/input.rs @@ -57,23 +57,23 @@ where } impl<'a, 'b, T: InputValue, FT: 'b> InputConstraints<'a, 'b, T, FT> for Input<'a, 'b, T> { - fn validate(&self, value: Option) -> Result<(), Vec> { + fn validate(&self, _value: Option) -> Result<(), Vec> { todo!() } - fn validate1(&self, value: Option) -> Result<(), Vec> { + fn validate1(&self, _value: Option) -> Result<(), Vec> { todo!() } - fn filter(&self, value: Option) -> Option { + fn filter(&self, _value: Option) -> Option { todo!() } - fn validate_and_filter(&self, x: Option) -> Result, Vec> { + fn validate_and_filter(&self, _x: Option) -> Result, Vec> { todo!() } - fn validate_and_filter1(&self, x: Option) -> Result, Vec> { + fn validate_and_filter1(&self, _x: Option) -> Result, Vec> { todo!() } } diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index 9db491c..83cfaa1 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -110,7 +110,7 @@ impl<'a, T> NumberInput<'a, T> if value < min { errs.push(( ConstraintViolation::RangeUnderflow, - (&self.range_underflow)(self, Some(value)), + (self.range_underflow)(self, Some(value)), )); if self.break_on_failure { return Err(errs); } @@ -122,7 +122,7 @@ impl<'a, T> NumberInput<'a, T> if value > max { errs.push(( ConstraintViolation::TooLong, - (&self.range_overflow)(self, Some(value)), + (self.range_overflow)(self, Some(value)), )); if self.break_on_failure { return Err(errs); } @@ -134,7 +134,7 @@ impl<'a, T> NumberInput<'a, T> if value != equal { errs.push(( ConstraintViolation::NotEqual, - (&self.not_equal)(self, Some(value)), + (self.not_equal)(self, Some(value)), )); if self.break_on_failure { return Err(errs); } @@ -183,7 +183,7 @@ impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for NumberInput<'a, T> if self.required { Err(vec![( ConstraintViolation::ValueMissing, - (&self.value_missing)(self), + (self.value_missing)(self), )]) } else { Ok(()) @@ -219,7 +219,7 @@ impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for NumberInput<'a, T> fn filter(&self, value: Option) -> Option { let v = match value { - None => self.default_value.map(|x| x.into()), + None => self.default_value.map(|x| x), Some(x) => Some(x) }; @@ -246,7 +246,7 @@ impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for NumberInput<'a, T> impl<'a, T: ScalarValue> WithName<'a> for NumberInput<'a, T> { fn get_name(&self) -> Option> { - self.name.map(|xs| Cow::Borrowed(xs)) + self.name.map(Cow::Borrowed) } } diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 6207bbd..08e146c 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -34,7 +34,7 @@ pub fn too_long_msg(rules: &StringInput, xs: Option<&str>) -> String { pub fn str_not_equal_msg(rules: &StringInput, _: Option<&str>) -> String { format!( "Value is not equal to {}.", - &rules.equal.as_deref().unwrap_or("") + &rules.equal.unwrap_or("") ) } @@ -137,7 +137,7 @@ impl<'a, 'b> StringInput<'a, 'b> { if !pattern.is_match(value) { errs.push(( ConstraintViolation::PatternMismatch, - (&self. + (self. pattern_mismatch)(self, Some(value)), )); @@ -149,7 +149,7 @@ impl<'a, 'b> StringInput<'a, 'b> { if value != *equal { errs.push(( ConstraintViolation::NotEqual, - (&self.not_equal)(self, Some(value)), + (self.not_equal)(self, Some(value)), )); if self.break_on_failure { return Err(errs); } @@ -192,7 +192,7 @@ impl<'a, 'b> StringInput<'a, 'b> { impl<'a, 'b> WithName<'a> for StringInput<'a, 'b> { fn get_name(&self) -> Option> { - self.name.map(|xs| Cow::Borrowed(xs)) + self.name.map(Cow::Borrowed) } } @@ -242,7 +242,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, if self.required { Err(vec![( ConstraintViolation::ValueMissing, - (&self.value_missing)(self), + (self.value_missing)(self), )]) } else { Ok(()) @@ -316,7 +316,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, // @todo consolidate these (`validate_and_filter*`), into just `filter*` ( // since we really don't want to use filtered values without them being valid/etc.) fn validate_and_filter(&self, x: Option<&'b str>) -> Result>, Vec> { - self.validate(x).map(|_| self.filter(x.map(|_x| Cow::Borrowed(_x)))) + self.validate(x).map(|_| self.filter(x.map(Cow::Borrowed))) } /// Special case of `validate_and_filter` where the error type enums are ignored (in `Err(...)`) result, diff --git a/inputfilter/src/validator/pattern.rs b/inputfilter/src/validator/pattern.rs index 8492edd..7021a14 100644 --- a/inputfilter/src/validator/pattern.rs +++ b/inputfilter/src/validator/pattern.rs @@ -88,19 +88,19 @@ impl FnOnce<(&String, )> for PatternValidator<'_> { type Output = ValidationResult; extern "rust-call" fn call_once(self, args: (&String, )) -> Self::Output { - self.validate(&args.0) + self.validate(args.0) } } impl FnMut<(&String, )> for PatternValidator<'_> { extern "rust-call" fn call_mut(&mut self, args: (&String, )) -> Self::Output { - self.validate(&args.0) + self.validate(args.0) } } impl Fn<(&String, )> for PatternValidator<'_> { extern "rust-call" fn call(&self, args: (&String, )) -> Self::Output { - self.validate(&args.0) + self.validate(args.0) } } From 4b594a5007d0c50341ce04aef597c840fc25b935 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Mon, 4 Dec 2023 23:13:46 -0500 Subject: [PATCH 15/38] inputfilter - progress on types refactor - Moved 'input' to '_recycler'. --- .../src => _recycler/inputfilter}/input.rs | 0 inputfilter/src/filter/slug.rs | 18 +++++++----------- inputfilter/src/lib.rs | 2 -- inputfilter/src/scalar_input.rs | 2 +- inputfilter/src/string_input.rs | 12 ++++++------ inputfilter/src/validator/equal.rs | 8 ++++---- inputfilter/src/validator/number.rs | 8 ++++---- inputfilter/src/validator/pattern.rs | 4 ++-- 8 files changed, 24 insertions(+), 30 deletions(-) rename {inputfilter/src => _recycler/inputfilter}/input.rs (100%) diff --git a/inputfilter/src/input.rs b/_recycler/inputfilter/input.rs similarity index 100% rename from inputfilter/src/input.rs rename to _recycler/inputfilter/input.rs diff --git a/inputfilter/src/filter/slug.rs b/inputfilter/src/filter/slug.rs index e349286..e63541b 100644 --- a/inputfilter/src/filter/slug.rs +++ b/inputfilter/src/filter/slug.rs @@ -117,32 +117,28 @@ mod test { #[test] fn test_to_slug_standalone_method() { - for (cow_str, expected) in vec![ - (Cow::Borrowed("Hello World"), "hello-world"), + for (cow_str, expected) in [(Cow::Borrowed("Hello World"), "hello-world"), (Cow::Borrowed("#$@#$Hello World$@#$"), "hello-world"), - (Cow::Borrowed("$Hello'\"@$World$"), "hello----world"), - ] { + (Cow::Borrowed("$Hello'\"@$World$"), "hello----world")] { assert_eq!(to_slug(cow_str), expected); } } #[test] fn test_to_pretty_slug_standalone_method() { - for (cow_str, expected) in vec![ - (Cow::Borrowed("Hello World"), "hello-world"), + for (cow_str, expected) in [(Cow::Borrowed("Hello World"), "hello-world"), (Cow::Borrowed("$Hello World$"), "hello-world"), - (Cow::Borrowed("$Hello'\"@$World$"), "hello-world"), - ] { + (Cow::Borrowed("$Hello'\"@$World$"), "hello-world")] { assert_eq!(to_pretty_slug(cow_str), expected); } } #[test] fn test_slug_filter_constructor() { - for x in vec![0, 1, 2] { + for x in [0, 1, 2] { let instance = SlugFilter::new(x, false); assert_eq!(instance.max_length, x); - assert_eq!(instance.allow_duplicate_dashes, false); + assert!(!instance.allow_duplicate_dashes); } } @@ -150,7 +146,7 @@ mod test { fn test_slug_filter_builder() { let instance = SlugFilterBuilder::default().build().unwrap(); assert_eq!(instance.max_length, 200); - assert_eq!(instance.allow_duplicate_dashes, true); + assert!(instance.allow_duplicate_dashes); } #[test] diff --git a/inputfilter/src/lib.rs b/inputfilter/src/lib.rs index b5453d4..d44bf6a 100644 --- a/inputfilter/src/lib.rs +++ b/inputfilter/src/lib.rs @@ -7,14 +7,12 @@ extern crate derive_builder; pub mod types; pub mod validator; -pub mod input; pub mod filter; pub mod scalar_input; pub mod string_input; pub use types::*; pub use validator::*; -pub use input::*; pub use filter::*; pub use scalar_input::*; pub use string_input::*; diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index 83cfaa1..39b0551 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -219,7 +219,7 @@ impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for NumberInput<'a, T> fn filter(&self, value: Option) -> Option { let v = match value { - None => self.default_value.map(|x| x), + None => self.default_value, Some(x) => Some(x) }; diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 08e146c..9afdde8 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -493,7 +493,7 @@ mod test { // Mismatch check let value = "1000-99-999"; - match yyyy_mm_dd_input.validate(Some(&value)) { + match yyyy_mm_dd_input.validate(Some(value)) { Ok(_) => panic!("Expected Err(...); Received Ok(())"), Err(tuples) => { assert_eq!(tuples[0].0, PatternMismatch); @@ -509,14 +509,14 @@ mod test { // Valid check 2 let value = "1000-99-99"; - match yyyy_mm_dd_input.validate(Some(&value)) { + match yyyy_mm_dd_input.validate(Some(value)) { Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), Ok(()) => (), } // Valid check let value = "1000-99-99"; - match yyyy_mm_dd_input2.validate(Some(&value)) { + match yyyy_mm_dd_input2.validate(Some(value)) { Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), Ok(()) => (), } @@ -550,7 +550,7 @@ mod test { }, ); - let handle2 = thread::spawn(move || match str_input_instance.validate(Some(&"")) { + let handle2 = thread::spawn(move || match str_input_instance.validate(Some("")) { Err(x) => { assert_eq!( x[0].1.as_str(), @@ -631,7 +631,7 @@ mod test { }, ); - scope.spawn(|| match ymd_check_input_instance.validate(Some(&"")) { + scope.spawn(|| match ymd_check_input_instance.validate(Some("")) { Err(x) => { assert_eq!(x[0].1.as_str(), ymd_mismatch_msg("", ymd_rx.as_str())); } @@ -639,7 +639,7 @@ mod test { }); scope.spawn(|| { - if let Err(_err_tuple) = ymd_check_input_instance.validate(Some(&"2013-08-31")) { + if let Err(_err_tuple) = ymd_check_input_instance.validate(Some("2013-08-31")) { panic!("Expected `Ok(()); Received Err(...)`") } }); diff --git a/inputfilter/src/validator/equal.rs b/inputfilter/src/validator/equal.rs index ddd021b..c63a2e1 100644 --- a/inputfilter/src/validator/equal.rs +++ b/inputfilter/src/validator/equal.rs @@ -77,12 +77,12 @@ mod test { #[test] fn test_construction() -> Result<(), Box> { let instance = EqualityValidatorBuilder::<&str>::default() - .rhs_value("foo".into()) + .rhs_value("foo") .build()?; assert_eq!(instance.rhs_value, "foo"); - assert_eq!((&instance.not_equal_msg)(&instance, "foo"), + assert_eq!((instance.not_equal_msg)(&instance, "foo"), not_equal_msg(&instance, "foo"), "Default 'not_equal_msg' fn should return expected value"); @@ -104,14 +104,14 @@ mod test { if should_be_ok { assert!(validator.validate(lhs_value).is_ok()); - assert!((&validator)(lhs_value).is_ok()); + assert!(validator(lhs_value).is_ok()); } else { assert_eq!( validator.validate(lhs_value), Err(vec![(NotEqual, not_equal_msg(&validator, lhs_value))]) ); assert_eq!( - (&validator)(lhs_value), + validator(lhs_value), Err(vec![(NotEqual, not_equal_msg(&validator, lhs_value))]) ); } diff --git a/inputfilter/src/validator/number.rs b/inputfilter/src/validator/number.rs index 31bd3e2..8622068 100644 --- a/inputfilter/src/validator/number.rs +++ b/inputfilter/src/validator/number.rs @@ -289,16 +289,16 @@ mod test { // ---- let test_value = 99; - assert_eq!((&instance.range_overflow)(&instance, test_value), + assert_eq!((instance.range_overflow)(&instance, test_value), range_overflow_msg(&instance, test_value)); - assert_eq!((&instance.range_underflow)(&instance, test_value), + assert_eq!((instance.range_underflow)(&instance, test_value), range_underflow_msg(&instance, test_value)); - assert_eq!((&instance.step_mismatch)(&instance, test_value), + assert_eq!((instance.step_mismatch)(&instance, test_value), step_mismatch_msg(&instance, test_value)); - assert_eq!((&instance.not_equal)(&instance, test_value), + assert_eq!((instance.not_equal)(&instance, test_value), not_equal_msg(&instance, test_value)); } diff --git a/inputfilter/src/validator/pattern.rs b/inputfilter/src/validator/pattern.rs index 7021a14..1e7b0b7 100644 --- a/inputfilter/src/validator/pattern.rs +++ b/inputfilter/src/validator/pattern.rs @@ -133,10 +133,10 @@ mod test { let _rx = Regex::new(r"^\w{2,55}$")?; fn on_custom_pattern_mismatch(_: &PatternValidator, _: &str) -> String { - return "custom pattern mismatch err message".into() + "custom pattern mismatch err message".into() } - for (name, instance, passingValue, failingValue, err_callback) in [ + for (name, instance, passingValue, failingValue, _err_callback) in [ ("Default", PatternValidatorBuilder::default() .pattern(Cow::Owned(_rx.clone())) .build()?, "abc", "!@#)(*", &pattern_mismatch_msg), From cbf5da2640c76baf92883840eb47ba57114cee0c Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Mon, 4 Dec 2023 23:26:47 -0500 Subject: [PATCH 16/38] inputfilter - progress on types refactor - Stabelized tests and performed some cleanup. --- inputfilter/src/scalar_input.rs | 10 +++------- inputfilter/src/string_input.rs | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index 39b0551..c11e99f 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -22,7 +22,7 @@ pub fn range_overflow_msg(rules: &NumberInput, x: Option) ) } -pub fn num_not_equal_msg( +pub fn scalar_not_equal_msg( rules: &NumberInput, x: Option, ) -> String { @@ -33,10 +33,6 @@ pub fn num_not_equal_msg( ) } -pub fn num_missing_msg(_: &NumberInput, _: Option) -> String { - "Value is missing.".to_string() -} - #[derive(Builder, Clone)] #[builder(setter(strip_option))] pub struct NumberInput<'a, T: ScalarValue> { @@ -74,7 +70,7 @@ pub struct NumberInput<'a, T: ScalarValue> { #[builder(default = "&range_overflow_msg")] pub range_overflow: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), - #[builder(default = "&num_not_equal_msg")] + #[builder(default = "&scalar_not_equal_msg")] pub not_equal: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), #[builder(default = "&value_missing_msg")] @@ -97,7 +93,7 @@ impl<'a, T> NumberInput<'a, T> filters: None, range_underflow: &(range_underflow_msg), range_overflow: &(range_overflow_msg), - not_equal: &(num_not_equal_msg), + not_equal: &(scalar_not_equal_msg), value_missing: &value_missing_msg, } } diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 9afdde8..cef2f92 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -214,7 +214,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// /// let str_input = StringInputBuilder::default() /// .required(true) - /// .value_missing(&|_, _| "Value missing".to_string()) + /// .value_missing(&|_| "Value missing".to_string()) /// .min_length(3usize) /// .too_short(&|_, _| "Too short".to_string()) /// .max_length(200usize) // Default violation message callback used here. @@ -275,7 +275,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// /// let input = StringInputBuilder::default() /// .required(true) - /// .value_missing(&|_, _| "Value missing".to_string()) + /// .value_missing(&|_| "Value missing".to_string()) /// .validators(vec![&|x: &str| { /// if x.len() < 3 { /// return Err(vec![( @@ -328,7 +328,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// /// let input = StringInputBuilder::default() /// .required(true) - /// .value_missing(&|_, _| "Value missing".to_string()) + /// .value_missing(&|_| "Value missing".to_string()) /// .validators(vec![&|x: &str| { /// if x.len() < 3 { /// return Err(vec![( @@ -391,10 +391,6 @@ impl Debug for StringInput<'_, '_> { } } -pub fn str_missing_msg(_: &StringInput, _: Option<&str>) -> String { - "Value is missing.".to_string() -} - #[cfg(test)] mod test { use super::*; From 809ecfb3fde59e222607b70f0e56ee962daf720e Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Mon, 4 Dec 2023 23:29:17 -0500 Subject: [PATCH 17/38] inputfilter - progress on types refactor - Renamed 'NumberInput' to 'ScalarInput'. --- inputfilter/src/scalar_input.rs | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index c11e99f..34121d1 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -4,9 +4,7 @@ use std::fmt::{Debug, Display, Formatter}; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; use crate::{ConstraintViolation, ScalarValue, ValidationErrTuple, value_missing_msg, ValueMissingCallback, WithName}; -pub type NumMissingViolationCallback = dyn Fn(&NumberInput, Option) -> ViolationMessage + Send + Sync; - -pub fn range_underflow_msg(rules: &NumberInput, x: Option) -> String { +pub fn range_underflow_msg(rules: &ScalarInput, x: Option) -> String { format!( "`{:}` is less than minimum `{:}`.", x.unwrap(), @@ -14,7 +12,7 @@ pub fn range_underflow_msg(rules: &NumberInput, x: Option) ) } -pub fn range_overflow_msg(rules: &NumberInput, x: Option) -> String { +pub fn range_overflow_msg(rules: &ScalarInput, x: Option) -> String { format!( "`{:}` is greater than maximum `{:}`.", x.unwrap(), @@ -23,7 +21,7 @@ pub fn range_overflow_msg(rules: &NumberInput, x: Option) } pub fn scalar_not_equal_msg( - rules: &NumberInput, + rules: &ScalarInput, x: Option, ) -> String { format!( @@ -35,7 +33,7 @@ pub fn scalar_not_equal_msg( #[derive(Builder, Clone)] #[builder(setter(strip_option))] -pub struct NumberInput<'a, T: ScalarValue> { +pub struct ScalarInput<'a, T: ScalarValue> { #[builder(default = "true")] pub break_on_failure: bool, @@ -65,23 +63,23 @@ pub struct NumberInput<'a, T: ScalarValue> { pub filters: Option>>>, #[builder(default = "&range_underflow_msg")] - pub range_underflow: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), + pub range_underflow: &'a (dyn Fn(&ScalarInput<'a, T>, Option) -> String + Send + Sync), #[builder(default = "&range_overflow_msg")] - pub range_overflow: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), + pub range_overflow: &'a (dyn Fn(&ScalarInput<'a, T>, Option) -> String + Send + Sync), #[builder(default = "&scalar_not_equal_msg")] - pub not_equal: &'a (dyn Fn(&NumberInput<'a, T>, Option) -> String + Send + Sync), + pub not_equal: &'a (dyn Fn(&ScalarInput<'a, T>, Option) -> String + Send + Sync), #[builder(default = "&value_missing_msg")] pub value_missing: &'a ValueMissingCallback, } -impl<'a, T> NumberInput<'a, T> +impl<'a, T> ScalarInput<'a, T> where T: ScalarValue { pub fn new(name: Option<&'a str>) -> Self { - NumberInput { + ScalarInput { break_on_failure: false, name, min: None, @@ -171,7 +169,7 @@ impl<'a, T> NumberInput<'a, T> } } -impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for NumberInput<'a, T> +impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for ScalarInput<'a, T> where T: ScalarValue + Copy { fn validate(&self, value: Option) -> Result<(), Vec> { match value { @@ -240,19 +238,19 @@ impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for NumberInput<'a, T> } } -impl<'a, T: ScalarValue> WithName<'a> for NumberInput<'a, T> { +impl<'a, T: ScalarValue> WithName<'a> for ScalarInput<'a, T> { fn get_name(&self) -> Option> { self.name.map(Cow::Borrowed) } } -impl Default for NumberInput<'_, T> { +impl Default for ScalarInput<'_, T> { fn default() -> Self { Self::new(None) } } -impl Display for NumberInput<'_, T> { +impl Display for ScalarInput<'_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, @@ -273,7 +271,7 @@ impl Display for NumberInput<'_, T> { } } -impl Debug for NumberInput<'_, T> { +impl Debug for ScalarInput<'_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self) } From 5940cd12c6dffe6600458ff2dd76a45bae5ac7cf Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Mon, 4 Dec 2023 23:38:55 -0500 Subject: [PATCH 18/38] inputfilter - progress on types refactor - Performed minor cleanup on 'NumberValidator', and updated 'derive_builder' version. --- inputfilter/Cargo.toml | 2 +- inputfilter/src/validator/number.rs | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/inputfilter/Cargo.toml b/inputfilter/Cargo.toml index d75e373..4890a7a 100644 --- a/inputfilter/Cargo.toml +++ b/inputfilter/Cargo.toml @@ -8,7 +8,7 @@ include = ["src/**/*", "Cargo.toml"] [dependencies] bytes = "1.4.0" regex = "1.3.1" -derive_builder = "0.9.0" +derive_builder = "0.12.0" serde = { version = "1.0.103", features = ["derive"] } serde_json = "1.0.82" ammonia = "3.3.0" diff --git a/inputfilter/src/validator/number.rs b/inputfilter/src/validator/number.rs index 8622068..58a0212 100644 --- a/inputfilter/src/validator/number.rs +++ b/inputfilter/src/validator/number.rs @@ -36,7 +36,7 @@ pub struct NumberValidator<'a, T: NumberValue> { #[builder(default = "&step_mismatch_msg")] pub step_mismatch: &'a (dyn Fn(&NumberValidator<'a, T>, T) -> String + Send + Sync), - #[builder(default = "¬_equal_msg")] + #[builder(default = "&num_not_equal_msg")] pub not_equal: &'a (dyn Fn(&NumberValidator<'a, T>, T) -> String + Send + Sync), } @@ -97,7 +97,7 @@ where range_underflow: &range_underflow_msg, range_overflow: &range_overflow_msg, step_mismatch: &step_mismatch_msg, - not_equal: ¬_equal_msg, + not_equal: &num_not_equal_msg, } } } @@ -137,10 +137,6 @@ impl ToAttributesList for NumberValidator<'_, T> attrs.push(("step".to_string(), to_json_value(step).unwrap())); } - if let Some(equal) = self.equal { - attrs.push(("pattern".to_string(), to_json_value(equal).unwrap())); - } - if attrs.is_empty() { None } else { @@ -242,7 +238,7 @@ pub fn step_mismatch_msg( ) } -pub fn not_equal_msg( +pub fn num_not_equal_msg( rules: &NumberValidator, x: T, ) -> String { @@ -299,7 +295,7 @@ mod test { step_mismatch_msg(&instance, test_value)); assert_eq!((instance.not_equal)(&instance, test_value), - not_equal_msg(&instance, test_value)); + num_not_equal_msg(&instance, test_value)); } Ok(()) @@ -377,7 +373,7 @@ mod test { Err(_enum) => { let err_msg_tuple = match _enum { StepMismatch => (StepMismatch, step_mismatch_msg(&validator, value)), - NotEqual => (NotEqual, not_equal_msg(&validator, value)), + NotEqual => (NotEqual, num_not_equal_msg(&validator, value)), RangeUnderflow => (RangeUnderflow, range_underflow_msg(&validator, value)), RangeOverflow => (RangeOverflow, range_overflow_msg(&validator, value)), _ => panic!("Unknown enum variant encountered") From d41e331013ba5881b895e9258e6a22d3b424a35a Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Mon, 4 Dec 2023 23:55:40 -0500 Subject: [PATCH 19/38] inputfilter - progress on types refactor - Performed minor lint cleanups. --- _recycler/form/Cargo.toml | 2 +- acl/src/simple.rs | 2 +- graph/src/digraph/symbol_graph.rs | 2 +- inputfilter/src/scalar_input.rs | 4 ++-- inputfilter/src/string_input.rs | 4 ++-- inputfilter/src/validator/number.rs | 6 +++--- inputfilter/src/validator/pattern.rs | 14 +++++++------- src/lib.rs | 1 - 8 files changed, 17 insertions(+), 18 deletions(-) diff --git a/_recycler/form/Cargo.toml b/_recycler/form/Cargo.toml index 2960f23..7d3c4f2 100644 --- a/_recycler/form/Cargo.toml +++ b/_recycler/form/Cargo.toml @@ -9,6 +9,6 @@ edition = "2021" walrs_inputfilter = { path = "../inputfilter" } bytes = "1.4.0" regex = "1.3.1" -derive_builder = "0.9.0" +derive_builder = "0.12.0" serde = { version = "1.0.103", features = ["derive"] } serde_json = "1.0.82" diff --git a/acl/src/simple.rs b/acl/src/simple.rs index 90d8395..83eb88c 100644 --- a/acl/src/simple.rs +++ b/acl/src/simple.rs @@ -986,7 +986,7 @@ impl From for Acl { impl<'a> From<&'a mut File> for Acl { fn from(file: &mut File) -> Self { - (AclData::from(file)).into() + AclData::from(file).into() } } diff --git a/graph/src/digraph/symbol_graph.rs b/graph/src/digraph/symbol_graph.rs index 18671d6..792405f 100644 --- a/graph/src/digraph/symbol_graph.rs +++ b/graph/src/digraph/symbol_graph.rs @@ -733,7 +733,7 @@ mod test { let mut reader = BufReader::new(f); // Create graph - let dg: DisymGraph = (&mut reader).into(); + let _: DisymGraph = (&mut reader).into(); // println!("{:?}", dg); diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index 34121d1..943cd41 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -145,7 +145,7 @@ impl<'a, T> ScalarInput<'a, T> if !self.break_on_failure { return vs.iter().fold( Vec::::new(), - |mut agg, f| match (f)(value) { + |mut agg, f| match f(value) { Err(mut message_tuples) => { agg.append(message_tuples.as_mut()); agg @@ -157,7 +157,7 @@ impl<'a, T> ScalarInput<'a, T> // Else break on, and capture, first failure. let mut agg = Vec::::new(); for f in vs.iter() { - if let Err(mut message_tuples) = (f)(value) { + if let Err(mut message_tuples) = f(value) { agg.append(message_tuples.as_mut()); break; } diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index cef2f92..23ade40 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -166,7 +166,7 @@ impl<'a, 'b> StringInput<'a, 'b> { if !self.break_on_failure { return vs.iter().fold( Vec::::new(), - |mut agg, f| match (f)(value) { + |mut agg, f| match f(value) { Err(mut message_tuples) => { agg.append(message_tuples.as_mut()); agg @@ -178,7 +178,7 @@ impl<'a, 'b> StringInput<'a, 'b> { // Else break on, and capture, first failure. let mut agg = Vec::::new(); for f in vs.iter() { - if let Err(mut message_tuples) = (f)(value) { + if let Err(mut message_tuples) = f(value) { agg.append(message_tuples.as_mut()); break; } diff --git a/inputfilter/src/validator/number.rs b/inputfilter/src/validator/number.rs index 58a0212..a9bcffa 100644 --- a/inputfilter/src/validator/number.rs +++ b/inputfilter/src/validator/number.rs @@ -85,7 +85,7 @@ where _ => unreachable!("Unsupported Constraint Violation Enum matched"), }; - f.map(|_f| (_f)(self, value)).unwrap() + f.map(|_f| _f(self, value)).unwrap() } pub fn new() -> Self { @@ -260,7 +260,7 @@ mod test { fn test_construction() -> Result<(), Box> { // Assert all property states for difference construction scenarios // ---- - for (testName, instance, min, max, step, equal) in [ + for (test_name, instance, min, max, step, equal) in [ ("Default", NumberValidatorBuilder::::default() .build()?, None, None, None, None), ("With Range", NumberValidatorBuilder::::default() @@ -274,7 +274,7 @@ mod test { .step(5) .build()?, None, None, Some(5), None), ] { - println!("\"{}\" test {:}", testName, &instance); + println!("\"{}\" test {:}", test_name, &instance); assert_eq!(instance.min, min); assert_eq!(instance.max, max); diff --git a/inputfilter/src/validator/pattern.rs b/inputfilter/src/validator/pattern.rs index 1e7b0b7..5303160 100644 --- a/inputfilter/src/validator/pattern.rs +++ b/inputfilter/src/validator/pattern.rs @@ -136,7 +136,7 @@ mod test { "custom pattern mismatch err message".into() } - for (name, instance, passingValue, failingValue, _err_callback) in [ + for (name, instance, passing_value, failing_value, _err_callback) in [ ("Default", PatternValidatorBuilder::default() .pattern(Cow::Owned(_rx.clone())) .build()?, "abc", "!@#)(*", &pattern_mismatch_msg), @@ -154,15 +154,15 @@ mod test { println!("{}", name); // Test as an `Fn*` trait - assert_eq!((&instance)(passingValue), Ok(())); - assert_eq!((&instance)(failingValue), Err(vec![ - (PatternMismatch, (&instance.pattern_mismatch)(&instance, failingValue)) + assert_eq!((&instance)(passing_value), Ok(())); + assert_eq!((&instance)(failing_value), Err(vec![ + (PatternMismatch, (&instance.pattern_mismatch)(&instance, failing_value)) ])); // Test `validate` method directly - assert_eq!(instance.validate(passingValue), Ok(())); - assert_eq!(instance.validate(failingValue), Err(vec![ - (PatternMismatch, (&instance.pattern_mismatch)(&instance, failingValue)) + assert_eq!(instance.validate(passing_value), Ok(())); + assert_eq!(instance.validate(failing_value), Err(vec![ + (PatternMismatch, (&instance.pattern_mismatch)(&instance, failing_value)) ])); } diff --git a/src/lib.rs b/src/lib.rs index bf195b1..234303d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,5 @@ pub use walrs_inputfilter::validator; pub use walrs_inputfilter::filter; -pub use walrs_inputfilter::input; pub use walrs_inputfilter::string_input; pub use walrs_inputfilter::types; From 164c2b89a8ff0390562c20e4f1246e9cce99daf4 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Thu, 7 Dec 2023 00:03:59 -0500 Subject: [PATCH 20/38] inputfilter - Added 'WithName' constraint to 'InputConstraints' type, and performed minor cleanup on private API for '(String|Scalar)Input'. --- inputfilter/src/scalar_input.rs | 12 ++++++------ inputfilter/src/string_input.rs | 8 ++++---- inputfilter/src/types.rs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index 943cd41..ceda309 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -138,8 +138,8 @@ impl<'a, T> ScalarInput<'a, T> if errs.is_empty() { Ok(()) } else { Err(errs) } } - fn _validate_against_validators(&self, value: T, validators: Option<&[&'a Validator]>) -> Result<(), Vec> { - validators.map(|vs| { + fn _validate_against_validators(&self, value: T) -> Result<(), Vec> { + self.validators.as_deref().map(|vs| { // If not break on failure then capture all validation errors. if !self.break_on_failure { @@ -170,8 +170,8 @@ impl<'a, T> ScalarInput<'a, T> } impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for ScalarInput<'a, T> - where T: ScalarValue + Copy { - fn validate(&self, value: Option) -> Result<(), Vec> { + where T: ScalarValue { + fn validate(&self, value: Option) -> Result<(), Vec> { match value { None => { if self.required { @@ -185,11 +185,11 @@ impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for ScalarInput<'a, T> } // Else if value is populated validate it Some(v) => match self._validate_against_self(v) { - Ok(_) => self._validate_against_validators(v, self.validators.as_deref()), + Ok(_) => self._validate_against_validators(v), Err(messages1) => if self.break_on_failure { Err(messages1) } else { - match self._validate_against_validators(v, self.validators.as_deref()) { + match self._validate_against_validators(v) { Ok(_) => Ok(()), Err(mut messages2) => { let mut agg = messages1; diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 23ade40..d6dc09d 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -159,8 +159,8 @@ impl<'a, 'b> StringInput<'a, 'b> { if errs.is_empty() { Ok(()) } else { Err(errs) } } - fn _validate_against_validators(&self, value: &'b str, validators: Option<&[&'a Validator<&'b str>]>) -> Result<(), Vec> { - validators.map(|vs| { + fn _validate_against_validators(&self, value: &'b str) -> Result<(), Vec> { + self.validators.as_deref().map(|vs| { // If not break on failure then capture all validation errors. if !self.break_on_failure { @@ -250,11 +250,11 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, } // Else if value is populated validate it Some(v) => match self._validate_against_self(v) { - Ok(_) => self._validate_against_validators(v, self.validators.as_deref()), + Ok(_) => self._validate_against_validators(v), Err(messages1) => if self.break_on_failure { Err(messages1) } else { - match self._validate_against_validators(v, self.validators.as_deref()) { + match self._validate_against_validators(v) { Ok(_) => Ok(()), Err(mut messages2) => { let mut agg = messages1; diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index 69f1635..ae07465 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -109,7 +109,7 @@ pub trait FilterValue { pub type ValueMissingCallback = dyn Fn(&dyn WithName) -> ViolationMessage + Send + Sync; -pub trait InputConstraints<'a, 'b, T: 'b, FT: 'b>: Display + Debug +pub trait InputConstraints<'a, 'b, T: 'b, FT: 'b>: Display + Debug + WithName<'a> where T: InputValue { fn validate(&self, value: Option) -> Result<(), Vec>; From d2a282fa1d987c5a9891aed536bb0e0460113a32 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Thu, 7 Dec 2023 00:13:04 -0500 Subject: [PATCH 21/38] inputfilters - Fixed some lint warnings. --- inputfilter/src/string_input.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index d6dc09d..20297ad 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -482,9 +482,8 @@ mod test { .build()?; // Missing value check - match less_than_1990_input.validate(None) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), + if let Err(errs) = less_than_1990_input.validate(None) { + panic!("Expected Ok(()); Received Err({:#?})", &errs); } // Mismatch check @@ -498,23 +497,20 @@ mod test { } // Valid check - match yyyy_mm_dd_input.validate(None) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), + if let Err(errs) = yyyy_mm_dd_input.validate(None) { + panic!("Expected Ok(()); Received Err({:#?})", &errs); } // Valid check 2 let value = "1000-99-99"; - match yyyy_mm_dd_input.validate(Some(value)) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), + if let Err(errs) = yyyy_mm_dd_input.validate(Some(value)) { + panic!("Expected Ok(()); Received Err({:#?})", &errs); } // Valid check let value = "1000-99-99"; - match yyyy_mm_dd_input2.validate(Some(value)) { - Err(errs) => panic!("Expected Ok(()); Received Err({:#?})", &errs), - Ok(()) => (), + if let Err(errs) = yyyy_mm_dd_input2.validate(Some(value)) { + panic!("Expected Ok(()); Received Err({:#?})", &errs); } Ok(()) From 6253788aed5bb239a895a922636d1abbf8d4dfc8 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Sun, 10 Dec 2023 13:17:35 -0500 Subject: [PATCH 22/38] inputfilter - Progress on scalar_input tests. --- inputfilter/src/scalar_input.rs | 608 +++++++++++++++++++++----------- 1 file changed, 397 insertions(+), 211 deletions(-) diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index ceda309..c0214a1 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -2,280 +2,466 @@ use std::borrow::Cow; use std::fmt::{Debug, Display, Formatter}; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; -use crate::{ConstraintViolation, ScalarValue, ValidationErrTuple, value_missing_msg, ValueMissingCallback, WithName}; - -pub fn range_underflow_msg(rules: &ScalarInput, x: Option) -> String { - format!( - "`{:}` is less than minimum `{:}`.", - x.unwrap(), - &rules.min.unwrap() - ) +use crate::{ + value_missing_msg, ConstraintViolation, ScalarValue, ValidationErrTuple, ValueMissingCallback, + WithName, +}; + +pub fn range_underflow_msg(rules: &ScalarInput, x: T) -> String { + format!( + "`{:}` is less than minimum `{:}`.", + x, + &rules.min.unwrap() + ) } -pub fn range_overflow_msg(rules: &ScalarInput, x: Option) -> String { - format!( - "`{:}` is greater than maximum `{:}`.", - x.unwrap(), - &rules.max.unwrap() - ) +pub fn range_overflow_msg(rules: &ScalarInput, x: T) -> String { + format!( + "`{:}` is greater than maximum `{:}`.", + x, + &rules.max.unwrap() + ) } -pub fn scalar_not_equal_msg( - rules: &ScalarInput, - x: Option, -) -> String { - format!( - "`{:}` is not equal to `{:}`.", - x.unwrap(), - &rules.equal.unwrap() - ) +pub fn scalar_not_equal_msg(rules: &ScalarInput, x: T) -> String { + format!( + "`{:}` is not equal to `{:}`.", + x, + &rules.equal.unwrap() + ) } #[derive(Builder, Clone)] #[builder(setter(strip_option))] pub struct ScalarInput<'a, T: ScalarValue> { - #[builder(default = "true")] - pub break_on_failure: bool, + #[builder(default = "true")] + pub break_on_failure: bool, - /// @todo This should be an `Option>`, for compatibility. - #[builder(setter(into), default = "None")] - pub name: Option<&'a str>, + #[builder(setter(into), default = "None")] + pub name: Option<&'a str>, - #[builder(default = "None")] - pub min: Option, + #[builder(default = "None")] + pub min: Option, - #[builder(default = "None")] - pub max: Option, + #[builder(default = "None")] + pub max: Option, - #[builder(default = "None")] - pub equal: Option, + #[builder(default = "None")] + pub equal: Option, - #[builder(default = "false")] - pub required: bool, + #[builder(default = "false")] + pub required: bool, - #[builder(default = "None")] - pub default_value: Option, + #[builder(default = "None")] + pub default_value: Option, - #[builder(default = "None")] - pub validators: Option>>, + #[builder(default = "None")] + pub validators: Option>>, - #[builder(default = "None")] - pub filters: Option>>>, + #[builder(default = "None")] + pub filters: Option>>>, - #[builder(default = "&range_underflow_msg")] - pub range_underflow: &'a (dyn Fn(&ScalarInput<'a, T>, Option) -> String + Send + Sync), + #[builder(default = "&range_underflow_msg")] + pub range_underflow: &'a (dyn Fn(&ScalarInput<'a, T>, T) -> String + Send + Sync), - #[builder(default = "&range_overflow_msg")] - pub range_overflow: &'a (dyn Fn(&ScalarInput<'a, T>, Option) -> String + Send + Sync), + #[builder(default = "&range_overflow_msg")] + pub range_overflow: &'a (dyn Fn(&ScalarInput<'a, T>, T) -> String + Send + Sync), - #[builder(default = "&scalar_not_equal_msg")] - pub not_equal: &'a (dyn Fn(&ScalarInput<'a, T>, Option) -> String + Send + Sync), + #[builder(default = "&scalar_not_equal_msg")] + pub not_equal: &'a (dyn Fn(&ScalarInput<'a, T>, T) -> String + Send + Sync), - #[builder(default = "&value_missing_msg")] - pub value_missing: &'a ValueMissingCallback, + #[builder(default = "&value_missing_msg")] + pub value_missing: &'a ValueMissingCallback, } impl<'a, T> ScalarInput<'a, T> - where T: ScalarValue +where + T: ScalarValue, { - pub fn new(name: Option<&'a str>) -> Self { - ScalarInput { - break_on_failure: false, - name, - min: None, - max: None, - equal: None, - required: false, - default_value: None, - validators: None, - filters: None, - range_underflow: &(range_underflow_msg), - range_overflow: &(range_overflow_msg), - not_equal: &(scalar_not_equal_msg), - value_missing: &value_missing_msg, - } + /// Returns a new instance containing defaults. + pub fn new(name: Option<&'a str>) -> Self { + ScalarInput { + break_on_failure: false, + name, + min: None, + max: None, + equal: None, + required: false, + default_value: None, + validators: None, + filters: None, + range_underflow: &(range_underflow_msg), + range_overflow: &(range_overflow_msg), + not_equal: &(scalar_not_equal_msg), + value_missing: &value_missing_msg, } + } - fn _validate_against_self(&self, value: T) -> Result<(), Vec> { - let mut errs = vec![]; + pub fn fmap(&self, f: impl FnOnce(&Self) -> Self) -> Self { + f(self) + } - // Test lower bound - if let Some(min) = self.min { - if value < min { - errs.push(( - ConstraintViolation::RangeUnderflow, - (self.range_underflow)(self, Some(value)), - )); + fn _run_own_validators_on(&self, value: T) -> Result<(), Vec> { + let mut errs = vec![]; - if self.break_on_failure { return Err(errs); } - } + // Test lower bound + if let Some(min) = self.min { + if value < min { + errs.push(( + ConstraintViolation::RangeUnderflow, + (self.range_underflow)(self, value), + )); + + if self.break_on_failure { + return Err(errs); } + } + } - // Test upper bound - if let Some(max) = self.max { - if value > max { - errs.push(( - ConstraintViolation::TooLong, - (self.range_overflow)(self, Some(value)), - )); + // Test upper bound + if let Some(max) = self.max { + if value > max { + errs.push(( + ConstraintViolation::RangeOverflow, + (self.range_overflow)(self, value), + )); - if self.break_on_failure { return Err(errs); } - } + if self.break_on_failure { + return Err(errs); } + } + } - // Test equality - if let Some(equal) = self.equal { - if value != equal { - errs.push(( - ConstraintViolation::NotEqual, - (self.not_equal)(self, Some(value)), - )); + // Test equality + if let Some(equal) = self.equal { + if value != equal { + errs.push(( + ConstraintViolation::NotEqual, + (self.not_equal)(self, value), + )); - if self.break_on_failure { return Err(errs); } - } + if self.break_on_failure { + return Err(errs); } - - if errs.is_empty() { Ok(()) } else { Err(errs) } + } } - fn _validate_against_validators(&self, value: T) -> Result<(), Vec> { - self.validators.as_deref().map(|vs| { - - // If not break on failure then capture all validation errors. - if !self.break_on_failure { - return vs.iter().fold( - Vec::::new(), - |mut agg, f| match f(value) { - Err(mut message_tuples) => { - agg.append(message_tuples.as_mut()); - agg - } - _ => agg, - }); - } - - // Else break on, and capture, first failure. - let mut agg = Vec::::new(); - for f in vs.iter() { - if let Err(mut message_tuples) = f(value) { - agg.append(message_tuples.as_mut()); - break; - } - } - agg - }) - .and_then(|messages| if messages.is_empty() { None } else { Some(messages) }) - .map_or(Ok(()), Err) + if errs.is_empty() { + Ok(()) + } else { + Err(errs) } + } + + fn _run_validators_on(&self, value: T) -> Result<(), Vec> { + self + .validators + .as_deref() + .map(|vs| { + // If not break on failure then capture all validation errors. + if !self.break_on_failure { + return vs + .iter() + .fold(Vec::::new(), |mut agg, f| { + match f(value) { + Err(mut message_tuples) => { + agg.append(message_tuples.as_mut()); + agg + } + _ => agg, + } + }); + } + + // Else break on, and capture, first failure. + let mut agg = Vec::::new(); + for f in vs.iter() { + if let Err(mut message_tuples) = f(value) { + agg.append(message_tuples.as_mut()); + break; + } + } + agg + }) + .and_then(|messages| { + if messages.is_empty() { + None + } else { + Some(messages) + } + }) + .map_or(Ok(()), Err) + } } impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for ScalarInput<'a, T> - where T: ScalarValue { - fn validate(&self, value: Option) -> Result<(), Vec> { - match value { - None => { - if self.required { - Err(vec![( - ConstraintViolation::ValueMissing, - (self.value_missing)(self), - )]) - } else { - Ok(()) - } +where + T: ScalarValue, +{ + /// Validates given value against contained constraints. + /// + /// ```rust + /// use walrs_inputfilter::{ + /// ScalarInput, InputConstraints, ConstraintViolation, ScalarInputBuilder, + /// range_underflow_msg, range_overflow_msg, + /// ScalarValue + /// }; + /// use walrs_inputfilter::equal::not_equal_msg; + /// + /// ``` + fn validate(&self, value: Option) -> Result<(), Vec> { + match value { + None => { + if self.required { + Err(vec![( + ConstraintViolation::ValueMissing, + (self.value_missing)(self), + )]) + } else { + Ok(()) + } + } + // Else if value is populated validate it + Some(v) => + match self._run_own_validators_on(v) { + Ok(_) => self._run_validators_on(v), + Err(messages1) => + if self.break_on_failure { + Err(messages1) + } else if let Err(mut messages2) = self._run_validators_on(v) { + let mut agg = messages1; + agg.append(messages2.as_mut()); + Err(agg) + } else { + Err(messages1) } - // Else if value is populated validate it - Some(v) => match self._validate_against_self(v) { - Ok(_) => self._validate_against_validators(v), - Err(messages1) => if self.break_on_failure { - Err(messages1) - } else { - match self._validate_against_validators(v) { - Ok(_) => Ok(()), - Err(mut messages2) => { - let mut agg = messages1; - agg.append(messages2.as_mut()); - Err(agg) - } - } - } - }, } } + } - fn validate1(&self, value: Option) -> Result<(), Vec> { - match self.validate(value) { - // If errors, extract messages and return them - Err(messages) => - Err(messages.into_iter().map(|(_, message)| message).collect()), - Ok(_) => Ok(()), - } + fn validate1(&self, value: Option) -> Result<(), Vec> { + match self.validate(value) { + // If errors, extract messages and return them + Err(messages) => Err(messages.into_iter().map(|(_, message)| message).collect()), + Ok(_) => Ok(()), } + } - fn filter(&self, value: Option) -> Option { - let v = match value { - None => self.default_value, - Some(x) => Some(x) - }; + fn filter(&self, value: Option) -> Option { + let v = match value { + None => self.default_value, + Some(x) => Some(x), + }; - match self.filters.as_deref() { - None => v, - Some(fs) => fs.iter().fold(v, |agg, f| f(agg)), - } + match self.filters.as_deref() { + None => v, + Some(fs) => fs.iter().fold(v, |agg, f| f(agg)), } - - // @todo consolidate these (`validate_and_filter*`), into just `filter*` ( - // since we really don't want to use filtered values without them being valid/etc.) - fn validate_and_filter(&self, x: Option) -> Result, Vec> { - self.validate(x).map(|_| self.filter(x)) - } - - fn validate_and_filter1(&self, x: Option) -> Result, Vec> { - match self.validate_and_filter(x) { - Err(messages) => - Err(messages.into_iter().map(|(_, message)| message).collect()), - Ok(filtered) => Ok(filtered), - } + } + + // @todo consolidate these (`validate_and_filter*`), into just `filter*` ( + // since we really don't want to use filtered values without them being valid/etc.) + fn validate_and_filter(&self, x: Option) -> Result, Vec> { + self.validate(x).map(|_| self.filter(x)) + } + + fn validate_and_filter1(&self, x: Option) -> Result, Vec> { + match self.validate_and_filter(x) { + Err(messages) => Err(messages.into_iter().map(|(_, message)| message).collect()), + Ok(filtered) => Ok(filtered), } + } } impl<'a, T: ScalarValue> WithName<'a> for ScalarInput<'a, T> { - fn get_name(&self) -> Option> { - self.name.map(Cow::Borrowed) - } + fn get_name(&self) -> Option> { + self.name.map(Cow::Borrowed) + } } impl Default for ScalarInput<'_, T> { - fn default() -> Self { - Self::new(None) - } + fn default() -> Self { + Self::new(None) + } } impl Display for ScalarInput<'_, T> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "ScalarInput {{ name: {}, required: {}, validators: {}, filters: {} }}", - self.name.unwrap_or("None"), - self.required, - self - .validators - .as_deref() - .map(|vs| format!("Some([Validator; {}])", vs.len())) - .unwrap_or("None".to_string()), - self - .filters - .as_deref() - .map(|fs| format!("Some([Filter; {}])", fs.len())) - .unwrap_or("None".to_string()), - ) - } + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "ScalarInput {{ name: {}, required: {}, validators: {}, filters: {} }}", + self.name.unwrap_or("None"), + self.required, + self + .validators + .as_deref() + .map(|vs| format!("Some([Validator; {}])", vs.len())) + .unwrap_or("None".to_string()), + self + .filters + .as_deref() + .map(|fs| format!("Some([Filter; {}])", fs.len())) + .unwrap_or("None".to_string()), + ) + } } impl Debug for ScalarInput<'_, T> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", &self) - } + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self) + } } #[cfg(test)] -mod test {} +mod test { + use super::*; + use crate::{ConstraintViolation, InputConstraints}; + + #[test] + fn test_validate() { + // Ensure each logic case in method is sound, and that method is callable for each scalar type: + // 1) Test method logic + // ---- + let validate_is_even = |x: usize| if x % 2 != 0 { + Err(vec![(ConstraintViolation::CustomError, "Must be even".to_string())]) + } else { + Ok(()) + }; + + let usize_input_default = ScalarInputBuilder::::default() + .build() + .unwrap(); + + let usize_not_required = ScalarInputBuilder::::default() + .min(1) + .max(10) + .validators(vec![&validate_is_even]) + .build() + .unwrap(); + + let usize_required = usize_not_required.fmap(|input| { + let mut new_input = input.clone(); + new_input.required = true; + new_input + }); + + let usize_no_break_on_failure = usize_required.fmap(|input| { + let mut new_input = input.clone(); + // new_input.validators.push(&|x: usize| if x % 2 != 0 { + // Err(vec![(ConstraintViolation::CustomError, "Must be even".to_string())]) + // } else { + // Ok(()) + // }); + new_input.break_on_failure = true; + new_input + }); + + let test_cases = vec![ + ("Default, with no value", &usize_input_default, None, Ok(())), + ("Default, with value", &usize_input_default, Some(1), Ok(())), + + // Not required + // ---- + ("1-10, Even, no value", &usize_not_required, None, Ok(())), + ("1-10, Even, with valid value", &usize_not_required, Some(2), Ok(())), + ("1-10, Even, with valid value (2)", &usize_not_required, Some(10), Ok(())), + ("1-10, Even, with invalid value", &usize_not_required, Some(0), Err(vec![ + (ConstraintViolation::RangeUnderflow, + range_underflow_msg(&usize_not_required, 0)) + ])), + ("1-10, Even, with invalid value(2)", &usize_not_required, Some(11), Err(vec![ + (ConstraintViolation::RangeOverflow, + range_overflow_msg(&usize_not_required, 11)), + ])), + ("1-10, Even, with invalid value (3)", &usize_not_required, Some(7), Err(vec![ + (ConstraintViolation::CustomError, + "Must be even".to_string()), + ])), + ("1-10, Even, with value value", &usize_not_required, Some(8), Ok(())), + + // Required + // ---- + ("1-10, Even, required, no value", &usize_required, None, Err(vec![ + (ConstraintViolation::ValueMissing, + value_missing_msg(&usize_required)), + ])), + ("1-10, Even, required, with valid value", &usize_required, Some(2), Ok(())), + ("1-10, Even, required, with valid value (2)", &usize_required, Some(10), Ok(())), + ("1-10, Even, required, with invalid value", &usize_required, Some(0), Err(vec![ + (ConstraintViolation::RangeUnderflow, + range_underflow_msg(&usize_required, 0)), + ])), + ("1-10, Even, required, with invalid value(2)", &usize_required, Some(11), Err(vec![ + (ConstraintViolation::RangeOverflow, + range_overflow_msg(&usize_required, 11)), + ])), + ("1-10, Even, required, with invalid value (3)", &usize_required, Some(7), Err(vec![ + (ConstraintViolation::CustomError, + "Must be even".to_string()), + ])), + ("1-10, Even, required, with value value", &usize_required, Some(8), Ok(())), + // ("1-10, Even, 'break-on-failure: true' false", &usize_no_break_on_failure, Some(7), Err(vec![ + // (ConstraintViolation::CustomError, + // "Must be even".to_string()), + // ])), + ]; + + for (i, (test_name, input, subj, expected)) in test_cases.into_iter().enumerate() { + println!("Case {}: {}", i + 1, test_name); + + assert_eq!(input.validate(subj), expected); + } + + // Test basic usage with other types + // ---- + // Validates `f64`, and `f32` types + let f64_input_required = ScalarInputBuilder::::default() + .required(true) + .min(1.0) + .max(10.0) + .validators(vec![&|x: f64| if x % 2.0 != 0.0 { + Err(vec![(ConstraintViolation::CustomError, "Must be even".to_string())]) + } else { + Ok(()) + }]) + .build() + .unwrap(); + + assert_eq!(f64_input_required.validate(None), Err(vec![ + (ConstraintViolation::ValueMissing, + value_missing_msg(&f64_input_required)), + ])); + assert_eq!(f64_input_required.validate(Some(2.0)), Ok(())); + assert_eq!(f64_input_required.validate(Some(11.0)), Err(vec![ + (ConstraintViolation::RangeOverflow, + range_overflow_msg(&f64_input_required, 11.0)), + ])); + + // Test `char` type usage + let char_input = ScalarInputBuilder::::default() + .min('a') + .max('f') + .build() + .unwrap(); + + assert_eq!(char_input.validate(None), Ok(())); + assert_eq!(char_input.validate(Some('a')), Ok(())); + assert_eq!(char_input.validate(Some('f')), Ok(())); + assert_eq!(char_input.validate(Some('g')), Err(vec![ + (ConstraintViolation::RangeOverflow, + "`g` is greater than maximum `f`.".to_string()), + ])); + + // Test `equal` field usage + let char_input_equal = ScalarInputBuilder::::default() + .equal('a') + .build() + .unwrap(); + + assert_eq!(char_input_equal.validate(None), Ok(())); + assert_eq!(char_input_equal.validate(Some('b')), Err(vec![ + (ConstraintViolation::NotEqual, + "`b` is not equal to `a`.".to_string()), + ])); + } +} From 5df7d4f7410ae7e569ff45ff2e3ff6f311eb9258 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Thu, 25 Jan 2024 23:18:57 -0400 Subject: [PATCH 23/38] inputfilter - Changed 'ValidationErrTuple' to 'ViolationTuple', and 'ConstraintViolation' to 'ViolationEnum'. Other: - Removed 'fmap' method from 'ScalarInput'. --- inputfilter/src/scalar_input.rs | 72 +++++++++++++--------------- inputfilter/src/string_input.rs | 34 ++++++------- inputfilter/src/types.rs | 10 ++-- inputfilter/src/validator/equal.rs | 6 +-- inputfilter/src/validator/number.rs | 10 ++-- inputfilter/src/validator/pattern.rs | 4 +- 6 files changed, 66 insertions(+), 70 deletions(-) diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index c0214a1..598779e 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Display, Formatter}; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; use crate::{ - value_missing_msg, ConstraintViolation, ScalarValue, ValidationErrTuple, ValueMissingCallback, + value_missing_msg, ViolationEnum, ScalarValue, ViolationTuple, ValueMissingCallback, WithName, }; @@ -97,18 +97,14 @@ where } } - pub fn fmap(&self, f: impl FnOnce(&Self) -> Self) -> Self { - f(self) - } - - fn _run_own_validators_on(&self, value: T) -> Result<(), Vec> { + fn _run_own_validators_on(&self, value: T) -> Result<(), Vec> { let mut errs = vec![]; // Test lower bound if let Some(min) = self.min { if value < min { errs.push(( - ConstraintViolation::RangeUnderflow, + ViolationEnum::RangeUnderflow, (self.range_underflow)(self, value), )); @@ -122,7 +118,7 @@ where if let Some(max) = self.max { if value > max { errs.push(( - ConstraintViolation::RangeOverflow, + ViolationEnum::RangeOverflow, (self.range_overflow)(self, value), )); @@ -136,7 +132,7 @@ where if let Some(equal) = self.equal { if value != equal { errs.push(( - ConstraintViolation::NotEqual, + ViolationEnum::NotEqual, (self.not_equal)(self, value), )); @@ -153,7 +149,7 @@ where } } - fn _run_validators_on(&self, value: T) -> Result<(), Vec> { + fn _run_validators_on(&self, value: T) -> Result<(), Vec> { self .validators .as_deref() @@ -162,7 +158,7 @@ where if !self.break_on_failure { return vs .iter() - .fold(Vec::::new(), |mut agg, f| { + .fold(Vec::::new(), |mut agg, f| { match f(value) { Err(mut message_tuples) => { agg.append(message_tuples.as_mut()); @@ -174,7 +170,7 @@ where } // Else break on, and capture, first failure. - let mut agg = Vec::::new(); + let mut agg = Vec::::new(); for f in vs.iter() { if let Err(mut message_tuples) = f(value) { agg.append(message_tuples.as_mut()); @@ -202,19 +198,19 @@ where /// /// ```rust /// use walrs_inputfilter::{ - /// ScalarInput, InputConstraints, ConstraintViolation, ScalarInputBuilder, + /// ScalarInput, InputConstraints, ViolationEnum, ScalarInputBuilder, /// range_underflow_msg, range_overflow_msg, /// ScalarValue /// }; /// use walrs_inputfilter::equal::not_equal_msg; /// /// ``` - fn validate(&self, value: Option) -> Result<(), Vec> { + fn validate(&self, value: Option) -> Result<(), Vec> { match value { None => { if self.required { Err(vec![( - ConstraintViolation::ValueMissing, + ViolationEnum::ValueMissing, (self.value_missing)(self), )]) } else { @@ -261,7 +257,7 @@ where // @todo consolidate these (`validate_and_filter*`), into just `filter*` ( // since we really don't want to use filtered values without them being valid/etc.) - fn validate_and_filter(&self, x: Option) -> Result, Vec> { + fn validate_and_filter(&self, x: Option) -> Result, Vec> { self.validate(x).map(|_| self.filter(x)) } @@ -315,7 +311,7 @@ impl Debug for ScalarInput<'_, T> { #[cfg(test)] mod test { use super::*; - use crate::{ConstraintViolation, InputConstraints}; + use crate::{ViolationEnum, InputConstraints}; #[test] fn test_validate() { @@ -323,7 +319,7 @@ mod test { // 1) Test method logic // ---- let validate_is_even = |x: usize| if x % 2 != 0 { - Err(vec![(ConstraintViolation::CustomError, "Must be even".to_string())]) + Err(vec![(ViolationEnum::CustomError, "Must be even".to_string())]) } else { Ok(()) }; @@ -339,14 +335,14 @@ mod test { .build() .unwrap(); - let usize_required = usize_not_required.fmap(|input| { - let mut new_input = input.clone(); - new_input.required = true; - new_input - }); + let usize_required = (|| -> ScalarInput { + let mut new_input = usize_not_required.clone(); + new_input.required = true; + new_input + })(); - let usize_no_break_on_failure = usize_required.fmap(|input| { - let mut new_input = input.clone(); + let _usize_no_break_on_failure = (|| -> ScalarInput { + let mut new_input = usize_required.clone(); // new_input.validators.push(&|x: usize| if x % 2 != 0 { // Err(vec![(ConstraintViolation::CustomError, "Must be even".to_string())]) // } else { @@ -354,7 +350,7 @@ mod test { // }); new_input.break_on_failure = true; new_input - }); + })(); let test_cases = vec![ ("Default, with no value", &usize_input_default, None, Ok(())), @@ -366,15 +362,15 @@ mod test { ("1-10, Even, with valid value", &usize_not_required, Some(2), Ok(())), ("1-10, Even, with valid value (2)", &usize_not_required, Some(10), Ok(())), ("1-10, Even, with invalid value", &usize_not_required, Some(0), Err(vec![ - (ConstraintViolation::RangeUnderflow, + (ViolationEnum::RangeUnderflow, range_underflow_msg(&usize_not_required, 0)) ])), ("1-10, Even, with invalid value(2)", &usize_not_required, Some(11), Err(vec![ - (ConstraintViolation::RangeOverflow, + (ViolationEnum::RangeOverflow, range_overflow_msg(&usize_not_required, 11)), ])), ("1-10, Even, with invalid value (3)", &usize_not_required, Some(7), Err(vec![ - (ConstraintViolation::CustomError, + (ViolationEnum::CustomError, "Must be even".to_string()), ])), ("1-10, Even, with value value", &usize_not_required, Some(8), Ok(())), @@ -382,21 +378,21 @@ mod test { // Required // ---- ("1-10, Even, required, no value", &usize_required, None, Err(vec![ - (ConstraintViolation::ValueMissing, + (ViolationEnum::ValueMissing, value_missing_msg(&usize_required)), ])), ("1-10, Even, required, with valid value", &usize_required, Some(2), Ok(())), ("1-10, Even, required, with valid value (2)", &usize_required, Some(10), Ok(())), ("1-10, Even, required, with invalid value", &usize_required, Some(0), Err(vec![ - (ConstraintViolation::RangeUnderflow, + (ViolationEnum::RangeUnderflow, range_underflow_msg(&usize_required, 0)), ])), ("1-10, Even, required, with invalid value(2)", &usize_required, Some(11), Err(vec![ - (ConstraintViolation::RangeOverflow, + (ViolationEnum::RangeOverflow, range_overflow_msg(&usize_required, 11)), ])), ("1-10, Even, required, with invalid value (3)", &usize_required, Some(7), Err(vec![ - (ConstraintViolation::CustomError, + (ViolationEnum::CustomError, "Must be even".to_string()), ])), ("1-10, Even, required, with value value", &usize_required, Some(8), Ok(())), @@ -420,7 +416,7 @@ mod test { .min(1.0) .max(10.0) .validators(vec![&|x: f64| if x % 2.0 != 0.0 { - Err(vec![(ConstraintViolation::CustomError, "Must be even".to_string())]) + Err(vec![(ViolationEnum::CustomError, "Must be even".to_string())]) } else { Ok(()) }]) @@ -428,12 +424,12 @@ mod test { .unwrap(); assert_eq!(f64_input_required.validate(None), Err(vec![ - (ConstraintViolation::ValueMissing, + (ViolationEnum::ValueMissing, value_missing_msg(&f64_input_required)), ])); assert_eq!(f64_input_required.validate(Some(2.0)), Ok(())); assert_eq!(f64_input_required.validate(Some(11.0)), Err(vec![ - (ConstraintViolation::RangeOverflow, + (ViolationEnum::RangeOverflow, range_overflow_msg(&f64_input_required, 11.0)), ])); @@ -448,7 +444,7 @@ mod test { assert_eq!(char_input.validate(Some('a')), Ok(())); assert_eq!(char_input.validate(Some('f')), Ok(())); assert_eq!(char_input.validate(Some('g')), Err(vec![ - (ConstraintViolation::RangeOverflow, + (ViolationEnum::RangeOverflow, "`g` is greater than maximum `f`.".to_string()), ])); @@ -460,7 +456,7 @@ mod test { assert_eq!(char_input_equal.validate(None), Ok(())); assert_eq!(char_input_equal.validate(Some('b')), Err(vec![ - (ConstraintViolation::NotEqual, + (ViolationEnum::NotEqual, "`b` is not equal to `a`.".to_string()), ])); } diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 20297ad..382c13c 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Display, Formatter}; use regex::Regex; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; -use crate::{ConstraintViolation, ValidationErrTuple, ValidationResult, value_missing_msg, ValueMissingCallback, WithName}; +use crate::{ViolationEnum, ViolationTuple, ValidationResult, value_missing_msg, ValueMissingCallback, WithName}; pub type StrMissingViolationCallback = dyn Fn(&StringInput, Option<&str>) -> ViolationMessage + Send + Sync; @@ -114,7 +114,7 @@ impl<'a, 'b> StringInput<'a, 'b> { if let Some(min_length) = self.min_length { if value.len() < min_length { errs.push(( - ConstraintViolation::TooShort, + ViolationEnum::TooShort, (self.too_short)(self, Some(value)), )); @@ -125,7 +125,7 @@ impl<'a, 'b> StringInput<'a, 'b> { if let Some(max_length) = self.max_length { if value.len() > max_length { errs.push(( - ConstraintViolation::TooLong, + ViolationEnum::TooLong, (self.too_long)(self, Some(value)), )); @@ -136,7 +136,7 @@ impl<'a, 'b> StringInput<'a, 'b> { if let Some(pattern) = &self.pattern { if !pattern.is_match(value) { errs.push(( - ConstraintViolation::PatternMismatch, + ViolationEnum::PatternMismatch, (self. pattern_mismatch)(self, Some(value)), )); @@ -148,7 +148,7 @@ impl<'a, 'b> StringInput<'a, 'b> { if let Some(equal) = &self.equal { if value != *equal { errs.push(( - ConstraintViolation::NotEqual, + ViolationEnum::NotEqual, (self.not_equal)(self, Some(value)), )); @@ -159,13 +159,13 @@ impl<'a, 'b> StringInput<'a, 'b> { if errs.is_empty() { Ok(()) } else { Err(errs) } } - fn _validate_against_validators(&self, value: &'b str) -> Result<(), Vec> { + fn _validate_against_validators(&self, value: &'b str) -> Result<(), Vec> { self.validators.as_deref().map(|vs| { // If not break on failure then capture all validation errors. if !self.break_on_failure { return vs.iter().fold( - Vec::::new(), + Vec::::new(), |mut agg, f| match f(value) { Err(mut message_tuples) => { agg.append(message_tuples.as_mut()); @@ -176,7 +176,7 @@ impl<'a, 'b> StringInput<'a, 'b> { } // Else break on, and capture, first failure. - let mut agg = Vec::::new(); + let mut agg = Vec::::new(); for f in vs.iter() { if let Err(mut message_tuples) = f(value) { agg.append(message_tuples.as_mut()); @@ -207,7 +207,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// ```rust /// use walrs_inputfilter::*; /// use walrs_inputfilter::pattern::PatternValidator; - /// use walrs_inputfilter::types::ConstraintViolation::{ + /// use walrs_inputfilter::types::ViolationEnum::{ /// ValueMissing, TooShort, TooLong, TypeMismatch, CustomError, /// RangeOverflow, RangeUnderflow, StepMismatch /// }; @@ -236,12 +236,12 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// assert_eq!(str_input.validate(Some(&"abc")), Err(vec![ (TypeMismatch, "Invalid email".to_string()) ])); /// assert_eq!(str_input.validate(Some(&"abc@def")), Ok(())); /// ``` - fn validate(&self, value: Option<&'b str>) -> Result<(), Vec> { + fn validate(&self, value: Option<&'b str>) -> Result<(), Vec> { match value { None => { if self.required { Err(vec![( - ConstraintViolation::ValueMissing, + ViolationEnum::ValueMissing, (self.value_missing)(self), )]) } else { @@ -279,7 +279,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// .validators(vec![&|x: &str| { /// if x.len() < 3 { /// return Err(vec![( - /// ConstraintViolation::TooShort, + /// ViolationEnum::TooShort, /// "Too short".to_string(), /// )]); /// } @@ -315,7 +315,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, // @todo consolidate these (`validate_and_filter*`), into just `filter*` ( // since we really don't want to use filtered values without them being valid/etc.) - fn validate_and_filter(&self, x: Option<&'b str>) -> Result>, Vec> { + fn validate_and_filter(&self, x: Option<&'b str>) -> Result>, Vec> { self.validate(x).map(|_| self.filter(x.map(Cow::Borrowed))) } @@ -332,7 +332,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// .validators(vec![&|x: &str| { /// if x.len() < 3 { /// return Err(vec![( - /// ConstraintViolation::TooShort, + /// ViolationEnum::TooShort, /// "Too short".to_string(), /// )]); /// } @@ -395,8 +395,8 @@ impl Debug for StringInput<'_, '_> { mod test { use super::*; use crate::types::{ - ConstraintViolation, - ConstraintViolation::{PatternMismatch, RangeOverflow}, + ViolationEnum, + ViolationEnum::{PatternMismatch, RangeOverflow}, InputConstraints, ValidationResult, }; use crate::validator::pattern::PatternValidator; @@ -667,7 +667,7 @@ mod test { Ok(()) } else { Err(vec![( - ConstraintViolation::TypeMismatch, + ViolationEnum::TypeMismatch, "Error".to_string(), )]) } diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index ae07465..44b408c 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -72,7 +72,7 @@ impl NumberValue for f32 {} impl NumberValue for f64 {} #[derive(PartialEq, Debug, Clone, Copy)] -pub enum ConstraintViolation { +pub enum ViolationEnum { CustomError, PatternMismatch, RangeOverflow, @@ -87,9 +87,9 @@ pub enum ConstraintViolation { pub type ViolationMessage = String; -pub type ValidationErrTuple = (ConstraintViolation, ViolationMessage); +pub type ViolationTuple = (ViolationEnum, ViolationMessage); -pub type ValidationResult = Result<(), Vec>; +pub type ValidationResult = Result<(), Vec>; pub type Filter = dyn Fn(T) -> T + Send + Sync; @@ -112,13 +112,13 @@ pub type ValueMissingCallback = dyn Fn(&dyn WithName) -> ViolationMessage + Send pub trait InputConstraints<'a, 'b, T: 'b, FT: 'b>: Display + Debug + WithName<'a> where T: InputValue { - fn validate(&self, value: Option) -> Result<(), Vec>; + fn validate(&self, value: Option) -> Result<(), Vec>; fn validate1(&self, value: Option) -> Result<(), Vec>; fn filter(&self, value: Option) -> Option; - fn validate_and_filter(&self, x: Option) -> Result, Vec>; + fn validate_and_filter(&self, x: Option) -> Result, Vec>; fn validate_and_filter1(&self, x: Option) -> Result, Vec>; } diff --git a/inputfilter/src/validator/equal.rs b/inputfilter/src/validator/equal.rs index c63a2e1..235c3ca 100644 --- a/inputfilter/src/validator/equal.rs +++ b/inputfilter/src/validator/equal.rs @@ -1,6 +1,6 @@ use std::fmt::Display; use crate::ToAttributesList; -use crate::types::ConstraintViolation; +use crate::types::ViolationEnum; use crate::types::{InputValue, ValidateValue, ValidationResult}; #[derive(Builder, Clone)] @@ -20,7 +20,7 @@ impl<'a, T> ValidateValue for EqualityValidator<'a, T> if x == self.rhs_value { Ok(()) } else { - Err(vec![(ConstraintViolation::NotEqual, (self.not_equal_msg)(self, x))]) + Err(vec![(ViolationEnum::NotEqual, (self.not_equal_msg)(self, x))]) } } } @@ -71,7 +71,7 @@ pub fn not_equal_msg(_: &EqualityValidator, value: T) #[cfg(test)] mod test { use std::error::Error; - use crate::ConstraintViolation::NotEqual; + use crate::ViolationEnum::NotEqual; use super::*; #[test] diff --git a/inputfilter/src/validator/number.rs b/inputfilter/src/validator/number.rs index a9bcffa..dc43f8b 100644 --- a/inputfilter/src/validator/number.rs +++ b/inputfilter/src/validator/number.rs @@ -1,8 +1,8 @@ use std::fmt::{Display, Formatter}; use crate::ToAttributesList; use crate::types::{ - ConstraintViolation, - ConstraintViolation::{ + ViolationEnum, + ViolationEnum::{ NotEqual, RangeOverflow, RangeUnderflow, StepMismatch, }, NumberValue, ValidateValue, ValidationResult, }; @@ -44,7 +44,7 @@ impl<'a, T> NumberValidator<'a, T> where T: NumberValue, { - fn _validate_integer(&self, v: T) -> Option { + fn _validate_integer(&self, v: T) -> Option { // Test Min if let Some(min) = self.min { if v < min { @@ -76,7 +76,7 @@ where None } - fn _get_violation_msg(&self, violation: ConstraintViolation, value: T) -> String { + fn _get_violation_msg(&self, violation: ViolationEnum, value: T) -> String { let f = match violation { RangeUnderflow => Some(&self.range_underflow), RangeOverflow => Some(&self.range_overflow), @@ -253,7 +253,7 @@ pub fn num_not_equal_msg( mod test { use std::error::Error; - use crate::ConstraintViolation::NotEqual; + use crate::ViolationEnum::NotEqual; use super::*; #[test] diff --git a/inputfilter/src/validator/pattern.rs b/inputfilter/src/validator/pattern.rs index 5303160..d820b85 100644 --- a/inputfilter/src/validator/pattern.rs +++ b/inputfilter/src/validator/pattern.rs @@ -3,7 +3,7 @@ use std::fmt::Display; use regex::Regex; use crate::ToAttributesList; -use crate::types::ConstraintViolation::PatternMismatch; +use crate::types::ViolationEnum::PatternMismatch; use crate::types::{ValidationResult, ValidateValue}; pub type PatternViolationCallback = dyn Fn(&PatternValidator, &str) -> String + Send + Sync; @@ -124,7 +124,7 @@ mod test { use std::error::Error; use regex::Regex; use crate::{ValidateValue}; - use crate::ConstraintViolation::PatternMismatch; + use crate::ViolationEnum::PatternMismatch; use super::*; From 9f7c0b0036b657c88a86051fa1be4a47b278b400 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:15:15 -0400 Subject: [PATCH 24/38] inputfilter - Removed some 'todos'. --- inputfilter/src/scalar_input.rs | 2 -- inputfilter/src/string_input.rs | 2 -- 2 files changed, 4 deletions(-) diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index 598779e..f884cd5 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -255,8 +255,6 @@ where } } - // @todo consolidate these (`validate_and_filter*`), into just `filter*` ( - // since we really don't want to use filtered values without them being valid/etc.) fn validate_and_filter(&self, x: Option) -> Result, Vec> { self.validate(x).map(|_| self.filter(x)) } diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 382c13c..0f92055 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -313,8 +313,6 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, } } - // @todo consolidate these (`validate_and_filter*`), into just `filter*` ( - // since we really don't want to use filtered values without them being valid/etc.) fn validate_and_filter(&self, x: Option<&'b str>) -> Result>, Vec> { self.validate(x).map(|_| self.filter(x.map(Cow::Borrowed))) } From 60f489b6c27ad098bc7340c2066122890a321863 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Fri, 26 Jan 2024 11:10:28 -0400 Subject: [PATCH 25/38] inputfilter - Renamed 'validate1' methods to 'validate_detailed', in filter structs, and progress on changing 'validate' methods to return 'Result<(), Vec>', and 'validate*_detailed' methods to return 'Result<(), Vec>'. Additional: - Progress on naming conventaions cleanup, and normalizing API. --- inputfilter/src/scalar_input.rs | 48 +++++++++++---------- inputfilter/src/string_input.rs | 74 ++++++++++++++++----------------- inputfilter/src/types.rs | 8 ++-- 3 files changed, 66 insertions(+), 64 deletions(-) diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index f884cd5..59d2168 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -194,7 +194,7 @@ impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for ScalarInput<'a, T> where T: ScalarValue, { - /// Validates given value against contained constraints. + /// Validates given value against contained constraints and returns a result of unit and/or a Vec of violation tuples. /// /// ```rust /// use walrs_inputfilter::{ @@ -205,7 +205,7 @@ where /// use walrs_inputfilter::equal::not_equal_msg; /// /// ``` - fn validate(&self, value: Option) -> Result<(), Vec> { + fn validate_detailed(&self, value: Option) -> Result<(), Vec> { match value { None => { if self.required { @@ -235,14 +235,17 @@ where } } - fn validate1(&self, value: Option) -> Result<(), Vec> { - match self.validate(value) { + /// Validates given value against contained constraints, and returns a result of unit, and/or, a Vec of + /// Violation messages. + fn validate(&self, value: Option) -> Result<(), Vec> { + match self.validate_detailed(value) { // If errors, extract messages and return them Err(messages) => Err(messages.into_iter().map(|(_, message)| message).collect()), Ok(_) => Ok(()), } } + /// Filters value against contained filters. fn filter(&self, value: Option) -> Option { let v = match value { None => self.default_value, @@ -255,16 +258,19 @@ where } } - fn validate_and_filter(&self, x: Option) -> Result, Vec> { - self.validate(x).map(|_| self.filter(x)) - } - - fn validate_and_filter1(&self, x: Option) -> Result, Vec> { - match self.validate_and_filter(x) { + /// Validates, and filters, given value against contained rules, validators, and filters, respectively. + fn validate_and_filter(&self, x: Option) -> Result, Vec> { + match self.validate_and_filter_detailed(x) { Err(messages) => Err(messages.into_iter().map(|(_, message)| message).collect()), Ok(filtered) => Ok(filtered), } } + + /// Validates, and filters, given value against contained rules, validators, and filters, respectively and + /// returns a result of filtered value or a Vec of Violation tuples. + fn validate_and_filter_detailed(&self, x: Option) -> Result, Vec> { + self.validate_detailed(x).map(|_| self.filter(x)) + } } impl<'a, T: ScalarValue> WithName<'a> for ScalarInput<'a, T> { @@ -312,7 +318,7 @@ mod test { use crate::{ViolationEnum, InputConstraints}; #[test] - fn test_validate() { + fn test_validate_detailed() { // Ensure each logic case in method is sound, and that method is callable for each scalar type: // 1) Test method logic // ---- @@ -403,7 +409,7 @@ mod test { for (i, (test_name, input, subj, expected)) in test_cases.into_iter().enumerate() { println!("Case {}: {}", i + 1, test_name); - assert_eq!(input.validate(subj), expected); + assert_eq!(input.validate_detailed(subj), expected); } // Test basic usage with other types @@ -421,12 +427,12 @@ mod test { .build() .unwrap(); - assert_eq!(f64_input_required.validate(None), Err(vec![ + assert_eq!(f64_input_required.validate_detailed(None), Err(vec![ (ViolationEnum::ValueMissing, value_missing_msg(&f64_input_required)), ])); - assert_eq!(f64_input_required.validate(Some(2.0)), Ok(())); - assert_eq!(f64_input_required.validate(Some(11.0)), Err(vec![ + assert_eq!(f64_input_required.validate_detailed(Some(2.0)), Ok(())); + assert_eq!(f64_input_required.validate_detailed(Some(11.0)), Err(vec![ (ViolationEnum::RangeOverflow, range_overflow_msg(&f64_input_required, 11.0)), ])); @@ -438,10 +444,10 @@ mod test { .build() .unwrap(); - assert_eq!(char_input.validate(None), Ok(())); - assert_eq!(char_input.validate(Some('a')), Ok(())); - assert_eq!(char_input.validate(Some('f')), Ok(())); - assert_eq!(char_input.validate(Some('g')), Err(vec![ + assert_eq!(char_input.validate_detailed(None), Ok(())); + assert_eq!(char_input.validate_detailed(Some('a')), Ok(())); + assert_eq!(char_input.validate_detailed(Some('f')), Ok(())); + assert_eq!(char_input.validate_detailed(Some('g')), Err(vec![ (ViolationEnum::RangeOverflow, "`g` is greater than maximum `f`.".to_string()), ])); @@ -452,8 +458,8 @@ mod test { .build() .unwrap(); - assert_eq!(char_input_equal.validate(None), Ok(())); - assert_eq!(char_input_equal.validate(Some('b')), Err(vec![ + assert_eq!(char_input_equal.validate_detailed(None), Ok(())); + assert_eq!(char_input_equal.validate_detailed(Some('b')), Err(vec![ (ViolationEnum::NotEqual, "`b` is not equal to `a`.".to_string()), ])); diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 0f92055..f234480 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -108,7 +108,7 @@ impl<'a, 'b> StringInput<'a, 'b> { } } - fn _validate_against_self(&self, value: &'b str) -> ValidationResult { + fn _validate_against_own_constraints(&self, value: &'b str) -> ValidationResult { let mut errs = vec![]; if let Some(min_length) = self.min_length { @@ -197,12 +197,8 @@ impl<'a, 'b> WithName<'a> for StringInput<'a, 'b> { } impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, 'b> { - /// Validates value against any own `validate_custom` implementation and any set validators - - /// E.g., runs `validate_custom(...)`, then, if it is `Ok`, `validate_against_validators(...)` method. - /// - /// Additionally, note, `break_on_failure` is only guaranteed to be respected for the - /// the validators list, and input filters defined in the library; E.g., It is not guaranteed for - /// `validate_custom()` call in external libraries (e.g., this is left to implementing struct authors). + /// Validates value against contained constraints and validators, and returns a result of unit and/or a Vec of + /// Violation tuples. /// /// ```rust /// use walrs_inputfilter::*; @@ -230,13 +226,13 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// /// let too_long_str = &"ab".repeat(201); /// - /// assert_eq!(str_input.validate(None), Err(vec![ (ValueMissing, "Value missing".to_string()) ])); - /// assert_eq!(str_input.validate(Some(&"ab")), Err(vec![ (TooShort, "Too short".to_string()) ])); - /// assert_eq!(str_input.validate(Some(&too_long_str)), Err(vec![ (TooLong, too_long_msg(&str_input, Some(&too_long_str))) ])); - /// assert_eq!(str_input.validate(Some(&"abc")), Err(vec![ (TypeMismatch, "Invalid email".to_string()) ])); - /// assert_eq!(str_input.validate(Some(&"abc@def")), Ok(())); + /// assert_eq!(str_input.validate_detailed(None), Err(vec![ (ValueMissing, "Value missing".to_string()) ])); + /// assert_eq!(str_input.validate_detailed(Some(&"ab")), Err(vec![ (TooShort, "Too short".to_string()) ])); + /// assert_eq!(str_input.validate_detailed(Some(&too_long_str)), Err(vec![ (TooLong, too_long_msg(&str_input, Some(&too_long_str))) ])); + /// assert_eq!(str_input.validate_detailed(Some(&"abc")), Err(vec![ (TypeMismatch, "Invalid email".to_string()) ])); + /// assert_eq!(str_input.validate_detailed(Some(&"abc@def")), Ok(())); /// ``` - fn validate(&self, value: Option<&'b str>) -> Result<(), Vec> { + fn validate_detailed(&self, value: Option<&'b str>) -> Result<(), Vec> { match value { None => { if self.required { @@ -249,7 +245,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, } } // Else if value is populated validate it - Some(v) => match self._validate_against_self(v) { + Some(v) => match self._validate_against_own_constraints(v) { Ok(_) => self._validate_against_validators(v), Err(messages1) => if self.break_on_failure { Err(messages1) @@ -267,8 +263,8 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, } } - /// Special case of `validate` where the error type enums are ignored (in `Err(...)`) result, - /// and only the error messages are returned. + /// Special case of `validate_detailed` where the error type enums are ignored [in `Err(...)`] result, + /// and only the violation messages are returned. /// /// ```rust /// use walrs_inputfilter::*; @@ -289,11 +285,11 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// .unwrap() /// ; /// - /// assert_eq!(input.validate1(Some(&"ab")), Err(vec!["Too short".to_string()])); - /// assert_eq!(input.validate1(None), Err(vec!["Value missing".to_string()])); + /// assert_eq!(input.validate(Some(&"ab")), Err(vec!["Too short".to_string()])); + /// assert_eq!(input.validate(None), Err(vec!["Value missing".to_string()])); /// ``` - fn validate1(&self, value: Option<&'b str>) -> Result<(), Vec> { - match self.validate(value) { + fn validate(&self, value: Option<&'b str>) -> Result<(), Vec> { + match self.validate_detailed(value) { // If errors, extract messages and return them Err(messages) => Err(messages.into_iter().map(|(_, message)| message).collect()), @@ -313,8 +309,8 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, } } - fn validate_and_filter(&self, x: Option<&'b str>) -> Result>, Vec> { - self.validate(x).map(|_| self.filter(x.map(Cow::Borrowed))) + fn validate_and_filter_detailed(&self, x: Option<&'b str>) -> Result>, Vec> { + self.validate_detailed(x).map(|_| self.filter(x.map(Cow::Borrowed))) } /// Special case of `validate_and_filter` where the error type enums are ignored (in `Err(...)`) result, @@ -343,12 +339,12 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// .unwrap() /// ; /// - /// assert_eq!(input.validate_and_filter1(Some(&"ab")), Err(vec!["Too short".to_string()])); - /// assert_eq!(input.validate_and_filter1(Some(&"Abba")), Ok(Some("Abba".to_lowercase().into()))); - /// assert_eq!(input.validate_and_filter1(None), Err(vec!["Value missing".to_string()])); + /// assert_eq!(input.validate_and_filter(Some(&"ab")), Err(vec!["Too short".to_string()])); + /// assert_eq!(input.validate_and_filter(Some(&"Abba")), Ok(Some("Abba".to_lowercase().into()))); + /// assert_eq!(input.validate_and_filter(None), Err(vec!["Value missing".to_string()])); /// ``` - fn validate_and_filter1(&self, x: Option<&'b str>) -> Result>, Vec> { - match self.validate_and_filter(x) { + fn validate_and_filter(&self, x: Option<&'b str>) -> Result>, Vec> { + match self.validate_and_filter_detailed(x) { Err(messages) => Err(messages.into_iter().map(|(_, message)| message).collect()), Ok(filtered) => Ok(filtered), @@ -486,7 +482,7 @@ mod test { // Mismatch check let value = "1000-99-999"; - match yyyy_mm_dd_input.validate(Some(value)) { + match yyyy_mm_dd_input.validate_detailed(Some(value)) { Ok(_) => panic!("Expected Err(...); Received Ok(())"), Err(tuples) => { assert_eq!(tuples[0].0, PatternMismatch); @@ -495,19 +491,19 @@ mod test { } // Valid check - if let Err(errs) = yyyy_mm_dd_input.validate(None) { + if let Err(errs) = yyyy_mm_dd_input.validate_detailed(None) { panic!("Expected Ok(()); Received Err({:#?})", &errs); } // Valid check 2 let value = "1000-99-99"; - if let Err(errs) = yyyy_mm_dd_input.validate(Some(value)) { + if let Err(errs) = yyyy_mm_dd_input.validate_detailed(Some(value)) { panic!("Expected Ok(()); Received Err({:#?})", &errs); } // Valid check let value = "1000-99-99"; - if let Err(errs) = yyyy_mm_dd_input2.validate(Some(value)) { + if let Err(errs) = yyyy_mm_dd_input2.validate_detailed(Some(value)) { panic!("Expected Ok(()); Received Err({:#?})", &errs); } @@ -532,7 +528,7 @@ mod test { let handle = thread::spawn( - move || match less_than_input_instance.validate(Some("2023-12-31")) { + move || match less_than_input_instance.validate_detailed(Some("2023-12-31")) { Err(x) => { assert_eq!(x[0].1.as_str(), less_than_1990_msg("2023-12-31")); } @@ -540,7 +536,7 @@ mod test { }, ); - let handle2 = thread::spawn(move || match str_input_instance.validate(Some("")) { + let handle2 = thread::spawn(move || match str_input_instance.validate_detailed(Some("")) { Err(x) => { assert_eq!( x[0].1.as_str(), @@ -601,7 +597,7 @@ mod test { thread::scope(|scope| { scope.spawn( - || match less_than_input_instance.validate(Some("2023-12-31")) { + || match less_than_input_instance.validate_detailed(Some("2023-12-31")) { Err(x) => { assert_eq!(x[0].1.as_str(), &less_than_1990_msg("2023-12-31")); } @@ -610,7 +606,7 @@ mod test { ); scope.spawn( - || match less_than_input_instance.validate_and_filter(Some("1989-01-01")) { + || match less_than_input_instance.validate_and_filter_detailed(Some("1989-01-01")) { Err(err) => panic!( "Expected `Ok(Some({:#?})`; Received `Err({:#?})`", Cow::::Owned("1989-01-31".to_string()), @@ -621,7 +617,7 @@ mod test { }, ); - scope.spawn(|| match ymd_check_input_instance.validate(Some("")) { + scope.spawn(|| match ymd_check_input_instance.validate_detailed(Some("")) { Err(x) => { assert_eq!(x[0].1.as_str(), ymd_mismatch_msg("", ymd_rx.as_str())); } @@ -639,7 +635,7 @@ mod test { } #[test] - fn test_validate_and_filter() { + fn test_validate_and_filter_detailed() { let input = StringInputBuilder::default() .name("hello") .required(true) @@ -649,11 +645,11 @@ mod test { .unwrap(); assert_eq!( - input.validate_and_filter(Some("2023-12-31")), + input.validate_and_filter_detailed(Some("2023-12-31")), Err(vec![(RangeOverflow, less_than_1990_msg("2023-12-31"))]) ); assert_eq!( - input.validate_and_filter(Some("1989-01-01")), + input.validate_and_filter_detailed(Some("1989-01-01")), Ok(Some(Cow::Owned("1989-01-31".to_string()))) ); } diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index 44b408c..274ddd4 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -112,15 +112,15 @@ pub type ValueMissingCallback = dyn Fn(&dyn WithName) -> ViolationMessage + Send pub trait InputConstraints<'a, 'b, T: 'b, FT: 'b>: Display + Debug + WithName<'a> where T: InputValue { - fn validate(&self, value: Option) -> Result<(), Vec>; + fn validate(&self, value: Option) -> Result<(), Vec>; - fn validate1(&self, value: Option) -> Result<(), Vec>; + fn validate_detailed(&self, value: Option) -> Result<(), Vec>; fn filter(&self, value: Option) -> Option; - fn validate_and_filter(&self, x: Option) -> Result, Vec>; + fn validate_and_filter(&self, value: Option) -> Result, Vec>; - fn validate_and_filter1(&self, x: Option) -> Result, Vec>; + fn validate_and_filter_detailed(&self, value: Option) -> Result, Vec>; } pub trait ToAttributesList { From 0e6edefc9513233509302a8414787f9268539b7c Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Sat, 27 Jan 2024 10:08:25 -0400 Subject: [PATCH 26/38] inputfilter - Removed the use of 'default_value' prop.. --- inputfilter/src/scalar_input.rs | 17 ++++------------- inputfilter/src/string_input.rs | 17 ++++------------- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index 59d2168..a1b7827 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -52,9 +52,6 @@ pub struct ScalarInput<'a, T: ScalarValue> { #[builder(default = "false")] pub required: bool, - #[builder(default = "None")] - pub default_value: Option, - #[builder(default = "None")] pub validators: Option>>, @@ -87,7 +84,6 @@ where max: None, equal: None, required: false, - default_value: None, validators: None, filters: None, range_underflow: &(range_underflow_msg), @@ -247,14 +243,9 @@ where /// Filters value against contained filters. fn filter(&self, value: Option) -> Option { - let v = match value { - None => self.default_value, - Some(x) => Some(x), - }; - match self.filters.as_deref() { - None => v, - Some(fs) => fs.iter().fold(v, |agg, f| f(agg)), + None => value, + Some(fs) => fs.iter().fold(value, |agg, f| f(agg)), } } @@ -414,7 +405,7 @@ mod test { // Test basic usage with other types // ---- - // Validates `f64`, and `f32` types + // Validates `f64`, and `f32` usage let f64_input_required = ScalarInputBuilder::::default() .required(true) .min(1.0) @@ -437,7 +428,7 @@ mod test { range_overflow_msg(&f64_input_required, 11.0)), ])); - // Test `char` type usage + // Test `char` usage let char_input = ScalarInputBuilder::::default() .min('a') .max('f') diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index f234480..a824a08 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -62,9 +62,6 @@ pub struct StringInput<'a, 'b> { #[builder(default = "false")] pub required: bool, - #[builder(default = "None")] - pub default_value: Option, - #[builder(default = "None")] pub validators: Option>>, @@ -97,7 +94,6 @@ impl<'a, 'b> StringInput<'a, 'b> { pattern: None, equal: None, required: false, - default_value: None, validators: None, filters: None, too_short: &(too_long_msg), @@ -263,8 +259,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, } } - /// Special case of `validate_detailed` where the error type enums are ignored [in `Err(...)`] result, - /// and only the violation messages are returned. + /// Same as `validate_detailed` only the violation messages are returned. /// /// ```rust /// use walrs_inputfilter::*; @@ -298,14 +293,10 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, } fn filter(&self, value: Option>) -> Option> { - let v = match value { - None => self.default_value.clone().map(|x| x.into()), - Some(x) => Some(x) - }; - match self.filters.as_deref() { - None => v, - Some(fs) => fs.iter().fold(v, |agg, f| f(agg)), + None => value, + Some(fs) => + fs.iter().fold(value, |agg, f| f(agg)), } } From f7e101faeb82ad7b2a784c7eb98ff1d133eed2d8 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Sat, 27 Jan 2024 10:21:18 -0400 Subject: [PATCH 27/38] inputfilter - Removed the use of 'equal' prop.. --- inputfilter/src/scalar_input.rs | 43 --------------------------------- inputfilter/src/string_input.rs | 26 -------------------- 2 files changed, 69 deletions(-) diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index a1b7827..4fcc1a2 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -23,14 +23,6 @@ pub fn range_overflow_msg(rules: &ScalarInput, x: T) -> Strin ) } -pub fn scalar_not_equal_msg(rules: &ScalarInput, x: T) -> String { - format!( - "`{:}` is not equal to `{:}`.", - x, - &rules.equal.unwrap() - ) -} - #[derive(Builder, Clone)] #[builder(setter(strip_option))] pub struct ScalarInput<'a, T: ScalarValue> { @@ -46,9 +38,6 @@ pub struct ScalarInput<'a, T: ScalarValue> { #[builder(default = "None")] pub max: Option, - #[builder(default = "None")] - pub equal: Option, - #[builder(default = "false")] pub required: bool, @@ -64,9 +53,6 @@ pub struct ScalarInput<'a, T: ScalarValue> { #[builder(default = "&range_overflow_msg")] pub range_overflow: &'a (dyn Fn(&ScalarInput<'a, T>, T) -> String + Send + Sync), - #[builder(default = "&scalar_not_equal_msg")] - pub not_equal: &'a (dyn Fn(&ScalarInput<'a, T>, T) -> String + Send + Sync), - #[builder(default = "&value_missing_msg")] pub value_missing: &'a ValueMissingCallback, } @@ -82,13 +68,11 @@ where name, min: None, max: None, - equal: None, required: false, validators: None, filters: None, range_underflow: &(range_underflow_msg), range_overflow: &(range_overflow_msg), - not_equal: &(scalar_not_equal_msg), value_missing: &value_missing_msg, } } @@ -124,20 +108,6 @@ where } } - // Test equality - if let Some(equal) = self.equal { - if value != equal { - errs.push(( - ViolationEnum::NotEqual, - (self.not_equal)(self, value), - )); - - if self.break_on_failure { - return Err(errs); - } - } - } - if errs.is_empty() { Ok(()) } else { @@ -198,7 +168,6 @@ where /// range_underflow_msg, range_overflow_msg, /// ScalarValue /// }; - /// use walrs_inputfilter::equal::not_equal_msg; /// /// ``` fn validate_detailed(&self, value: Option) -> Result<(), Vec> { @@ -442,17 +411,5 @@ mod test { (ViolationEnum::RangeOverflow, "`g` is greater than maximum `f`.".to_string()), ])); - - // Test `equal` field usage - let char_input_equal = ScalarInputBuilder::::default() - .equal('a') - .build() - .unwrap(); - - assert_eq!(char_input_equal.validate_detailed(None), Ok(())); - assert_eq!(char_input_equal.validate_detailed(Some('b')), Err(vec![ - (ViolationEnum::NotEqual, - "`b` is not equal to `a`.".to_string()), - ])); } } diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index a824a08..24f3611 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -31,13 +31,6 @@ pub fn too_long_msg(rules: &StringInput, xs: Option<&str>) -> String { ) } -pub fn str_not_equal_msg(rules: &StringInput, _: Option<&str>) -> String { - format!( - "Value is not equal to {}.", - &rules.equal.unwrap_or("") - ) -} - #[derive(Builder, Clone)] #[builder(pattern = "owned", setter(strip_option))] pub struct StringInput<'a, 'b> { @@ -56,9 +49,6 @@ pub struct StringInput<'a, 'b> { #[builder(default = "None")] pub pattern: Option, - #[builder(default = "None")] - pub equal: Option<&'b str>, - #[builder(default = "false")] pub required: bool, @@ -77,9 +67,6 @@ pub struct StringInput<'a, 'b> { #[builder(default = "&pattern_mismatch_msg")] pub pattern_mismatch: &'a StrMissingViolationCallback, - #[builder(default = "&str_not_equal_msg")] - pub not_equal: &'a StrMissingViolationCallback, - #[builder(default = "&value_missing_msg")] pub value_missing: &'a ValueMissingCallback, } @@ -92,14 +79,12 @@ impl<'a, 'b> StringInput<'a, 'b> { min_length: None, max_length: None, pattern: None, - equal: None, required: false, validators: None, filters: None, too_short: &(too_long_msg), too_long: &(too_long_msg), pattern_mismatch: &(pattern_mismatch_msg), - not_equal: &(str_not_equal_msg), value_missing: &value_missing_msg, } } @@ -141,17 +126,6 @@ impl<'a, 'b> StringInput<'a, 'b> { } } - if let Some(equal) = &self.equal { - if value != *equal { - errs.push(( - ViolationEnum::NotEqual, - (self.not_equal)(self, Some(value)), - )); - - if self.break_on_failure { return Err(errs); } - } - } - if errs.is_empty() { Ok(()) } else { Err(errs) } } From e087ff84dd5e2f7fc1845d2bf5096106ed706118 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Sat, 27 Jan 2024 10:53:56 -0400 Subject: [PATCH 28/38] inputfilter - Removed the use of the 'name' prop., and the 'WithName' trait - These are now delegated to external implementation/compositions. --- _recycler/form/src/traits.rs | 4 ++++ inputfilter/src/lib.rs | 2 +- inputfilter/src/scalar_input.rs | 24 ++++++------------------ inputfilter/src/string_input.rs | 32 +++++++++----------------------- inputfilter/src/types.rs | 8 ++------ 5 files changed, 22 insertions(+), 48 deletions(-) diff --git a/_recycler/form/src/traits.rs b/_recycler/form/src/traits.rs index d7a3725..1e8ed94 100644 --- a/_recycler/form/src/traits.rs +++ b/_recycler/form/src/traits.rs @@ -83,3 +83,7 @@ pub trait FormControl<'a, 'b, Value: 'b, Constraints: 'a> }) } } + +pub trait WithName<'a> { + fn get_name(&self) -> Option>; +} diff --git a/inputfilter/src/lib.rs b/inputfilter/src/lib.rs index d44bf6a..dc2aec6 100644 --- a/inputfilter/src/lib.rs +++ b/inputfilter/src/lib.rs @@ -17,6 +17,6 @@ pub use filter::*; pub use scalar_input::*; pub use string_input::*; -pub fn value_missing_msg(_: &dyn WithName) -> String { +pub fn value_missing_msg() -> String { "Value missing".to_string() } diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index 4fcc1a2..3413b68 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -4,7 +4,6 @@ use std::fmt::{Debug, Display, Formatter}; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; use crate::{ value_missing_msg, ViolationEnum, ScalarValue, ViolationTuple, ValueMissingCallback, - WithName, }; pub fn range_underflow_msg(rules: &ScalarInput, x: T) -> String { @@ -29,9 +28,6 @@ pub struct ScalarInput<'a, T: ScalarValue> { #[builder(default = "true")] pub break_on_failure: bool, - #[builder(setter(into), default = "None")] - pub name: Option<&'a str>, - #[builder(default = "None")] pub min: Option, @@ -62,10 +58,9 @@ where T: ScalarValue, { /// Returns a new instance containing defaults. - pub fn new(name: Option<&'a str>) -> Self { + pub fn new() -> Self { ScalarInput { break_on_failure: false, - name, min: None, max: None, required: false, @@ -176,7 +171,7 @@ where if self.required { Err(vec![( ViolationEnum::ValueMissing, - (self.value_missing)(self), + (self.value_missing)(), )]) } else { Ok(()) @@ -233,15 +228,9 @@ where } } -impl<'a, T: ScalarValue> WithName<'a> for ScalarInput<'a, T> { - fn get_name(&self) -> Option> { - self.name.map(Cow::Borrowed) - } -} - impl Default for ScalarInput<'_, T> { fn default() -> Self { - Self::new(None) + Self::new() } } @@ -249,8 +238,7 @@ impl Display for ScalarInput<'_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "ScalarInput {{ name: {}, required: {}, validators: {}, filters: {} }}", - self.name.unwrap_or("None"), + "ScalarInput {{ required: {}, validators: {}, filters: {} }}", self.required, self .validators @@ -343,7 +331,7 @@ mod test { // ---- ("1-10, Even, required, no value", &usize_required, None, Err(vec![ (ViolationEnum::ValueMissing, - value_missing_msg(&usize_required)), + value_missing_msg()), ])), ("1-10, Even, required, with valid value", &usize_required, Some(2), Ok(())), ("1-10, Even, required, with valid value (2)", &usize_required, Some(10), Ok(())), @@ -389,7 +377,7 @@ mod test { assert_eq!(f64_input_required.validate_detailed(None), Err(vec![ (ViolationEnum::ValueMissing, - value_missing_msg(&f64_input_required)), + value_missing_msg()), ])); assert_eq!(f64_input_required.validate_detailed(Some(2.0)), Ok(())); assert_eq!(f64_input_required.validate_detailed(Some(11.0)), Err(vec![ diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 24f3611..1e6c581 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Display, Formatter}; use regex::Regex; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; -use crate::{ViolationEnum, ViolationTuple, ValidationResult, value_missing_msg, ValueMissingCallback, WithName}; +use crate::{ViolationEnum, ViolationTuple, ValidationResult, value_missing_msg, ValueMissingCallback}; pub type StrMissingViolationCallback = dyn Fn(&StringInput, Option<&str>) -> ViolationMessage + Send + Sync; @@ -37,9 +37,6 @@ pub struct StringInput<'a, 'b> { #[builder(default = "true")] pub break_on_failure: bool, - #[builder(setter(into), default = "None")] - pub name: Option<&'a str>, - #[builder(default = "None")] pub min_length: Option, @@ -72,10 +69,9 @@ pub struct StringInput<'a, 'b> { } impl<'a, 'b> StringInput<'a, 'b> { - pub fn new(name: Option<&'a str>) -> Self { + pub fn new() -> Self { StringInput { break_on_failure: false, - name, min_length: None, max_length: None, pattern: None, @@ -160,12 +156,6 @@ impl<'a, 'b> StringInput<'a, 'b> { } } -impl<'a, 'b> WithName<'a> for StringInput<'a, 'b> { - fn get_name(&self) -> Option> { - self.name.map(Cow::Borrowed) - } -} - impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, 'b> { /// Validates value against contained constraints and validators, and returns a result of unit and/or a Vec of /// Violation tuples. @@ -180,7 +170,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// /// let str_input = StringInputBuilder::default() /// .required(true) - /// .value_missing(&|_| "Value missing".to_string()) + /// .value_missing(&|| "Value missing".to_string()) /// .min_length(3usize) /// .too_short(&|_, _| "Too short".to_string()) /// .max_length(200usize) // Default violation message callback used here. @@ -208,7 +198,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, if self.required { Err(vec![( ViolationEnum::ValueMissing, - (self.value_missing)(self), + (self.value_missing)(), )]) } else { Ok(()) @@ -240,7 +230,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// /// let input = StringInputBuilder::default() /// .required(true) - /// .value_missing(&|_| "Value missing".to_string()) + /// .value_missing(&|| "Value missing".to_string()) /// .validators(vec![&|x: &str| { /// if x.len() < 3 { /// return Err(vec![( @@ -287,7 +277,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// /// let input = StringInputBuilder::default() /// .required(true) - /// .value_missing(&|_| "Value missing".to_string()) + /// .value_missing(&|| "Value missing".to_string()) /// .validators(vec![&|x: &str| { /// if x.len() < 3 { /// return Err(vec![( @@ -319,7 +309,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, impl Default for StringInput<'_, '_> { fn default() -> Self { - Self::new(None) + Self::new() } } @@ -327,8 +317,7 @@ impl Display for StringInput<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "StrInput {{ name: {}, required: {}, validators: {}, filters: {} }}", - self.name.unwrap_or("None"), + "StrInput {{ required: {}, validators: {}, filters: {} }}", self.required, self .validators @@ -602,7 +591,6 @@ mod test { #[test] fn test_validate_and_filter_detailed() { let input = StringInputBuilder::default() - .name("hello") .required(true) .validators(vec![&less_than_1990]) .filters(vec![&to_last_date_of_month]) @@ -633,7 +621,6 @@ mod test { }; let _input = StringInputBuilder::default() - .name("hello") .validators(vec![&callback1]) .build() .unwrap(); @@ -642,14 +629,13 @@ mod test { #[test] fn test_display() { let input = StringInputBuilder::default() - .name("hello") .validators(vec![&less_than_1990]) .build() .unwrap(); assert_eq!( input.to_string(), - "StrInput { name: hello, required: false, validators: Some([Validator; 1]), filters: None }" + "StrInput { required: false, validators: Some([Validator; 1]), filters: None }" ); } } diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index 274ddd4..0ca0de4 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -95,10 +95,6 @@ pub type Filter = dyn Fn(T) -> T + Send + Sync; pub type Validator = dyn Fn(T) -> ValidationResult + Send + Sync; -pub trait WithName<'a> { - fn get_name(&self) -> Option>; -} - pub trait ValidateValue { fn validate(&self, value: T) -> ValidationResult; } @@ -107,9 +103,9 @@ pub trait FilterValue { fn filter(&self, value: T) -> T; } -pub type ValueMissingCallback = dyn Fn(&dyn WithName) -> ViolationMessage + Send + Sync; +pub type ValueMissingCallback = dyn Fn() -> ViolationMessage + Send + Sync; -pub trait InputConstraints<'a, 'b, T: 'b, FT: 'b>: Display + Debug + WithName<'a> +pub trait InputConstraints<'a, 'b, T: 'b, FT: 'b>: Display + Debug where T: InputValue { fn validate(&self, value: Option) -> Result<(), Vec>; From bbca8088c3aaca5d68b86b8a635cb355b0c7c78d Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Sun, 28 Jan 2024 09:50:01 -0400 Subject: [PATCH 29/38] repo, inputfilter - Updated inputfilter Cargo.toml, inputfilter constraint struct violation message getter names, placement of some traits, and updated inputfilter 'derive_builder' version. --- Cargo.toml | 1 + inputfilter/Cargo.toml | 3 ++- inputfilter/src/filter/mod.rs | 1 + inputfilter/src/filter/strip_tags.rs | 8 +++++++ inputfilter/src/filter/traits.rs | 5 ++++ inputfilter/src/scalar_input.rs | 18 +++++++------- inputfilter/src/string_input.rs | 35 ++++++++++++++-------------- inputfilter/src/types.rs | 23 +++++++++++------- inputfilter/src/validator/equal.rs | 3 ++- inputfilter/src/validator/mod.rs | 1 + inputfilter/src/validator/number.rs | 3 ++- inputfilter/src/validator/pattern.rs | 5 ++-- inputfilter/src/validator/traits.rs | 5 ++++ 13 files changed, 70 insertions(+), 41 deletions(-) create mode 100644 inputfilter/src/filter/traits.rs create mode 100644 inputfilter/src/validator/traits.rs diff --git a/Cargo.toml b/Cargo.toml index 0dfbbba..2472712 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ name = "walrs" version = "0.1.0" edition = "2021" +rust-version = "1.77" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/inputfilter/Cargo.toml b/inputfilter/Cargo.toml index 4890a7a..5fa899f 100644 --- a/inputfilter/Cargo.toml +++ b/inputfilter/Cargo.toml @@ -4,11 +4,12 @@ version = "0.1.0" authors = ["Ely De La Cruz "] edition = "2021" include = ["src/**/*", "Cargo.toml"] +rust-version = "1.77" [dependencies] bytes = "1.4.0" regex = "1.3.1" -derive_builder = "0.12.0" +derive_builder = "0.13.0" serde = { version = "1.0.103", features = ["derive"] } serde_json = "1.0.82" ammonia = "3.3.0" diff --git a/inputfilter/src/filter/mod.rs b/inputfilter/src/filter/mod.rs index fedb2b8..9050d96 100644 --- a/inputfilter/src/filter/mod.rs +++ b/inputfilter/src/filter/mod.rs @@ -1,6 +1,7 @@ pub mod slug; pub mod strip_tags; pub mod xml_entities; +mod traits; pub use slug::*; pub use strip_tags::*; diff --git a/inputfilter/src/filter/strip_tags.rs b/inputfilter/src/filter/strip_tags.rs index 2e14fd2..c95c388 100644 --- a/inputfilter/src/filter/strip_tags.rs +++ b/inputfilter/src/filter/strip_tags.rs @@ -87,6 +87,14 @@ impl<'a> StripTags<'a> { /// ), /// "" /// ); + /// + /// // Can also be called as an function trait (has `FN*` traits implemented). + /// assert_eq!(filter( + /// "".into() + /// ), + /// "" + /// ); + /// /// ``` /// pub fn filter<'b>(&self, input: Cow<'b, str>) -> Cow<'b, str> { diff --git a/inputfilter/src/filter/traits.rs b/inputfilter/src/filter/traits.rs new file mode 100644 index 0000000..d8fa818 --- /dev/null +++ b/inputfilter/src/filter/traits.rs @@ -0,0 +1,5 @@ +use crate::InputValue; + +pub trait FilterValue { + fn filter(&self, value: T) -> T; +} diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/scalar_input.rs index 3413b68..ebaa6a0 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/scalar_input.rs @@ -44,13 +44,13 @@ pub struct ScalarInput<'a, T: ScalarValue> { pub filters: Option>>>, #[builder(default = "&range_underflow_msg")] - pub range_underflow: &'a (dyn Fn(&ScalarInput<'a, T>, T) -> String + Send + Sync), + pub range_underflow_msg: &'a (dyn Fn(&ScalarInput<'a, T>, T) -> String + Send + Sync), #[builder(default = "&range_overflow_msg")] - pub range_overflow: &'a (dyn Fn(&ScalarInput<'a, T>, T) -> String + Send + Sync), + pub range_overflow_msg: &'a (dyn Fn(&ScalarInput<'a, T>, T) -> String + Send + Sync), #[builder(default = "&value_missing_msg")] - pub value_missing: &'a ValueMissingCallback, + pub value_missing_msg: &'a ValueMissingCallback, } impl<'a, T> ScalarInput<'a, T> @@ -66,9 +66,9 @@ where required: false, validators: None, filters: None, - range_underflow: &(range_underflow_msg), - range_overflow: &(range_overflow_msg), - value_missing: &value_missing_msg, + range_underflow_msg: &(range_underflow_msg), + range_overflow_msg: &(range_overflow_msg), + value_missing_msg: &(value_missing_msg), } } @@ -80,7 +80,7 @@ where if value < min { errs.push(( ViolationEnum::RangeUnderflow, - (self.range_underflow)(self, value), + (self.range_underflow_msg)(self, value), )); if self.break_on_failure { @@ -94,7 +94,7 @@ where if value > max { errs.push(( ViolationEnum::RangeOverflow, - (self.range_overflow)(self, value), + (self.range_overflow_msg)(self, value), )); if self.break_on_failure { @@ -171,7 +171,7 @@ where if self.required { Err(vec![( ViolationEnum::ValueMissing, - (self.value_missing)(), + (self.value_missing_msg)(), )]) } else { Ok(()) diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/string_input.rs index 1e6c581..7d5bc71 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/string_input.rs @@ -5,7 +5,7 @@ use regex::Regex; use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; use crate::{ViolationEnum, ViolationTuple, ValidationResult, value_missing_msg, ValueMissingCallback}; -pub type StrMissingViolationCallback = dyn Fn(&StringInput, Option<&str>) -> ViolationMessage + Send + Sync; +pub type StringInputViolationCallback = dyn Fn(&StringInput, Option<&str>) -> ViolationMessage + Send + Sync; pub fn pattern_mismatch_msg(rules: &StringInput, xs: Option<&str>) -> String { format!( @@ -56,16 +56,16 @@ pub struct StringInput<'a, 'b> { pub filters: Option>>>>, #[builder(default = "&too_short_msg")] - pub too_short: &'a StrMissingViolationCallback, + pub too_short_msg: &'a StringInputViolationCallback, #[builder(default = "&too_long_msg")] - pub too_long: &'a StrMissingViolationCallback, + pub too_long_msg: &'a StringInputViolationCallback, #[builder(default = "&pattern_mismatch_msg")] - pub pattern_mismatch: &'a StrMissingViolationCallback, + pub pattern_mismatch_msg: &'a StringInputViolationCallback, #[builder(default = "&value_missing_msg")] - pub value_missing: &'a ValueMissingCallback, + pub value_missing_msg: &'a ValueMissingCallback, } impl<'a, 'b> StringInput<'a, 'b> { @@ -78,10 +78,10 @@ impl<'a, 'b> StringInput<'a, 'b> { required: false, validators: None, filters: None, - too_short: &(too_long_msg), - too_long: &(too_long_msg), - pattern_mismatch: &(pattern_mismatch_msg), - value_missing: &value_missing_msg, + too_short_msg: &(too_long_msg), + too_long_msg: &(too_long_msg), + pattern_mismatch_msg: &(pattern_mismatch_msg), + value_missing_msg: &value_missing_msg, } } @@ -92,7 +92,7 @@ impl<'a, 'b> StringInput<'a, 'b> { if value.len() < min_length { errs.push(( ViolationEnum::TooShort, - (self.too_short)(self, Some(value)), + (self.too_short_msg)(self, Some(value)), )); if self.break_on_failure { return Err(errs); } @@ -103,7 +103,7 @@ impl<'a, 'b> StringInput<'a, 'b> { if value.len() > max_length { errs.push(( ViolationEnum::TooLong, - (self.too_long)(self, Some(value)), + (self.too_long_msg)(self, Some(value)), )); if self.break_on_failure { return Err(errs); } @@ -114,8 +114,7 @@ impl<'a, 'b> StringInput<'a, 'b> { if !pattern.is_match(value) { errs.push(( ViolationEnum::PatternMismatch, - (self. - pattern_mismatch)(self, Some(value)), + (self.pattern_mismatch_msg)(self, Some(value)), )); if self.break_on_failure { return Err(errs); } @@ -170,9 +169,9 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// /// let str_input = StringInputBuilder::default() /// .required(true) - /// .value_missing(&|| "Value missing".to_string()) + /// .value_missing_msg(&|| "Value missing".to_string()) /// .min_length(3usize) - /// .too_short(&|_, _| "Too short".to_string()) + /// .too_short_msg(&|_, _| "Too short".to_string()) /// .max_length(200usize) // Default violation message callback used here. /// // Naive email pattern validator (naive for this example). /// .validators(vec![&|x: &str| { @@ -198,7 +197,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, if self.required { Err(vec![( ViolationEnum::ValueMissing, - (self.value_missing)(), + (self.value_missing_msg)(), )]) } else { Ok(()) @@ -230,7 +229,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// /// let input = StringInputBuilder::default() /// .required(true) - /// .value_missing(&|| "Value missing".to_string()) + /// .value_missing_msg(&|| "Value missing".to_string()) /// .validators(vec![&|x: &str| { /// if x.len() < 3 { /// return Err(vec![( @@ -277,7 +276,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// /// let input = StringInputBuilder::default() /// .required(true) - /// .value_missing(&|| "Value missing".to_string()) + /// .value_missing_msg(&|| "Value missing".to_string()) /// .validators(vec![&|x: &str| { /// if x.len() < 3 { /// return Err(vec![( diff --git a/inputfilter/src/types.rs b/inputfilter/src/types.rs index 0ca0de4..2d5f441 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/types.rs @@ -1,5 +1,4 @@ use std::ops::{Add, Div, Mul, Rem, Sub}; -use std::borrow::Cow; use std::fmt::{Debug, Display}; use serde::Serialize; @@ -71,6 +70,14 @@ impl NumberValue for usize {} impl NumberValue for f32 {} impl NumberValue for f64 {} +/// Violation Enum types represent the possible violation types that may be returned, along with error messages, +/// from any given "validation" operation. +/// +/// These additionally provide a runtime opportunity to override +/// returned violation message(s), via returned validation result `Err` tuples, and the ability to provide the +/// violation type from "constraint" structures that perform validation against their own constraint props.; E.g., +/// `StringInput` (etc.) with it's `pattern`, `min_length`, `max_length` props. etc. +/// #[derive(PartialEq, Debug, Clone, Copy)] pub enum ViolationEnum { CustomError, @@ -85,31 +92,29 @@ pub enum ViolationEnum { ValueMissing, } +/// A validation violation message. pub type ViolationMessage = String; +/// A validation violation tuple. pub type ViolationTuple = (ViolationEnum, ViolationMessage); +/// Returned from validators, and Input Constraint struct `*_detailed` validation methods. pub type ValidationResult = Result<(), Vec>; pub type Filter = dyn Fn(T) -> T + Send + Sync; pub type Validator = dyn Fn(T) -> ValidationResult + Send + Sync; -pub trait ValidateValue { - fn validate(&self, value: T) -> ValidationResult; -} - -pub trait FilterValue { - fn filter(&self, value: T) -> T; -} - +/// Violation message getter for `ValueMissing` Violation Enum type. pub type ValueMissingCallback = dyn Fn() -> ViolationMessage + Send + Sync; pub trait InputConstraints<'a, 'b, T: 'b, FT: 'b>: Display + Debug where T: InputValue { + // @todo - Move this to `ValidateValue` trait. fn validate(&self, value: Option) -> Result<(), Vec>; + // @todo - Move this to `ValidateValue` trait. fn validate_detailed(&self, value: Option) -> Result<(), Vec>; fn filter(&self, value: Option) -> Option; diff --git a/inputfilter/src/validator/equal.rs b/inputfilter/src/validator/equal.rs index 235c3ca..2146c01 100644 --- a/inputfilter/src/validator/equal.rs +++ b/inputfilter/src/validator/equal.rs @@ -1,7 +1,8 @@ use std::fmt::Display; use crate::ToAttributesList; +use crate::traits::ValidateValue; use crate::types::ViolationEnum; -use crate::types::{InputValue, ValidateValue, ValidationResult}; +use crate::types::{InputValue, ValidationResult}; #[derive(Builder, Clone)] pub struct EqualityValidator<'a, T> diff --git a/inputfilter/src/validator/mod.rs b/inputfilter/src/validator/mod.rs index 4f49079..19d7b66 100644 --- a/inputfilter/src/validator/mod.rs +++ b/inputfilter/src/validator/mod.rs @@ -1,3 +1,4 @@ pub mod pattern; pub mod number; pub mod equal; +pub mod traits; diff --git a/inputfilter/src/validator/number.rs b/inputfilter/src/validator/number.rs index dc43f8b..65a4c51 100644 --- a/inputfilter/src/validator/number.rs +++ b/inputfilter/src/validator/number.rs @@ -1,10 +1,11 @@ use std::fmt::{Display, Formatter}; use crate::ToAttributesList; +use crate::traits::ValidateValue; use crate::types::{ ViolationEnum, ViolationEnum::{ NotEqual, RangeOverflow, RangeUnderflow, StepMismatch, - }, NumberValue, ValidateValue, ValidationResult, + }, NumberValue, ValidationResult, }; use serde_json::value::to_value as to_json_value; diff --git a/inputfilter/src/validator/pattern.rs b/inputfilter/src/validator/pattern.rs index d820b85..1f7a908 100644 --- a/inputfilter/src/validator/pattern.rs +++ b/inputfilter/src/validator/pattern.rs @@ -2,9 +2,10 @@ use std::borrow::Cow; use std::fmt::Display; use regex::Regex; use crate::ToAttributesList; +use crate::traits::ValidateValue; use crate::types::ViolationEnum::PatternMismatch; -use crate::types::{ValidationResult, ValidateValue}; +use crate::types::{ValidationResult}; pub type PatternViolationCallback = dyn Fn(&PatternValidator, &str) -> String + Send + Sync; @@ -123,7 +124,7 @@ mod test { use std::borrow::Cow; use std::error::Error; use regex::Regex; - use crate::{ValidateValue}; + use crate::traits::{ValidateValue}; use crate::ViolationEnum::PatternMismatch; use super::*; diff --git a/inputfilter/src/validator/traits.rs b/inputfilter/src/validator/traits.rs new file mode 100644 index 0000000..0e7c21a --- /dev/null +++ b/inputfilter/src/validator/traits.rs @@ -0,0 +1,5 @@ +use crate::{InputValue, ValidationResult}; + +pub trait ValidateValue { + fn validate(&self, value: T) -> ValidationResult; +} From 3b43235058f7a4025285ff61927b10f7e616ab2b Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Sun, 28 Jan 2024 11:31:32 -0400 Subject: [PATCH 30/38] inputfilter - Moved input constraint modules to 'constraints' module, and individualized message handlers to allow proper exports of all crate members. --- inputfilter/src/constraints/mod.rs | 11 ++++++ .../src/{ => constraints}/scalar_input.rs | 3 +- .../src/{ => constraints}/string_input.rs | 10 +++--- inputfilter/src/constraints/traits.rs | 25 ++++++++++++++ inputfilter/src/filter/mod.rs | 2 +- inputfilter/src/lib.rs | 14 +++----- inputfilter/src/{types.rs => traits.rs} | 26 +------------- inputfilter/src/validator/equal.rs | 20 +++++------ inputfilter/src/validator/mod.rs | 5 +++ inputfilter/src/validator/number.rs | 34 +++++++++---------- inputfilter/src/validator/pattern.rs | 15 ++++---- src/lib.rs | 4 +-- 12 files changed, 90 insertions(+), 79 deletions(-) create mode 100644 inputfilter/src/constraints/mod.rs rename inputfilter/src/{ => constraints}/scalar_input.rs (99%) rename inputfilter/src/{ => constraints}/string_input.rs (98%) create mode 100644 inputfilter/src/constraints/traits.rs rename inputfilter/src/{types.rs => traits.rs} (76%) diff --git a/inputfilter/src/constraints/mod.rs b/inputfilter/src/constraints/mod.rs new file mode 100644 index 0000000..32bc2ab --- /dev/null +++ b/inputfilter/src/constraints/mod.rs @@ -0,0 +1,11 @@ +pub mod scalar_input; +pub mod string_input; +pub mod traits; + +pub use scalar_input::*; +pub use string_input::*; +pub use traits::*; + +pub fn value_missing_msg() -> String { + "Value missing".to_string() +} diff --git a/inputfilter/src/scalar_input.rs b/inputfilter/src/constraints/scalar_input.rs similarity index 99% rename from inputfilter/src/scalar_input.rs rename to inputfilter/src/constraints/scalar_input.rs index ebaa6a0..86115b7 100644 --- a/inputfilter/src/scalar_input.rs +++ b/inputfilter/src/constraints/scalar_input.rs @@ -1,9 +1,8 @@ -use std::borrow::Cow; use std::fmt::{Debug, Display, Formatter}; -use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; use crate::{ value_missing_msg, ViolationEnum, ScalarValue, ViolationTuple, ValueMissingCallback, + Filter, InputConstraints, Validator, ViolationMessage }; pub fn range_underflow_msg(rules: &ScalarInput, x: T) -> String { diff --git a/inputfilter/src/string_input.rs b/inputfilter/src/constraints/string_input.rs similarity index 98% rename from inputfilter/src/string_input.rs rename to inputfilter/src/constraints/string_input.rs index 7d5bc71..11a5e44 100644 --- a/inputfilter/src/string_input.rs +++ b/inputfilter/src/constraints/string_input.rs @@ -2,8 +2,10 @@ use std::borrow::Cow; use std::fmt::{Debug, Display, Formatter}; use regex::Regex; -use crate::types::{Filter, InputConstraints, Validator, ViolationMessage}; -use crate::{ViolationEnum, ViolationTuple, ValidationResult, value_missing_msg, ValueMissingCallback}; +use crate::{ + ViolationEnum, ViolationTuple, ValidationResult, value_missing_msg, ValueMissingCallback, + Filter, InputConstraints, Validator, ViolationMessage +}; pub type StringInputViolationCallback = dyn Fn(&StringInput, Option<&str>) -> ViolationMessage + Send + Sync; @@ -162,7 +164,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// ```rust /// use walrs_inputfilter::*; /// use walrs_inputfilter::pattern::PatternValidator; - /// use walrs_inputfilter::types::ViolationEnum::{ + /// use walrs_inputfilter::traits::ViolationEnum::{ /// ValueMissing, TooShort, TooLong, TypeMismatch, CustomError, /// RangeOverflow, RangeUnderflow, StepMismatch /// }; @@ -341,7 +343,7 @@ impl Debug for StringInput<'_, '_> { #[cfg(test)] mod test { use super::*; - use crate::types::{ + use crate::{ ViolationEnum, ViolationEnum::{PatternMismatch, RangeOverflow}, InputConstraints, ValidationResult, diff --git a/inputfilter/src/constraints/traits.rs b/inputfilter/src/constraints/traits.rs new file mode 100644 index 0000000..ceb4ad6 --- /dev/null +++ b/inputfilter/src/constraints/traits.rs @@ -0,0 +1,25 @@ +use std::fmt::{Debug, Display}; +use crate::{InputValue, ViolationMessage, ViolationTuple, ValidationResult}; + +pub type Filter = dyn Fn(T) -> T + Send + Sync; + +pub type Validator = dyn Fn(T) -> ValidationResult + Send + Sync; + +/// Violation message getter for `ValueMissing` Violation Enum type. +pub type ValueMissingCallback = dyn Fn() -> ViolationMessage + Send + Sync; + +pub trait InputConstraints<'a, 'b, T: 'b, FT: 'b>: Display + Debug + where T: InputValue { + + // @todo - Move this to `ValidateValue` trait. + fn validate(&self, value: Option) -> Result<(), Vec>; + + // @todo - Move this to `ValidateValue` trait. + fn validate_detailed(&self, value: Option) -> Result<(), Vec>; + + fn filter(&self, value: Option) -> Option; + + fn validate_and_filter(&self, value: Option) -> Result, Vec>; + + fn validate_and_filter_detailed(&self, value: Option) -> Result, Vec>; +} diff --git a/inputfilter/src/filter/mod.rs b/inputfilter/src/filter/mod.rs index 9050d96..6714432 100644 --- a/inputfilter/src/filter/mod.rs +++ b/inputfilter/src/filter/mod.rs @@ -1,7 +1,7 @@ pub mod slug; pub mod strip_tags; pub mod xml_entities; -mod traits; +pub mod traits; pub use slug::*; pub use strip_tags::*; diff --git a/inputfilter/src/lib.rs b/inputfilter/src/lib.rs index dc2aec6..d1e6122 100644 --- a/inputfilter/src/lib.rs +++ b/inputfilter/src/lib.rs @@ -5,18 +5,12 @@ #[macro_use] extern crate derive_builder; -pub mod types; +pub mod traits; pub mod validator; pub mod filter; -pub mod scalar_input; -pub mod string_input; +pub mod constraints; -pub use types::*; +pub use traits::*; pub use validator::*; pub use filter::*; -pub use scalar_input::*; -pub use string_input::*; - -pub fn value_missing_msg() -> String { - "Value missing".to_string() -} +pub use constraints::*; diff --git a/inputfilter/src/types.rs b/inputfilter/src/traits.rs similarity index 76% rename from inputfilter/src/types.rs rename to inputfilter/src/traits.rs index 2d5f441..3fa98b4 100644 --- a/inputfilter/src/types.rs +++ b/inputfilter/src/traits.rs @@ -25,7 +25,6 @@ impl InputValue for bool {} impl InputValue for char {} impl InputValue for str {} - impl InputValue for &str {} pub trait ScalarValue: InputValue + Default + Copy {} @@ -48,7 +47,6 @@ impl ScalarValue for f32 {} impl ScalarValue for f64 {} impl ScalarValue for bool {} - impl ScalarValue for char {} pub trait NumberValue: ScalarValue + Add + Sub + Mul + Div + Rem {} @@ -101,29 +99,7 @@ pub type ViolationTuple = (ViolationEnum, ViolationMessage); /// Returned from validators, and Input Constraint struct `*_detailed` validation methods. pub type ValidationResult = Result<(), Vec>; -pub type Filter = dyn Fn(T) -> T + Send + Sync; - -pub type Validator = dyn Fn(T) -> ValidationResult + Send + Sync; - -/// Violation message getter for `ValueMissing` Violation Enum type. -pub type ValueMissingCallback = dyn Fn() -> ViolationMessage + Send + Sync; - -pub trait InputConstraints<'a, 'b, T: 'b, FT: 'b>: Display + Debug - where T: InputValue { - - // @todo - Move this to `ValidateValue` trait. - fn validate(&self, value: Option) -> Result<(), Vec>; - - // @todo - Move this to `ValidateValue` trait. - fn validate_detailed(&self, value: Option) -> Result<(), Vec>; - - fn filter(&self, value: Option) -> Option; - - fn validate_and_filter(&self, value: Option) -> Result, Vec>; - - fn validate_and_filter_detailed(&self, value: Option) -> Result, Vec>; -} - +/// Allows serialization of properties that can be used for html form control contexts. pub trait ToAttributesList { fn to_attributes_list(&self) -> Option> { None diff --git a/inputfilter/src/validator/equal.rs b/inputfilter/src/validator/equal.rs index 2146c01..7a22b38 100644 --- a/inputfilter/src/validator/equal.rs +++ b/inputfilter/src/validator/equal.rs @@ -1,8 +1,8 @@ use std::fmt::Display; use crate::ToAttributesList; -use crate::traits::ValidateValue; -use crate::types::ViolationEnum; -use crate::types::{InputValue, ValidationResult}; +use crate::ValidateValue; +use crate::traits::ViolationEnum; +use crate::traits::{InputValue, ValidationResult}; #[derive(Builder, Clone)] pub struct EqualityValidator<'a, T> @@ -10,7 +10,7 @@ pub struct EqualityValidator<'a, T> { pub rhs_value: T, - #[builder(default = "¬_equal_msg")] + #[builder(default = "&equal_vldr_not_equal_msg")] pub not_equal_msg: &'a (dyn Fn(&EqualityValidator<'a, T>, T) -> String + Send + Sync), } @@ -63,7 +63,7 @@ impl Fn<(T, )> for EqualityValidator<'_, T> { } } -pub fn not_equal_msg(_: &EqualityValidator, value: T) -> String +pub fn equal_vldr_not_equal_msg(_: &EqualityValidator, value: T) -> String where T: InputValue, { format!("Value must equal {}", value) @@ -84,8 +84,8 @@ mod test { assert_eq!(instance.rhs_value, "foo"); assert_eq!((instance.not_equal_msg)(&instance, "foo"), - not_equal_msg(&instance, "foo"), - "Default 'not_equal_msg' fn should return expected value"); + equal_vldr_not_equal_msg(&instance, "foo"), + "Default 'equal_vldr_not_equal_msg' fn should return expected value"); Ok(()) } @@ -100,7 +100,7 @@ mod test { ] { let validator = EqualityValidatorBuilder::<&str>::default() .rhs_value(rhs_value) - .not_equal_msg(¬_equal_msg) + .not_equal_msg(&equal_vldr_not_equal_msg) .build()?; if should_be_ok { @@ -109,11 +109,11 @@ mod test { } else { assert_eq!( validator.validate(lhs_value), - Err(vec![(NotEqual, not_equal_msg(&validator, lhs_value))]) + Err(vec![(NotEqual, equal_vldr_not_equal_msg(&validator, lhs_value))]) ); assert_eq!( validator(lhs_value), - Err(vec![(NotEqual, not_equal_msg(&validator, lhs_value))]) + Err(vec![(NotEqual, equal_vldr_not_equal_msg(&validator, lhs_value))]) ); } } diff --git a/inputfilter/src/validator/mod.rs b/inputfilter/src/validator/mod.rs index 19d7b66..bb6af9a 100644 --- a/inputfilter/src/validator/mod.rs +++ b/inputfilter/src/validator/mod.rs @@ -2,3 +2,8 @@ pub mod pattern; pub mod number; pub mod equal; pub mod traits; + +pub use pattern::*; +pub use number::*; +pub use equal::*; +pub use traits::*; diff --git a/inputfilter/src/validator/number.rs b/inputfilter/src/validator/number.rs index 65a4c51..9817adb 100644 --- a/inputfilter/src/validator/number.rs +++ b/inputfilter/src/validator/number.rs @@ -1,7 +1,7 @@ use std::fmt::{Display, Formatter}; use crate::ToAttributesList; -use crate::traits::ValidateValue; -use crate::types::{ +use crate::ValidateValue; +use crate::traits::{ ViolationEnum, ViolationEnum::{ NotEqual, RangeOverflow, RangeUnderflow, StepMismatch, @@ -28,13 +28,13 @@ pub struct NumberValidator<'a, T: NumberValue> { #[builder(default = "None")] pub equal: Option, - #[builder(default = "&range_underflow_msg")] + #[builder(default = "&num_range_underflow_msg")] pub range_underflow: &'a (dyn Fn(&NumberValidator<'a, T>, T) -> String + Send + Sync), - #[builder(default = "&range_overflow_msg")] + #[builder(default = "&num_range_overflow_msg")] pub range_overflow: &'a (dyn Fn(&NumberValidator<'a, T>, T) -> String + Send + Sync), - #[builder(default = "&step_mismatch_msg")] + #[builder(default = "&num_step_mismatch_msg")] pub step_mismatch: &'a (dyn Fn(&NumberValidator<'a, T>, T) -> String + Send + Sync), #[builder(default = "&num_not_equal_msg")] @@ -95,9 +95,9 @@ where max: None, step: None, equal: None, - range_underflow: &range_underflow_msg, - range_overflow: &range_overflow_msg, - step_mismatch: &step_mismatch_msg, + range_underflow: &num_range_underflow_msg, + range_overflow: &num_range_overflow_msg, + step_mismatch: &num_step_mismatch_msg, not_equal: &num_not_equal_msg, } } @@ -206,7 +206,7 @@ impl Display for NumberValidator<'_, T> { } } -pub fn range_underflow_msg(rules: &NumberValidator, x: T) -> String +pub fn num_range_underflow_msg(rules: &NumberValidator, x: T) -> String where T: NumberValue, { @@ -217,7 +217,7 @@ where ) } -pub fn range_overflow_msg(rules: &NumberValidator, x: T) -> String +pub fn num_range_overflow_msg(rules: &NumberValidator, x: T) -> String where T: NumberValue, { @@ -228,7 +228,7 @@ where ) } -pub fn step_mismatch_msg( +pub fn num_step_mismatch_msg( rules: &NumberValidator, x: T, ) -> String { @@ -287,13 +287,13 @@ mod test { let test_value = 99; assert_eq!((instance.range_overflow)(&instance, test_value), - range_overflow_msg(&instance, test_value)); + num_range_overflow_msg(&instance, test_value)); assert_eq!((instance.range_underflow)(&instance, test_value), - range_underflow_msg(&instance, test_value)); + num_range_underflow_msg(&instance, test_value)); assert_eq!((instance.step_mismatch)(&instance, test_value), - step_mismatch_msg(&instance, test_value)); + num_step_mismatch_msg(&instance, test_value)); assert_eq!((instance.not_equal)(&instance, test_value), num_not_equal_msg(&instance, test_value)); @@ -373,10 +373,10 @@ mod test { }, Err(_enum) => { let err_msg_tuple = match _enum { - StepMismatch => (StepMismatch, step_mismatch_msg(&validator, value)), + StepMismatch => (StepMismatch, num_step_mismatch_msg(&validator, value)), NotEqual => (NotEqual, num_not_equal_msg(&validator, value)), - RangeUnderflow => (RangeUnderflow, range_underflow_msg(&validator, value)), - RangeOverflow => (RangeOverflow, range_overflow_msg(&validator, value)), + RangeUnderflow => (RangeUnderflow, num_range_underflow_msg(&validator, value)), + RangeOverflow => (RangeOverflow, num_range_overflow_msg(&validator, value)), _ => panic!("Unknown enum variant encountered") }; diff --git a/inputfilter/src/validator/pattern.rs b/inputfilter/src/validator/pattern.rs index 1f7a908..4ea05f7 100644 --- a/inputfilter/src/validator/pattern.rs +++ b/inputfilter/src/validator/pattern.rs @@ -1,11 +1,10 @@ use std::borrow::Cow; use std::fmt::Display; use regex::Regex; -use crate::ToAttributesList; -use crate::traits::ValidateValue; +use crate::{ToAttributesList, ValidateValue}; -use crate::types::ViolationEnum::PatternMismatch; -use crate::types::{ValidationResult}; +use crate::ViolationEnum::PatternMismatch; +use crate::traits::{ValidationResult}; pub type PatternViolationCallback = dyn Fn(&PatternValidator, &str) -> String + Send + Sync; @@ -13,7 +12,7 @@ pub type PatternViolationCallback = dyn Fn(&PatternValidator, &str) -> String + pub struct PatternValidator<'a> { pub pattern: Cow<'a, Regex>, - #[builder(default = "&pattern_mismatch_msg")] + #[builder(default = "&pattern_vldr_pattern_mismatch_msg")] pub pattern_mismatch: &'a PatternViolationCallback, } @@ -111,7 +110,7 @@ impl Display for PatternValidator<'_> { } } -pub fn pattern_mismatch_msg(rules: &PatternValidator, xs: &str) -> String { +pub fn pattern_vldr_pattern_mismatch_msg(rules: &PatternValidator, xs: &str) -> String { format!( "`{:}` does not match pattern `{:}`.", xs, @@ -124,7 +123,7 @@ mod test { use std::borrow::Cow; use std::error::Error; use regex::Regex; - use crate::traits::{ValidateValue}; + use crate::{ValidateValue}; use crate::ViolationEnum::PatternMismatch; use super::*; @@ -140,7 +139,7 @@ mod test { for (name, instance, passing_value, failing_value, _err_callback) in [ ("Default", PatternValidatorBuilder::default() .pattern(Cow::Owned(_rx.clone())) - .build()?, "abc", "!@#)(*", &pattern_mismatch_msg), + .build()?, "abc", "!@#)(*", &pattern_vldr_pattern_mismatch_msg), ("Custom ", PatternValidatorBuilder::default() .pattern(Cow::Owned(_rx.clone())) .pattern_mismatch(&on_custom_pattern_mismatch) diff --git a/src/lib.rs b/src/lib.rs index 234303d..7561f8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,5 +3,5 @@ pub use walrs_inputfilter::validator; pub use walrs_inputfilter::filter; -pub use walrs_inputfilter::string_input; -pub use walrs_inputfilter::types; +pub use walrs_inputfilter::constraints; +pub use walrs_inputfilter::traits; From 7c3100ba91a83c228490fc8100dbc060c5ea3a39 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Sun, 28 Jan 2024 11:57:14 -0400 Subject: [PATCH 31/38] inputfilter - Renamed '(String|Scalar)Input' to '(String|Scalar)Constraints' and changed their module names to 'string', and 'scalar', respectively. --- inputfilter/DESIGN.md | 8 +-- inputfilter/src/constraints/mod.rs | 8 +-- .../{scalar_input.rs => scalar.rs} | 38 ++++++------ .../{string_input.rs => string.rs} | 60 +++++++++---------- inputfilter/src/traits.rs | 2 +- 5 files changed, 58 insertions(+), 58 deletions(-) rename inputfilter/src/constraints/{scalar_input.rs => scalar.rs} (89%) rename inputfilter/src/constraints/{string_input.rs => string.rs} (91%) diff --git a/inputfilter/DESIGN.md b/inputfilter/DESIGN.md index cdbaeea..23dc43c 100644 --- a/inputfilter/DESIGN.md +++ b/inputfilter/DESIGN.md @@ -5,7 +5,7 @@ Controls here should: - not be stateful - In the sense of 'changing' state; E.g., should not hold on to/mutate values. -- Should only work with primitive values; E.g., scalars, array, vector, hash_map, etc. (note we can support arbitrary structures (later) via derive macros, etc.). +- Should only work with primitive values; E.g., scalars, array, vector, hash_map, etc., to limit implementation complexity (note we can support arbitrary structures (later) via derive macros, etc.). ## Inspiration @@ -19,10 +19,10 @@ Original inspiration comes from: Due to the above, in this library, we'll require less Validator, and Filter, structs since type coercion is handled for us. -## Where and how would we use `Input` controls +## Where and how would we use `*Input`/`*Constraint` controls -- In action handlers where we might need to instantiate a validator, or optionally, retrieve a globally instantiated/stored one. -- In a terminal application where we might want to reuse the same functionality stored (though in this instance rust built-in facilities for working with command line flags might be more appropriate (possibly less memory overhead, et al.?)). +- In action handlers where we might need to instantiate a constraints object, or optionally, retrieve a globally instantiated/stored one. +- In a terminal application where we might want to reuse the same functionality stored (though in this instance rust's built-in facilities for working with command line flags might be more appropriate (possibly less memory overhead, et al.?)). ## Questions diff --git a/inputfilter/src/constraints/mod.rs b/inputfilter/src/constraints/mod.rs index 32bc2ab..ff506dc 100644 --- a/inputfilter/src/constraints/mod.rs +++ b/inputfilter/src/constraints/mod.rs @@ -1,9 +1,9 @@ -pub mod scalar_input; -pub mod string_input; +pub mod scalar; +pub mod string; pub mod traits; -pub use scalar_input::*; -pub use string_input::*; +pub use scalar::*; +pub use string::*; pub use traits::*; pub fn value_missing_msg() -> String { diff --git a/inputfilter/src/constraints/scalar_input.rs b/inputfilter/src/constraints/scalar.rs similarity index 89% rename from inputfilter/src/constraints/scalar_input.rs rename to inputfilter/src/constraints/scalar.rs index 86115b7..0ee87f6 100644 --- a/inputfilter/src/constraints/scalar_input.rs +++ b/inputfilter/src/constraints/scalar.rs @@ -5,7 +5,7 @@ use crate::{ Filter, InputConstraints, Validator, ViolationMessage }; -pub fn range_underflow_msg(rules: &ScalarInput, x: T) -> String { +pub fn range_underflow_msg(rules: &ScalarConstraints, x: T) -> String { format!( "`{:}` is less than minimum `{:}`.", x, @@ -13,7 +13,7 @@ pub fn range_underflow_msg(rules: &ScalarInput, x: T) -> Stri ) } -pub fn range_overflow_msg(rules: &ScalarInput, x: T) -> String { +pub fn range_overflow_msg(rules: &ScalarConstraints, x: T) -> String { format!( "`{:}` is greater than maximum `{:}`.", x, @@ -23,7 +23,7 @@ pub fn range_overflow_msg(rules: &ScalarInput, x: T) -> Strin #[derive(Builder, Clone)] #[builder(setter(strip_option))] -pub struct ScalarInput<'a, T: ScalarValue> { +pub struct ScalarConstraints<'a, T: ScalarValue> { #[builder(default = "true")] pub break_on_failure: bool, @@ -43,22 +43,22 @@ pub struct ScalarInput<'a, T: ScalarValue> { pub filters: Option>>>, #[builder(default = "&range_underflow_msg")] - pub range_underflow_msg: &'a (dyn Fn(&ScalarInput<'a, T>, T) -> String + Send + Sync), + pub range_underflow_msg: &'a (dyn Fn(&ScalarConstraints<'a, T>, T) -> String + Send + Sync), #[builder(default = "&range_overflow_msg")] - pub range_overflow_msg: &'a (dyn Fn(&ScalarInput<'a, T>, T) -> String + Send + Sync), + pub range_overflow_msg: &'a (dyn Fn(&ScalarConstraints<'a, T>, T) -> String + Send + Sync), #[builder(default = "&value_missing_msg")] pub value_missing_msg: &'a ValueMissingCallback, } -impl<'a, T> ScalarInput<'a, T> +impl<'a, T> ScalarConstraints<'a, T> where T: ScalarValue, { /// Returns a new instance containing defaults. pub fn new() -> Self { - ScalarInput { + ScalarConstraints { break_on_failure: false, min: None, max: None, @@ -150,7 +150,7 @@ where } } -impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for ScalarInput<'a, T> +impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for ScalarConstraints<'a, T> where T: ScalarValue, { @@ -158,7 +158,7 @@ where /// /// ```rust /// use walrs_inputfilter::{ - /// ScalarInput, InputConstraints, ViolationEnum, ScalarInputBuilder, + /// ScalarConstraints, InputConstraints, ViolationEnum, ScalarConstraintsBuilder, /// range_underflow_msg, range_overflow_msg, /// ScalarValue /// }; @@ -227,17 +227,17 @@ where } } -impl Default for ScalarInput<'_, T> { +impl Default for ScalarConstraints<'_, T> { fn default() -> Self { Self::new() } } -impl Display for ScalarInput<'_, T> { +impl Display for ScalarConstraints<'_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "ScalarInput {{ required: {}, validators: {}, filters: {} }}", + "ScalarConstraints {{ required: {}, validators: {}, filters: {} }}", self.required, self .validators @@ -253,7 +253,7 @@ impl Display for ScalarInput<'_, T> { } } -impl Debug for ScalarInput<'_, T> { +impl Debug for ScalarConstraints<'_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self) } @@ -275,24 +275,24 @@ mod test { Ok(()) }; - let usize_input_default = ScalarInputBuilder::::default() + let usize_input_default = ScalarConstraintsBuilder::::default() .build() .unwrap(); - let usize_not_required = ScalarInputBuilder::::default() + let usize_not_required = ScalarConstraintsBuilder::::default() .min(1) .max(10) .validators(vec![&validate_is_even]) .build() .unwrap(); - let usize_required = (|| -> ScalarInput { + let usize_required = (|| -> ScalarConstraints { let mut new_input = usize_not_required.clone(); new_input.required = true; new_input })(); - let _usize_no_break_on_failure = (|| -> ScalarInput { + let _usize_no_break_on_failure = (|| -> ScalarConstraints { let mut new_input = usize_required.clone(); // new_input.validators.push(&|x: usize| if x % 2 != 0 { // Err(vec![(ConstraintViolation::CustomError, "Must be even".to_string())]) @@ -362,7 +362,7 @@ mod test { // Test basic usage with other types // ---- // Validates `f64`, and `f32` usage - let f64_input_required = ScalarInputBuilder::::default() + let f64_input_required = ScalarConstraintsBuilder::::default() .required(true) .min(1.0) .max(10.0) @@ -385,7 +385,7 @@ mod test { ])); // Test `char` usage - let char_input = ScalarInputBuilder::::default() + let char_input = ScalarConstraintsBuilder::::default() .min('a') .max('f') .build() diff --git a/inputfilter/src/constraints/string_input.rs b/inputfilter/src/constraints/string.rs similarity index 91% rename from inputfilter/src/constraints/string_input.rs rename to inputfilter/src/constraints/string.rs index 11a5e44..549a893 100644 --- a/inputfilter/src/constraints/string_input.rs +++ b/inputfilter/src/constraints/string.rs @@ -7,9 +7,9 @@ use crate::{ Filter, InputConstraints, Validator, ViolationMessage }; -pub type StringInputViolationCallback = dyn Fn(&StringInput, Option<&str>) -> ViolationMessage + Send + Sync; +pub type StringConstraintsViolationCallback = dyn Fn(&StringConstraints, Option<&str>) -> ViolationMessage + Send + Sync; -pub fn pattern_mismatch_msg(rules: &StringInput, xs: Option<&str>) -> String { +pub fn pattern_mismatch_msg(rules: &StringConstraints, xs: Option<&str>) -> String { format!( "`{}` does not match pattern `{}`", &xs.as_ref().unwrap(), @@ -17,7 +17,7 @@ pub fn pattern_mismatch_msg(rules: &StringInput, xs: Option<&str>) -> String { ) } -pub fn too_short_msg(rules: &StringInput, xs: Option<&str>) -> String { +pub fn too_short_msg(rules: &StringConstraints, xs: Option<&str>) -> String { format!( "Value length `{:}` is less than allowed minimum `{:}`.", &xs.as_ref().unwrap().len(), @@ -25,7 +25,7 @@ pub fn too_short_msg(rules: &StringInput, xs: Option<&str>) -> String { ) } -pub fn too_long_msg(rules: &StringInput, xs: Option<&str>) -> String { +pub fn too_long_msg(rules: &StringConstraints, xs: Option<&str>) -> String { format!( "Value length `{:}` is greater than allowed maximum `{:}`.", &xs.as_ref().unwrap().len(), @@ -35,7 +35,7 @@ pub fn too_long_msg(rules: &StringInput, xs: Option<&str>) -> String { #[derive(Builder, Clone)] #[builder(pattern = "owned", setter(strip_option))] -pub struct StringInput<'a, 'b> { +pub struct StringConstraints<'a, 'b> { #[builder(default = "true")] pub break_on_failure: bool, @@ -58,21 +58,21 @@ pub struct StringInput<'a, 'b> { pub filters: Option>>>>, #[builder(default = "&too_short_msg")] - pub too_short_msg: &'a StringInputViolationCallback, + pub too_short_msg: &'a StringConstraintsViolationCallback, #[builder(default = "&too_long_msg")] - pub too_long_msg: &'a StringInputViolationCallback, + pub too_long_msg: &'a StringConstraintsViolationCallback, #[builder(default = "&pattern_mismatch_msg")] - pub pattern_mismatch_msg: &'a StringInputViolationCallback, + pub pattern_mismatch_msg: &'a StringConstraintsViolationCallback, #[builder(default = "&value_missing_msg")] pub value_missing_msg: &'a ValueMissingCallback, } -impl<'a, 'b> StringInput<'a, 'b> { +impl<'a, 'b> StringConstraints<'a, 'b> { pub fn new() -> Self { - StringInput { + StringConstraints { break_on_failure: false, min_length: None, max_length: None, @@ -157,7 +157,7 @@ impl<'a, 'b> StringInput<'a, 'b> { } } -impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, 'b> { +impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringConstraints<'a, 'b> { /// Validates value against contained constraints and validators, and returns a result of unit and/or a Vec of /// Violation tuples. /// @@ -169,7 +169,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// RangeOverflow, RangeUnderflow, StepMismatch /// }; /// - /// let str_input = StringInputBuilder::default() + /// let str_input = StringConstraintsBuilder::default() /// .required(true) /// .value_missing_msg(&|| "Value missing".to_string()) /// .min_length(3usize) @@ -229,7 +229,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// ```rust /// use walrs_inputfilter::*; /// - /// let input = StringInputBuilder::default() + /// let input = StringConstraintsBuilder::default() /// .required(true) /// .value_missing_msg(&|| "Value missing".to_string()) /// .validators(vec![&|x: &str| { @@ -276,7 +276,7 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, /// use walrs_inputfilter::*; /// use std::borrow::Cow; /// - /// let input = StringInputBuilder::default() + /// let input = StringConstraintsBuilder::default() /// .required(true) /// .value_missing_msg(&|| "Value missing".to_string()) /// .validators(vec![&|x: &str| { @@ -308,17 +308,17 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringInput<'a, } } -impl Default for StringInput<'_, '_> { +impl Default for StringConstraints<'_, '_> { fn default() -> Self { Self::new() } } -impl Display for StringInput<'_, '_> { +impl Display for StringConstraints<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "StrInput {{ required: {}, validators: {}, filters: {} }}", + "StringConstraints {{ required: {}, validators: {}, filters: {} }}", self.required, self .validators @@ -334,7 +334,7 @@ impl Display for StringInput<'_, '_> { } } -impl Debug for StringInput<'_, '_> { +impl Debug for StringConstraints<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", &self) } @@ -418,15 +418,15 @@ mod test { }, }; - let less_than_1990_input = StringInputBuilder::default() + let less_than_1990_input = StringConstraintsBuilder::default() .validators(vec![&less_than_1990]) .build()?; - let yyyy_mm_dd_input = StringInputBuilder::default() + let yyyy_mm_dd_input = StringConstraintsBuilder::default() .validators(vec![&ymd_check]) .build()?; - let yyyy_mm_dd_input2 = StringInputBuilder::default() + let yyyy_mm_dd_input2 = StringConstraintsBuilder::default() .validators(vec![&pattern_validator]) .build()?; @@ -467,11 +467,11 @@ mod test { #[test] fn test_thread_safety() -> Result<(), Box> { - let less_than_1990_input = StringInputBuilder::default() + let less_than_1990_input = StringConstraintsBuilder::default() .validators(vec![&less_than_1990]) .build()?; - let ymd_input = StringInputBuilder::default() + let ymd_input = StringConstraintsBuilder::default() .validators(vec![&ymd_check]) .build()?; @@ -519,7 +519,7 @@ mod test { Ok(()) } - /// Example showing shared references in `StrInput`, and user-land, controls. + /// Example showing shared references in `StringConstraints`, and user-land, controls. #[test] fn test_thread_safety_with_scoped_threads_and_closures() -> Result<(), Box> { let ymd_rx = Arc::new(Regex::new(r"^\d{1,4}-\d{1,2}-\d{1,2}$").unwrap()); @@ -536,12 +536,12 @@ mod test { Ok(()) }; - let less_than_1990_input = StringInputBuilder::default() + let less_than_1990_input = StringConstraintsBuilder::default() .validators(vec![&less_than_1990]) .filters(vec![&to_last_date_of_month]) .build()?; - let ymd_input = StringInputBuilder::default() + let ymd_input = StringConstraintsBuilder::default() .validators(vec![&ymd_check]) .build()?; @@ -591,7 +591,7 @@ mod test { #[test] fn test_validate_and_filter_detailed() { - let input = StringInputBuilder::default() + let input = StringConstraintsBuilder::default() .required(true) .validators(vec![&less_than_1990]) .filters(vec![&to_last_date_of_month]) @@ -621,7 +621,7 @@ mod test { } }; - let _input = StringInputBuilder::default() + let _input = StringConstraintsBuilder::default() .validators(vec![&callback1]) .build() .unwrap(); @@ -629,14 +629,14 @@ mod test { #[test] fn test_display() { - let input = StringInputBuilder::default() + let input = StringConstraintsBuilder::default() .validators(vec![&less_than_1990]) .build() .unwrap(); assert_eq!( input.to_string(), - "StrInput { required: false, validators: Some([Validator; 1]), filters: None }" + "StringConstraints { required: false, validators: Some([Validator; 1]), filters: None }" ); } } diff --git a/inputfilter/src/traits.rs b/inputfilter/src/traits.rs index 3fa98b4..d7fece9 100644 --- a/inputfilter/src/traits.rs +++ b/inputfilter/src/traits.rs @@ -74,7 +74,7 @@ impl NumberValue for f64 {} /// These additionally provide a runtime opportunity to override /// returned violation message(s), via returned validation result `Err` tuples, and the ability to provide the /// violation type from "constraint" structures that perform validation against their own constraint props.; E.g., -/// `StringInput` (etc.) with it's `pattern`, `min_length`, `max_length` props. etc. +/// `StringConstraints` (etc.) with it's `pattern`, `min_length`, `max_length` props. etc. /// #[derive(PartialEq, Debug, Clone, Copy)] pub enum ViolationEnum { From d7da19d1864b7ddf729fc70585b93c8601dd7a72 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Sun, 28 Jan 2024 12:11:38 -0400 Subject: [PATCH 32/38] inputfilter - Renamed 'validator', and 'filter' modules to 'validators', and 'filters'. --- inputfilter/README.md | 18 +++++++---- inputfilter/src/constraints/string.rs | 2 +- inputfilter/src/{filter => filters}/mod.rs | 0 inputfilter/src/{filter => filters}/slug.rs | 4 +-- .../src/{filter => filters}/strip_tags.rs | 30 +++++++++---------- inputfilter/src/{filter => filters}/traits.rs | 0 .../src/{filter => filters}/xml_entities.rs | 4 +-- inputfilter/src/lib.rs | 8 ++--- .../src/{validator => validators}/DESIGN.md | 0 .../src/{validator => validators}/equal.rs | 0 .../src/{validator => validators}/mod.rs | 0 .../src/{validator => validators}/number.rs | 0 .../src/{validator => validators}/pattern.rs | 0 .../src/{validator => validators}/traits.rs | 0 src/lib.rs | 4 +-- 15 files changed, 38 insertions(+), 32 deletions(-) rename inputfilter/src/{filter => filters}/mod.rs (100%) rename inputfilter/src/{filter => filters}/slug.rs (98%) rename inputfilter/src/{filter => filters}/strip_tags.rs (87%) rename inputfilter/src/{filter => filters}/traits.rs (100%) rename inputfilter/src/{filter => filters}/xml_entities.rs (97%) rename inputfilter/src/{validator => validators}/DESIGN.md (100%) rename inputfilter/src/{validator => validators}/equal.rs (100%) rename inputfilter/src/{validator => validators}/mod.rs (100%) rename inputfilter/src/{validator => validators}/number.rs (100%) rename inputfilter/src/{validator => validators}/pattern.rs (100%) rename inputfilter/src/{validator => validators}/traits.rs (100%) diff --git a/inputfilter/README.md b/inputfilter/README.md index a80b4cf..9254ebb 100644 --- a/inputfilter/README.md +++ b/inputfilter/README.md @@ -1,14 +1,20 @@ # wal_inputfilter -A set of `Input` validation structs used to validate primitive values as they pertain to web applications. +A set of input validation structs used to validate primitive values as they pertain to web applications. ## Members -- `Input` - Rule struct to add validators, and/or 'filters' to. -- `validators/` - - `NumberValidator` - - `PatternValidator` - - `EqualityValidator` +- `constraints` - Contains constraint structs. + - `ScalarConstraint` - Validates scalar values. + - `StringConstraint` - Validates string/string slice values. +- `validators` + - `NumberValidator` - Validates numeric values. + - `PatternValidator` - Validates values against a regular expression. + - `EqualityValidator` - Validates values against a stored right-hand-side value. +- `filters` + - `SlugFilter` - Filters value to valid "slug" values. + - `StripTagsFilter` - Filters values against a regular expression. + - `XmlEntitiesFilter` - Filters values against a stored right-hand-side ## Usage: diff --git a/inputfilter/src/constraints/string.rs b/inputfilter/src/constraints/string.rs index 549a893..68546b2 100644 --- a/inputfilter/src/constraints/string.rs +++ b/inputfilter/src/constraints/string.rs @@ -348,7 +348,7 @@ mod test { ViolationEnum::{PatternMismatch, RangeOverflow}, InputConstraints, ValidationResult, }; - use crate::validator::pattern::PatternValidator; + use crate::validators::pattern::PatternValidator; use regex::Regex; use std::{borrow::Cow, error::Error, sync::Arc, thread}; diff --git a/inputfilter/src/filter/mod.rs b/inputfilter/src/filters/mod.rs similarity index 100% rename from inputfilter/src/filter/mod.rs rename to inputfilter/src/filters/mod.rs diff --git a/inputfilter/src/filter/slug.rs b/inputfilter/src/filters/slug.rs similarity index 98% rename from inputfilter/src/filter/slug.rs rename to inputfilter/src/filters/slug.rs index e63541b..cb65c38 100644 --- a/inputfilter/src/filter/slug.rs +++ b/inputfilter/src/filters/slug.rs @@ -21,7 +21,7 @@ pub fn get_dash_filter_regex() -> &'static Regex { /// /// ```rust /// use std::borrow::Cow; -/// use walrs_inputfilter::filter::slug::to_slug; +/// use walrs_inputfilter::filters::slug::to_slug; /// /// assert_eq!(to_slug(Cow::Borrowed("Hello World")), "hello-world"); /// ``` @@ -33,7 +33,7 @@ pub fn to_slug(xs: Cow) -> Cow { /// /// ```rust /// use std::borrow::Cow; -/// use walrs_inputfilter::filter::slug::to_pretty_slug; +/// use walrs_inputfilter::filters::slug::to_pretty_slug; /// /// assert_eq!(to_pretty_slug(Cow::Borrowed("%$Hello@#$@#!(World$$")), "hello-world"); /// ``` diff --git a/inputfilter/src/filter/strip_tags.rs b/inputfilter/src/filters/strip_tags.rs similarity index 87% rename from inputfilter/src/filter/strip_tags.rs rename to inputfilter/src/filters/strip_tags.rs index c95c388..a31d5e0 100644 --- a/inputfilter/src/filter/strip_tags.rs +++ b/inputfilter/src/filters/strip_tags.rs @@ -7,10 +7,10 @@ static DEFAULT_AMMONIA_BUILDER: OnceLock = OnceLock::new(); /// Sanitizes incoming HTML using the [Ammonia](https://docs.rs/ammonia/1.0.0/ammonia/) crate. /// /// ```rust -/// use walrs_inputfilter::filter::StripTags; +/// use walrs_inputfilter::filters::StripTagsFilter; /// use std::borrow::Cow; /// -/// let filter = StripTags::new(); +/// let filter = StripTagsFilter::new(); /// /// for (i, (incoming_src, expected_src)) in [ /// ("", ""), @@ -31,15 +31,15 @@ static DEFAULT_AMMONIA_BUILDER: OnceLock = OnceLock::new(); /// } /// ``` /// -pub struct StripTags<'a> { +pub struct StripTagsFilter<'a> { /// Ammonia builder used to sanitize incoming HTML. /// /// If `None`, a default builder is used when `filter`/instance is called. pub ammonia: Option>, } -impl<'a> StripTags<'a> { - /// Constructs a new `StripTags` instance. +impl<'a> StripTagsFilter<'a> { + /// Constructs a new `StripTagsFilter` instance. pub fn new() -> Self { Self { ammonia: None, @@ -53,10 +53,10 @@ impl<'a> StripTags<'a> { /// use std::borrow::Cow; /// use std::sync::OnceLock; /// use ammonia::Builder as AmmoniaBuilder; - /// use walrs_inputfilter::filter::StripTags; + /// use walrs_inputfilter::filters::StripTagsFilter; /// /// // Using default settings: - /// let filter = StripTags::new(); + /// let filter = StripTagsFilter::new(); /// /// let subject = r#"

Hello

/// "#; @@ -77,7 +77,7 @@ impl<'a> StripTags<'a> { /// // Remove 'style' tag from "tags-blacklist" /// .rm_clean_content_tags(&additional_allowed_tags); /// - /// let filter = StripTags { + /// let filter = StripTagsFilter { /// ammonia: Some(sanitizer) /// }; /// @@ -110,13 +110,13 @@ impl<'a> StripTags<'a> { } } -impl<'a> Default for StripTags<'a> { +impl<'a> Default for StripTagsFilter<'a> { fn default() -> Self { Self::new() } } -impl<'a, 'b> FnOnce<(Cow<'b, str>, )> for StripTags<'a> { +impl<'a, 'b> FnOnce<(Cow<'b, str>, )> for StripTagsFilter<'a> { type Output = Cow<'b, str>; extern "rust-call" fn call_once(self, args: (Cow<'b, str>, )) -> Self::Output { @@ -124,13 +124,13 @@ impl<'a, 'b> FnOnce<(Cow<'b, str>, )> for StripTags<'a> { } } -impl<'a, 'b> FnMut<(Cow<'b, str>, )> for StripTags<'a> { +impl<'a, 'b> FnMut<(Cow<'b, str>, )> for StripTagsFilter<'a> { extern "rust-call" fn call_mut(&mut self, args: (Cow<'b, str>, )) -> Self::Output { self.filter(args.0) } } -impl<'a, 'b> Fn<(Cow<'b, str>, )> for StripTags<'a> { +impl<'a, 'b> Fn<(Cow<'b, str>, )> for StripTagsFilter<'a> { extern "rust-call" fn call(&self, args: (Cow<'b, str>, )) -> Self::Output { self.filter(args.0) } @@ -142,15 +142,15 @@ mod test { #[test] fn test_construction() { - let _ = StripTags::new(); - let _ = StripTags { + let _ = StripTagsFilter::new(); + let _ = StripTagsFilter { ammonia: Some(ammonia::Builder::default()), }; } #[test] fn test_filter() { - let filter = StripTags::new(); + let filter = StripTagsFilter::new(); for (i, (incoming_src, expected_src)) in [ ("", ""), diff --git a/inputfilter/src/filter/traits.rs b/inputfilter/src/filters/traits.rs similarity index 100% rename from inputfilter/src/filter/traits.rs rename to inputfilter/src/filters/traits.rs diff --git a/inputfilter/src/filter/xml_entities.rs b/inputfilter/src/filters/xml_entities.rs similarity index 97% rename from inputfilter/src/filter/xml_entities.rs rename to inputfilter/src/filters/xml_entities.rs index 47f4bb1..96c07ed 100644 --- a/inputfilter/src/filter/xml_entities.rs +++ b/inputfilter/src/filters/xml_entities.rs @@ -13,7 +13,7 @@ static DEFAULT_CHARS_ASSOC_MAP: OnceLock> = OnceLock /// E.g., ignore results like `&amp;` for string `&`, etc. /// /// ```rust -/// use walrs_inputfilter::filter::XmlEntitiesFilter; +/// use walrs_inputfilter::filters::XmlEntitiesFilter; /// /// let filter = XmlEntitiesFilter::new(); /// @@ -52,7 +52,7 @@ impl<'a> XmlEntitiesFilter<'a> { /// xml entities. /// /// ```rust - /// use walrs_inputfilter::filter::XmlEntitiesFilter; + /// use walrs_inputfilter::filters::XmlEntitiesFilter; /// /// let filter = XmlEntitiesFilter::new(); /// diff --git a/inputfilter/src/lib.rs b/inputfilter/src/lib.rs index d1e6122..22afdf6 100644 --- a/inputfilter/src/lib.rs +++ b/inputfilter/src/lib.rs @@ -6,11 +6,11 @@ extern crate derive_builder; pub mod traits; -pub mod validator; -pub mod filter; +pub mod validators; +pub mod filters; pub mod constraints; pub use traits::*; -pub use validator::*; -pub use filter::*; +pub use validators::*; +pub use filters::*; pub use constraints::*; diff --git a/inputfilter/src/validator/DESIGN.md b/inputfilter/src/validators/DESIGN.md similarity index 100% rename from inputfilter/src/validator/DESIGN.md rename to inputfilter/src/validators/DESIGN.md diff --git a/inputfilter/src/validator/equal.rs b/inputfilter/src/validators/equal.rs similarity index 100% rename from inputfilter/src/validator/equal.rs rename to inputfilter/src/validators/equal.rs diff --git a/inputfilter/src/validator/mod.rs b/inputfilter/src/validators/mod.rs similarity index 100% rename from inputfilter/src/validator/mod.rs rename to inputfilter/src/validators/mod.rs diff --git a/inputfilter/src/validator/number.rs b/inputfilter/src/validators/number.rs similarity index 100% rename from inputfilter/src/validator/number.rs rename to inputfilter/src/validators/number.rs diff --git a/inputfilter/src/validator/pattern.rs b/inputfilter/src/validators/pattern.rs similarity index 100% rename from inputfilter/src/validator/pattern.rs rename to inputfilter/src/validators/pattern.rs diff --git a/inputfilter/src/validator/traits.rs b/inputfilter/src/validators/traits.rs similarity index 100% rename from inputfilter/src/validator/traits.rs rename to inputfilter/src/validators/traits.rs diff --git a/src/lib.rs b/src/lib.rs index 7561f8c..cb952ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![feature(fn_traits)] #![feature(unboxed_closures)] -pub use walrs_inputfilter::validator; -pub use walrs_inputfilter::filter; +pub use walrs_inputfilter::validators; +pub use walrs_inputfilter::filters; pub use walrs_inputfilter::constraints; pub use walrs_inputfilter::traits; From cadd16bcadf6251cc511fff6d9ae3fbd4eab6891 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Sun, 28 Jan 2024 12:20:37 -0400 Subject: [PATCH 33/38] inputfilter - Renamed some private methods, and fixed some typos in READMEs. --- inputfilter/README.md | 4 ++-- inputfilter/src/constraints/scalar.rs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/inputfilter/README.md b/inputfilter/README.md index 9254ebb..3aa2675 100644 --- a/inputfilter/README.md +++ b/inputfilter/README.md @@ -5,8 +5,8 @@ A set of input validation structs used to validate primitive values as they pert ## Members - `constraints` - Contains constraint structs. - - `ScalarConstraint` - Validates scalar values. - - `StringConstraint` - Validates string/string slice values. + - `ScalarConstraints` - Validates scalar values. + - `StringConstraints` - Validates string/string slice values. - `validators` - `NumberValidator` - Validates numeric values. - `PatternValidator` - Validates values against a regular expression. diff --git a/inputfilter/src/constraints/scalar.rs b/inputfilter/src/constraints/scalar.rs index 0ee87f6..8f91c89 100644 --- a/inputfilter/src/constraints/scalar.rs +++ b/inputfilter/src/constraints/scalar.rs @@ -71,7 +71,7 @@ where } } - fn _run_own_validators_on(&self, value: T) -> Result<(), Vec> { + fn _validate_against_own_constraints(&self, value: T) -> Result<(), Vec> { let mut errs = vec![]; // Test lower bound @@ -109,7 +109,7 @@ where } } - fn _run_validators_on(&self, value: T) -> Result<(), Vec> { + fn _validate_against_validators(&self, value: T) -> Result<(), Vec> { self .validators .as_deref() @@ -178,12 +178,12 @@ where } // Else if value is populated validate it Some(v) => - match self._run_own_validators_on(v) { - Ok(_) => self._run_validators_on(v), + match self._validate_against_own_constraints(v) { + Ok(_) => self._validate_against_validators(v), Err(messages1) => if self.break_on_failure { Err(messages1) - } else if let Err(mut messages2) = self._run_validators_on(v) { + } else if let Err(mut messages2) = self._validate_against_validators(v) { let mut agg = messages1; agg.append(messages2.as_mut()); Err(agg) From 8ebcb01f37655f73f8a5213eae15de632b354dfd Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Sun, 28 Jan 2024 12:34:40 -0400 Subject: [PATCH 34/38] inputfilter - Added doc test for 'ScalarConstraints::new'. --- inputfilter/src/constraints/scalar.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/inputfilter/src/constraints/scalar.rs b/inputfilter/src/constraints/scalar.rs index 8f91c89..e268e35 100644 --- a/inputfilter/src/constraints/scalar.rs +++ b/inputfilter/src/constraints/scalar.rs @@ -57,6 +57,27 @@ where T: ScalarValue, { /// Returns a new instance containing defaults. + /// + /// ```rust + /// use walrs_inputfilter::{ + /// ScalarConstraints, + /// range_underflow_msg, + /// range_overflow_msg, + /// value_missing_msg + /// }; + /// + /// let constraints = ScalarConstraints::new(); + /// + /// assert_eq!(constraints.break_on_failure, false); + /// assert_eq!(constraints.min, None); + /// assert_eq!(constraints.max, None); + /// assert_eq!(constraints.required, false); + /// assert_eq!(constraints.validators, None); + /// assert_eq!(constraints.filters, None); + /// assert_eq!(constraints.range_underflow_msg, &range_underflow_msg); + /// assert_eq!(constraints.range_overflow_msg, &range_overflow_msg); + /// assert_eq!(constraints.value_missing_msg, &value_missing_msg); + /// ``` pub fn new() -> Self { ScalarConstraints { break_on_failure: false, @@ -158,7 +179,8 @@ where /// /// ```rust /// use walrs_inputfilter::{ - /// ScalarConstraints, InputConstraints, ViolationEnum, ScalarConstraintsBuilder, + /// ScalarConstraints, InputConstraints, ViolationEnum, + /// ScalarConstraintsBuilder, /// range_underflow_msg, range_overflow_msg, /// ScalarValue /// }; From 220ff5589ca153d6a063f55d6f15abacd3abe087 Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Sun, 28 Jan 2024 18:55:48 -0400 Subject: [PATCH 35/38] inputfilter - Added test cases for 'ScalarConstraints.break_on_failure', and some doc tests. --- inputfilter/src/constraints/scalar.rs | 177 ++++++++++++++++++++------ 1 file changed, 139 insertions(+), 38 deletions(-) diff --git a/inputfilter/src/constraints/scalar.rs b/inputfilter/src/constraints/scalar.rs index e268e35..54282c2 100644 --- a/inputfilter/src/constraints/scalar.rs +++ b/inputfilter/src/constraints/scalar.rs @@ -24,7 +24,7 @@ pub fn range_overflow_msg(rules: &ScalarConstraints, x: T) -> #[derive(Builder, Clone)] #[builder(setter(strip_option))] pub struct ScalarConstraints<'a, T: ScalarValue> { - #[builder(default = "true")] + #[builder(default = "false")] pub break_on_failure: bool, #[builder(default = "None")] @@ -56,27 +56,24 @@ impl<'a, T> ScalarConstraints<'a, T> where T: ScalarValue, { - /// Returns a new instance containing defaults. + /// Returns a new instance with all fields set defaults. /// /// ```rust /// use walrs_inputfilter::{ - /// ScalarConstraints, - /// range_underflow_msg, - /// range_overflow_msg, - /// value_missing_msg + /// ScalarConstraints, InputConstraints, ViolationEnum, + /// range_overflow_msg, range_underflow_msg, value_missing_msg, /// }; /// - /// let constraints = ScalarConstraints::new(); + /// let input = ScalarConstraints::::new(); /// - /// assert_eq!(constraints.break_on_failure, false); - /// assert_eq!(constraints.min, None); - /// assert_eq!(constraints.max, None); - /// assert_eq!(constraints.required, false); - /// assert_eq!(constraints.validators, None); - /// assert_eq!(constraints.filters, None); - /// assert_eq!(constraints.range_underflow_msg, &range_underflow_msg); - /// assert_eq!(constraints.range_overflow_msg, &range_overflow_msg); - /// assert_eq!(constraints.value_missing_msg, &value_missing_msg); + /// // Assert defaults + /// // ---- + /// assert_eq!(input.break_on_failure, false); + /// assert_eq!(input.min, None); + /// assert_eq!(input.max, None); + /// assert_eq!(input.required, false); + /// assert!(input.validators.is_none()); + /// assert!(input.filters.is_none()); /// ``` pub fn new() -> Self { ScalarConstraints { @@ -175,16 +172,67 @@ impl<'a, 'b, T: 'b> InputConstraints<'a, 'b, T, T> for ScalarConstraints<'a, T> where T: ScalarValue, { - /// Validates given value against contained constraints and returns a result of unit and/or a Vec of violation tuples. + /// Validates given value against contained constraints and returns a result of unit and/or a Vec of violation tuples + /// if value doesn't pass validation. /// /// ```rust /// use walrs_inputfilter::{ /// ScalarConstraints, InputConstraints, ViolationEnum, /// ScalarConstraintsBuilder, - /// range_underflow_msg, range_overflow_msg, + /// range_underflow_msg, range_overflow_msg, value_missing_msg, /// ScalarValue /// }; /// + /// // Setup a custom validator + /// let validate_is_even = |x: usize| if x % 2 != 0 { + /// Err(vec![(ViolationEnum::CustomError, "Must be even".to_string())]) + /// } else { + /// Ok(()) + /// }; + /// + /// // Setup input constraints + /// let usize_required = ScalarConstraintsBuilder::::default() + /// .min(1) + /// .max(10) + /// .required(true) + /// .validators(vec![&validate_is_even]) + /// .build() + /// .unwrap(); + /// + /// let usize_break_on_failure = (|| { + /// let mut new_input = usize_required.clone(); + /// new_input.break_on_failure = true; + /// new_input + /// })(); + /// + /// let test_cases = [ + /// ("No value", &usize_required, None, Err(vec![ + /// (ViolationEnum::ValueMissing, + /// value_missing_msg()), + /// ])), + /// ("With valid value", &usize_required, Some(4), Ok(())), + /// ("With \"out of lower bounds\" value", &usize_required, Some(0), Err(vec![ + /// (ViolationEnum::RangeUnderflow, + /// range_underflow_msg(&usize_required, 0)), + /// ])), + /// ("With \"out of upper bounds\" value", &usize_required, Some(11), Err(vec![ + /// (ViolationEnum::RangeOverflow, range_overflow_msg(&usize_required, 11)), + /// (ViolationEnum::CustomError, "Must be even".to_string()), + /// ])), + /// ("With \"out of upper bounds\" value, and 'break_on_failure: true'", &usize_break_on_failure, Some(11), Err(vec![ + /// (ViolationEnum::RangeOverflow, range_overflow_msg(&usize_required, 11)), + /// ])), + /// ("With \"not Even\" value", &usize_required, Some(7), Err(vec![ + /// (ViolationEnum::CustomError, + /// "Must be even".to_string()), + /// ])), + /// ]; + /// + /// // Run test cases + /// for (i, (test_name, input, value, expected_rslt)) in test_cases.into_iter().enumerate() { + /// println!("Case {}: {}", i + 1, test_name); + /// assert_eq!(input.validate_detailed(value), expected_rslt); + /// } /// ``` fn validate_detailed(&self, value: Option) -> Result<(), Vec> { match value { @@ -218,6 +266,63 @@ where /// Validates given value against contained constraints, and returns a result of unit, and/or, a Vec of /// Violation messages. + /// + /// + /// ```rust + /// use walrs_inputfilter::{ + /// ScalarConstraints, InputConstraints, ViolationEnum, + /// ScalarConstraintsBuilder, + /// range_underflow_msg, range_overflow_msg, value_missing_msg, + /// ScalarValue + /// }; + /// + /// // Setup a custom validator + /// let validate_is_even = |x: usize| if x % 2 != 0 { + /// Err(vec![(ViolationEnum::CustomError, "Must be even".to_string())]) + /// } else { + /// Ok(()) + /// }; + /// + /// // Setup input constraints + /// let usize_required = ScalarConstraintsBuilder::::default() + /// .min(1) + /// .max(10) + /// .required(true) + /// .validators(vec![&validate_is_even]) + /// .build() + /// .unwrap(); + /// + /// let usize_break_on_failure = (|| { + /// let mut new_input = usize_required.clone(); + /// new_input.break_on_failure = true; + /// new_input + /// })(); + /// + /// let test_cases = [ + /// ("No value", &usize_required, None, Err(vec![ + /// value_missing_msg(), + /// ])), + /// ("With valid value", &usize_required, Some(4), Ok(())), + /// ("With \"out of lower bounds\" value", &usize_required, Some(0), Err(vec![ + /// range_underflow_msg(&usize_required, 0), + /// ])), + /// ("With \"out of upper bounds\" value", &usize_required, Some(11), Err(vec![ + /// range_overflow_msg(&usize_required, 11), + /// "Must be even".to_string(), + /// ])), + /// ("With \"out of upper bounds\" value, and 'break_on_failure: true'", &usize_break_on_failure, Some(11), Err(vec![ + /// range_overflow_msg(&usize_required, 11), + /// ])), + /// ("With \"not Even\" value", &usize_required, Some(7), Err(vec![ + /// "Must be even".to_string(), + /// ])), + /// ]; + /// + /// // Run test cases + /// for (i, (test_name, input, value, expected_rslt)) in test_cases.into_iter().enumerate() { + /// println!("Case {}: {}", i + 1, test_name); + /// assert_eq!(input.validate(value), expected_rslt); + /// } fn validate(&self, value: Option) -> Result<(), Vec> { match self.validate_detailed(value) { // If errors, extract messages and return them @@ -259,7 +364,8 @@ impl Display for ScalarConstraints<'_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "ScalarConstraints {{ required: {}, validators: {}, filters: {} }}", + "ScalarConstraints {{ break_on_failure: {}, required: {}, validators: {}, filters: {} }}", + self.break_on_failure, self.required, self .validators @@ -314,13 +420,8 @@ mod test { new_input })(); - let _usize_no_break_on_failure = (|| -> ScalarConstraints { + let usize_break_on_failure = (|| -> ScalarConstraints { let mut new_input = usize_required.clone(); - // new_input.validators.push(&|x: usize| if x % 2 != 0 { - // Err(vec![(ConstraintViolation::CustomError, "Must be even".to_string())]) - // } else { - // Ok(()) - // }); new_input.break_on_failure = true; new_input })(); @@ -339,14 +440,14 @@ mod test { range_underflow_msg(&usize_not_required, 0)) ])), ("1-10, Even, with invalid value(2)", &usize_not_required, Some(11), Err(vec![ - (ViolationEnum::RangeOverflow, - range_overflow_msg(&usize_not_required, 11)), + (ViolationEnum::RangeOverflow, range_overflow_msg(&usize_not_required, 11)), + (ViolationEnum::CustomError, "Must be even".to_string()), ])), ("1-10, Even, with invalid value (3)", &usize_not_required, Some(7), Err(vec![ (ViolationEnum::CustomError, "Must be even".to_string()), ])), - ("1-10, Even, with value value", &usize_not_required, Some(8), Ok(())), + ("1-10, Even, with valid value", &usize_not_required, Some(8), Ok(())), // Required // ---- @@ -355,24 +456,24 @@ mod test { value_missing_msg()), ])), ("1-10, Even, required, with valid value", &usize_required, Some(2), Ok(())), - ("1-10, Even, required, with valid value (2)", &usize_required, Some(10), Ok(())), + ("1-10, Even, required, with valid value (1)", &usize_required, Some(4), Ok(())), + ("1-10, Even, required, with valid value (2)", &usize_required, Some(8), Ok(())), + ("1-10, Even, required, with valid value (3)", &usize_required, Some(10), Ok(())), ("1-10, Even, required, with invalid value", &usize_required, Some(0), Err(vec![ (ViolationEnum::RangeUnderflow, range_underflow_msg(&usize_required, 0)), ])), ("1-10, Even, required, with invalid value(2)", &usize_required, Some(11), Err(vec![ - (ViolationEnum::RangeOverflow, - range_overflow_msg(&usize_required, 11)), + (ViolationEnum::RangeOverflow, range_overflow_msg(&usize_required, 11)), + (ViolationEnum::CustomError, "Must be even".to_string()), ])), ("1-10, Even, required, with invalid value (3)", &usize_required, Some(7), Err(vec![ (ViolationEnum::CustomError, "Must be even".to_string()), ])), - ("1-10, Even, required, with value value", &usize_required, Some(8), Ok(())), - // ("1-10, Even, 'break-on-failure: true' false", &usize_no_break_on_failure, Some(7), Err(vec![ - // (ConstraintViolation::CustomError, - // "Must be even".to_string()), - // ])), + ("1-10, Even, required, 'break-on-failure: true', with multiple violations", &usize_break_on_failure, Some(11), Err(vec![ + (ViolationEnum::RangeOverflow, range_overflow_msg(&usize_break_on_failure, 11)), + ])), ]; for (i, (test_name, input, subj, expected)) in test_cases.into_iter().enumerate() { @@ -402,8 +503,8 @@ mod test { ])); assert_eq!(f64_input_required.validate_detailed(Some(2.0)), Ok(())); assert_eq!(f64_input_required.validate_detailed(Some(11.0)), Err(vec![ - (ViolationEnum::RangeOverflow, - range_overflow_msg(&f64_input_required, 11.0)), + (ViolationEnum::RangeOverflow, range_overflow_msg(&f64_input_required, 11.0)), + (ViolationEnum::CustomError, "Must be even".to_string()), ])); // Test `char` usage From 275ecc318e2ee00fa279f7762238d00f5c49fb0e Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Sun, 28 Jan 2024 19:26:50 -0400 Subject: [PATCH 36/38] inputfilter - Updated some doc tests and added missing props. to 'Display' impls. --- inputfilter/src/constraints/scalar.rs | 4 +- inputfilter/src/constraints/string.rs | 64 +++++++++++++++++++-------- inputfilter/src/constraints/traits.rs | 2 - 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/inputfilter/src/constraints/scalar.rs b/inputfilter/src/constraints/scalar.rs index 54282c2..7645b92 100644 --- a/inputfilter/src/constraints/scalar.rs +++ b/inputfilter/src/constraints/scalar.rs @@ -364,8 +364,10 @@ impl Display for ScalarConstraints<'_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "ScalarConstraints {{ break_on_failure: {}, required: {}, validators: {}, filters: {} }}", + "ScalarConstraints {{ break_on_failure: {}, min: {}, max: {}, required: {}, validators: {}, filters: {} }}", self.break_on_failure, + self.min.map_or("None".to_string(), |x| x.to_string()), + self.max.map_or("None".to_string(), |x| x.to_string()), self.required, self .validators diff --git a/inputfilter/src/constraints/string.rs b/inputfilter/src/constraints/string.rs index 68546b2..5ef1b11 100644 --- a/inputfilter/src/constraints/string.rs +++ b/inputfilter/src/constraints/string.rs @@ -36,7 +36,7 @@ pub fn too_long_msg(rules: &StringConstraints, xs: Option<&str>) -> String { #[derive(Builder, Clone)] #[builder(pattern = "owned", setter(strip_option))] pub struct StringConstraints<'a, 'b> { - #[builder(default = "true")] + #[builder(default = "false")] pub break_on_failure: bool, #[builder(default = "None")] @@ -188,8 +188,14 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringConstrain /// let too_long_str = &"ab".repeat(201); /// /// assert_eq!(str_input.validate_detailed(None), Err(vec![ (ValueMissing, "Value missing".to_string()) ])); - /// assert_eq!(str_input.validate_detailed(Some(&"ab")), Err(vec![ (TooShort, "Too short".to_string()) ])); - /// assert_eq!(str_input.validate_detailed(Some(&too_long_str)), Err(vec![ (TooLong, too_long_msg(&str_input, Some(&too_long_str))) ])); + /// assert_eq!(str_input.validate_detailed(Some(&"ab")), Err(vec![ + /// (TooShort, "Too short".to_string()), + /// (TypeMismatch, "Invalid email".to_string()), + /// ])); + /// assert_eq!(str_input.validate_detailed(Some(&too_long_str)), Err(vec![ + /// (TooLong, too_long_msg(&str_input, Some(&too_long_str))), + /// (TypeMismatch, "Invalid email".to_string()), + /// ])); /// assert_eq!(str_input.validate_detailed(Some(&"abc")), Err(vec![ (TypeMismatch, "Invalid email".to_string()) ])); /// assert_eq!(str_input.validate_detailed(Some(&"abc@def")), Ok(())); /// ``` @@ -228,25 +234,41 @@ impl<'a, 'b> InputConstraints<'a, 'b, &'b str, Cow<'b, str>> for StringConstrain /// /// ```rust /// use walrs_inputfilter::*; + /// use walrs_inputfilter::pattern::PatternValidator; + /// use walrs_inputfilter::traits::ViolationEnum::{ + /// ValueMissing, TooShort, TooLong, TypeMismatch, CustomError, + /// RangeOverflow, RangeUnderflow, StepMismatch + /// }; /// - /// let input = StringConstraintsBuilder::default() - /// .required(true) - /// .value_missing_msg(&|| "Value missing".to_string()) - /// .validators(vec![&|x: &str| { - /// if x.len() < 3 { - /// return Err(vec![( - /// ViolationEnum::TooShort, - /// "Too short".to_string(), - /// )]); + /// let str_input = StringConstraintsBuilder::default() + /// .required(true) + /// .value_missing_msg(&|| "Value missing".to_string()) + /// .min_length(3usize) + /// .too_short_msg(&|_, _| "Too short".to_string()) + /// .max_length(200usize) // Default violation message callback used here. + /// // Naive email pattern validator (naive for this example). + /// .validators(vec![&|x: &str| { + /// if !x.contains('@') { + /// return Err(vec![(TypeMismatch, "Invalid email".to_string())]); /// } /// Ok(()) /// }]) - /// .build() - /// .unwrap() - /// ; + /// .build() + /// .unwrap(); + /// + /// let too_long_str = &"ab".repeat(201); /// - /// assert_eq!(input.validate(Some(&"ab")), Err(vec!["Too short".to_string()])); - /// assert_eq!(input.validate(None), Err(vec!["Value missing".to_string()])); + /// assert_eq!(str_input.validate(None), Err(vec![ "Value missing".to_string() ])); + /// assert_eq!(str_input.validate(Some(&"ab")), Err(vec![ + /// "Too short".to_string(), + /// "Invalid email".to_string(), + /// ])); + /// assert_eq!(str_input.validate(Some(&too_long_str)), Err(vec![ + /// too_long_msg(&str_input, Some(&too_long_str)), + /// "Invalid email".to_string(), + /// ])); + /// assert_eq!(str_input.validate(Some(&"abc")), Err(vec![ "Invalid email".to_string() ])); + /// assert_eq!(str_input.validate(Some(&"abc@def")), Ok(())); /// ``` fn validate(&self, value: Option<&'b str>) -> Result<(), Vec> { match self.validate_detailed(value) { @@ -318,7 +340,11 @@ impl Display for StringConstraints<'_, '_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "StringConstraints {{ required: {}, validators: {}, filters: {} }}", + "StringConstraints {{ break_on_failure: {}, min_length: {}, max_length: {}, pattern: {}, required: {}, validators: {}, filters: {} }}", + self.break_on_failure, + self.min_length.map_or("None".to_string(), |x| x.to_string()), + self.max_length.map_or("None".to_string(), |x| x.to_string()), + self.pattern.as_ref().map_or("None".to_string(), |rx| rx.to_string()), self.required, self .validators @@ -636,7 +662,7 @@ mod test { assert_eq!( input.to_string(), - "StringConstraints { required: false, validators: Some([Validator; 1]), filters: None }" + "StringConstraints { break_on_failure: false, min_length: None, max_length: None, pattern: None, required: false, validators: Some([Validator; 1]), filters: None }", ); } } diff --git a/inputfilter/src/constraints/traits.rs b/inputfilter/src/constraints/traits.rs index ceb4ad6..2e1b351 100644 --- a/inputfilter/src/constraints/traits.rs +++ b/inputfilter/src/constraints/traits.rs @@ -11,10 +11,8 @@ pub type ValueMissingCallback = dyn Fn() -> ViolationMessage + Send + Sync; pub trait InputConstraints<'a, 'b, T: 'b, FT: 'b>: Display + Debug where T: InputValue { - // @todo - Move this to `ValidateValue` trait. fn validate(&self, value: Option) -> Result<(), Vec>; - // @todo - Move this to `ValidateValue` trait. fn validate_detailed(&self, value: Option) -> Result<(), Vec>; fn filter(&self, value: Option) -> Option; From 87cd8add575f8f6a71b7e3c6f2c67cc1f5ef46de Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:54:41 -0400 Subject: [PATCH 37/38] navigation - Progress on initial implementation ideas [unstable] - Planning to go with a solution that just uses a symbol graph with some limitations. --- navigation/Cargo.toml | 3 +++ navigation/DESIGN.md | 8 ++++++++ navigation/README.md | 2 ++ navigation/src/lib.rs | 3 +++ navigation/src/navigation.rs | 36 +++++++++++++++++++++++++----------- 5 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 navigation/DESIGN.md create mode 100644 navigation/README.md diff --git a/navigation/Cargo.toml b/navigation/Cargo.toml index 0c6c228..f03ae6e 100644 --- a/navigation/Cargo.toml +++ b/navigation/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +derive_builder = "0.13.0" +serde = { version = "1.0.103", features = ["derive"] } +serde_json = "1.0.82" diff --git a/navigation/DESIGN.md b/navigation/DESIGN.md new file mode 100644 index 0000000..1092b59 --- /dev/null +++ b/navigation/DESIGN.md @@ -0,0 +1,8 @@ +# Design + +```pseudo +- nav-item + - items + - nav-item + - items +``` \ No newline at end of file diff --git a/navigation/README.md b/navigation/README.md new file mode 100644 index 0000000..f09df33 --- /dev/null +++ b/navigation/README.md @@ -0,0 +1,2 @@ +# Navigation + diff --git a/navigation/src/lib.rs b/navigation/src/lib.rs index b07ee10..ee3da3a 100644 --- a/navigation/src/lib.rs +++ b/navigation/src/lib.rs @@ -1 +1,4 @@ +#[macro_use] +extern crate derive_builder; + pub mod navigation; diff --git a/navigation/src/navigation.rs b/navigation/src/navigation.rs index 1ac95fd..a7e7b34 100644 --- a/navigation/src/navigation.rs +++ b/navigation/src/navigation.rs @@ -1,24 +1,27 @@ // use std::borrow::Cow; +use serde_json; pub trait NavigationItem<'a> { // fn get_uri(&self) -> Option>; // fn get_label() -> Cow<'a, str>; - fn add(&mut self, item: NavItem) -> isize; - fn remove(&mut self, pred: &'a impl Fn(&'a NavItem) -> bool) -> Option; + fn add(&mut self, item: NavItem<'a>) -> isize; + fn remove(&mut self, pred: &'a impl Fn(&'a NavItem) -> bool) -> Option>; fn find(&mut self, pred: &'a impl Fn(&'a NavItem) -> bool) -> Option<&'a NavItem>; /// Gets number of nav items in nav tree. fn size(&mut self) -> isize; } -pub struct NavItem { +#[derive(Builder)] +pub struct NavItem<'a> { pub active: bool, - pub attributes: Option>, + pub attributes: Option>, pub children_only: bool, pub fragment: Option, - pub items: Option>, - pub label: String, + pub items: Option>>, + pub label: Option, pub order: u64, + pub parent: Option>>, pub privilege: Option, pub resource: Option, pub uri: Option, @@ -29,23 +32,34 @@ pub struct NavItem { _reevaluate_size: bool, } -impl<'a> NavigationItem<'a> for NavItem { +impl<'a> NavigationItem<'a> for NavItem<'a> { // fn get_uri(&self) -> Option> { // self.uri.map(|uri| Cow::Borrowed(&uri.as_str())) // } - fn add(&mut self, item: NavItem) -> isize { + fn add(&mut self, mut item: NavItem<'a>) -> isize { self._reevaluate_size = true; - todo!() + item.parent = Some(Box::new(self)); + + if self.items.is_none() { + self.items = Some(vec![item]); + } else { + self.items.push(item); + } + + self.size() } fn remove(&mut self, pred: &'a impl Fn(&'a NavItem) -> bool) -> Option { self._reevaluate_size = true; + // self.find(pred)d todo!() } - fn find(&mut self, pred: &'a impl Fn(&'a NavItem) -> bool) -> Option<&'a NavItem> { - todo!() + fn find(&mut self, pred: &'a impl Fn(&'a NavItem) -> bool) -> Option> { + self.items.map(|items| { + items.iter().find(pred).map(|x| Box::new(x)) + }).flatten() } fn size(&mut self) -> isize { From fda734045e225c6875d5241d00ff89489c55adde Mon Sep 17 00:00:00 2001 From: Ely De La Cruz <603428+elycruz@users.noreply.github.com> Date: Thu, 1 Feb 2024 23:58:29 -0500 Subject: [PATCH 38/38] graph, navigation - Added some todo notes, and progress on 'navigation' control idea. --- acl/src/simple.rs | 2 +- graph/src/digraph/dfs.rs | 2 +- graph/src/digraph/dipaths_dfs.rs | 2 +- graph/src/digraph/mod.rs | 4 +-- .../{symbol_graph.rs => symbol_digraph.rs} | 8 ++++-- graph/src/graph/README.md | 2 +- graph/src/graph/graph.rs | 21 +++++++++++--- graph/src/graph/mod.rs | 1 + graph/src/graph/shared_utils.rs | 4 +-- graph/src/graph/symbol_graph.rs | 2 ++ graph/src/graph/traits.rs | 1 + graph/src/lib.rs | 4 +++ graph/tests/graph_test.rs | 3 ++ navigation/src/navigation.rs | 28 +++++++++---------- 14 files changed, 54 insertions(+), 30 deletions(-) rename graph/src/digraph/{symbol_graph.rs => symbol_digraph.rs} (98%) diff --git a/acl/src/simple.rs b/acl/src/simple.rs index 83eb88c..2d87153 100644 --- a/acl/src/simple.rs +++ b/acl/src/simple.rs @@ -6,7 +6,7 @@ use serde_derive::{Deserialize, Serialize}; use serde_json; use walrs_graph::digraph::dfs::{DigraphDFS, DigraphDFSShape}; -use walrs_graph::digraph::symbol_graph::DisymGraph; +use walrs_graph::digraph::symbol_digraph::DisymGraph; pub type Role = String; pub type Resource = String; diff --git a/graph/src/digraph/dfs.rs b/graph/src/digraph/dfs.rs index 0f6becf..f2bf6c6 100644 --- a/graph/src/digraph/dfs.rs +++ b/graph/src/digraph/dfs.rs @@ -73,7 +73,7 @@ impl DigraphDFSShape for DigraphDFS { #[cfg(test)] mod test { - use crate::digraph::symbol_graph::DisymGraph; + use crate::digraph::symbol_digraph::DisymGraph; use crate::math::triangular_num; use super::*; diff --git a/graph/src/digraph/dipaths_dfs.rs b/graph/src/digraph/dipaths_dfs.rs index cb4ec63..aa3037a 100644 --- a/graph/src/digraph/dipaths_dfs.rs +++ b/graph/src/digraph/dipaths_dfs.rs @@ -80,7 +80,7 @@ impl DigraphDipathsDFS { #[cfg(test)] mod test { - use crate::digraph::symbol_graph::DisymGraph; + use crate::digraph::symbol_digraph::DisymGraph; use crate::math::triangular_num; use super::*; diff --git a/graph/src/digraph/mod.rs b/graph/src/digraph/mod.rs index 0e6560d..3be0433 100644 --- a/graph/src/digraph/mod.rs +++ b/graph/src/digraph/mod.rs @@ -1,9 +1,9 @@ pub mod dfs; pub mod dipaths_dfs; -pub mod symbol_graph; +pub mod symbol_digraph; pub mod digraph; pub use dfs::*; pub use dipaths_dfs::*; -pub use symbol_graph::*; +pub use symbol_digraph::*; pub use digraph::*; diff --git a/graph/src/digraph/symbol_graph.rs b/graph/src/digraph/symbol_digraph.rs similarity index 98% rename from graph/src/digraph/symbol_graph.rs rename to graph/src/digraph/symbol_digraph.rs index 792405f..153cce6 100644 --- a/graph/src/digraph/symbol_graph.rs +++ b/graph/src/digraph/symbol_digraph.rs @@ -2,7 +2,9 @@ use std::io::{BufRead, BufReader}; use crate::digraph::Digraph; -/// `SymbolGraph` A Directed Acyclic Graph (B-DAG) data structure. +/// `DisymGraph` A Directed Acyclic Graph (B-DAG) data structure. +/// @todo Consider renaming struct to `SymbolDigraph`. +/// /// ```rust /// // @todo /// ``` @@ -208,7 +210,7 @@ impl Default for DisymGraph { /// `From` trait usage example. /// /// ```rust -/// use walrs_graph::digraph::symbol_graph::DisymGraph; +/// use walrs_graph::digraph::symbol_digraph::DisymGraph; /// use std::io::{BufRead, BufReader, Lines}; /// use std::fs::File; /// @@ -248,7 +250,7 @@ mod test { use std::fs::File; use std::io::BufReader; - use crate::digraph::symbol_graph::DisymGraph; + use crate::digraph::symbol_digraph::DisymGraph; #[test] fn test_new() -> Result<(), String> { diff --git a/graph/src/graph/README.md b/graph/src/graph/README.md index b48d74d..9c59924 100644 --- a/graph/src/graph/README.md +++ b/graph/src/graph/README.md @@ -52,7 +52,7 @@ Example graph data for graph that uses strings as it's vertices: #### Plain text example -Format: `symbol [,symbol]` - First `symbol` inherits from adjacent symbols (Backward Directed Graph representation etc.). +Format: `symbol [,symbol]` - First `symbol` inherits from adjacent symbols (Backward Directed Graph representation) etc. `roles.txt` diff --git a/graph/src/graph/graph.rs b/graph/src/graph/graph.rs index a6b4394..cdb5dd5 100644 --- a/graph/src/graph/graph.rs +++ b/graph/src/graph/graph.rs @@ -2,15 +2,28 @@ use crate::graph::shared_utils::extract_vert_and_edge_counts_from_bufreader; use std::fmt::Debug; use std::io::{BufRead, BufReader, Lines}; +/// A basic index graph that tracks edges on vertex indices in adjacency lists. #[derive(Debug)] pub struct Graph { + // @todo - Should be `Vec>>`, more memory efficient. _adj_lists: Vec>, _edge_count: usize, + // @todo - Error message for invalid vertex should be customizable. } impl Graph { - /// Returns a new graph initialized with `vert_count` number of empty adjacency - /// lists (one for each expected vertex). + /// Returns a new graph containing given `vert_count` number of vertex slots. + /// + /// ```rust + /// use walrs_graph::Graph; + /// + /// for vert_count in 0..3 { + /// let g = Graph::new(vert_count); + /// + /// assert_eq!(g.vert_count(), vert_count); + /// assert_eq!(g.edge_count(), 0); + /// } + /// ``` pub fn new(vert_count: usize) -> Self { Graph { _adj_lists: vec![Vec::new(); vert_count], @@ -18,7 +31,7 @@ impl Graph { } } - /// Returns vertex count + /// Returns vertex count. pub fn vert_count(&self) -> usize { self._adj_lists.len() } @@ -191,7 +204,7 @@ pub fn invalid_vertex_msg(v: usize, max_v: usize) -> String { impl From<&mut BufReader> for Graph { fn from(reader: &mut BufReader) -> Self { let vert_count = match extract_vert_and_edge_counts_from_bufreader(reader) { - Ok((vc, _)) => vc, + Ok((v_count, _)) => v_count, Err(err) => panic!("{:?}", err), }; diff --git a/graph/src/graph/mod.rs b/graph/src/graph/mod.rs index b95e786..235c35f 100644 --- a/graph/src/graph/mod.rs +++ b/graph/src/graph/mod.rs @@ -8,3 +8,4 @@ pub use shared_utils::*; pub use single_source_dfs::*; pub use symbol_graph::*; pub use graph::*; +pub use traits::*; diff --git a/graph/src/graph/shared_utils.rs b/graph/src/graph/shared_utils.rs index a8bc51c..c542586 100644 --- a/graph/src/graph/shared_utils.rs +++ b/graph/src/graph/shared_utils.rs @@ -2,7 +2,7 @@ use std::io::{BufRead, BufReader}; /// Extracts vertex, and, edge counts from top (first two lines) of text file containing /// vertices, and their edges; E.g., -/// **note:** annotations are only for example here - only numbers are allowed in the file +/// **note:** annotations are only for example here - only numbers are allowed in the file; /// control errors out on 'parse error' otherwise.. /// /// ```text @@ -29,7 +29,7 @@ pub fn extract_vert_and_edge_counts_from_bufreader( reader .read_line(&mut s) .expect(&format!("Unable to read \"edge count\" line from buffer")); - let edges_count: usize = s.trim().parse::().unwrap(); + let edges_count = s.trim().parse::().unwrap(); Ok((vertices_count, edges_count)) } diff --git a/graph/src/graph/symbol_graph.rs b/graph/src/graph/symbol_graph.rs index 5d98713..95a0c3c 100644 --- a/graph/src/graph/symbol_graph.rs +++ b/graph/src/graph/symbol_graph.rs @@ -58,6 +58,8 @@ impl SymbolGraph where T: Symbol { pub fn new() -> Self { SymbolGraph { _vertices: Vec::new(), + + // @todo Allow setting initial graph size. _graph: Graph::new(0), } } diff --git a/graph/src/graph/traits.rs b/graph/src/graph/traits.rs index 1f48684..9a9890d 100644 --- a/graph/src/graph/traits.rs +++ b/graph/src/graph/traits.rs @@ -2,5 +2,6 @@ use std::fmt::Debug; use std::str::FromStr; pub trait Symbol: Clone + Debug + PartialEq + FromStr + From { + // @todo fn name should be a less generic name; E.g., `get_symbol_id`, etc. fn id(&self) -> String; } diff --git a/graph/src/lib.rs b/graph/src/lib.rs index e658d3e..ebf13ed 100644 --- a/graph/src/lib.rs +++ b/graph/src/lib.rs @@ -1,3 +1,7 @@ pub mod digraph; pub mod graph; pub mod math; + +pub use graph::*; +pub use digraph::*; +pub use math::*; diff --git a/graph/tests/graph_test.rs b/graph/tests/graph_test.rs index 87c8b16..0d16830 100644 --- a/graph/tests/graph_test.rs +++ b/graph/tests/graph_test.rs @@ -51,14 +51,17 @@ pub fn test_graph_tiny_text_undirected() -> std::io::Result<()> { g.edge_count(), "both graphs should contain same edge count" ); + assert_eq!( g1.vert_count(), g.vert_count(), "both graphs should contain same vert count" ); + // @todo Test all contained edges, and/or, adjacency lists. // Print graph // println!("{:?}", &g); + Ok(()) } Err(err) => panic!("{:?}", err), diff --git a/navigation/src/navigation.rs b/navigation/src/navigation.rs index a7e7b34..b268547 100644 --- a/navigation/src/navigation.rs +++ b/navigation/src/navigation.rs @@ -4,24 +4,23 @@ use serde_json; pub trait NavigationItem<'a> { // fn get_uri(&self) -> Option>; // fn get_label() -> Cow<'a, str>; - fn add(&mut self, item: NavItem<'a>) -> isize; - fn remove(&mut self, pred: &'a impl Fn(&'a NavItem) -> bool) -> Option>; - fn find(&mut self, pred: &'a impl Fn(&'a NavItem) -> bool) -> Option<&'a NavItem>; + fn add(&mut self, item: NavItem) -> isize; + fn remove(&mut self, pred: impl Fn(&NavItem) -> bool) -> Option; + fn find(&self, pred: impl Fn(&NavItem) -> bool) -> Option; /// Gets number of nav items in nav tree. fn size(&mut self) -> isize; } -#[derive(Builder)] -pub struct NavItem<'a> { +#[derive(Default, Clone, Builder)] +pub struct NavItem { pub active: bool, pub attributes: Option>, pub children_only: bool, pub fragment: Option, - pub items: Option>>, + pub items: Option>, pub label: Option, pub order: u64, - pub parent: Option>>, pub privilege: Option, pub resource: Option, pub uri: Option, @@ -32,33 +31,32 @@ pub struct NavItem<'a> { _reevaluate_size: bool, } -impl<'a> NavigationItem<'a> for NavItem<'a> { +impl<'a> NavigationItem<'a> for NavItem { // fn get_uri(&self) -> Option> { // self.uri.map(|uri| Cow::Borrowed(&uri.as_str())) // } - fn add(&mut self, mut item: NavItem<'a>) -> isize { + fn add(&mut self, item: NavItem) -> isize { self._reevaluate_size = true; - item.parent = Some(Box::new(self)); if self.items.is_none() { self.items = Some(vec![item]); } else { - self.items.push(item); + self.items.as_mut().unwrap().push(item); } self.size() } - fn remove(&mut self, pred: &'a impl Fn(&'a NavItem) -> bool) -> Option { + fn remove(&mut self, pred: impl Fn(&'a NavItem) -> bool) -> Option { self._reevaluate_size = true; // self.find(pred)d todo!() } - fn find(&mut self, pred: &'a impl Fn(&'a NavItem) -> bool) -> Option> { - self.items.map(|items| { - items.iter().find(pred).map(|x| Box::new(x)) + fn find(&self, pred: impl Fn(&NavItem) -> bool) -> Option { + self.items.as_deref().map(|items| { + items.iter().find(|item| pred(*item)).map(|x| x.clone()) }).flatten() }