jprochazk/garde

Is there an idiomatic way to validate the contents (keys & values) of a map?

Opened this issue · 2 comments

I appreciate garde. It has made validating structures not just easier, but a natural part of my development. However, I've run into a situation where I need to validate a HashMap (actually a DashMap) that is a member of a struct. If I try to validate it directly, it fails because there is no Display implementation for DashMap. I suspect I may be able to write a custom validator function...maybe? Or..I could wrap the DashMap in a newtype and implement Validate on that type manually...maybe? But, is there an existing best way to validate the keys and values of a map? I would love to be able to use the validation rules built into garde on the keys and values in the map and if I write custom validation code, I lose all of that.

Thanks in advance.

Validating keys is not possible right now without custom validators, and the only way you can validate the values in a HashMap is via #[garde(dive)], which is definitely easier now with #[garde(transparent)], but still not great if all you have is a String value or something along those lines.

One possible improvement here would be to add a modifier similar to inner, but have it apply the validation rules to the key of a keyed collection. Rough idea:

struct Foo {
    #[garde(
        key(length(min = 3)),    // applied to `K`
        inner(length(min = 10)), // applied to `V`
    )]
    bar: HashMap<K, V>,
}

The error message would probably look like:

Foo {
    bar: HashMap {
        "a": "test"
    }
}

value.bar: invalid key "a", length is lower than 3
value.bar.a: length is lower than 10

As for validating a DashMap, right now you need to use a newtype to be able to implement the various validation traits which enable Garde's functionality. I'm not totally sure if there's a way out here other than adding a bunch of feature-gated impls to either library.

One thing I've seen done by serde is to implement a way to "adapt" one type to be validated as if it was another. Once again, here's a very rough idea:

mod dashmap_adapter {
    // the `adapt` modifier will cause the proc macro to bypass the usual trait-based validation
    // logic, and instead rely on functions present in this module:
    
    // re-export all the garde rules
    // because of the glob import, anything defined in this module will take precedence
    pub use garde::rules::*;
    
    // for example, here's how you'd adapt the `inner` modifier to work on `DashMap`:
    pub mod inner {
        pub fn apply<K, V, F>(map: &DashMap<K, V>, f: F)
        where
            K: PathComponentKind,
            F: FnMut(&V, &K),
        {
            for (key, value) in map.iter() {
                f(value, key)
            }
        }
    }
}

struct Foo {
    #[garde(
        inner(length(min = 10)),
        adapt(dashmap_adapter), // use the `dashmap_adapter` module
    )]
    bar: DashMap<K, V>,
}

Now instead of calling garde::rules::inner::apply on the bar field, it will call dashmap_adapter::inner::apply. The module is reusable across your entire application, doesn't require using newtypes, and you still get most of the benefit of garde.

Thank you for your in-depth response. I'll dig into it and hopefully have something I can share back to garde to help others when I'm done.