zino_core/validation/
mod.rs

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