cjtoolkit-structured-validator 0.5.4

A library for validating structured data.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
//! This module contains structures and traits for working with usernames.

use crate::base::string_rules::{StringLengthRules, StringMandatoryRules};
use crate::common::locale::{
    LocaleData, LocaleMessage, ValidateErrorCollector, ValidateErrorStore,
};
use crate::common::string_validator::{StrValidationExtension, StringValidator};
use crate::common::validation_check::ValidationCheck;
use std::sync::Arc;
use thiserror::Error;

/// Struct representing the rules and constraints applied to a username.
///
/// This struct defines attributes to control whether a username is required
/// and imposes optional length restrictions.
///
/// # Fields
/// - `is_mandatory`
///   A boolean flag indicating if the username is mandatory.
///   If `true`, a username must be provided; if `false`, it is optional.
///
/// - `min_length`
///   An optional `usize` specifying the minimum allowable length for the username.
///   If `Some(value)`, the username must be at least `value` characters long.
///   If `None`, there is no minimum length restriction.
///
/// - `max_length`
///   An optional `usize` specifying the maximum allowable length for the username.
///   If `Some(value)`, the username must be at most `value` characters long.
///   If `None`, there is no maximum length restriction.
///
/// This example specifies a username requirement that is mandatory, with a
/// minimum of 3 characters and a maximum of 16 characters.
pub struct UsernameRules {
    pub is_mandatory: bool,
    pub min_length: Option<usize>,
    pub max_length: Option<usize>,
}

impl Default for UsernameRules {
    fn default() -> Self {
        Self {
            is_mandatory: true,
            min_length: Some(5),
            max_length: Some(30),
        }
    }
}

impl Into<(StringMandatoryRules, StringLengthRules)> for &UsernameRules {
    fn into(self) -> (StringMandatoryRules, StringLengthRules) {
        (
            StringMandatoryRules {
                is_mandatory: self.is_mandatory,
            },
            StringLengthRules {
                min_length: self.min_length,
                max_length: self.max_length,
            },
        )
    }
}

impl UsernameRules {
    fn rules(&self) -> (StringMandatoryRules, StringLengthRules) {
        self.into()
    }

    fn check(
        &self,
        messages: &mut ValidateErrorCollector,
        subject: &StringValidator,
        is_none: bool,
    ) {
        if !self.is_mandatory && is_none {
            return;
        }
        let (mandatory_rule, length_rule) = self.rules();
        mandatory_rule.check(messages, subject);
        if !messages.is_empty() {
            return;
        }
        length_rule.check(messages, subject);
    }
}

///
/// Represents an error encountered during the validation of a username.
///
/// This struct is used to encapsulate validation errors specific to usernames
/// and provides a mechanism to store and manage multiple validation error details.
///
/// # Attributes
/// - `ValidateErrorStore`: A collection or storage that contains detailed information
///   about the specific validation errors encountered.
///
/// # Derives
/// - `Debug`: Enables formatting of the `UsernameError` for debugging purposes.
/// - `Error`: Implements the `std::error::Error` trait for compatibility with Rust's error handling conventions.
/// - `PartialEq`: Enables comparison between `UsernameError` instances for equality.
/// - `Clone`: Allows cloning of `UsernameError` instances.
/// - `Default`: Provides a default value for `UsernameError`, initializing an empty `ValidateErrorStore`.
///
/// # Error Message
/// The default error message for this type is: `"Username Validation Error"`.
///
/// # Usage
/// This struct is typically used internally in validation logic and may be returned
/// when a username fails to meet the specified validation criteria.
#[derive(Debug, Error, PartialEq, Clone, Default)]
#[error("Username Validation Error")]
pub struct UsernameError(pub ValidateErrorStore);

impl ValidationCheck for UsernameError {
    fn validate_new(messages: ValidateErrorStore) -> Self {
        Self(messages)
    }
}

impl Into<ValidateErrorStore> for &UsernameError {
    fn into(self) -> ValidateErrorStore {
        self.0.clone()
    }
}

/// A struct that represents a username with additional metadata.
///
/// The `Username` struct is a tuple struct consisting of:
/// - A `String` representing the username itself.
/// - A `bool` indicating additional information about the username, none if `true`, otherwise `false`.
///
/// # Traits Implemented
/// - `Debug`: Enables formatting the `Username` struct for debugging purposes.
/// - `PartialEq`: Allows for equality comparison between `Username` instances.
/// - `Clone`: Provides the ability to create duplicate instances of `Username`.
#[derive(Debug, PartialEq, Clone)]
pub struct Username(String, bool);

#[cfg(any(feature = "allow-default-value", test))]
impl Default for Username {
    fn default() -> Self {
        Self(String::new(), true)
    }
}

/// A trait that defines a method to check if a provided username is already taken.
///
/// This trait can be implemented for types that manage or validate usernames, allowing them to
/// provide functionality to check if a given username exists (i.e., is already in use).
pub trait IsUsernameTaken {
    fn is_username_taken(&self, username: &str) -> bool;
}

/// This trait defines an asynchronous method to check if a given username is already taken.
///
/// # Required Method
///
/// - `is_username_taken_async`: Takes a reference to a username (`&str`) and returns
///   a future that resolves to a `bool`, indicating whether the username is already taken.
///
/// # Parameters
///
/// - `self`: The implementor object of the trait.
/// - `username`: A string slice that contains the username to check.
///
/// # Returns
///
/// This method returns an `impl Future` with an output of `bool`. When awaited, this future
/// will resolve to:
/// - `true`: If the username is already in use.
/// - `false`: If the username is available.
pub trait IsUsernameTakenAsync {
    fn is_username_taken_async(&self, username: &str) -> impl Future<Output = bool>;
}

/// A struct representing the locale or message type for the "username taken" error.
///
/// This struct can be used as part of an error handling system or localization framework
/// to represent scenarios where the provided username is already in use.
///
/// # Key
/// `validate-username-taken`
pub struct UsernameTakenLocale;

impl LocaleMessage for UsernameTakenLocale {
    fn get_locale_data(&self) -> Arc<LocaleData> {
        LocaleData::new("validate-username-taken")
    }
}

impl Username {
    /// Parses and validates a custom username string based on predefined rules.
    ///
    /// # Parameters
    /// - `s`: An `Option<&str>` representing the input username string to be parsed.
    ///        If `None`, a default empty string will be used.
    /// - `rules`: A `UsernameRules` object which defines the validation rules to
    ///            enforce on the provided username.
    ///
    /// # Returns
    /// - `Result<Self, UsernameError>`: If the username passes all validation rules, it
    ///   returns an instance of the implementing struct wrapped in `Ok`. If validation fails,
    ///   returns a `UsernameError` wrapped in `Err`.
    ///
    /// # Workflow
    /// 1. Checks if the input username `s` is `None` and assigns a default value (empty string) if true.
    /// 2. Converts the input string into a proper validation subject (likely for easier validation handling).
    /// 3. Initializes a `ValidateErrorCollector` to aggregate any rule validation errors that occur.
    /// 4. Applies the specified rules to check the username, capturing errors if validation fails.
    /// 5. Checks the collected errors using `UsernameError::validate_check`. If errors exist, an appropriate
    ///    `UsernameError` is returned.
    /// 6. Constructs and returns the implementing struct containing the parsed string and its `is_none` status.
    ///
    /// # Errors
    /// - Returns a `UsernameError` if validation fails, providing detailed information on why the parsing or checks failed.
    ///
    /// # Examples
    /// ```rust
    /// use cjtoolkit_structured_validator::types::username::{UsernameRules, Username};
    /// let rules = UsernameRules::default();
    /// let result = Username::parse_custom(Some("validuser123"), rules);
    /// assert!(result.is_ok());
    ///
    /// let rules = UsernameRules::default();
    /// let result = Username::parse_custom(Some("inv"), rules);
    /// assert!(result.is_err());
    /// ```
    pub fn parse_custom(s: Option<&str>, rules: UsernameRules) -> Result<Self, UsernameError> {
        let is_none = s.is_none();
        let s = s.unwrap_or_default();
        let subject = s.as_string_validator();
        let mut messages = ValidateErrorCollector::new();
        rules.check(&mut messages, &subject, is_none);
        UsernameError::validate_check(messages)?;
        Ok(Self(s.to_string(), is_none))
    }

    /// Parses a given string slice (`Option<&str>`) into a `Self` instance using the default username rules.
    ///
    /// # Arguments
    /// - `s`: An optional string slice (`Option<&str>`) representing the username to be parsed.
    ///        If `None`, parsing will fail with an appropriate error.
    ///
    /// # Returns
    /// - `Ok(Self)`: If the provided string slice satisfies the default username rules.
    /// - `Err(UsernameError)`: If the parsing fails, for instance, due to the input violating username rules or being `None`.
    ///
    /// # Behavior
    /// This function relies on the `parse_custom` method to perform the parsing operation,
    /// delegating it with `UsernameRules::default()` which represents the default validation rules.
    ///
    /// # Examples
    /// ```
    /// use cjtoolkit_structured_validator::types::username::Username;
    /// let username = Some("valid_username");
    /// let parsed = Username::parse(username);
    /// assert!(parsed.is_ok());
    ///
    /// let invalid_username = Some("inv");
    /// let parsed = Username::parse(invalid_username);
    /// assert!(parsed.is_err());
    /// ```
    ///
    /// # Errors
    /// Return a `UsernameError` if:
    /// - The provided input is malformed or invalid, according to the default rules.
    /// - The input is `None` and cannot be processed.
    ///
    /// # See Also
    /// Refer to `parse_custom` if you need to parse with custom rules.
    pub fn parse(s: Option<&str>) -> Result<Self, UsernameError> {
        Self::parse_custom(s, UsernameRules::default())
    }

    /// Checks whether the username represented by the current instance is already taken.
    ///
    /// This method relies on an external service implementing the `IsUsernameTaken` trait
    /// to determine if the username is already registered or in use. If the username is
    /// taken, an error message is added to the validation error collector, and an error is
    /// returned. If the username is not taken, the method returns `Ok` with the current instance.
    ///
    /// # Type Parameters
    /// * `T` - A type that implements the `IsUsernameTaken` trait. This is used to query
    ///         whether the username is taken or not.
    ///
    /// # Parameters
    /// * `service` - A reference to an object implementing the `IsUsernameTaken` trait,
    ///               which is used for checking username availability.
    ///
    /// # Returns
    /// * `Ok(Self)` - If the username is successfully validated and is not taken.
    /// * `Err(UsernameError)` - If the username is already taken or if there are validation
    ///                          issues.
    ///
    /// # Errors
    /// * Returns a `UsernameError` if the username is already taken, with a localized message
    ///   indicating the issue. Validation failures are captured and reported through the error.
    ///
    /// # Implementation Details
    /// This method uses a `ValidateErrorCollector` to gather error messages. If the `is_username_taken`
    /// method on the provided service returns `true`, it adds a localized error message indicating
    /// that the username is already taken. The `UsernameError::validate_check` function is then
    /// called to process the collected errors and return a result.
    pub fn check_username_taken<T: IsUsernameTaken>(
        &self,
        service: &T,
    ) -> Result<Self, UsernameError> {
        let mut messages = ValidateErrorCollector::new();

        service.is_username_taken(self.as_str()).then(|| {
            messages.push(("Already taken".to_string(), Box::new(UsernameTakenLocale)));
        });

        UsernameError::validate_check(messages)?;
        Ok(self.clone())
    }

    /// Asynchronously checks if the username is already taken using the provided service and validates the result.
    ///
    /// # Arguments
    ///
    /// * `service` - A reference to a type that implements the `IsUsernameTakenAsync` trait. This service is used to
    ///   determine if the username is already taken.
    ///
    /// # Returns
    ///
    /// * `Ok(Self)` - Returns a clone of the current instance (`Self`) if the username passes validation (not taken).
    /// * `Err(UsernameError)` - Returns an error of type `UsernameError` if the username is already taken or a validation error occurs.
    ///
    /// # Errors
    ///
    /// * Returns a `UsernameError` if the username is determined to be already taken by the `service`.
    /// * May return a `UsernameError` if one or more validation rules fail during the check.
    ///
    /// # Type Parameters
    ///
    /// * `T` - A type that implements the `IsUsernameTakenAsync` trait, which defines the asynchronous method
    ///   `is_username_taken_async` used for checking the username's availability.
    ///
    /// # Notes
    ///
    /// * The function uses an internal `ValidateErrorCollector` to collect validation errors.
    /// * This function internally calls `is_username_taken_async` on the `service` and expects an asynchronous boolean result.
    ///   If the result is `true`, it registers an error indicating the username is already taken.
    /// * Any validation error, such as the username being taken, will be propagated as a `UsernameError`.
    ///
    /// # Implementation Details
    ///
    /// * The function uses `ValidateErrorCollector` to aggregate errors.
    /// * If `is_username_taken_async` resolves to `true`, a localized error message ("Already taken") is pushed into the
    ///   error collector along with a reference to `UsernameTakenLocale`.
    /// * The `UsernameError::validate_check(messages)` call ensures that collected errors, if any, are validated and returned,
    ///   halting further execution if errors are present.
    pub async fn check_username_taken_async<T: IsUsernameTakenAsync>(
        &self,
        service: &T,
    ) -> Result<Self, UsernameError> {
        let mut messages = ValidateErrorCollector::new();

        service
            .is_username_taken_async(self.as_str())
            .await
            .then(|| {
                messages.push(("Already taken".to_string(), Box::new(UsernameTakenLocale)));
            });

        UsernameError::validate_check(messages)?;
        Ok(self.clone())
    }

    /// Returns the string slice representation of the current object.
    ///
    /// # Returns
    ///
    /// A string slice (`&str`) that refers to the internal string data.
    ///
    /// This method borrows from the underlying string data, meaning no
    /// additional allocations or copies are made.
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Converts the `Username` wrapper into an `Option<Username>` type.
    ///
    /// If the internal boolean flag (`self.1`) is `true`, it returns `None`.
    /// Otherwise, it wraps the `Username` instance in a `Some` and returns it.
    ///
    /// # Returns
    ///
    /// * `None` - when the internal boolean flag is `true`.
    /// * `Some(Username)` - when the internal boolean flag is `false`.
    pub fn into_option(self) -> Option<Username> {
        if self.1 { None } else { Some(self) }
    }
}

impl Into<String> for &Username {
    fn into(self) -> String {
        self.as_str().to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct FakeUsernameCheckService(String);

    impl IsUsernameTaken for FakeUsernameCheckService {
        fn is_username_taken(&self, username: &str) -> bool {
            username == self.0.as_str()
        }
    }

    impl IsUsernameTakenAsync for FakeUsernameCheckService {
        async fn is_username_taken_async(&self, username: &str) -> bool {
            username == self.0.as_str()
        }
    }

    #[test]
    fn username_is_taken() {
        let username_result = Username("taken".to_string(), false);

        assert!(
            username_result
                .check_username_taken(&FakeUsernameCheckService("taken".to_string()))
                .is_err()
        )
    }

    #[test]
    fn username_is_not_taken() {
        let username_result = Username("not_taken".to_string(), false);

        assert!(
            username_result
                .check_username_taken(&FakeUsernameCheckService("taken".to_string()))
                .is_ok()
        )
    }

    #[tokio::test]
    async fn username_is_taken_async() {
        let username_result = Username("taken".to_string(), false);

        assert!(
            username_result
                .check_username_taken_async(&FakeUsernameCheckService("taken".to_string()))
                .await
                .is_err()
        )
    }

    #[tokio::test]
    async fn username_is_not_taken_async() {
        let username_result = Username("not_taken".to_string(), false);

        assert!(
            username_result
                .check_username_taken_async(&FakeUsernameCheckService("taken".to_string()))
                .await
                .is_ok()
        )
    }
}