Repository: Keats/validator (rust)

Issue:
Add derive Deserialize to ValidationErrors
The ValidationErrors type already implements Serialize, presumably to allow sending the error as json in an API. It would be helpful to allow implement Deserialize to read those errors back out on the client side in frontend frameworks like Leptos

Pull request description:
feat!: support `Deserialize` for `ValidationErrors`
Use `Cow<'static, str>` as suggested in https://github.com/Keats/validator/issues/358#issuecomment-2491283345.

Fixes #358 

Patch:
diff --git a/validator/src/traits.rs b/validator/src/traits.rs
index 790ed26..123eaf7 100644
--- a/validator/src/traits.rs
+++ b/validator/src/traits.rs
@@ -1,4 +1,5 @@
 use crate::types::{ValidationErrors, ValidationErrorsKind};
+use std::borrow::Cow;
 use std::collections::btree_map::BTreeMap;
 use std::collections::HashMap;
 
@@ -32,7 +33,7 @@ macro_rules! impl_validate_list {
                 } else {
                     let err_kind = ValidationErrorsKind::List(vec_err);
                     let errors = ValidationErrors(std::collections::HashMap::from([(
-                        "_tmp_validator",
+                        Cow::Borrowed("_tmp_validator"),
                         err_kind,
                     )]));
                     Err(errors)
@@ -64,8 +65,10 @@ impl<T: Validate, const N: usize> Validate for [T; N] {
             Ok(())
         } else {
             let err_kind = ValidationErrorsKind::List(vec_err);
-            let errors =
-                ValidationErrors(std::collections::HashMap::from([("_tmp_validator", err_kind)]));
+            let errors = ValidationErrors(std::collections::HashMap::from([(
+                Cow::Borrowed("_tmp_validator"),
+                err_kind,
+            )]));
             Err(errors)
         }
     }
@@ -85,7 +88,8 @@ impl<K, V: Validate, S> Validate for &HashMap<K, V, S> {
             Ok(())
         } else {
             let err_kind = ValidationErrorsKind::List(vec_err);
-            let errors = ValidationErrors(HashMap::from([("_tmp_validator", err_kind)]));
+            let errors =
+                ValidationErrors(HashMap::from([(Cow::Borrowed("_tmp_validator"), err_kind)]));
             Err(errors)
         }
     }
@@ -105,7 +109,8 @@ impl<K, V: Validate> Validate for &BTreeMap<K, V> {
             Ok(())
         } else {
             let err_kind = ValidationErrorsKind::List(vec_err);
-            let errors = ValidationErrors(HashMap::from([("_tmp_validator", err_kind)]));
+            let errors =
+                ValidationErrors(HashMap::from([(Cow::Borrowed("_tmp_validator"), err_kind)]));
             Err(errors)
         }
     }
diff --git a/validator/src/types.rs b/validator/src/types.rs
index 861f2d2..9bf49da 100644
--- a/validator/src/types.rs
+++ b/validator/src/types.rs
@@ -38,7 +38,7 @@ impl std::error::Error for ValidationError {
     }
 }
 
-#[derive(Debug, Serialize, Clone, PartialEq)]
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
 #[serde(untagged)]
 pub enum ValidationErrorsKind {
     Struct(Box<ValidationErrors>),
@@ -46,8 +46,8 @@ pub enum ValidationErrorsKind {
     Field(Vec<ValidationError>),
 }
 
-#[derive(Default, Debug, Serialize, Clone, PartialEq)]
-pub struct ValidationErrors(pub HashMap<&'static str, ValidationErrorsKind>);
+#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq)]
+pub struct ValidationErrors(pub HashMap<Cow<'static, str>, ValidationErrorsKind>);
 
 impl ValidationErrors {
     pub fn new() -> ValidationErrors {
@@ -134,28 +134,28 @@ impl ValidationErrors {
 
     /// Returns a map of field-level validation errors found for the struct that was validated and
     /// any of it's nested structs that are tagged for validation.
-    pub fn errors(&self) -> &HashMap<&'static str, ValidationErrorsKind> {
+    pub fn errors(&self) -> &HashMap<Cow<'static, str>, ValidationErrorsKind> {
         &self.0
     }
 
     /// Returns a mutable map of field-level validation errors found for the struct that was validated and
     /// any of it's nested structs that are tagged for validation.
-    pub fn errors_mut(&mut self) -> &mut HashMap<&'static str, ValidationErrorsKind> {
+    pub fn errors_mut(&mut self) -> &mut HashMap<Cow<'static, str>, ValidationErrorsKind> {
         &mut self.0
     }
 
     /// Consume the struct, returning the validation errors found
-    pub fn into_errors(self) -> HashMap<&'static str, ValidationErrorsKind> {
+    pub fn into_errors(self) -> HashMap<Cow<'static, str>, ValidationErrorsKind> {
         self.0
     }
 
     /// Returns a map of only field-level validation errors found for the struct that was validated.
-    pub fn field_errors(&self) -> HashMap<&'static str, &Vec<ValidationError>> {
+    pub fn field_errors(&self) -> HashMap<Cow<'static, str>, &Vec<ValidationError>> {
         self.0
             .iter()
             .filter_map(|(k, v)| {
                 if let ValidationErrorsKind::Field(errors) = v {
-                    Some((*k, errors))
+                    Some((k.clone(), errors))
                 } else {
                     None
                 }
@@ -164,8 +164,10 @@ impl ValidationErrors {
     }
 
     pub fn add(&mut self, field: &'static str, error: ValidationError) {
-        if let ValidationErrorsKind::Field(ref mut vec) =
-            self.0.entry(field).or_insert_with(|| ValidationErrorsKind::Field(vec![]))
+        if let ValidationErrorsKind::Field(ref mut vec) = self
+            .0
+            .entry(Cow::Borrowed(field))
+            .or_insert_with(|| ValidationErrorsKind::Field(vec![]))
         {
             vec.push(error);
         } else {
@@ -179,7 +181,7 @@ impl ValidationErrors {
     }
 
     fn add_nested(&mut self, field: &'static str, errors: ValidationErrorsKind) {
-        if let Vacant(entry) = self.0.entry(field) {
+        if let Vacant(entry) = self.0.entry(Cow::Borrowed(field)) {
             entry.insert(errors);
         } else {
             panic!("Attempt to replace non-empty ValidationErrors entry");
diff --git a/validator_derive/src/tokens/nested.rs b/validator_derive/src/tokens/nested.rs
index b74aab4..f4840e4 100644
--- a/validator_derive/src/tokens/nested.rs
+++ b/validator_derive/src/tokens/nested.rs
@@ -5,7 +5,7 @@ pub fn nested_tokens(
     field_name_str: &str,
 ) -> proc_macro2::TokenStream {
     quote! {
-        if let std::collections::hash_map::Entry::Vacant(entry) = errors.0.entry(#field_name_str) {
+        if let std::collections::hash_map::Entry::Vacant(entry) = errors.0.entry(::std::borrow::Cow::Borrowed(#field_name_str)) {
             errors.merge_self(#field_name_str, (&#field_name).validate());
         }
     }


Test patch:
diff --git a/validator_derive_tests/tests/complex.rs b/validator_derive_tests/tests/complex.rs
index 342eea1..33e9e8f 100644
--- a/validator_derive_tests/tests/complex.rs
+++ b/validator_derive_tests/tests/complex.rs
@@ -1,4 +1,4 @@
-use std::collections::HashMap;
+use std::{borrow::Cow, collections::HashMap};
 
 use once_cell::sync::Lazy;
 use regex::Regex;
@@ -213,7 +213,7 @@ fn test_works_with_none_values() {
 #[allow(dead_code)]
 fn unwrap_map<F>(errors: &ValidationErrors, f: F)
 where
-    F: FnOnce(HashMap<&'static str, ValidationErrorsKind>),
+    F: FnOnce(HashMap<Cow<'static, str>, ValidationErrorsKind>),
 {
     let errors = errors.clone();
     f(errors.errors().clone());
diff --git a/validator_derive_tests/tests/custom.rs b/validator_derive_tests/tests/custom.rs
index b48cd25..1b2e440 100644
--- a/validator_derive_tests/tests/custom.rs
+++ b/validator_derive_tests/tests/custom.rs
@@ -1,4 +1,4 @@
-use std::collections::HashMap;
+use std::{borrow::Cow, collections::HashMap};
 
 use validator::{Validate, ValidationError, ValidationErrors, ValidationErrorsKind};
 
@@ -171,9 +171,9 @@ fn custom_fn_on_optional_types_work() {
     assert_eq!(
         t.validate(),
         Err(ValidationErrors(HashMap::from_iter([
-            ("plain", error_kind.clone()),
-            ("option", error_kind.clone()),
-            ("option_option", error_kind),
+            (Cow::Borrowed("plain"), error_kind.clone()),
+            (Cow::Borrowed("option"), error_kind.clone()),
+            (Cow::Borrowed("option_option"), error_kind),
         ])))
     );
 }
diff --git a/validator_derive_tests/tests/nested.rs b/validator_derive_tests/tests/nested.rs
index ee1abd4..96ddd2c 100644
--- a/validator_derive_tests/tests/nested.rs
+++ b/validator_derive_tests/tests/nested.rs
@@ -72,10 +72,10 @@ fn fails_nested_validation_multiple_members() {
     assert_eq!(
         root.validate(),
         Err(ValidationErrors(HashMap::from_iter([(
-            "a",
+            Cow::Borrowed("a"),
             ValidationErrorsKind::Struct(Box::new(ValidationErrors(HashMap::from_iter([
-                ("value1", error_kind.clone()),
-                ("value2", error_kind),
+                (Cow::Borrowed("value1"), error_kind.clone()),
+                (Cow::Borrowed("value2"), error_kind),
             ]))))
         )])))
     );
@@ -905,7 +905,7 @@ fn test_field_validations_evaluated_after_nested_validations_fails() {
 #[allow(dead_code)]
 fn unwrap_map<F>(errors: &ValidationErrors, f: F)
 where
-    F: FnOnce(HashMap<&'static str, ValidationErrorsKind>),
+    F: FnOnce(HashMap<Cow<'static, str>, ValidationErrorsKind>),
 {
     let errors = errors.clone();
     f(errors.errors().clone());