1pub mod error;
2
3use log::debug;
4pub use oca_ast::ast::OCAAst;
5mod instructions;
6
7use self::{
8 error::ParseError,
9 instructions::{add::AddInstruction, from::FromInstruction, remove::RemoveInstruction},
10};
11use crate::ocafile::error::InstructionError;
12use oca_ast::{
13 ast::{
14 self, Command, CommandMeta, NestedAttrType, RefValue,
15 recursive_attributes::NestedAttrTypeFrame,
16 },
17 validator::{OCAValidator, Validator},
18};
19use overlay_file::overlay_registry::OverlayRegistry;
20use pest::Parser;
21use recursion::CollapsibleExt;
22
23#[derive(pest_derive::Parser)]
24#[grammar = "ocafile.pest"]
25pub struct OCAfileParser;
26
27pub type Pair<'a> = pest::iterators::Pair<'a, Rule>;
28
29pub trait TryFromPair {
30 type Error;
31 fn try_from_pair(
32 pair: Pair<'_>,
33 registry: &dyn OverlayRegistry,
34 ) -> Result<Command, Self::Error>;
35}
36
37impl TryFromPair for Command {
38 type Error = InstructionError;
39 fn try_from_pair(
40 record: Pair,
41 registry: &dyn OverlayRegistry,
42 ) -> std::result::Result<Self, Self::Error> {
43 let instruction: Command = match record.as_rule() {
44 Rule::from => FromInstruction::from_record(record, 0)?,
45 Rule::add => AddInstruction::from_record(record, 0, registry)?,
46 Rule::remove => RemoveInstruction::from_record(record, 0)?,
47 _ => return Err(InstructionError::UnexpectedToken(record.to_string())),
48 };
49 Ok(instruction)
50 }
51}
52
53pub fn parse_from_string(
54 unparsed_file: String,
55 registry: &dyn OverlayRegistry,
56) -> Result<OCAAst, ParseError> {
57 let file = OCAfileParser::parse(Rule::file, &unparsed_file)
58 .map_err(|e| {
59 let (line_number, column_number) = match e.line_col {
60 pest::error::LineColLocation::Pos((line, column)) => (line, column),
61 pest::error::LineColLocation::Span((line, column), _) => (line, column),
62 };
63 ParseError::GrammarError {
64 line_number,
65 column_number,
66 raw_line: e.line().to_string(),
67 message: e.variant.to_string(),
68 }
69 })?
70 .next()
71 .unwrap();
72
73 let mut oca_ast = OCAAst::new();
74
75 let validator = OCAValidator {};
76
77 for (n, line) in file.into_inner().enumerate() {
78 if let Rule::EOI = line.as_rule() {
79 continue;
80 }
81 if let Rule::comment = line.as_rule() {
82 continue;
83 }
84 if let Rule::meta_comment = line.as_rule() {
85 let mut key = "".to_string();
86 let mut value = "".to_string();
87 for attr in line.into_inner() {
88 match attr.as_rule() {
89 Rule::meta_attr_key => {
90 key = attr.as_str().to_string();
91 }
92 Rule::meta_attr_value => {
93 value = attr.as_str().to_string();
94 }
95 _ => {
96 return Err(ParseError::MetaError(attr.as_str().to_string()));
97 }
98 }
99 }
100 if key.is_empty() {
101 return Err(ParseError::MetaError("key is empty".to_string()));
102 }
103 if value.is_empty() {
104 return Err(ParseError::MetaError("value is empty".to_string()));
105 }
106 oca_ast.meta.insert(key, value);
107 continue;
108 }
109 if let Rule::empty_line = line.as_rule() {
110 continue;
111 }
112
113 match Command::try_from_pair(line.clone(), registry) {
114 Ok(command) => match validator.validate(&oca_ast, command.clone()) {
115 Ok(_) => {
116 oca_ast.commands.push(command);
117 oca_ast.commands_meta.insert(
118 oca_ast.commands.len() - 1,
119 CommandMeta {
120 line_number: n + 1,
121 raw_line: line.as_str().to_string().to_lowercase(),
122 },
123 );
124 }
125 Err(e) => {
126 return Err(ParseError::Custom(format!(
127 "Error validating instruction: {}",
128 e
129 )));
130 }
131 },
132 Err(e) => {
133 return Err(ParseError::InstructionError(e));
134 }
135 };
136 }
137 Ok(oca_ast)
138}
139
140fn format_reference(ref_value: RefValue) -> String {
142 match ref_value {
143 RefValue::Said(said) => format!("refs:{}", said),
144 _ => panic!("Unsupported reference type: {:?}", ref_value),
145 }
146}
147
148fn oca_file_format(nested: NestedAttrType) -> String {
150 nested.collapse_frames(|frame| match frame {
151 NestedAttrTypeFrame::Reference(ref_value) => format_reference(ref_value),
152 NestedAttrTypeFrame::Value(value) => {
153 format!("{}", value)
154 }
155 NestedAttrTypeFrame::Array(arr) => {
157 format!("[{}]", arr)
158 }
159 NestedAttrTypeFrame::Null => "".to_string(),
160 })
161}
162
163fn format_nested_value(value: &ast::NestedValue, indent: usize) -> String {
164 match value {
165 ast::NestedValue::Value(v) => v.to_string(),
166 ast::NestedValue::Reference(ref_value) => format_reference(ref_value.clone()),
167 ast::NestedValue::Object(obj) => obj
168 .iter()
169 .map(|(k, v)| {
170 let formatted_value = format_nested_value(v, indent + 2);
171 if v.is_object() {
172 format!("{}{}\n{}", " ".repeat(indent), k, formatted_value)
173 } else if v.is_array() || v.is_reference() {
174 format!("{}{}={}", " ".repeat(indent), k, formatted_value)
175 } else {
176 format!("{}{}=\"{}\"", " ".repeat(indent), k, formatted_value)
177 }
178 })
179 .collect::<Vec<_>>()
180 .join("\n"),
181 ast::NestedValue::Array(arr) => {
182 let formatted = arr
183 .iter()
184 .map(|v| format_nested_value(v, 0))
185 .collect::<Vec<_>>();
186 let formatted = formatted.join("\", \"");
187 format!("[\"{}\"]", formatted)
188 }
189 }
190}
191
192pub fn generate_from_ast(ast: &OCAAst) -> String {
198 let mut ocafile = String::new();
199
200 ast.commands.iter().for_each(|command| {
201 let mut line = String::new();
202
203 debug!("Processing command: {:?}", command);
204 match command.kind {
205 ast::CommandType::Add => {
206 line.push_str("ADD ");
207 match &command.object_kind {
208 ast::ObjectKind::CaptureBase(content) => {
209 if let Some(attributes) = &content.attributes {
210 line.push_str("ATTRIBUTE");
211 for (key, value) in attributes {
212 line.push_str(&format!(" {}=", key));
213 let out = oca_file_format(value.clone());
215 line.push_str(&out);
216 }
217 }
218 }
219 ast::ObjectKind::Overlay(content) => {
220 line.push_str("Overlay ");
221 let name = content.overlay_def.get_name();
222 line.push_str(name);
223 if let Some(content) = command.object_kind.overlay_content()
224 && let Some(ref properties) = content.properties
225 {
226 let properties = properties.clone();
227 if !properties.is_empty() {
228 line.push('\n');
229 properties.iter().for_each(|(key, value)| {
230 let formatted_value = format_nested_value(value, 4);
231 if value.is_object() {
232 line.push_str(&format!(" {}\n{}\n", key, formatted_value));
233 } else if value.is_array() {
234 line.push_str(&format!(" {}={}\n", key, formatted_value));
235 } else if value.is_reference() {
236 line.push_str(&format!(" {}={}\n", key, value));
237 } else {
238 line.push_str(&format!(
239 " {}=\"{}\"\n",
240 key, formatted_value
241 ));
242 }
243 });
244 }
245 };
246 }
247 _ => {
248 return;
249 }
250 }
251 }
252 ast::CommandType::Remove => match &command.object_kind {
253 ast::ObjectKind::CaptureBase(content) => {
254 line.push_str("REMOVE ");
255 if let Some(attributes) = &content.attributes {
256 line.push_str("ATTRIBUTE");
257 for (key, _) in attributes {
258 line.push_str(&format!(" {}", key));
259 }
260 }
261 }
262 ast::ObjectKind::Overlay(_) => {
263 todo!()
264 }
265 _ => {}
266 },
267 ast::CommandType::From => {
268 line.push_str("FROM ");
269 }
270 ast::CommandType::Modify => todo!(),
271 }
272
273 ocafile.push_str(format!("{}\n", line).as_str());
274 });
275
276 ocafile
277}
278
279#[cfg(test)]
280mod tests {
281 use oca_ast::ast::AttributeType;
282 use overlay_file::overlay_registry::OverlayLocalRegistry;
283 use said::derivation::{HashFunction, HashFunctionCode};
284
285 use super::{error::ExtractingAttributeError, *};
286
287 #[test]
288 fn parse_from_string_valid() {
289 let _ = env_logger::builder().is_test(true).try_init();
290 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
291
292 let unparsed_file = r#"
293-- version=2.0.0
294-- name=プラスウルトラ
295ADD ATTRIBUTE remove=Text
296ADD ATTRIBUTE name=Text
297 age=Numeric
298 car=[refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu]
299REMOVE ATTRIBUTE remove
300ADD ATTRIBUTE incidentals_spare_parts=[[refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu]]
301ADD ATTRIBUTE d=Text i=Text passed=Boolean
302ADD Overlay META
303 language="en"
304 description="Entrance credential"
305 name="Entrance credential"
306ADD Overlay CHARACTER_ENCODING
307 attribute_character_encodings
308 d="utf-8"
309 i="utf-8"
310 passed="utf-8"
311ADD Overlay CONFORMANCE
312 attribute_conformances=["d", "i", "passed"]
313ADD Overlay LABEL
314 language="en"
315 attribute_labels
316 d="Schema digest"
317 i="Credential Issuee"
318 passed="Passed"
319ADD Overlay FORMAT
320 attribute_formats
321 d="image/jpeg"
322ADD Overlay UNIT
323 metric_system="SI"
324 attribute_units
325 i="m^2"
326 d="°"
327ADD ATTRIBUTE list=[Text] el=Text
328ADD Overlay CARDINALITY
329 attribute_cardinalities
330 list="1-2"
331ADD Overlay ENTRY_CODE
332 attribute_entry_codes
333 list=refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu
334 el=["o1", "o2", "o3"]
335ADD Overlay ENTRY
336 language="en"
337 attribute_entries
338 list=refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu
339 el
340 o1="o1_label"
341 o2="o2_label"
342 o3="o3_label"
343"#;
344 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
345 assert_eq!(oca_ast.meta.get("version").unwrap(), "2.0.0");
346 assert_eq!(oca_ast.meta.get("name").unwrap(), "プラスウルトラ");
347 assert_eq!(oca_ast.commands.len(), 15);
348 let character_encoding_overlay = oca_ast.commands[6].object_kind.clone();
349 assert_eq!(
350 character_encoding_overlay
351 .overlay_content()
352 .unwrap()
353 .overlay_def
354 .get_full_name(),
355 "character_encoding/2.0.0".to_string()
356 );
357 }
358
359 #[test]
360 fn parse_meta_from_string_valid() {
361 let _ = env_logger::builder().is_test(true).try_init();
362 let unparsed_file = r#"
363-- version=0.0.1
364-- name=Objekt
365ADD attribute name=Text age=Numeric
366"#;
367
368 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
369 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
370 assert_eq!(oca_ast.meta.get("version").unwrap(), "0.0.1");
371 assert_eq!(oca_ast.meta.get("name").unwrap(), "Objekt");
372 }
373
374 #[test]
375 fn test_deserialization_ast_to_ocafile() {
376 let _ = env_logger::builder().is_test(true).try_init();
377 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
378 let unparsed_file = r#"ADD ATTRIBUTE name=Text age=Numeric radio=Text list=Text
379ADD Overlay LABEL
380 language="eo"
381 attribute_labels
382 name="Nomo"
383 age="aĝo"
384 radio="radio"
385
386ADD Overlay CHARACTER_ENCODING
387 attribute_character_encodings
388 name="utf-8"
389 age="utf-8"
390
391ADD Overlay ENTRY_CODE
392 attribute_entry_codes
393 radio=["o1", "o2", "o3"]
394
395ADD Overlay ENTRY
396 language="eo"
397 attribute_entries
398 radio
399 o1="etikedo1"
400 o2="etikedo2"
401 o3="etikiedo3"
402
403ADD Overlay ENTRY
404 language="pl"
405 attribute_entries
406 radio
407 o1="etykieta1"
408 o2="etykieta2"
409 o3="etykieta3"
410
411ADD Overlay ENTRY_CODE
412 attribute_entry_codes
413 list
414 g1=["el1"]
415 g2=["el2", "el3"]
416
417ADD Overlay ENTRY
418 language="pl"
419 attribute_entries
420 list
421 el1="element1"
422 el2="element2"
423 el3="element3"
424 g1="grupa1"
425 g2="grupa2"
426
427"#;
428 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
429
430 let ocafile = generate_from_ast(&oca_ast);
431 let oca_ast2 = parse_from_string(ocafile.clone(), ®istry).unwrap();
432 assert_eq!(oca_ast, oca_ast2,);
433 }
434
435 #[test]
436 fn test_attributes_with_special_names() {
437 let _ = env_logger::builder().is_test(true).try_init();
438 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
439 let unparsed_file = r#"ADD ATTRIBUTE person.name=Text Experiment...Range..original.values.=[Text]
440"#;
441 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
442
443 let ocafile = generate_from_ast(&oca_ast);
444 assert_eq!(
445 ocafile, unparsed_file,
446 "left:\n{} \n right:\n {}",
447 ocafile, unparsed_file
448 );
449 }
450
451 #[test]
452 fn test_attributes_from_ast_to_ocafile() {
453 let _ = env_logger::builder().is_test(true).try_init();
454 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
455 let unparsed_file = r#"ADD ATTRIBUTE name=Text age=Numeric
456ADD ATTRIBUTE list=[Text] el=Text
457"#;
458 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
459
460 let ocafile = generate_from_ast(&oca_ast);
461 assert_eq!(
462 ocafile, unparsed_file,
463 "left:\n{} \n right:\n {}",
464 ocafile, unparsed_file
465 );
466 }
467
468 #[test]
469 fn test_nested_attributes_from_ocafile_to_ast() {
470 let _ = env_logger::builder().is_test(true).try_init();
471 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
472 let unparsed_file = r#"ADD ATTRIBUTE name=Text age=Numeric car=[[Text]]
473ADD ATTRIBUTE incidentals_spare_parts=[refs:EJVVlVSZJqVNnuAMLHLkeSQgwfxYLWTKBELi9e8j1PW0]
474"#;
475 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
476
477 let ocafile = generate_from_ast(&oca_ast);
478 assert_eq!(
479 ocafile, unparsed_file,
480 "left:\n{} \n right:\n {}",
481 ocafile, unparsed_file
482 );
483 }
484
485 #[test]
486 fn test_wrong_said() {
487 let _ = env_logger::builder().is_test(true).try_init();
488 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
489 let unparsed_file = r#"ADD ATTRIBUTE said=refs:digest"#;
490 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry);
491 match oca_ast.unwrap_err() {
492 ParseError::InstructionError(InstructionError::ExtractError(
493 ExtractingAttributeError::SaidError(e),
494 )) => {
495 assert_eq!(e.to_string(), "Unknown code")
496 }
497 _ => unreachable!(),
498 }
499 }
500
501 #[test]
502 fn test_oca_file_format() {
503 let _ = env_logger::builder().is_test(true).try_init();
504 let text_type = NestedAttrType::Value(AttributeType::Text);
505 assert_eq!(oca_file_format(text_type), "Text");
506
507 let numeric_type = NestedAttrType::Value(AttributeType::Numeric);
508 assert_eq!(oca_file_format(numeric_type), "Numeric");
509
510 let ref_type = NestedAttrType::Reference(RefValue::Said(
511 HashFunction::from(HashFunctionCode::Blake3_256).derive("example".as_bytes()),
512 ));
513
514 let attr = NestedAttrType::Array(Box::new(NestedAttrType::Array(Box::new(ref_type))));
515
516 let out = oca_file_format(attr);
517 assert_eq!(out, "[[refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu]]");
518 }
519
520 #[test]
521 fn test_line_breaking_in_ocafile() {
522 let _ = env_logger::builder().is_test(true).try_init();
523 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
524
525 let unparsed_file = r#"
527ADD ATTRIBUTE dateOfBirth=DateTime \
528 documentNumber=Text
529"#;
530 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
531
532 assert_eq!(oca_ast.commands.len(), 1);
534
535 if let ast::ObjectKind::CaptureBase(content) = &oca_ast.commands[0].object_kind {
537 let attributes = content.attributes.as_ref().unwrap();
538 assert_eq!(attributes.len(), 2);
539 assert!(attributes.contains_key("dateOfBirth"));
540 assert!(attributes.contains_key("documentNumber"));
541 } else {
542 panic!("Expected CaptureBase");
543 }
544
545 let ocafile = generate_from_ast(&oca_ast);
547 let oca_ast2 = parse_from_string(ocafile.clone(), ®istry).unwrap();
548
549 if let ast::ObjectKind::CaptureBase(content) = &oca_ast2.commands[0].object_kind {
550 let attributes = content.attributes.as_ref().unwrap();
551 assert_eq!(attributes.len(), 2);
552 assert!(attributes.contains_key("dateOfBirth"));
553 assert!(attributes.contains_key("documentNumber"));
554 } else {
555 panic!("Expected CaptureBase");
556 }
557 }
558
559 #[test]
560 fn test_attribute_type_case_insensitive() {
561 let unparsed_file = r#"ADD ATTRIBUTE a=Text b=TEXT c=text d=TeXt e=Numeric f=NUMERIC g=numeric h=BoOlEaN i=BINARY j=datetime"#;
562
563 let registry = OverlayLocalRegistry::new();
564 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
565 let attrs = oca_ast.commands[0]
566 .object_kind
567 .capture_content()
568 .unwrap()
569 .attributes
570 .as_ref()
571 .unwrap();
572 assert_eq!(
573 attrs.get("a").unwrap(),
574 &NestedAttrType::Value(AttributeType::Text)
575 );
576 assert_eq!(
577 attrs.get("b").unwrap(),
578 &NestedAttrType::Value(AttributeType::Text)
579 );
580 assert_eq!(
581 attrs.get("c").unwrap(),
582 &NestedAttrType::Value(AttributeType::Text)
583 );
584 assert_eq!(
585 attrs.get("d").unwrap(),
586 &NestedAttrType::Value(AttributeType::Text)
587 );
588 assert_eq!(
589 attrs.get("e").unwrap(),
590 &NestedAttrType::Value(AttributeType::Numeric)
591 );
592 assert_eq!(
593 attrs.get("f").unwrap(),
594 &NestedAttrType::Value(AttributeType::Numeric)
595 );
596 assert_eq!(
597 attrs.get("g").unwrap(),
598 &NestedAttrType::Value(AttributeType::Numeric)
599 );
600 assert_eq!(
601 attrs.get("h").unwrap(),
602 &NestedAttrType::Value(AttributeType::Boolean)
603 );
604 assert_eq!(
605 attrs.get("i").unwrap(),
606 &NestedAttrType::Value(AttributeType::Binary)
607 );
608 assert_eq!(
609 attrs.get("j").unwrap(),
610 &NestedAttrType::Value(AttributeType::DateTime)
611 );
612 }
613
614 #[test]
615 fn test_language_variants_roundtrip() {
616 let _ = env_logger::builder().is_test(true).try_init();
617 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
618 let language_cases = [
619 "PL", "pl", "pol", "pl_PL", "en_UK", "eng", "EN", "en", "en-UK",
620 ];
621
622 for lang in language_cases {
623 let unparsed_file = format!(
624 "ADD ATTRIBUTE name=Text\nADD Overlay LABEL\n language=\"{}\"\n attribute_labels\n name=\"Name\"\n",
625 lang
626 );
627
628 let oca_ast = parse_from_string(unparsed_file, ®istry).unwrap();
629 let ocafile = generate_from_ast(&oca_ast);
630 let oca_ast2 = parse_from_string(ocafile, ®istry).unwrap();
631
632 assert_eq!(oca_ast, oca_ast2);
633 }
634 }
635}