Skip to main content

tanzim_validate/
enumeration.rs

1use crate::error::{Error, ErrorKind};
2use crate::{Meta, Validator};
3use tanzim_value::Value;
4
5/// (`enumeration` feature) Accepts a value drawn from a fixed allow-list. The allowed values may be of any type,
6/// and are compared by equality (no coercion).
7#[derive(Debug, Clone, Default)]
8pub struct Enum {
9    meta: Meta,
10    allowed: Vec<Value>,
11    case_insensitive: bool,
12}
13
14impl Enum {
15    /// Attach human-facing metadata (name, description, examples, default, output conversion).
16    pub fn with_meta(mut self, meta: Meta) -> Self {
17        self.meta = meta;
18        self
19    }
20
21    /// Build from the allowed values, e.g. `Enum::new([Value::Int(1), Value::Int(2)])`.
22    pub fn new(values: impl IntoIterator<Item = Value>) -> Self {
23        let mut allowed = Vec::new();
24        for value in values {
25            allowed.push(value);
26        }
27        Self {
28            meta: Meta::default(),
29            allowed,
30            case_insensitive: false,
31        }
32    }
33
34    /// Compare string values ignoring ASCII case (no effect on other types).
35    pub fn case_insensitive(mut self) -> Self {
36        self.case_insensitive = true;
37        self
38    }
39}
40
41crate::impl_meta_methods!(Enum);
42
43impl Validator for Enum {
44    fn meta(&self) -> &Meta {
45        &self.meta
46    }
47
48    fn meta_mut(&mut self) -> &mut Meta {
49        &mut self.meta
50    }
51
52    fn check(&self, value: &mut Value) -> Result<(), Error> {
53        for candidate in &self.allowed {
54            let matches = match (candidate, &*value) {
55                (Value::String(allowed), Value::String(actual)) if self.case_insensitive => {
56                    allowed.eq_ignore_ascii_case(actual)
57                }
58                (allowed, actual) => allowed == actual,
59            };
60            if matches {
61                return Ok(());
62            }
63        }
64
65        Err(Error::new(ErrorKind::NotAllowed {
66            value: value.to_string(),
67        }))
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn string_membership() {
77        let validator = Enum::new([Value::String("debug".into()), Value::String("info".into())]);
78        assert!(
79            validator
80                .validate(&mut Value::String("info".into()))
81                .is_ok()
82        );
83        let error = validator
84            .validate(&mut Value::String("trace".into()))
85            .unwrap_err();
86        assert!(matches!(error.kind, ErrorKind::NotAllowed { .. }));
87    }
88
89    #[test]
90    fn accepts_non_string_types() {
91        let validator = Enum::new([Value::Int(1), Value::Int(2), Value::Bool(true)]);
92        assert!(validator.validate(&mut Value::Int(2)).is_ok());
93        assert!(validator.validate(&mut Value::Bool(true)).is_ok());
94        assert!(validator.validate(&mut Value::Int(3)).is_err());
95    }
96
97    #[test]
98    fn case_insensitive_strings() {
99        let validator = Enum::new([Value::String("Info".into())]).case_insensitive();
100        assert!(
101            validator
102                .validate(&mut Value::String("INFO".into()))
103                .is_ok()
104        );
105    }
106}