icydb_core/traits/
validate.rs

1use icydb_error::ErrorTree;
2use std::collections::{HashMap, HashSet};
3
4///
5/// Validate
6///
7
8pub trait Validate: ValidateAuto + ValidateCustom {}
9
10impl<T> Validate for T where T: ValidateAuto + ValidateCustom {}
11
12///
13/// ValidateContext
14///
15/// Context that can be provided during validation.
16///
17/// NOTE: ValidateContext is reserved for future context-aware sanitization.
18/// The *_with() methods are currently thin wrappers that delegate to the
19/// stateless versions. In the future, we may pass runtime data (e.g. now, is_new,
20/// actor) here so validators can behave contextually without changing the trait shape.
21///
22
23#[derive(Clone, Debug, Default)]
24pub struct ValidateContext;
25
26///
27/// ValidateAuto
28///
29/// derived code that is used to generate the validation rules for a type and
30/// its children, via schema validation rules
31///
32/// this shouldn't be used with primitive types, it's only really for validation
33/// rules put in by macros
34///
35
36pub trait ValidateAuto {
37    fn validate_self(&self) -> Result<(), ErrorTree> {
38        Ok(())
39    }
40
41    fn validate_children(&self) -> Result<(), ErrorTree> {
42        Ok(())
43    }
44
45    fn validate_self_with(&self, _ctx: &ValidateContext) -> Result<(), ErrorTree> {
46        self.validate_self()
47    }
48
49    fn validate_children_with(&self, _ctx: &ValidateContext) -> Result<(), ErrorTree> {
50        self.validate_children()
51    }
52}
53
54impl<T: ValidateAuto> ValidateAuto for Box<T> {
55    fn validate_self(&self) -> Result<(), ErrorTree> {
56        (**self).validate_self()
57    }
58
59    fn validate_children(&self) -> Result<(), ErrorTree> {
60        (**self).validate_children()
61    }
62}
63
64impl<T: ValidateAuto> ValidateAuto for Option<T> {
65    fn validate_self(&self) -> Result<(), ErrorTree> {
66        self.as_ref().map_or(Ok(()), ValidateAuto::validate_self)
67    }
68
69    fn validate_children(&self) -> Result<(), ErrorTree> {
70        self.as_ref()
71            .map_or(Ok(()), ValidateAuto::validate_children)
72    }
73}
74
75impl<T: ValidateAuto> ValidateAuto for Vec<T> {
76    fn validate_self(&self) -> Result<(), ErrorTree> {
77        ErrorTree::collect(self.iter().map(ValidateAuto::validate_self))
78    }
79
80    fn validate_children(&self) -> Result<(), ErrorTree> {
81        ErrorTree::collect(self.iter().map(ValidateAuto::validate_children))
82    }
83}
84
85impl<T: ValidateAuto, S> ValidateAuto for HashSet<T, S> {
86    fn validate_self(&self) -> Result<(), ErrorTree> {
87        ErrorTree::collect(self.iter().map(ValidateAuto::validate_self))
88    }
89
90    fn validate_children(&self) -> Result<(), ErrorTree> {
91        ErrorTree::collect(self.iter().map(ValidateAuto::validate_children))
92    }
93}
94
95impl<K: ValidateAuto, V: ValidateAuto, S> ValidateAuto for HashMap<K, V, S> {
96    fn validate_self(&self) -> Result<(), ErrorTree> {
97        ErrorTree::collect(
98            self.iter()
99                .flat_map(|(k, v)| [k.validate_self(), v.validate_self()]),
100        )
101    }
102
103    fn validate_children(&self) -> Result<(), ErrorTree> {
104        ErrorTree::collect(
105            self.iter()
106                .flat_map(|(k, v)| [k.validate_children(), v.validate_children()]),
107        )
108    }
109}
110
111impl_primitive!(ValidateAuto);
112
113///
114/// ValidateCustom
115///
116/// custom validation behaviour that can be added to any type
117///
118
119pub trait ValidateCustom {
120    fn validate_custom(&self) -> Result<(), ErrorTree> {
121        Ok(())
122    }
123
124    fn validate_custom_with(&self, _ctx: &ValidateContext) -> Result<(), ErrorTree> {
125        self.validate_custom()
126    }
127}
128
129impl<T: ValidateCustom> ValidateCustom for Box<T> {
130    fn validate_custom(&self) -> Result<(), ErrorTree> {
131        (**self).validate_custom()
132    }
133}
134
135impl<T: ValidateCustom> ValidateCustom for Option<T> {
136    fn validate_custom(&self) -> Result<(), ErrorTree> {
137        self.as_ref()
138            .map_or(Ok(()), ValidateCustom::validate_custom)
139    }
140}
141
142impl<T: ValidateCustom> ValidateCustom for Vec<T> {
143    fn validate_custom(&self) -> Result<(), ErrorTree> {
144        ErrorTree::collect(self.iter().map(ValidateCustom::validate_custom))
145    }
146}
147
148impl<T: ValidateCustom, S> ValidateCustom for HashSet<T, S> {
149    fn validate_custom(&self) -> Result<(), ErrorTree> {
150        ErrorTree::collect(self.iter().map(ValidateCustom::validate_custom))
151    }
152}
153
154impl<K: ValidateCustom, V: ValidateCustom, S> ValidateCustom for HashMap<K, V, S> {
155    fn validate_custom(&self) -> Result<(), ErrorTree> {
156        ErrorTree::collect(
157            self.iter()
158                .flat_map(|(k, v)| [k.validate_custom(), v.validate_custom()]),
159        )
160    }
161}
162
163impl_primitive!(ValidateCustom);
164
165///
166/// TESTS
167///
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    /// A dummy type that always fails validation
174    #[derive(Debug, Eq, Hash, PartialEq)]
175    struct Bad;
176
177    impl ValidateAuto for Bad {
178        fn validate_self(&self) -> Result<(), ErrorTree> {
179            Err("bad self".into())
180        }
181        fn validate_children(&self) -> Result<(), ErrorTree> {
182            Err("bad children".into())
183        }
184    }
185
186    impl ValidateCustom for Bad {
187        fn validate_custom(&self) -> Result<(), ErrorTree> {
188            Err("bad custom".into())
189        }
190    }
191
192    #[test]
193    #[allow(clippy::zero_sized_map_values)]
194    fn hashmap_collects_key_and_value_errors() {
195        let mut map = HashMap::new();
196        map.insert(Bad, Bad);
197
198        // Run self-validation
199        let result = map.validate_self();
200
201        assert!(result.is_err(), "expected error from validation");
202        let errs = result.unwrap_err();
203
204        // Flatten should contain both key and value errors
205        let flat = errs.flatten_ref();
206
207        // At least 2 distinct errors should be present
208        assert!(
209            flat.iter().any(|(_, msg)| msg.contains("bad self")),
210            "missing key/value self errors: {flat:?}",
211        );
212    }
213
214    #[test]
215    #[allow(clippy::zero_sized_map_values)]
216    fn hashmap_collects_custom_errors() {
217        let mut map = HashMap::new();
218        map.insert(Bad, Bad);
219
220        let result = map.validate_custom();
221
222        assert!(result.is_err(), "expected error from custom validation");
223        let errs = result.unwrap_err();
224
225        let flat = errs.flatten_ref();
226        assert!(
227            flat.iter().any(|(_, msg)| msg.contains("bad custom")),
228            "missing key/value custom errors: {flat:?}",
229        );
230    }
231}