validator_async/
types.rs

1use std::borrow::Cow;
2use std::collections::{hash_map::Entry::Vacant, BTreeMap, HashMap};
3
4use serde::ser::Serialize;
5use serde_derive::{Deserialize, Serialize};
6use serde_json::{to_value, Value};
7
8#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
9pub struct ValidationError {
10    pub code: Cow<'static, str>,
11    pub message: Option<Cow<'static, str>>,
12    pub params: HashMap<Cow<'static, str>, Value>,
13}
14
15impl ValidationError {
16    pub fn new(code: &'static str) -> ValidationError {
17        ValidationError { code: Cow::from(code), message: None, params: HashMap::new() }
18    }
19
20    pub fn add_param<T: Serialize>(&mut self, name: Cow<'static, str>, val: &T) {
21        self.params.insert(name, to_value(val).unwrap());
22    }
23
24    /// Adds a custom message to a `ValidationError` that will be used when displaying the
25    /// `ValidationError`, instead of an auto-generated description.
26    pub fn with_message(mut self, message: Cow<'static, str>) -> ValidationError {
27        self.message = Some(message);
28        self
29    }
30}
31
32impl std::error::Error for ValidationError {
33    fn description(&self) -> &str {
34        &self.code
35    }
36    fn cause(&self) -> Option<&dyn std::error::Error> {
37        None
38    }
39}
40
41#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
42#[serde(untagged)]
43pub enum ValidationErrorsKind {
44    Struct(Box<ValidationErrors>),
45    List(BTreeMap<usize, Box<ValidationErrors>>),
46    Field(Vec<ValidationError>),
47}
48
49#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq)]
50pub struct ValidationErrors(pub HashMap<Cow<'static, str>, ValidationErrorsKind>);
51
52impl ValidationErrors {
53    pub fn new() -> ValidationErrors {
54        ValidationErrors(HashMap::new())
55    }
56
57    /// Returns a boolean indicating whether a validation result includes validation errors for a
58    /// given field. May be used as a condition for performing nested struct validations on a field
59    /// in the absence of field-level validation errors.
60    #[must_use]
61    pub fn has_error(result: &Result<(), ValidationErrors>, field: &'static str) -> bool {
62        match result {
63            Ok(()) => false,
64            Err(ref errs) => errs.contains_key(field),
65        }
66    }
67
68    pub fn merge_self(
69        &mut self,
70        field: &'static str,
71        child: Result<(), ValidationErrors>,
72    ) -> &mut ValidationErrors {
73        match child {
74            Ok(()) => self,
75            Err(mut errors) => {
76                // This is a bit of a hack to be able to support collections which return a
77                // `ValidationErrors` with a made-up `_tmp_validator` entry which we need to strip
78                // off.
79                if let Some(collection) = errors.0.remove("_tmp_validator") {
80                    self.add_nested(field, collection);
81                } else {
82                    self.add_nested(field, ValidationErrorsKind::Struct(Box::new(errors)));
83                }
84
85                self
86            }
87        }
88    }
89
90    /// Returns the combined outcome of a struct's validation result along with the nested
91    /// validation result for one of its fields.
92    pub fn merge(
93        parent: Result<(), ValidationErrors>,
94        field: &'static str,
95        child: Result<(), ValidationErrors>,
96    ) -> Result<(), ValidationErrors> {
97        match child {
98            Ok(()) => parent,
99            Err(errors) => {
100                parent.and_then(|_| Err(ValidationErrors::new())).map_err(|mut parent_errors| {
101                    parent_errors.add_nested(field, ValidationErrorsKind::Struct(Box::new(errors)));
102                    parent_errors
103                })
104            }
105        }
106    }
107
108    /// Returns the combined outcome of a struct's validation result along with the nested
109    /// validation result for one of its fields where that field is a vector of validating structs.
110    pub fn merge_all(
111        parent: Result<(), ValidationErrors>,
112        field: &'static str,
113        children: Vec<Result<(), ValidationErrors>>,
114    ) -> Result<(), ValidationErrors> {
115        let errors = children
116            .into_iter()
117            .enumerate()
118            .filter_map(|(i, res)| res.err().map(|mut err| (i, err.remove(field))))
119            .filter_map(|(i, entry)| match entry {
120                Some(ValidationErrorsKind::Struct(errors)) => Some((i, errors)),
121                _ => None,
122            })
123            .collect::<BTreeMap<_, _>>();
124
125        if errors.is_empty() {
126            parent
127        } else {
128            parent.and_then(|_| Err(ValidationErrors::new())).map_err(|mut parent_errors| {
129                parent_errors.add_nested(field, ValidationErrorsKind::List(errors));
130                parent_errors
131            })
132        }
133    }
134
135    /// Returns a map of field-level validation errors found for the struct that was validated and
136    /// any of it's nested structs that are tagged for validation.
137    pub fn errors(&self) -> &HashMap<Cow<'static, str>, ValidationErrorsKind> {
138        &self.0
139    }
140
141    /// Returns a mutable map of field-level validation errors found for the struct that was validated and
142    /// any of it's nested structs that are tagged for validation.
143    pub fn errors_mut(&mut self) -> &mut HashMap<Cow<'static, str>, ValidationErrorsKind> {
144        &mut self.0
145    }
146
147    /// Consume the struct, returning the validation errors found
148    pub fn into_errors(self) -> HashMap<Cow<'static, str>, ValidationErrorsKind> {
149        self.0
150    }
151
152    /// Returns a map of only field-level validation errors found for the struct that was validated.
153    pub fn field_errors(&self) -> HashMap<Cow<'static, str>, &Vec<ValidationError>> {
154        self.0
155            .iter()
156            .filter_map(|(k, v)| {
157                if let ValidationErrorsKind::Field(errors) = v {
158                    Some((k.clone(), errors))
159                } else {
160                    None
161                }
162            })
163            .collect::<HashMap<_, _>>()
164    }
165
166    pub fn add(&mut self, field: &'static str, error: ValidationError) {
167        if let ValidationErrorsKind::Field(ref mut vec) = self
168            .0
169            .entry(Cow::Borrowed(field))
170            .or_insert_with(|| ValidationErrorsKind::Field(vec![]))
171        {
172            vec.push(error);
173        } else {
174            panic!("Attempt to add field validation to a non-Field ValidationErrorsKind instance");
175        }
176    }
177
178    #[must_use]
179    pub fn is_empty(&self) -> bool {
180        self.0.is_empty()
181    }
182
183    fn add_nested(&mut self, field: &'static str, errors: ValidationErrorsKind) {
184        if let Vacant(entry) = self.0.entry(Cow::Borrowed(field)) {
185            entry.insert(errors);
186        } else {
187            panic!("Attempt to replace non-empty ValidationErrors entry");
188        }
189    }
190
191    #[must_use]
192    fn contains_key(&self, field: &'static str) -> bool {
193        self.0.contains_key(field)
194    }
195
196    fn remove(&mut self, field: &'static str) -> Option<ValidationErrorsKind> {
197        self.0.remove(field)
198    }
199}
200
201impl std::error::Error for ValidationErrors {
202    fn description(&self) -> &str {
203        "Validation failed"
204    }
205    fn cause(&self) -> Option<&dyn std::error::Error> {
206        None
207    }
208}