use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldError {
pub message: String,
pub code: Option<String>,
}
impl FieldError {
#[must_use]
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
code: None,
}
}
#[must_use]
pub fn with_code(message: impl Into<String>, code: impl Into<String>) -> Self {
Self {
message: message.into(),
code: Some(code.into()),
}
}
}
impl std::fmt::Display for FieldError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
#[derive(Debug, Clone, Default)]
pub struct ValidationErrors {
errors: HashMap<String, Vec<FieldError>>,
}
impl ValidationErrors {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, field: impl Into<String>, message: impl Into<String>) {
let field = field.into();
self.errors
.entry(field)
.or_default()
.push(FieldError::new(message));
}
pub fn add_with_code(
&mut self,
field: impl Into<String>,
message: impl Into<String>,
code: impl Into<String>,
) {
let field = field.into();
self.errors
.entry(field)
.or_default()
.push(FieldError::with_code(message, code));
}
#[must_use]
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
#[must_use]
pub fn has_field_error(&self, field: &str) -> bool {
self.errors.contains_key(field)
}
#[must_use]
pub fn for_field(&self, field: &str) -> &[FieldError] {
self.errors.get(field).map_or(&[], Vec::as_slice)
}
#[must_use]
pub fn fields_with_errors(&self) -> Vec<&str> {
self.errors.keys().map(String::as_str).collect()
}
#[must_use]
pub fn count(&self) -> usize {
self.errors.values().map(Vec::len).sum()
}
pub fn clear(&mut self) {
self.errors.clear();
}
pub fn merge(&mut self, other: &Self) {
for (field, errors) in &other.errors {
self.errors
.entry(field.clone())
.or_default()
.extend(errors.iter().cloned());
}
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &[FieldError])> {
self.errors
.iter()
.map(|(k, v)| (k.as_str(), v.as_slice()))
}
}
impl From<validator::ValidationErrors> for ValidationErrors {
fn from(errors: validator::ValidationErrors) -> Self {
let mut result = Self::new();
for (field, field_errors) in errors.field_errors() {
for error in field_errors {
let message = error
.message
.as_ref()
.map_or_else(|| error.code.to_string(), ToString::to_string);
result.add_with_code(field.to_string(), message, error.code.to_string());
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_field_error() {
let error = FieldError::new("is required");
assert_eq!(error.message, "is required");
assert!(error.code.is_none());
}
#[test]
fn test_field_error_with_code() {
let error = FieldError::with_code("is required", "required");
assert_eq!(error.message, "is required");
assert_eq!(error.code.as_deref(), Some("required"));
}
#[test]
fn test_validation_errors_new() {
let errors = ValidationErrors::new();
assert!(!errors.has_errors());
assert_eq!(errors.count(), 0);
}
#[test]
fn test_validation_errors_add() {
let mut errors = ValidationErrors::new();
errors.add("email", "is required");
errors.add("email", "is invalid");
assert!(errors.has_errors());
assert!(errors.has_field_error("email"));
assert!(!errors.has_field_error("password"));
assert_eq!(errors.for_field("email").len(), 2);
assert_eq!(errors.count(), 2);
}
#[test]
fn test_validation_errors_merge() {
let mut errors1 = ValidationErrors::new();
errors1.add("email", "is required");
let mut errors2 = ValidationErrors::new();
errors2.add("password", "too short");
errors2.add("email", "is invalid");
errors1.merge(&errors2);
assert_eq!(errors1.for_field("email").len(), 2);
assert_eq!(errors1.for_field("password").len(), 1);
assert_eq!(errors1.count(), 3);
}
#[test]
fn test_validation_errors_clear() {
let mut errors = ValidationErrors::new();
errors.add("email", "is required");
assert!(errors.has_errors());
errors.clear();
assert!(!errors.has_errors());
}
#[test]
fn test_fields_with_errors() {
let mut errors = ValidationErrors::new();
errors.add("email", "is required");
errors.add("password", "too short");
let fields = errors.fields_with_errors();
assert_eq!(fields.len(), 2);
assert!(fields.contains(&"email"));
assert!(fields.contains(&"password"));
}
}