1#![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#[derive(Debug)]
92pub struct Error<T> where T: Debug + Any {
93 details: Option<T>,
94 message: String,
95}
96
97#[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
104pub type SimpleError = Error<()>;
106
107pub type SimpleErrors = Errors<()>;
109
110pub trait Validate<T> where T: Debug + Any {
112 fn validate(&self) -> Result<(), Errors<T>>;
116}
117
118impl<T> Error<T> where T: Debug + Any {
119 pub fn new<S>(message: S) -> Self where S: Into<String> {
121 Error {
122 details: None,
123 message: message.into(),
124 }
125 }
126
127 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 pub fn details(&self) -> Option<&T> {
137 self.details.as_ref()
138 }
139
140 pub fn set_details(&mut self, details: T) {
142 self.details = Some(details);
143 }
144
145 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 pub fn new() -> Self {
166 Errors {
167 base: None,
168 fields: None,
169 }
170 }
171
172 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 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 pub fn base<'a>(&'a self) -> Option<&'a [Error<T>]> {
215 self.base.as_ref().map(Vec::as_slice)
216 }
217
218 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 pub fn is_empty(&self) -> bool {
229 self.base.is_none() && self.fields.is_none()
230 }
231
232 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}