type_lib/combinator.rs
1//! Combinators for composing [`Validator`]s.
2//!
3//! [`And`], [`Or`], and [`Not`] build compound rules from simpler ones, entirely
4//! at the type level. Because they are themselves [`Validator`]s, they nest:
5//! `And<A, Or<B, C>>` is a valid rule.
6//!
7//! `And` and `Or` require their two sub-rules to share one error type, which is
8//! the case for every [built-in rule](crate::rules) (all report
9//! [`ValidationError`]). `Not` works with any sub-rule and reports its own
10//! [`ValidationError`].
11//!
12//! # Examples
13//!
14//! ```rust
15//! use type_lib::combinator::And;
16//! use type_lib::rules::{Ascii, NonEmpty};
17//! use type_lib::Refined;
18//!
19//! // A token: non-empty and ASCII-only.
20//! type Token<'a> = Refined<&'a str, And<NonEmpty, Ascii>>;
21//!
22//! assert!(Token::new("abc123").is_ok());
23//! assert!(Token::new("").is_err()); // fails NonEmpty
24//! assert!(Token::new("café").is_err()); // fails Ascii
25//! ```
26
27use core::marker::PhantomData;
28
29use crate::{ValidationError, Validator};
30
31/// Passes only when **both** sub-rules `A` and `B` pass.
32///
33/// `A` is checked first; if it fails its error is returned and `B` is not run.
34/// Both sub-rules must share the same [`Validator::Error`] type.
35///
36/// # Examples
37///
38/// ```rust
39/// use type_lib::combinator::And;
40/// use type_lib::rules::{MaxLen, NonEmpty};
41/// use type_lib::Validator;
42///
43/// type ShortNonEmpty = And<NonEmpty, MaxLen<8>>;
44///
45/// assert!(ShortNonEmpty::validate("ok").is_ok());
46/// assert!(ShortNonEmpty::validate("").is_err()); // NonEmpty fails first
47/// assert!(ShortNonEmpty::validate("way too long").is_err()); // MaxLen fails
48/// ```
49pub struct And<A, B>(PhantomData<fn() -> (A, B)>);
50
51impl<T, A, B, E> Validator<T> for And<A, B>
52where
53 T: ?Sized,
54 A: Validator<T, Error = E>,
55 B: Validator<T, Error = E>,
56{
57 type Error = E;
58
59 fn validate(value: &T) -> Result<(), Self::Error> {
60 A::validate(value)?;
61 B::validate(value)
62 }
63}
64
65/// Passes when **either** sub-rule `A` or `B` passes.
66///
67/// `A` is checked first; if it passes, `B` is not run. If `A` fails, the result
68/// of `B` is returned — so when both fail, the error is `B`'s. Both sub-rules
69/// must share the same [`Validator::Error`] type.
70///
71/// # Examples
72///
73/// ```rust
74/// use type_lib::combinator::Or;
75/// use type_lib::rules::{Alphanumeric, Trimmed};
76/// use type_lib::Validator;
77///
78/// type AlnumOrTrimmed = Or<Alphanumeric, Trimmed>;
79///
80/// assert!(AlnumOrTrimmed::validate("abc123").is_ok()); // alphanumeric
81/// assert!(AlnumOrTrimmed::validate("a b c").is_ok()); // trimmed
82/// assert!(AlnumOrTrimmed::validate(" oops ").is_err()); // neither
83/// ```
84pub struct Or<A, B>(PhantomData<fn() -> (A, B)>);
85
86impl<T, A, B, E> Validator<T> for Or<A, B>
87where
88 T: ?Sized,
89 A: Validator<T, Error = E>,
90 B: Validator<T, Error = E>,
91{
92 type Error = E;
93
94 fn validate(value: &T) -> Result<(), Self::Error> {
95 match A::validate(value) {
96 Ok(()) => Ok(()),
97 Err(_) => B::validate(value),
98 }
99 }
100}
101
102/// Passes exactly when the sub-rule `A` **fails**.
103///
104/// `Not` inverts any [`Validator`], regardless of its error type, and reports a
105/// [`ValidationError`] with code `"not"` when `A` unexpectedly passes.
106///
107/// # Examples
108///
109/// ```rust
110/// use type_lib::combinator::Not;
111/// use type_lib::rules::Ascii;
112/// use type_lib::Validator;
113///
114/// // Require that a string is *not* pure ASCII.
115/// type NonAscii = Not<Ascii>;
116///
117/// assert!(NonAscii::validate("café").is_ok()); // not ASCII -> passes
118/// assert!(NonAscii::validate("plain").is_err()); // ASCII -> fails
119/// ```
120pub struct Not<A>(PhantomData<fn() -> A>);
121
122impl<T, A> Validator<T> for Not<A>
123where
124 T: ?Sized,
125 A: Validator<T>,
126{
127 type Error = ValidationError;
128
129 fn validate(value: &T) -> Result<(), Self::Error> {
130 match A::validate(value) {
131 Ok(()) => Err(ValidationError::new(
132 "not",
133 "value matched a rule it was required to violate",
134 )),
135 Err(_) => Ok(()),
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 #![allow(clippy::unwrap_used, clippy::expect_used)]
143
144 use super::*;
145 use crate::rules::{Ascii, MaxLen, NonEmpty};
146
147 #[test]
148 fn and_requires_both() {
149 type R = And<NonEmpty, MaxLen<4>>;
150 assert!(R::validate("abc").is_ok());
151 assert_eq!(R::validate("").unwrap_err().code(), "non_empty");
152 assert_eq!(R::validate("toolong").unwrap_err().code(), "max_len");
153 }
154
155 #[test]
156 fn or_accepts_either() {
157 type R = Or<NonEmpty, MaxLen<0>>;
158 assert!(R::validate("x").is_ok()); // NonEmpty passes
159 assert!(R::validate("").is_ok()); // NonEmpty fails, but MaxLen<0> passes on ""
160 }
161
162 #[test]
163 fn or_returns_second_error_when_both_fail() {
164 use crate::rules::MinLen;
165
166 type R = Or<MinLen<5>, MinLen<10>>;
167 // "abc" (len 3) fails both bounds; Or returns the second rule's error.
168 assert_eq!(R::validate("abc").unwrap_err().code(), "min_len");
169 }
170
171 #[test]
172 fn not_inverts() {
173 type R = Not<Ascii>;
174 assert!(R::validate("café").is_ok());
175 assert_eq!(R::validate("ascii").unwrap_err().code(), "not");
176 }
177}