Skip to main content

tanzim_validate/
static_map.rs

1use crate::error::{Error, ErrorKind};
2use crate::{Meta, Validator};
3use tanzim_value::{Value, ValueType};
4
5struct Field {
6    key: String,
7    required: bool,
8    validator: Option<Box<dyn Validator>>,
9}
10
11/// (`static_map` feature) Accepts a map with a known set of keys.
12///
13/// Each declared key is either required or optional, and may carry a validator for its
14/// value. By default keys not declared in the schema are rejected; call
15/// [`StaticMap::allow_unknown`] to permit them.
16#[derive(Default)]
17pub struct StaticMap {
18    meta: Meta,
19    fields: Vec<Field>,
20    deny_unknown: bool,
21}
22
23impl StaticMap {
24    /// Attach human-facing metadata (name, description, examples, default, output conversion).
25    pub fn with_meta(mut self, meta: Meta) -> Self {
26        self.meta = meta;
27        self
28    }
29
30    pub fn new() -> Self {
31        Self {
32            meta: Meta::default(),
33            fields: Vec::new(),
34            deny_unknown: true,
35        }
36    }
37
38    /// Declare a required key whose value is validated by `validator`.
39    pub fn required(
40        mut self,
41        key: impl Into<String>,
42        validator: impl Into<Box<dyn Validator>>,
43    ) -> Self {
44        self.fields.push(Field {
45            key: key.into(),
46            required: true,
47            validator: Some(validator.into()),
48        });
49        self
50    }
51
52    /// Declare an optional key whose value, when present, is validated by `validator`.
53    pub fn optional(
54        mut self,
55        key: impl Into<String>,
56        validator: impl Into<Box<dyn Validator>>,
57    ) -> Self {
58        self.fields.push(Field {
59            key: key.into(),
60            required: false,
61            validator: Some(validator.into()),
62        });
63        self
64    }
65
66    /// Declare a required key whose value is accepted without validation.
67    pub fn required_any(mut self, key: impl Into<String>) -> Self {
68        self.fields.push(Field {
69            key: key.into(),
70            required: true,
71            validator: None,
72        });
73        self
74    }
75
76    /// Declare an optional key whose value is accepted without validation.
77    pub fn optional_any(mut self, key: impl Into<String>) -> Self {
78        self.fields.push(Field {
79            key: key.into(),
80            required: false,
81            validator: None,
82        });
83        self
84    }
85
86    /// Reject keys not declared in the schema (the default).
87    pub fn deny_unknown(mut self) -> Self {
88        self.deny_unknown = true;
89        self
90    }
91
92    /// Accept keys not declared in the schema, leaving their values untouched.
93    pub fn allow_unknown(mut self) -> Self {
94        self.deny_unknown = false;
95        self
96    }
97}
98
99crate::impl_meta_methods!(StaticMap);
100
101impl Validator for StaticMap {
102    fn meta(&self) -> &Meta {
103        &self.meta
104    }
105
106    fn meta_mut(&mut self) -> &mut Meta {
107        &mut self.meta
108    }
109
110    fn check(&self, value: &mut Value) -> Result<(), Error> {
111        let map = match value.map_mut() {
112            Some(map) => map,
113            None => {
114                return Err(Error::new(ErrorKind::Type {
115                    expected: ValueType::Map,
116                    found: value.type_name(),
117                }));
118            }
119        };
120
121        for field in &self.fields {
122            if field.required && !map.contains_key(&field.key) {
123                return Err(Error::new(ErrorKind::MissingKey {
124                    key: field.key.clone(),
125                }));
126            }
127        }
128
129        for field in &self.fields {
130            if let Some(validator) = &field.validator
131                && let Some(entry) = map.get_mut(&field.key)
132            {
133                match validator.validate(entry.value_mut()) {
134                    Ok(()) => {}
135                    Err(error) => return Err(error.under_key(&field.key, entry.location())),
136                }
137            }
138        }
139
140        if self.deny_unknown {
141            for (key, entry) in map.entries() {
142                let mut declared = false;
143                for field in &self.fields {
144                    if &field.key == key {
145                        declared = true;
146                        break;
147                    }
148                }
149                if !declared {
150                    return Err(Error::new(ErrorKind::UnknownKey { key: key.clone() })
151                        .with_location(entry.location()));
152                }
153            }
154        }
155
156        Ok(())
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::{Integer, Str};
164    use tanzim_value::{LocatedValue, Location, Map};
165
166    fn entry(value: Value) -> LocatedValue {
167        LocatedValue::new(value, Location::at("file", "test", Some(1), Some(1), None))
168    }
169
170    fn map_of(pairs: &[(&str, Value)]) -> Value {
171        let mut map = Map::new();
172        for (key, value) in pairs {
173            map.insert((*key).to_string(), entry(value.clone()));
174        }
175        Value::Map(map)
176    }
177
178    #[test]
179    fn missing_required_key_fails() {
180        let schema = StaticMap::new().required("host", Str::new());
181        let mut value = map_of(&[]);
182        let error = schema.validate(&mut value).unwrap_err();
183        assert!(matches!(error.kind, ErrorKind::MissingKey { .. }));
184    }
185
186    #[test]
187    fn optional_absent_is_ok() {
188        let schema = StaticMap::new().optional("port", Integer::new());
189        let mut value = map_of(&[]);
190        assert!(schema.validate(&mut value).is_ok());
191    }
192
193    #[test]
194    fn value_validator_reports_key_path() {
195        let schema = StaticMap::new().required("port", Integer::new());
196        let mut value = map_of(&[("port", Value::String("x".into()))]);
197        let error = schema.validate(&mut value).unwrap_err();
198        assert_eq!(error.path.len(), 1);
199        assert!(matches!(error.kind, ErrorKind::NotConvertible { .. }));
200    }
201
202    #[test]
203    fn unknown_key_denied_by_default() {
204        let schema = StaticMap::new().required("host", Str::new());
205        let mut value = map_of(&[
206            ("host", Value::String("h".into())),
207            ("extra", Value::Int(1)),
208        ]);
209        let error = schema.validate(&mut value).unwrap_err();
210        assert!(matches!(error.kind, ErrorKind::UnknownKey { .. }));
211    }
212
213    #[test]
214    fn unknown_key_allowed_when_opted_in() {
215        let schema = StaticMap::new()
216            .required("host", Str::new())
217            .allow_unknown();
218        let mut value = map_of(&[
219            ("host", Value::String("h".into())),
220            ("extra", Value::Int(1)),
221        ]);
222        assert!(schema.validate(&mut value).is_ok());
223    }
224}