zino_core/validation/
mod.rs

1//! Generic validator and common validation rules.
2use crate::{Map, SharedString, error::Error, extension::JsonObjectExt};
3use smallvec::SmallVec;
4use std::fmt;
5
6mod validator;
7
8pub use validator::{
9    AlphabeticValidator, AlphanumericValidator, AsciiAlphabeticValidator,
10    AsciiAlphanumericValidator, AsciiDigitValidator, AsciiHexdigitValidator,
11    AsciiLowercaseValidator, AsciiUppercaseValidator, AsciiValidator, DateTimeValidator,
12    DateValidator, HostValidator, HostnameValidator, IpAddrValidator, Ipv4AddrValidator,
13    Ipv6AddrValidator, LowercaseValidator, NumericValidator, TimeValidator, UppercaseValidator,
14    UriValidator, UuidValidator, Validator,
15};
16
17#[cfg(feature = "validator-credit-card")]
18pub use validator::CreditCardValidator;
19#[cfg(feature = "validator-email")]
20pub use validator::EmailValidator;
21#[cfg(feature = "validator-phone-number")]
22pub use validator::PhoneNumberValidator;
23#[cfg(feature = "validator-regex")]
24pub use validator::RegexValidator;
25
26/// A record of validation results.
27#[derive(Debug, Default)]
28pub struct Validation {
29    failed_entries: SmallVec<[(SharedString, Error); 4]>,
30}
31
32impl Validation {
33    /// Creates a new instance.
34    #[inline]
35    pub fn new() -> Self {
36        Self {
37            failed_entries: SmallVec::new(),
38        }
39    }
40
41    /// Creates a new instance with the entry.
42    #[inline]
43    pub fn from_entry(key: impl Into<SharedString>, err: impl Into<Error>) -> Self {
44        let mut entries = SmallVec::new();
45        entries.push((key.into(), err.into()));
46        Self {
47            failed_entries: entries,
48        }
49    }
50
51    /// Records an entry with the supplied message.
52    #[inline]
53    pub fn record(&mut self, key: impl Into<SharedString>, message: impl Into<SharedString>) {
54        self.failed_entries.push((key.into(), Error::new(message)));
55    }
56
57    /// Records an entry for the error.
58    #[inline]
59    pub fn record_fail(&mut self, key: impl Into<SharedString>, err: impl Into<Error>) {
60        self.failed_entries.push((key.into(), err.into()));
61    }
62
63    /// Validates the string value with a specific format.
64    pub fn validate_format(&mut self, key: impl Into<SharedString>, value: &str, format: &str) {
65        match format {
66            "alphabetic" => {
67                if let Err(err) = AlphabeticValidator.validate(value) {
68                    self.record_fail(key, err);
69                }
70            }
71            "alphanumeric" => {
72                if let Err(err) = AlphanumericValidator.validate(value) {
73                    self.record_fail(key, err);
74                }
75            }
76            "ascii" => {
77                if let Err(err) = AsciiValidator.validate(value) {
78                    self.record_fail(key, err);
79                }
80            }
81            "ascii-alphabetic" => {
82                if let Err(err) = AsciiAlphabeticValidator.validate(value) {
83                    self.record_fail(key, err);
84                }
85            }
86            "ascii-alphanumeric" => {
87                if let Err(err) = AsciiAlphanumericValidator.validate(value) {
88                    self.record_fail(key, err);
89                }
90            }
91            "ascii-digit" => {
92                if let Err(err) = AsciiDigitValidator.validate(value) {
93                    self.record_fail(key, err);
94                }
95            }
96            "ascii-hexdigit" => {
97                if let Err(err) = AsciiHexdigitValidator.validate(value) {
98                    self.record_fail(key, err);
99                }
100            }
101            "ascii-lowercase" => {
102                if let Err(err) = AsciiLowercaseValidator.validate(value) {
103                    self.record_fail(key, err);
104                }
105            }
106            "ascii-uppercase" => {
107                if let Err(err) = AsciiUppercaseValidator.validate(value) {
108                    self.record_fail(key, err);
109                }
110            }
111            #[cfg(feature = "validator-credit-card")]
112            "credit-card" => {
113                if let Err(err) = CreditCardValidator.validate(value) {
114                    self.record_fail(key, err);
115                }
116            }
117            "date" => {
118                if let Err(err) = DateValidator.validate(value) {
119                    self.record_fail(key, err);
120                }
121            }
122            "date-time" => {
123                if let Err(err) = DateTimeValidator.validate(value) {
124                    self.record_fail(key, err);
125                }
126            }
127            #[cfg(feature = "validator-email")]
128            "email" => {
129                if let Err(err) = EmailValidator.validate(value) {
130                    self.record_fail(key, err);
131                }
132            }
133            "host" => {
134                if let Err(err) = HostValidator.validate(value) {
135                    self.record_fail(key, err);
136                }
137            }
138            "hostname" => {
139                if let Err(err) = HostnameValidator.validate(value) {
140                    self.record_fail(key, err);
141                }
142            }
143            "ip" => {
144                if let Err(err) = IpAddrValidator.validate(value) {
145                    self.record_fail(key, err);
146                }
147            }
148            "ipv4" => {
149                if let Err(err) = Ipv4AddrValidator.validate(value) {
150                    self.record_fail(key, err);
151                }
152            }
153            "ipv6" => {
154                if let Err(err) = Ipv6AddrValidator.validate(value) {
155                    self.record_fail(key, err);
156                }
157            }
158            "lowercase" => {
159                if let Err(err) = LowercaseValidator.validate(value) {
160                    self.record_fail(key, err);
161                }
162            }
163            "numeric" => {
164                if let Err(err) = NumericValidator.validate(value) {
165                    self.record_fail(key, err);
166                }
167            }
168            #[cfg(feature = "validator-phone-number")]
169            "phone-number" => {
170                if let Err(err) = PhoneNumberValidator.validate(value) {
171                    self.record_fail(key, err);
172                }
173            }
174            #[cfg(feature = "validator-regex")]
175            "regex" => {
176                if let Err(err) = RegexValidator.validate(value) {
177                    self.record_fail(key, err);
178                }
179            }
180            "time" => {
181                if let Err(err) = TimeValidator.validate(value) {
182                    self.record_fail(key, err);
183                }
184            }
185            "uppercase" => {
186                if let Err(err) = UppercaseValidator.validate(value) {
187                    self.record_fail(key, err);
188                }
189            }
190            "uri" => {
191                if let Err(err) = UriValidator.validate(value) {
192                    self.record_fail(key, err);
193                }
194            }
195            "uuid" => {
196                if let Err(err) = UuidValidator.validate(value) {
197                    self.record_fail(key, err);
198                }
199            }
200            _ => {
201                let field = key.into();
202                tracing::warn!("unsupported format `{format}` for the field `{field}`");
203            }
204        }
205    }
206
207    /// Returns true if the validation contains a value for the specified key.
208    #[inline]
209    pub fn contains_key(&self, key: &str) -> bool {
210        self.failed_entries.iter().any(|(field, _)| field == key)
211    }
212
213    /// Returns `true` if the validation is success.
214    #[inline]
215    pub fn is_success(&self) -> bool {
216        self.failed_entries.is_empty()
217    }
218
219    /// Returns a list of invalid params.
220    #[inline]
221    pub fn invalid_params(&self) -> Vec<&str> {
222        self.failed_entries
223            .iter()
224            .map(|entry| entry.0.as_ref())
225            .collect()
226    }
227
228    /// Consumes the validation and returns as a json object.
229    #[must_use]
230    pub fn into_map(self) -> Map {
231        let mut map = Map::new();
232        for (key, err) in self.failed_entries {
233            let message = err.message();
234            tracing::warn!("invalid value for `{key}`: {message}");
235            map.upsert(key, message);
236        }
237        map
238    }
239}
240
241impl fmt::Display for Validation {
242    #[inline]
243    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
244        let failed_entries = &self.failed_entries;
245        let mut errors = Vec::with_capacity(failed_entries.len());
246        for (key, err) in failed_entries {
247            let message = format!("invalid value for `{key}`: {}", err.message());
248            errors.push(message);
249        }
250        errors.join(",").fmt(f)
251    }
252}