Skip to main content

tanzim_validate/
dynamic_map.rs

1use crate::error::{Error, ErrorKind};
2use crate::{Meta, Validator};
3use tanzim_value::{Value, ValueType};
4
5/// (`dynamic_map` feature) Accepts a map with arbitrary keys and uniform values.
6///
7/// Optional entry-count bounds and an optional validator applied to every value.
8/// Coercion: an empty list becomes an empty map (the list counterpart of an empty
9/// collection). A non-empty list or any other type is rejected.
10#[derive(Default)]
11pub struct DynamicMap {
12    meta: Meta,
13    min_len: Option<usize>,
14    max_len: Option<usize>,
15    values: Option<Box<dyn Validator>>,
16}
17
18impl DynamicMap {
19    /// Attach human-facing metadata (name, description, examples, default, output conversion).
20    pub fn with_meta(mut self, meta: Meta) -> Self {
21        self.meta = meta;
22        self
23    }
24
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    pub fn min_len(mut self, min: usize) -> Self {
30        self.min_len = Some(min);
31        self
32    }
33
34    pub fn max_len(mut self, max: usize) -> Self {
35        self.max_len = Some(max);
36        self
37    }
38
39    /// Validate every value with `validator`.
40    pub fn values(mut self, validator: impl Into<Box<dyn Validator>>) -> Self {
41        self.values = Some(validator.into());
42        self
43    }
44}
45
46crate::impl_meta_methods!(DynamicMap);
47
48impl Validator for DynamicMap {
49    fn meta(&self) -> &Meta {
50        &self.meta
51    }
52
53    fn meta_mut(&mut self) -> &mut Meta {
54        &mut self.meta
55    }
56
57    fn check(&self, value: &mut Value) -> Result<(), Error> {
58        match value {
59            Value::Map(_) => {}
60            Value::List(list) if list.is_empty() => *value = Value::new_map(),
61            _ => {
62                return Err(Error::new(ErrorKind::Type {
63                    expected: ValueType::Map,
64                    found: value.type_name(),
65                }));
66            }
67        }
68
69        let map = match value.map_mut() {
70            Some(map) => map,
71            None => unreachable!("value coerced to a map above"),
72        };
73
74        let length = map.len();
75        if let Some(min) = self.min_len
76            && length < min
77        {
78            return Err(Error::new(ErrorKind::TooShort { len: length, min }));
79        }
80        if let Some(max) = self.max_len
81            && length > max
82        {
83            return Err(Error::new(ErrorKind::TooLong { len: length, max }));
84        }
85
86        if let Some(validator) = &self.values {
87            for (key, entry) in map.entries_mut() {
88                match validator.validate(entry.value_mut()) {
89                    Ok(()) => {}
90                    Err(error) => return Err(error.under_key(key, entry.location())),
91                }
92            }
93        }
94
95        Ok(())
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::Integer;
103    use tanzim_value::{LocatedValue, Location, Map};
104
105    fn entry(value: Value) -> LocatedValue {
106        LocatedValue::new(value, Location::at("file", "test", Some(1), Some(1), None))
107    }
108
109    #[test]
110    fn empty_list_becomes_empty_map() {
111        let mut value = Value::new_list();
112        DynamicMap::new().validate(&mut value).unwrap();
113        assert_eq!(value, Value::new_map());
114    }
115
116    #[test]
117    fn enforces_count_bounds() {
118        let mut map = Map::new();
119        map.insert("a".into(), entry(Value::Int(1)));
120        let mut value = Value::Map(map);
121        let error = DynamicMap::new()
122            .min_len(2)
123            .validate(&mut value)
124            .unwrap_err();
125        assert!(matches!(error.kind, ErrorKind::TooShort { .. }));
126    }
127
128    #[test]
129    fn value_validator_reports_key_path() {
130        let mut map = Map::new();
131        map.insert("a".into(), entry(Value::String("x".into())));
132        let mut value = Value::Map(map);
133        let error = DynamicMap::new()
134            .values(Integer::new())
135            .validate(&mut value)
136            .unwrap_err();
137        assert_eq!(error.path.len(), 1);
138        assert!(matches!(error.kind, ErrorKind::NotConvertible { .. }));
139    }
140}