use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
pub trait Translator {
fn translate(
&self,
code: &str,
field: &str,
params: Option<&HashMap<String, serde_json::Value>>,
) -> Option<String>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldError {
pub field: String,
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<HashMap<String, serde_json::Value>>,
}
impl FieldError {
pub fn new(
field: impl Into<String>,
code: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
field: field.into(),
code: code.into(),
message: message.into(),
params: None,
}
}
pub fn with_params(
field: impl Into<String>,
code: impl Into<String>,
message: impl Into<String>,
params: HashMap<String, serde_json::Value>,
) -> Self {
Self {
field: field.into(),
code: code.into(),
message: message.into(),
params: Some(params),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ErrorBody {
#[serde(rename = "type")]
error_type: String,
message: String,
fields: Vec<FieldError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ErrorWrapper {
error: ErrorBody,
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub fields: Vec<FieldError>,
pub message: String,
}
impl ValidationError {
pub fn new(fields: Vec<FieldError>) -> Self {
Self {
fields,
message: "Validation failed".to_string(),
}
}
pub fn with_message(fields: Vec<FieldError>, message: impl Into<String>) -> Self {
Self {
fields,
message: message.into(),
}
}
pub fn field(
field: impl Into<String>,
code: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self::new(vec![FieldError::new(field, code, message)])
}
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
pub fn len(&self) -> usize {
self.fields.len()
}
pub fn add(&mut self, error: FieldError) {
self.fields.push(error);
}
pub fn localize<T: Translator>(&self, translator: &T) -> Self {
let fields = self
.fields
.iter()
.map(|f| {
let message = translator
.translate(&f.code, &f.field, f.params.as_ref())
.unwrap_or_else(|| f.message.clone());
FieldError {
field: f.field.clone(),
code: f.code.clone(),
message,
params: f.params.clone(),
}
})
.collect();
Self {
fields,
message: self.message.clone(),
}
}
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {} field error(s)", self.message, self.fields.len())
}
}
impl std::error::Error for ValidationError {}
impl Serialize for ValidationError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let wrapper = ErrorWrapper {
error: ErrorBody {
error_type: "validation_error".to_string(),
message: self.message.clone(),
fields: self.fields.clone(),
},
};
wrapper.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ValidationError {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let wrapper = ErrorWrapper::deserialize(deserializer)?;
Ok(Self {
fields: wrapper.error.fields,
message: wrapper.error.message,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn field_error_creation() {
let error = FieldError::new("email", "email", "Invalid email format");
assert_eq!(error.field, "email");
assert_eq!(error.code, "email");
assert_eq!(error.message, "Invalid email format");
assert!(error.params.is_none());
}
#[test]
fn validation_error_serialization() {
let error = ValidationError::new(vec![FieldError::new(
"email",
"email",
"Invalid email format",
)]);
let json = serde_json::to_value(&error).unwrap();
assert_eq!(json["error"]["type"], "validation_error");
assert_eq!(json["error"]["message"], "Validation failed");
assert_eq!(json["error"]["fields"][0]["field"], "email");
}
#[test]
fn validation_error_display() {
let error = ValidationError::new(vec![
FieldError::new("email", "email", "Invalid email"),
FieldError::new("age", "range", "Out of range"),
]);
assert_eq!(error.to_string(), "Validation failed: 2 field error(s)");
}
}