Skip to main content

oca_file/ocafile/
mod.rs

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
140// Format reference to oca file syntax
141fn 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
148// Convert NestedAttrType to oca file syntax
149fn 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        // TODO how to convert nested arrays?
156        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
192/// Generate OCA file from AST
193///
194/// # Arguments
195/// * `ast` - AST
196///
197pub 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                                // TODO avoid clone
214                                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(), &registry).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(), &registry).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(), &registry).unwrap();
429
430        let ocafile = generate_from_ast(&oca_ast);
431        let oca_ast2 = parse_from_string(ocafile.clone(), &registry).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(), &registry).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(), &registry).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(), &registry).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(), &registry);
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        // Test multi-line attribute definitions
526        let unparsed_file = r#"
527ADD ATTRIBUTE dateOfBirth=DateTime \
528    documentNumber=Text
529"#;
530        let oca_ast = parse_from_string(unparsed_file.to_string(), &registry).unwrap();
531
532        // Verify that attributes are parsed correctly despite line breaks
533        assert_eq!(oca_ast.commands.len(), 1);
534
535        // Verify first command has all attributes
536        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        // Test that regenerated ocafile maintains structure
546        let ocafile = generate_from_ast(&oca_ast);
547        let oca_ast2 = parse_from_string(ocafile.clone(), &registry).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(), &registry).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, &registry).unwrap();
629            let ocafile = generate_from_ast(&oca_ast);
630            let oca_ast2 = parse_from_string(ocafile, &registry).unwrap();
631
632            assert_eq!(oca_ast, oca_ast2);
633        }
634    }
635}