tanzim_validate/
dynamic_map.rs1use crate::error::{Error, ErrorKind};
2use crate::{Meta, Validator};
3use tanzim_value::{Value, ValueType};
4
5#[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 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 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}