cjtoolkit_structured_validator/base/
string_rules.rs

1//! This module contains structures and traits for defining rules for validating strings.
2
3use crate::common::locale::{LocaleData, LocaleMessage, LocaleValue, ValidateErrorCollector};
4use crate::common::string_validator::StringValidator;
5use std::sync::Arc;
6
7/// A struct representing a mandatory locale for string processing.
8///
9/// The `StringMandatoryLocale` struct is a placeholder or marker to enforce the use
10/// of a specific locale in processes or systems that require a string to always be
11/// associated with a locale. This struct does not currently carry any data or functionality
12/// on its own but can be used for typing or enforcing constraints within a program.
13///
14/// # Use Case
15/// - Enforcing locale-specific business logic.
16/// - Providing stricter typing in functions or structs requiring locale-based string operations.
17/// # Key
18/// * `validate-cannot-be-empty`
19pub struct StringMandatoryLocale;
20
21impl LocaleMessage for StringMandatoryLocale {
22    fn get_locale_data(&self) -> Arc<LocaleData> {
23        LocaleData::new("validate-cannot-be-empty")
24    }
25}
26
27/// A struct representing rules for mandatory string fields.
28///
29/// This struct is designed to hold the configuration for whether a particular
30/// string field is mandatory or optional. It provides a simple boolean flag
31/// indicating the "mandatory" nature of the string.
32///
33/// # Fields
34///
35/// * `is_mandatory`
36///   - A boolean field that determines whether the string is mandatory.
37///   - When set to `true`, the associated string must be provided.
38///   - When set to `false`, the associated string is optional.
39///
40/// # Traits
41///
42/// * The `Default` trait is implemented for this struct, allowing you to
43///   create a default instance where `is_mandatory` is set to `false`.
44///
45#[derive(Default)]
46pub struct StringMandatoryRules {
47    pub is_mandatory: bool,
48}
49
50impl StringMandatoryRules {
51    /// Validates a string based on the provided `StringValidator` and collects validation errors.
52    ///
53    /// # Parameters
54    /// - `messages`: A mutable reference to a `ValidateErrorCollector` that accumulates validation errors encountered during the check.
55    /// - `subject`: A reference to a `StringValidator` representing the string to be validated.
56    ///
57    /// # Behavior
58    /// - If the `self.is_mandatory` field is `true` and the `subject` is empty, an error message with the text `"Cannot be empty"`
59    ///   is pushed into the `messages` collector along with a locale identifier (`StringMandatoryLocale`).
60    ///
61    /// # Example
62    /// ```
63    /// use cjtoolkit_structured_validator::common::locale::ValidateErrorCollector;
64    /// use cjtoolkit_structured_validator::common::string_validator::StrValidationExtension;
65    /// use cjtoolkit_structured_validator::base::string_rules::StringMandatoryRules;
66    /// let mut messages = ValidateErrorCollector::new();
67    /// let validator = StringMandatoryRules { is_mandatory: true };
68    /// let subject = "".as_string_validator();
69    ///
70    /// validator.check(&mut messages, &subject);
71    ///
72    /// assert_eq!(messages.len(), 1); // If the subject is empty and is_mandatory is true, an error will be collected.
73    /// ```
74    pub fn check(&self, messages: &mut ValidateErrorCollector, subject: &StringValidator) {
75        if self.is_mandatory && subject.is_empty() {
76            messages.push((
77                "Cannot be empty".to_string(),
78                Box::new(StringMandatoryLocale),
79            ));
80        }
81    }
82}
83
84/// An enumeration representing the constraints for string length,
85/// either specifying a minimum length or a maximum length.
86///
87/// # Variants
88///
89/// - `MinLength(usize)`
90///   Specifies the minimum length that a string is allowed to have.
91///   The `usize` represents the minimum number of characters required.
92///
93/// - `MaxLength(usize)`
94///   Specifies the maximum length that a string is allowed to have.
95///   The `usize` represents the maximum number of characters allowed.
96///
97pub enum StringLengthLocale {
98    /// Minimum length constraint.
99    /// # Key
100    /// `validate-min-length`
101    MinLength(usize),
102    /// Maximum length constraint.
103    /// # Key
104    /// `validate-max-length`
105    MaxLength(usize),
106}
107
108impl LocaleMessage for StringLengthLocale {
109    fn get_locale_data(&self) -> Arc<LocaleData> {
110        use LocaleData as ld;
111        use LocaleValue as lv;
112        match self {
113            Self::MinLength(min_length) => ld::new_with_vec(
114                "validate-min-length",
115                vec![("min".to_string(), lv::from(*min_length))],
116            ),
117            Self::MaxLength(max_length) => ld::new_with_vec(
118                "validate-max-length",
119                vec![("max".to_string(), lv::from(*max_length))],
120            ),
121        }
122    }
123}
124
125/// A structure representing rules for validating the length of a string.
126///
127/// This struct allows specifying optional minimum and maximum length constraints
128/// for a string. Both constraints are optional, meaning one or both of them
129/// can be unset depending on the requirements.
130///
131/// # Fields
132/// * `min_length` - An optional minimum length constraint for the string.
133///   If set, the string must have at least this many characters to pass validation.
134/// * `max_length` - An optional maximum length constraint for the string.
135///   If set, the string must not exceed this many characters to pass validation.
136///
137/// # Defaults
138/// When derived using `Default`, both `min_length` and `max_length` will be set to `None`.
139///
140#[derive(Default)]
141pub struct StringLengthRules {
142    pub min_length: Option<usize>,
143    pub max_length: Option<usize>,
144}
145
146impl StringLengthRules {
147    /// Validates the length of a given string using the specified criteria for minimum and maximum
148    /// lengths. If the string does not meet the specified length constraints, an error message is added
149    /// to the validation error collector.
150    ///
151    /// # Parameters
152    ///
153    /// * `messages` - A mutable reference to a `ValidateErrorCollector` for storing validation error
154    ///   messages if any constraints are violated.
155    /// * `subject` - A reference to a `StringValidator` that provides the string to validate against
156    ///   the defined length rules.
157    ///
158    /// # Behavior
159    ///
160    /// 1. If a minimum length (`min_length`) is specified via `self` and the `subject` string's
161    ///    grapheme count is less than the minimum; an error message is added to the `messages` collector
162    ///    indicating that the string must be at least the specified number of characters.
163    /// 2. If a maximum length (`max_length`) is specified via `self` and the `subject` string's
164    ///    grapheme count exceeds the maximum, an error message is added to the `messages` collector
165    ///    indicating that the string must be at most the specified number of characters.
166    ///
167    /// # Notes
168    ///
169    /// This function assumes the `count_graphemes` method is available on the `subject` to properly count
170    /// grapheme clusters, ensuring correctness when dealing with multibyte characters or special Unicode
171    /// characters.
172    ///
173    /// # Examples
174    ///
175    /// ```rust
176    /// use cjtoolkit_structured_validator::common::locale::ValidateErrorCollector;
177    /// use cjtoolkit_structured_validator::common::string_validator::StrValidationExtension;
178    /// use cjtoolkit_structured_validator::base::string_rules::StringLengthRules;
179    /// let mut messages = ValidateErrorCollector::new();
180    /// let validator = "example".as_string_validator();
181    /// let criteria = StringLengthRules { min_length: Some(5), max_length: Some(10) };
182    ///
183    /// criteria.check(&mut messages, &validator);
184    ///
185    /// assert!(messages.is_empty()); // The string "example" satisfies the length constraints.
186    /// ```
187    pub fn check(&self, messages: &mut ValidateErrorCollector, subject: &StringValidator) {
188        if let Some(min_length) = self.min_length {
189            if subject.count_graphemes() < min_length {
190                messages.push((
191                    format!("Must be at least {} characters", min_length),
192                    Box::new(StringLengthLocale::MinLength(min_length)),
193                ));
194            }
195        }
196        if let Some(max_length) = self.max_length {
197            if subject.count_graphemes() > max_length {
198                messages.push((
199                    format!("Must be at most {} characters", max_length),
200                    Box::new(StringLengthLocale::MaxLength(max_length)),
201                ));
202            }
203        }
204    }
205}
206
207/// An enumeration defining various string constraints or requirements based on the presence of
208/// special characters, case sensitivity, or digits.
209///
210/// This enum can be used to specify which kind of validation or rules should be applied
211/// to a string across different locales, ensuring compliance with specific character requirements.
212///
213/// # Variants
214///
215/// - `MustHaveSpecialChars`
216///   Enforces that the string must contain at least one special character
217///   (e.g., symbols like `@`, `#`, `$`, etc.).
218///
219/// - `MustHaveUppercaseAndLowercase`
220///   Enforces that the string must contain at least one uppercase and one lowercase character.
221///
222///
223/// - `MustHaveUppercase`
224///   Enforces that the string must contain at least one uppercase character.
225///
226/// - `MustHaveLowercase`
227///   Enforces that the string must contain at least one lowercase character.
228///
229/// - `MustHaveDigit`
230///   Enforces that the string must contain at least one numeric digit (0-9).
231///
232pub enum StringSpecialCharLocale {
233    /// Must have special characters.
234    /// # Key
235    /// `validate-must-have-special-chars`
236    MustHaveSpecialChars,
237    /// Must have uppercase and lowercase characters.
238    /// # Key
239    /// `validate-must-have-uppercase-and-lowercase`
240    MustHaveUppercaseAndLowercase,
241    /// Must have uppercase characters.
242    /// # Key
243    /// `validate-must-have-uppercase`
244    MustHaveUppercase,
245    /// Must have lowercase characters.
246    /// # Key
247    /// `validate-must-have-lowercase`
248    MustHaveLowercase,
249    /// Must have digits.
250    /// # Key
251    /// `validate-must-have-digit`
252    MustHaveDigit,
253}
254
255impl LocaleMessage for StringSpecialCharLocale {
256    fn get_locale_data(&self) -> Arc<LocaleData> {
257        use LocaleData as ld;
258        match self {
259            Self::MustHaveSpecialChars => ld::new("validate-must-have-special-chars"),
260            Self::MustHaveUppercaseAndLowercase => {
261                ld::new("validate-must-have-uppercase-and-lowercase")
262            }
263            Self::MustHaveUppercase => ld::new("validate-must-have-uppercase"),
264            Self::MustHaveLowercase => ld::new("validate-must-have-lowercase"),
265            Self::MustHaveDigit => ld::new("validate-must-have-digit"),
266        }
267    }
268}
269
270/// A structure that defines rules for validating the presence
271/// of characters in a string. This can be used to enforce certain validation criteria
272/// for strings containing uppercase letters, lowercase letters, special characters,
273/// and numeric digits.
274///
275/// # Fields
276///
277/// * `must_have_uppercase` - A boolean flag indicating whether the string must contain
278///   at least one uppercase letter (`true` if required, `false` otherwise).
279///
280/// * `must_have_lowercase` - A boolean flag indicating whether the string must contain
281///   at least one lowercase letter (`true` if required, `false` otherwise).
282///
283/// * `must_have_special_chars` - A boolean flag indicating whether the string must contain
284///   at least one special character (e.g., `!`, `@`, `#`, etc.) (`true` if required, `false` otherwise).
285///
286/// * `must_have_digit` - A boolean flag indicating whether the string must contain
287///   at least one numeric digit (`true` if required, `false` otherwise).
288///
289/// # Default Implementation
290///
291/// By default, all fields are set to `false`, meaning no specific character requirements
292/// will be enforced unless explicitly configured.
293///
294/// This structure can be used in validation logic where customizable character rules
295/// are required, such as password or input string checks.
296///
297#[derive(Default)]
298pub struct StringSpecialCharRules {
299    pub must_have_uppercase: bool,
300    pub must_have_lowercase: bool,
301    pub must_have_special_chars: bool,
302    pub must_have_digit: bool,
303}
304
305impl StringSpecialCharRules {
306    /// Validates a string based on multiple constraints such as the presence of special characters,
307    /// uppercase and lowercase letters, and digits. If any constraint is not met, an error
308    /// message along with the corresponding error locale is added to the provided `ValidateErrorCollector`.
309    ///
310    /// # Parameters
311    ///
312    /// * `messages`: A mutable reference to a `ValidateErrorCollector`, which collects validation errors
313    ///   encountered during the checks.
314    /// * `subject`: A reference to a `StringValidator` object, which provides methods to check
315    ///   various string properties based on constraints.
316    ///
317    /// # Behavior
318    ///
319    /// - If `must_have_special_chars` is true, the method checks whether the subject contains
320    ///   at least one special character. If not, an error is added to `messages`.
321    /// - If both `must_have_uppercase` and `must_have_lowercase` are true, the method verifies
322    ///   that the subject has at least one uppercase and one lowercase letter. An error is added
323    ///   if this condition is not met.
324    /// - If only `must_have_uppercase` is true, the method ensures the presence of at least one
325    ///   uppercase letter in the subject, adding an error if the condition fails.
326    /// - If only `must_have_lowercase` is true, the method ensures the presence of at least one
327    ///   lowercase letter in the subject, adding an error if the condition fails.
328    /// - If `must_have_digit` is true, the method checks that the subject contains at least one
329    ///   numeric digit. If not, an error is added to `messages`.
330    ///
331    /// # Error Handling
332    ///
333    /// Each validation failure results in an entry being added to the `ValidateErrorCollector`,
334    /// consisting of an error message string and a corresponding locale represented by `StringSpecialCharLocale`.
335    ///
336    /// # Example
337    ///
338    /// ```rust
339    /// use cjtoolkit_structured_validator::common::locale::ValidateErrorCollector;
340    /// use cjtoolkit_structured_validator::common::string_validator::StrValidationExtension;
341    /// use cjtoolkit_structured_validator::base::string_rules::StringSpecialCharRules;
342    /// let mut errors = ValidateErrorCollector::new();
343    /// let validator = "Password123!".as_string_validator();
344    /// let rules = StringSpecialCharRules {
345    ///     must_have_special_chars: true,
346    ///     must_have_uppercase: true,
347    ///     must_have_lowercase: true,
348    ///     must_have_digit: true,
349    /// };
350    ///
351    /// rules.check(&mut errors, &validator);
352    ///
353    /// if errors.is_empty() {
354    ///     println!("Validation passed!");
355    /// } else {
356    ///     println!("Validation failed with errors");
357    /// }
358    /// ```
359    pub fn check(&self, messages: &mut ValidateErrorCollector, subject: &StringValidator) {
360        if self.must_have_special_chars {
361            if !subject.has_special_chars() {
362                messages.push((
363                    "Must contain at least one special character".to_string(),
364                    Box::new(StringSpecialCharLocale::MustHaveSpecialChars),
365                ));
366            }
367        }
368        if self.must_have_uppercase && self.must_have_lowercase {
369            if !subject.has_ascii_uppercase_and_lowercase() {
370                messages.push((
371                    "Must contain at least one uppercase and lowercase letter".to_string(),
372                    Box::new(StringSpecialCharLocale::MustHaveUppercaseAndLowercase),
373                ));
374            }
375        } else {
376            if self.must_have_uppercase {
377                if !subject.has_ascii_uppercase() {
378                    messages.push((
379                        "Must contain at least one uppercase letter".to_string(),
380                        Box::new(StringSpecialCharLocale::MustHaveUppercase),
381                    ));
382                }
383            }
384            if self.must_have_lowercase {
385                if !subject.has_ascii_lowercase() {
386                    messages.push((
387                        "Must contain at least one lowercase letter".to_string(),
388                        Box::new(StringSpecialCharLocale::MustHaveLowercase),
389                    ));
390                }
391            }
392        }
393        if self.must_have_digit {
394            if !subject.has_ascii_digit() {
395                messages.push((
396                    "Must contain at least one digit".to_string(),
397                    Box::new(StringSpecialCharLocale::MustHaveDigit),
398                ));
399            }
400        }
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use crate::common::string_validator::StrValidationExtension;
408
409    mod string_mandatory_rule {
410        use super::*;
411
412        #[test]
413        fn test_string_mandatory_rule_check_empty_string() {
414            let mut messages = ValidateErrorCollector::new();
415            let subject = "".as_string_validator();
416            let rule = StringMandatoryRules { is_mandatory: true };
417            rule.check(&mut messages, &subject);
418            assert_eq!(messages.len(), 1);
419            assert_eq!(messages.0[0].0, "Cannot be empty");
420        }
421
422        #[test]
423        fn test_string_mandatory_rule_check_not_empty_string() {
424            let mut messages = ValidateErrorCollector::new();
425            let subject = "Hello".as_string_validator();
426            let rule = StringMandatoryRules { is_mandatory: true };
427            rule.check(&mut messages, &subject);
428            assert_eq!(messages.len(), 0);
429        }
430    }
431
432    mod string_length_rule {
433        use super::*;
434
435        #[test]
436        fn test_string_length_rule_check_empty_string() {
437            let mut messages = ValidateErrorCollector::new();
438            let subject = "".as_string_validator();
439            let rule = StringLengthRules {
440                min_length: Some(5),
441                max_length: Some(10),
442            };
443            rule.check(&mut messages, &subject);
444            assert_eq!(messages.len(), 1);
445            assert_eq!(messages.0[0].0, "Must be at least 5 characters");
446        }
447
448        #[test]
449        fn test_string_length_rule_check_too_long_string() {
450            let mut messages = ValidateErrorCollector::new();
451            let subject = "Hello".as_string_validator();
452            let rule = StringLengthRules {
453                min_length: Some(2),
454                max_length: Some(4),
455            };
456            rule.check(&mut messages, &subject);
457            assert_eq!(messages.len(), 1);
458            assert_eq!(messages.0[0].0, "Must be at most 4 characters");
459        }
460    }
461
462    mod string_special_char_rule {
463        use super::*;
464
465        #[test]
466        fn test_string_special_char_rule_check_empty_string() {
467            let mut messages = ValidateErrorCollector::new();
468            let subject = "".as_string_validator();
469            let rule = StringSpecialCharRules {
470                must_have_uppercase: true,
471                must_have_lowercase: true,
472                must_have_special_chars: true,
473                must_have_digit: true,
474            };
475            rule.check(&mut messages, &subject);
476            assert_eq!(messages.len(), 3);
477            assert_eq!(
478                messages.0[0].0,
479                "Must contain at least one special character"
480            );
481            assert_eq!(
482                messages.0[1].0,
483                "Must contain at least one uppercase and lowercase letter"
484            );
485            assert_eq!(messages.0[2].0, "Must contain at least one digit");
486        }
487
488        #[test]
489        fn test_string_special_char_rule_check_not_empty_string() {
490            let mut messages = ValidateErrorCollector::new();
491            let subject = "Hello".as_string_validator();
492            let rule = StringSpecialCharRules {
493                must_have_uppercase: true,
494                must_have_lowercase: true,
495                must_have_special_chars: true,
496                must_have_digit: true,
497            };
498            rule.check(&mut messages, &subject);
499            assert_eq!(messages.len(), 2);
500            assert_eq!(
501                messages.0[0].0,
502                "Must contain at least one special character"
503            );
504            assert_eq!(messages.0[1].0, "Must contain at least one digit");
505        }
506
507        #[test]
508        fn test_string_special_char_rule_check_not_empty_string_with_uppercase_and_lowercase_and_symbol()
509         {
510            let mut messages = ValidateErrorCollector::new();
511            let subject = "Hello@".as_string_validator();
512            let rule = StringSpecialCharRules {
513                must_have_uppercase: true,
514                must_have_lowercase: true,
515                must_have_special_chars: true,
516                must_have_digit: true,
517            };
518            rule.check(&mut messages, &subject);
519            assert_eq!(messages.len(), 1);
520            assert_eq!(messages.0[0].0, "Must contain at least one digit");
521        }
522
523        #[test]
524        fn test_string_special_char_rule_check_not_empty_string_with_uppercase_and_lowercase_and_digit()
525         {
526            let mut messages = ValidateErrorCollector::new();
527            let subject = "Hello1".as_string_validator();
528            let rule = StringSpecialCharRules {
529                must_have_uppercase: true,
530                must_have_lowercase: true,
531                must_have_special_chars: true,
532                must_have_digit: true,
533            };
534            rule.check(&mut messages, &subject);
535            assert_eq!(messages.len(), 1);
536            assert_eq!(
537                messages.0[0].0,
538                "Must contain at least one special character"
539            );
540        }
541
542        #[test]
543        fn test_string_special_char_rule_check_not_empty_string_with_uppercase_and_lowercase_digit_and_symbol()
544         {
545            let mut messages = ValidateErrorCollector::new();
546            let subject = "Hello1@".as_string_validator();
547            let rule = StringSpecialCharRules {
548                must_have_uppercase: true,
549                must_have_lowercase: true,
550                must_have_special_chars: true,
551                must_have_digit: true,
552            };
553            rule.check(&mut messages, &subject);
554            assert_eq!(messages.len(), 0);
555        }
556    }
557}