use serde_json::{json, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValidationErrorCode {
MissingField,
InvalidFormat,
OutOfRange,
NotAllowed,
TooShort,
TooLong,
TooFewItems,
TooManyItems,
PatternMismatch,
SecurityViolation,
TypeMismatch,
CustomValidation,
}
impl ValidationErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
Self::MissingField => "missing_field",
Self::InvalidFormat => "invalid_format",
Self::OutOfRange => "out_of_range",
Self::NotAllowed => "not_allowed",
Self::TooShort => "too_short",
Self::TooLong => "too_long",
Self::TooFewItems => "too_few_items",
Self::TooManyItems => "too_many_items",
Self::PatternMismatch => "pattern_mismatch",
Self::SecurityViolation => "security_violation",
Self::TypeMismatch => "type_mismatch",
Self::CustomValidation => "custom_validation",
}
}
pub fn description(&self) -> &'static str {
match self {
Self::MissingField => "Required field is missing",
Self::InvalidFormat => "Field value has invalid format",
Self::OutOfRange => "Value is outside the allowed range",
Self::NotAllowed => "Value is not in the allowed set",
Self::TooShort => "Value is too short",
Self::TooLong => "Value is too long",
Self::TooFewItems => "Collection has too few items",
Self::TooManyItems => "Collection has too many items",
Self::PatternMismatch => "Value does not match the required pattern",
Self::SecurityViolation => "Security constraint violated",
Self::TypeMismatch => "Value has incorrect type",
Self::CustomValidation => "Custom validation failed",
}
}
}
impl std::fmt::Display for ValidationErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub code: ValidationErrorCode,
pub field: String,
pub message: String,
pub expected: Option<String>,
pub actual: Option<Value>,
pub context: Option<Value>,
}
impl ValidationError {
pub fn new(code: ValidationErrorCode, field: impl Into<String>) -> Self {
let field = field.into();
let message = format!("{} for field '{}'", code.description(), field);
Self {
code,
field,
message,
expected: None,
actual: None,
context: None,
}
}
pub fn expected(mut self, expected: impl Into<String>) -> Self {
self.expected = Some(expected.into());
self
}
pub fn actual(mut self, actual: impl Into<Value>) -> Self {
self.actual = Some(actual.into());
self
}
pub fn context(mut self, context: impl Into<Value>) -> Self {
self.context = Some(context.into());
self
}
pub fn message(mut self, message: impl Into<String>) -> Self {
self.message = message.into();
self
}
pub fn to_json(&self) -> Value {
let mut obj = json!({
"code": self.code.as_str(),
"field": self.field,
"message": self.message,
"elicit": true,
});
if let Some(expected) = &self.expected {
obj["expected"] = json!(expected);
}
if let Some(actual) = &self.actual {
obj["actual"] = actual.clone();
}
if let Some(context) = &self.context {
obj["context"] = context.clone();
}
obj
}
pub fn to_error(&self) -> crate::Error {
crate::Error::Validation(self.to_json().to_string())
}
}
pub trait IntoValidationError {
fn into_validation_error(self, code: ValidationErrorCode, field: &str) -> crate::Error;
}
impl<T, E> IntoValidationError for Result<T, E>
where
E: std::fmt::Display,
{
fn into_validation_error(self, code: ValidationErrorCode, field: &str) -> crate::Error {
match self {
Ok(_) => panic!("Cannot convert Ok result to validation error"),
Err(e) => ValidationError::new(code, field)
.message(format!("{}: {}", code.description(), e))
.to_error(),
}
}
}
pub fn missing_field(field: &str) -> crate::Error {
ValidationError::new(ValidationErrorCode::MissingField, field)
.expected("This field is required")
.to_error()
}
pub fn invalid_format(field: &str, expected: &str) -> crate::Error {
ValidationError::new(ValidationErrorCode::InvalidFormat, field)
.expected(expected)
.to_error()
}
pub fn out_of_range<T: std::fmt::Display>(
field: &str,
value: T,
min: Option<T>,
max: Option<T>,
) -> crate::Error {
let expected = match (min, max) {
(Some(min), Some(max)) => format!("Value between {} and {}", min, max),
(Some(min), None) => format!("Value >= {}", min),
(None, Some(max)) => format!("Value <= {}", max),
(None, None) => "Value within valid range".to_string(),
};
ValidationError::new(ValidationErrorCode::OutOfRange, field)
.expected(expected)
.actual(json!(value.to_string()))
.to_error()
}
pub fn not_allowed<T: std::fmt::Display>(field: &str, value: T, allowed: &[T]) -> crate::Error {
let allowed_str = allowed
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(", ");
ValidationError::new(ValidationErrorCode::NotAllowed, field)
.expected(format!("One of: {}", allowed_str))
.actual(json!(value.to_string()))
.to_error()
}
pub fn pattern_mismatch(field: &str, pattern: &str) -> crate::Error {
ValidationError::new(ValidationErrorCode::PatternMismatch, field)
.expected(format!("Match pattern: {}", pattern))
.to_error()
}
pub fn security_violation(field: &str, reason: &str) -> crate::Error {
ValidationError::new(ValidationErrorCode::SecurityViolation, field)
.message(format!("Security violation: {}", reason))
.to_error()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_error_json() {
let error = ValidationError::new(ValidationErrorCode::OutOfRange, "age")
.expected("18-120")
.actual(json!(150))
.message("Age must be between 18 and 120");
let json = error.to_json();
assert_eq!(json["code"], "out_of_range");
assert_eq!(json["field"], "age");
assert_eq!(json["expected"], "18-120");
assert_eq!(json["actual"], 150);
assert_eq!(json["elicit"], true);
}
#[test]
fn test_convenience_functions() {
let error = missing_field("email");
assert!(error.to_string().contains("missing_field"));
let error = invalid_format("email", "user@example.com");
assert!(error.to_string().contains("invalid_format"));
let error = out_of_range("age", 200, Some(18), Some(120));
assert!(error.to_string().contains("out_of_range"));
}
}