Skip to main content

tanzim_validate/
list.rs

1use crate::error::{Error, ErrorKind};
2use crate::{Meta, Validator};
3use tanzim_value::{Value, ValueType};
4
5/// (`list` feature) Accepts a list, with optional length bounds, a uniqueness check, and an optional
6/// per-item validator.
7///
8/// Coercion: an empty map becomes an empty list (matching formats that render an empty
9/// collection as `{}`). A non-empty map or any other type is rejected.
10#[derive(Default)]
11pub struct List {
12    meta: Meta,
13    min_len: Option<usize>,
14    max_len: Option<usize>,
15    unique: bool,
16    items: Option<Box<dyn Validator>>,
17}
18
19impl List {
20    /// Attach human-facing metadata (name, description, examples, default, output conversion).
21    pub fn with_meta(mut self, meta: Meta) -> Self {
22        self.meta = meta;
23        self
24    }
25
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    pub fn min_len(mut self, min: usize) -> Self {
31        self.min_len = Some(min);
32        self
33    }
34
35    pub fn max_len(mut self, max: usize) -> Self {
36        self.max_len = Some(max);
37        self
38    }
39
40    pub fn unique(mut self) -> Self {
41        self.unique = true;
42        self
43    }
44
45    /// Validate every item with `validator`.
46    pub fn items(mut self, validator: impl Into<Box<dyn Validator>>) -> Self {
47        self.items = Some(validator.into());
48        self
49    }
50}
51
52crate::impl_meta_methods!(List);
53
54impl Validator for List {
55    fn meta(&self) -> &Meta {
56        &self.meta
57    }
58
59    fn meta_mut(&mut self) -> &mut Meta {
60        &mut self.meta
61    }
62
63    fn check(&self, value: &mut Value) -> Result<(), Error> {
64        match value {
65            Value::List(_) => {}
66            Value::Map(map) if map.is_empty() => *value = Value::new_list(),
67            _ => {
68                return Err(Error::new(ErrorKind::Type {
69                    expected: ValueType::List,
70                    found: value.type_name(),
71                }));
72            }
73        }
74
75        let items = match value.list_mut() {
76            Some(items) => items,
77            None => unreachable!("value coerced to a list above"),
78        };
79
80        let length = items.len();
81        if let Some(min) = self.min_len
82            && length < min
83        {
84            return Err(Error::new(ErrorKind::TooShort { len: length, min }));
85        }
86        if let Some(max) = self.max_len
87            && length > max
88        {
89            return Err(Error::new(ErrorKind::TooLong { len: length, max }));
90        }
91
92        if let Some(validator) = &self.items {
93            for (index, item) in items.iter_mut().enumerate() {
94                match validator.validate(item.value_mut()) {
95                    Ok(()) => {}
96                    Err(error) => return Err(error.under_index(index, item.location())),
97                }
98            }
99        }
100
101        if self.unique {
102            let mut seen: Vec<&Value> = Vec::new();
103            for (index, item) in items.iter().enumerate() {
104                for previous in &seen {
105                    if **previous == *item.value() {
106                        return Err(Error::new(ErrorKind::Duplicate { index })
107                            .with_location(item.location()));
108                    }
109                }
110                seen.push(item.value());
111            }
112        }
113
114        Ok(())
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::Integer;
122    use tanzim_value::{LocatedValue, Location};
123
124    fn item(value: Value) -> LocatedValue {
125        LocatedValue::new(value, Location::at("file", "test", Some(1), Some(1), None))
126    }
127
128    #[test]
129    fn empty_map_becomes_empty_list() {
130        let mut value = Value::new_map();
131        List::new().validate(&mut value).unwrap();
132        assert_eq!(value, Value::new_list());
133    }
134
135    #[test]
136    fn enforces_length_bounds() {
137        let mut value = Value::List(vec![item(Value::Int(1))]);
138        let error = List::new().min_len(2).validate(&mut value).unwrap_err();
139        assert!(matches!(error.kind, ErrorKind::TooShort { .. }));
140    }
141
142    #[test]
143    fn detects_duplicates() {
144        let mut value = Value::List(vec![item(Value::Int(1)), item(Value::Int(1))]);
145        let error = List::new().unique().validate(&mut value).unwrap_err();
146        assert!(matches!(error.kind, ErrorKind::Duplicate { index: 1 }));
147    }
148
149    #[test]
150    fn item_validator_reports_index_path() {
151        let mut value = Value::List(vec![item(Value::Int(1)), item(Value::String("x".into()))]);
152        let error = List::new()
153            .items(Integer::new())
154            .validate(&mut value)
155            .unwrap_err();
156        assert_eq!(error.path.len(), 1);
157        assert!(matches!(error.kind, ErrorKind::NotConvertible { .. }));
158    }
159
160    #[test]
161    fn item_coercion_persists() {
162        let mut value = Value::List(vec![item(Value::String("5".into()))]);
163        List::new()
164            .items(Integer::new())
165            .validate(&mut value)
166            .unwrap();
167        assert_eq!(*value.as_list().unwrap()[0].value(), Value::Int(5));
168    }
169}