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
99impl Validator for StaticMap {
100    fn meta(&self) -> &Meta {
101        &self.meta
102    }
103
104    fn meta_mut(&mut self) -> &mut Meta {
105        &mut self.meta
106    }
107
108    fn check(&self, value: &mut Value) -> Result<(), Error> {
109        let map = match value.map_mut() {
110            Some(map) => map,
111            None => {
112                return Err(Error::new(ErrorKind::Type {
113                    expected: ValueType::Map,
114                    found: value.type_name(),
115                }));
116            }
117        };
118
119        for field in &self.fields {
120            if field.required && !map.contains_key(&field.key) {
121                return Err(Error::new(ErrorKind::MissingKey {
122                    key: field.key.clone(),
123                }));
124            }
125        }
126
127        for field in &self.fields {
128            if let Some(validator) = &field.validator
129                && let Some(entry) = map.get_mut(&field.key)
130            {
131                match validator.validate(entry.value_mut()) {
132                    Ok(()) => {}
133                    Err(error) => return Err(error.under_key(&field.key, entry.location())),
134                }
135            }
136        }
137
138        if self.deny_unknown {
139            for (key, entry) in map.entries() {
140                let mut declared = false;
141                for field in &self.fields {
142                    if &field.key == key {
143                        declared = true;
144                        break;
145                    }
146                }
147                if !declared {
148                    return Err(Error::new(ErrorKind::UnknownKey { key: key.clone() })
149                        .with_location(entry.location()));
150                }
151            }
152        }
153
154        Ok(())
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::{Integer, Str};
162    use tanzim_value::{LocatedValue, Location, Map};
163
164    fn entry(value: Value) -> LocatedValue {
165        LocatedValue::new(value, Location::at("file", "test", Some(1), Some(1), None))
166    }
167
168    fn map_of(pairs: &[(&str, Value)]) -> Value {
169        let mut map = Map::new();
170        for (key, value) in pairs {
171            map.insert((*key).to_string(), entry(value.clone()));
172        }
173        Value::Map(map)
174    }
175
176    #[test]
177    fn missing_required_key_fails() {
178        let schema = StaticMap::new().required("host", Str::new());
179        let mut value = map_of(&[]);
180        let error = schema.validate(&mut value).unwrap_err();
181        assert!(matches!(error.kind, ErrorKind::MissingKey { .. }));
182    }
183
184    #[test]
185    fn optional_absent_is_ok() {
186        let schema = StaticMap::new().optional("port", Integer::new());
187        let mut value = map_of(&[]);
188        assert!(schema.validate(&mut value).is_ok());
189    }
190
191    #[test]
192    fn value_validator_reports_key_path() {
193        let schema = StaticMap::new().required("port", Integer::new());
194        let mut value = map_of(&[("port", Value::String("x".into()))]);
195        let error = schema.validate(&mut value).unwrap_err();
196        assert_eq!(error.path.len(), 1);
197        assert!(matches!(error.kind, ErrorKind::NotConvertible { .. }));
198    }
199
200    #[test]
201    fn unknown_key_denied_by_default() {
202        let schema = StaticMap::new().required("host", Str::new());
203        let mut value = map_of(&[
204            ("host", Value::String("h".into())),
205            ("extra", Value::Int(1)),
206        ]);
207        let error = schema.validate(&mut value).unwrap_err();
208        assert!(matches!(error.kind, ErrorKind::UnknownKey { .. }));
209    }
210
211    #[test]
212    fn unknown_key_allowed_when_opted_in() {
213        let schema = StaticMap::new()
214            .required("host", Str::new())
215            .allow_unknown();
216        let mut value = map_of(&[
217            ("host", Value::String("h".into())),
218            ("extra", Value::Int(1)),
219        ]);
220        assert!(schema.validate(&mut value).is_ok());
221    }
222}