use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RuleError {
pub code: String,
pub message: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub params: HashMap<String, serde_json::Value>,
}
impl RuleError {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
Self {
code: code.into(),
message: message.into(),
params: HashMap::new(),
}
}
pub fn with_params(
code: impl Into<String>,
message: impl Into<String>,
params: HashMap<String, serde_json::Value>,
) -> Self {
Self {
code: code.into(),
message: message.into(),
params,
}
}
pub fn param(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
if let Ok(v) = serde_json::to_value(value) {
self.params.insert(key.into(), v);
}
self
}
pub fn interpolate_with_locale(&self, locale: Option<&str>) -> String {
let msg = crate::v2::i18n::translate(&self.message, locale);
let mut result = msg;
for (key, value) in &self.params {
let placeholder = format!("{{{}}}", key);
let p_placeholder = format!("%{{{}}}", key);
let replacement = match value {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
_ => value.to_string(),
};
result = result.replace(&p_placeholder, &replacement);
result = result.replace(&placeholder, &replacement);
}
result
}
pub fn interpolate_message(&self) -> String {
self.interpolate_with_locale(None)
}
}
impl fmt::Display for RuleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}] {}", self.code, self.interpolate_message())
}
}
impl std::error::Error for RuleError {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ValidationErrors {
#[serde(flatten)]
pub fields: HashMap<String, Vec<RuleError>>,
}
impl ValidationErrors {
pub fn new() -> Self {
Self {
fields: HashMap::new(),
}
}
pub fn add(&mut self, field: impl Into<String>, error: RuleError) {
self.fields.entry(field.into()).or_default().push(error);
}
pub fn add_all(&mut self, field: impl Into<String>, errors: Vec<RuleError>) {
let field = field.into();
for error in errors {
self.add(field.clone(), error);
}
}
pub fn merge(&mut self, other: ValidationErrors) {
for (field, errors) in other.fields {
self.add_all(field, errors);
}
}
pub fn is_empty(&self) -> bool {
self.fields.is_empty()
}
pub fn len(&self) -> usize {
self.fields.values().map(|v| v.len()).sum()
}
pub fn get(&self, field: &str) -> Option<&Vec<RuleError>> {
self.fields.get(field)
}
pub fn into_result(self) -> Result<(), Self> {
if self.is_empty() {
Ok(())
} else {
Err(self)
}
}
pub fn field_names(&self) -> Vec<&str> {
self.fields.keys().map(|s| s.as_str()).collect()
}
pub fn to_api_error_with_locale(&self, locale: Option<&str>) -> ApiValidationError {
let fields: Vec<FieldErrorResponse> = self
.fields
.iter()
.flat_map(|(field, errors)| {
errors.iter().map(move |e| FieldErrorResponse {
field: field.clone(),
code: e.code.clone(),
message: e.interpolate_with_locale(locale),
params: if e.params.is_empty() {
None
} else {
Some(e.params.clone())
},
})
})
.collect();
ApiValidationError {
error: ErrorBody {
error_type: "validation_error".to_string(),
message: "Validation failed".to_string(),
fields,
},
}
}
pub fn to_api_error(&self) -> ApiValidationError {
self.to_api_error_with_locale(None)
}
}
impl fmt::Display for ValidationErrors {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Validation failed: {} error(s)", self.len())
}
}
impl std::error::Error for ValidationErrors {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiValidationError {
pub error: ErrorBody,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorBody {
#[serde(rename = "type")]
pub error_type: String,
pub message: String,
pub fields: Vec<FieldErrorResponse>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldErrorResponse {
pub field: String,
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<HashMap<String, serde_json::Value>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rule_error_creation() {
let error = RuleError::new("email", "Invalid email format");
assert_eq!(error.code, "email");
assert_eq!(error.message, "Invalid email format");
assert!(error.params.is_empty());
}
#[test]
fn rule_error_with_params() {
let error = RuleError::new("length", "Must be between {min} and {max} characters")
.param("min", 3)
.param("max", 50);
assert_eq!(
error.interpolate_message(),
"Must be between 3 and 50 characters"
);
}
#[test]
fn validation_errors_add_and_get() {
let mut errors = ValidationErrors::new();
errors.add("email", RuleError::new("email", "Invalid email"));
errors.add("email", RuleError::new("required", "Email is required"));
errors.add("age", RuleError::new("range", "Age out of range"));
assert_eq!(errors.len(), 3);
assert_eq!(errors.get("email").unwrap().len(), 2);
assert_eq!(errors.get("age").unwrap().len(), 1);
}
#[test]
fn validation_errors_into_result() {
let errors = ValidationErrors::new();
assert!(errors.into_result().is_ok());
let mut errors = ValidationErrors::new();
errors.add("field", RuleError::new("code", "message"));
assert!(errors.into_result().is_err());
}
#[test]
fn validation_errors_to_api_error() {
let mut errors = ValidationErrors::new();
errors.add("email", RuleError::new("email", "Invalid email format"));
let api_error = errors.to_api_error();
assert_eq!(api_error.error.error_type, "validation_error");
assert_eq!(api_error.error.fields.len(), 1);
assert_eq!(api_error.error.fields[0].field, "email");
}
#[test]
fn validation_errors_merge() {
let mut errors1 = ValidationErrors::new();
errors1.add("email", RuleError::new("email", "Invalid"));
let mut errors2 = ValidationErrors::new();
errors2.add("age", RuleError::new("range", "Out of range"));
errors1.merge(errors2);
assert_eq!(errors1.len(), 2);
assert!(errors1.get("email").is_some());
assert!(errors1.get("age").is_some());
}
}