Skip to main content

oca_bundle/state/
validator.rs

1use isolang::Language;
2use std::collections::{HashMap, HashSet};
3
4use crate::state::oca_bundle::OCABundleModel;
5
6#[derive(Debug)]
7pub enum Error {
8    Custom(String),
9    MissingTranslations(Language),
10    MissingMetaTranslation(Language, String),
11    UnexpectedTranslations(Language),
12    MissingAttributeTranslation(Language, String),
13}
14
15impl std::fmt::Display for Error {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        match self {
18            Error::Custom(error) => write!(f, "{error}"),
19            Error::MissingTranslations(language) => {
20                write!(f, "Missing translation in {language} language")
21            }
22            Error::MissingMetaTranslation(language, attr) => write!(
23                f,
24                "Missing meta translation for {attr} in {language} language"
25            ),
26            Error::UnexpectedTranslations(language) => {
27                write!(f, "Unexpected translations in {language} language")
28            }
29            Error::MissingAttributeTranslation(language, attr) => {
30                write!(f, "Missing translation for {attr} in {language} language")
31            }
32        }
33    }
34}
35
36impl std::error::Error for Error {}
37
38pub enum SemanticValidationStatus {
39    Valid,
40    Invalid(Vec<Error>),
41}
42
43pub fn validate(oca_bundle: &OCABundleModel) -> Result<SemanticValidationStatus, String> {
44    let validator = Validator::new();
45    match validator.validate(oca_bundle) {
46        Ok(_) => Ok(SemanticValidationStatus::Valid),
47        Err(errors) => Ok(SemanticValidationStatus::Invalid(errors)),
48    }
49}
50
51pub struct Validator {
52    enforced_translations: Vec<Language>,
53}
54
55impl Default for Validator {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl Validator {
62    pub fn new() -> Validator {
63        Validator {
64            enforced_translations: vec![],
65        }
66    }
67
68    pub fn enforce_translations(mut self, languages: Vec<Language>) -> Validator {
69        self.enforced_translations = self
70            .enforced_translations
71            .into_iter()
72            .chain(languages)
73            .collect::<Vec<Language>>();
74        self
75    }
76
77    pub fn validate(self, _oca_bundle: &OCABundleModel) -> Result<(), Vec<Error>> {
78        let _enforced_langs: HashSet<_> = self.enforced_translations.iter().collect();
79        let mut errors: Vec<Error> = vec![];
80        self.validate_unique_keys(_oca_bundle, &mut errors);
81
82        /* let oca_bundle: OCABundle = serde_json::from_str(oca_str.as_str())
83                   .map_err(|e| vec![Error::Custom(e.to_string())])?;
84        */
85        // let mut recalculated_oca_bundle = oca_bundle.clone();
86        // recalculated_oca_bundle.fill_said();
87        //
88        // if oca_bundle.said.ne(&recalculated_oca_bundle.said) {
89        //     errors.push(Error::Custom("OCA Bundle: Malformed SAID".to_string()));
90        // }
91        //
92        // let capture_base = &oca_bundle.capture_base;
93        //
94        // let mut recalculated_capture_base = capture_base.clone();
95        // recalculated_capture_base.calculate_said();
96        //
97        // if capture_base.said.ne(&recalculated_capture_base.said) {
98        //     errors.push(Error::Custom("capture_base: Malformed SAID".to_string()));
99        // }
100
101        // for o in &oca_bundle.overlays {
102        //     let mut recalculated_overlay = o.clone();
103        //     recalculated_overlay.fill_said();
104        //     if o.digest.ne(&recalculated_overlay.digest) {
105        //         // let msg = match o.language() {
106        //         //     Some(lang) => format!("{} ({}): Malformed SAID", o.overlay_type(), lang),
107        //         //     None => format!("{}: Malformed SAID", o.overlay_type()),
108        //         // };
109        //         let msg = format!("{}: Malformed SAID", o.name);
110        //         errors.push(Error::Custom(msg));
111        //     }
112        //
113        //     if o.capture_base.ne(&capture_base.said) {
114        //         // let msg = match o.language() {
115        //         //     Some(lang) => {
116        //         //         format!("{} ({}): Mismatch capture_base SAI", o.overlay_type(), lang)
117        //         //     }
118        //         //     None => format!("{}: Mismatch capture_base SAI", o.overlay_type()),
119        //         // };
120        //         let msg = format!("{}: Mismatch capture_base SAI", o.name);
121        //         errors.push(Error::Custom(msg));
122        //     }
123        // }
124
125        // if !enforced_langs.is_empty() {
126        //     let meta_overlays = oca_bundle
127        //         .overlays
128        //         .iter()
129        //         .filter_map(|x| x.as_any().downcast_ref::<overlay::Meta>())
130        //         .collect::<Vec<_>>();
131        //
132        //     if !meta_overlays.is_empty() {
133        //         if let Err(meta_errors) = self.validate_meta(&enforced_langs, meta_overlays) {
134        //             errors = errors
135        //                 .into_iter()
136        //                 .chain(meta_errors.into_iter().map(|e| {
137        //                     if let Error::UnexpectedTranslations(lang) = e {
138        //                         Error::Custom(format!(
139        //                             "meta overlay: translations in {lang:?} language are not enforced"
140        //                         ))
141        //                     } else if let Error::MissingTranslations(lang) = e {
142        //                         Error::Custom(format!(
143        //                             "meta overlay: translations in {lang:?} language are missing"
144        //                         ))
145        //                     } else if let Error::MissingMetaTranslation(lang, attr) = e {
146        //                         Error::Custom(format!(
147        //                             "meta overlay: for '{attr}' translation in {lang:?} language is missing"
148        //                         ))
149        //                     } else {
150        //                         e
151        //                     }
152        //                 }))
153        //                 .collect();
154        //         }
155        //     }
156        //
157        //     for overlay_type in &["Entry", "Label"] {
158        //         let typed_overlays: Vec<_> = oca_bundle
159        //             .overlays
160        //             .iter()
161        //             .filter(|x| x.overlay_type().to_string().eq(&overlay_type.to_string()))
162        //             .collect();
163        //         if typed_overlays.is_empty() {
164        //             continue;
165        //         }
166        //
167        //         if let Err(translation_errors) =
168        //             self.validate_translations(&enforced_langs, typed_overlays)
169        //         {
170        //             errors = errors.into_iter().chain(
171        //                 translation_errors.into_iter().map(|e| {
172        //                     if let Error::UnexpectedTranslations(lang) = e {
173        //                         Error::Custom(
174        //                             format!("{overlay_type} overlay: translations in {lang:?} language are not enforced")
175        //                         )
176        //                     } else if let Error::MissingTranslations(lang) = e {
177        //                         Error::Custom(
178        //                             format!("{overlay_type} overlay: translations in {lang:?} language are missing")
179        //                         )
180        //                     } else if let Error::MissingAttributeTranslation(lang, attr_name) = e {
181        //                         Error::Custom(
182        //                             format!("{overlay_type} overlay: for '{attr_name}' attribute missing translations in {lang:?} language")
183        //                         )
184        //                     } else {
185        //                         e
186        //                     }
187        //                 })
188        //             ).collect();
189        //         }
190        //     }
191        // }
192
193        if errors.is_empty() {
194            Ok(())
195        } else {
196            Err(errors)
197        }
198    }
199
200    fn validate_unique_keys(&self, oca_bundle: &OCABundleModel, errors: &mut Vec<Error>) {
201        let mut seen: HashMap<String, HashSet<String>> = HashMap::new();
202
203        for overlay in &oca_bundle.overlays {
204            let overlay_def = match &overlay.overlay_def {
205                Some(def) => def,
206                None => continue,
207            };
208            if overlay_def.unique_keys.is_empty() {
209                continue;
210            }
211            let properties = match &overlay.properties {
212                Some(props) => props,
213                None => {
214                    errors.push(Error::Custom(format!(
215                        "Overlay {} is missing properties for unique keys",
216                        overlay_def.get_full_name()
217                    )));
218                    continue;
219                }
220            };
221
222            let mut parts = Vec::new();
223            let mut missing = Vec::new();
224            for key in &overlay_def.unique_keys {
225                match properties.get(key) {
226                    Some(value) => {
227                        let value_str =
228                            serde_json::to_string(value).unwrap_or_else(|_| value.to_string());
229                        parts.push(format!("{}={}", key, value_str));
230                    }
231                    None => missing.push(key.clone()),
232                }
233            }
234
235            if !missing.is_empty() {
236                errors.push(Error::Custom(format!(
237                    "Overlay {} is missing unique keys: {}",
238                    overlay_def.get_full_name(),
239                    missing.join(", ")
240                )));
241                continue;
242            }
243
244            let signature = parts.join("|");
245            let entry = seen.entry(overlay_def.get_full_name()).or_default();
246            if !entry.insert(signature.clone()) {
247                errors.push(Error::Custom(format!(
248                    "Duplicate overlay {} with unique keys {}",
249                    overlay_def.get_full_name(),
250                    signature
251                )));
252            }
253        }
254    }
255
256    // fn validate_meta(
257    //     &self,
258    //     enforced_langs: &HashSet<&Language>,
259    //     meta_overlays: Vec<&overlay::Meta>,
260    // ) -> Result<(), Vec<Error>> {
261    //     let mut errors: Vec<Error> = vec![];
262    //     let translation_langs: HashSet<_> = meta_overlays
263    //         .iter()
264    //         .map(|o| o.language().unwrap())
265    //         .collect();
266    //
267    //     let missing_enforcement: HashSet<&_> =
268    //         translation_langs.difference(enforced_langs).collect();
269    //     for m in missing_enforcement {
270    //         errors.push(Error::UnexpectedTranslations(**m));
271    //     }
272    //
273    //     let missing_translations: HashSet<&_> =
274    //         enforced_langs.difference(&translation_langs).collect();
275    //     for m in missing_translations {
276    //         errors.push(Error::MissingTranslations(**m));
277    //     }
278    //
279    //     let attributes = meta_overlays
280    //         .iter()
281    //         .flat_map(|o| o.attr_pairs.keys())
282    //         .collect::<HashSet<_>>();
283    //
284    //     for meta_overlay in meta_overlays {
285    //         attributes.iter().for_each(|attr| {
286    //             if !meta_overlay.attr_pairs.contains_key(*attr) {
287    //                 errors.push(Error::MissingMetaTranslation(
288    //                     *meta_overlay.language().unwrap(),
289    //                     attr.to_string(),
290    //                 ));
291    //             }
292    //         });
293    //     }
294    //
295    //     if errors.is_empty() {
296    //         Ok(())
297    //     } else {
298    //         Err(errors)
299    //     }
300    // }
301
302    // fn validate_translations(
303    //     &self,
304    //     enforced_langs: &HashSet<&Language>,
305    //     overlays: Vec<&DynOverlay>,
306    // ) -> Result<(), Vec<Error>> {
307    //     let mut errors: Vec<Error> = vec![];
308    //
309    //     let overlay_langs: HashSet<_> = overlays.iter().map(|x| x.language().unwrap()).collect();
310    //
311    //     let missing_enforcement: HashSet<&_> = overlay_langs.difference(enforced_langs).collect();
312    //     for m in missing_enforcement {
313    //         errors.push(Error::UnexpectedTranslations(**m)); // why we have && here?
314    //     }
315    //
316    //     let missing_translations: HashSet<&_> = enforced_langs.difference(&overlay_langs).collect();
317    //     for m in missing_translations {
318    //         errors.push(Error::MissingTranslations(**m)); // why we have && here?
319    //     }
320    //
321    //     let all_attributes: HashSet<&String> =
322    //         overlays.iter().flat_map(|o| o.attributes()).collect();
323    //     for overlay in overlays.iter() {
324    //         let attributes: HashSet<_> = overlay.attributes().into_iter().collect();
325    //
326    //         let missing_attr_translation: HashSet<&_> =
327    //             all_attributes.difference(&attributes).collect();
328    //         for m in missing_attr_translation {
329    //             errors.push(Error::MissingAttributeTranslation(
330    //                 *overlay.language().unwrap(),
331    //                 m.to_string(),
332    //             ));
333    //         }
334    //     }
335    //
336    //     if errors.is_empty() {
337    //         Ok(())
338    //     } else {
339    //         Err(errors)
340    //     }
341    // }
342}
343
344#[cfg(test)]
345mod tests {
346    use indexmap::IndexMap;
347    use oca_ast::ast::{NestedValue, OverlayContent};
348    use overlay_file::overlay_registry::OverlayLocalRegistry;
349    use overlay_file::parse_from_string;
350
351    use super::*;
352    use crate::controller::load_oca;
353    use crate::state::oca_bundle::OCABundleModel;
354    use crate::state::oca_bundle::capture_base::CaptureBase;
355    use crate::state::oca_bundle::overlay::OverlayModel;
356
357    #[test]
358    fn validate_valid_oca() {
359        // let validator = Validator::new().enforce_translations(vec![Language::Eng, Language::Pol]);
360        //
361        // // let mut oca = cascade! {
362        // //     OCABox::new();
363        // //     // ..add_meta(Language::Eng, "name".to_string(), "Driving Licence".to_string());
364        // //     // ..add_meta(Language::Eng, "description".to_string(), "DL".to_string());
365        // //     // ..add_meta(Language::Pol, "name".to_string(), "Prawo Jazdy".to_string());
366        // //     // ..add_meta(Language::Pol, "description".to_string(), "PJ".to_string());
367        // // };
368        //
369        // let attribute = cascade! {
370        //     Attribute::new("name".to_string());
371        //     ..set_attribute_type(NestedAttrType::Value(AttributeType::Text));
372        //     // ..set_encoding(Encoding::Utf8);
373        //     // ..set_label(Language::Eng, "Name: ".to_string());
374        //     // ..set_label(Language::Pol, "ImiÄ™: ".to_string());
375        // };
376        //
377        // oca.add_attribute(attribute);
378        //
379        // let attribute_2 = cascade! {
380        //     Attribute::new("age".to_string());
381        //     ..set_attribute_type(NestedAttrType::Value(AttributeType::Numeric));
382        //     // ..set_label(Language::Eng, "Age: ".to_string());
383        //     // ..set_label(Language::Pol, "Wiek: ".to_string());
384        // };
385        //
386        // oca.add_attribute(attribute_2);
387        //
388        // let oca_bundle = oca.generate_bundle();
389        //
390        // let result = validator.validate(&oca_bundle);
391        //
392        // if let Err(ref errors) = result {
393        //     println!("{errors:?}");
394        // }
395        //assert!(result.is_ok());
396    }
397
398    #[test]
399    fn validate_oca_with_missing_name_translation() {
400        // let validator = Validator::new().enforce_translations(vec![Language::Eng, Language::Pol]);
401        //
402        // let mut oca = cascade! {
403        //     OCABox::new();
404        //     // ..add_meta(Language::Eng, "name".to_string(), "Driving Licence".to_string());
405        // };
406        //
407        // let oca_bundle = oca.generate_bundle();
408        //
409        // let result = validator.validate(&oca_bundle);
410        //
411        // assert!(result.is_err());
412        // if let Err(errors) = result {
413        //     assert_eq!(errors.len(), 1);
414        // }
415    }
416
417    #[test]
418    #[ignore]
419    fn validate_oca_with_invalid_saids() {
420        let validator = Validator::new();
421        let data = r#"
422{
423  "v": "OCAS02JSON0007c1_",
424  "digest": "EDTaoqiaaL504P-HTxYWuiniwhrzGcP9ji-mPeJgudLk",
425  "capture_base": {
426    "digest": "EA0l-Sazi2X9cLn2pbVLr6C-t4-lVsSx3E_yJyEwTwum",
427    "type": "capture_base/2.0.0",
428    "attributes": {
429      "d": "Text",
430      "el": "Text",
431      "i": "Text",
432      "list": [
433        "Text"
434      ],
435      "passed": "Boolean"
436    }
437  },
438  "overlays": [
439    {
440      "digest": "EKN9PGIHxLuZe92ZDyrZulScFgTfAdjEc9xXEVb_WULX",
441      "capture_base": "EA0l-Sazi2X9cLn2pbVLr6C-t4-lVsSx3E_yJyEwTwum",
442      "type": "Meta/2.0.0",
443      "description": "Entrance credential",
444      "name": "Entrance credential"
445    },
446    {
447      "digest": "EFOAxxDMSnOiuah9OwoCdwkns8EfsurcHXF57-XdGnen",
448      "capture_base": "EA0l-Sazi2X9cLn2pbVLr6C-t4-lVsSx3E_yJyEwTwum",
449      "type": "Character_Encoding/2.0.0",
450      "d": "utf-8",
451      "i": "utf-8",
452      "passed": "utf-8"
453    },
454    {
455      "digest": "ELivUa6QlCOpidnqLDs9Il1uqILb9pBUj2rLdGgqWDwv",
456      "capture_base": "EA0l-Sazi2X9cLn2pbVLr6C-t4-lVsSx3E_yJyEwTwum",
457      "type": "conformance/2.0.0",
458      "d": "M",
459      "i": "M",
460      "passed": "M"
461    },
462    {
463      "digest": "ECsW-Zb7A0TfG_M_HNH9wwKqil3rSiyKEfPE4398aQdC",
464      "capture_base": "EA0l-Sazi2X9cLn2pbVLr6C-t4-lVsSx3E_yJyEwTwum",
465      "type": "label/2.0.0",
466      "d": "Schema digest",
467      "i": "Credential Issuee",
468      "passed": "Passed"
469    },
470    {
471      "digest": "EJcEfNE3s_lZeUF1C_tez3qbThSsIJq4qV6WHlo-hmIL",
472      "capture_base": "EA0l-Sazi2X9cLn2pbVLr6C-t4-lVsSx3E_yJyEwTwum",
473      "type": "format/2.0.0",
474      "d": "image/jpeg"
475    },
476    {
477      "digest": "EICOF_bxwUyKC7W-blp51-YPPieJxqDPL7wrSkeT8jOg",
478      "capture_base": "EA0l-Sazi2X9cLn2pbVLr6C-t4-lVsSx3E_yJyEwTwum",
479      "type": "unit/2.0.0",
480      "i": "m"
481    },
482    {
483      "digest": "EPT1EDp2ofO1xJSQFehyZb8kCfMqXV8giTs0MeqQOp2a",
484      "capture_base": "EA0l-Sazi2X9cLn2pbVLr6C-t4-lVsSx3E_yJyEwTwum",
485      "type": "cardinality/2.0.0",
486      "list": "1-2"
487    },
488    {
489      "digest": "EKJ1z6PIFXqfP7wy6Hj21Of23HcoiT-b5P1qs_DgYJHo",
490      "capture_base": "EA0l-Sazi2X9cLn2pbVLr6C-t4-lVsSx3E_yJyEwTwum",
491      "type": "ENTRY_CODE/2.0.0",
492      "el": [
493        "o1",
494        "o2",
495        "o3"
496      ],
497      "list": "entry_code_said"
498    },
499    {
500      "digest": "EKohdNuyxHWPZ1dy-Om5Rx4RxufHM5jjDKBa3jyRvp52",
501      "capture_base": "EA0l-Sazi2X9cLn2pbVLr6C-t4-lVsSx3E_yJyEwTwum",
502      "type": "ENTRY/2.0.0",
503      "el": {
504        "o1": "o1_label",
505        "o2": "o2_label",
506        "o3": "o3_label"
507      },
508      "list": "refs:ENrf7niTCnz7HD-Ci88rlxHlxkpQ2NIZNNv08fQnXANI"
509    }
510  ]
511}
512"#;
513
514        let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays/").unwrap();
515        let oca_bundle = load_oca(&mut data.as_bytes(), &registry);
516        match oca_bundle {
517            Ok(oca_bundle) => {
518                let result = validator.validate(&oca_bundle);
519                assert!(result.is_err());
520                if let Err(errors) = result {
521                    println!("{:?}", errors);
522                    assert_eq!(errors.len(), 4);
523                }
524            }
525            Err(e) => {
526                println!("{:?}", e);
527                panic!("Failed to load OCA bundle");
528            }
529        }
530    }
531
532    #[test]
533    fn validate_unique_keys_multiple_and_duplicate_overlay() {
534        let overlay_file = r#"
535--name=Test
536ADD OVERLAY ReferenceValues
537  VERSION 1.0.1
538  UNIQUE KEYS [language, region]
539  ADD ATTRIBUTES language=Lang
540  ADD ATTRIBUTES region=Text
541"#;
542
543        let overlay_def = parse_from_string(overlay_file.to_string())
544            .unwrap()
545            .overlays_def
546            .remove(0);
547        assert_eq!(
548            overlay_def.unique_keys,
549            vec!["language".to_string(), "region".to_string()]
550        );
551
552        let mut properties = IndexMap::new();
553        properties.insert("language".to_string(), NestedValue::Value("en".to_string()));
554        properties.insert("region".to_string(), NestedValue::Value("US".to_string()));
555
556        let overlay_1 = OverlayModel::new(OverlayContent {
557            properties: Some(properties.clone()),
558            overlay_def: overlay_def.clone(),
559        });
560        let overlay_2 = OverlayModel::new(OverlayContent {
561            properties: Some(properties),
562            overlay_def,
563        });
564
565        let oca_bundle = OCABundleModel::new(CaptureBase::new(), vec![overlay_1, overlay_2]);
566        let validator = Validator::new();
567        let result = validator.validate(&oca_bundle);
568
569        assert!(result.is_err());
570        if let Err(errors) = result {
571            assert!(
572                errors
573                    .iter()
574                    .any(|error| error.to_string().contains("Duplicate overlay"))
575            );
576        }
577    }
578}