oca_bundle_semantics/state/
validator.rs

1use crate::state::oca::overlay::Overlay;
2use crate::state::oca::DynOverlay;
3use indexmap::IndexMap;
4use isolang::Language;
5use oca_ast_semantics::ast::{AttributeType, NestedAttrType, OverlayType};
6use std::{
7    collections::{HashMap, HashSet},
8    error::Error as StdError,
9};
10
11use super::oca::{overlay, OCABundle};
12use piccolo::{Closure, Lua, Thread};
13
14#[derive(Debug)]
15pub enum Error {
16    Custom(String),
17    MissingTranslations(Language),
18    MissingMetaTranslation(Language, String),
19    UnexpectedTranslations(Language),
20    MissingAttributeTranslation(Language, String),
21}
22
23impl std::fmt::Display for Error {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            Error::Custom(error) => write!(f, "{error}"),
27            Error::MissingTranslations(language) => {
28                write!(f, "Missing translation in {language} language")
29            }
30            Error::MissingMetaTranslation(language, attr) => write!(
31                f,
32                "Missing meta translation for {attr} in {language} language"
33            ),
34            Error::UnexpectedTranslations(language) => {
35                write!(f, "Unexpected translations in {language} language")
36            }
37            Error::MissingAttributeTranslation(language, attr) => {
38                write!(f, "Missing translation for {attr} in {language} language")
39            }
40        }
41    }
42}
43
44impl std::error::Error for Error {}
45
46pub enum SemanticValidationStatus {
47    Valid,
48    Invalid(Vec<Error>),
49}
50
51pub fn validate(oca_bundle: &OCABundle) -> Result<SemanticValidationStatus, String> {
52    let validator = Validator::new();
53    match validator.validate(oca_bundle) {
54        Ok(_) => Ok(SemanticValidationStatus::Valid),
55        Err(errors) => Ok(SemanticValidationStatus::Invalid(errors)),
56    }
57}
58
59pub struct Validator {
60    enforced_translations: Vec<Language>,
61}
62
63impl Default for Validator {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl Validator {
70    pub fn new() -> Validator {
71        Validator {
72            enforced_translations: vec![],
73        }
74    }
75
76    pub fn enforce_translations(mut self, languages: Vec<Language>) -> Validator {
77        self.enforced_translations = self
78            .enforced_translations
79            .into_iter()
80            .chain(languages)
81            .collect::<Vec<Language>>();
82        self
83    }
84
85    pub fn validate(self, oca_bundle: &OCABundle) -> Result<(), Vec<Error>> {
86        let enforced_langs: HashSet<_> = self.enforced_translations.iter().collect();
87        let mut errors: Vec<Error> = vec![];
88
89        /* let oca_bundle: OCABundle = serde_json::from_str(oca_str.as_str())
90                   .map_err(|e| vec![Error::Custom(e.to_string())])?;
91        */
92        let mut recalculated_oca_bundle = oca_bundle.clone();
93        recalculated_oca_bundle.fill_said();
94
95        if oca_bundle.said.ne(&recalculated_oca_bundle.said) {
96            errors.push(Error::Custom("OCA Bundle: Malformed SAID".to_string()));
97        }
98
99        let capture_base = &oca_bundle.capture_base;
100
101        let mut recalculated_capture_base = capture_base.clone();
102        recalculated_capture_base.sign();
103
104        if capture_base.said.ne(&recalculated_capture_base.said) {
105            errors.push(Error::Custom("capture_base: Malformed SAID".to_string()));
106        }
107
108        for o in &oca_bundle.overlays {
109            let mut recalculated_overlay = o.clone();
110            recalculated_overlay.fill_said();
111            if o.said().ne(recalculated_overlay.said()) {
112                let msg = match o.language() {
113                    Some(lang) => format!("{} ({}): Malformed SAID", o.overlay_type(), lang),
114                    None => format!("{}: Malformed SAID", o.overlay_type()),
115                };
116                errors.push(Error::Custom(msg));
117            }
118
119            if o.capture_base().ne(&capture_base.said) {
120                let msg = match o.language() {
121                    Some(lang) => {
122                        format!("{} ({}): Mismatch capture_base SAI", o.overlay_type(), lang)
123                    }
124                    None => format!("{}: Mismatch capture_base SAI", o.overlay_type()),
125                };
126                errors.push(Error::Custom(msg));
127            }
128        }
129
130        let conditional_overlay = oca_bundle
131            .overlays
132            .iter()
133            .find_map(|x| x.as_any().downcast_ref::<overlay::Conditional>());
134
135        if let Some(conditional_overlay) = conditional_overlay {
136            self.validate_conditional(
137                oca_bundle.capture_base.attributes.clone(),
138                conditional_overlay,
139            )?;
140        }
141
142        if !enforced_langs.is_empty() {
143            let meta_overlays = oca_bundle
144                .overlays
145                .iter()
146                .filter_map(|x| x.as_any().downcast_ref::<overlay::Meta>())
147                .collect::<Vec<_>>();
148
149            if !meta_overlays.is_empty() {
150                if let Err(meta_errors) = self.validate_meta(&enforced_langs, meta_overlays) {
151                    errors = errors
152                        .into_iter()
153                        .chain(meta_errors.into_iter().map(|e| {
154                            if let Error::UnexpectedTranslations(lang) = e {
155                                Error::Custom(format!(
156                                    "meta overlay: translations in {lang:?} language are not enforced"
157                                ))
158                            } else if let Error::MissingTranslations(lang) = e {
159                                Error::Custom(format!(
160                                    "meta overlay: translations in {lang:?} language are missing"
161                                ))
162                            } else if let Error::MissingMetaTranslation(lang, attr) = e {
163                                Error::Custom(format!(
164                                    "meta overlay: for '{attr}' translation in {lang:?} language is missing"
165                                ))
166                            } else {
167                                e
168                            }
169                        }))
170                        .collect();
171                }
172            }
173
174            let overlay_version = "1.1".to_string();
175            for overlay_type in &[
176                OverlayType::Entry(overlay_version.clone()),
177                OverlayType::Information(overlay_version.clone()),
178                OverlayType::Label(overlay_version.clone()),
179            ] {
180                let typed_overlays: Vec<_> = oca_bundle
181                    .overlays
182                    .iter()
183                    .filter(|x| x.overlay_type().to_string().eq(&overlay_type.to_string()))
184                    .collect();
185                if typed_overlays.is_empty() {
186                    continue;
187                }
188
189                if let Err(translation_errors) =
190                    self.validate_translations(&enforced_langs, typed_overlays)
191                {
192                    errors = errors.into_iter().chain(
193                        translation_errors.into_iter().map(|e| {
194                            if let Error::UnexpectedTranslations(lang) = e {
195                                Error::Custom(
196                                    format!("{overlay_type} overlay: translations in {lang:?} language are not enforced")
197                                )
198                            } else if let Error::MissingTranslations(lang) = e {
199                                Error::Custom(
200                                    format!("{overlay_type} overlay: translations in {lang:?} language are missing")
201                                )
202                            } else if let Error::MissingAttributeTranslation(lang, attr_name) = e {
203                                Error::Custom(
204                                    format!("{overlay_type} overlay: for '{attr_name}' attribute missing translations in {lang:?} language")
205                                )
206                            } else {
207                                e
208                            }
209                        })
210                    ).collect();
211                }
212            }
213        }
214
215        if errors.is_empty() {
216            Ok(())
217        } else {
218            Err(errors)
219        }
220    }
221
222    fn validate_conditional(
223        &self,
224        attr_types: IndexMap<String, NestedAttrType>,
225        overlay: &overlay::Conditional,
226    ) -> Result<(), Vec<Error>> {
227        let mut errors: Vec<Error> = vec![];
228
229        let conditions = overlay.attribute_conditions.clone();
230        let dependencies = overlay.attribute_dependencies.clone();
231        let re = regex::Regex::new(r"\$\{(\d+)\}").unwrap();
232        for &attr in overlay.attributes().iter() {
233            let condition = conditions.get(attr).unwrap(); // todo
234            let condition_dependencies = dependencies.get(attr).unwrap(); // todo
235            if condition_dependencies.contains(attr) {
236                errors.push(Error::Custom(format!(
237                    "Attribute '{attr}' cannot be a dependency of itself"
238                )));
239                continue;
240            }
241
242            let mut attr_mocks: HashMap<String, String> = HashMap::new();
243            condition_dependencies.iter().for_each(|dep| {
244                let dep_type = attr_types.get(dep).unwrap(); // todo
245                let value = match dep_type {
246                    NestedAttrType::Null => "null".to_string(),
247                    NestedAttrType::Value(base_type) => match base_type {
248                        AttributeType::Text => "'test'".to_string(),
249                        AttributeType::Numeric => "0".to_string(),
250                        AttributeType::DateTime => "'2020-01-01'".to_string(),
251                        AttributeType::Binary => "test".to_string(),
252                        AttributeType::Boolean => "true".to_string(),
253                    },
254                    // TODO validate nested objects
255                    NestedAttrType::Array(boxed_type) => match **boxed_type {
256                        NestedAttrType::Value(base_type) => match base_type {
257                            AttributeType::Text => "['test']".to_string(),
258                            AttributeType::Numeric => "[0]".to_string(),
259                            AttributeType::DateTime => "['2020-01-01']".to_string(),
260                            AttributeType::Binary => "[test]".to_string(),
261                            AttributeType::Boolean => "[true]".to_string(),
262                        },
263                        _ => panic!("Invalid or not supported array type"),
264                    },
265                    NestedAttrType::Reference(ref_value) => ref_value.to_string(),
266                };
267                attr_mocks.insert(dep.to_string(), value);
268            });
269
270            let script = re
271                .replace_all(condition, |caps: &regex::Captures| {
272                    attr_mocks
273                        .get(&condition_dependencies[caps[1].parse::<usize>().unwrap()].clone())
274                        .unwrap()
275                        .to_string()
276                })
277                .to_string();
278
279            let mut lua = Lua::new();
280            let thread_result = lua.try_run(|ctx| {
281                let closure = Closure::load(ctx, format!("return {script}").as_bytes())?;
282                let thread = Thread::new(&ctx);
283                thread.start(ctx, closure.into(), ())?;
284                Ok(ctx.state.registry.stash(&ctx, thread))
285            });
286
287            match thread_result {
288                Ok(thread) => {
289                    if let Err(e) = lua.run_thread::<bool>(&thread) {
290                        errors.push(Error::Custom(format!(
291                            "Attribute '{attr}' has invalid condition: {}",
292                            e.source().unwrap()
293                        )));
294                    }
295                }
296                Err(e) => {
297                    errors.push(Error::Custom(format!(
298                        "Attribute '{attr}' has invalid condition: {}",
299                        e.source().unwrap()
300                    )));
301                }
302            }
303        }
304
305        if errors.is_empty() {
306            Ok(())
307        } else {
308            Err(errors)
309        }
310    }
311
312    fn validate_meta(
313        &self,
314        enforced_langs: &HashSet<&Language>,
315        meta_overlays: Vec<&overlay::Meta>,
316    ) -> Result<(), Vec<Error>> {
317        let mut errors: Vec<Error> = vec![];
318        let translation_langs: HashSet<_> = meta_overlays
319            .iter()
320            .map(|o| o.language().unwrap())
321            .collect();
322
323        let missing_enforcement: HashSet<&_> =
324            translation_langs.difference(enforced_langs).collect();
325        for m in missing_enforcement {
326            errors.push(Error::UnexpectedTranslations(**m));
327        }
328
329        let missing_translations: HashSet<&_> =
330            enforced_langs.difference(&translation_langs).collect();
331        for m in missing_translations {
332            errors.push(Error::MissingTranslations(**m));
333        }
334
335        let attributes = meta_overlays
336            .iter()
337            .flat_map(|o| o.attr_pairs.keys())
338            .collect::<HashSet<_>>();
339
340        for meta_overlay in meta_overlays {
341            attributes.iter().for_each(|attr| {
342                if !meta_overlay.attr_pairs.contains_key(*attr) {
343                    errors.push(Error::MissingMetaTranslation(
344                        *meta_overlay.language().unwrap(),
345                        attr.to_string(),
346                    ));
347                }
348            });
349        }
350
351        if errors.is_empty() {
352            Ok(())
353        } else {
354            Err(errors)
355        }
356    }
357
358    fn validate_translations(
359        &self,
360        enforced_langs: &HashSet<&Language>,
361        overlays: Vec<&DynOverlay>,
362    ) -> Result<(), Vec<Error>> {
363        let mut errors: Vec<Error> = vec![];
364
365        let overlay_langs: HashSet<_> = overlays.iter().map(|x| x.language().unwrap()).collect();
366
367        let missing_enforcement: HashSet<&_> = overlay_langs.difference(enforced_langs).collect();
368        for m in missing_enforcement {
369            errors.push(Error::UnexpectedTranslations(**m)); // why we have && here?
370        }
371
372        let missing_translations: HashSet<&_> = enforced_langs.difference(&overlay_langs).collect();
373        for m in missing_translations {
374            errors.push(Error::MissingTranslations(**m)); // why we have && here?
375        }
376
377        let all_attributes: HashSet<&String> =
378            overlays.iter().flat_map(|o| o.attributes()).collect();
379        for overlay in overlays.iter() {
380            let attributes: HashSet<_> = overlay.attributes().into_iter().collect();
381
382            let missing_attr_translation: HashSet<&_> =
383                all_attributes.difference(&attributes).collect();
384            for m in missing_attr_translation {
385                errors.push(Error::MissingAttributeTranslation(
386                    *overlay.language().unwrap(),
387                    m.to_string(),
388                ));
389            }
390        }
391
392        if errors.is_empty() {
393            Ok(())
394        } else {
395            Err(errors)
396        }
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use crate::controller::load_oca;
404    use crate::state::{
405        attribute::{Attribute, AttributeType},
406        encoding::Encoding,
407        oca::overlay::character_encoding::CharacterEncodings,
408        oca::overlay::conditional::Conditionals,
409        oca::overlay::label::Labels,
410        oca::overlay::meta::Metas,
411        oca::OCABox,
412    };
413
414    #[test]
415    fn validate_valid_oca() {
416        let validator = Validator::new().enforce_translations(vec![Language::Eng, Language::Pol]);
417
418        let mut oca = cascade! {
419            OCABox::new();
420            ..add_meta(Language::Eng, "name".to_string(), "Driving Licence".to_string());
421            ..add_meta(Language::Eng, "description".to_string(), "DL".to_string());
422            ..add_meta(Language::Pol, "name".to_string(), "Prawo Jazdy".to_string());
423            ..add_meta(Language::Pol, "description".to_string(), "PJ".to_string());
424        };
425
426        let attribute = cascade! {
427            Attribute::new("name".to_string());
428            ..set_attribute_type(NestedAttrType::Value(AttributeType::Text));
429            ..set_encoding(Encoding::Utf8);
430            ..set_label(Language::Eng, "Name: ".to_string());
431            ..set_label(Language::Pol, "ImiÄ™: ".to_string());
432        };
433
434        oca.add_attribute(attribute);
435
436        let attribute_2 = cascade! {
437            Attribute::new("age".to_string());
438            ..set_attribute_type(NestedAttrType::Value(AttributeType::Numeric));
439            ..set_label(Language::Eng, "Age: ".to_string());
440            ..set_label(Language::Pol, "Wiek: ".to_string());
441        };
442
443        oca.add_attribute(attribute_2);
444
445        let oca_bundle = oca.generate_bundle();
446
447        let result = validator.validate(&oca_bundle);
448
449        if let Err(ref errors) = result {
450            println!("{errors:?}");
451        }
452        assert!(result.is_ok());
453    }
454
455    #[test]
456    fn validate_oca_with_missing_name_translation() {
457        let validator = Validator::new().enforce_translations(vec![Language::Eng, Language::Pol]);
458
459        let mut oca = cascade! {
460            OCABox::new();
461            ..add_meta(Language::Eng, "name".to_string(), "Driving Licence".to_string());
462        };
463
464        let oca_bundle = oca.generate_bundle();
465
466        let result = validator.validate(&oca_bundle);
467
468        assert!(result.is_err());
469        if let Err(errors) = result {
470            assert_eq!(errors.len(), 1);
471        }
472    }
473
474    #[test]
475    fn validate_oca_with_standards() {
476        /*         let validator = Validator::new();
477
478        let oca = OCABuilder::new(Encoding::Utf8)
479            .add_attribute(
480                AttributeBuilder::new("test".to_string(), AttributeType::Text)
481                    .add_standard("asd".to_string())
482                    .build(),
483            )
484            .finalize();
485
486        let result = validator.validate(&oca);
487
488        assert!(result.is_err());
489        if let Err(errors) = result {
490            assert_eq!(errors.len(), 1);
491        } */
492    }
493
494    #[test]
495    fn validate_oca_with_invalid_saids() {
496        let validator = Validator::new();
497        let data = r#"
498{
499    "version": "OCAB10000023_",
500    "said": "EBQMQm_tXSC8tnNICl7paGUeGg0SyF1tceHhTUutn1PN",
501    "capture_base": {
502        "type": "spec/capture_base/1.0",
503        "said": "EBQMQm_tXSC8tnNICl7paGUeGg0SyF1tceHhTUutn1PN",
504        "classification": "",
505        "attributes": {
506            "n1": "Text",
507            "n2": "DateTime",
508            "n3": "refs:EBQMQm_tXSC8tnNICl7paGUeGg0SyF1tceHhTUutn1aP"
509        },
510        "flagged_attributes": ["n1"]
511    },
512    "overlays": {
513        "character_encoding": {
514            "capture_base": "EDRt2wL8yVWVSJdF8aMFtU9VQ6aWzXZTgWj3WqsIKLqm",
515            "said": "EBQMQm_tXSC8tnNICl7paGUeGg0SyF1tceHhTUutn1PN",
516            "type": "spec/overlays/character_encoding/1.0",
517            "default_character_encoding": "utf-8",
518            "attribute_character_encoding": {}
519        }
520    }
521}
522        "#;
523        let oca_bundle = load_oca(&mut data.as_bytes());
524        match oca_bundle {
525            Ok(oca_bundle) => {
526                let result = validator.validate(&oca_bundle);
527                assert!(result.is_err());
528                if let Err(errors) = result {
529                    println!("{:?}", errors);
530                    assert_eq!(errors.len(), 4);
531                }
532            }
533            Err(e) => {
534                println!("{:?}", e);
535                panic!("Failed to load OCA bundle");
536            }
537        }
538    }
539
540    #[test]
541    fn validate_oca_with_conditional() {
542        let validator = Validator::new();
543
544        let mut oca = OCABox::new();
545
546        let attribute_age = cascade! {
547            Attribute::new("age".to_string());
548            ..set_attribute_type(NestedAttrType::Value(AttributeType::Numeric));
549            ..set_encoding(Encoding::Utf8);
550        };
551
552        oca.add_attribute(attribute_age);
553
554        let attribute_name = cascade! {
555            Attribute::new("name".to_string());
556            ..set_attribute_type(NestedAttrType::Value(AttributeType::Text));
557            ..set_condition(
558                "${age} > 18 and ${age} < 30".to_string()
559            );
560        };
561
562        oca.add_attribute(attribute_name);
563
564        let oca_bundle = oca.generate_bundle();
565        let result = validator.validate(&oca_bundle);
566        assert!(result.is_ok());
567
568        /* println!("{:?}", result);
569        assert!(result.is_err());
570        if let Err(errors) = result {
571            assert_eq!(errors.len(), 1);
572        } */
573    }
574}