Skip to main content

oca_bundle/
build.rs

1use crate::state::oca_bundle::OCABundleModel;
2use crate::state::oca_bundle::overlay::OverlayModel;
3use log::info;
4use oca_ast::ast;
5use oca_ast::ast::NestedValue;
6use overlay_file::OverlayDef;
7
8/// OCABuild represents a build process of an OCA bundle from OCA AST.
9/// It contains the final OCA bundle and a list of steps that were applied to create it.
10#[derive(Debug)]
11pub struct OCABuild {
12    pub oca_bundle: OCABundleModel,
13    pub steps: Vec<OCABuildStep>,
14}
15
16#[derive(Debug)]
17pub struct OCABuildStep {
18    pub parent_said: Option<said::SelfAddressingIdentifier>,
19    pub command: ast::Command,
20    pub result: OCABundleModel,
21}
22
23#[derive(Debug, Clone, serde::Serialize)]
24pub struct FromASTError {
25    pub line_number: usize,
26    pub raw_line: String,
27    pub message: String,
28}
29
30#[derive(thiserror::Error, Debug, Clone, serde::Serialize)]
31#[serde(untagged)]
32pub enum Error {
33    #[error("Error at line {line_number} ({raw_line}): {message}")]
34    FromASTError {
35        #[serde(rename = "ln")]
36        line_number: usize,
37        #[serde(rename = "c")]
38        raw_line: String,
39        #[serde(rename = "e")]
40        message: String,
41    },
42}
43
44/// Create a new OCA build from OCA AST
45pub fn from_ast(
46    from_bundle: Option<OCABundleModel>,
47    oca_ast: &ast::OCAAst,
48) -> Result<OCABuild, Vec<Error>> {
49    let mut errors = vec![];
50    let mut steps = vec![];
51    let mut parent_said: Option<said::SelfAddressingIdentifier> = match &from_bundle {
52        Some(oca_bundle) => oca_bundle.digest.clone(),
53        None => None,
54    };
55    let has_from_bundle = from_bundle.is_some();
56
57    let mut oca_bundle = from_bundle.unwrap_or_default();
58
59    let default_command_meta = ast::CommandMeta {
60        line_number: 0,
61        raw_line: "unknown".to_string(),
62    };
63    for (i, command) in oca_ast.commands.iter().enumerate() {
64        let command_index = if has_from_bundle { i + 1 } else { i };
65
66        // todo pass the references
67        let command_meta = oca_ast
68            .commands_meta
69            .get(&command_index)
70            .unwrap_or(&default_command_meta);
71
72        match apply_command(&mut oca_bundle, command.clone()) {
73            Ok(oca_bundle) => {
74                /* if oca_bundle.said == parent_said {
75                    errors.push(Error::FromASTError {
76                        line_number: command_meta.line_number,
77                        raw_line: command_meta.raw_line.clone(),
78                        message: "Applying command failed".to_string(),
79                    });
80                } else { */
81                steps.push(OCABuildStep {
82                    parent_said: parent_said.clone(),
83                    command: command.clone(),
84                    result: oca_bundle.clone(),
85                });
86                parent_said.clone_from(&oca_bundle.digest);
87            }
88            Err(mut err) => {
89                errors.extend(err.iter_mut().map(|e| Error::FromASTError {
90                    line_number: command_meta.line_number,
91                    raw_line: command_meta.raw_line.clone(),
92                    message: e.clone(),
93                }));
94            }
95        }
96    }
97    if errors.is_empty() {
98        Ok(OCABuild { oca_bundle, steps })
99    } else {
100        Err(errors)
101    }
102}
103
104pub fn apply_command(
105    base: &mut OCABundleModel,
106    op: ast::Command,
107) -> Result<&OCABundleModel, Vec<String>> {
108    let mut errors = vec![];
109
110    match (op.kind, op.object_kind) {
111        (ast::CommandType::From, _) => {
112            errors.push(
113                "Unsupported FROM command, it should be resolved before applying commands"
114                    .to_string(),
115            );
116        }
117        (ast::CommandType::Add, ast::ObjectKind::CaptureBase(content)) => {
118            if let Some(ref attributes) = content.attributes {
119                base.capture_base.attributes.extend(attributes.clone());
120            }
121        }
122        (ast::CommandType::Add, ast::ObjectKind::Overlay(content)) => {
123            let mut overlay = OverlayModel::new(content.clone());
124            overlay.overlay_def = Some(content.overlay_def);
125            if let Err(err) = merge_or_add_overlay(base, overlay) {
126                errors.push(err);
127            }
128        }
129        (ast::CommandType::Add, ast::ObjectKind::OCABundle(_)) => todo!(),
130        (ast::CommandType::Remove, ast::ObjectKind::CaptureBase(content)) => {
131            if let Some(ref attributes) = content.attributes {
132                for (attr_name, _) in attributes {
133                    base.remove_attribute(attr_name);
134                }
135            }
136        }
137        (ast::CommandType::Remove, ast::ObjectKind::OCABundle(_)) => todo!(),
138        (ast::CommandType::Remove, ast::ObjectKind::Overlay(_)) => todo!(),
139        (ast::CommandType::Modify, ast::ObjectKind::CaptureBase(_)) => todo!(),
140        (ast::CommandType::Modify, ast::ObjectKind::OCABundle(_)) => todo!(),
141        (ast::CommandType::Modify, ast::ObjectKind::Overlay(_)) => todo!(),
142    }
143    // Calculate and fill digest for bundle, capture base and overlays
144    match base.compute_and_fill_digest() {
145        Ok(_) => info!("Digests filled successfully"),
146        Err(e) => return Err(vec![format!("Error filling digests: {}", e)]),
147    }
148    base.fill_attributes();
149    if errors.is_empty() {
150        Ok(base)
151    } else {
152        Err(errors)
153    }
154}
155
156fn merge_or_add_overlay(base: &mut OCABundleModel, incoming: OverlayModel) -> Result<(), String> {
157    let overlay_def = incoming
158        .overlay_def
159        .as_ref()
160        .ok_or_else(|| "Overlay definition missing".to_string())?;
161    if overlay_def.unique_keys.is_empty() {
162        base.overlays.push(incoming);
163        return Ok(());
164    }
165
166    let incoming_properties = incoming.properties.as_ref().ok_or_else(|| {
167        format!(
168            "Overlay {} is missing properties for unique keys",
169            overlay_def.get_full_name()
170        )
171    })?;
172
173    let signature = unique_keys_signature(overlay_def, incoming_properties)?;
174    let incoming_name = overlay_def.get_full_name();
175
176    for existing in &mut base.overlays {
177        let existing_def = match &existing.overlay_def {
178            Some(def) => def,
179            None => continue,
180        };
181        if existing_def.get_full_name() != incoming_name {
182            continue;
183        }
184        let existing_props = match existing.properties.as_mut() {
185            Some(props) => props,
186            None => {
187                return Err(format!(
188                    "Overlay {} is missing properties for unique keys",
189                    existing_def.get_full_name()
190                ));
191            }
192        };
193        let existing_signature = unique_keys_signature(existing_def, existing_props)?;
194        if existing_signature != signature {
195            continue;
196        }
197
198        // Merge properties for same overlay + unique signature.
199        merge_properties(existing_props, incoming_properties)?;
200        return Ok(());
201    }
202
203    base.overlays.push(incoming);
204    Ok(())
205}
206
207fn unique_keys_signature(
208    overlay_def: &OverlayDef,
209    properties: &indexmap::IndexMap<String, NestedValue>,
210) -> Result<String, String> {
211    let mut parts = Vec::new();
212    let mut missing = Vec::new();
213    for key in &overlay_def.unique_keys {
214        match properties.get(key) {
215            Some(value) => {
216                let value_str = serde_json::to_string(value).unwrap_or_else(|_| value.to_string());
217                parts.push(format!("{}={}", key, value_str));
218            }
219            None => missing.push(key.clone()),
220        }
221    }
222    if !missing.is_empty() {
223        return Err(format!(
224            "Overlay {} is missing unique keys: {}",
225            overlay_def.get_full_name(),
226            missing.join(", ")
227        ));
228    }
229    Ok(parts.join("|"))
230}
231
232fn merge_properties(
233    target: &mut indexmap::IndexMap<String, NestedValue>,
234    incoming: &indexmap::IndexMap<String, NestedValue>,
235) -> Result<(), String> {
236    for (key, value) in incoming {
237        match target.get_mut(key) {
238            Some(existing) => merge_nested_value(existing, value, key)?,
239            None => {
240                target.insert(key.clone(), value.clone());
241            }
242        }
243    }
244    Ok(())
245}
246
247fn merge_nested_value(
248    target: &mut NestedValue,
249    incoming: &NestedValue,
250    path: &str,
251) -> Result<(), String> {
252    match target {
253        NestedValue::Object(target_obj) => match incoming {
254            NestedValue::Object(incoming_obj) => {
255                for (key, value) in incoming_obj.iter() {
256                    if let Some(existing) = target_obj.get_mut(key) {
257                        let nested_path = format!("{path}.{key}");
258                        merge_nested_value(existing, value, &nested_path)?;
259                    } else {
260                        target_obj.insert(key.clone(), value.clone());
261                    }
262                }
263                Ok(())
264            }
265            _ => {
266                if target == incoming {
267                    Ok(())
268                } else {
269                    Err(format!(
270                        "Overlay attribute override for {path}: existing value differs"
271                    ))
272                }
273            }
274        },
275        _ => {
276            if target == incoming {
277                Ok(())
278            } else {
279                Err(format!(
280                    "Overlay attribute override for {path}: existing value differs"
281                ))
282            }
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use std::io::Error;
290
291    use crate::state::oca_bundle::OCABundle;
292
293    use super::*;
294    use indexmap::IndexMap;
295    use oca_ast::ast::{AttributeType, CaptureContent};
296    use oca_file::ocafile::parse_from_string;
297    use overlay_file::overlay_registry::{OverlayLocalRegistry, OverlayRegistry};
298
299    #[test]
300    fn test_add_step() -> Result<(), Box<dyn std::error::Error>> {
301        let _ = env_logger::builder().is_test(true).try_init();
302        let mut commands = vec![];
303
304        let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays/")?;
305
306        let mut attributes = IndexMap::new();
307        attributes.insert(
308            "d".to_string(),
309            ast::NestedAttrType::Value(AttributeType::Text),
310        );
311        attributes.insert(
312            "i".to_string(),
313            ast::NestedAttrType::Value(AttributeType::Text),
314        );
315        attributes.insert(
316            "passed".to_string(),
317            ast::NestedAttrType::Value(AttributeType::Boolean),
318        );
319        commands.push(ast::Command {
320            kind: ast::CommandType::Add,
321            object_kind: ast::ObjectKind::CaptureBase(CaptureContent {
322                attributes: Some(attributes),
323            }),
324        });
325
326        let mut properties = IndexMap::new();
327        properties.insert(
328            "language".to_string(),
329            ast::NestedValue::Value("en".to_string()),
330        );
331        properties.insert(
332            "name".to_string(),
333            ast::NestedValue::Value("Entrance credential".to_string()),
334        );
335        properties.insert(
336            "description".to_string(),
337            ast::NestedValue::Value("Entrance credential".to_string()),
338        );
339        let meta_ov_def = registry.get_overlay("Meta/2.0.0").unwrap();
340        commands.push(ast::Command {
341            kind: ast::CommandType::Add,
342            object_kind: ast::ObjectKind::Overlay(ast::OverlayContent {
343                properties: Some(properties),
344                overlay_def: meta_ov_def.clone(),
345            }),
346        });
347
348        let mut properties = IndexMap::new();
349        properties.insert(
350            "d".to_string(),
351            ast::NestedValue::Value("Schema digest".to_string()),
352        );
353        properties.insert(
354            "i".to_string(),
355            ast::NestedValue::Value("Credential Issuee".to_string()),
356        );
357        properties.insert(
358            "passed".to_string(),
359            ast::NestedValue::Value("Passed".to_string()),
360        );
361        let mut attr_labels = IndexMap::new();
362        attr_labels.insert(
363            "language".to_string(),
364            ast::NestedValue::Value("en".to_string()),
365        );
366        attr_labels.insert(
367            "attribute_labels".to_string(),
368            ast::NestedValue::Object(properties.clone()),
369        );
370        let label_ov_def = registry.get_overlay("Label/2.0.0").unwrap();
371        commands.push(ast::Command {
372            kind: ast::CommandType::Add,
373            object_kind: ast::ObjectKind::Overlay(ast::OverlayContent {
374                properties: Some(attr_labels),
375                overlay_def: label_ov_def.clone(),
376            }),
377        });
378
379        let mut attributes = IndexMap::new();
380        attributes.insert(
381            "d".to_string(),
382            ast::NestedValue::Value("Schema digest".to_string()),
383        );
384        attributes.insert(
385            "i".to_string(),
386            ast::NestedValue::Value("Credential Issuee".to_string()),
387        );
388        attributes.insert(
389            "passed".to_string(),
390            ast::NestedValue::Value("Enables or disables passing".to_string()),
391        );
392        let mut properties = IndexMap::new();
393        properties.insert(
394            "language".to_string(),
395            ast::NestedValue::Value("en".to_string()),
396        );
397
398        let mut properties = IndexMap::new();
399        properties.insert(
400            "d".to_string(),
401            ast::NestedValue::Value("utf-8".to_string()),
402        );
403        properties.insert(
404            "i".to_string(),
405            ast::NestedValue::Value("utf-8".to_string()),
406        );
407        properties.insert(
408            "passed".to_string(),
409            ast::NestedValue::Value("utf-8".to_string()),
410        );
411        let character_encoding_ov_def = registry.get_overlay("Character_Encoding/2.0.0").unwrap();
412        commands.push(ast::Command {
413            kind: ast::CommandType::Add,
414            object_kind: ast::ObjectKind::Overlay(ast::OverlayContent {
415                properties: Some(properties),
416                overlay_def: character_encoding_ov_def.clone(),
417            }),
418        });
419
420        let mut properties = IndexMap::new();
421        properties.insert("d".to_string(), ast::NestedValue::Value("M".to_string()));
422        properties.insert("i".to_string(), ast::NestedValue::Value("M".to_string()));
423        properties.insert(
424            "passed".to_string(),
425            ast::NestedValue::Value("M".to_string()),
426        );
427        let conformance_ov_def = registry.get_overlay("Conformance/2.0.0").unwrap();
428        commands.push(ast::Command {
429            kind: ast::CommandType::Add,
430            object_kind: ast::ObjectKind::Overlay(ast::OverlayContent {
431                properties: Some(properties),
432                overlay_def: conformance_ov_def.clone(),
433            }),
434        });
435
436        // todo test if references with name are working
437        let mut base = OCABundleModel::default();
438
439        for command in commands {
440            match apply_command(&mut base, command.clone()) {
441                Ok(oca) => {
442                    println!("Bundle: {}", serde_json::to_string_pretty(oca)?);
443                }
444                Err(errors) => {
445                    println!("Error applying command: {:?}", errors);
446                    return Err(Box::new(Error::other(format!("{:?}", errors))));
447                }
448            }
449        }
450        assert_eq!(
451            base.attributes.unwrap().len(),
452            3,
453            "Expected 5 attributes in the base after applying commands"
454        );
455        // TODO check if overlays are created correctly
456        // let mut oca = apply_command(base, command);
457        // base = Some(oca);
458        Ok(())
459    }
460
461    #[test]
462    fn merge_label_overlays_conflict_should_error_and_keep_single_overlay() {
463        let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays/").unwrap();
464        let ocafile = r#"
465ADD ATTRIBUTE first_name=Text last_name=Text
466
467ADD OVERLAY LABEL
468  language="en"
469  attribute_labels
470    first_name="First Name"
471    last_name="Last Name"
472
473ADD OVERLAY LABEL
474  language="en"
475  attribute_labels
476    first_name="Name"
477    last_name="Name"
478"#;
479
480        let oca_ast = parse_from_string(ocafile.to_string(), &registry).unwrap();
481        let mut base = OCABundleModel::default();
482        let mut errors = Vec::new();
483        for command in oca_ast.commands {
484            if let Err(mut err) = apply_command(&mut base, command) {
485                errors.append(&mut err);
486            }
487        }
488
489        assert!(!errors.is_empty());
490        assert_eq!(base.overlays.len(), 1);
491    }
492
493    #[test]
494    fn merge_label_overlays_disjoint_should_merge() {
495        let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays/").unwrap();
496        let ocafile = r#"
497ADD ATTRIBUTE first_name=Text last_name=Text description=Text info=Text
498
499ADD OVERLAY LABEL
500  language="en"
501  attribute_labels
502    first_name="First Name"
503    last_name="Last Name"
504
505ADD OVERLAY LABEL
506  language="en"
507  attribute_labels
508    description="Name"
509    info="Name"
510"#;
511
512        let oca_ast = parse_from_string(ocafile.to_string(), &registry).unwrap();
513        let mut base = OCABundleModel::default();
514        let mut errors = Vec::new();
515        for command in oca_ast.commands {
516            if let Err(mut err) = apply_command(&mut base, command) {
517                errors.append(&mut err);
518            }
519        }
520
521        assert!(errors.is_empty());
522        assert_eq!(base.overlays.len(), 1);
523
524        let properties = base.overlays[0].properties.as_ref().unwrap();
525        let labels = properties.get("attribute_labels").unwrap();
526        match labels {
527            NestedValue::Object(map) => {
528                assert!(map.contains_key("first_name"));
529                assert!(map.contains_key("last_name"));
530                assert!(map.contains_key("description"));
531                assert!(map.contains_key("info"));
532            }
533            _ => panic!("attribute_labels should be an object"),
534        }
535    }
536
537    #[test]
538    fn build_from_ast() {
539        let registry = OverlayLocalRegistry::from_dir("../overlay-file/core_overlays").unwrap();
540
541        let unparsed_file = r#"
542-- version=2.0.0
543-- name=プラスウルトラ
544ADD ATTRIBUTE remove=Text
545ADD ATTRIBUTE name=Text age=Numeric car=[refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu]
546REMOVE ATTRIBUTE remove
547ADD ATTRIBUTE incidentals_spare_parts=[[refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu]]
548ADD ATTRIBUTE d=Text i=Text passed=Boolean
549ADD Overlay META
550  language="en"
551  description="Entrance credential"
552  name="Entrance credential"
553ADD Overlay CHARACTER_ENCODING
554  attribute_character_encodings
555    d="utf-8"
556    i="utf-8"
557    passed="utf-8"
558ADD Overlay CONFORMANCE
559  attribute_conformances
560    d="M"
561    i="M"
562    passed="M"
563ADD Overlay LABEL
564  language="en"
565  attribute_labels
566    d="Schema digest"
567    i="Credential Issuee"
568    passed="Passed"
569ADD Overlay FORMAT
570  attribute_formats
571    d="image/jpeg"
572ADD Overlay UNIT
573  metric_system="SI"
574  attribute_units
575    i="m^2"
576    d="°"
577ADD ATTRIBUTE list=[Text] el=Text
578ADD Overlay CARDINALITY
579  attribute_cardinalities
580    list="1-2"
581ADD Overlay ENTRY_CODE
582  attribute_entry_codes
583    list=refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu
584    el=["o1", "o2", "o3"]
585ADD Overlay ENTRY
586  language="en"
587  attribute_entries
588    list=refs:EJeWVGxkqxWrdGi0efOzwg1YQK8FrA-ZmtegiVEtAVcu
589    el
590     o1="o1_label"
591     o2="o2_label"
592     o3="o3_label"
593"#;
594        let oca_ast = parse_from_string(unparsed_file.to_string(), &registry).unwrap();
595
596        let oca_bundle = OCABundleModel::default();
597
598        let mut oca_bundle_model = from_ast(Some(oca_bundle), &oca_ast).unwrap().oca_bundle;
599        let _ = oca_bundle_model.compute_and_fill_digest();
600        assert_eq!(oca_bundle_model.overlays.len(), 9);
601        assert_eq!(oca_bundle_model.capture_base.attributes.len(), 9);
602        assert!(oca_bundle_model.digest.is_some());
603        let oca_bundle = OCABundle::from(oca_bundle_model);
604        let overlay_name = oca_bundle.overlays.first().unwrap().model.name.clone();
605        assert_eq!(overlay_name, "meta");
606        // let json = serde_json::to_string_pretty(&oca_bundle).unwrap();
607        // println!(" >>> Bundle: {}", json);
608    }
609}