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
313 d="M"
314 i="M"
315 passed="M"
316ADD Overlay LABEL
317 language="en"
318 attribute_labels
319 d="Schema digest"
320 i="Credential Issuee"
321 passed="Passed"
322ADD Overlay FORMAT
323 attribute_formats
324 d="image/jpeg"
325ADD Overlay UNIT
326 metric_system="SI"
327 attribute_units
328 i="m^2"
329 d="°"
330ADD ATTRIBUTE list=[Text] el=Text
331ADD Overlay CARDINALITY
332 attribute_cardinalities
333 list="1-2"
334ADD Overlay ENTRY_CODE
335 attribute_entry_codes
336 list=refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu
337 el=["o1", "o2", "o3"]
338ADD Overlay ENTRY
339 language="en"
340 attribute_entries
341 list=refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu
342 el
343 o1="o1_label"
344 o2="o2_label"
345 o3="o3_label"
346"#;
347 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
348 assert_eq!(oca_ast.meta.get("version").unwrap(), "2.0.0");
349 assert_eq!(oca_ast.meta.get("name").unwrap(), "プラスウルトラ");
350 assert_eq!(oca_ast.commands.len(), 15);
351 let character_encoding_overlay = oca_ast.commands[6].object_kind.clone();
352 assert_eq!(
353 character_encoding_overlay
354 .overlay_content()
355 .unwrap()
356 .overlay_def
357 .get_full_name(),
358 "character_encoding/2.0.0".to_string()
359 );
360 }
361
362 #[test]
363 fn parse_meta_from_string_valid() {
364 let _ = env_logger::builder().is_test(true).try_init();
365 let unparsed_file = r#"
366-- version=0.0.1
367-- name=Objekt
368ADD attribute name=Text age=Numeric
369"#;
370
371 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
372 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
373 assert_eq!(oca_ast.meta.get("version").unwrap(), "0.0.1");
374 assert_eq!(oca_ast.meta.get("name").unwrap(), "Objekt");
375 }
376
377 #[test]
378 fn test_deserialization_ast_to_ocafile() {
379 let _ = env_logger::builder().is_test(true).try_init();
380 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
381 let unparsed_file = r#"ADD ATTRIBUTE name=Text age=Numeric radio=Text list=Text
382ADD Overlay LABEL
383 language="eo"
384 attribute_labels
385 name="Nomo"
386 age="aĝo"
387 radio="radio"
388
389ADD Overlay CHARACTER_ENCODING
390 attribute_character_encodings
391 name="utf-8"
392 age="utf-8"
393
394ADD Overlay ENTRY_CODE
395 attribute_entry_codes
396 radio=["o1", "o2", "o3"]
397
398ADD Overlay ENTRY
399 language="eo"
400 attribute_entries
401 radio
402 o1="etikedo1"
403 o2="etikedo2"
404 "o3"="etikiedo3"
405
406ADD Overlay ENTRY
407 language="pl"
408 attribute_entries
409 radio
410 "o1"="etykieta1"
411 "o2"="etykieta2"
412 "o3"="etykieta3"
413
414ADD Overlay ENTRY_CODE
415 attribute_entry_codes
416 list
417 "g1"=["el1"]
418 "g2"=["el2", "el3"]
419
420ADD Overlay ENTRY
421 language="pl"
422 attribute_entries
423 list
424 "el1"="element1"
425 "el2"="element2"
426 "el3"="element3"
427 "g1"="grupa1"
428 "g2"="grupa2"
429
430"#;
431 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
432
433 let ocafile = generate_from_ast(&oca_ast);
434 let oca_ast2 = parse_from_string(ocafile.clone(), ®istry).unwrap();
435 assert_eq!(oca_ast, oca_ast2,);
436 }
437
438 #[test]
439 fn test_attributes_with_special_names() {
440 let _ = env_logger::builder().is_test(true).try_init();
441 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
442 let unparsed_file = r#"ADD ATTRIBUTE "person.name"=Text "Experiment...Range..original.values."=[Text]
443"#;
444 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
445
446 let ocafile = generate_from_ast(&oca_ast);
447 assert_eq!(
448 ocafile, unparsed_file,
449 "left:\n{} \n right:\n {}",
450 ocafile, unparsed_file
451 );
452 }
453
454 #[test]
455 fn test_attributes_from_ast_to_ocafile() {
456 let _ = env_logger::builder().is_test(true).try_init();
457 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
458 let unparsed_file = r#"ADD ATTRIBUTE name=Text age=Numeric
459ADD ATTRIBUTE list=[Text] el=Text
460"#;
461 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
462
463 let ocafile = generate_from_ast(&oca_ast);
464 assert_eq!(
465 ocafile, unparsed_file,
466 "left:\n{} \n right:\n {}",
467 ocafile, unparsed_file
468 );
469 }
470
471 #[test]
472 fn test_nested_attributes_from_ocafile_to_ast() {
473 let _ = env_logger::builder().is_test(true).try_init();
474 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
475 let unparsed_file = r#"ADD ATTRIBUTE name=Text age=Numeric car=[[Text]]
476ADD ATTRIBUTE incidentals_spare_parts=[refs:EJVVlVSZJqVNnuAMLHLkeSQgwfxYLWTKBELi9e8j1PW0]
477"#;
478 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
479
480 let ocafile = generate_from_ast(&oca_ast);
481 assert_eq!(
482 ocafile, unparsed_file,
483 "left:\n{} \n right:\n {}",
484 ocafile, unparsed_file
485 );
486 }
487
488 #[test]
489 fn test_wrong_said() {
490 let _ = env_logger::builder().is_test(true).try_init();
491 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
492 let unparsed_file = r#"ADD ATTRIBUTE said=refs:digest"#;
493 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry);
494 match oca_ast.unwrap_err() {
495 ParseError::InstructionError(InstructionError::ExtractError(
496 ExtractingAttributeError::SaidError(e),
497 )) => {
498 assert_eq!(e.to_string(), "Unknown code")
499 }
500 _ => unreachable!(),
501 }
502 }
503
504 #[test]
505 fn test_oca_file_format() {
506 let _ = env_logger::builder().is_test(true).try_init();
507 let text_type = NestedAttrType::Value(AttributeType::Text);
508 assert_eq!(oca_file_format(text_type), "Text");
509
510 let numeric_type = NestedAttrType::Value(AttributeType::Numeric);
511 assert_eq!(oca_file_format(numeric_type), "Numeric");
512
513 let ref_type = NestedAttrType::Reference(RefValue::Said(
514 HashFunction::from(HashFunctionCode::Blake3_256).derive("example".as_bytes()),
515 ));
516
517 let attr = NestedAttrType::Array(Box::new(NestedAttrType::Array(Box::new(ref_type))));
518
519 let out = oca_file_format(attr);
520 assert_eq!(out, "[[refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu]]");
521 }
522
523 #[test]
524 fn test_line_breaking_in_ocafile() {
525 let _ = env_logger::builder().is_test(true).try_init();
526 let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
527
528 let unparsed_file = r#"
530ADD ATTRIBUTE dateOfBirth=DateTime \
531 documentNumber=Text
532"#;
533 let oca_ast = parse_from_string(unparsed_file.to_string(), ®istry).unwrap();
534
535 assert_eq!(oca_ast.commands.len(), 1);
537
538 if let ast::ObjectKind::CaptureBase(content) = &oca_ast.commands[0].object_kind {
540 let attributes = content.attributes.as_ref().unwrap();
541 assert_eq!(attributes.len(), 2);
542 assert!(attributes.contains_key("dateOfBirth"));
543 assert!(attributes.contains_key("documentNumber"));
544 } else {
545 panic!("Expected CaptureBase");
546 }
547
548 let ocafile = generate_from_ast(&oca_ast);
550 let oca_ast2 = parse_from_string(ocafile.clone(), ®istry).unwrap();
551
552 if let ast::ObjectKind::CaptureBase(content) = &oca_ast2.commands[0].object_kind {
553 let attributes = content.attributes.as_ref().unwrap();
554 assert_eq!(attributes.len(), 2);
555 assert!(attributes.contains_key("dateOfBirth"));
556 assert!(attributes.contains_key("documentNumber"));
557 } else {
558 panic!("Expected CaptureBase");
559 }
560 }
561}