#![deny(missing_docs)]
#![deny(warnings)]
use std::any::Any;
use std::collections::hash_map::{Entry, HashMap};
use std::error::Error as StdError;
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
#[derive(Debug)]
pub struct Error<T> where T: Debug + Any {
details: Option<T>,
message: String,
}
#[derive(Debug)]
pub struct Errors<T> where T: Debug + Any {
base: Option<Vec<Error<T>>>,
fields: Option<HashMap<String, Box<Errors<T>>>>,
}
pub type SimpleError = Error<()>;
pub type SimpleErrors = Errors<()>;
pub trait Validate<T> where T: Debug + Any {
fn validate(&self) -> Result<(), Errors<T>>;
}
impl<T> Error<T> where T: Debug + Any {
pub fn new<S>(message: S) -> Self where S: Into<String> {
Error {
details: None,
message: message.into(),
}
}
pub fn with_details<S>(message: S, details: T) -> Self where S: Into<String> {
Error {
details: Some(details),
message: message.into(),
}
}
pub fn details(&self) -> Option<&T> {
self.details.as_ref()
}
pub fn set_details(&mut self, details: T) {
self.details = Some(details);
}
pub fn message(&self) -> &str {
&self.message
}
}
impl<T> Display for Error<T> where T: Debug + Any {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "{}", &self.message)
}
}
impl<T> StdError for Error<T> where T: Debug + Any {
fn description(&self) -> &str {
&self.message
}
}
impl<T> Errors<T> where T: Debug + Any {
pub fn new() -> Self {
Errors {
base: None,
fields: None,
}
}
pub fn add_error(&mut self, error: Error<T>) {
match self.base {
Some(ref mut base_errors) => base_errors.push(error),
None => self.base = Some(vec![error]),
}
}
pub fn add_field_error<S>(&mut self, field: S, error: Error<T>) where S: Into<String>{
match self.fields {
Some(ref mut field_errors) => {
match field_errors.entry(field.into()) {
Entry::Occupied(mut entry) => {
entry.get_mut().add_error(error);
}
Entry::Vacant(entry) => {
let mut errors = Errors::new();
errors.add_error(error);
entry.insert(Box::new(errors));
}
}
}
None => {
let mut errors = Errors::new();
errors.add_error(error);
let mut map = HashMap::new();
map.insert(field.into(), Box::new(errors));
self.fields = Some(map);
}
}
}
pub fn base<'a>(&'a self) -> Option<&'a [Error<T>]> {
self.base.as_ref().map(Vec::as_slice)
}
pub fn field<F>(&self, field: F) -> Option<&Box<Errors<T>>> where F: Into<String> {
if self.fields.is_some() {
self.fields.as_ref().unwrap().get(&field.into())
} else {
None
}
}
pub fn is_empty(&self) -> bool {
self.base.is_none() && self.fields.is_none()
}
pub fn set_field_errors<S>(&mut self, field: S, errors: Errors<T>) where S: Into<String>{
match self.fields {
Some(ref mut field_errors) => {
field_errors.insert(field.into(), Box::new(errors));
}
None => {
let mut map = HashMap::new();
map.insert(field.into(), Box::new(errors));
self.fields = Some(map);
}
}
}
}
impl<T> Display for Errors<T> where T: Debug + Any {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
write!(f, "validation failed")
}
}
impl<T> StdError for Errors<T> where T: Debug + Any {
fn description(&self) -> &str {
"validation failed"
}
}
#[cfg(test)]
mod tests {
use super::{Error, Errors, Validate};
#[derive(Debug)]
struct AddressBookEntry {
cell_number: Option<PhoneNumber>,
email: Option<Email>,
home_number: Option<PhoneNumber>,
name: &'static str,
}
#[derive(Debug)]
struct Email(&'static str);
#[derive(Debug)]
struct PhoneNumber {
area_code: &'static str,
number: &'static str,
}
#[derive(Debug)]
struct InvalidCharacters {
invalid_characters: Vec<char>,
}
impl Validate<InvalidCharacters> for AddressBookEntry {
fn validate(&self) -> Result<(), Errors<InvalidCharacters>> {
let mut errors = Errors::new();
if self.cell_number.is_none() && self.home_number.is_none() {
errors.add_error(Error::new("at least one phone number is required"));
}
if self.name.len() == 0 {
errors.add_field_error("name", Error::new("can't be blank"));
}
if let Some(ref email) = self.email {
if let Err(field_errors) = email.validate() {
errors.set_field_errors("email", field_errors);
}
}
let numbers_to_check = [
("home_number", &self.home_number),
("cell_number", &self.cell_number),
];
for &(field_name, field) in &numbers_to_check {
if field.is_some() {
let invalid_characters = InvalidCharacters::check_digits(
&field.as_ref().unwrap().full_number()
);
if invalid_characters.len() > 0 {
errors.add_field_error(
field_name,
Error::with_details(
"has invalid characters",
InvalidCharacters {
invalid_characters: invalid_characters,
},
),
);
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}
impl Validate<InvalidCharacters> for Email {
fn validate(&self) -> Result<(), Errors<InvalidCharacters>> {
let email = self.0;
if !email.contains("@") {
let mut errors = Errors::new();
errors.add_error(Error::new("must contain an @ symbol"));
return Err(errors);
}
Ok(())
}
}
impl PhoneNumber {
pub fn full_number(&self) -> String {
format!("{}-{}", self.area_code, self.number)
}
}
impl InvalidCharacters {
pub fn check_digits(number: &str) -> Vec<char> {
number.replace("-", "").chars().filter(|c| !c.is_digit(10)).collect()
}
pub fn invalid_characters(&self) -> &[char] {
self.invalid_characters.as_slice()
}
}
#[test]
fn valid_value() {
let entry = AddressBookEntry {
cell_number: None,
email: Some(Email("rcohle@dps.la.gov")),
home_number: Some(PhoneNumber {
area_code: "555",
number: "555-5555",
}),
name: "Rust Cohle",
};
assert!(entry.validate().is_ok());
}
#[test]
fn base_error() {
let entry = AddressBookEntry {
cell_number: None,
email: Some(Email("rcohle@dps.la.gov")),
home_number: None,
name: "Rust Cohle",
};
let errors = entry.validate().err().unwrap();
assert_eq!(
errors.base().unwrap()[0].message(),
"at least one phone number is required".to_string()
);
}
#[test]
fn field_error() {
let entry = AddressBookEntry {
cell_number: None,
email: Some(Email("rcohle@dps.la.gov")),
home_number: Some(PhoneNumber {
area_code: "555",
number: "555-5555",
}),
name: "",
};
let errors = entry.validate().err().unwrap();
assert_eq!(
errors.field("name").unwrap().base().unwrap()[0].message(),
"can't be blank".to_string()
);
}
#[test]
fn delegate_to_field() {
let entry = AddressBookEntry {
cell_number: None,
email: Some(Email("rcohle")),
home_number: Some(PhoneNumber {
area_code: "555",
number: "555-5555",
}),
name: "Rust Cohle",
};
let errors = entry.validate().err().unwrap();
assert_eq!(
errors.field("email").unwrap().base().unwrap()[0].message(),
"must contain an @ symbol".to_string()
);
}
#[test]
fn details() {
let entry = AddressBookEntry {
cell_number: None,
email: Some(Email("rcohle@dps.la.gov")),
home_number: Some(PhoneNumber {
area_code: "555",
number: "x55-55t5",
}),
name: "",
};
let errors = entry.validate().err().unwrap();
let invalid_characters = errors.field("home_number").unwrap().base().unwrap()[0]
.details().unwrap().invalid_characters();
assert!(invalid_characters.contains(&'x'));
assert!(invalid_characters.contains(&'t'));
}
}