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}