avocado_schema/visitor/
validator.rs

1use crate::core::field::array::ArrayField;
2use crate::core::field::object::ObjectField;
3use crate::core::field::Field;
4use crate::core::field::FieldEnum;
5use crate::core::value::{FieldValue, Reflect};
6use std::collections::BTreeMap;
7use std::error::Error;
8use std::fmt::{Display, Formatter};
9
10#[derive(Debug)]
11pub struct ValidationError {
12    message: String,
13}
14
15impl Display for ValidationError {
16    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
17        write!(f, "{}", self.message)
18    }
19}
20
21impl Error for ValidationError {}
22
23struct State {
24    value: FieldValue,
25    field_names: Vec<String>,
26    errors: BTreeMap<String, Vec<ValidationError>>,
27}
28
29#[derive(Debug)]
30pub struct Validator {
31    schema: FieldEnum,
32}
33
34impl Validator {
35    fn report_error(&self, error: ValidationError, state: &mut State) {
36        let field = state.field_names.clone().join("/");
37        if state.errors.contains_key(field.as_str()) {
38            state.errors.get_mut(field.as_str()).unwrap().push(error);
39        } else {
40            state.errors.insert(field, vec![error]);
41        }
42    }
43
44    fn validate_field(&self, field: &(impl Field + ?Sized), state: &mut State) {
45        state.field_names.push(field.name().clone());
46        for constraint in field.constrains() {
47            match constraint.validate(&state.value) {
48                Ok(_) => {}
49                Err(e) => {
50                    self.report_error(
51                        ValidationError {
52                            message: e.to_string(),
53                        },
54                        state,
55                    );
56                }
57            }
58        }
59        state.field_names.pop();
60    }
61
62    fn visit_array(&self, array: &ArrayField, state: &mut State) {
63        self.validate_field(array, state);
64        state.field_names.push(array.name().clone());
65        if let FieldValue::Array(values) = state.value.clone() {
66            if let Some(item) = &array.item {
67                for value in values {
68                    state.value = value;
69                    self.visit(item, state);
70                }
71            }
72        }
73        state.field_names.pop();
74    }
75
76    fn visit_object(&self, object: &ObjectField, state: &mut State) {
77        self.validate_field(object, state);
78        state.field_names.push(object.name().clone());
79        if let FieldValue::Object(o) = state.value.clone() {
80            for (name, value) in o {
81                if let Some(field) = object.properties.get(name.as_str()) {
82                    state.value = value;
83                    self.visit(field, state);
84                };
85            }
86        }
87        state.field_names.pop();
88    }
89
90    fn visit(&self, field: &FieldEnum, state: &mut State) {
91        match field {
92            FieldEnum::Array(f) => self.visit_array(f, state),
93            FieldEnum::Boolean(f) => self.validate_field(f, state),
94            FieldEnum::Float(f) => self.validate_field(f, state),
95            FieldEnum::Integer(f) => self.validate_field(f, state),
96            FieldEnum::UInteger(f) => self.validate_field(f, state),
97            FieldEnum::Object(f) => self.visit_object(f, state),
98            FieldEnum::String(f) => self.validate_field(f, state),
99            FieldEnum::Email(f) => self.validate_field(f, state),
100            FieldEnum::Datetime(f) => self.validate_field(f, state),
101            FieldEnum::Date(f) => self.validate_field(f, state),
102            FieldEnum::Time(f) => self.validate_field(f, state),
103        }
104    }
105
106    pub fn new(field: impl Field) -> Self {
107        Validator {
108            schema: field.into(),
109        }
110    }
111
112    pub fn validate(
113        &self,
114        value: &impl Reflect,
115    ) -> Result<(), BTreeMap<String, Vec<ValidationError>>> {
116        let mut state = State {
117            value: value.field_value(),
118            field_names: vec![],
119            errors: Default::default(),
120        };
121
122        self.visit(&self.schema, &mut state);
123        if state.errors.is_empty() {
124            Ok(())
125        } else {
126            Err(state.errors)
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use crate::core::field::object::ObjectField;
134    use crate::core::value::{FieldValue, Reflect};
135    use crate::visitor::validator::Validator;
136    use std::collections::BTreeMap;
137
138    #[test]
139    fn test_validate() {
140        struct Client {
141            first_name: String,
142            last_name: String,
143            age: u64,
144        }
145
146        impl Reflect for Client {
147            fn field_value(&self) -> FieldValue {
148                FieldValue::Object(BTreeMap::from([
149                    ("first_name".to_string(), self.first_name.field_value()),
150                    ("last_name".to_string(), self.last_name.field_value()),
151                    ("age".to_string(), self.age.field_value()),
152                ]))
153            }
154        }
155
156        let schema_json = r#"
157        {
158            "type":"object",
159            "name": "client",
160            "properties": {
161                "first_name": {
162                    "type": "string",
163                    "name": "first_name",
164                    "max_length": 32,
165                    "min_length": 8
166                },
167                "last_name": {
168                    "type": "string",
169                    "name": "last_name",
170                    "max_length": 32,
171                    "min_length": 8
172                },
173                "age": {
174                    "type": "uinteger",
175                    "name": "age",
176                    "maximum": 200,
177                    "minimum": 0
178                }
179            }
180        }"#;
181        let schema: ObjectField = serde_json::from_str(schema_json).unwrap();
182        let validator = Validator::new(schema);
183
184        let valid_client = Client {
185            first_name: "Robert".to_string(),
186            last_name: "Li".to_string(),
187            age: 32,
188        };
189        assert!(validator.validate(&valid_client).is_ok());
190
191        let invalid_client = Client {
192            first_name: "Robert".to_string(),
193            last_name: "Li".to_string(),
194            age: 201,
195        };
196        let result = validator.validate(&invalid_client);
197        assert!(result.is_err());
198        assert!(result
199            .err()
200            .unwrap()
201            .get("client/age")
202            .unwrap()
203            .get(0)
204            .unwrap()
205            .message
206            .contains("value 201 is larger then 200 (Maximum)"));
207    }
208}