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