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