use std::fmt;
mod builder;
mod code;
pub mod format;
pub mod i18n;
mod i18n_langs;
pub use crate::object::{FieldResult, ParseResult};
pub use builder::IssueBuilder;
pub use code::{DateKind, FileTypeKind, FormatKind, IssueCode, SizeOrigin, StringValidation};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum PathSegment {
Field(String),
Index(usize),
}
impl fmt::Display for PathSegment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PathSegment::Field(name) => write!(f, ".{}", name),
PathSegment::Index(idx) => write!(f, "[{}]", idx),
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ValidationIssue {
pub code: IssueCode,
pub message: String,
pub path: Vec<PathSegment>,
pub received: Option<serde_json::Value>,
}
impl ValidationIssue {
pub fn new(code: IssueCode) -> Self {
Self {
message: code.default_message(),
code,
path: vec![],
received: None,
}
}
pub fn with_message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
pub fn with_received(mut self, value: &serde_json::Value) -> Self {
self.received = Some(truncate_value(value));
self
}
pub fn has_custom_message(&self) -> bool {
let (_, message) = i18n::split_union_prefix(&self.message);
message != self.code.default_message()
}
}
impl From<ValidationIssue> for VldError {
fn from(issue: ValidationIssue) -> Self {
Self {
issues: vec![issue],
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct VldError {
pub issues: Vec<ValidationIssue>,
}
impl VldError {
pub fn new() -> Self {
Self { issues: vec![] }
}
pub fn from_code(code: IssueCode) -> Self {
ValidationIssue::new(code).into()
}
pub fn from_code_with_value(code: IssueCode, received: &serde_json::Value) -> Self {
ValidationIssue::new(code).with_received(received).into()
}
pub fn single(code: IssueCode, message: impl Into<String>) -> Self {
ValidationIssue::new_raw(code, message.into()).into()
}
pub fn single_with_value(
code: IssueCode,
message: impl Into<String>,
received: &serde_json::Value,
) -> Self {
ValidationIssue::new_raw(code, message.into())
.with_received(received)
.into()
}
pub fn with_prefix(mut self, segment: PathSegment) -> Self {
for issue in &mut self.issues {
issue.path.insert(0, segment.clone());
}
self
}
pub fn merge(mut self, other: VldError) -> Self {
self.issues.extend(other.issues);
self
}
pub fn is_empty(&self) -> bool {
self.issues.is_empty()
}
pub fn push(&mut self, code: IssueCode, message: impl Into<String>) {
self.issues
.push(ValidationIssue::new_raw(code, message.into()));
}
pub fn push_with_value(
&mut self,
code: IssueCode,
message: impl Into<String>,
received: &serde_json::Value,
) {
self.issues
.push(ValidationIssue::new_raw(code, message.into()).with_received(received));
}
pub fn push_code(&mut self, code: IssueCode) {
self.issues.push(ValidationIssue::new(code));
}
pub fn push_code_with_value(&mut self, code: IssueCode, received: &serde_json::Value) {
self.issues
.push(ValidationIssue::new(code).with_received(received));
}
}
impl ValidationIssue {
fn new_raw(code: IssueCode, message: String) -> Self {
Self {
code,
message,
path: vec![],
received: None,
}
}
pub(crate) fn with_override(code: IssueCode, custom: Option<&String>) -> Self {
match custom {
Some(msg) => Self::new_raw(code, msg.clone()),
None => Self::new(code),
}
}
}
pub(crate) fn issue_error(
code: IssueCode,
custom: Option<&String>,
received: &serde_json::Value,
) -> VldError {
ValidationIssue::with_override(code, custom)
.with_received(received)
.into()
}
pub(crate) fn absent_field_error(err: VldError) -> VldError {
let rejected_null = err.issues.iter().find(|i| {
i.path.is_empty()
&& matches!(&i.code, IssueCode::InvalidType { received, .. } if received == "null")
});
match rejected_null {
Some(issue) => {
let custom = issue.message != issue.code.default_message();
let missing = ValidationIssue::new(IssueCode::MissingField);
if custom {
missing.with_message(issue.message.clone()).into()
} else {
missing.into()
}
}
None => err,
}
}
impl Default for VldError {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for VldError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, issue) in self.issues.iter().enumerate() {
if i > 0 {
writeln!(f)?;
}
if !issue.path.is_empty() {
let path_str: String = issue.path.iter().map(|p| p.to_string()).collect();
write!(f, "{}: ", path_str)?;
}
write!(f, "{}", issue.message)?;
if let (Some(val), false) = (
&issue.received,
matches!(issue.code, IssueCode::InvalidType { .. }),
) {
write!(f, ", received {}", format_value_short(val))?;
}
}
Ok(())
}
}
impl std::error::Error for VldError {}
pub(crate) fn value_type_name(value: &serde_json::Value) -> String {
match value {
serde_json::Value::Null => "null",
serde_json::Value::Bool(_) => "boolean",
serde_json::Value::Number(_) => "number",
serde_json::Value::String(_) => "string",
serde_json::Value::Array(_) => "array",
serde_json::Value::Object(_) => "object",
}
.to_string()
}
pub(crate) fn expect_object(
value: &serde_json::Value,
) -> Result<&serde_json::Map<String, serde_json::Value>, VldError> {
value
.as_object()
.ok_or_else(|| crate::primitives::type_error(&None, "object", value))
}
pub(crate) fn format_value_short(value: &serde_json::Value) -> String {
match value {
serde_json::Value::Null => "null".to_string(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::String(s) => {
let char_count = s.chars().count();
if char_count > 50 {
let prefix: String = s.chars().take(47).collect();
format!("\"{}...\"", prefix)
} else {
format!("\"{}\"", s)
}
}
serde_json::Value::Array(arr) => format!("Array(len={})", arr.len()),
serde_json::Value::Object(obj) => format!("Object(keys={})", obj.len()),
}
}
pub(crate) fn truncate_value(value: &serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::String(s) if s.chars().count() > 100 => {
let prefix: String = s.chars().take(97).collect();
serde_json::Value::String(format!("{}...", prefix))
}
serde_json::Value::Array(arr) if arr.len() > 5 => {
let mut truncated: Vec<serde_json::Value> = arr[..5].to_vec();
truncated.push(serde_json::Value::String(format!(
"... ({} more)",
arr.len() - 5
)));
serde_json::Value::Array(truncated)
}
_ => value.clone(),
}
}