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#[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
44pub 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 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 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 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(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 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 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(), ®istry).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(), ®istry).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(), ®istry).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 }
609}