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(), ®istry);
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}