oca_bundle_semantics/state/
validator.rs1use 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 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(); let condition_dependencies = dependencies.get(attr).unwrap(); 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(); 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 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: ®ex::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)); }
371
372 let missing_translations: HashSet<&_> = enforced_langs.difference(&overlay_langs).collect();
373 for m in missing_translations {
374 errors.push(Error::MissingTranslations(**m)); }
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 }
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 }
574}