type_lib/rules/string.rs
1//! String-content rules.
2//!
3//! Each rule applies to any `S: AsRef<str>`, so it accepts `&str`, `String`, and
4//! borrowed string types alike. They check character content only; combine with
5//! the [length rules](super::length) (via [`And`](crate::combinator::And)) when
6//! you also need a length bound.
7
8use crate::{ValidationError, Validator};
9
10/// Accepts strings whose characters are all ASCII.
11///
12/// An empty string passes (it has no non-ASCII characters). Combine with
13/// [`NonEmpty`](super::NonEmpty) if you also require content.
14///
15/// # Examples
16///
17/// ```rust
18/// use type_lib::rules::Ascii;
19/// use type_lib::Validator;
20///
21/// assert!(Ascii::validate("plain-text_123").is_ok());
22/// assert!(Ascii::validate("café").is_err());
23/// ```
24pub struct Ascii;
25
26impl<S: AsRef<str> + ?Sized> Validator<S> for Ascii {
27 type Error = ValidationError;
28
29 fn validate(value: &S) -> Result<(), Self::Error> {
30 if value.as_ref().is_ascii() {
31 Ok(())
32 } else {
33 Err(ValidationError::new(
34 "ascii",
35 "value must contain only ASCII characters",
36 ))
37 }
38 }
39}
40
41/// Accepts strings whose characters are all alphanumeric.
42///
43/// Uses the Unicode definition of alphanumeric ([`char::is_alphanumeric`]). An
44/// empty string passes vacuously; combine with [`NonEmpty`](super::NonEmpty) to
45/// also require content.
46///
47/// # Examples
48///
49/// ```rust
50/// use type_lib::rules::Alphanumeric;
51/// use type_lib::Validator;
52///
53/// assert!(Alphanumeric::validate("abc123").is_ok());
54/// assert!(Alphanumeric::validate("abc 123").is_err()); // space
55/// assert!(Alphanumeric::validate("user_name").is_err()); // underscore
56/// ```
57pub struct Alphanumeric;
58
59impl<S: AsRef<str> + ?Sized> Validator<S> for Alphanumeric {
60 type Error = ValidationError;
61
62 fn validate(value: &S) -> Result<(), Self::Error> {
63 if value.as_ref().chars().all(char::is_alphanumeric) {
64 Ok(())
65 } else {
66 Err(ValidationError::new(
67 "alphanumeric",
68 "value must contain only alphanumeric characters",
69 ))
70 }
71 }
72}
73
74/// Accepts strings with no leading or trailing whitespace.
75///
76/// Useful for rejecting un-normalized input before it is stored. Whitespace is
77/// defined by [`char::is_whitespace`] via [`str::trim`].
78///
79/// # Examples
80///
81/// ```rust
82/// use type_lib::rules::Trimmed;
83/// use type_lib::Validator;
84///
85/// assert!(Trimmed::validate("clean").is_ok());
86/// assert!(Trimmed::validate(" padded ").is_err());
87/// assert!(Trimmed::validate("trailing\n").is_err());
88/// ```
89pub struct Trimmed;
90
91impl<S: AsRef<str> + ?Sized> Validator<S> for Trimmed {
92 type Error = ValidationError;
93
94 fn validate(value: &S) -> Result<(), Self::Error> {
95 let value = value.as_ref();
96 if value == value.trim() {
97 Ok(())
98 } else {
99 Err(ValidationError::new(
100 "trimmed",
101 "value must not have leading or trailing whitespace",
102 ))
103 }
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 #![allow(clippy::unwrap_used, clippy::expect_used)]
110
111 use super::*;
112
113 #[test]
114 fn ascii_rule() {
115 assert!(Ascii::validate("abc123!").is_ok());
116 assert!(Ascii::validate("").is_ok());
117 assert_eq!(Ascii::validate("é").unwrap_err().code(), "ascii");
118 }
119
120 #[test]
121 fn alphanumeric_rule() {
122 assert!(Alphanumeric::validate("Abc123").is_ok());
123 assert!(Alphanumeric::validate("").is_ok());
124 assert!(Alphanumeric::validate("a-b").is_err());
125 }
126
127 #[test]
128 fn trimmed_rule() {
129 assert!(Trimmed::validate("ok").is_ok());
130 assert!(Trimmed::validate(" leading").is_err());
131 assert!(Trimmed::validate("trailing ").is_err());
132 assert!(Trimmed::validate("inner space ok".trim()).is_ok());
133 }
134}