use crate::context::{join_path, PathPart};
use crate::node::Kind;
use crate::value::{value_kind, value_to_string, Map, Value};
use std::fmt;
pub const MARK_OBJECT_REQUIRED: i64 = 1010;
pub const MARK_OBJECT_TYPE: i64 = 1020;
pub const MARK_ARRAY_REQUIRED: i64 = 1030;
pub const MARK_ARRAY_TYPE: i64 = 1040;
pub const MARK_CHECK_TYPE: i64 = 1045;
pub const MARK_SCALAR_TYPE: i64 = 1050;
pub const MARK_SCALAR_REQUIRED: i64 = 1060;
pub const MARK_NEVER: i64 = 1070;
pub const MARK_REGEXP: i64 = 1045;
pub const MARK_UNDEF_REQUIRED: i64 = 1080;
pub const MARK_ARRAY_CLOSED: i64 = 1090;
pub const MARK_OBJECT_CLOSED: i64 = 1100;
pub const MARK_CUSTOM_CHECK_ERR: i64 = 2010;
pub const MARK_CUSTOM_CHECK_TEXT: i64 = 4000;
pub const WHY_TYPE: &str = "type";
pub const WHY_REQUIRED: &str = "required";
pub const WHY_CLOSED: &str = "closed";
pub const WHY_CHECK: &str = "check";
pub const WHY_NEVER: &str = "never";
pub const WHY_REGEXP: &str = "regexp";
#[derive(Clone, Debug, Default)]
pub struct FieldError {
pub path: String,
pub path_arr: Vec<PathPart>,
pub key: String,
pub kind: Kind,
pub value: Value,
pub why: String,
pub mark: i64,
pub text: String,
pub args: Map,
pub check: String,
pub(crate) parent_arr: bool,
pub(crate) absent: bool,
pub(crate) regexp_src: String,
pub(crate) plural: bool,
pub(crate) terse: bool,
}
impl fmt::Display for FieldError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.text.is_empty() {
return f.write_str(&self.text);
}
if self.path.is_empty() {
f.write_str(&self.why)
} else {
write!(f, "{}: {}", self.path, self.why)
}
}
}
#[derive(Clone, Debug, Default)]
pub struct ValidationError {
pub issues: Vec<FieldError>,
pub(crate) terse: bool,
pub(crate) n: usize,
}
impl ValidationError {
pub(crate) fn add(&mut self, err: FieldError) {
if self.terse {
self.n += 1;
return;
}
self.issues.push(err);
}
pub fn has_any(&self) -> bool {
self.n > 0 || !self.issues.is_empty()
}
pub(crate) fn count(&self) -> usize {
self.n + self.issues.len()
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let parts: Vec<String> = self.issues.iter().map(|e| e.to_string()).collect();
f.write_str(&parts.join("\n"))
}
}
impl std::error::Error for ValidationError {}
pub(crate) struct At<'a> {
pub path: &'a [PathPart],
pub key: &'a str,
pub kind: Kind,
pub value: &'a Value,
pub parent_arr: bool,
pub absent: bool,
pub check: &'a str,
pub regexp_src: Option<String>,
pub terse: bool,
}
pub(crate) fn make_err(at: &At<'_>, why: &str, mark: i64, text: &str) -> FieldError {
let why = if why.is_empty() { WHY_CHECK } else { why };
let mark = if mark == 0 {
MARK_CUSTOM_CHECK_TEXT
} else {
mark
};
if at.terse {
return FieldError {
kind: at.kind,
mark,
terse: true,
..Default::default()
};
}
let path = join_path(at.path);
let mut err = FieldError {
path,
path_arr: at.path.to_vec(),
key: at.key.to_string(),
kind: at.kind,
value: at.value.clone(),
why: why.to_string(),
mark,
check: at.check.to_string(),
parent_arr: at.parent_arr,
absent: at.absent,
regexp_src: at.regexp_src.clone().unwrap_or_default(),
..Default::default()
};
err.text = if text.is_empty() {
default_err_text(&err)
} else {
expand_err_text(text, &err.path, at.value, at.absent)
};
err
}
pub(crate) fn expand_err_text(text: &str, path: &str, val: &Value, absent: bool) -> String {
let valstr = if absent {
"undefined".to_string()
} else {
value_to_string(val)
};
text.replace("$PATH", path).replace("$VALUE", &valstr)
}
pub(crate) fn default_err_text(e: &FieldError) -> String {
let (valstr, valkind) = if e.absent {
("undefined".to_string(), "value")
} else {
(value_to_string(&e.value), value_kind(&e.value))
};
let propkind = if e.parent_arr || valstr.starts_with('[') {
"index"
} else {
"property"
};
if e.why == WHY_REQUIRED && e.absent && !e.path.is_empty() {
let noun = if propkind == "index" {
"element"
} else {
"property"
};
return format!(
"Validation failed for {} \"{}\" because the {} is missing.",
propkind, e.path, noun
);
}
let mut out = String::with_capacity(96 + e.path.len() + valstr.len() + e.key.len());
out.push_str("Validation failed for ");
if !e.path.is_empty() {
out.push_str(propkind);
out.push_str(" \"");
out.push_str(&e.path);
out.push_str("\" with ");
}
out.push_str(valkind);
out.push_str(" \"");
out.push_str(&valstr);
out.push_str("\" because ");
match e.why.as_str() {
WHY_TYPE => {
out.push_str("the ");
out.push_str(valkind);
out.push_str(" is not of type ");
out.push_str(e.kind.as_str());
out.push('.');
}
WHY_REQUIRED => {
if matches!(e.value, Value::Str(ref s) if s.is_empty()) {
out.push_str("an empty string is not allowed.");
} else if e.value.is_null() || e.absent {
out.push_str("the value is required.");
} else {
out.push_str("the ");
out.push_str(valkind);
out.push_str(" is required.");
}
}
WHY_CLOSED => {
let (noun, verb) = if e.plural {
("properties", "are")
} else {
(propkind, "is")
};
out.push_str("the ");
out.push_str(noun);
out.push_str(" \"");
out.push_str(&e.key);
out.push_str("\" ");
out.push_str(verb);
out.push_str(" not allowed.");
}
WHY_NEVER => out.push_str("no value is allowed."),
WHY_REGEXP => {
out.push_str("the ");
out.push_str(valkind);
out.push_str(" did not match ");
out.push_str(&e.regexp_src);
out.push('.');
}
_ => {
let name = if e.check.is_empty() {
e.why.as_str()
} else {
e.check.as_str()
};
out.push_str("check \"");
out.push_str(name);
out.push_str("\" failed.");
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn at<'a>(path: &'a [PathPart], value: &'a Value, absent: bool) -> At<'a> {
At {
path,
key: "k",
kind: Kind::String,
value,
parent_arr: false,
absent,
check: "",
regexp_src: None,
terse: false,
}
}
#[test]
fn texts() {
let p = vec![PathPart::Key("a".into())];
let v = Value::from(1);
let e = make_err(&at(&p, &v, false), WHY_TYPE, MARK_SCALAR_TYPE, "");
assert_eq!(e.text, "Validation failed for property \"a\" with number \"1\" because the number is not of type string.");
assert_eq!(e.to_string(), e.text);
let e = make_err(&at(&p, &Value::Undefined, true), WHY_REQUIRED, 0, "");
assert_eq!(
e.text,
"Validation failed for property \"a\" because the property is missing."
);
assert_eq!(e.mark, MARK_CUSTOM_CHECK_TEXT);
let e = make_err(&at(&[], &Value::Undefined, true), WHY_REQUIRED, 1, "");
assert_eq!(
e.text,
"Validation failed for value \"undefined\" because the value is required."
);
let e = make_err(&at(&p, &Value::from(""), false), WHY_REQUIRED, 1, "");
assert_eq!(e.text, "Validation failed for property \"a\" with string \"\" because an empty string is not allowed.");
let e = make_err(&at(&p, &Value::Null, false), WHY_REQUIRED, 1, "");
assert_eq!(e.text, "Validation failed for property \"a\" with value \"null\" because the value is required.");
let e = make_err(&at(&p, &Value::from(2), false), WHY_REQUIRED, 1, "");
assert_eq!(e.text, "Validation failed for property \"a\" with number \"2\" because the number is required.");
let mut e = make_err(&at(&p, &v, false), WHY_CLOSED, 1, "");
e.plural = true;
e.key = "x, y".into();
assert_eq!(default_err_text(&e), "Validation failed for property \"a\" with number \"1\" because the properties \"x, y\" are not allowed.");
e.plural = false;
assert_eq!(default_err_text(&e), "Validation failed for property \"a\" with number \"1\" because the property \"x, y\" is not allowed.");
let e = make_err(&at(&p, &v, false), WHY_NEVER, 1, "");
assert_eq!(
e.text,
"Validation failed for property \"a\" with number \"1\" because no value is allowed."
);
let mut a = at(&p, &v, false);
a.regexp_src = Some("/x/".into());
let e = make_err(&a, WHY_REGEXP, 1, "");
assert_eq!(e.text, "Validation failed for property \"a\" with number \"1\" because the number did not match /x/.");
let mut a = at(&p, &v, false);
a.check = "Min";
let e = make_err(&a, "", 1, "");
assert_eq!(
e.text,
"Validation failed for property \"a\" with number \"1\" because check \"Min\" failed."
);
let e = make_err(&at(&p, &v, false), "why", 1, "");
assert_eq!(
e.text,
"Validation failed for property \"a\" with number \"1\" because check \"why\" failed."
);
let e = make_err(
&at(&p, &v, false),
"why",
1,
"Value \"$VALUE\" at \"$PATH\" is bad.",
);
assert_eq!(e.text, "Value \"1\" at \"a\" is bad.");
assert_eq!(
expand_err_text("$VALUE", "", &Value::Null, true),
"undefined"
);
let arr = Value::Arr(vec![Value::from(1)]);
let e = make_err(&at(&p, &arr, false), WHY_TYPE, 1, "");
assert_eq!(e.text, "Validation failed for index \"a\" with array \"[1]\" because the array is not of type string.");
let mut a = at(&p, &v, false);
a.parent_arr = true;
a.absent = true;
let e = make_err(&a, WHY_REQUIRED, 1, "");
assert_eq!(
e.text,
"Validation failed for index \"a\" because the element is missing."
);
let mut a = at(&p, &v, false);
a.terse = true;
let e = make_err(&a, WHY_TYPE, 1, "");
assert!(e.terse && e.text.is_empty() && e.path.is_empty());
let plain = FieldError {
path: "a".into(),
why: "w".into(),
..Default::default()
};
assert_eq!(plain.to_string(), "a: w");
let plain = FieldError {
why: "w".into(),
..Default::default()
};
assert_eq!(plain.to_string(), "w");
}
#[test]
fn collector() {
let mut v = ValidationError::default();
assert!(!v.has_any());
v.add(FieldError {
text: "a".into(),
..Default::default()
});
v.add(FieldError {
text: "b".into(),
..Default::default()
});
assert_eq!(v.to_string(), "a\nb");
let mut t = ValidationError {
terse: true,
..Default::default()
};
t.add(FieldError::default());
t.add(FieldError::default());
assert!(t.has_any() && t.issues.is_empty() && t.n == 2);
let _: &dyn std::error::Error = &v;
}
}