Skip to main content

deser_validate/
macros.rs

1//! The `validator!` macro.
2use std::borrow::Cow;
3
4use crate::Violation;
5
6/// Creates a validator type.
7///
8/// The validator is a unit struct which implements [`Validator`](crate::Validator)
9/// for the type of its argument and for all types that borrow as it (see
10/// [`Borrow`](std::borrow::Borrow)): a validator of `str` validates `String`,
11/// `Box<str>` and `Cow<str>` too, a validator of `[T]` validates `Vec<T>`.
12/// There are three forms:
13///
14/// ```
15/// use deser_validate::{Validator, validator};
16///
17/// // a condition and the message if it's false
18/// validator!(pub NonZero(port: &u16) => *port != 0, "must not be zero");
19///
20/// // a function
21/// fn check_slug(value: &str) -> Result<(), &'static str> {
22///     if value.bytes().all(|b| b.is_ascii_lowercase() || b == b'-') {
23///         Ok(())
24///     } else {
25///         Err("must be a lowercase identifier")
26///     }
27/// }
28/// validator!(pub Slug(value: &str) = check_slug);
29///
30/// // a block
31/// validator!(
32///     /// Requires an even number of items.
33///     pub EvenLength(items: &[u32]) {
34///         if items.len() % 2 != 0 {
35///             return Err(format!(
36///                 "must have an even number of items, not {}",
37///                 items.len()
38///             ));
39///         }
40///         Ok(())
41///     }
42/// );
43///
44/// assert!(NonZero::validate(&80u16).is_ok());
45/// assert!(Slug::validate(&String::from("my-service")).is_ok());
46/// let violation = EvenLength::validate(&vec![1, 2, 3]).unwrap_err();
47/// assert_eq!(violation.code(), "even_length");
48/// assert_eq!(
49///     violation.message(),
50///     "must have an even number of items, not 3"
51/// );
52/// ```
53///
54/// Functions and blocks return a `Result<(), E>`, where the error is a
55/// message (`&'static str`, `String` or `Cow<'static, str>`) or a
56/// [`Violation`], or a `bool`.  Messages become violations with the name of
57/// the validator in snake case as code (`NonZero` has the code `non_zero`).
58/// Functions that return `false` fail with the message `is not valid`.
59///
60/// The code is part of what clients see, renaming the validator changes it.
61/// All forms accept a code that is used instead of the name as last
62/// argument, violations that functions return keep their code:
63///
64/// ```
65/// use deser_validate::{Validator, validator};
66///
67/// validator!(
68///     pub Port(port: &u16) => *port != 0,
69///     "must not be zero",
70///     code = "port"
71/// );
72/// assert_eq!(Port::validate(&0u16).unwrap_err().code(), "port");
73/// ```
74///
75/// The macro does not support types with generics or lifetimes (like
76/// `Either<T>` or `Name<'a>`).  For those, implement
77/// [`Validator`](crate::Validator) yourself (see there).
78#[macro_export]
79macro_rules! validator {
80    (
81        @impl [$(#[$meta:meta])*]
82        $vis:vis $name:ident($arg:ident: &$ty:ty) $body:block ($code:expr)
83    ) => {
84        $(#[$meta])*
85        $vis struct $name;
86
87        impl<T> $crate::Validator<T> for $name
88        where
89            T: ?::std::marker::Sized + ::std::borrow::Borrow<$ty>,
90        {
91            fn validate(value: &T) -> ::std::result::Result<(), $crate::Violation> {
92                let check = |$arg: &$ty| $body;
93                $crate::__private::into_result(
94                    check(::std::borrow::Borrow::borrow(value)),
95                    ::std::stringify!($name),
96                    $code,
97                )
98            }
99        }
100    };
101    (
102        $(#[$meta:meta])*
103        $vis:vis $name:ident($arg:ident: &$ty:ty) => $cond:expr, $message:expr,
104        code = $code:literal $(,)?
105    ) => {
106        $crate::validator!(
107            @impl [$(#[$meta])*] $vis $name($arg: &$ty) {
108                if $cond { Ok(()) } else { Err($message) }
109            } (::std::option::Option::Some($code))
110        );
111    };
112    (
113        $(#[$meta:meta])*
114        $vis:vis $name:ident($arg:ident: &$ty:ty) => $cond:expr, $message:expr $(,)?
115    ) => {
116        $crate::validator!(
117            @impl [$(#[$meta])*] $vis $name($arg: &$ty) {
118                if $cond { Ok(()) } else { Err($message) }
119            } (::std::option::Option::None)
120        );
121    };
122    (
123        $(#[$meta:meta])*
124        $vis:vis $name:ident($arg:ident: &$ty:ty) = $func:path, code = $code:literal $(,)?
125    ) => {
126        $crate::validator!(
127            @impl [$(#[$meta])*] $vis $name($arg: &$ty) {
128                $func($arg)
129            } (::std::option::Option::Some($code))
130        );
131    };
132    (
133        $(#[$meta:meta])*
134        $vis:vis $name:ident($arg:ident: &$ty:ty) = $func:path $(,)?
135    ) => {
136        $crate::validator!(
137            @impl [$(#[$meta])*] $vis $name($arg: &$ty) {
138                $func($arg)
139            } (::std::option::Option::None)
140        );
141    };
142    (
143        $(#[$meta:meta])*
144        $vis:vis $name:ident($arg:ident: &$ty:ty) $body:block, code = $code:literal $(,)?
145    ) => {
146        $crate::validator!(
147            @impl [$(#[$meta])*] $vis $name($arg: &$ty) $body (::std::option::Option::Some($code))
148        );
149    };
150    (
151        $(#[$meta:meta])*
152        $vis:vis $name:ident($arg:ident: &$ty:ty) $body:block $(,)?
153    ) => {
154        $crate::validator!(
155            @impl [$(#[$meta])*] $vis $name($arg: &$ty) $body (::std::option::Option::None)
156        );
157    };
158}
159
160/// What the functions of validators return.
161pub trait ValidationResult {
162    /// Converts the result.
163    ///
164    /// `name` is the name of the validator, `code` the code it was given.
165    fn into_result(self, name: &'static str, code: Option<&'static str>) -> Result<(), Violation>;
166}
167
168impl ValidationResult for bool {
169    fn into_result(self, name: &'static str, code: Option<&'static str>) -> Result<(), Violation> {
170        match self {
171            true => Ok(()),
172            false => Err("is not valid".into_violation(name, code)),
173        }
174    }
175}
176
177impl<E: IntoViolation> ValidationResult for Result<(), E> {
178    fn into_result(self, name: &'static str, code: Option<&'static str>) -> Result<(), Violation> {
179        self.map_err(|err| err.into_violation(name, code))
180    }
181}
182
183/// What the functions of validators fail with.
184pub trait IntoViolation {
185    /// Converts the error.
186    ///
187    /// `name` is the name of the validator, `code` the code it was given.
188    /// Messages become violations with the code, or the name in snake case
189    /// if it has none.
190    fn into_violation(self, name: &'static str, code: Option<&'static str>) -> Violation;
191}
192
193impl IntoViolation for Violation {
194    fn into_violation(self, _name: &'static str, _code: Option<&'static str>) -> Violation {
195        self
196    }
197}
198
199/// Returns the code of a validator.
200fn code_of(name: &'static str, code: Option<&'static str>) -> Cow<'static, str> {
201    match code {
202        Some(code) => Cow::Borrowed(code),
203        None => Cow::Owned(snake_case(name)),
204    }
205}
206
207impl IntoViolation for &'static str {
208    fn into_violation(self, name: &'static str, code: Option<&'static str>) -> Violation {
209        Violation::new(code_of(name, code), self)
210    }
211}
212
213impl IntoViolation for String {
214    fn into_violation(self, name: &'static str, code: Option<&'static str>) -> Violation {
215        Violation::new(code_of(name, code), self)
216    }
217}
218
219impl IntoViolation for Cow<'static, str> {
220    fn into_violation(self, name: &'static str, code: Option<&'static str>) -> Violation {
221        Violation::new(code_of(name, code), self)
222    }
223}
224
225/// Returns the code of a validator: its name in snake case.
226///
227/// Acronyms are kept together (`URLCheck` is `url_check`).
228fn snake_case(name: &str) -> String {
229    let chars: Vec<char> = name.chars().collect();
230    let mut rv = String::with_capacity(name.len() + 4);
231    for (idx, &c) in chars.iter().enumerate() {
232        if c.is_uppercase() && idx > 0 {
233            let prev = chars[idx - 1];
234            let next_is_lower = chars.get(idx + 1).is_some_and(|x| x.is_lowercase());
235            if prev.is_lowercase()
236                || prev.is_ascii_digit()
237                || (prev.is_uppercase() && next_is_lower)
238            {
239                rv.push('_');
240            }
241        }
242        rv.extend(c.to_lowercase());
243    }
244    rv
245}
246
247/// Used by the `validator!` macro.  Not public API.
248#[doc(hidden)]
249pub mod __private {
250    use super::ValidationResult;
251    use crate::Violation;
252
253    #[inline]
254    pub fn into_result<R: ValidationResult>(
255        rv: R,
256        name: &'static str,
257        code: Option<&'static str>,
258    ) -> Result<(), Violation> {
259        rv.into_result(name, code)
260    }
261}
262
263#[test]
264fn test_snake_case() {
265    assert_eq!(snake_case("NonZero"), "non_zero");
266    assert_eq!(snake_case("Slug"), "slug");
267    assert_eq!(snake_case("URL"), "url");
268    assert_eq!(snake_case("URLCheck"), "url_check");
269    assert_eq!(snake_case("MaxLen2"), "max_len2");
270}