Skip to main content

cjtoolkit_structured_validator/base/
number_rules.rs

1//! This module contains the `NumberMandatoryRules` and `NumberRangeRules` structs,
2//! which are used to define rules for validating numerical values.
3
4use crate::common::locale::{LocaleData, LocaleMessage, LocaleValue, ValidateErrorCollector};
5use std::fmt::Display;
6use std::sync::Arc;
7
8/// `NumberMandatoryLocale` is a struct representing a type that may be used
9/// to enforce the concept.
10///
11///
12/// # Possible key values:
13/// * `validate-cannot-be-empty`
14pub struct NumberMandatoryLocale;
15
16impl LocaleMessage for NumberMandatoryLocale {
17    fn get_locale_data(&self) -> Arc<LocaleData> {
18        LocaleData::new("validate-cannot-be-empty")
19    }
20}
21
22/// Represents a set of rules determining whether a number field or value is mandatory.
23///
24/// # Fields
25/// - `is_mandatory` (`bool`):
26///   Specifies whether the associated number is mandatory or not.
27///   - `true`: The number is required (mandatory).
28///   - `false`: The number is optional.
29pub struct NumberMandatoryRules {
30    pub is_mandatory: bool,
31}
32
33impl NumberMandatoryRules {
34    /// Checks whether a given subject is valid based on the rules of the current instance
35    /// and collects any validation errors in the provided `ValidateErrorCollector`.
36    ///
37    /// # Type Parameters
38    /// - `T`: A type that implements the `Into<LocaleValue>` trait, representing the value to be checked.
39    ///
40    /// # Parameters
41    /// - `&self`: Immutable reference to the instance containing validation settings (`is_mandatory` in this case).
42    /// - `messages`: A mutable reference to a `ValidateErrorCollector`, used to collect error messages
43    ///   if the validation fails.
44    /// - `subject`: An optional input value of type `T` to be validated. If `None` and the rule `is_mandatory`
45    ///   is `true`, it triggers a validation error.
46    ///
47    /// # Behavior
48    /// - If `is_mandatory` is `true` and the `subject` is `None` (i.e., no value is provided):
49    ///   - The method appends an error message to `messages`, with the description `"Cannot be empty"`
50    ///     and a boxed `NumberMandatoryLocale` as additional context.
51    ///
52    /// # Example
53    /// ```rust
54    /// use cjtoolkit_structured_validator::base::number_rules::NumberMandatoryRules;
55    /// use cjtoolkit_structured_validator::common::locale::ValidateErrorCollector;
56    /// let validator = NumberMandatoryRules { is_mandatory: true };
57    /// let mut errors = ValidateErrorCollector::new();
58    ///
59    /// // Example with a subject as None (will cause validation error)
60    /// validator.check::<f64>(&mut errors, None);
61    /// assert_eq!(errors.len(), 1);
62    ///
63    /// // Example with a valid subject (no validation error)
64    /// validator.check::<f64>(&mut errors, Some(1.0));
65    /// assert_eq!(errors.len(), 1); // No additional errors added.
66    /// ```
67    ///
68    /// # Note
69    /// - Ensure that `ValidateErrorCollector` is properly initialized and passed by mutable reference
70    ///   to capture errors.
71    /// - The subject, when provided, must implement the `Into<LocaleValue>` trait for compatibility.
72    pub fn check<T: Into<LocaleValue>>(
73        &self,
74        messages: &mut ValidateErrorCollector,
75        subject: Option<T>,
76    ) {
77        if self.is_mandatory && subject.is_none() {
78            messages.push((
79                "Cannot be empty".to_string(),
80                Box::new(NumberMandatoryLocale),
81            ));
82        }
83    }
84}
85
86/// An enumeration representing a range of values with localization support.
87///
88/// `NumberRangeLocale` is a generic enum used to define a localized numerical
89/// range. It encapsulates a minimum or maximum value that can be converted into
90/// a locale-specific representation using the `LocaleValue` type.
91///
92/// # Type Parameters
93/// - `T`: A type that implements the traits `Into<LocaleValue>`, `Send`, `Sync`,
94///   and `Clone`. This allows for flexible and thread-safe representation of values
95///   that can be converted into localized formats.
96///
97/// # Variants
98/// - `MinValue(T)`: Represents the minimum localized value for the range.
99/// - `MaxValue(T)`: Represents the maximum localized value for the range.
100///
101pub enum NumberRangeLocale<T: Into<LocaleValue> + Send + Sync + Clone> {
102    /// Represents the minimum localized value for the range.
103    /// # Key
104    /// * `validate-number-min-value`
105    MinValue(T),
106    /// Represents the maximum localized value for the range.
107    /// # Key
108    /// * `validate-number-max-value`
109    MaxValue(T),
110}
111
112impl<T: Into<LocaleValue> + Send + Sync + Clone> LocaleMessage for NumberRangeLocale<T>
113where
114    LocaleValue: From<T>,
115{
116    fn get_locale_data(&self) -> Arc<LocaleData> {
117        use LocaleData as ld;
118        use LocaleValue as lv;
119        match self {
120            Self::MinValue(min) => ld::new_with_vec(
121                "validate-number-min-value",
122                vec![("min".to_string(), lv::from(min.clone()))],
123            ),
124            Self::MaxValue(max) => ld::new_with_vec(
125                "validate-number-max-value",
126                vec![("max".to_string(), lv::from(max.clone()))],
127            ),
128        }
129    }
130}
131
132/// A struct that represents rules for defining a range of numeric values with optional minimum and maximum bounds.
133///
134/// This struct is generic and can work with any type `T` that meets the following trait bounds:
135/// - `Clone`: The type can be cloned.
136/// - `Into<LocaleValue>`: The type can be converted into a `LocaleValue`. This allows for localization support.
137/// - `Default`: The type has a default value.
138/// - `PartialOrd`: The type supports partial ordering, enabling comparisons like less than or greater than.
139/// - `Display`: The type can be formatted as a string for display purposes.
140///
141/// # Fields
142/// - `min` (`Option<T>`): The optional lower bound of the range. If `None`, there is no restriction on the minimum value.
143/// - `max` (`Option<T>`): The optional upper bound of the range. If `None`, there is no restriction on the maximum value.
144///
145pub struct NumberRangeRules<T>
146where
147    T: Clone + Into<LocaleValue> + Default + PartialOrd + Display,
148{
149    pub min: Option<T>,
150    pub max: Option<T>,
151}
152
153impl<T> NumberRangeRules<T>
154where
155    T: Clone + Into<LocaleValue> + Default + PartialOrd + Display,
156{
157    /// Validates a given `subject` against optional minimum and maximum value constraints.
158    ///
159    /// # Parameters
160    ///
161    /// - `&self`: A reference to the current instance of the object containing validation constraints.
162    /// - `messages`: A mutable reference to a `ValidateErrorCollector`, where validation error messages
163    ///   will be stored if the `subject` does not meet the constraints.
164    /// - `subject`: An optional value of type `T` to be validated against the constraints.
165    ///
166    /// # Behavior
167    ///
168    /// - If the `subject` is `Some`:
169    ///     - It checks whether the value is less than the optional minimum value (`self.min`).
170    ///         - If the value is less, an error message is added to `messages` stating that the value
171    ///           must be at least the specified minimum.
172    ///     - It checks whether the value is greater than the optional maximum value (`self.max`).
173    ///         - If the value is greater, an error message is added to `messages` stating that the value
174    ///           must be at most the specified maximum.
175    /// - If the `subject` is `None`, the default value is used during validation (`T::default()`).
176    /// - Does nothing if both `self.min` and `self.max` are `None`.
177    ///
178    /// # Usage
179    ///
180    /// This function is intended to validate numerical ranges or similar constraints. The errors detected
181    /// during validation are collected into the `ValidateErrorCollector` provided in the `messages` parameter.
182    ///
183    /// # Examples
184    ///
185    /// ```
186    /// use cjtoolkit_structured_validator::common::locale::ValidateErrorCollector;
187    /// use cjtoolkit_structured_validator::base::number_rules::NumberRangeRules;
188    /// let mut error_collector = ValidateErrorCollector::new();
189    /// let validator = NumberRangeRules::<usize> {
190    ///     min: Some(10),
191    ///     max: Some(100),
192    /// };
193    ///
194    /// validator.check(&mut error_collector, Some(5));   // Value too small, error is added.
195    /// validator.check(&mut error_collector, Some(105)); // Value too large, error is added.
196    /// validator.check(&mut error_collector, Some(50));  // Valid value, no error.
197    /// ```
198    ///
199    /// # Note
200    ///
201    /// - It is assumed that `T` implements the `Default`, `PartialOrd`, and `Clone` traits.
202    /// - The `ValidateErrorCollector` and `NumberRangeLocale` types are expected to support the operations shown above.
203    ///
204    pub fn check(&self, messages: &mut ValidateErrorCollector, subject: Option<T>) {
205        let is_some = subject.is_some();
206        let subject = subject.unwrap_or_default();
207        if let Some(min) = &self.min {
208            if is_some && subject < *min {
209                messages.push((
210                    format!("Must be at least {}", min),
211                    Box::new(NumberRangeLocale::MinValue(min.clone().into())),
212                ));
213            }
214        }
215        if let Some(max) = &self.max {
216            if is_some && subject > *max {
217                messages.push((
218                    format!("Must be at most {}", max),
219                    Box::new(NumberRangeLocale::MaxValue(max.clone().into())),
220                ));
221            }
222        }
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    mod number_mandatory_rule {
231        use super::*;
232
233        #[test]
234        fn test_empty_value() {
235            let mut messages = ValidateErrorCollector::new();
236            let subject: Option<f64> = None;
237            let rules = NumberMandatoryRules { is_mandatory: true };
238            rules.check(&mut messages, subject);
239            assert_eq!(messages.len(), 1);
240            assert_eq!(messages.0[0].0, "Cannot be empty");
241        }
242
243        #[test]
244        fn test_not_empty_value() {
245            let mut messages = ValidateErrorCollector::new();
246            let subject: Option<f64> = Some(1.0);
247            let rules = NumberMandatoryRules { is_mandatory: true };
248            rules.check(&mut messages, subject);
249            assert_eq!(messages.len(), 0);
250        }
251    }
252
253    mod number_range_rule {
254        use super::*;
255
256        #[test]
257        fn test_invalid_min_value_rule() {
258            let mut messages = ValidateErrorCollector::new();
259            let subject: Option<f64> = Some(1.0);
260            let rules = NumberRangeRules {
261                min: Some(2.0),
262                max: None,
263            };
264            rules.check(&mut messages, subject);
265            assert_eq!(messages.len(), 1);
266            assert_eq!(messages.0[0].0, "Must be at least 2");
267        }
268
269        #[test]
270        fn test_valid_min_value_rule() {
271            let mut messages = ValidateErrorCollector::new();
272            let subject: Option<f64> = Some(2.0);
273            let rules = NumberRangeRules {
274                min: Some(2.0),
275                max: None,
276            };
277            rules.check(&mut messages, subject);
278            assert_eq!(messages.len(), 0);
279        }
280
281        #[test]
282        fn test_max_value_rule() {
283            let mut messages = ValidateErrorCollector::new();
284            let subject: Option<f64> = Some(1.0);
285            let rules = NumberRangeRules {
286                min: None,
287                max: Some(2.0),
288            };
289            rules.check(&mut messages, subject);
290            assert_eq!(messages.len(), 0);
291        }
292
293        #[test]
294        fn test_valid_max_value_rule() {
295            let mut messages = ValidateErrorCollector::new();
296            let subject: Option<f64> = Some(2.1);
297            let rules = NumberRangeRules {
298                min: None,
299                max: Some(2.0),
300            };
301            rules.check(&mut messages, subject);
302            assert_eq!(messages.len(), 1);
303            assert_eq!(messages.0[0].0, "Must be at most 2");
304        }
305    }
306}