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
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(), &registry).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(), &registry).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(), &registry).unwrap();
432
433        let ocafile = generate_from_ast(&oca_ast);
434        let oca_ast2 = parse_from_string(ocafile.clone(), &registry).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(), &registry).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(), &registry).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(), &registry).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(), &registry);
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        // Test multi-line attribute definitions
529        let unparsed_file = r#"
530ADD ATTRIBUTE dateOfBirth=DateTime \
531    documentNumber=Text
532"#;
533        let oca_ast = parse_from_string(unparsed_file.to_string(), &registry).unwrap();
534
535        // Verify that attributes are parsed correctly despite line breaks
536        assert_eq!(oca_ast.commands.len(), 1);
537
538        // Verify first command has all attributes
539        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        // Test that regenerated ocafile maintains structure
549        let ocafile = generate_from_ast(&oca_ast);
550        let oca_ast2 = parse_from_string(ocafile.clone(), &registry).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}