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/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/_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/_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/input.rs b/_recycler/inputfilter/input.rs similarity index 86% rename from inputfilter/src/input.rs rename to _recycler/inputfilter/input.rs index 773a252..f12ca4c 100644 --- a/inputfilter/src/input.rs +++ b/_recycler/inputfilter/input.rs @@ -1,469 +1,466 @@ -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}; +use crate::{ValidationErrTuple, 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> +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>, Option<&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, + } + } + + fn validate_custom(&self, _: &'b T) -> ValidationResult { + Ok(()) + } +} + +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!() + } + + fn validate1(&self, _value: Option) -> Result<(), Vec> { + todo!() + } + + fn filter(&self, _value: Option) -> Option { + todo!() + } + + fn validate_and_filter(&self, _x: Option) -> Result, Vec> { + todo!() + } + + fn validate_and_filter1(&self, _x: Option) -> Result, Vec> { + todo!() + } +} + +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, _: Option<&T>) -> 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_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/acl/src/simple.rs b/acl/src/simple.rs index 90d8395..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; @@ -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/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 18671d6..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> { @@ -733,7 +735,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/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/inputfilter/Cargo.toml b/inputfilter/Cargo.toml index d75e373..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.9.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/DESIGN.md b/inputfilter/DESIGN.md index 5b20fcd..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). +- 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/README.md b/inputfilter/README.md index a80b4cf..3aa2675 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. + - `ScalarConstraints` - Validates scalar values. + - `StringConstraints` - 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/mod.rs b/inputfilter/src/constraints/mod.rs new file mode 100644 index 0000000..ff506dc --- /dev/null +++ b/inputfilter/src/constraints/mod.rs @@ -0,0 +1,11 @@ +pub mod scalar; +pub mod string; +pub mod traits; + +pub use scalar::*; +pub use string::*; +pub use traits::*; + +pub fn value_missing_msg() -> String { + "Value missing".to_string() +} diff --git a/inputfilter/src/constraints/scalar.rs b/inputfilter/src/constraints/scalar.rs new file mode 100644 index 0000000..7645b92 --- /dev/null +++ b/inputfilter/src/constraints/scalar.rs @@ -0,0 +1,527 @@ +use std::fmt::{Debug, Display, Formatter}; + +use crate::{ + value_missing_msg, ViolationEnum, ScalarValue, ViolationTuple, ValueMissingCallback, + Filter, InputConstraints, Validator, ViolationMessage +}; + +pub fn range_underflow_msg(rules: &ScalarConstraints, x: T) -> String { + format!( + "`{:}` is less than minimum `{:}`.", + x, + &rules.min.unwrap() + ) +} + +pub fn range_overflow_msg(rules: &ScalarConstraints, x: T) -> String { + format!( + "`{:}` is greater than maximum `{:}`.", + x, + &rules.max.unwrap() + ) +} + +#[derive(Builder, Clone)] +#[builder(setter(strip_option))] +pub struct ScalarConstraints<'a, T: ScalarValue> { + #[builder(default = "false")] + pub break_on_failure: bool, + + #[builder(default = "None")] + pub min: Option, + + #[builder(default = "None")] + pub max: Option, + + #[builder(default = "false")] + pub required: bool, + + #[builder(default = "None")] + pub validators: Option>>, + + #[builder(default = "None")] + pub filters: Option>>>, + + #[builder(default = "&range_underflow_msg")] + 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(&ScalarConstraints<'a, T>, T) -> String + Send + Sync), + + #[builder(default = "&value_missing_msg")] + pub value_missing_msg: &'a ValueMissingCallback, +} + +impl<'a, T> ScalarConstraints<'a, T> +where + T: ScalarValue, +{ + /// Returns a new instance with all fields set defaults. + /// + /// ```rust + /// use walrs_inputfilter::{ + /// ScalarConstraints, InputConstraints, ViolationEnum, + /// range_overflow_msg, range_underflow_msg, value_missing_msg, + /// }; + /// + /// let input = ScalarConstraints::::new(); + /// + /// // 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 { + break_on_failure: false, + min: None, + max: None, + required: false, + validators: None, + filters: None, + range_underflow_msg: &(range_underflow_msg), + range_overflow_msg: &(range_overflow_msg), + value_missing_msg: &(value_missing_msg), + } + } + + fn _validate_against_own_constraints(&self, value: T) -> Result<(), Vec> { + let mut errs = vec![]; + + // Test lower bound + if let Some(min) = self.min { + if value < min { + errs.push(( + ViolationEnum::RangeUnderflow, + (self.range_underflow_msg)(self, value), + )); + + if self.break_on_failure { + return Err(errs); + } + } + } + + // Test upper bound + if let Some(max) = self.max { + if value > max { + errs.push(( + ViolationEnum::RangeOverflow, + (self.range_overflow_msg)(self, value), + )); + + 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) + } +} + +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 + /// if value doesn't pass validation. + /// + /// ```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![ + /// (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 { + None => { + if self.required { + Err(vec![( + ViolationEnum::ValueMissing, + (self.value_missing_msg)(), + )]) + } else { + Ok(()) + } + } + // Else if value is populated validate it + Some(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._validate_against_validators(v) { + let mut agg = messages1; + agg.append(messages2.as_mut()); + Err(agg) + } else { + Err(messages1) + } + } + } + } + + /// 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 + Err(messages) => Err(messages.into_iter().map(|(_, message)| message).collect()), + Ok(_) => Ok(()), + } + } + + /// Filters value against contained filters. + fn filter(&self, value: Option) -> Option { + match self.filters.as_deref() { + None => value, + Some(fs) => fs.iter().fold(value, |agg, f| f(agg)), + } + } + + /// 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 Default for ScalarConstraints<'_, T> { + fn default() -> Self { + Self::new() + } +} + +impl Display for ScalarConstraints<'_, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "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 + .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 ScalarConstraints<'_, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ViolationEnum, InputConstraints}; + + #[test] + 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 + // ---- + let validate_is_even = |x: usize| if x % 2 != 0 { + Err(vec![(ViolationEnum::CustomError, "Must be even".to_string())]) + } else { + Ok(()) + }; + + let usize_input_default = ScalarConstraintsBuilder::::default() + .build() + .unwrap(); + + let usize_not_required = ScalarConstraintsBuilder::::default() + .min(1) + .max(10) + .validators(vec![&validate_is_even]) + .build() + .unwrap(); + + let usize_required = (|| -> ScalarConstraints { + let mut new_input = usize_not_required.clone(); + new_input.required = true; + new_input + })(); + + let usize_break_on_failure = (|| -> ScalarConstraints { + let mut new_input = usize_required.clone(); + 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![ + (ViolationEnum::RangeUnderflow, + 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::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 valid value", &usize_not_required, Some(8), Ok(())), + + // Required + // ---- + ("1-10, Even, required, no value", &usize_required, None, Err(vec![ + (ViolationEnum::ValueMissing, + value_missing_msg()), + ])), + ("1-10, Even, required, with valid value", &usize_required, Some(2), 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::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, '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() { + println!("Case {}: {}", i + 1, test_name); + + assert_eq!(input.validate_detailed(subj), expected); + } + + // Test basic usage with other types + // ---- + // Validates `f64`, and `f32` usage + let f64_input_required = ScalarConstraintsBuilder::::default() + .required(true) + .min(1.0) + .max(10.0) + .validators(vec![&|x: f64| if x % 2.0 != 0.0 { + Err(vec![(ViolationEnum::CustomError, "Must be even".to_string())]) + } else { + Ok(()) + }]) + .build() + .unwrap(); + + assert_eq!(f64_input_required.validate_detailed(None), Err(vec![ + (ViolationEnum::ValueMissing, + 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![ + (ViolationEnum::RangeOverflow, range_overflow_msg(&f64_input_required, 11.0)), + (ViolationEnum::CustomError, "Must be even".to_string()), + ])); + + // Test `char` usage + let char_input = ScalarConstraintsBuilder::::default() + .min('a') + .max('f') + .build() + .unwrap(); + + 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()), + ])); + } +} diff --git a/inputfilter/src/constraints/string.rs b/inputfilter/src/constraints/string.rs new file mode 100644 index 0000000..5ef1b11 --- /dev/null +++ b/inputfilter/src/constraints/string.rs @@ -0,0 +1,668 @@ +use std::borrow::Cow; +use std::fmt::{Debug, Display, Formatter}; +use regex::Regex; + +use crate::{ + ViolationEnum, ViolationTuple, ValidationResult, value_missing_msg, ValueMissingCallback, + Filter, InputConstraints, Validator, ViolationMessage +}; + +pub type StringConstraintsViolationCallback = dyn Fn(&StringConstraints, Option<&str>) -> ViolationMessage + Send + Sync; + +pub fn pattern_mismatch_msg(rules: &StringConstraints, xs: Option<&str>) -> String { + format!( + "`{}` does not match pattern `{}`", + &xs.as_ref().unwrap(), + rules.pattern.as_ref().unwrap() + ) +} + +pub fn too_short_msg(rules: &StringConstraints, 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: &StringConstraints, xs: Option<&str>) -> String { + format!( + "Value length `{:}` is greater than allowed maximum `{:}`.", + &xs.as_ref().unwrap().len(), + &rules.min_length.unwrap_or(0) + ) +} + +#[derive(Builder, Clone)] +#[builder(pattern = "owned", setter(strip_option))] +pub struct StringConstraints<'a, 'b> { + #[builder(default = "false")] + pub break_on_failure: bool, + + #[builder(default = "None")] + pub min_length: Option, + + #[builder(default = "None")] + pub max_length: Option, + + #[builder(default = "None")] + pub pattern: Option, + + #[builder(default = "false")] + pub required: bool, + + #[builder(default = "None")] + pub validators: Option>>, + + #[builder(default = "None")] + pub filters: Option>>>>, + + #[builder(default = "&too_short_msg")] + pub too_short_msg: &'a StringConstraintsViolationCallback, + + #[builder(default = "&too_long_msg")] + pub too_long_msg: &'a StringConstraintsViolationCallback, + + #[builder(default = "&pattern_mismatch_msg")] + pub pattern_mismatch_msg: &'a StringConstraintsViolationCallback, + + #[builder(default = "&value_missing_msg")] + pub value_missing_msg: &'a ValueMissingCallback, +} + +impl<'a, 'b> StringConstraints<'a, 'b> { + pub fn new() -> Self { + StringConstraints { + break_on_failure: false, + min_length: None, + max_length: None, + pattern: None, + required: false, + validators: None, + filters: None, + too_short_msg: &(too_long_msg), + too_long_msg: &(too_long_msg), + pattern_mismatch_msg: &(pattern_mismatch_msg), + value_missing_msg: &value_missing_msg, + } + } + + fn _validate_against_own_constraints(&self, value: &'b str) -> ValidationResult { + let mut errs = vec![]; + + if let Some(min_length) = self.min_length { + if value.len() < min_length { + errs.push(( + ViolationEnum::TooShort, + (self.too_short_msg)(self, Some(value)), + )); + + if self.break_on_failure { return Err(errs); } + } + } + + if let Some(max_length) = self.max_length { + if value.len() > max_length { + errs.push(( + ViolationEnum::TooLong, + (self.too_long_msg)(self, Some(value)), + )); + + if self.break_on_failure { return Err(errs); } + } + } + + if let Some(pattern) = &self.pattern { + if !pattern.is_match(value) { + errs.push(( + ViolationEnum::PatternMismatch, + (self.pattern_mismatch_msg)(self, Some(value)), + )); + + if self.break_on_failure { return Err(errs); } + } + } + + if errs.is_empty() { Ok(()) } else { Err(errs) } + } + + 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(), + |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> 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. + /// + /// ```rust + /// use walrs_inputfilter::*; + /// use walrs_inputfilter::pattern::PatternValidator; + /// use walrs_inputfilter::traits::ViolationEnum::{ + /// ValueMissing, TooShort, TooLong, TypeMismatch, CustomError, + /// RangeOverflow, RangeUnderflow, StepMismatch + /// }; + /// + /// 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(); + /// + /// 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()), + /// (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(())); + /// ``` + fn validate_detailed(&self, value: Option<&'b str>) -> Result<(), Vec> { + match value { + None => { + if self.required { + Err(vec![( + ViolationEnum::ValueMissing, + (self.value_missing_msg)(), + )]) + } else { + Ok(()) + } + } + // Else if value is populated validate it + Some(v) => match self._validate_against_own_constraints(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) + } + } + } + }, + } + } + + /// Same as `validate_detailed` only the violation messages are returned. + /// + /// ```rust + /// use walrs_inputfilter::*; + /// use walrs_inputfilter::pattern::PatternValidator; + /// use walrs_inputfilter::traits::ViolationEnum::{ + /// ValueMissing, TooShort, TooLong, TypeMismatch, CustomError, + /// RangeOverflow, RangeUnderflow, StepMismatch + /// }; + /// + /// 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(); + /// + /// let too_long_str = &"ab".repeat(201); + /// + /// 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) { + // 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> { + match self.filters.as_deref() { + None => value, + Some(fs) => + fs.iter().fold(value, |agg, f| f(agg)), + } + } + + 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, + /// and only the error messages are returned, for `Err` case. + /// + /// ```rust + /// use walrs_inputfilter::*; + /// use std::borrow::Cow; + /// + /// 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(), + /// )]); + /// } + /// Ok(()) + /// }]) + /// .filters(vec![&|xs: Option>| { + /// xs.map(|xs| Cow::Owned(xs.to_lowercase())) + /// }]) + /// .build() + /// .unwrap() + /// ; + /// + /// 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_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), + } + } +} + +impl Default for StringConstraints<'_, '_> { + fn default() -> Self { + Self::new() + } +} + +impl Display for StringConstraints<'_, '_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "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 + .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 StringConstraints<'_, '_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", &self) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + ViolationEnum, + ViolationEnum::{PatternMismatch, RangeOverflow}, + InputConstraints, ValidationResult, + }; + use crate::validators::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()) + }, + }; + + let less_than_1990_input = StringConstraintsBuilder::default() + .validators(vec![&less_than_1990]) + .build()?; + + let yyyy_mm_dd_input = StringConstraintsBuilder::default() + .validators(vec![&ymd_check]) + .build()?; + + let yyyy_mm_dd_input2 = StringConstraintsBuilder::default() + .validators(vec![&pattern_validator]) + .build()?; + + // Missing value check + if let Err(errs) = less_than_1990_input.validate(None) { + panic!("Expected Ok(()); Received Err({:#?})", &errs); + } + + // Mismatch check + let value = "1000-99-999"; + match yyyy_mm_dd_input.validate_detailed(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 + 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_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_detailed(Some(value)) { + panic!("Expected Ok(()); Received Err({:#?})", &errs); + } + + Ok(()) + } + + #[test] + fn test_thread_safety() -> Result<(), Box> { + let less_than_1990_input = StringConstraintsBuilder::default() + .validators(vec![&less_than_1990]) + .build()?; + + let ymd_input = StringConstraintsBuilder::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_detailed(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_detailed(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 `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()); + 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 = StringConstraintsBuilder::default() + .validators(vec![&less_than_1990]) + .filters(vec![&to_last_date_of_month]) + .build()?; + + let ymd_input = StringConstraintsBuilder::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_detailed(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_detailed(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_detailed(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_detailed() { + let input = StringConstraintsBuilder::default() + .required(true) + .validators(vec![&less_than_1990]) + .filters(vec![&to_last_date_of_month]) + .build() + .unwrap(); + + assert_eq!( + 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_detailed(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![( + ViolationEnum::TypeMismatch, + "Error".to_string(), + )]) + } + }; + + let _input = StringConstraintsBuilder::default() + .validators(vec![&callback1]) + .build() + .unwrap(); + } + + #[test] + fn test_display() { + let input = StringConstraintsBuilder::default() + .validators(vec![&less_than_1990]) + .build() + .unwrap(); + + assert_eq!( + input.to_string(), + "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 new file mode 100644 index 0000000..2e1b351 --- /dev/null +++ b/inputfilter/src/constraints/traits.rs @@ -0,0 +1,23 @@ +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 { + + fn validate(&self, value: Option) -> Result<(), Vec>; + + 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/filters/mod.rs similarity index 88% rename from inputfilter/src/filter/mod.rs rename to inputfilter/src/filters/mod.rs index fedb2b8..6714432 100644 --- a/inputfilter/src/filter/mod.rs +++ b/inputfilter/src/filters/mod.rs @@ -1,6 +1,7 @@ pub mod slug; pub mod strip_tags; pub mod xml_entities; +pub mod traits; pub use slug::*; pub use strip_tags::*; diff --git a/inputfilter/src/filter/slug.rs b/inputfilter/src/filters/slug.rs similarity index 88% rename from inputfilter/src/filter/slug.rs rename to inputfilter/src/filters/slug.rs index ce8c0fb..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"); /// ``` @@ -98,13 +98,13 @@ impl<'a> FnOnce<(Cow<'a, str>, )> for SlugFilter { } } -impl<'a, 'b> Fn<(Cow<'a, str>, )> for SlugFilter { +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, 'b> FnMut<(Cow<'a, str>, )> for SlugFilter { +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) } @@ -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/filter/strip_tags.rs b/inputfilter/src/filters/strip_tags.rs similarity index 82% rename from inputfilter/src/filter/strip_tags.rs rename to inputfilter/src/filters/strip_tags.rs index 7232b42..a31d5e0 100644 --- a/inputfilter/src/filter/strip_tags.rs +++ b/inputfilter/src/filters/strip_tags.rs @@ -1,166 +1,174 @@ -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::filters::StripTagsFilter; +/// use std::borrow::Cow; +/// +/// let filter = StripTagsFilter::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 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> StripTagsFilter<'a> { + /// Constructs a new `StripTagsFilter` 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::filters::StripTagsFilter; + /// + /// // Using default settings: + /// let filter = StripTagsFilter::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 = StripTagsFilter { + /// ammonia: Some(sanitizer) + /// }; + /// + /// // Notice `style` tags are no longer removed. + /// assert_eq!(filter.filter( + /// "".into() + /// ), + /// "" + /// ); + /// + /// // 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> { + 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 StripTagsFilter<'a> { + fn default() -> Self { + Self::new() + } +} + +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 { + self.filter(args.0) + } +} + +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 StripTagsFilter<'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 _ = StripTagsFilter::new(); + let _ = StripTagsFilter { + ammonia: Some(ammonia::Builder::default()), + }; + } + + #[test] + fn test_filter() { + let filter = StripTagsFilter::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/filters/traits.rs b/inputfilter/src/filters/traits.rs new file mode 100644 index 0000000..d8fa818 --- /dev/null +++ b/inputfilter/src/filters/traits.rs @@ -0,0 +1,5 @@ +use crate::InputValue; + +pub trait FilterValue { + fn filter(&self, value: T) -> T; +} diff --git a/inputfilter/src/filter/xml_entities.rs b/inputfilter/src/filters/xml_entities.rs similarity index 93% rename from inputfilter/src/filter/xml_entities.rs rename to inputfilter/src/filters/xml_entities.rs index cf590d7..96c07ed 100644 --- a/inputfilter/src/filter/xml_entities.rs +++ b/inputfilter/src/filters/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::filters::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::filters::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/lib.rs b/inputfilter/src/lib.rs index ae73578..22afdf6 100644 --- a/inputfilter/src/lib.rs +++ b/inputfilter/src/lib.rs @@ -1,18 +1,16 @@ #![feature(fn_traits)] #![feature(unboxed_closures)] +#![feature(associated_type_defaults)] #[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. +pub mod traits; +pub mod validators; +pub mod filters; +pub mod constraints; +pub use traits::*; +pub use validators::*; +pub use filters::*; +pub use constraints::*; diff --git a/inputfilter/src/traits.rs b/inputfilter/src/traits.rs new file mode 100644 index 0000000..d7fece9 --- /dev/null +++ b/inputfilter/src/traits.rs @@ -0,0 +1,107 @@ +use std::ops::{Add, Div, Mul, Rem, Sub}; +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 char {} +impl InputValue for str {} +impl InputValue for &str {} + +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 {} +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 {} + +/// 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., +/// `StringConstraints` (etc.) with it's `pattern`, `min_length`, `max_length` props. etc. +/// +#[derive(PartialEq, Debug, Clone, Copy)] +pub enum ViolationEnum { + CustomError, + PatternMismatch, + RangeOverflow, + RangeUnderflow, + StepMismatch, + TooLong, + TooShort, + NotEqual, + TypeMismatch, + 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>; + +/// 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/types.rs b/inputfilter/src/types.rs deleted file mode 100644 index b68eb00..0000000 --- a/inputfilter/src/types.rs +++ /dev/null @@ -1,159 +0,0 @@ -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 str {} -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: InputValue>: 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)))) - } -} - diff --git a/inputfilter/src/validator/mod.rs b/inputfilter/src/validator/mod.rs deleted file mode 100644 index 4f49079..0000000 --- a/inputfilter/src/validator/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod pattern; -pub mod number; -pub mod equal; 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 73% rename from inputfilter/src/validator/equal.rs rename to inputfilter/src/validators/equal.rs index ddd021b..7a22b38 100644 --- a/inputfilter/src/validator/equal.rs +++ b/inputfilter/src/validators/equal.rs @@ -1,7 +1,8 @@ use std::fmt::Display; use crate::ToAttributesList; -use crate::types::ConstraintViolation; -use crate::types::{InputValue, ValidateValue, ValidationResult}; +use crate::ValidateValue; +use crate::traits::ViolationEnum; +use crate::traits::{InputValue, ValidationResult}; #[derive(Builder, Clone)] pub struct EqualityValidator<'a, T> @@ -9,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), } @@ -20,7 +21,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))]) } } } @@ -62,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) @@ -71,20 +72,20 @@ 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] 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"), - not_equal_msg(&instance, "foo"), - "Default 'not_equal_msg' fn should return expected value"); + assert_eq!((instance.not_equal_msg)(&instance, "foo"), + equal_vldr_not_equal_msg(&instance, "foo"), + "Default 'equal_vldr_not_equal_msg' fn should return expected value"); Ok(()) } @@ -99,20 +100,20 @@ 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 { 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))]) + 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))]) + validator(lhs_value), + Err(vec![(NotEqual, equal_vldr_not_equal_msg(&validator, lhs_value))]) ); } } diff --git a/inputfilter/src/validators/mod.rs b/inputfilter/src/validators/mod.rs new file mode 100644 index 0000000..bb6af9a --- /dev/null +++ b/inputfilter/src/validators/mod.rs @@ -0,0 +1,9 @@ +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/validators/number.rs similarity index 79% rename from inputfilter/src/validator/number.rs rename to inputfilter/src/validators/number.rs index e31e4cc..9817adb 100644 --- a/inputfilter/src/validator/number.rs +++ b/inputfilter/src/validators/number.rs @@ -1,10 +1,11 @@ use std::fmt::{Display, Formatter}; use crate::ToAttributesList; -use crate::types::{ - ConstraintViolation, - ConstraintViolation::{ +use crate::ValidateValue; +use crate::traits::{ + ViolationEnum, + ViolationEnum::{ NotEqual, RangeOverflow, RangeUnderflow, StepMismatch, - }, NumberValue, ValidateValue, ValidationResult, + }, NumberValue, ValidationResult, }; use serde_json::value::to_value as to_json_value; @@ -13,29 +14,30 @@ 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")] + #[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 = "¬_equal_msg")] + #[builder(default = "&num_not_equal_msg")] pub not_equal: &'a (dyn Fn(&NumberValidator<'a, T>, T) -> String + Send + Sync), } @@ -43,7 +45,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 { @@ -75,7 +77,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), @@ -84,7 +86,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 { @@ -93,10 +95,10 @@ where max: None, step: None, equal: None, - range_underflow: &range_underflow_msg, - range_overflow: &range_overflow_msg, - step_mismatch: &step_mismatch_msg, - not_equal: ¬_equal_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, } } } @@ -136,10 +138,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 { @@ -208,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, { @@ -219,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, { @@ -230,7 +228,7 @@ where ) } -pub fn step_mismatch_msg( +pub fn num_step_mismatch_msg( rules: &NumberValidator, x: T, ) -> String { @@ -241,7 +239,7 @@ pub fn step_mismatch_msg( ) } -pub fn not_equal_msg( +pub fn num_not_equal_msg( rules: &NumberValidator, x: T, ) -> String { @@ -256,14 +254,14 @@ pub fn not_equal_msg( mod test { use std::error::Error; - use crate::ConstraintViolation::NotEqual; + use crate::ViolationEnum::NotEqual; use super::*; #[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() @@ -277,7 +275,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); @@ -288,17 +286,17 @@ mod test { // ---- let test_value = 99; - assert_eq!((&instance.range_overflow)(&instance, test_value), - range_overflow_msg(&instance, test_value)); + assert_eq!((instance.range_overflow)(&instance, test_value), + num_range_overflow_msg(&instance, test_value)); - assert_eq!((&instance.range_underflow)(&instance, test_value), - range_underflow_msg(&instance, test_value)); + assert_eq!((instance.range_underflow)(&instance, test_value), + num_range_underflow_msg(&instance, test_value)); - assert_eq!((&instance.step_mismatch)(&instance, test_value), - step_mismatch_msg(&instance, test_value)); + assert_eq!((instance.step_mismatch)(&instance, test_value), + num_step_mismatch_msg(&instance, test_value)); - assert_eq!((&instance.not_equal)(&instance, test_value), - not_equal_msg(&instance, test_value)); + assert_eq!((instance.not_equal)(&instance, test_value), + num_not_equal_msg(&instance, test_value)); } Ok(()) @@ -375,10 +373,10 @@ mod test { }, Err(_enum) => { let err_msg_tuple = match _enum { - StepMismatch => (StepMismatch, step_mismatch_msg(&validator, value)), - NotEqual => (NotEqual, not_equal_msg(&validator, value)), - RangeUnderflow => (RangeUnderflow, range_underflow_msg(&validator, value)), - RangeOverflow => (RangeOverflow, range_overflow_msg(&validator, value)), + StepMismatch => (StepMismatch, num_step_mismatch_msg(&validator, value)), + NotEqual => (NotEqual, num_not_equal_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/validators/pattern.rs similarity index 51% rename from inputfilter/src/validator/pattern.rs rename to inputfilter/src/validators/pattern.rs index 34a9a52..4ea05f7 100644 --- a/inputfilter/src/validator/pattern.rs +++ b/inputfilter/src/validators/pattern.rs @@ -1,232 +1,171 @@ -use std::borrow::Cow; -use std::fmt::Display; -use regex::Regex; -use crate::ToAttributesList; - -use crate::types::ConstraintViolation::PatternMismatch; -use crate::types::{ValidationResult, ValidateValue}; - -pub type PatternViolationCallback = dyn Fn(&PatternValidator, &str) -> String + Send + Sync; - -#[derive(Builder, Clone)] -pub struct PatternValidator<'a> { - pub pattern: Cow<'a, Regex>, - - #[builder(default = "&pattern_mismatch_msg")] - pub pattern_mismatch: &'a PatternViolationCallback, -} - -impl PatternValidator<'_> { - pub fn new() -> Self { - PatternValidatorBuilder::default().build().unwrap() - } -} - -impl Default for PatternValidator<'_> { - fn default() -> Self { - PatternValidatorBuilder::default().build().unwrap() - } -} - -impl ValidateValue<&str> for PatternValidator<'_> -where { - fn validate(&self, value: &str) -> ValidationResult { - match self.pattern.is_match(value) { - false => Err(vec![(PatternMismatch, (self.pattern_mismatch)(self, value))]), - _ => Ok(()) - } - } -} - -impl ToAttributesList for PatternValidator<'_> { - fn to_attributes_list(&self) -> Option> { - Some(vec![("pattern".into(), self.pattern.to_string().into())]) - } -} - -impl FnOnce<(&str, )> for PatternValidator<'_> { - type Output = ValidationResult; - - extern "rust-call" fn call_once(self, args: (&str, )) -> Self::Output { - self.validate(args.0) - } -} - -impl FnMut<(&str, )> for PatternValidator<'_> { - extern "rust-call" fn call_mut(&mut self, args: (&str, )) -> Self::Output { - self.validate(args.0) - } -} - -impl Fn<(&str, )> for PatternValidator<'_> { - extern "rust-call" fn call(&self, args: (&str, )) -> Self::Output { - self.validate(args.0) - } -} - -impl FnOnce<(&&str, )> for PatternValidator<'_> { - type Output = ValidationResult; - - extern "rust-call" fn call_once(self, args: (&&str, )) -> Self::Output { - self.validate(args.0) - } -} - -impl FnMut<(&&str, )> for PatternValidator<'_> { - extern "rust-call" fn call_mut(&mut self, args: (&&str, )) -> Self::Output { - self.validate(args.0) - } -} - -impl Fn<(&&str, )> for PatternValidator<'_> { - extern "rust-call" fn call(&self, args: (&&str, )) -> 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<(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()) - } -} - -pub fn pattern_mismatch_msg(rules: &PatternValidator, xs: &str) -> String { - format!( - "`{:}` does not match pattern `{:}`.", - xs, - &rules.pattern.to_string() - ) -} - -#[cfg(test)] -mod test { - use std::borrow::Cow; - use std::error::Error; - use regex::Regex; - use crate::{ValidateValue}; - use crate::ConstraintViolation::PatternMismatch; - - use super::*; - - #[test] - fn test_construction_and_validation() -> Result<(), Box> { - let _rx = Regex::new(r"^\w{2,55}$")?; - - fn on_custom_pattern_mismatch(_: &PatternValidator, _: &str) -> String { - return "custom pattern mismatch err message".into() - } - - for (name, instance, passingValue, failingValue, err_callback) in [ - ("Default", PatternValidatorBuilder::default() - .pattern(Cow::Owned(_rx.clone())) - .build()?, "abc", "!@#)(*", &pattern_mismatch_msg), - ("Custom ", PatternValidatorBuilder::default() - .pattern(Cow::Owned(_rx.clone())) - .pattern_mismatch(&on_custom_pattern_mismatch) - .build()?, "abc", "!@#)(*", &on_custom_pattern_mismatch) - ] as [( - &str, - PatternValidator, - &str, - &str, - &PatternViolationCallback - ); 2] { - println!("{}", name); - - // Test as an `Fn*` trait - assert_eq!((&instance)(passingValue), Ok(())); - assert_eq!((&instance)(failingValue), Err(vec![ - (PatternMismatch, (&instance.pattern_mismatch)(&instance, failingValue)) - ])); - - // Test `validate` method directly - assert_eq!(instance.validate(passingValue), Ok(())); - assert_eq!(instance.validate(failingValue), Err(vec![ - (PatternMismatch, (&instance.pattern_mismatch)(&instance, failingValue)) - ])); - - // Passing value as `&Box` (same as passing `&str`, but from the heap), to `Fn*` trait - // ---- - assert_eq!((&instance)(&Box::from(passingValue)), Ok(())); - assert_eq!((&instance)(&Box::from(failingValue)), Err(vec![ - (PatternMismatch, (&instance.pattern_mismatch)(&instance, &Box::from(failingValue))) - ])); - - // Passing value as `Box` (heap allocated slice), to `Fn*` trait - // ---- - assert_eq!((&instance)(Box::from(passingValue)), Ok(())); - assert_eq!((&instance)(Box::from(failingValue)), Err(vec![ - (PatternMismatch, (&instance.pattern_mismatch)(&instance, failingValue)) - ])); - - // Passing value as `String`, to `Fn*` trait - // ---- - assert_eq!((&instance)(passingValue.to_string()), Ok(())); - assert_eq!((&instance)(failingValue.to_string()), Err(vec![ - (PatternMismatch, (&instance.pattern_mismatch)(&instance, failingValue)) - ])); - } - - Ok(()) - } -} \ No newline at end of file +use std::borrow::Cow; +use std::fmt::Display; +use regex::Regex; +use crate::{ToAttributesList, ValidateValue}; + +use crate::ViolationEnum::PatternMismatch; +use crate::traits::{ValidationResult}; + +pub type PatternViolationCallback = dyn Fn(&PatternValidator, &str) -> String + Send + Sync; + +#[derive(Builder, Clone)] +pub struct PatternValidator<'a> { + pub pattern: Cow<'a, Regex>, + + #[builder(default = "&pattern_vldr_pattern_mismatch_msg")] + pub pattern_mismatch: &'a PatternViolationCallback, +} + +impl PatternValidator<'_> { + pub fn new() -> Self { + PatternValidatorBuilder::default().build().unwrap() + } +} + +impl Default for PatternValidator<'_> { + fn default() -> Self { + PatternValidatorBuilder::default().build().unwrap() + } +} + +impl ValidateValue<&str> for PatternValidator<'_> +where { + fn validate(&self, value: &str) -> ValidationResult { + match self.pattern.is_match(value) { + false => Err(vec![(PatternMismatch, (self.pattern_mismatch)(self, value))]), + _ => Ok(()) + } + } +} + +impl ToAttributesList for PatternValidator<'_> { + fn to_attributes_list(&self) -> Option> { + Some(vec![("pattern".into(), self.pattern.to_string().into())]) + } +} + +impl FnOnce<(&str, )> for PatternValidator<'_> { + type Output = ValidationResult; + + extern "rust-call" fn call_once(self, args: (&str, )) -> Self::Output { + self.validate(args.0) + } +} + +impl FnMut<(&str, )> for PatternValidator<'_> { + extern "rust-call" fn call_mut(&mut self, args: (&str, )) -> Self::Output { + self.validate(args.0) + } +} + +impl Fn<(&str, )> for PatternValidator<'_> { + extern "rust-call" fn call(&self, args: (&str, )) -> Self::Output { + self.validate(args.0) + } +} + +impl FnOnce<(&&str, )> for PatternValidator<'_> { + type Output = ValidationResult; + + extern "rust-call" fn call_once(self, args: (&&str, )) -> Self::Output { + self.validate(args.0) + } +} + +impl FnMut<(&&str, )> for PatternValidator<'_> { + extern "rust-call" fn call_mut(&mut self, args: (&&str, )) -> Self::Output { + self.validate(args.0) + } +} + +impl Fn<(&&str, )> for PatternValidator<'_> { + extern "rust-call" fn call(&self, args: (&&str, )) -> 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()) + } +} + +pub fn pattern_vldr_pattern_mismatch_msg(rules: &PatternValidator, xs: &str) -> String { + format!( + "`{:}` does not match pattern `{:}`.", + xs, + &rules.pattern.to_string() + ) +} + +#[cfg(test)] +mod test { + use std::borrow::Cow; + use std::error::Error; + use regex::Regex; + use crate::{ValidateValue}; + use crate::ViolationEnum::PatternMismatch; + + use super::*; + + #[test] + fn test_construction_and_validation() -> Result<(), Box> { + let _rx = Regex::new(r"^\w{2,55}$")?; + + fn on_custom_pattern_mismatch(_: &PatternValidator, _: &str) -> String { + "custom pattern mismatch err message".into() + } + + for (name, instance, passing_value, failing_value, _err_callback) in [ + ("Default", PatternValidatorBuilder::default() + .pattern(Cow::Owned(_rx.clone())) + .build()?, "abc", "!@#)(*", &pattern_vldr_pattern_mismatch_msg), + ("Custom ", PatternValidatorBuilder::default() + .pattern(Cow::Owned(_rx.clone())) + .pattern_mismatch(&on_custom_pattern_mismatch) + .build()?, "abc", "!@#)(*", &on_custom_pattern_mismatch) + ] as [( + &str, + PatternValidator, + &str, + &str, + &PatternViolationCallback + ); 2] { + println!("{}", name); + + // Test as an `Fn*` trait + 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(passing_value), Ok(())); + assert_eq!(instance.validate(failing_value), Err(vec![ + (PatternMismatch, (&instance.pattern_mismatch)(&instance, failing_value)) + ])); + } + + Ok(()) + } +} diff --git a/inputfilter/src/validators/traits.rs b/inputfilter/src/validators/traits.rs new file mode 100644 index 0000000..0e7c21a --- /dev/null +++ b/inputfilter/src/validators/traits.rs @@ -0,0 +1,5 @@ +use crate::{InputValue, ValidationResult}; + +pub trait ValidateValue { + fn validate(&self, value: T) -> ValidationResult; +} 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..d62c196 --- /dev/null +++ b/navigation/README.md @@ -0,0 +1,3 @@ +# Navigation + +TODO. 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..b268547 100644 --- a/navigation/src/navigation.rs +++ b/navigation/src/navigation.rs @@ -1,23 +1,25 @@ // 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 find(&mut self, pred: &'a impl Fn(&'a NavItem) -> bool) -> Option<&'a NavItem>; + 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(Default, Clone, Builder)] pub struct NavItem { pub active: bool, - pub attributes: Option>, + pub attributes: Option>, pub children_only: bool, pub fragment: Option, pub items: Option>, - pub label: String, + pub label: Option, pub order: u64, pub privilege: Option, pub resource: Option, @@ -36,16 +38,26 @@ impl<'a> NavigationItem<'a> for NavItem { fn add(&mut self, item: NavItem) -> isize { self._reevaluate_size = true; - todo!() + + if self.items.is_none() { + self.items = Some(vec![item]); + } else { + 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<&'a NavItem> { - todo!() + 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() } fn size(&mut self) -> isize { diff --git a/src/lib.rs b/src/lib.rs index f4057a9..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::input; -pub use walrs_inputfilter::types; +pub use walrs_inputfilter::validators; +pub use walrs_inputfilter::filters; +pub use walrs_inputfilter::constraints; +pub use walrs_inputfilter::traits;