Skip to main content

deser_validate/
validators.rs

1//! The validators provided by this crate.
2use std::borrow::Cow;
3use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
4use std::marker::PhantomData;
5
6use crate::{Validator, Violation};
7
8/// Values that have a length (see [`Len`] and [`NonEmpty`]).
9///
10/// The length of strings is the number of characters, of collections the
11/// number of items.
12pub trait Length {
13    /// Returns the length.
14    fn length(&self) -> usize;
15}
16
17impl Length for str {
18    fn length(&self) -> usize {
19        self.chars().count()
20    }
21}
22
23impl Length for String {
24    fn length(&self) -> usize {
25        self.as_str().length()
26    }
27}
28
29impl<T> Length for [T] {
30    fn length(&self) -> usize {
31        self.len()
32    }
33}
34
35impl<T, const N: usize> Length for [T; N] {
36    fn length(&self) -> usize {
37        N
38    }
39}
40
41macro_rules! impl_length {
42    ($($ty:ty => [$($param:tt)*];)*) => {
43        $(
44            impl<$($param)*> Length for $ty {
45                fn length(&self) -> usize {
46                    self.len()
47                }
48            }
49        )*
50    };
51}
52
53impl_length! {
54    Vec<T> => [T];
55    VecDeque<T> => [T];
56    BTreeSet<T> => [T];
57    HashSet<T, S> => [T, S];
58    BTreeMap<K, V> => [K, V];
59    HashMap<K, V, S> => [K, V, S];
60}
61
62impl<T: Length + ?Sized> Length for &T {
63    fn length(&self) -> usize {
64        (**self).length()
65    }
66}
67
68impl<T: Length + ?Sized> Length for Box<T> {
69    fn length(&self) -> usize {
70        (**self).length()
71    }
72}
73
74impl<T: Length + ToOwned + ?Sized> Length for Cow<'_, T> {
75    fn length(&self) -> usize {
76        (**self).length()
77    }
78}
79
80/// Requires a string or collection that is not empty.
81///
82/// The code of the violation is `non_empty`.
83pub struct NonEmpty;
84
85impl<T: Length + ?Sized> Validator<T> for NonEmpty {
86    fn validate(value: &T) -> Result<(), Violation> {
87        if value.length() == 0 {
88            Err(Violation::new("non_empty", "must not be empty"))
89        } else {
90            Ok(())
91        }
92    }
93}
94
95/// Requires the length of a string or collection to be in a range.
96///
97/// Both ends are inclusive.  Strings count characters, collections items
98/// (see [`Length`]).  The code of the violation is the name of the
99/// validator: `len`, `min_len` ([`MinLen`]) or `max_len` ([`MaxLen`]).  It
100/// has the parameters `min` and `max` (if they are set).
101///
102/// ```
103/// use deser_validate::{Len, MaxLen, Validator};
104///
105/// assert!(Len::<1, 3>::validate("abc").is_ok());
106/// assert_eq!(
107///     Len::<1, 3>::validate("abcd").unwrap_err().message(),
108///     "length must be between 1 and 3"
109/// );
110/// assert!(MaxLen::<2>::validate(&vec![1, 2, 3]).is_err());
111/// ```
112pub struct Len<const MIN: usize, const MAX: usize = { usize::MAX }>;
113
114/// Requires a minimum length (see [`Len`]).
115pub type MinLen<const N: usize> = Len<N>;
116
117/// Requires a maximum length (see [`Len`]).
118pub type MaxLen<const N: usize> = Len<0, N>;
119
120impl<T: Length + ?Sized, const MIN: usize, const MAX: usize> Validator<T> for Len<MIN, MAX> {
121    fn validate(value: &T) -> Result<(), Violation> {
122        let len = value.length();
123        if (MIN..=MAX).contains(&len) {
124            return Ok(());
125        }
126        let violation = match (MIN, MAX) {
127            (min, usize::MAX) => {
128                Violation::new("min_len", format!("length must be at least {}", min))
129                    .with_param("min", min)
130            }
131            (0, max) => Violation::new("max_len", format!("length must be at most {}", max))
132                .with_param("max", max),
133            (min, max) => {
134                Violation::new("len", format!("length must be between {} and {}", min, max))
135                    .with_param("min", min)
136                    .with_param("max", max)
137            }
138        };
139        Err(violation)
140    }
141}
142
143/// Integers (see [`Range`]).
144pub trait Integer {
145    /// Returns the value as `i128`, `None` if it's larger.
146    fn to_i128(&self) -> Option<i128>;
147}
148
149macro_rules! impl_integer {
150    ($($ty:ty),*) => {
151        $(
152            impl Integer for $ty {
153                fn to_i128(&self) -> Option<i128> {
154                    i128::try_from(*self).ok()
155                }
156            }
157        )*
158    };
159}
160
161impl_integer!(
162    i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize
163);
164
165/// Requires an integer to be in a range.
166///
167/// Both ends are inclusive.  The code of the violation is the name of the
168/// validator: `range`, `min` ([`Min`]) or `max` ([`Max`]).  It has the
169/// parameters `min` and `max` (if they are set).
170///
171/// ```
172/// use deser_validate::{Min, Range, Validator};
173///
174/// assert!(Range::<1, 65535>::validate(&80u16).is_ok());
175/// assert_eq!(
176///     Range::<1, 65535>::validate(&0u16).unwrap_err().message(),
177///     "must be between 1 and 65535"
178/// );
179/// assert!(Min::<1>::validate(&0u32).is_err());
180/// ```
181pub struct Range<const MIN: i128, const MAX: i128>;
182
183/// Requires a minimum value (see [`Range`]).
184pub type Min<const N: i128> = Range<N, { i128::MAX }>;
185
186/// Requires a maximum value (see [`Range`]).
187pub type Max<const N: i128> = Range<{ i128::MIN }, N>;
188
189impl<T: Integer + ?Sized, const MIN: i128, const MAX: i128> Validator<T> for Range<MIN, MAX> {
190    fn validate(value: &T) -> Result<(), Violation> {
191        // values that do not fit into i128 are larger than every maximum
192        let valid = match value.to_i128() {
193            Some(value) => (MIN..=MAX).contains(&value),
194            None => MAX == i128::MAX,
195        };
196        if valid {
197            return Ok(());
198        }
199        let violation = match (MIN, MAX) {
200            (min, i128::MAX) => {
201                Violation::new("min", format!("must be at least {}", min)).with_param("min", min)
202            }
203            (i128::MIN, max) => {
204                Violation::new("max", format!("must be at most {}", max)).with_param("max", max)
205            }
206            (min, max) => Violation::new("range", format!("must be between {} and {}", min, max))
207                .with_param("min", min)
208                .with_param("max", max),
209        };
210        Err(violation)
211    }
212}
213
214/// Requires a string that looks like an email address.
215///
216/// This checks the structure only: a local part and a domain separated by
217/// a single `@`, the domain has at least two labels and there is no
218/// whitespace.  Whether the address exists can only be found out by
219/// sending an email.  The code of the violation is `email`.
220///
221/// ```
222/// use deser_validate::{Email, Validator};
223///
224/// assert!(Email::validate("jane@example.com").is_ok());
225/// assert!(Email::validate("jane@localhost").is_err());
226/// assert!(Email::validate("jane example.com").is_err());
227/// ```
228pub struct Email;
229
230impl<T: AsRef<str> + ?Sized> Validator<T> for Email {
231    fn validate(value: &T) -> Result<(), Violation> {
232        let value = value.as_ref();
233        let valid = match value.split_once('@') {
234            Some((local, domain)) => {
235                !local.is_empty()
236                    && !domain.contains('@')
237                    && !value.chars().any(char::is_whitespace)
238                    && domain.contains('.')
239                    && domain.split('.').all(|label| !label.is_empty())
240            }
241            None => false,
242        };
243        if valid {
244            Ok(())
245        } else {
246            Err(Violation::new("email", "must be an email address"))
247        }
248    }
249}
250
251/// Validates every item of a collection with `V`.
252///
253/// The violation of the first invalid item is reported with its index as
254/// parameter `index`, its code is the code of the violation of the item.
255/// For an `Option` the value is validated if there is one.
256///
257/// ```
258/// use deser_validate::{Each, Email, Validator};
259///
260/// let emails = vec!["jane@example.com".to_string(), "nope".to_string()];
261/// let violation = Each::<Email>::validate(&emails).unwrap_err();
262/// assert_eq!(violation.message(), "item 1: must be an email address");
263/// assert!(Each::<Email>::validate(&None::<String>).is_ok());
264/// ```
265pub struct Each<V>(PhantomData<fn() -> V>);
266
267impl<V> Each<V> {
268    fn validate_items<'a, T: 'a, I>(items: I) -> Result<(), Violation>
269    where
270        V: Validator<T>,
271        I: IntoIterator<Item = &'a T>,
272    {
273        for (index, item) in items.into_iter().enumerate() {
274            if let Err(violation) = V::validate(item) {
275                let message = format!("item {}: {}", index, violation.message());
276                let mut rv = Violation::new(violation.code().to_string(), message);
277                for (name, value) in violation.params() {
278                    rv = rv.with_param(name, value.clone());
279                }
280                return Err(rv.with_param("index", index));
281            }
282        }
283        Ok(())
284    }
285}
286
287macro_rules! impl_each {
288    ($($ty:ty => [$($param:tt)*];)*) => {
289        $(
290            impl<V: Validator<T>, $($param)*> Validator<$ty> for Each<V> {
291                fn validate(value: &$ty) -> Result<(), Violation> {
292                    Each::<V>::validate_items(value.iter())
293                }
294            }
295        )*
296    };
297}
298
299impl_each! {
300    [T] => [T];
301    Vec<T> => [T];
302    VecDeque<T> => [T];
303    BTreeSet<T> => [T];
304    HashSet<T, S> => [T, S];
305    Option<T> => [T];
306}
307
308impl<V: Validator<T>, T, const N: usize> Validator<[T; N]> for Each<V> {
309    fn validate(value: &[T; N]) -> Result<(), Violation> {
310        Each::<V>::validate_items(value.iter())
311    }
312}