oca_ast/
validator.rs

1use crate::{
2    ast::{Command, CommandType, NestedAttrType, NestedValue, OCAAst, ObjectKind},
3    errors::Error,
4};
5use indexmap::{indexmap, IndexMap};
6use log::debug;
7
8type CaptureAttributes = IndexMap<String, NestedAttrType>;
9
10/// Validates given commands against existing valid OCA AST
11///
12/// # Arguments
13/// * `ast` - valid OCA AST
14/// * `command` - Command to validate against AST
15///
16/// # Returns
17/// * `Result<bool, Error>` - Result of validation
18pub trait Validator {
19    fn validate(&self, ast: &OCAAst, command: Command) -> Result<bool, Error>;
20}
21
22pub struct OCAValidator {}
23
24impl Validator for OCAValidator {
25    fn validate(&self, ast: &OCAAst, command: Command) -> Result<bool, Error> {
26        let mut errors = Vec::new();
27        let mut valid = true;
28        match ast.version.as_str() {
29            "1.0.0" => {
30                let version_validator = validate_1_0_0(ast, command);
31                if version_validator.is_err() {
32                    valid = false;
33                    errors.push(version_validator.err().unwrap());
34                }
35            }
36            "" => {
37                valid = false;
38                errors.push(Error::MissingVersion());
39            }
40            _ => {
41                valid = false;
42                errors.push(Error::InvalidVersion(ast.version.to_string()));
43            }
44        }
45        if valid {
46            Ok(true)
47        } else {
48            Err(Error::Validation(errors))
49        }
50    }
51}
52
53fn validate_1_0_0(ast: &OCAAst, command: Command) -> Result<bool, Error> {
54    // Rules
55    // Cannot remove if does not exist on stack
56    // Cannot modify if does not exist on stack
57    // Cannot add if already exists on stack
58    // Attributes must have valid type
59    let mut valid = true;
60    let mut errors = Vec::new();
61    match (&command.kind, &command.object_kind) {
62        (CommandType::Add, ObjectKind::CaptureBase(_)) => {
63            match rule_add_attr_if_not_exist(ast, command) {
64                Ok(result) => {
65                    if !result {
66                        valid = result;
67                    }
68                }
69                Err(error) => {
70                    valid = false;
71                    errors.push(error);
72                }
73            }
74        }
75        (CommandType::Remove, ObjectKind::CaptureBase(_)) => {
76            match rule_remove_attr_if_exist(ast, command) {
77                Ok(result) => {
78                    if !result {
79                        valid = result;
80                    }
81                }
82                Err(error) => {
83                    valid = false;
84                    errors.push(error);
85                }
86            }
87        }
88
89        _ => {
90            // TODO: Add support for FROM, MODIFY with combination of different object kinds
91        }
92    }
93    // CommandType::Modify => {
94    //     match rule_modify_if_exist(ast, command) {
95    //         Ok(result) => {
96    //             if !result {
97    //                 valid = result;
98    //             }
99    //         }
100    //         Err(error) => {
101    //             valid = false;
102    //             errors.push(error);
103    //         }
104    //     }
105    // }
106
107    if valid {
108        Ok(true)
109    } else {
110        Err(Error::Validation(errors))
111    }
112}
113
114/// Check rule for remove command
115/// Rule would be valid if attributes which commands tries to remove exist in the stack
116///
117/// # Arguments
118/// * `ast` - valid OCA AST
119/// * `command` - Command to validate against AST
120///
121/// # Returns
122/// * `Result<bool, Error>` - Result of validation
123fn rule_remove_attr_if_exist(ast: &OCAAst, command_to_validate: Command) -> Result<bool, Error> {
124    let mut errors = Vec::new();
125
126    let attributes = extract_attributes(ast);
127    let properties = extract_properties(ast);
128
129    let content = command_to_validate.object_kind.capture_content();
130
131    println!("attributes: {:?}", attributes);
132    println!("properties: {:?}", properties);
133
134    match (
135        content,
136        content.as_ref().and_then(|c| c.attributes.as_ref()),
137    ) {
138        (Some(_content), Some(attrs_to_remove)) => {
139            println!("attr to remove: {:?}", attrs_to_remove);
140            let valid = attrs_to_remove
141                .keys()
142                .all(|key| attributes.contains_key(key));
143            if !valid {
144                errors.push(Error::InvalidOperation(
145                    "Cannot remove attribute if does not exists".to_string(),
146                ));
147            }
148        }
149        (None, None) => (),
150        (None, Some(_)) => (),
151        (Some(_), None) => (),
152    }
153
154    match (
155        content,
156        content.as_ref().and_then(|c| c.properties.as_ref()),
157    ) {
158        (Some(_content), Some(props_to_remove)) => {
159            let valid = props_to_remove
160                .keys()
161                .all(|key| properties.contains_key(key));
162            if !valid {
163                errors.push(Error::InvalidOperation(
164                    "Cannot remove property if does not exists".to_string(),
165                ));
166                return Err(Error::Validation(errors));
167            }
168        }
169        (None, None) => (),
170        (None, Some(_)) => (),
171        (Some(_), None) => (),
172    }
173    if errors.is_empty() {
174        Ok(true)
175    } else {
176        Err(Error::Validation(errors))
177    }
178}
179
180/// Check rule for add command
181/// Rule would be valid if attributes which commands tries to add do not exist in the stack
182///
183/// # Arguments
184/// * `ast` - valid OCA AST
185/// * `command` - Command to validate against AST
186///
187/// # Returns
188/// * `Result<bool, Error>` - Result of validation
189fn rule_add_attr_if_not_exist(ast: &OCAAst, command_to_validate: Command) -> Result<bool, Error> {
190    let mut errors = Vec::new();
191    // Create a list of all attributes ADDed and REMOVEd via commands and check if what left covers needs of new command
192    let default_attrs: IndexMap<String, NestedAttrType> = indexmap! {};
193
194    let attributes = extract_attributes(ast);
195
196    let content = command_to_validate.object_kind.capture_content();
197
198    match content {
199        Some(content) => {
200            let attrs_to_add = content.attributes.clone().unwrap_or(default_attrs);
201            debug!("attrs_to_add: {:?}", attrs_to_add);
202
203            let existing_keys: Vec<_> = attrs_to_add
204                .keys()
205                .filter(|key| attributes.contains_key(*key))
206                .collect();
207
208            if !existing_keys.is_empty() {
209                errors.push(Error::InvalidOperation(format!(
210                    "Cannot add attribute if already exists: {:?}",
211                    existing_keys
212                )));
213                Err(Error::Validation(errors))
214            } else {
215                Ok(true)
216            }
217        }
218        None => {
219            errors.push(Error::InvalidOperation(
220                "No attribtues specify to be added".to_string(),
221            ));
222            Err(Error::Validation(errors))
223        }
224    }
225}
226
227fn extract_attributes(ast: &OCAAst) -> CaptureAttributes {
228    let default_attrs: IndexMap<String, NestedAttrType> = indexmap! {};
229    let mut attributes: CaptureAttributes = indexmap! {};
230    for instruction in &ast.commands {
231        match (instruction.kind.clone(), instruction.object_kind.clone()) {
232            (CommandType::Remove, ObjectKind::CaptureBase(capture_content)) => {
233                let attrs = capture_content
234                    .attributes
235                    .as_ref()
236                    .unwrap_or(&default_attrs);
237                attributes.retain(|key, _value| !attrs.contains_key(key));
238            }
239            (CommandType::Add, ObjectKind::CaptureBase(capture_content)) => {
240                let attrs = capture_content
241                    .attributes
242                    .as_ref()
243                    .unwrap_or(&default_attrs);
244                attributes.extend(attrs.iter().map(|(k, v)| (k.clone(), v.clone())));
245            }
246            _ => {}
247        }
248    }
249    attributes
250}
251
252fn extract_properties(ast: &OCAAst) -> IndexMap<String, NestedValue> {
253    let default_attrs: IndexMap<String, NestedValue> = indexmap! {};
254    let mut properties: IndexMap<String, NestedValue> = indexmap! {};
255    for instruction in &ast.commands {
256        match (instruction.kind.clone(), instruction.object_kind.clone()) {
257            (CommandType::Remove, ObjectKind::CaptureBase(capture_content)) => {
258                let props = capture_content
259                    .properties
260                    .as_ref()
261                    .unwrap_or(&default_attrs);
262                properties.retain(|key, _value| !props.contains_key(key));
263            }
264            (CommandType::Add, ObjectKind::CaptureBase(capture_content)) => {
265                let props = capture_content
266                    .properties
267                    .as_ref()
268                    .unwrap_or(&default_attrs);
269                properties.extend(props.iter().map(|(k, v)| (k.clone(), v.clone())));
270            }
271            _ => {}
272        }
273    }
274    properties
275}
276
277#[cfg(test)]
278mod tests {
279    use indexmap::indexmap;
280
281    use super::*;
282    use crate::ast::{
283        AttributeType, CaptureContent, Command, CommandType, NestedValue, OCAAst, ObjectKind,
284    };
285
286    #[test]
287    fn test_rule_remove_if_exist() {
288        let command = Command {
289            kind: CommandType::Add,
290            object_kind: ObjectKind::CaptureBase(CaptureContent {
291                attributes: Some(indexmap! {
292                    "name".to_string() => NestedAttrType::Value(AttributeType::Text),
293                    "documentType".to_string() => NestedAttrType::Value(AttributeType::Text),
294                    "photo".to_string() => NestedAttrType::Value(AttributeType::Binary),
295                }),
296                properties: Some(indexmap! {
297                    "classification".to_string() => NestedValue::Value("GICS:1234".to_string()),
298                }),
299                flagged_attributes: None,
300            }),
301        };
302
303        let command2 = Command {
304            kind: CommandType::Add,
305            object_kind: ObjectKind::CaptureBase(CaptureContent {
306                attributes: Some(indexmap! {
307                    "issuer".to_string() => NestedAttrType::Value(AttributeType::Text),
308                    "last_name".to_string() => NestedAttrType::Value(AttributeType::Binary),
309                }),
310                properties: Some(indexmap! {
311                    "classification".to_string() => NestedValue::Value("GICS:1234".to_string()),
312                }),
313                flagged_attributes: None,
314            }),
315        };
316
317        let remove_command = Command {
318            kind: CommandType::Remove,
319            object_kind: ObjectKind::CaptureBase(CaptureContent {
320                attributes: Some(indexmap! {
321                    "name".to_string() => NestedAttrType::Null,
322                    "documentType".to_string() => NestedAttrType::Null,
323                }),
324                properties: Some(indexmap! {}),
325                flagged_attributes: None,
326            }),
327        };
328
329        let add_command = Command {
330            kind: CommandType::Add,
331            object_kind: ObjectKind::CaptureBase(CaptureContent {
332                attributes: Some(indexmap! {
333                    "name".to_string() => NestedAttrType::Value(AttributeType::Text),
334                }),
335                properties: Some(indexmap! {}),
336                flagged_attributes: None,
337            }),
338        };
339
340        let valid_command = Command {
341            kind: CommandType::Remove,
342            object_kind: ObjectKind::CaptureBase(CaptureContent {
343                attributes: Some(indexmap! {
344                    "name".to_string() => NestedAttrType::Null,
345                    "issuer".to_string() => NestedAttrType::Null,
346                }),
347                properties: Some(indexmap! {}),
348                flagged_attributes: None,
349            }),
350        };
351
352        let invalid_command = Command {
353            kind: CommandType::Remove,
354            object_kind: ObjectKind::CaptureBase(CaptureContent {
355                attributes: Some(indexmap! {
356                    "documentType".to_string() => NestedAttrType::Null,
357                }),
358                properties: Some(indexmap! {}),
359                flagged_attributes: None,
360            }),
361        };
362
363        let mut ocaast = OCAAst::new();
364        ocaast.commands.push(command);
365        ocaast.commands.push(command2);
366        ocaast.commands.push(remove_command);
367        ocaast.commands.push(add_command);
368        let mut result = rule_remove_attr_if_exist(&ocaast, valid_command.clone());
369        assert!(result.is_ok());
370        ocaast.commands.push(invalid_command.clone());
371        result = rule_remove_attr_if_exist(&ocaast, invalid_command);
372        assert!(result.is_err());
373    }
374
375    #[test]
376    fn test_rule_add_if_not_exist() {
377        let command = Command {
378            kind: CommandType::Add,
379            object_kind: ObjectKind::CaptureBase(CaptureContent {
380                attributes: Some(indexmap! {
381                    "name".to_string() => NestedAttrType::Value(AttributeType::Text),
382                    "documentType".to_string() => NestedAttrType::Value(AttributeType::Text),
383                    "photo".to_string() => NestedAttrType::Value(AttributeType::Binary),
384                }),
385                properties: Some(indexmap! {
386                    "classification".to_string() => NestedValue::Value("GICS:1234".to_string()),
387                }),
388                flagged_attributes: None,
389            }),
390        };
391
392        let command2 = Command {
393            kind: CommandType::Add,
394            object_kind: ObjectKind::CaptureBase(CaptureContent {
395                attributes: Some(indexmap! {
396                    "issuer".to_string() => NestedAttrType::Value(AttributeType::Text),
397                    "last_name".to_string() => NestedAttrType::Value(AttributeType::Binary),
398                }),
399                properties: Some(indexmap! {}),
400                flagged_attributes: None,
401            }),
402        };
403
404        let valid_command = Command {
405            kind: CommandType::Add,
406            object_kind: ObjectKind::CaptureBase(CaptureContent {
407                attributes: Some(indexmap! {
408                    "first_name".to_string() => NestedAttrType::Value(AttributeType::Text),
409                    "address".to_string() => NestedAttrType::Value(AttributeType::Text),
410                }),
411                properties: Some(indexmap! {}),
412                flagged_attributes: None,
413            }),
414        };
415
416        let invalid_command = Command {
417            kind: CommandType::Add,
418            object_kind: ObjectKind::CaptureBase(CaptureContent {
419                attributes: Some(indexmap! {
420                    "name".to_string() => NestedAttrType::Value(AttributeType::Text),
421                    "phone".to_string() => NestedAttrType::Value(AttributeType::Text),
422                }),
423                properties: Some(indexmap! {}),
424                flagged_attributes: None,
425            }),
426        };
427
428        let mut ocaast = OCAAst::new();
429        ocaast.commands.push(command);
430        ocaast.commands.push(command2);
431        let mut result = rule_add_attr_if_not_exist(&ocaast, valid_command.clone());
432        assert!(result.is_ok());
433        ocaast.commands.push(invalid_command.clone());
434        result = rule_add_attr_if_not_exist(&ocaast, invalid_command.clone());
435        assert!(result.is_err());
436    }
437}