validations/
lib.rs

1//! Crate `validations` provides an interface to check the validity of arbitrary types.
2//!
3//! The `Validate` trait provides the `validate` method, which runs arbitrary validation logic and
4//! returns a result indicating whether or not the value is valid. A return value of `Ok(())`
5//! indicates a valid value. A return value of `Err(Errors)` indicates an invalid value,
6//! and includes details of why the value failed validation.
7//!
8//! `Errors` is a container that can hold both general and field-specific validation
9//! errors for an invalid value. An individual validation error is represented by
10//! `Error`, which contains a human-readable error message, and an optional type of the
11//! programmer's choice that includes additional contextual information about the error.
12//!
13//! Types that implement `Validate` should handle validation logic for each of their fields, as
14//! necessary. If the type of a field implements `Validate` itself, it's also possible to delegate
15//! to the field to validate itself and assign any resulting errors back to the parent type's
16//! errors.
17//!
18//! Instead of implementing `Validate`, another approach is to implement validation logic inside the
19//! constructor function of a type `T`, and return `Result<T, Errors>`, preventing an invalid value
20//! from being created in the first place. This may not always be possible, as the value may be
21//! created through other means. For example, the value may be deserialized from a format like JSON
22//! from an external source. In this case, the `Validate` trait allows deserialization logic to be
23//! decoupled from domain-level validation logic.
24//!
25//! # Examples
26//!
27//! Validating a value:
28//!
29//! ```
30//! let entry = AddressBookEntry {
31//!     cell_number: None,
32//!     email: Some(Email("rcohle@dps.la.gov")),
33//!     home_number: Some(PhoneNumber {
34//!         area_code: "555",
35//!         number: "555-5555",
36//!     }),
37//!     name: "Rust Cohle",
38//! };
39//!
40//! assert!(entry.validate().is_ok());
41//! ```
42//!
43//! Validating a value with a non-field-specific error:
44//!
45//! ```
46//! let entry = AddressBookEntry {
47//!     cell_number: None,
48//!     email: Some(Email("rcohle@dps.la.gov")),
49//!     home_number: None,
50//!     name: "Rust Cohle",
51//! };
52//!
53//! let errors = entry.validate().err().unwrap();
54//!
55//! assert_eq!(
56//!     errors.base().unwrap()[0].message(),
57//!     "at least one phone number is required".to_string()
58//! );
59//! ```
60//!
61//! Validating a value with a field error:
62//!
63//! ```
64//! let entry = AddressBookEntry {
65//!     cell_number: None,
66//!     email: Some(Email("rcohle@dps.la.gov")),
67//!     home_number: Some(PhoneNumber {
68//!         area_code: "555",
69//!         number: "555-5555",
70//!     }),
71//!     name: "",
72//! };
73//!
74//! let errors = entry.validate().err().unwrap();
75//!
76//! assert_eq!(
77//!     errors.field("name").unwrap().base().unwrap()[0].message(),
78//!     "can't be blank".to_string()
79//! );
80//! ```
81
82#![deny(missing_docs)]
83#![deny(warnings)]
84
85use std::any::Any;
86use std::collections::hash_map::{Entry, HashMap};
87use std::error::Error as StdError;
88use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
89
90/// An individual validation error.
91#[derive(Debug)]
92pub struct Error<T> where T: Debug + Any {
93    details: Option<T>,
94    message: String,
95}
96
97/// A collection of errors returned by a failed validation.
98#[derive(Debug)]
99pub struct Errors<T> where T: Debug + Any {
100    base: Option<Vec<Error<T>>>,
101    fields: Option<HashMap<String, Box<Errors<T>>>>,
102}
103
104/// An `Error` with no custom details, to avoid the generic parameter when not needed.
105pub type SimpleError = Error<()>;
106
107/// `Errors` with no custom details, to avoid the generic parameter when not needed.
108pub type SimpleErrors = Errors<()>;
109
110/// A validatable type.
111pub trait Validate<T> where T: Debug + Any {
112    /// Validates the value.
113    ///
114    /// If invalid, returns details about why the value failed validation.
115    fn validate(&self) -> Result<(), Errors<T>>;
116}
117
118impl<T> Error<T> where T: Debug + Any {
119    /// Constructs a validation error.
120    pub fn new<S>(message: S) -> Self where S: Into<String> {
121        Error {
122            details: None,
123            message: message.into(),
124        }
125    }
126
127    /// Constructs a validation error with additional details.
128    pub fn with_details<S>(message: S, details: T) -> Self where S: Into<String> {
129        Error {
130            details: Some(details),
131            message: message.into(),
132        }
133    }
134
135    /// Additional contextual information about the error, if provided.
136    pub fn details(&self) -> Option<&T> {
137        self.details.as_ref()
138    }
139
140    /// Sets the details of this error.
141    pub fn set_details(&mut self, details: T) {
142        self.details = Some(details);
143    }
144
145    /// A human-readable message explaining the error.
146    pub fn message(&self) -> &str {
147        &self.message
148    }
149}
150
151impl<T> Display for Error<T> where T: Debug + Any {
152    fn fmt(&self, f: &mut Formatter) -> FmtResult {
153        write!(f, "{}", &self.message)
154    }
155}
156
157impl<T> StdError for Error<T> where T: Debug + Any {
158    fn description(&self) -> &str {
159        &self.message
160    }
161}
162
163impl<T> Errors<T> where T: Debug + Any {
164    /// Constructs an empty `Errors` value.
165    pub fn new() -> Self {
166        Errors {
167            base: None,
168            fields: None,
169        }
170    }
171
172    /// Adds a validation error that is not specific to any field.
173    pub fn add_error(&mut self, error: Error<T>) {
174        match self.base {
175            Some(ref mut base_errors) => base_errors.push(error),
176            None => self.base = Some(vec![error]),
177        }
178    }
179
180    /// Adds a validation error for the given field.
181    ///
182    /// Calling this method will overwrite any errors assigned via `set_field_errors`.
183    pub fn add_field_error<S>(&mut self, field: S, error: Error<T>) where S: Into<String>{
184        match self.fields {
185            Some(ref mut field_errors) => {
186                match field_errors.entry(field.into()) {
187                    Entry::Occupied(mut entry) => {
188                        entry.get_mut().add_error(error);
189                    }
190                    Entry::Vacant(entry) => {
191                        let mut errors = Errors::new();
192
193                        errors.add_error(error);
194
195                        entry.insert(Box::new(errors));
196                    }
197                }
198            }
199            None => {
200                let mut errors = Errors::new();
201
202                errors.add_error(error);
203
204                let mut map = HashMap::new();
205
206                map.insert(field.into(), Box::new(errors));
207
208                self.fields = Some(map);
209            }
210        }
211    }
212
213    /// A slice of non-field-specific errors, if any.
214    pub fn base<'a>(&'a self) -> Option<&'a [Error<T>]> {
215        self.base.as_ref().map(Vec::as_slice)
216    }
217
218    /// The `Errors` for the given field, if any.
219    pub fn field<F>(&self, field: F) -> Option<&Box<Errors<T>>> where F: Into<String> {
220        if self.fields.is_some() {
221            self.fields.as_ref().unwrap().get(&field.into())
222        } else {
223            None
224        }
225    }
226
227    /// Returns `true` if there are no errors.
228    pub fn is_empty(&self) -> bool {
229        self.base.is_none() && self.fields.is_none()
230    }
231
232    /// Sets the given field's errors to the given `Errors`.
233    ///
234    /// This is useful if the field itself implements `Validate`. In that case, the parent type can
235    /// simply delegate to the field to validate itself and assign the resulting errors using this
236    /// method.
237    ///
238    /// Calling this method will overwrite any field errors previously added with
239    /// `add_field_error`.
240    pub fn set_field_errors<S>(&mut self, field: S, errors: Errors<T>) where S: Into<String>{
241        match self.fields {
242            Some(ref mut field_errors) => {
243                field_errors.insert(field.into(), Box::new(errors));
244            }
245            None => {
246                let mut map = HashMap::new();
247
248                map.insert(field.into(), Box::new(errors));
249
250                self.fields = Some(map);
251            }
252        }
253    }
254}
255
256impl<T> Display for Errors<T> where T: Debug + Any {
257    fn fmt(&self, f: &mut Formatter) -> FmtResult {
258        write!(f, "validation failed")
259    }
260}
261
262impl<T> StdError for Errors<T> where T: Debug + Any {
263    fn description(&self) -> &str {
264        "validation failed"
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::{Error, Errors, Validate};
271
272    #[derive(Debug)]
273    struct AddressBookEntry {
274        cell_number: Option<PhoneNumber>,
275        email: Option<Email>,
276        home_number: Option<PhoneNumber>,
277        name: &'static str,
278    }
279
280    #[derive(Debug)]
281    struct Email(&'static str);
282
283    #[derive(Debug)]
284    struct PhoneNumber {
285        area_code: &'static str,
286        number: &'static str,
287    }
288
289    #[derive(Debug)]
290    struct InvalidCharacters {
291        invalid_characters: Vec<char>,
292    }
293
294
295    impl Validate<InvalidCharacters> for AddressBookEntry {
296        fn validate(&self) -> Result<(), Errors<InvalidCharacters>> {
297            let mut errors = Errors::new();
298
299            if self.cell_number.is_none() && self.home_number.is_none() {
300                errors.add_error(Error::new("at least one phone number is required"));
301            }
302
303            if self.name.len() == 0 {
304                errors.add_field_error("name", Error::new("can't be blank"));
305            }
306
307            if let Some(ref email) = self.email {
308                if let Err(field_errors) = email.validate() {
309                    errors.set_field_errors("email", field_errors);
310                }
311            }
312
313            let numbers_to_check = [
314                ("home_number", &self.home_number),
315                ("cell_number", &self.cell_number),
316            ];
317
318            for &(field_name, field) in &numbers_to_check {
319                if field.is_some() {
320                    let invalid_characters = InvalidCharacters::check_digits(
321                        &field.as_ref().unwrap().full_number()
322                    );
323
324                    if invalid_characters.len() > 0 {
325                        errors.add_field_error(
326                            field_name,
327                            Error::with_details(
328                                "has invalid characters",
329                                InvalidCharacters {
330                                    invalid_characters: invalid_characters,
331                                },
332                            ),
333                        );
334                    }
335                }
336            }
337
338            if errors.is_empty() {
339                Ok(())
340            } else {
341                Err(errors)
342            }
343        }
344    }
345
346    impl Validate<InvalidCharacters> for Email {
347        fn validate(&self) -> Result<(), Errors<InvalidCharacters>> {
348            let email = self.0;
349
350            if !email.contains("@") {
351                let mut errors = Errors::new();
352
353                errors.add_error(Error::new("must contain an @ symbol"));
354
355                return Err(errors);
356            }
357
358            Ok(())
359        }
360    }
361
362    impl PhoneNumber {
363        pub fn full_number(&self) -> String {
364            format!("{}-{}", self.area_code, self.number)
365        }
366    }
367
368    impl InvalidCharacters {
369        pub fn check_digits(number: &str) -> Vec<char> {
370            number.replace("-", "").chars().filter(|c| !c.is_digit(10)).collect()
371        }
372
373        pub fn invalid_characters(&self) -> &[char] {
374            self.invalid_characters.as_slice()
375        }
376    }
377
378    #[test]
379    fn valid_value() {
380        let entry = AddressBookEntry {
381            cell_number: None,
382            email: Some(Email("rcohle@dps.la.gov")),
383            home_number: Some(PhoneNumber {
384                area_code: "555",
385                number: "555-5555",
386            }),
387            name: "Rust Cohle",
388        };
389
390        assert!(entry.validate().is_ok());
391    }
392
393    #[test]
394    fn base_error() {
395        let entry = AddressBookEntry {
396            cell_number: None,
397            email: Some(Email("rcohle@dps.la.gov")),
398            home_number: None,
399            name: "Rust Cohle",
400        };
401
402        let errors = entry.validate().err().unwrap();
403
404        assert_eq!(
405            errors.base().unwrap()[0].message(),
406            "at least one phone number is required".to_string()
407        );
408    }
409
410    #[test]
411    fn field_error() {
412        let entry = AddressBookEntry {
413            cell_number: None,
414            email: Some(Email("rcohle@dps.la.gov")),
415            home_number: Some(PhoneNumber {
416                area_code: "555",
417                number: "555-5555",
418            }),
419            name: "",
420        };
421
422        let errors = entry.validate().err().unwrap();
423
424        assert_eq!(
425            errors.field("name").unwrap().base().unwrap()[0].message(),
426            "can't be blank".to_string()
427        );
428    }
429
430    #[test]
431    fn delegate_to_field() {
432        let entry = AddressBookEntry {
433            cell_number: None,
434            email: Some(Email("rcohle")),
435            home_number: Some(PhoneNumber {
436                area_code: "555",
437                number: "555-5555",
438            }),
439            name: "Rust Cohle",
440        };
441
442        let errors = entry.validate().err().unwrap();
443
444        assert_eq!(
445            errors.field("email").unwrap().base().unwrap()[0].message(),
446            "must contain an @ symbol".to_string()
447        );
448    }
449
450    #[test]
451    fn details() {
452        let entry = AddressBookEntry {
453            cell_number: None,
454            email: Some(Email("rcohle@dps.la.gov")),
455            home_number: Some(PhoneNumber {
456                area_code: "555",
457                number: "x55-55t5",
458            }),
459            name: "",
460        };
461
462        let errors = entry.validate().err().unwrap();
463
464        let invalid_characters = errors.field("home_number").unwrap().base().unwrap()[0]
465                .details().unwrap().invalid_characters();
466
467        assert!(invalid_characters.contains(&'x'));
468        assert!(invalid_characters.contains(&'t'));
469    }
470}