use std::fmt;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "deserialize", derive(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)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
pub enum StringValidation {
Email,
Url,
UrlStrict,
Uri,
Uuid,
UuidV1,
UuidV4,
UuidV7,
Ip,
Slug,
Color,
Currency,
CountryCode,
Locale,
Cron,
Regex,
StartsWith,
EndsWith,
Ipv4,
Ipv6,
Cidr,
Mac,
Hex,
CreditCard,
Phone,
PhoneE164,
Semver,
SemverFull,
Jwt,
Ascii,
Alpha,
Alphanumeric,
Lowercase,
Uppercase,
Base64,
IsoDate,
IsoDatetime,
IsoTime,
Hostname,
Cuid2,
Ulid,
Nanoid,
Emoji,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
pub enum IssueCode {
InvalidType { expected: String, received: String },
TooSmall { minimum: f64, inclusive: bool },
TooBig { maximum: f64, inclusive: bool },
InvalidString { validation: StringValidation },
NotInt,
NotFinite,
MissingField,
UnrecognizedField,
IoError,
ParseError,
Custom { code: String },
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
pub struct ValidationIssue {
pub code: IssueCode,
pub message: String,
pub path: Vec<PathSegment>,
pub received: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
pub struct VldError {
pub issues: Vec<ValidationIssue>,
}
impl VldError {
pub fn new() -> Self {
Self { issues: vec![] }
}
pub fn single(code: IssueCode, message: impl Into<String>) -> Self {
Self {
issues: vec![ValidationIssue {
code,
message: message.into(),
path: vec![],
received: None,
}],
}
}
pub fn single_with_value(
code: IssueCode,
message: impl Into<String>,
received: &serde_json::Value,
) -> Self {
Self {
issues: vec![ValidationIssue {
code,
message: message.into(),
path: vec![],
received: Some(truncate_value(received)),
}],
}
}
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 {
code,
message: message.into(),
path: vec![],
received: None,
});
}
pub fn push_with_value(
&mut self,
code: IssueCode,
message: impl Into<String>,
received: &serde_json::Value,
) {
self.issues.push(ValidationIssue {
code,
message: message.into(),
path: vec![],
received: Some(truncate_value(received)),
});
}
}
pub struct IssueBuilder<'a> {
errors: &'a mut VldError,
code: IssueCode,
message: Option<String>,
path: Vec<PathSegment>,
received: Option<serde_json::Value>,
}
impl<'a> IssueBuilder<'a> {
pub fn message(mut self, msg: impl Into<String>) -> Self {
self.message = Some(msg.into());
self
}
pub fn path_field(mut self, name: impl Into<String>) -> Self {
self.path.push(PathSegment::Field(name.into()));
self
}
pub fn path_index(mut self, idx: usize) -> Self {
self.path.push(PathSegment::Index(idx));
self
}
pub fn received(mut self, value: &serde_json::Value) -> Self {
self.received = Some(truncate_value(value));
self
}
pub fn finish(self) {
let msg = self
.message
.unwrap_or_else(|| format!("Validation error: {}", self.code.key()));
self.errors.issues.push(ValidationIssue {
code: self.code,
message: msg,
path: self.path,
received: self.received,
});
}
}
impl VldError {
pub fn issue(&mut self, code: IssueCode) -> IssueBuilder<'_> {
IssueBuilder {
errors: self,
code,
message: None,
path: vec![],
received: None,
}
}
}
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) = &issue.received {
write!(f, ", received {}", format_value_short(val))?;
}
}
Ok(())
}
}
impl std::error::Error for VldError {}
impl IssueCode {
pub fn key(&self) -> &str {
match self {
IssueCode::InvalidType { .. } => "invalid_type",
IssueCode::TooSmall { .. } => "too_small",
IssueCode::TooBig { .. } => "too_big",
IssueCode::InvalidString { .. } => "invalid_string",
IssueCode::NotInt => "not_int",
IssueCode::NotFinite => "not_finite",
IssueCode::MissingField => "missing_field",
IssueCode::UnrecognizedField => "unrecognized_field",
IssueCode::IoError => "io_error",
IssueCode::ParseError => "parse_error",
IssueCode::Custom { code } => code,
}
}
pub fn params(&self) -> Vec<(&str, String)> {
match self {
IssueCode::InvalidType { expected, received } => {
vec![
("expected", expected.clone()),
("received", received.clone()),
]
}
IssueCode::TooSmall { minimum, inclusive } => {
vec![
("minimum", minimum.to_string()),
("inclusive", inclusive.to_string()),
]
}
IssueCode::TooBig { maximum, inclusive } => {
vec![
("maximum", maximum.to_string()),
("inclusive", inclusive.to_string()),
]
}
IssueCode::InvalidString { validation } => {
vec![("validation", format!("{:?}", validation))]
}
_ => vec![],
}
}
}
#[doc(hidden)]
pub 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 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()),
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))]
pub struct FieldResult {
pub name: String,
pub input: serde_json::Value,
pub result: Result<serde_json::Value, VldError>,
}
impl FieldResult {
pub fn is_ok(&self) -> bool {
self.result.is_ok()
}
pub fn is_err(&self) -> bool {
self.result.is_err()
}
}
impl std::fmt::Display for FieldResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.result {
Ok(v) => write!(f, "✔ {}: {}", self.name, format_value_short(v)),
Err(e) => {
let received = format_value_short(&self.input);
for (i, issue) in e.issues.iter().enumerate() {
if i > 0 {
writeln!(f)?;
}
write!(
f,
"✖ {}: {} (received: {})",
self.name, issue.message, received
)?;
}
Ok(())
}
}
}
}
#[derive(Debug)]
pub struct ParseResult<T> {
pub value: T,
field_results: Vec<FieldResult>,
}
impl<T> ParseResult<T> {
pub fn new(value: T, field_results: Vec<FieldResult>) -> Self {
Self {
value,
field_results,
}
}
pub fn fields(&self) -> &[FieldResult] {
&self.field_results
}
pub fn field(&self, name: &str) -> Option<&FieldResult> {
self.field_results.iter().find(|f| f.name == name)
}
pub fn valid_fields(&self) -> Vec<&FieldResult> {
self.field_results.iter().filter(|f| f.is_ok()).collect()
}
pub fn error_fields(&self) -> Vec<&FieldResult> {
self.field_results.iter().filter(|f| f.is_err()).collect()
}
pub fn is_valid(&self) -> bool {
self.field_results.iter().all(|f| f.is_ok())
}
pub fn has_errors(&self) -> bool {
self.field_results.iter().any(|f| f.is_err())
}
pub fn valid_count(&self) -> usize {
self.field_results.iter().filter(|f| f.is_ok()).count()
}
pub fn error_count(&self) -> usize {
self.field_results.iter().filter(|f| f.is_err()).count()
}
pub fn into_value(self) -> T {
self.value
}
pub fn into_parts(self) -> (T, Vec<FieldResult>) {
(self.value, self.field_results)
}
}
#[cfg(feature = "serialize")]
impl<T: serde::Serialize> ParseResult<T> {
#[cfg(feature = "std")]
pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
let json = serde_json::to_string_pretty(&self.value)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
std::fs::write(path, json)
}
pub fn to_json_string(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(&self.value)
}
pub fn to_json_value(&self) -> Result<serde_json::Value, serde_json::Error> {
serde_json::to_value(&self.value)
}
}
impl<T: std::fmt::Debug> std::fmt::Display for ParseResult<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"ParseResult ({} valid, {} errors):",
self.valid_count(),
self.error_count()
)?;
for field in &self.field_results {
writeln!(f, " {}", field)?;
}
Ok(())
}
}
fn truncate_value(value: &serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::String(s) if s.len() > 100 => {
serde_json::Value::String(format!("{}...", &s[..97]))
}
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(),
}
}