type_lib/validator.rs
1//! The [`Validator`] trait: reusable, type-level validation rules.
2
3/// A reusable validation rule applied to values of type `T`.
4///
5/// A `Validator` is a *type-level* predicate. It carries no state and is never
6/// instantiated — you implement it on a zero-sized marker type and the rule is
7/// selected purely through the type system. Pairing a value with the validator
8/// that vouched for it is exactly what [`Refined`](crate::Refined) does, at no
9/// runtime cost.
10///
11/// # Choosing the value type
12///
13/// `T` is `?Sized`, so a rule can target an unsized type such as `str` or `[u8]`
14/// and be reused for the owned forms by being generic over a borrow. The
15/// `NonEmpty` rule below works for `str`, `String`, and `&str` alike because it
16/// is written over `S: AsRef<str>`:
17///
18/// ```rust
19/// use type_lib::{ValidationError, Validator};
20///
21/// struct NonEmpty;
22///
23/// impl<S: AsRef<str> + ?Sized> Validator<S> for NonEmpty {
24/// type Error = ValidationError;
25///
26/// fn validate(value: &S) -> Result<(), Self::Error> {
27/// if value.as_ref().is_empty() {
28/// Err(ValidationError::new("non_empty", "value must not be empty"))
29/// } else {
30/// Ok(())
31/// }
32/// }
33/// }
34///
35/// assert!(NonEmpty::validate("hello").is_ok());
36/// assert!(NonEmpty::validate("").is_err());
37/// ```
38///
39/// # Custom error types
40///
41/// [`Error`](Validator::Error) is an associated type, so a rule can report a
42/// rich, structured failure instead of the bundled [`ValidationError`]:
43///
44/// ```rust
45/// use type_lib::Validator;
46///
47/// #[derive(Debug, PartialEq)]
48/// struct TooLong {
49/// limit: usize,
50/// actual: usize,
51/// }
52///
53/// struct MaxLen8;
54///
55/// impl Validator<str> for MaxLen8 {
56/// type Error = TooLong;
57///
58/// fn validate(value: &str) -> Result<(), Self::Error> {
59/// let actual = value.chars().count();
60/// if actual > 8 {
61/// Err(TooLong { limit: 8, actual })
62/// } else {
63/// Ok(())
64/// }
65/// }
66/// }
67///
68/// assert_eq!(
69/// MaxLen8::validate("far-too-long"),
70/// Err(TooLong { limit: 8, actual: 12 }),
71/// );
72/// ```
73///
74/// [`ValidationError`]: crate::ValidationError
75pub trait Validator<T: ?Sized> {
76 /// The failure produced when a value violates the rule.
77 ///
78 /// Use [`ValidationError`](crate::ValidationError) for simple cases, or a
79 /// bespoke type when callers need to inspect structured details of the
80 /// failure.
81 type Error;
82
83 /// Checks `value` against the rule.
84 ///
85 /// Returns `Ok(())` when the value satisfies the invariant, or
86 /// `Err(Self::Error)` describing why it does not. This is a pure predicate:
87 /// it never mutates or transforms the value.
88 ///
89 /// # Errors
90 ///
91 /// Returns [`Self::Error`](Validator::Error) when `value` fails the rule.
92 fn validate(value: &T) -> Result<(), Self::Error>;
93}
94
95#[cfg(test)]
96mod tests {
97 #![allow(clippy::unwrap_used, clippy::expect_used)]
98
99 use super::*;
100 use crate::ValidationError;
101
102 struct NonEmpty;
103
104 impl<S: AsRef<str> + ?Sized> Validator<S> for NonEmpty {
105 type Error = ValidationError;
106
107 fn validate(value: &S) -> Result<(), Self::Error> {
108 if value.as_ref().is_empty() {
109 Err(ValidationError::new("non_empty", "value must not be empty"))
110 } else {
111 Ok(())
112 }
113 }
114 }
115
116 #[test]
117 fn accepts_valid_value() {
118 assert!(NonEmpty::validate("hello").is_ok());
119 }
120
121 #[test]
122 fn rejects_invalid_value() {
123 let err = NonEmpty::validate("").unwrap_err();
124 assert_eq!(err.code(), "non_empty");
125 }
126
127 #[test]
128 fn rule_is_reusable_across_borrow_forms() {
129 // Same marker type, different value representations.
130 assert!(<NonEmpty as Validator<str>>::validate("ok").is_ok());
131 let owned = "ok";
132 assert!(<NonEmpty as Validator<&str>>::validate(&owned).is_ok());
133 }
134}