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}