use serde_json::{Map, Value};
use validator::{ValidationError, ValidationErrors, ValidationErrorsKind};
use crate::api::{Problem, ProblemKind};
#[must_use]
pub fn validation_problem(errors: ValidationErrors) -> Problem {
Problem::of(ProblemKind::Validation)
.with_detail("Request validation failed")
.with_extension("errors", flatten_errors(&errors))
}
pub fn validate_or_problem<T>(value: &T) -> Result<(), Problem>
where
T: validator::Validate,
{
value.validate().map_err(validation_problem)
}
const COLLECTION_ROOT_KEY: &str = "_tmp_validator";
fn flatten_errors(errors: &ValidationErrors) -> Value {
if errors.0.len() == 1
&& let Some(kind) = errors.0.get(COLLECTION_ROOT_KEY)
{
return flatten_kind(kind);
}
let mut map = Map::new();
for (field, kind) in &errors.0 {
if field == COLLECTION_ROOT_KEY {
map.insert((*field).to_string(), flatten_kind(kind));
continue;
}
map.insert((*field).to_string(), flatten_kind(kind));
}
Value::Object(map)
}
fn flatten_kind(kind: &ValidationErrorsKind) -> Value {
match kind {
ValidationErrorsKind::Field(errors) => {
Value::Array(errors.iter().map(error_object).collect())
}
ValidationErrorsKind::Struct(inner) => flatten_errors(inner),
ValidationErrorsKind::List(indexed) => {
let mut map = Map::new();
for (index, inner) in indexed {
map.insert(index.to_string(), flatten_errors(inner));
}
Value::Object(map)
}
}
}
fn error_object(error: &ValidationError) -> Value {
let mut map = Map::new();
map.insert("code".to_string(), Value::String(error.code.to_string()));
if let Some(message) = &error.message {
map.insert("message".to_string(), Value::String(message.to_string()));
}
Value::Object(map)
}
#[cfg(test)]
mod tests {
use super::*;
use std::borrow::Cow;
use std::collections::BTreeMap;
fn field_error(code: &'static str) -> ValidationError {
ValidationError::new(code)
}
fn field_error_with_message(code: &'static str, message: &'static str) -> ValidationError {
ValidationError::new(code).with_message(Cow::from(message))
}
#[test]
fn flat_field_errors_serialize_to_array() {
let mut errors = ValidationErrors::new();
errors.add("name", field_error("required"));
errors.add("name", field_error("length"));
errors.add("email", field_error("email"));
let value = flatten_errors(&errors);
assert_eq!(value["name"][0]["code"], "required");
assert_eq!(value["name"][1]["code"], "length");
assert_eq!(value["email"][0]["code"], "email");
assert!(value["name"][0].get("message").is_none());
}
#[test]
fn message_is_emitted_when_present() {
let mut errors = ValidationErrors::new();
errors.add("name", field_error_with_message("custom", "name taken"));
let value = flatten_errors(&errors);
assert_eq!(value["name"][0]["code"], "custom");
assert_eq!(value["name"][0]["message"], "name taken");
}
#[test]
fn nested_struct_errors_recurse() {
let mut inner = ValidationErrors::new();
inner.add("street", field_error("required"));
let mut outer = ValidationErrors::new();
outer.0.insert(
Cow::Borrowed("address"),
ValidationErrorsKind::Struct(Box::new(inner)),
);
let value = flatten_errors(&outer);
assert_eq!(value["address"]["street"][0]["code"], "required");
}
#[test]
fn list_errors_are_keyed_by_index() {
let mut item0 = ValidationErrors::new();
item0.add("id", field_error("range"));
let mut indexed: BTreeMap<usize, Box<ValidationErrors>> = BTreeMap::new();
indexed.insert(0, Box::new(item0));
let mut list_root = ValidationErrors::new();
list_root.0.insert(
Cow::Borrowed(COLLECTION_ROOT_KEY),
ValidationErrorsKind::List(indexed),
);
let value = flatten_errors(&list_root);
assert_eq!(value["0"]["id"][0]["code"], "range");
assert!(value.get(COLLECTION_ROOT_KEY).is_none());
}
#[test]
fn validation_problem_has_errors_extension_and_422() {
let mut errors = ValidationErrors::new();
errors.add("name", field_error("required"));
let problem = validation_problem(errors);
let json = problem.to_json();
assert_eq!(json["status"], 422);
assert_eq!(json["type"], "urn:arcature:problem:validation");
assert_eq!(json["errors"]["name"][0]["code"], "required");
}
#[test]
fn error_object_omits_params() {
let mut error = field_error("range");
error.add_param(Cow::from("min"), &0);
error.add_param(Cow::from("value"), &9999);
let value = flatten_errors(&{
let mut e = ValidationErrors::new();
e.add("n", error);
e
});
assert!(value["n"][0].get("params").is_none());
assert_eq!(value["n"][0]["code"], "range");
}
#[test]
fn validate_or_problem_passes_on_valid_and_errors_on_invalid() {
use validator::Validate;
#[derive(Validate)]
struct Ok {
#[validate(length(min = 1))]
name: String,
}
#[derive(Validate)]
struct Bad {
#[validate(length(min = 1))]
name: String,
}
assert!(validate_or_problem(&Ok { name: "x".into() }).is_ok());
let err = validate_or_problem(&Bad {
name: String::new(),
})
.unwrap_err();
assert_eq!(err.status().as_u16(), 422);
}
}