1use std::collections::{HashMap, HashSet};
7use std::path::Path;
8
9use mig_assembly::assembler::{
10 AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
11};
12use mig_types::schema::mig::MigSchema;
13use mig_types::segment::OwnedSegment;
14
15use crate::definition::{FieldMapping, MappingDefinition};
16use crate::error::MappingError;
17use crate::segment_structure::SegmentStructure;
18
19pub struct MappingEngine {
22 definitions: Vec<MappingDefinition>,
23 segment_structure: Option<SegmentStructure>,
24 code_lookup: Option<crate::code_lookup::CodeLookup>,
25}
26
27impl MappingEngine {
28 pub fn new_empty() -> Self {
30 Self {
31 definitions: Vec::new(),
32 segment_structure: None,
33 code_lookup: None,
34 }
35 }
36
37 pub fn load(dir: &Path) -> Result<Self, MappingError> {
39 let mut definitions = Vec::new();
40
41 let mut entries: Vec<_> = std::fs::read_dir(dir)?.filter_map(|e| e.ok()).collect();
42 entries.sort_by_key(|e| e.file_name());
43
44 for entry in entries {
45 let path = entry.path();
46 if path.extension().map(|e| e == "toml").unwrap_or(false) {
47 let content = std::fs::read_to_string(&path)?;
48 let def: MappingDefinition =
49 toml::from_str(&content).map_err(|e| MappingError::TomlParse {
50 file: path.display().to_string(),
51 message: e.to_string(),
52 })?;
53 definitions.push(def);
54 }
55 }
56
57 Ok(Self {
58 definitions,
59 segment_structure: None,
60 code_lookup: None,
61 })
62 }
63
64 pub fn load_split(
70 message_dir: &Path,
71 transaction_dir: &Path,
72 ) -> Result<(Self, Self), MappingError> {
73 let msg_engine = Self::load(message_dir)?;
74 let tx_engine = Self::load(transaction_dir)?;
75 Ok((msg_engine, tx_engine))
76 }
77
78 pub fn load_merged(dirs: &[&Path]) -> Result<Self, MappingError> {
83 let mut definitions = Vec::new();
84 for dir in dirs {
85 let engine = Self::load(dir)?;
86 definitions.extend(engine.definitions);
87 }
88 Ok(Self {
89 definitions,
90 segment_structure: None,
91 code_lookup: None,
92 })
93 }
94
95 pub fn load_with_common(
104 common_dir: &Path,
105 pid_dir: &Path,
106 schema_index: &crate::pid_schema_index::PidSchemaIndex,
107 ) -> Result<Self, MappingError> {
108 let mut common_defs = Self::load(common_dir)?.definitions;
109
110 common_defs.retain(|d| {
112 d.meta
113 .source_path
114 .as_deref()
115 .map(|sp| schema_index.has_group(sp))
116 .unwrap_or(true)
117 });
118
119 let pid_defs = Self::load(pid_dir)?.definitions;
120
121 let normalize_sg = |sg: &str| -> String {
126 sg.split('.')
127 .map(|part| part.split(':').next().unwrap_or(part))
128 .collect::<Vec<_>>()
129 .join(".")
130 };
131 let pid_keys: HashSet<(String, Option<String>)> = pid_defs
132 .iter()
133 .flat_map(|d| {
134 let sg = normalize_sg(&d.meta.source_group);
135 let disc = d.meta.discriminator.clone();
136 let mut keys = vec![(sg.clone(), disc.clone())];
137 if let Some(ref disc_str) = disc {
139 if let Some(base) = disc_str.rsplit_once('#') {
140 if base.1.chars().all(|c| c.is_ascii_digit()) {
141 keys.push((sg, Some(base.0.to_string())));
142 }
143 }
144 }
145 keys
146 })
147 .collect();
148
149 common_defs.retain(|d| {
151 let key = (
152 normalize_sg(&d.meta.source_group),
153 d.meta.discriminator.clone(),
154 );
155 !pid_keys.contains(&key)
156 });
157
158 let mut definitions = common_defs;
160 definitions.extend(pid_defs);
161
162 Ok(Self {
163 definitions,
164 segment_structure: None,
165 code_lookup: None,
166 })
167 }
168
169 pub fn load_common_only(
173 common_dir: &Path,
174 schema_index: &crate::pid_schema_index::PidSchemaIndex,
175 ) -> Result<Self, MappingError> {
176 let mut common_defs = Self::load(common_dir)?.definitions;
177
178 common_defs.retain(|d| {
180 d.meta
181 .source_path
182 .as_deref()
183 .map(|sp| schema_index.has_group(sp))
184 .unwrap_or(true)
185 });
186
187 Ok(Self {
188 definitions: common_defs,
189 segment_structure: None,
190 code_lookup: None,
191 })
192 }
193
194 pub fn load_split_with_common(
199 message_dir: &Path,
200 common_dir: &Path,
201 transaction_dir: &Path,
202 schema_index: &crate::pid_schema_index::PidSchemaIndex,
203 ) -> Result<(Self, Self), MappingError> {
204 let msg_engine = Self::load(message_dir)?;
205 let tx_engine = Self::load_with_common(common_dir, transaction_dir, schema_index)?;
206 Ok((msg_engine, tx_engine))
207 }
208
209 pub fn from_definitions(definitions: Vec<MappingDefinition>) -> Self {
211 Self {
212 definitions,
213 segment_structure: None,
214 code_lookup: None,
215 }
216 }
217
218 pub fn save_cached(&self, path: &Path) -> Result<(), MappingError> {
224 let encoded =
225 serde_json::to_vec(&self.definitions).map_err(|e| MappingError::CacheWrite {
226 path: path.display().to_string(),
227 message: e.to_string(),
228 })?;
229 if let Some(parent) = path.parent() {
230 std::fs::create_dir_all(parent)?;
231 }
232 std::fs::write(path, encoded)?;
233 Ok(())
234 }
235
236 pub fn load_cached_or_toml(cache_path: &Path, toml_dir: &Path) -> Result<Self, MappingError> {
241 if cache_path.exists() {
242 Self::load_cached(cache_path)
243 } else {
244 Self::load(toml_dir)
245 }
246 }
247
248 pub fn load_cached(path: &Path) -> Result<Self, MappingError> {
253 let bytes = std::fs::read(path)?;
254 let definitions: Vec<MappingDefinition> =
255 serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
256 path: path.display().to_string(),
257 message: e.to_string(),
258 })?;
259 Ok(Self {
260 definitions,
261 segment_structure: None,
262 code_lookup: None,
263 })
264 }
265
266 pub fn with_segment_structure(mut self, ss: SegmentStructure) -> Self {
271 self.segment_structure = Some(ss);
272 self
273 }
274
275 pub fn with_code_lookup(mut self, cl: crate::code_lookup::CodeLookup) -> Self {
280 self.code_lookup = Some(cl);
281 self
282 }
283
284 pub fn with_path_resolver(mut self, resolver: crate::path_resolver::PathResolver) -> Self {
290 for def in &mut self.definitions {
291 def.normalize_paths(&resolver);
292 }
293 self
294 }
295
296 pub fn definitions(&self) -> &[MappingDefinition] {
298 &self.definitions
299 }
300
301 pub fn definition_for_entity(&self, entity: &str) -> Option<&MappingDefinition> {
303 self.definitions.iter().find(|d| d.meta.entity == entity)
304 }
305
306 pub fn extract_field(
315 &self,
316 tree: &AssembledTree,
317 group_path: &str,
318 path: &str,
319 repetition: usize,
320 ) -> Option<String> {
321 let instance = Self::resolve_group_instance(tree, group_path, repetition)?;
322 Self::extract_from_instance(instance, path)
323 }
324
325 pub fn resolve_group_instance<'a>(
334 tree: &'a AssembledTree,
335 group_path: &str,
336 repetition: usize,
337 ) -> Option<&'a AssembledGroupInstance> {
338 let parts: Vec<&str> = group_path.split('.').collect();
339
340 let (first_id, first_rep) = parse_group_spec(parts[0]);
341 let first_group = tree.groups.iter().find(|g| g.group_id == first_id)?;
342
343 if parts.len() == 1 {
344 let rep = first_rep.unwrap_or(repetition);
346 return first_group.repetitions.get(rep);
347 }
348
349 let mut current_instance = first_group.repetitions.get(first_rep.unwrap_or(0))?;
352
353 for (i, part) in parts[1..].iter().enumerate() {
354 let (group_id, explicit_rep) = parse_group_spec(part);
355 let child_group = current_instance
356 .child_groups
357 .iter()
358 .find(|g| g.group_id == group_id)?;
359
360 if i == parts.len() - 2 {
361 let rep = explicit_rep.unwrap_or(repetition);
363 return child_group.repetitions.get(rep);
364 }
365 current_instance = child_group.repetitions.get(explicit_rep.unwrap_or(0))?;
367 }
368
369 None
370 }
371
372 pub fn resolve_by_source_path<'a>(
380 tree: &'a AssembledTree,
381 source_path: &str,
382 ) -> Option<&'a AssembledGroupInstance> {
383 let parts: Vec<&str> = source_path.split('.').collect();
384 if parts.is_empty() {
385 return None;
386 }
387
388 let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
389 let first_group = tree
390 .groups
391 .iter()
392 .find(|g| g.group_id.eq_ignore_ascii_case(first_id))?;
393
394 let mut current_instance = if let Some(q) = first_qualifier {
395 find_rep_by_entry_qualifier(&first_group.repetitions, q)?
396 } else {
397 first_group.repetitions.first()?
398 };
399
400 if parts.len() == 1 {
401 return Some(current_instance);
402 }
403
404 for part in &parts[1..] {
405 let (group_id, qualifier) = parse_source_path_part(part);
406 let child_group = current_instance
407 .child_groups
408 .iter()
409 .find(|g| g.group_id.eq_ignore_ascii_case(group_id))?;
410
411 current_instance = if let Some(q) = qualifier {
412 find_rep_by_entry_qualifier(&child_group.repetitions, q)?
413 } else {
414 child_group.repetitions.first()?
415 };
416 }
417
418 Some(current_instance)
419 }
420
421 pub fn resolve_all_by_source_path<'a>(
429 tree: &'a AssembledTree,
430 source_path: &str,
431 ) -> Vec<&'a AssembledGroupInstance> {
432 let parts: Vec<&str> = source_path.split('.').collect();
433 if parts.is_empty() {
434 return vec![];
435 }
436
437 let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
439 let first_group = match tree
440 .groups
441 .iter()
442 .find(|g| g.group_id.eq_ignore_ascii_case(first_id))
443 {
444 Some(g) => g,
445 None => return vec![],
446 };
447
448 let mut current_instances: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
449 find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
450 } else {
451 first_group.repetitions.iter().collect()
452 };
453
454 for part in &parts[1..] {
457 let (group_id, qualifier) = parse_source_path_part(part);
458 let mut next_instances = Vec::new();
459
460 for instance in ¤t_instances {
461 if let Some(child_group) = instance
462 .child_groups
463 .iter()
464 .find(|g| g.group_id.eq_ignore_ascii_case(group_id))
465 {
466 if let Some(q) = qualifier {
467 next_instances.extend(find_all_reps_by_entry_qualifier(
468 &child_group.repetitions,
469 q,
470 ));
471 } else {
472 next_instances.extend(child_group.repetitions.iter());
473 }
474 }
475 }
476
477 current_instances = next_instances;
478 }
479
480 current_instances
481 }
482
483 fn compute_child_indices(
496 tree: &AssembledTree,
497 source_path: &str,
498 indexed: &[(usize, &AssembledGroupInstance)],
499 ) -> Vec<usize> {
500 let parts: Vec<&str> = source_path.split('.').collect();
501 if parts.len() < 2 {
502 return vec![];
503 }
504 let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
506 let first_group = match tree
507 .groups
508 .iter()
509 .find(|g| g.group_id.eq_ignore_ascii_case(first_id))
510 {
511 Some(g) => g,
512 None => return vec![],
513 };
514 let parent_reps: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
515 find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
516 } else {
517 first_group.repetitions.iter().collect()
518 };
519 let (child_id, _child_qualifier) = parse_source_path_part(parts[parts.len() - 1]);
521 let mut result = Vec::new();
522 for (_, inst) in indexed {
523 let mut found = false;
525 for parent in &parent_reps {
526 if let Some(child_group) = parent
527 .child_groups
528 .iter()
529 .find(|g| g.group_id.eq_ignore_ascii_case(child_id))
530 {
531 if let Some(pos) = child_group
532 .repetitions
533 .iter()
534 .position(|r| std::ptr::eq(r, *inst))
535 {
536 result.push(pos);
537 found = true;
538 break;
539 }
540 }
541 }
542 if !found {
543 result.push(usize::MAX); }
545 }
546 result
547 }
548
549 pub fn resolve_all_with_parent_indices<'a>(
551 tree: &'a AssembledTree,
552 source_path: &str,
553 ) -> Vec<(usize, &'a AssembledGroupInstance)> {
554 let parts: Vec<&str> = source_path.split('.').collect();
555 if parts.is_empty() {
556 return vec![];
557 }
558
559 let (first_id, first_qualifier) = parse_source_path_part(parts[0]);
561 let first_group = match tree
562 .groups
563 .iter()
564 .find(|g| g.group_id.eq_ignore_ascii_case(first_id))
565 {
566 Some(g) => g,
567 None => return vec![],
568 };
569
570 if parts.len() == 1 {
572 let instances: Vec<&AssembledGroupInstance> = if let Some(q) = first_qualifier {
573 find_all_reps_by_entry_qualifier(&first_group.repetitions, q)
574 } else {
575 first_group.repetitions.iter().collect()
576 };
577 return instances.into_iter().map(|i| (0, i)).collect();
578 }
579
580 let first_reps: Vec<(usize, &AssembledGroupInstance)> = if let Some(q) = first_qualifier {
585 let matching = find_all_reps_by_entry_qualifier(&first_group.repetitions, q);
586 let mut result = Vec::new();
587 for m in matching {
588 let idx = first_group
589 .repetitions
590 .iter()
591 .position(|r| std::ptr::eq(r, m))
592 .unwrap_or(0);
593 result.push((idx, m));
594 }
595 result
596 } else {
597 first_group.repetitions.iter().enumerate().collect()
598 };
599
600 let mut current: Vec<(usize, &AssembledGroupInstance)> = first_reps;
601 let remaining = &parts[1..];
602
603 for (level, part) in remaining.iter().enumerate() {
604 let is_leaf = level == remaining.len() - 1;
605 let (group_id, qualifier) = parse_source_path_part(part);
606 let mut next: Vec<(usize, &AssembledGroupInstance)> = Vec::new();
607
608 for (prev_parent_idx, instance) in ¤t {
609 if let Some(child_group) = instance
610 .child_groups
611 .iter()
612 .find(|g| g.group_id.eq_ignore_ascii_case(group_id))
613 {
614 let matching: Vec<(usize, &AssembledGroupInstance)> = if let Some(q) = qualifier
615 {
616 let filtered =
617 find_all_reps_by_entry_qualifier(&child_group.repetitions, q);
618 filtered
619 .into_iter()
620 .map(|m| {
621 let idx = child_group
622 .repetitions
623 .iter()
624 .position(|r| std::ptr::eq(r, m))
625 .unwrap_or(0);
626 (idx, m)
627 })
628 .collect()
629 } else {
630 child_group.repetitions.iter().enumerate().collect()
631 };
632
633 for (rep_idx, child_rep) in matching {
634 if is_leaf {
635 next.push((*prev_parent_idx, child_rep));
637 } else {
638 next.push((rep_idx, child_rep));
640 }
641 }
642 }
643 }
644
645 current = next;
646 }
647
648 current
649 }
650
651 pub fn extract_from_instance(instance: &AssembledGroupInstance, path: &str) -> Option<String> {
657 let parts: Vec<&str> = path.split('.').collect();
658 if parts.is_empty() {
659 return None;
660 }
661
662 let (segment_tag, qualifier, occurrence) = parse_tag_qualifier(parts[0]);
665
666 let segment = if let Some(q) = qualifier {
667 instance
668 .segments
669 .iter()
670 .filter(|s| {
671 s.tag.eq_ignore_ascii_case(&segment_tag)
672 && s.elements
673 .first()
674 .and_then(|e| e.first())
675 .map(|v| v.as_str())
676 == Some(q)
677 })
678 .nth(occurrence)?
679 } else {
680 instance
681 .segments
682 .iter()
683 .filter(|s| s.tag.eq_ignore_ascii_case(&segment_tag))
684 .nth(occurrence)?
685 };
686
687 Self::resolve_field_path(segment, &parts[1..])
688 }
689
690 pub fn extract_all_from_instance(instance: &AssembledGroupInstance, path: &str) -> Vec<String> {
696 let parts: Vec<&str> = path.split('.').collect();
697 if parts.is_empty() {
698 return vec![];
699 }
700
701 let (segment_tag, qualifier, _) = parse_tag_qualifier(parts[0]);
702
703 let matching_segments: Vec<&AssembledSegment> = if let Some(q) = qualifier {
704 instance
705 .segments
706 .iter()
707 .filter(|s| {
708 s.tag.eq_ignore_ascii_case(&segment_tag)
709 && s.elements
710 .first()
711 .and_then(|e| e.first())
712 .map(|v| v.as_str())
713 == Some(q)
714 })
715 .collect()
716 } else {
717 instance
718 .segments
719 .iter()
720 .filter(|s| s.tag.eq_ignore_ascii_case(&segment_tag))
721 .collect()
722 };
723
724 matching_segments
725 .into_iter()
726 .filter_map(|seg| Self::resolve_field_path(seg, &parts[1..]))
727 .collect()
728 }
729
730 pub fn map_forward(
739 &self,
740 tree: &AssembledTree,
741 def: &MappingDefinition,
742 repetition: usize,
743 ) -> serde_json::Value {
744 self.map_forward_inner(tree, def, repetition, true)
745 }
746
747 fn map_forward_inner(
749 &self,
750 tree: &AssembledTree,
751 def: &MappingDefinition,
752 repetition: usize,
753 enrich_codes: bool,
754 ) -> serde_json::Value {
755 let mut result = serde_json::Map::new();
756
757 if def.meta.source_group.is_empty() {
762 let mut all_root_segs = tree.segments.clone();
763 for segs in tree.inter_group_segments.values() {
764 all_root_segs.extend(segs.iter().cloned());
765 }
766 let root_instance = AssembledGroupInstance {
767 segments: all_root_segs,
768 child_groups: vec![],
769 skipped_segments: Vec::new(),
770 };
771 self.extract_fields_from_instance(&root_instance, def, &mut result, enrich_codes);
772 self.extract_companion_fields(&root_instance, def, &mut result, enrich_codes);
773 return serde_json::Value::Object(result);
774 }
775
776 let instance = if let Some(ref sp) = def.meta.source_path {
782 if has_source_path_qualifiers(sp) && !def.meta.source_group.contains(':') {
783 Self::resolve_by_source_path(tree, sp).or_else(|| {
784 Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
785 })
786 } else {
787 Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
788 }
789 } else {
790 Self::resolve_group_instance(tree, &def.meta.source_group, repetition)
791 };
792
793 if let Some(instance) = instance {
794 if let Some(ref tag) = def.meta.repeat_on_tag {
796 let matching: Vec<_> = instance
797 .segments
798 .iter()
799 .filter(|s| s.tag.eq_ignore_ascii_case(tag))
800 .collect();
801
802 if matching.len() > 1 {
803 let mut arr = Vec::new();
804 for seg in &matching {
805 let sub_instance = AssembledGroupInstance {
806 segments: vec![(*seg).clone()],
807 child_groups: vec![],
808 skipped_segments: Vec::new(),
809 };
810 let mut elem_result = serde_json::Map::new();
811 self.extract_fields_from_instance(
812 &sub_instance,
813 def,
814 &mut elem_result,
815 enrich_codes,
816 );
817 self.extract_companion_fields(
818 &sub_instance,
819 def,
820 &mut elem_result,
821 enrich_codes,
822 );
823 if !elem_result.is_empty() {
824 arr.push(serde_json::Value::Object(elem_result));
825 }
826 }
827 if !arr.is_empty() {
828 return serde_json::Value::Array(arr);
829 }
830 }
831 }
832
833 self.extract_fields_from_instance(instance, def, &mut result, enrich_codes);
834 self.extract_companion_fields(instance, def, &mut result, enrich_codes);
835 }
836
837 serde_json::Value::Object(result)
838 }
839
840 fn extract_companion_fields(
845 &self,
846 instance: &AssembledGroupInstance,
847 def: &MappingDefinition,
848 result: &mut serde_json::Map<String, serde_json::Value>,
849 enrich_codes: bool,
850 ) {
851 if let Some(ref companion_fields) = def.companion_fields {
852 let raw_key = def.meta.companion_type.as_deref().unwrap_or("_companion");
853 let companion_key = to_camel_case(raw_key);
854 let mut companion_result = serde_json::Map::new();
855
856 for (path, field_mapping) in companion_fields {
857 let (target, enum_map, also_target, also_enum_map) = match field_mapping {
858 FieldMapping::Simple(t) => (t.as_str(), None, None, None),
859 FieldMapping::Structured(s) => (
860 s.target.as_str(),
861 s.enum_map.as_ref(),
862 s.also_target.as_deref(),
863 s.also_enum_map.as_ref(),
864 ),
865 FieldMapping::Nested(_) => continue,
866 };
867 if target.is_empty() {
868 continue;
869 }
870
871 if is_collect_all_path(path) {
873 let all = Self::extract_all_from_instance(instance, path);
874 if !all.is_empty() {
875 let arr: Vec<serde_json::Value> = all
876 .into_iter()
877 .map(|v| {
878 let mapped = if let Some(map) = enum_map {
879 map.get(&v).cloned().unwrap_or_else(|| v.clone())
880 } else {
881 v
882 };
883 serde_json::Value::String(mapped)
884 })
885 .collect();
886 set_nested_value_json(
887 &mut companion_result,
888 target,
889 serde_json::Value::Array(arr),
890 );
891 }
892 continue;
893 }
894
895 if let Some(val) = Self::extract_from_instance(instance, path) {
896 let mapped_val = if let Some(map) = enum_map {
897 map.get(&val).cloned().unwrap_or_else(|| val.clone())
898 } else {
899 val.clone()
900 };
901
902 if enrich_codes {
904 if let (Some(ref code_lookup), Some(ref source_path)) =
905 (&self.code_lookup, &def.meta.source_path)
906 {
907 let parts: Vec<&str> = path.split('.').collect();
908 let (seg_tag, _qualifier, _occ) = parse_tag_qualifier(parts[0]);
909 let (element_idx, component_idx) =
910 Self::parse_element_component(&parts[1..]);
911
912 if code_lookup.is_code_field(
913 source_path,
914 &seg_tag,
915 element_idx,
916 component_idx,
917 ) {
918 let enrichment = code_lookup.enrichment_for(
922 source_path,
923 &seg_tag,
924 element_idx,
925 component_idx,
926 &val,
927 );
928 let meaning = enrichment
929 .map(|e| serde_json::Value::String(e.meaning.clone()))
930 .unwrap_or(serde_json::Value::Null);
931
932 let mut obj = serde_json::Map::new();
933 obj.insert("code".into(), serde_json::json!(mapped_val));
934 obj.insert("meaning".into(), meaning);
935 if let Some(enum_key) = enrichment.and_then(|e| e.enum_key.as_ref())
936 {
937 obj.insert("enum".into(), serde_json::json!(enum_key));
938 }
939 let enriched = serde_json::Value::Object(obj);
940 set_nested_value_json(&mut companion_result, target, enriched);
941 continue;
942 }
943 }
944 }
945
946 set_nested_value(&mut companion_result, target, mapped_val);
947
948 if let (Some(at), Some(am)) = (also_target, also_enum_map) {
952 if let Some(also_mapped) = am.get(&val) {
953 set_nested_value(&mut companion_result, at, also_mapped.clone());
954 }
955 }
956 }
957 }
958
959 if !companion_result.is_empty() {
960 result.insert(
961 companion_key.to_string(),
962 serde_json::Value::Object(companion_result),
963 );
964 }
965 }
966 }
967
968 fn extract_fields_from_instance(
973 &self,
974 instance: &AssembledGroupInstance,
975 def: &MappingDefinition,
976 result: &mut serde_json::Map<String, serde_json::Value>,
977 enrich_codes: bool,
978 ) {
979 for (path, field_mapping) in &def.fields {
980 let (target, enum_map) = match field_mapping {
981 FieldMapping::Simple(t) => (t.as_str(), None),
982 FieldMapping::Structured(s) => (s.target.as_str(), s.enum_map.as_ref()),
983 FieldMapping::Nested(_) => continue,
984 };
985 if target.is_empty() {
986 continue;
987 }
988 if let Some(val) = Self::extract_from_instance(instance, path) {
989 let mapped_val = if let Some(map) = enum_map {
990 map.get(&val).cloned().unwrap_or_else(|| val.clone())
991 } else {
992 val.clone()
993 };
994
995 if enrich_codes {
997 if let (Some(ref code_lookup), Some(ref source_path)) =
998 (&self.code_lookup, &def.meta.source_path)
999 {
1000 let parts: Vec<&str> = path.split('.').collect();
1001 let (seg_tag, _qualifier, _occ) = parse_tag_qualifier(parts[0]);
1002 let (element_idx, component_idx) =
1003 Self::parse_element_component(&parts[1..]);
1004
1005 if code_lookup.is_code_field(
1006 source_path,
1007 &seg_tag,
1008 element_idx,
1009 component_idx,
1010 ) {
1011 let enrichment = code_lookup.enrichment_for(
1015 source_path,
1016 &seg_tag,
1017 element_idx,
1018 component_idx,
1019 &val,
1020 );
1021 let meaning = enrichment
1022 .map(|e| serde_json::Value::String(e.meaning.clone()))
1023 .unwrap_or(serde_json::Value::Null);
1024
1025 let mut obj = serde_json::Map::new();
1026 obj.insert("code".into(), serde_json::json!(mapped_val));
1027 obj.insert("meaning".into(), meaning);
1028 if let Some(enum_key) = enrichment.and_then(|e| e.enum_key.as_ref()) {
1029 obj.insert("enum".into(), serde_json::json!(enum_key));
1030 }
1031 let enriched = serde_json::Value::Object(obj);
1032 set_nested_value_json(result, target, enriched);
1033 continue;
1034 }
1035 }
1036 }
1037
1038 set_nested_value(result, target, mapped_val);
1039 }
1040 }
1041 }
1042
1043 pub fn map_forward_from_segments(
1049 &self,
1050 segments: &[OwnedSegment],
1051 def: &MappingDefinition,
1052 ) -> serde_json::Value {
1053 let assembled_segments: Vec<AssembledSegment> = segments
1054 .iter()
1055 .map(|s| AssembledSegment {
1056 tag: s.id.clone(),
1057 elements: s.elements.clone(),
1058 })
1059 .collect();
1060
1061 let instance = AssembledGroupInstance {
1062 segments: assembled_segments,
1063 child_groups: vec![],
1064 skipped_segments: Vec::new(),
1065 };
1066
1067 let mut result = serde_json::Map::new();
1068 self.extract_fields_from_instance(&instance, def, &mut result, true);
1069 serde_json::Value::Object(result)
1070 }
1071
1072 pub fn map_reverse(
1085 &self,
1086 bo4e_value: &serde_json::Value,
1087 def: &MappingDefinition,
1088 ) -> AssembledGroupInstance {
1089 if def.meta.repeat_on_tag.is_some() {
1091 if let Some(arr) = bo4e_value.as_array() {
1092 let mut all_segments = Vec::new();
1093 for elem in arr {
1094 let sub = self.map_reverse_single(elem, def);
1095 all_segments.extend(sub.segments);
1096 }
1097 return AssembledGroupInstance {
1098 segments: all_segments,
1099 child_groups: vec![],
1100 skipped_segments: Vec::new(),
1101 };
1102 }
1103 }
1104 self.map_reverse_single(bo4e_value, def)
1105 }
1106
1107 fn map_reverse_single(
1108 &self,
1109 bo4e_value: &serde_json::Value,
1110 def: &MappingDefinition,
1111 ) -> AssembledGroupInstance {
1112 let mut field_values: Vec<(String, String, usize, usize, String)> =
1115 Vec::with_capacity(def.fields.len());
1116
1117 let mut has_real_data = false;
1124 let mut has_data_fields = false;
1125 let mut seg_has_data_field: HashSet<String> = HashSet::new();
1128 let mut seg_has_real_data: HashSet<String> = HashSet::new();
1129 let mut injected_qualifiers: HashSet<String> = HashSet::new();
1130
1131 for (path, field_mapping) in &def.fields {
1132 let (target, default, enum_map, when_filled) = match field_mapping {
1133 FieldMapping::Simple(t) => (t.as_str(), None, None, None),
1134 FieldMapping::Structured(s) => (
1135 s.target.as_str(),
1136 s.default.as_ref(),
1137 s.enum_map.as_ref(),
1138 s.when_filled.as_ref(),
1139 ),
1140 FieldMapping::Nested(_) => continue,
1141 };
1142
1143 let parts: Vec<&str> = path.split('.').collect();
1144 if parts.len() < 2 {
1145 continue;
1146 }
1147
1148 let (seg_tag, qualifier, _occ) = parse_tag_qualifier(parts[0]);
1149 let seg_key = parts[0].to_uppercase();
1152 let sub_path = &parts[1..];
1153
1154 let (element_idx, component_idx) = if let Ok(ei) = sub_path[0].parse::<usize>() {
1156 let ci = if sub_path.len() > 1 {
1157 sub_path[1].parse::<usize>().unwrap_or(0)
1158 } else {
1159 0
1160 };
1161 (ei, ci)
1162 } else {
1163 match sub_path.len() {
1164 1 => (0, 0),
1165 2 => (1, 0),
1166 _ => continue,
1167 }
1168 };
1169
1170 let val = if target.is_empty() {
1172 match (default, when_filled) {
1173 (Some(d), Some(fields)) => {
1176 let companion_key_for_check =
1177 def.meta.companion_type.as_deref().map(to_camel_case);
1178 let companion_for_check = companion_key_for_check
1179 .as_ref()
1180 .and_then(|k| bo4e_value.get(k))
1181 .unwrap_or(&serde_json::Value::Null);
1182 let any_filled = fields.iter().any(|f| {
1183 self.populate_field(bo4e_value, f).is_some()
1184 || self.populate_field(companion_for_check, f).is_some()
1185 });
1186 if any_filled {
1187 has_real_data = true;
1191 Some(d.clone())
1192 } else {
1193 None
1194 }
1195 }
1196 (Some(d), None) => Some(d.clone()),
1198 (None, _) => None,
1199 }
1200 } else {
1201 has_data_fields = true;
1202 seg_has_data_field.insert(seg_key.clone());
1203 let bo4e_val = self.populate_field(bo4e_value, target);
1204 if bo4e_val.is_some() {
1205 has_real_data = true;
1206 seg_has_real_data.insert(seg_key.clone());
1207 }
1208 let mapped_val = match (bo4e_val, enum_map) {
1210 (Some(v), Some(map)) => {
1211 map.iter()
1213 .find(|(_, bo4e_v)| *bo4e_v == &v)
1214 .map(|(edifact_k, _)| edifact_k.clone())
1215 .or(Some(v))
1216 }
1217 (v, _) => v,
1218 };
1219 mapped_val.or_else(|| default.cloned())
1220 };
1221
1222 if let Some(val) = val {
1223 field_values.push((
1224 seg_key.clone(),
1225 seg_tag.clone(),
1226 element_idx,
1227 component_idx,
1228 val,
1229 ));
1230 }
1231
1232 if let Some(q) = qualifier {
1234 if injected_qualifiers.insert(seg_key.clone()) {
1235 field_values.push((seg_key, seg_tag, 0, 0, q.to_string()));
1236 }
1237 }
1238 }
1239
1240 if let Some(ref companion_fields) = def.companion_fields {
1242 let raw_key = def.meta.companion_type.as_deref().unwrap_or("_companion");
1243 let companion_key = to_camel_case(raw_key);
1244 let companion_value = bo4e_value
1245 .get(&companion_key)
1246 .unwrap_or(&serde_json::Value::Null);
1247
1248 for (path, field_mapping) in companion_fields {
1249 let (target, default, enum_map, when_filled, also_target, also_enum_map) =
1250 match field_mapping {
1251 FieldMapping::Simple(t) => (t.as_str(), None, None, None, None, None),
1252 FieldMapping::Structured(s) => (
1253 s.target.as_str(),
1254 s.default.as_ref(),
1255 s.enum_map.as_ref(),
1256 s.when_filled.as_ref(),
1257 s.also_target.as_deref(),
1258 s.also_enum_map.as_ref(),
1259 ),
1260 FieldMapping::Nested(_) => continue,
1261 };
1262
1263 let parts: Vec<&str> = path.split('.').collect();
1264 if parts.len() < 2 {
1265 continue;
1266 }
1267
1268 let (seg_tag, qualifier, _occ) = parse_tag_qualifier(parts[0]);
1269 let seg_key = parts[0].to_uppercase();
1270 let sub_path = &parts[1..];
1271
1272 let (element_idx, component_idx) = if let Ok(ei) = sub_path[0].parse::<usize>() {
1273 let ci = if sub_path.len() > 1 {
1274 sub_path[1].parse::<usize>().unwrap_or(0)
1275 } else {
1276 0
1277 };
1278 (ei, ci)
1279 } else {
1280 match sub_path.len() {
1281 1 => (0, 0),
1282 2 => (1, 0),
1283 _ => continue,
1284 }
1285 };
1286
1287 if is_collect_all_path(path) && !target.is_empty() {
1289 if let Some(arr) = self
1290 .populate_field_json(companion_value, target)
1291 .and_then(|v| v.as_array().cloned())
1292 {
1293 has_data_fields = true;
1294 if !arr.is_empty() {
1295 has_real_data = true;
1296 }
1297 for (idx, item) in arr.iter().enumerate() {
1298 if let Some(val_str) = item.as_str() {
1299 let mapped = if let Some(map) = enum_map {
1300 map.iter()
1301 .find(|(_, bo4e_v)| *bo4e_v == val_str)
1302 .map(|(edifact_k, _)| edifact_k.clone())
1303 .unwrap_or_else(|| val_str.to_string())
1304 } else {
1305 val_str.to_string()
1306 };
1307 let occ_key = if let Some(q) = qualifier {
1308 format!("{}[{},{}]", seg_tag, q, idx)
1309 } else {
1310 format!("{}[*,{}]", seg_tag, idx)
1311 };
1312 field_values.push((
1313 occ_key.clone(),
1314 seg_tag.clone(),
1315 element_idx,
1316 component_idx,
1317 mapped,
1318 ));
1319 if let Some(q) = qualifier {
1321 if injected_qualifiers.insert(occ_key.clone()) {
1322 field_values.push((
1323 occ_key,
1324 seg_tag.clone(),
1325 0,
1326 0,
1327 q.to_string(),
1328 ));
1329 }
1330 }
1331 }
1332 }
1333 }
1334 continue;
1335 }
1336
1337 let val = if target.is_empty() {
1338 match (default, when_filled) {
1339 (Some(d), Some(fields)) => {
1340 let any_filled = fields.iter().any(|f| {
1341 self.populate_field(bo4e_value, f).is_some()
1342 || self.populate_field(companion_value, f).is_some()
1343 });
1344 if any_filled {
1345 has_real_data = true;
1346 Some(d.clone())
1347 } else {
1348 None
1349 }
1350 }
1351 (Some(d), None) => Some(d.clone()),
1352 (None, _) => None,
1353 }
1354 } else {
1355 has_data_fields = true;
1356 seg_has_data_field.insert(seg_key.clone());
1357 let bo4e_val = self.populate_field(companion_value, target);
1358 if bo4e_val.is_some() {
1359 has_real_data = true;
1360 seg_has_real_data.insert(seg_key.clone());
1361 }
1362 let mapped_val = match (bo4e_val, enum_map) {
1363 (Some(v), Some(map)) => {
1364 if let (Some(at), Some(am)) = (also_target, also_enum_map) {
1365 let also_val = self.populate_field(companion_value, at);
1366 if let Some(av) = also_val.as_deref() {
1367 map.iter()
1369 .find(|(edifact_k, bo4e_v)| {
1370 *bo4e_v == &v
1371 && am.get(*edifact_k).is_some_and(|am_v| am_v == av)
1372 })
1373 .map(|(edifact_k, _)| edifact_k.clone())
1374 .or(Some(v))
1375 } else {
1376 map.iter()
1379 .find(|(edifact_k, bo4e_v)| {
1380 *bo4e_v == &v && !am.contains_key(*edifact_k)
1381 })
1382 .or_else(|| {
1383 map.iter().find(|(_, bo4e_v)| *bo4e_v == &v)
1385 })
1386 .map(|(edifact_k, _)| edifact_k.clone())
1387 .or(Some(v))
1388 }
1389 } else {
1390 map.iter()
1391 .find(|(_, bo4e_v)| *bo4e_v == &v)
1392 .map(|(edifact_k, _)| edifact_k.clone())
1393 .or(Some(v))
1394 }
1395 }
1396 (v, _) => v,
1397 };
1398 mapped_val.or_else(|| default.cloned())
1399 };
1400
1401 if let Some(val) = val {
1402 field_values.push((
1403 seg_key.clone(),
1404 seg_tag.clone(),
1405 element_idx,
1406 component_idx,
1407 val,
1408 ));
1409 }
1410
1411 if let Some(q) = qualifier {
1412 if injected_qualifiers.insert(seg_key.clone()) {
1413 field_values.push((seg_key, seg_tag, 0, 0, q.to_string()));
1414 }
1415 }
1416 }
1417 }
1418
1419 field_values.retain(|(seg_key, _, _, _, _)| {
1427 if !seg_key.contains('[') {
1428 return true; }
1430 !seg_has_data_field.contains(seg_key) || seg_has_real_data.contains(seg_key)
1431 });
1432
1433 if has_data_fields && !has_real_data {
1438 return AssembledGroupInstance {
1439 segments: vec![],
1440 child_groups: vec![],
1441 skipped_segments: Vec::new(),
1442 };
1443 }
1444
1445 let mut segments: Vec<AssembledSegment> = Vec::with_capacity(field_values.len());
1448 let mut seen_keys: HashMap<String, usize> = HashMap::new();
1449
1450 for (seg_key, seg_tag, element_idx, component_idx, val) in &field_values {
1451 let seg = if let Some(&pos) = seen_keys.get(seg_key) {
1452 &mut segments[pos]
1453 } else {
1454 let pos = segments.len();
1455 seen_keys.insert(seg_key.clone(), pos);
1456 segments.push(AssembledSegment {
1457 tag: seg_tag.clone(),
1458 elements: vec![],
1459 });
1460 &mut segments[pos]
1461 };
1462
1463 while seg.elements.len() <= *element_idx {
1464 seg.elements.push(vec![]);
1465 }
1466 while seg.elements[*element_idx].len() <= *component_idx {
1467 seg.elements[*element_idx].push(String::new());
1468 }
1469 seg.elements[*element_idx][*component_idx] = val.clone();
1470 }
1471
1472 for seg in &mut segments {
1475 let last_populated = seg.elements.iter().rposition(|e| !e.is_empty());
1476 if let Some(last_idx) = last_populated {
1477 for i in 0..last_idx {
1478 if seg.elements[i].is_empty() {
1479 seg.elements[i] = vec![String::new()];
1480 }
1481 }
1482 }
1483 }
1484
1485 if let Some(ref ss) = self.segment_structure {
1487 for seg in &mut segments {
1488 if let Some(expected) = ss.element_count(&seg.tag) {
1489 while seg.elements.len() < expected {
1490 seg.elements.push(vec![String::new()]);
1491 }
1492 }
1493 }
1494 }
1495
1496 AssembledGroupInstance {
1497 segments,
1498 child_groups: vec![],
1499 skipped_segments: Vec::new(),
1500 }
1501 }
1502
1503 fn resolve_field_path(segment: &AssembledSegment, path: &[&str]) -> Option<String> {
1516 if path.is_empty() {
1517 return None;
1518 }
1519
1520 if let Ok(element_idx) = path[0].parse::<usize>() {
1522 let component_idx = if path.len() > 1 {
1523 path[1].parse::<usize>().unwrap_or(0)
1524 } else {
1525 0
1526 };
1527 return segment
1528 .elements
1529 .get(element_idx)?
1530 .get(component_idx)
1531 .filter(|v| !v.is_empty())
1532 .cloned();
1533 }
1534
1535 match path.len() {
1537 1 => segment
1538 .elements
1539 .first()?
1540 .first()
1541 .filter(|v| !v.is_empty())
1542 .cloned(),
1543 2 => segment
1544 .elements
1545 .get(1)?
1546 .first()
1547 .filter(|v| !v.is_empty())
1548 .cloned(),
1549 _ => None,
1550 }
1551 }
1552
1553 fn parse_element_component(parts: &[&str]) -> (usize, usize) {
1556 if parts.is_empty() {
1557 return (0, 0);
1558 }
1559 let element_idx = parts[0].parse::<usize>().unwrap_or(0);
1560 let component_idx = if parts.len() > 1 {
1561 parts[1].parse::<usize>().unwrap_or(0)
1562 } else {
1563 0
1564 };
1565 (element_idx, component_idx)
1566 }
1567
1568 pub fn populate_field(
1571 &self,
1572 bo4e_value: &serde_json::Value,
1573 target_field: &str,
1574 ) -> Option<String> {
1575 let mut current = bo4e_value;
1576 for part in target_field.split('.') {
1577 current = current.get(part)?;
1578 }
1579 if let Some(code) = current.get("code").and_then(|v| v.as_str()) {
1581 return Some(code.to_string());
1582 }
1583 current.as_str().map(|s| s.to_string())
1584 }
1585
1586 fn populate_field_json<'a>(
1589 &self,
1590 bo4e_value: &'a serde_json::Value,
1591 target_field: &str,
1592 ) -> Option<&'a serde_json::Value> {
1593 let mut current = bo4e_value;
1594 for part in target_field.split('.') {
1595 current = current.get(part)?;
1596 }
1597 Some(current)
1598 }
1599
1600 pub fn build_segment_from_bo4e(
1602 &self,
1603 bo4e_value: &serde_json::Value,
1604 segment_tag: &str,
1605 target_field: &str,
1606 ) -> AssembledSegment {
1607 let value = self.populate_field(bo4e_value, target_field);
1608 let elements = if let Some(val) = value {
1609 vec![vec![val]]
1610 } else {
1611 vec![]
1612 };
1613 AssembledSegment {
1614 tag: segment_tag.to_uppercase(),
1615 elements,
1616 }
1617 }
1618
1619 pub fn resolve_repetition(
1628 tree: &AssembledTree,
1629 group_path: &str,
1630 discriminator: &str,
1631 ) -> Option<usize> {
1632 let (spec, expected) = discriminator.split_once('=')?;
1633 let parts: Vec<&str> = spec.split('.').collect();
1634 if parts.len() != 3 {
1635 return None;
1636 }
1637 let tag = parts[0];
1638 let element_idx: usize = parts[1].parse().ok()?;
1639 let component_idx: usize = parts[2].parse().ok()?;
1640
1641 let path_parts: Vec<&str> = group_path.split('.').collect();
1643
1644 let leaf_group = if path_parts.len() == 1 {
1645 let (group_id, _) = parse_group_spec(path_parts[0]);
1646 tree.groups.iter().find(|g| g.group_id == group_id)?
1647 } else {
1648 let parent_parts = &path_parts[..path_parts.len() - 1];
1650 let mut current_instance = {
1651 let (first_id, first_rep) = parse_group_spec(parent_parts[0]);
1652 let first_group = tree.groups.iter().find(|g| g.group_id == first_id)?;
1653 first_group.repetitions.get(first_rep.unwrap_or(0))?
1654 };
1655 for part in &parent_parts[1..] {
1656 let (group_id, explicit_rep) = parse_group_spec(part);
1657 let child_group = current_instance
1658 .child_groups
1659 .iter()
1660 .find(|g| g.group_id == group_id)?;
1661 current_instance = child_group.repetitions.get(explicit_rep.unwrap_or(0))?;
1662 }
1663 let (leaf_id, _) = parse_group_spec(path_parts.last()?);
1664 current_instance
1665 .child_groups
1666 .iter()
1667 .find(|g| g.group_id == leaf_id)?
1668 };
1669
1670 let expected_values: Vec<&str> = expected.split('|').collect();
1672 for (rep_idx, instance) in leaf_group.repetitions.iter().enumerate() {
1673 let matches = instance.segments.iter().any(|s| {
1674 s.tag.eq_ignore_ascii_case(tag)
1675 && s.elements
1676 .get(element_idx)
1677 .and_then(|e| e.get(component_idx))
1678 .map(|v| expected_values.iter().any(|ev| v == ev))
1679 .unwrap_or(false)
1680 });
1681 if matches {
1682 return Some(rep_idx);
1683 }
1684 }
1685
1686 None
1687 }
1688
1689 pub fn resolve_all_repetitions(
1694 tree: &AssembledTree,
1695 group_path: &str,
1696 discriminator: &str,
1697 ) -> Vec<usize> {
1698 let Some((spec, expected)) = discriminator.split_once('=') else {
1699 return Vec::new();
1700 };
1701 let parts: Vec<&str> = spec.split('.').collect();
1702 if parts.len() != 3 {
1703 return Vec::new();
1704 }
1705 let tag = parts[0];
1706 let element_idx: usize = match parts[1].parse() {
1707 Ok(v) => v,
1708 Err(_) => return Vec::new(),
1709 };
1710 let component_idx: usize = match parts[2].parse() {
1711 Ok(v) => v,
1712 Err(_) => return Vec::new(),
1713 };
1714
1715 let path_parts: Vec<&str> = group_path.split('.').collect();
1717
1718 let leaf_group = if path_parts.len() == 1 {
1719 let (group_id, _) = parse_group_spec(path_parts[0]);
1720 match tree.groups.iter().find(|g| g.group_id == group_id) {
1721 Some(g) => g,
1722 None => return Vec::new(),
1723 }
1724 } else {
1725 let parent_parts = &path_parts[..path_parts.len() - 1];
1726 let mut current_instance = {
1727 let (first_id, first_rep) = parse_group_spec(parent_parts[0]);
1728 let first_group = match tree.groups.iter().find(|g| g.group_id == first_id) {
1729 Some(g) => g,
1730 None => return Vec::new(),
1731 };
1732 match first_group.repetitions.get(first_rep.unwrap_or(0)) {
1733 Some(i) => i,
1734 None => return Vec::new(),
1735 }
1736 };
1737 for part in &parent_parts[1..] {
1738 let (group_id, explicit_rep) = parse_group_spec(part);
1739 let child_group = match current_instance
1740 .child_groups
1741 .iter()
1742 .find(|g| g.group_id == group_id)
1743 {
1744 Some(g) => g,
1745 None => return Vec::new(),
1746 };
1747 current_instance = match child_group.repetitions.get(explicit_rep.unwrap_or(0)) {
1748 Some(i) => i,
1749 None => return Vec::new(),
1750 };
1751 }
1752 let (leaf_id, _) = match path_parts.last() {
1753 Some(p) => parse_group_spec(p),
1754 None => return Vec::new(),
1755 };
1756 match current_instance
1757 .child_groups
1758 .iter()
1759 .find(|g| g.group_id == leaf_id)
1760 {
1761 Some(g) => g,
1762 None => return Vec::new(),
1763 }
1764 };
1765
1766 let (expected_raw, occurrence) = parse_discriminator_occurrence(expected);
1768
1769 let expected_values: Vec<&str> = expected_raw.split('|').collect();
1771 let mut result = Vec::new();
1772 for (rep_idx, instance) in leaf_group.repetitions.iter().enumerate() {
1773 let matches = instance.segments.iter().any(|s| {
1774 s.tag.eq_ignore_ascii_case(tag)
1775 && s.elements
1776 .get(element_idx)
1777 .and_then(|e| e.get(component_idx))
1778 .map(|v| expected_values.iter().any(|ev| v == ev))
1779 .unwrap_or(false)
1780 });
1781 if matches {
1782 result.push(rep_idx);
1783 }
1784 }
1785
1786 if let Some(occ) = occurrence {
1788 result.into_iter().nth(occ).into_iter().collect()
1789 } else {
1790 result
1791 }
1792 }
1793
1794 pub fn map_all_forward(&self, tree: &AssembledTree) -> serde_json::Value {
1816 self.map_all_forward_inner(tree, true).0
1817 }
1818
1819 pub fn map_all_forward_enriched(
1823 &self,
1824 tree: &AssembledTree,
1825 enrich_codes: bool,
1826 ) -> serde_json::Value {
1827 self.map_all_forward_inner(tree, enrich_codes).0
1828 }
1829
1830 fn map_all_forward_inner(
1837 &self,
1838 tree: &AssembledTree,
1839 enrich_codes: bool,
1840 ) -> (
1841 serde_json::Value,
1842 std::collections::HashMap<String, Vec<usize>>,
1843 ) {
1844 let mut result = serde_json::Map::new();
1845 let mut nesting_info: std::collections::HashMap<String, Vec<usize>> =
1846 std::collections::HashMap::new();
1847
1848 for def in &self.definitions {
1849 let entity = &def.meta.entity;
1850
1851 let bo4e = if let Some(ref disc) = def.meta.discriminator {
1852 let use_source_path = def
1857 .meta
1858 .source_path
1859 .as_ref()
1860 .is_some_and(|sp| has_source_path_qualifiers(sp));
1861 if use_source_path {
1862 let sp = def.meta.source_path.as_deref().unwrap();
1864 let all_instances = Self::resolve_all_by_source_path(tree, sp);
1865 let instances: Vec<_> = if let Some(matcher) = DiscriminatorMatcher::parse(disc)
1867 {
1868 matcher.filter_instances(all_instances)
1869 } else {
1870 all_instances
1871 };
1872 let extract = |instance: &AssembledGroupInstance| {
1873 let mut r = serde_json::Map::new();
1874 self.extract_fields_from_instance(instance, def, &mut r, enrich_codes);
1875 self.extract_companion_fields(instance, def, &mut r, enrich_codes);
1876 serde_json::Value::Object(r)
1877 };
1878 match instances.len() {
1879 0 => None,
1880 1 => Some(extract(instances[0])),
1881 _ => Some(serde_json::Value::Array(
1882 instances.iter().map(|i| extract(i)).collect(),
1883 )),
1884 }
1885 } else {
1886 let reps = Self::resolve_all_repetitions(tree, &def.meta.source_group, disc);
1887 match reps.len() {
1888 0 => None,
1889 1 => Some(self.map_forward_inner(tree, def, reps[0], enrich_codes)),
1890 _ => Some(serde_json::Value::Array(
1891 reps.iter()
1892 .map(|&rep| self.map_forward_inner(tree, def, rep, enrich_codes))
1893 .collect(),
1894 )),
1895 }
1896 }
1897 } else if def.meta.source_group.is_empty() {
1898 Some(self.map_forward_inner(tree, def, 0, enrich_codes))
1900 } else if def.meta.source_path.as_ref().is_some_and(|sp| {
1901 has_source_path_qualifiers(sp) || def.meta.source_group.contains('.')
1902 }) {
1903 let sp = def.meta.source_path.as_deref().unwrap();
1908 let mut indexed = Self::resolve_all_with_parent_indices(tree, sp);
1909
1910 if let Some(last_part) = sp.rsplit('.').next() {
1915 if !last_part.contains('_') {
1916 let base_prefix = if let Some(parent) = sp.rsplit_once('.') {
1920 format!("{}.", parent.0)
1921 } else {
1922 String::new()
1923 };
1924 let sibling_qualifiers: Vec<String> = self
1925 .definitions
1926 .iter()
1927 .filter_map(|d| d.meta.source_path.as_deref())
1928 .filter(|other_sp| {
1929 *other_sp != sp
1930 && other_sp.starts_with(&base_prefix)
1931 && other_sp.split('.').count() == sp.split('.').count()
1932 })
1933 .filter_map(|other_sp| {
1934 let other_last = other_sp.rsplit('.').next()?;
1935 let (base, q) = other_last.split_once('_')?;
1938 if base == last_part {
1939 Some(q.to_string())
1940 } else {
1941 None
1942 }
1943 })
1944 .collect();
1945
1946 if !sibling_qualifiers.is_empty() {
1947 indexed.retain(|(_, inst)| {
1948 let entry_qual = inst
1949 .segments
1950 .first()
1951 .and_then(|seg| seg.elements.first())
1952 .and_then(|el| el.first())
1953 .map(|v| v.to_lowercase());
1954 !entry_qual.is_some_and(|q| {
1957 sibling_qualifiers.iter().any(|sq| {
1958 sq.split('_').any(|part| part.eq_ignore_ascii_case(&q))
1959 })
1960 })
1961 });
1962 }
1963 }
1964 }
1965 let extract = |instance: &AssembledGroupInstance| {
1966 let mut r = serde_json::Map::new();
1967 self.extract_fields_from_instance(instance, def, &mut r, enrich_codes);
1968 self.extract_companion_fields(instance, def, &mut r, enrich_codes);
1969 serde_json::Value::Object(r)
1970 };
1971 if def.meta.source_group.contains('.') && !indexed.is_empty() {
1976 if let Some(sp) = &def.meta.source_path {
1977 let parent_indices: Vec<usize> =
1978 indexed.iter().map(|(idx, _)| *idx).collect();
1979 nesting_info.entry(sp.clone()).or_insert(parent_indices);
1980
1981 let child_key = format!("{sp}#child");
1984 if let std::collections::hash_map::Entry::Vacant(e) =
1985 nesting_info.entry(child_key)
1986 {
1987 let child_indices: Vec<usize> =
1988 Self::compute_child_indices(tree, sp, &indexed);
1989 if !child_indices.is_empty() {
1990 e.insert(child_indices);
1991 }
1992 }
1993 }
1994 }
1995 match indexed.len() {
1996 0 => None,
1997 1 => Some(extract(indexed[0].1)),
1998 _ => Some(serde_json::Value::Array(
1999 indexed.iter().map(|(_, i)| extract(i)).collect(),
2000 )),
2001 }
2002 } else {
2003 let num_reps = Self::count_repetitions(tree, &def.meta.source_group);
2004 if num_reps <= 1 {
2005 Some(self.map_forward_inner(tree, def, 0, enrich_codes))
2006 } else {
2007 let mut items = Vec::with_capacity(num_reps);
2009 for rep in 0..num_reps {
2010 items.push(self.map_forward_inner(tree, def, rep, enrich_codes));
2011 }
2012 Some(serde_json::Value::Array(items))
2013 }
2014 };
2015
2016 if let Some(bo4e) = bo4e {
2017 let bo4e = inject_bo4e_metadata(bo4e, &def.meta.bo4e_type);
2018 let key = to_camel_case(entity);
2019 deep_merge_insert(&mut result, &key, bo4e);
2020 }
2021 }
2022
2023 (serde_json::Value::Object(result), nesting_info)
2024 }
2025
2026 pub fn map_all_reverse(
2035 &self,
2036 entities: &serde_json::Value,
2037 nesting_info: Option<&std::collections::HashMap<String, Vec<usize>>>,
2038 ) -> AssembledTree {
2039 let mut root_segments: Vec<AssembledSegment> = Vec::new();
2040 let mut groups: Vec<AssembledGroup> = Vec::new();
2041
2042 for def in &self.definitions {
2043 let entity_key = to_camel_case(&def.meta.entity);
2044
2045 let entity_value = entities.get(&entity_key);
2047
2048 if entity_value.is_none() {
2049 continue;
2050 }
2051 let entity_value = entity_value.unwrap();
2052
2053 let leaf_group = def
2055 .meta
2056 .source_group
2057 .rsplit('.')
2058 .next()
2059 .unwrap_or(&def.meta.source_group);
2060
2061 if def.meta.source_group.is_empty() {
2062 let instance = self.map_reverse(entity_value, def);
2064 root_segments.extend(instance.segments);
2065 } else if entity_value.is_array() {
2066 let arr = entity_value.as_array().unwrap();
2068 let reps: Vec<_> = arr.iter().map(|item| self.map_reverse(item, def)).collect();
2069
2070 if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
2072 existing.repetitions.extend(reps);
2073 } else {
2074 groups.push(AssembledGroup {
2075 group_id: leaf_group.to_string(),
2076 repetitions: reps,
2077 });
2078 }
2079 } else {
2080 let instance = self.map_reverse(entity_value, def);
2082
2083 if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
2084 existing.repetitions.push(instance);
2085 } else {
2086 groups.push(AssembledGroup {
2087 group_id: leaf_group.to_string(),
2088 repetitions: vec![instance],
2089 });
2090 }
2091 }
2092 }
2093
2094 let nested_specs: Vec<(String, String)> = self
2100 .definitions
2101 .iter()
2102 .filter_map(|def| {
2103 let parts: Vec<&str> = def.meta.source_group.split('.').collect();
2104 if parts.len() > 1 {
2105 Some((parts[0].to_string(), parts[parts.len() - 1].to_string()))
2106 } else {
2107 None
2108 }
2109 })
2110 .collect();
2111 for (parent_id, child_id) in &nested_specs {
2112 let has_parent = groups.iter().any(|g| g.group_id == *parent_id);
2114 let has_child = groups.iter().any(|g| g.group_id == *child_id);
2115 if has_parent && has_child {
2116 let child_idx = groups.iter().position(|g| g.group_id == *child_id).unwrap();
2117 let child_group = groups.remove(child_idx);
2118 let parent = groups
2119 .iter_mut()
2120 .find(|g| g.group_id == *parent_id)
2121 .unwrap();
2122 let child_source_path = self
2126 .definitions
2127 .iter()
2128 .find(|d| {
2129 let parts: Vec<&str> = d.meta.source_group.split('.').collect();
2130 parts.len() > 1 && parts[parts.len() - 1] == *child_id
2131 })
2132 .and_then(|d| d.meta.source_path.as_deref());
2133 let distribution =
2134 child_source_path.and_then(|key| nesting_info.and_then(|ni| ni.get(key)));
2135 for (i, child_rep) in child_group.repetitions.into_iter().enumerate() {
2136 let target_idx = distribution
2137 .and_then(|dist| dist.get(i))
2138 .copied()
2139 .unwrap_or(0);
2140
2141 if let Some(target_rep) = parent.repetitions.get_mut(target_idx) {
2142 if let Some(existing) = target_rep
2143 .child_groups
2144 .iter_mut()
2145 .find(|g| g.group_id == *child_id)
2146 {
2147 existing.repetitions.push(child_rep);
2148 } else {
2149 target_rep.child_groups.push(AssembledGroup {
2150 group_id: child_id.clone(),
2151 repetitions: vec![child_rep],
2152 });
2153 }
2154 }
2155 }
2156 }
2157 }
2158
2159 let post_group_start = root_segments.len();
2160 AssembledTree {
2161 segments: root_segments,
2162 groups,
2163 post_group_start,
2164 inter_group_segments: std::collections::BTreeMap::new(),
2165 }
2166 }
2167
2168 fn count_repetitions(tree: &AssembledTree, group_path: &str) -> usize {
2170 let parts: Vec<&str> = group_path.split('.').collect();
2171
2172 let (first_id, first_rep) = parse_group_spec(parts[0]);
2173 let first_group = match tree.groups.iter().find(|g| g.group_id == first_id) {
2174 Some(g) => g,
2175 None => return 0,
2176 };
2177
2178 if parts.len() == 1 {
2179 return first_group.repetitions.len();
2180 }
2181
2182 let mut current_instance = match first_group.repetitions.get(first_rep.unwrap_or(0)) {
2184 Some(i) => i,
2185 None => return 0,
2186 };
2187
2188 for (i, part) in parts[1..].iter().enumerate() {
2189 let (group_id, explicit_rep) = parse_group_spec(part);
2190 let child_group = match current_instance
2191 .child_groups
2192 .iter()
2193 .find(|g| g.group_id == group_id)
2194 {
2195 Some(g) => g,
2196 None => return 0,
2197 };
2198
2199 if i == parts.len() - 2 {
2200 return child_group.repetitions.len();
2202 }
2203 current_instance = match child_group.repetitions.get(explicit_rep.unwrap_or(0)) {
2204 Some(i) => i,
2205 None => return 0,
2206 };
2207 }
2208
2209 0
2210 }
2211
2212 pub fn map_interchange(
2221 msg_engine: &MappingEngine,
2222 tx_engine: &MappingEngine,
2223 tree: &AssembledTree,
2224 transaction_group: &str,
2225 enrich_codes: bool,
2226 ) -> crate::model::MappedMessage {
2227 let (stammdaten, nesting_info) = msg_engine.map_all_forward_inner(tree, enrich_codes);
2229
2230 let transaktionen = tree
2232 .groups
2233 .iter()
2234 .find(|g| g.group_id == transaction_group)
2235 .map(|sg| {
2236 sg.repetitions
2237 .iter()
2238 .map(|instance| {
2239 let wrapped_tree = AssembledTree {
2242 segments: vec![],
2243 groups: vec![AssembledGroup {
2244 group_id: transaction_group.to_string(),
2245 repetitions: vec![instance.clone()],
2246 }],
2247 post_group_start: 0,
2248 inter_group_segments: std::collections::BTreeMap::new(),
2249 };
2250
2251 let (tx_result, tx_nesting) =
2252 tx_engine.map_all_forward_inner(&wrapped_tree, enrich_codes);
2253
2254 crate::model::MappedTransaktion {
2255 stammdaten: tx_result,
2256 nesting_info: tx_nesting,
2257 }
2258 })
2259 .collect()
2260 })
2261 .unwrap_or_default();
2262
2263 crate::model::MappedMessage {
2264 stammdaten,
2265 transaktionen,
2266 nesting_info,
2267 }
2268 }
2269
2270 pub fn map_interchange_reverse(
2280 msg_engine: &MappingEngine,
2281 tx_engine: &MappingEngine,
2282 mapped: &crate::model::MappedMessage,
2283 transaction_group: &str,
2284 filtered_mig: Option<&MigSchema>,
2285 ) -> AssembledTree {
2286 let msg_tree = msg_engine.map_all_reverse(
2288 &mapped.stammdaten,
2289 if mapped.nesting_info.is_empty() {
2290 None
2291 } else {
2292 Some(&mapped.nesting_info)
2293 },
2294 );
2295
2296 let mut sg4_reps: Vec<AssembledGroupInstance> = Vec::new();
2298
2299 struct DefWithMeta<'a> {
2303 def: &'a MappingDefinition,
2304 relative: String,
2305 depth: usize,
2306 }
2307
2308 let mut sorted_defs: Vec<DefWithMeta> = tx_engine
2309 .definitions
2310 .iter()
2311 .map(|def| {
2312 let relative = strip_tx_group_prefix(&def.meta.source_group, transaction_group);
2313 let depth = if relative.is_empty() {
2314 0
2315 } else {
2316 relative.chars().filter(|c| *c == '.').count() + 1
2317 };
2318 DefWithMeta {
2319 def,
2320 relative,
2321 depth,
2322 }
2323 })
2324 .collect();
2325
2326 let mut parent_rep_map: std::collections::HashMap<String, usize> =
2330 std::collections::HashMap::new();
2331 for dm in &sorted_defs {
2332 if dm.depth >= 2 {
2333 let parts: Vec<&str> = dm.relative.split('.').collect();
2334 let (_, parent_rep) = parse_group_spec(parts[0]);
2335 if let Some(rep_idx) = parent_rep {
2336 if let Some(sp) = &dm.def.meta.source_path {
2337 if let Some((parent_path, _)) = sp.rsplit_once('.') {
2338 parent_rep_map
2339 .entry(parent_path.to_string())
2340 .or_insert(rep_idx);
2341 }
2342 }
2343 }
2344 }
2345 }
2346
2347 for dm in &mut sorted_defs {
2350 if dm.depth == 1 && !dm.relative.contains(':') {
2351 if let Some(sp) = &dm.def.meta.source_path {
2352 if let Some(rep_idx) = parent_rep_map.get(sp.as_str()) {
2353 dm.relative = format!("{}:{}", dm.relative, rep_idx);
2354 }
2355 }
2356 }
2357 }
2358
2359 if let Some(mig) = filtered_mig {
2366 let mig_order = build_reverse_mig_group_order(mig, transaction_group);
2367 sorted_defs.sort_by(|a, b| {
2368 a.depth.cmp(&b.depth).then_with(|| {
2369 let a_id = a.relative.split(':').next().unwrap_or(&a.relative);
2370 let b_id = b.relative.split(':').next().unwrap_or(&b.relative);
2371 let a_pos = variant_mig_position(a.def, a_id, &mig_order);
2373 let b_pos = variant_mig_position(b.def, b_id, &mig_order);
2374 a_pos.cmp(&b_pos).then(a.relative.cmp(&b.relative))
2375 })
2376 });
2377 } else {
2378 sorted_defs.sort_by(|a, b| a.depth.cmp(&b.depth).then(a.relative.cmp(&b.relative)));
2379 }
2380
2381 for tx in &mapped.transaktionen {
2382 let mut root_segs: Vec<AssembledSegment> = Vec::new();
2383 let mut child_groups: Vec<AssembledGroup> = Vec::new();
2384
2385 let mut source_path_to_rep: std::collections::HashMap<String, Vec<usize>> =
2390 std::collections::HashMap::new();
2391
2392 for dm in &sorted_defs {
2393 let entity_key = to_camel_case(&dm.def.meta.entity);
2395 let bo4e_value = match tx.stammdaten.get(&entity_key) {
2396 Some(v) => v,
2397 None => continue,
2398 };
2399
2400 let items: Vec<&serde_json::Value> = if bo4e_value.is_array() {
2404 bo4e_value.as_array().unwrap().iter().collect()
2405 } else {
2406 vec![bo4e_value]
2407 };
2408
2409 for (item_idx, item) in items.iter().enumerate() {
2410 let instance = tx_engine.map_reverse(item, dm.def);
2411
2412 if instance.segments.is_empty() && instance.child_groups.is_empty() {
2414 continue;
2415 }
2416
2417 if dm.relative.is_empty() {
2418 root_segs.extend(instance.segments);
2419 } else {
2420 let effective_relative = if dm.depth >= 2 {
2424 let rel = if items.len() > 1 {
2427 strip_all_rep_indices(&dm.relative)
2428 } else {
2429 dm.relative.clone()
2430 };
2431 let skip_nesting = dm
2438 .def
2439 .meta
2440 .source_path
2441 .as_ref()
2442 .and_then(|sp| sp.rsplit_once('.'))
2443 .and_then(|(parent_path, _)| {
2444 source_path_to_rep.get(parent_path)
2445 })
2446 .is_some_and(|reps| reps.len() == 1);
2447 let nesting_idx = if items.len() > 1 && !skip_nesting {
2448 dm.def
2449 .meta
2450 .source_path
2451 .as_ref()
2452 .and_then(|sp| tx.nesting_info.get(sp))
2453 .and_then(|dist| dist.get(item_idx))
2454 .copied()
2455 } else {
2456 None
2457 };
2458 if let Some(parent_rep) = nesting_idx {
2459 let parts: Vec<&str> = rel.split('.').collect();
2461 let parent_id = parts[0].split(':').next().unwrap_or(parts[0]);
2462 let rest = parts[1..].join(".");
2463 format!("{}:{}.{}", parent_id, parent_rep, rest)
2464 } else {
2465 resolve_child_relative(
2466 &rel,
2467 dm.def.meta.source_path.as_deref(),
2468 &source_path_to_rep,
2469 item_idx,
2470 )
2471 }
2472 } else if dm.depth == 1 {
2473 let child_key = dm
2476 .def
2477 .meta
2478 .source_path
2479 .as_ref()
2480 .map(|sp| format!("{sp}#child"));
2481 if let Some(child_indices) =
2482 child_key.as_ref().and_then(|ck| tx.nesting_info.get(ck))
2483 {
2484 if let Some(&target) = child_indices.get(item_idx) {
2485 if target != usize::MAX {
2486 let base =
2487 dm.relative.split(':').next().unwrap_or(&dm.relative);
2488 format!("{}:{}", base, target)
2489 } else {
2490 dm.relative.clone()
2491 }
2492 } else if items.len() > 1 && item_idx > 0 {
2493 strip_rep_index(&dm.relative)
2494 } else {
2495 dm.relative.clone()
2496 }
2497 } else if items.len() > 1 && item_idx > 0 {
2498 strip_rep_index(&dm.relative)
2499 } else {
2500 dm.relative.clone()
2501 }
2502 } else if items.len() > 1 && item_idx > 0 {
2503 strip_rep_index(&dm.relative)
2506 } else {
2507 dm.relative.clone()
2508 };
2509
2510 let rep_used =
2511 place_in_groups(&mut child_groups, &effective_relative, instance);
2512
2513 if dm.depth == 1 {
2515 if let Some(sp) = &dm.def.meta.source_path {
2516 source_path_to_rep
2517 .entry(sp.clone())
2518 .or_default()
2519 .push(rep_used);
2520 }
2521 }
2522 }
2523 }
2524 }
2525
2526 if let Some(mig) = filtered_mig {
2531 sort_variant_reps_by_mig(&mut child_groups, mig, transaction_group);
2532 }
2533
2534 sg4_reps.push(AssembledGroupInstance {
2535 segments: root_segs,
2536 child_groups,
2537 skipped_segments: Vec::new(),
2538 });
2539 }
2540
2541 let mut root_segments = Vec::new();
2548 let mut uns_segments = Vec::new();
2549 let mut uns_is_summary = false;
2550 let mut found_uns = false;
2551 for seg in msg_tree.segments {
2552 if seg.tag == "UNS" {
2553 uns_is_summary = seg
2555 .elements
2556 .first()
2557 .and_then(|el| el.first())
2558 .map(|v| v == "S")
2559 .unwrap_or(false);
2560 uns_segments.push(seg);
2561 found_uns = true;
2562 } else if found_uns {
2563 uns_segments.push(seg);
2565 } else {
2566 root_segments.push(seg);
2567 }
2568 }
2569
2570 let pre_group_count = root_segments.len();
2571 let mut all_groups = msg_tree.groups;
2572 let mut inter_group = msg_tree.inter_group_segments;
2573
2574 let sg_num = |id: &str| -> usize {
2576 id.strip_prefix("SG")
2577 .and_then(|n| n.parse::<usize>().ok())
2578 .unwrap_or(0)
2579 };
2580
2581 if !sg4_reps.is_empty() {
2582 if uns_is_summary {
2583 all_groups.push(AssembledGroup {
2585 group_id: transaction_group.to_string(),
2586 repetitions: sg4_reps,
2587 });
2588 if !uns_segments.is_empty() {
2589 all_groups.sort_by_key(|g| sg_num(&g.group_id));
2594 let tx_num = sg_num(transaction_group);
2595 let uns_pos = all_groups
2596 .iter()
2597 .rposition(|g| sg_num(&g.group_id) <= tx_num)
2598 .map(|i| i + 1)
2599 .unwrap_or(all_groups.len());
2600 inter_group.insert(uns_pos, uns_segments);
2601 }
2602 } else {
2603 if !uns_segments.is_empty() {
2605 inter_group.insert(all_groups.len(), uns_segments);
2606 }
2607 all_groups.push(AssembledGroup {
2608 group_id: transaction_group.to_string(),
2609 repetitions: sg4_reps,
2610 });
2611 }
2612 } else if !uns_segments.is_empty() {
2613 if transaction_group.is_empty() {
2614 all_groups.sort_by_key(|g| sg_num(&g.group_id));
2619 if uns_is_summary {
2620 inter_group.insert(all_groups.len(), uns_segments);
2621 } else {
2622 inter_group.insert(0, uns_segments);
2623 }
2624 } else {
2625 all_groups.sort_by_key(|g| sg_num(&g.group_id));
2629 let tx_num = sg_num(transaction_group);
2630 let uns_pos = all_groups
2631 .iter()
2632 .rposition(|g| sg_num(&g.group_id) <= tx_num)
2633 .map(|i| i + 1)
2634 .unwrap_or(all_groups.len());
2635 inter_group.insert(uns_pos, uns_segments);
2636 }
2637 }
2638
2639 AssembledTree {
2640 segments: root_segments,
2641 groups: all_groups,
2642 post_group_start: pre_group_count,
2643 inter_group_segments: inter_group,
2644 }
2645 }
2646
2647 pub fn build_group_from_bo4e(
2649 &self,
2650 bo4e_value: &serde_json::Value,
2651 def: &MappingDefinition,
2652 ) -> AssembledGroup {
2653 let instance = self.map_reverse(bo4e_value, def);
2654 let leaf_group = def
2655 .meta
2656 .source_group
2657 .rsplit('.')
2658 .next()
2659 .unwrap_or(&def.meta.source_group);
2660
2661 AssembledGroup {
2662 group_id: leaf_group.to_string(),
2663 repetitions: vec![instance],
2664 }
2665 }
2666
2667 pub fn map_interchange_typed<M, T>(
2675 msg_engine: &MappingEngine,
2676 tx_engine: &MappingEngine,
2677 tree: &AssembledTree,
2678 tx_group: &str,
2679 enrich_codes: bool,
2680 nachrichtendaten: crate::model::Nachrichtendaten,
2681 interchangedaten: crate::model::Interchangedaten,
2682 ) -> Result<crate::model::Interchange<M, T>, serde_json::Error>
2683 where
2684 M: serde::de::DeserializeOwned,
2685 T: serde::de::DeserializeOwned,
2686 {
2687 let mapped = Self::map_interchange(msg_engine, tx_engine, tree, tx_group, enrich_codes);
2688 let nachricht = mapped.into_dynamic_nachricht(nachrichtendaten);
2689 let dynamic = crate::model::DynamicInterchange {
2690 interchangedaten,
2691 nachrichten: vec![nachricht],
2692 };
2693 let value = serde_json::to_value(&dynamic)?;
2694 serde_json::from_value(value)
2695 }
2696
2697 pub fn map_interchange_reverse_typed<M, T>(
2704 msg_engine: &MappingEngine,
2705 tx_engine: &MappingEngine,
2706 nachricht: &crate::model::Nachricht<M, T>,
2707 tx_group: &str,
2708 ) -> Result<AssembledTree, serde_json::Error>
2709 where
2710 M: serde::Serialize,
2711 T: serde::Serialize,
2712 {
2713 let stammdaten = serde_json::to_value(&nachricht.stammdaten)?;
2714 let transaktionen: Vec<crate::model::MappedTransaktion> = nachricht
2715 .transaktionen
2716 .iter()
2717 .map(|t| {
2718 Ok(crate::model::MappedTransaktion {
2719 stammdaten: serde_json::to_value(t)?,
2720 nesting_info: Default::default(),
2721 })
2722 })
2723 .collect::<Result<Vec<_>, serde_json::Error>>()?;
2724 let mapped = crate::model::MappedMessage {
2725 stammdaten,
2726 transaktionen,
2727 nesting_info: Default::default(),
2728 };
2729 Ok(Self::map_interchange_reverse(
2730 msg_engine, tx_engine, &mapped, tx_group, None,
2731 ))
2732 }
2733}
2734
2735fn parse_source_path_part(part: &str) -> (&str, Option<&str>) {
2742 if let Some(pos) = part.find('_') {
2746 let group = &part[..pos];
2747 let qualifier = &part[pos + 1..];
2748 if !qualifier.is_empty() {
2749 return (group, Some(qualifier));
2750 }
2751 }
2752 (part, None)
2753}
2754
2755fn build_reverse_mig_group_order(mig: &MigSchema, tx_group_id: &str) -> HashMap<String, usize> {
2763 let mut order = HashMap::new();
2764 if let Some(tg) = mig.segment_groups.iter().find(|g| g.id == tx_group_id) {
2765 for (i, nested) in tg.nested_groups.iter().enumerate() {
2766 if let Some(ref vc) = nested.variant_code {
2768 let variant_key = format!("{}_{}", nested.id, vc.to_uppercase());
2769 order.insert(variant_key, i);
2770 }
2771 order.entry(nested.id.clone()).or_insert(i);
2773 }
2774 }
2775 order
2776}
2777
2778fn variant_mig_position(
2784 def: &MappingDefinition,
2785 base_group_id: &str,
2786 mig_order: &HashMap<String, usize>,
2787) -> usize {
2788 if let Some(ref sp) = def.meta.source_path {
2791 let base_lower = base_group_id.to_lowercase();
2793 for part in sp.split('.') {
2794 if part.starts_with(&base_lower)
2795 || part.starts_with(base_group_id.to_lowercase().as_str())
2796 {
2797 if let Some(underscore_pos) = part.find('_') {
2799 let qualifier = &part[underscore_pos + 1..];
2800 let variant_key = format!("{}_{}", base_group_id, qualifier.to_uppercase());
2801 if let Some(&pos) = mig_order.get(&variant_key) {
2802 return pos;
2803 }
2804 }
2805 }
2806 }
2807 }
2808 mig_order.get(base_group_id).copied().unwrap_or(usize::MAX)
2810}
2811
2812fn find_rep_by_entry_qualifier<'a>(
2817 reps: &'a [AssembledGroupInstance],
2818 qualifier: &str,
2819) -> Option<&'a AssembledGroupInstance> {
2820 let parts: Vec<&str> = qualifier.split('_').collect();
2822 reps.iter().find(|inst| {
2823 inst.segments.first().is_some_and(|seg| {
2824 seg.elements
2825 .first()
2826 .and_then(|e| e.first())
2827 .is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
2828 })
2829 })
2830}
2831
2832fn find_all_reps_by_entry_qualifier<'a>(
2834 reps: &'a [AssembledGroupInstance],
2835 qualifier: &str,
2836) -> Vec<&'a AssembledGroupInstance> {
2837 let parts: Vec<&str> = qualifier.split('_').collect();
2839 reps.iter()
2840 .filter(|inst| {
2841 inst.segments.first().is_some_and(|seg| {
2842 seg.elements
2843 .first()
2844 .and_then(|e| e.first())
2845 .is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
2846 })
2847 })
2848 .collect()
2849}
2850
2851fn has_source_path_qualifiers(source_path: &str) -> bool {
2853 source_path.split('.').any(|part| {
2854 if let Some(pos) = part.find('_') {
2855 pos < part.len() - 1
2856 } else {
2857 false
2858 }
2859 })
2860}
2861
2862fn parse_group_spec(part: &str) -> (&str, Option<usize>) {
2863 if let Some(colon_pos) = part.find(':') {
2864 let id = &part[..colon_pos];
2865 let rep = part[colon_pos + 1..].parse::<usize>().ok();
2866 (id, rep)
2867 } else {
2868 (part, None)
2869 }
2870}
2871
2872fn strip_tx_group_prefix(source_group: &str, tx_group: &str) -> String {
2878 if source_group == tx_group || source_group.is_empty() {
2879 String::new()
2880 } else if let Some(rest) = source_group.strip_prefix(tx_group) {
2881 rest.strip_prefix('.').unwrap_or(rest).to_string()
2882 } else {
2883 source_group.to_string()
2884 }
2885}
2886
2887fn place_in_groups(
2895 groups: &mut Vec<AssembledGroup>,
2896 relative_path: &str,
2897 instance: AssembledGroupInstance,
2898) -> usize {
2899 let parts: Vec<&str> = relative_path.split('.').collect();
2900
2901 if parts.len() == 1 {
2902 let (id, rep) = parse_group_spec(parts[0]);
2904
2905 let group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == id) {
2907 g
2908 } else {
2909 groups.push(AssembledGroup {
2910 group_id: id.to_string(),
2911 repetitions: vec![],
2912 });
2913 groups.last_mut().unwrap()
2914 };
2915
2916 if let Some(rep_idx) = rep {
2917 while group.repetitions.len() <= rep_idx {
2919 group.repetitions.push(AssembledGroupInstance {
2920 segments: vec![],
2921 child_groups: vec![],
2922 skipped_segments: Vec::new(),
2923 });
2924 }
2925 group.repetitions[rep_idx]
2926 .segments
2927 .extend(instance.segments);
2928 group.repetitions[rep_idx]
2929 .child_groups
2930 .extend(instance.child_groups);
2931 rep_idx
2932 } else {
2933 let pos = group.repetitions.len();
2935 group.repetitions.push(instance);
2936 pos
2937 }
2938 } else {
2939 let (parent_id, parent_rep) = parse_group_spec(parts[0]);
2941 let rep_idx = parent_rep.unwrap_or(0);
2942
2943 let parent_group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == parent_id) {
2945 g
2946 } else {
2947 groups.push(AssembledGroup {
2948 group_id: parent_id.to_string(),
2949 repetitions: vec![],
2950 });
2951 groups.last_mut().unwrap()
2952 };
2953
2954 while parent_group.repetitions.len() <= rep_idx {
2956 parent_group.repetitions.push(AssembledGroupInstance {
2957 segments: vec![],
2958 child_groups: vec![],
2959 skipped_segments: Vec::new(),
2960 });
2961 }
2962
2963 let remaining = parts[1..].join(".");
2964 place_in_groups(
2965 &mut parent_group.repetitions[rep_idx].child_groups,
2966 &remaining,
2967 instance,
2968 );
2969 rep_idx
2970 }
2971}
2972
2973fn resolve_child_relative(
2985 relative: &str,
2986 source_path: Option<&str>,
2987 source_path_to_rep: &std::collections::HashMap<String, Vec<usize>>,
2988 item_idx: usize,
2989) -> String {
2990 let parts: Vec<&str> = relative.split('.').collect();
2991 if parts.is_empty() {
2992 return relative.to_string();
2993 }
2994
2995 let (parent_id, parent_rep) = parse_group_spec(parts[0]);
2997 if parent_rep.is_some() {
2998 return relative.to_string();
2999 }
3000
3001 if let Some(sp) = source_path {
3003 if let Some((parent_path, _child)) = sp.rsplit_once('.') {
3004 if let Some(rep_indices) = source_path_to_rep.get(parent_path) {
3005 let rep_idx = rep_indices
3007 .get(item_idx)
3008 .or_else(|| rep_indices.last())
3009 .copied()
3010 .unwrap_or(0);
3011 let rest = parts[1..].join(".");
3012 return format!("{}:{}.{}", parent_id, rep_idx, rest);
3013 }
3014 }
3015 }
3016
3017 relative.to_string()
3019}
3020
3021struct DiscriminatorMatcher<'a> {
3028 tag: &'a str,
3029 element_idx: usize,
3030 component_idx: usize,
3031 expected_values: Vec<&'a str>,
3032 occurrence: Option<usize>,
3034}
3035
3036impl<'a> DiscriminatorMatcher<'a> {
3037 fn parse(disc: &'a str) -> Option<Self> {
3038 let (spec, expected) = disc.split_once('=')?;
3039 let parts: Vec<&str> = spec.split('.').collect();
3040 if parts.len() != 3 {
3041 return None;
3042 }
3043 let (expected_raw, occurrence) = parse_discriminator_occurrence(expected);
3044 Some(Self {
3045 tag: parts[0],
3046 element_idx: parts[1].parse().ok()?,
3047 component_idx: parts[2].parse().ok()?,
3048 expected_values: expected_raw.split('|').collect(),
3049 occurrence,
3050 })
3051 }
3052
3053 fn matches(&self, instance: &AssembledGroupInstance) -> bool {
3054 instance.segments.iter().any(|s| {
3055 s.tag.eq_ignore_ascii_case(self.tag)
3056 && s.elements
3057 .get(self.element_idx)
3058 .and_then(|e| e.get(self.component_idx))
3059 .map(|v| self.expected_values.iter().any(|ev| v == ev))
3060 .unwrap_or(false)
3061 })
3062 }
3063
3064 fn filter_instances<'b>(
3066 &self,
3067 instances: Vec<&'b AssembledGroupInstance>,
3068 ) -> Vec<&'b AssembledGroupInstance> {
3069 let matching: Vec<_> = instances
3070 .into_iter()
3071 .filter(|inst| self.matches(inst))
3072 .collect();
3073 if let Some(occ) = self.occurrence {
3074 matching.into_iter().nth(occ).into_iter().collect()
3075 } else {
3076 matching
3077 }
3078 }
3079}
3080
3081fn parse_discriminator_occurrence(expected: &str) -> (&str, Option<usize>) {
3087 if let Some(hash_pos) = expected.rfind('#') {
3088 if let Ok(occ) = expected[hash_pos + 1..].parse::<usize>() {
3089 return (&expected[..hash_pos], Some(occ));
3090 }
3091 }
3092 (expected, None)
3093}
3094
3095fn strip_rep_index(relative: &str) -> String {
3099 let (id, _) = parse_group_spec(relative);
3100 id.to_string()
3101}
3102
3103fn strip_all_rep_indices(relative: &str) -> String {
3108 relative
3109 .split('.')
3110 .map(|part| {
3111 let (id, _) = parse_group_spec(part);
3112 id
3113 })
3114 .collect::<Vec<_>>()
3115 .join(".")
3116}
3117
3118fn is_collect_all_path(path: &str) -> bool {
3123 let tag_part = path.split('.').next().unwrap_or("");
3124 if let Some(bracket_start) = tag_part.find('[') {
3125 let inner = tag_part[bracket_start + 1..].trim_end_matches(']');
3126 if let Some(comma_pos) = inner.find(',') {
3127 let qualifier = &inner[..comma_pos];
3128 let occ = &inner[comma_pos + 1..];
3129 qualifier != "*" && occ == "*"
3131 } else {
3132 false
3133 }
3134 } else {
3135 false
3136 }
3137}
3138
3139fn parse_tag_qualifier(tag_part: &str) -> (String, Option<&str>, usize) {
3146 if let Some(bracket_start) = tag_part.find('[') {
3147 let tag = tag_part[..bracket_start].to_uppercase();
3148 let inner = tag_part[bracket_start + 1..].trim_end_matches(']');
3149 if let Some(comma_pos) = inner.find(',') {
3150 let qualifier = &inner[..comma_pos];
3151 let index = inner[comma_pos + 1..].parse::<usize>().unwrap_or(0);
3152 if qualifier == "*" {
3154 (tag, None, index)
3155 } else {
3156 (tag, Some(qualifier), index)
3157 }
3158 } else {
3159 (tag, Some(inner), 0)
3160 }
3161 } else {
3162 (tag_part.to_uppercase(), None, 0)
3163 }
3164}
3165
3166fn inject_bo4e_metadata(mut value: serde_json::Value, bo4e_type: &str) -> serde_json::Value {
3171 match &mut value {
3172 serde_json::Value::Object(map) => {
3173 map.entry("boTyp")
3174 .or_insert_with(|| serde_json::Value::String(bo4e_type.to_uppercase()));
3175 map.entry("versionStruktur")
3176 .or_insert_with(|| serde_json::Value::String("1".to_string()));
3177 }
3178 serde_json::Value::Array(items) => {
3179 for item in items {
3180 if let serde_json::Value::Object(map) = item {
3181 map.entry("boTyp")
3182 .or_insert_with(|| serde_json::Value::String(bo4e_type.to_uppercase()));
3183 map.entry("versionStruktur")
3184 .or_insert_with(|| serde_json::Value::String("1".to_string()));
3185 }
3186 }
3187 }
3188 _ => {}
3189 }
3190 value
3191}
3192
3193fn deep_merge_insert(
3199 result: &mut serde_json::Map<String, serde_json::Value>,
3200 entity: &str,
3201 bo4e: serde_json::Value,
3202) {
3203 if let Some(existing) = result.get_mut(entity) {
3204 if let (Some(existing_arr), Some(new_arr)) =
3207 (existing.as_array().map(|a| a.len()), bo4e.as_array())
3208 {
3209 if existing_arr == new_arr.len() {
3210 let existing_arr = existing.as_array_mut().unwrap();
3211 for (existing_elem, new_elem) in existing_arr.iter_mut().zip(new_arr) {
3212 if let (Some(existing_map), Some(new_map)) =
3213 (existing_elem.as_object_mut(), new_elem.as_object())
3214 {
3215 for (k, v) in new_map {
3216 if let Some(existing_v) = existing_map.get_mut(k) {
3217 if let (Some(existing_inner), Some(new_inner)) =
3218 (existing_v.as_object_mut(), v.as_object())
3219 {
3220 for (ik, iv) in new_inner {
3221 existing_inner
3222 .entry(ik.clone())
3223 .or_insert_with(|| iv.clone());
3224 }
3225 }
3226 } else {
3227 existing_map.insert(k.clone(), v.clone());
3228 }
3229 }
3230 }
3231 }
3232 return;
3233 }
3234 }
3235 if let (Some(existing_map), serde_json::Value::Object(new_map)) =
3237 (existing.as_object_mut(), &bo4e)
3238 {
3239 for (k, v) in new_map {
3240 if let Some(existing_v) = existing_map.get_mut(k) {
3241 if let (Some(existing_inner), Some(new_inner)) =
3243 (existing_v.as_object_mut(), v.as_object())
3244 {
3245 for (ik, iv) in new_inner {
3246 existing_inner
3247 .entry(ik.clone())
3248 .or_insert_with(|| iv.clone());
3249 }
3250 }
3251 } else {
3253 existing_map.insert(k.clone(), v.clone());
3254 }
3255 }
3256 return;
3257 }
3258 }
3259 result.insert(entity.to_string(), bo4e);
3260}
3261
3262fn to_camel_case(name: &str) -> String {
3268 let mut chars = name.chars();
3269 match chars.next() {
3270 Some(c) => c.to_lowercase().to_string() + chars.as_str(),
3271 None => String::new(),
3272 }
3273}
3274
3275fn set_nested_value(map: &mut serde_json::Map<String, serde_json::Value>, path: &str, val: String) {
3278 set_nested_value_json(map, path, serde_json::Value::String(val));
3279}
3280
3281fn set_nested_value_json(
3283 map: &mut serde_json::Map<String, serde_json::Value>,
3284 path: &str,
3285 val: serde_json::Value,
3286) {
3287 if let Some((prefix, leaf)) = path.rsplit_once('.') {
3288 let mut current = map;
3289 for part in prefix.split('.') {
3290 let entry = current
3291 .entry(part.to_string())
3292 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
3293 current = entry.as_object_mut().expect("expected object in path");
3294 }
3295 current.insert(leaf.to_string(), val);
3296 } else {
3297 map.insert(path.to_string(), val);
3298 }
3299}
3300
3301#[derive(serde::Serialize, serde::Deserialize)]
3306pub struct VariantCache {
3307 pub message_defs: Vec<MappingDefinition>,
3309 pub transaction_defs: HashMap<String, Vec<MappingDefinition>>,
3311 pub combined_defs: HashMap<String, Vec<MappingDefinition>>,
3313 #[serde(default)]
3315 pub code_lookups: HashMap<String, crate::code_lookup::CodeLookup>,
3316 #[serde(default)]
3318 pub mig_schema: Option<mig_types::schema::mig::MigSchema>,
3319 #[serde(default)]
3321 pub segment_structure: Option<crate::segment_structure::SegmentStructure>,
3322 #[serde(default)]
3325 pub pid_segment_numbers: HashMap<String, Vec<String>>,
3326 #[serde(default)]
3329 pub pid_requirements: HashMap<String, crate::pid_requirements::PidRequirements>,
3330 #[serde(default)]
3334 pub tx_groups: HashMap<String, String>,
3335}
3336
3337impl VariantCache {
3338 pub fn save(&self, path: &Path) -> Result<(), MappingError> {
3340 let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
3341 path: path.display().to_string(),
3342 message: e.to_string(),
3343 })?;
3344 if let Some(parent) = path.parent() {
3345 std::fs::create_dir_all(parent)?;
3346 }
3347 std::fs::write(path, encoded)?;
3348 Ok(())
3349 }
3350
3351 pub fn load(path: &Path) -> Result<Self, MappingError> {
3353 let bytes = std::fs::read(path)?;
3354 serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
3355 path: path.display().to_string(),
3356 message: e.to_string(),
3357 })
3358 }
3359
3360 pub fn tx_group(&self, pid: &str) -> Option<&str> {
3364 self.tx_groups
3365 .get(&format!("pid_{pid}"))
3366 .map(|s| s.as_str())
3367 }
3368
3369 pub fn msg_engine(&self) -> MappingEngine {
3371 MappingEngine::from_definitions(self.message_defs.clone())
3372 }
3373
3374 pub fn tx_engine(&self, pid: &str) -> Option<MappingEngine> {
3377 self.transaction_defs
3378 .get(&format!("pid_{pid}"))
3379 .map(|defs| MappingEngine::from_definitions(defs.clone()))
3380 }
3381
3382 pub fn filtered_mig(&self, pid: &str) -> Option<mig_types::schema::mig::MigSchema> {
3385 let mig = self.mig_schema.as_ref()?;
3386 let numbers = self.pid_segment_numbers.get(&format!("pid_{pid}"))?;
3387 let number_set: std::collections::HashSet<String> = numbers.iter().cloned().collect();
3388 Some(mig_assembly::pid_filter::filter_mig_for_pid(
3389 mig,
3390 &number_set,
3391 ))
3392 }
3393}
3394
3395#[derive(serde::Serialize, serde::Deserialize)]
3400pub struct DataBundle {
3401 pub format_version: String,
3402 pub bundle_version: u32,
3403 pub variants: HashMap<String, VariantCache>,
3404}
3405
3406impl DataBundle {
3407 pub const CURRENT_VERSION: u32 = 2;
3408
3409 pub fn variant(&self, name: &str) -> Option<&VariantCache> {
3410 self.variants.get(name)
3411 }
3412
3413 pub fn write_to<W: std::io::Write>(&self, writer: &mut W) -> Result<(), MappingError> {
3414 let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
3415 path: "<stream>".to_string(),
3416 message: e.to_string(),
3417 })?;
3418 writer.write_all(&encoded).map_err(MappingError::Io)
3419 }
3420
3421 pub fn read_from<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
3422 let mut bytes = Vec::new();
3423 reader.read_to_end(&mut bytes).map_err(MappingError::Io)?;
3424 serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
3425 path: "<stream>".to_string(),
3426 message: e.to_string(),
3427 })
3428 }
3429
3430 pub fn read_from_checked<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
3431 let bundle = Self::read_from(reader)?;
3432 if bundle.bundle_version != Self::CURRENT_VERSION {
3433 return Err(MappingError::CacheRead {
3434 path: "<stream>".to_string(),
3435 message: format!(
3436 "Incompatible bundle version {}, expected version {}. \
3437 Run `edifact-data update` to fetch compatible bundles.",
3438 bundle.bundle_version,
3439 Self::CURRENT_VERSION
3440 ),
3441 });
3442 }
3443 Ok(bundle)
3444 }
3445
3446 pub fn save(&self, path: &Path) -> Result<(), MappingError> {
3447 if let Some(parent) = path.parent() {
3448 std::fs::create_dir_all(parent)?;
3449 }
3450 let mut file = std::fs::File::create(path).map_err(MappingError::Io)?;
3451 self.write_to(&mut file)
3452 }
3453
3454 pub fn load(path: &Path) -> Result<Self, MappingError> {
3455 let mut file = std::fs::File::open(path).map_err(MappingError::Io)?;
3456 Self::read_from_checked(&mut file)
3457 }
3458}
3459
3460fn sort_variant_reps_by_mig(
3473 child_groups: &mut [AssembledGroup],
3474 mig: &MigSchema,
3475 transaction_group: &str,
3476) {
3477 let tx_def = match mig
3478 .segment_groups
3479 .iter()
3480 .find(|sg| sg.id == transaction_group)
3481 {
3482 Some(d) => d,
3483 None => return,
3484 };
3485
3486 for cg in child_groups.iter_mut() {
3487 if cg.repetitions.len() <= 1 {
3488 continue;
3489 }
3490
3491 let variant_defs: Vec<(usize, &mig_types::schema::mig::MigSegmentGroup)> = tx_def
3493 .nested_groups
3494 .iter()
3495 .enumerate()
3496 .filter(|(_, ng)| ng.id == cg.group_id && ng.variant_code.is_some())
3497 .collect();
3498
3499 if variant_defs.is_empty() {
3500 continue;
3501 }
3502
3503 cg.repetitions.sort_by_key(|rep| {
3506 let entry_seg = rep.segments.first();
3507 for &(mig_pos, variant_def) in &variant_defs {
3508 let (ei, ci) = variant_def.variant_qualifier_position.unwrap_or((0, 0));
3509 let actual_qual = entry_seg
3510 .and_then(|s| s.elements.get(ei))
3511 .and_then(|e| e.get(ci))
3512 .map(|s| s.as_str())
3513 .unwrap_or("");
3514 let matches = if !variant_def.variant_codes.is_empty() {
3515 variant_def
3516 .variant_codes
3517 .iter()
3518 .any(|c| actual_qual.eq_ignore_ascii_case(c))
3519 } else if let Some(ref expected_code) = variant_def.variant_code {
3520 actual_qual.eq_ignore_ascii_case(expected_code)
3521 } else {
3522 false
3523 };
3524 if matches {
3525 return mig_pos;
3526 }
3527 }
3528 usize::MAX });
3530 }
3531}
3532
3533#[cfg(test)]
3534mod variant_cache_helper_tests {
3535 use super::*;
3536
3537 fn make_test_cache() -> VariantCache {
3538 let mut tx_groups = HashMap::new();
3539 tx_groups.insert("pid_55001".to_string(), "SG4".to_string());
3540 tx_groups.insert("pid_21007".to_string(), "SG14".to_string());
3541
3542 let mut transaction_defs = HashMap::new();
3543 transaction_defs.insert("pid_55001".to_string(), vec![]);
3544 transaction_defs.insert("pid_21007".to_string(), vec![]);
3545
3546 VariantCache {
3547 message_defs: vec![],
3548 transaction_defs,
3549 combined_defs: HashMap::new(),
3550 code_lookups: HashMap::new(),
3551 mig_schema: None,
3552 segment_structure: None,
3553 pid_segment_numbers: HashMap::new(),
3554 pid_requirements: HashMap::new(),
3555 tx_groups,
3556 }
3557 }
3558
3559 #[test]
3560 fn test_tx_group_returns_correct_group() {
3561 let vc = make_test_cache();
3562 assert_eq!(vc.tx_group("55001").unwrap(), "SG4");
3563 assert_eq!(vc.tx_group("21007").unwrap(), "SG14");
3564 }
3565
3566 #[test]
3567 fn test_tx_group_unknown_pid_returns_none() {
3568 let vc = make_test_cache();
3569 assert!(vc.tx_group("99999").is_none());
3570 }
3571
3572 #[test]
3573 fn test_msg_engine_returns_engine() {
3574 let vc = make_test_cache();
3575 let engine = vc.msg_engine();
3576 assert_eq!(engine.definitions().len(), 0);
3577 }
3578
3579 #[test]
3580 fn test_tx_engine_returns_engine_for_known_pid() {
3581 let vc = make_test_cache();
3582 assert!(vc.tx_engine("55001").is_some());
3583 }
3584
3585 #[test]
3586 fn test_tx_engine_returns_none_for_unknown_pid() {
3587 let vc = make_test_cache();
3588 assert!(vc.tx_engine("99999").is_none());
3589 }
3590}
3591
3592#[cfg(test)]
3593mod tests {
3594 use super::*;
3595 use crate::definition::{MappingDefinition, MappingMeta, StructuredFieldMapping};
3596 use indexmap::IndexMap;
3597
3598 fn make_def(fields: IndexMap<String, FieldMapping>) -> MappingDefinition {
3599 MappingDefinition {
3600 meta: MappingMeta {
3601 entity: "Test".to_string(),
3602 bo4e_type: "Test".to_string(),
3603 companion_type: None,
3604 source_group: "SG4".to_string(),
3605 source_path: None,
3606 discriminator: None,
3607 repeat_on_tag: None,
3608 },
3609 fields,
3610 companion_fields: None,
3611 complex_handlers: None,
3612 }
3613 }
3614
3615 #[test]
3616 fn test_map_interchange_single_transaction_backward_compat() {
3617 use mig_assembly::assembler::*;
3618
3619 let tree = AssembledTree {
3621 segments: vec![
3622 AssembledSegment {
3623 tag: "UNH".to_string(),
3624 elements: vec![vec!["001".to_string()]],
3625 },
3626 AssembledSegment {
3627 tag: "BGM".to_string(),
3628 elements: vec![vec!["E01".to_string()], vec!["DOC001".to_string()]],
3629 },
3630 ],
3631 groups: vec![
3632 AssembledGroup {
3633 group_id: "SG2".to_string(),
3634 repetitions: vec![AssembledGroupInstance {
3635 segments: vec![AssembledSegment {
3636 tag: "NAD".to_string(),
3637 elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
3638 }],
3639 child_groups: vec![],
3640 skipped_segments: vec![],
3641 }],
3642 },
3643 AssembledGroup {
3644 group_id: "SG4".to_string(),
3645 repetitions: vec![AssembledGroupInstance {
3646 segments: vec![AssembledSegment {
3647 tag: "IDE".to_string(),
3648 elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
3649 }],
3650 child_groups: vec![AssembledGroup {
3651 group_id: "SG5".to_string(),
3652 repetitions: vec![AssembledGroupInstance {
3653 segments: vec![AssembledSegment {
3654 tag: "LOC".to_string(),
3655 elements: vec![
3656 vec!["Z16".to_string()],
3657 vec!["DE000111222333".to_string()],
3658 ],
3659 }],
3660 child_groups: vec![],
3661 skipped_segments: vec![],
3662 }],
3663 }],
3664 skipped_segments: vec![],
3665 }],
3666 },
3667 ],
3668 post_group_start: 2,
3669 inter_group_segments: std::collections::BTreeMap::new(),
3670 };
3671
3672 let msg_engine = MappingEngine::from_definitions(vec![]);
3674
3675 let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
3677 tx_fields.insert(
3678 "ide.1".to_string(),
3679 FieldMapping::Simple("vorgangId".to_string()),
3680 );
3681 let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
3682 malo_fields.insert(
3683 "loc.1".to_string(),
3684 FieldMapping::Simple("marktlokationsId".to_string()),
3685 );
3686
3687 let tx_engine = MappingEngine::from_definitions(vec![
3688 MappingDefinition {
3689 meta: MappingMeta {
3690 entity: "Prozessdaten".to_string(),
3691 bo4e_type: "Prozessdaten".to_string(),
3692 companion_type: None,
3693 source_group: "SG4".to_string(),
3694 source_path: None,
3695 discriminator: None,
3696 repeat_on_tag: None,
3697 },
3698 fields: tx_fields,
3699 companion_fields: None,
3700 complex_handlers: None,
3701 },
3702 MappingDefinition {
3703 meta: MappingMeta {
3704 entity: "Marktlokation".to_string(),
3705 bo4e_type: "Marktlokation".to_string(),
3706 companion_type: None,
3707 source_group: "SG4.SG5".to_string(),
3708 source_path: None,
3709 discriminator: None,
3710 repeat_on_tag: None,
3711 },
3712 fields: malo_fields,
3713 companion_fields: None,
3714 complex_handlers: None,
3715 },
3716 ]);
3717
3718 let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
3719
3720 assert_eq!(result.transaktionen.len(), 1);
3721 assert_eq!(
3722 result.transaktionen[0].stammdaten["prozessdaten"]["vorgangId"]
3723 .as_str()
3724 .unwrap(),
3725 "TX001"
3726 );
3727 assert_eq!(
3728 result.transaktionen[0].stammdaten["marktlokation"]["marktlokationsId"]
3729 .as_str()
3730 .unwrap(),
3731 "DE000111222333"
3732 );
3733 }
3734
3735 #[test]
3736 fn test_map_reverse_pads_intermediate_empty_elements() {
3737 let mut fields = IndexMap::new();
3739 fields.insert(
3740 "nad.0".to_string(),
3741 FieldMapping::Structured(StructuredFieldMapping {
3742 target: String::new(),
3743 transform: None,
3744 when: None,
3745 default: Some("Z09".to_string()),
3746 enum_map: None,
3747 when_filled: None,
3748 also_target: None,
3749 also_enum_map: None,
3750 }),
3751 );
3752 fields.insert(
3753 "nad.3.0".to_string(),
3754 FieldMapping::Simple("name".to_string()),
3755 );
3756 fields.insert(
3757 "nad.3.1".to_string(),
3758 FieldMapping::Simple("vorname".to_string()),
3759 );
3760
3761 let def = make_def(fields);
3762 let engine = MappingEngine::from_definitions(vec![]);
3763
3764 let bo4e = serde_json::json!({
3765 "name": "Muster",
3766 "vorname": "Max"
3767 });
3768
3769 let instance = engine.map_reverse(&bo4e, &def);
3770 assert_eq!(instance.segments.len(), 1);
3771
3772 let nad = &instance.segments[0];
3773 assert_eq!(nad.tag, "NAD");
3774 assert_eq!(nad.elements.len(), 4);
3775 assert_eq!(nad.elements[0], vec!["Z09"]);
3776 assert_eq!(nad.elements[1], vec![""]);
3778 assert_eq!(nad.elements[2], vec![""]);
3779 assert_eq!(nad.elements[3][0], "Muster");
3780 assert_eq!(nad.elements[3][1], "Max");
3781 }
3782
3783 #[test]
3784 fn test_map_reverse_no_padding_when_contiguous() {
3785 let mut fields = IndexMap::new();
3787 fields.insert(
3788 "dtm.0.0".to_string(),
3789 FieldMapping::Structured(StructuredFieldMapping {
3790 target: String::new(),
3791 transform: None,
3792 when: None,
3793 default: Some("92".to_string()),
3794 enum_map: None,
3795 when_filled: None,
3796 also_target: None,
3797 also_enum_map: None,
3798 }),
3799 );
3800 fields.insert(
3801 "dtm.0.1".to_string(),
3802 FieldMapping::Simple("value".to_string()),
3803 );
3804 fields.insert(
3805 "dtm.0.2".to_string(),
3806 FieldMapping::Structured(StructuredFieldMapping {
3807 target: String::new(),
3808 transform: None,
3809 when: None,
3810 default: Some("303".to_string()),
3811 enum_map: None,
3812 when_filled: None,
3813 also_target: None,
3814 also_enum_map: None,
3815 }),
3816 );
3817
3818 let def = make_def(fields);
3819 let engine = MappingEngine::from_definitions(vec![]);
3820
3821 let bo4e = serde_json::json!({ "value": "20250531" });
3822
3823 let instance = engine.map_reverse(&bo4e, &def);
3824 let dtm = &instance.segments[0];
3825 assert_eq!(dtm.elements.len(), 1);
3827 assert_eq!(dtm.elements[0], vec!["92", "20250531", "303"]);
3828 }
3829
3830 #[test]
3831 fn test_map_message_level_extracts_sg2_only() {
3832 use mig_assembly::assembler::*;
3833
3834 let tree = AssembledTree {
3836 segments: vec![
3837 AssembledSegment {
3838 tag: "UNH".to_string(),
3839 elements: vec![vec!["001".to_string()]],
3840 },
3841 AssembledSegment {
3842 tag: "BGM".to_string(),
3843 elements: vec![vec!["E01".to_string()]],
3844 },
3845 ],
3846 groups: vec![
3847 AssembledGroup {
3848 group_id: "SG2".to_string(),
3849 repetitions: vec![AssembledGroupInstance {
3850 segments: vec![AssembledSegment {
3851 tag: "NAD".to_string(),
3852 elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
3853 }],
3854 child_groups: vec![],
3855 skipped_segments: vec![],
3856 }],
3857 },
3858 AssembledGroup {
3859 group_id: "SG4".to_string(),
3860 repetitions: vec![AssembledGroupInstance {
3861 segments: vec![AssembledSegment {
3862 tag: "IDE".to_string(),
3863 elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
3864 }],
3865 child_groups: vec![],
3866 skipped_segments: vec![],
3867 }],
3868 },
3869 ],
3870 post_group_start: 2,
3871 inter_group_segments: std::collections::BTreeMap::new(),
3872 };
3873
3874 let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
3876 msg_fields.insert(
3877 "nad.0".to_string(),
3878 FieldMapping::Simple("marktrolle".to_string()),
3879 );
3880 msg_fields.insert(
3881 "nad.1".to_string(),
3882 FieldMapping::Simple("rollencodenummer".to_string()),
3883 );
3884 let msg_def = MappingDefinition {
3885 meta: MappingMeta {
3886 entity: "Marktteilnehmer".to_string(),
3887 bo4e_type: "Marktteilnehmer".to_string(),
3888 companion_type: None,
3889 source_group: "SG2".to_string(),
3890 source_path: None,
3891 discriminator: None,
3892 repeat_on_tag: None,
3893 },
3894 fields: msg_fields,
3895 companion_fields: None,
3896 complex_handlers: None,
3897 };
3898
3899 let engine = MappingEngine::from_definitions(vec![msg_def.clone()]);
3900 let result = engine.map_all_forward(&tree);
3901
3902 assert!(result.get("marktteilnehmer").is_some());
3904 let mt = &result["marktteilnehmer"];
3905 assert_eq!(mt["marktrolle"].as_str().unwrap(), "MS");
3906 assert_eq!(mt["rollencodenummer"].as_str().unwrap(), "9900123");
3907 }
3908
3909 #[test]
3910 fn test_map_transaction_scoped_to_sg4_instance() {
3911 use mig_assembly::assembler::*;
3912
3913 let tree = AssembledTree {
3915 segments: vec![
3916 AssembledSegment {
3917 tag: "UNH".to_string(),
3918 elements: vec![vec!["001".to_string()]],
3919 },
3920 AssembledSegment {
3921 tag: "BGM".to_string(),
3922 elements: vec![vec!["E01".to_string()]],
3923 },
3924 ],
3925 groups: vec![AssembledGroup {
3926 group_id: "SG4".to_string(),
3927 repetitions: vec![AssembledGroupInstance {
3928 segments: vec![AssembledSegment {
3929 tag: "IDE".to_string(),
3930 elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
3931 }],
3932 child_groups: vec![AssembledGroup {
3933 group_id: "SG5".to_string(),
3934 repetitions: vec![AssembledGroupInstance {
3935 segments: vec![AssembledSegment {
3936 tag: "LOC".to_string(),
3937 elements: vec![
3938 vec!["Z16".to_string()],
3939 vec!["DE000111222333".to_string()],
3940 ],
3941 }],
3942 child_groups: vec![],
3943 skipped_segments: vec![],
3944 }],
3945 }],
3946 skipped_segments: vec![],
3947 }],
3948 }],
3949 post_group_start: 2,
3950 inter_group_segments: std::collections::BTreeMap::new(),
3951 };
3952
3953 let mut proz_fields: IndexMap<String, FieldMapping> = IndexMap::new();
3955 proz_fields.insert(
3956 "ide.1".to_string(),
3957 FieldMapping::Simple("vorgangId".to_string()),
3958 );
3959 let proz_def = MappingDefinition {
3960 meta: MappingMeta {
3961 entity: "Prozessdaten".to_string(),
3962 bo4e_type: "Prozessdaten".to_string(),
3963 companion_type: None,
3964 source_group: "".to_string(), source_path: None,
3966 discriminator: None,
3967 repeat_on_tag: None,
3968 },
3969 fields: proz_fields,
3970 companion_fields: None,
3971 complex_handlers: None,
3972 };
3973
3974 let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
3975 malo_fields.insert(
3976 "loc.1".to_string(),
3977 FieldMapping::Simple("marktlokationsId".to_string()),
3978 );
3979 let malo_def = MappingDefinition {
3980 meta: MappingMeta {
3981 entity: "Marktlokation".to_string(),
3982 bo4e_type: "Marktlokation".to_string(),
3983 companion_type: None,
3984 source_group: "SG5".to_string(), source_path: None,
3986 discriminator: None,
3987 repeat_on_tag: None,
3988 },
3989 fields: malo_fields,
3990 companion_fields: None,
3991 complex_handlers: None,
3992 };
3993
3994 let tx_engine = MappingEngine::from_definitions(vec![proz_def, malo_def]);
3995
3996 let sg4 = &tree.groups[0]; let sg4_instance = &sg4.repetitions[0];
3999 let sub_tree = sg4_instance.as_assembled_tree();
4000
4001 let result = tx_engine.map_all_forward(&sub_tree);
4002
4003 assert_eq!(
4005 result["prozessdaten"]["vorgangId"].as_str().unwrap(),
4006 "TX001"
4007 );
4008
4009 assert_eq!(
4011 result["marktlokation"]["marktlokationsId"]
4012 .as_str()
4013 .unwrap(),
4014 "DE000111222333"
4015 );
4016 }
4017
4018 #[test]
4019 fn test_map_interchange_produces_full_hierarchy() {
4020 use mig_assembly::assembler::*;
4021
4022 let tree = AssembledTree {
4024 segments: vec![
4025 AssembledSegment {
4026 tag: "UNH".to_string(),
4027 elements: vec![vec!["001".to_string()]],
4028 },
4029 AssembledSegment {
4030 tag: "BGM".to_string(),
4031 elements: vec![vec!["E01".to_string()]],
4032 },
4033 ],
4034 groups: vec![
4035 AssembledGroup {
4036 group_id: "SG2".to_string(),
4037 repetitions: vec![AssembledGroupInstance {
4038 segments: vec![AssembledSegment {
4039 tag: "NAD".to_string(),
4040 elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
4041 }],
4042 child_groups: vec![],
4043 skipped_segments: vec![],
4044 }],
4045 },
4046 AssembledGroup {
4047 group_id: "SG4".to_string(),
4048 repetitions: vec![
4049 AssembledGroupInstance {
4050 segments: vec![AssembledSegment {
4051 tag: "IDE".to_string(),
4052 elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
4053 }],
4054 child_groups: vec![],
4055 skipped_segments: vec![],
4056 },
4057 AssembledGroupInstance {
4058 segments: vec![AssembledSegment {
4059 tag: "IDE".to_string(),
4060 elements: vec![vec!["24".to_string()], vec!["TX002".to_string()]],
4061 }],
4062 child_groups: vec![],
4063 skipped_segments: vec![],
4064 },
4065 ],
4066 },
4067 ],
4068 post_group_start: 2,
4069 inter_group_segments: std::collections::BTreeMap::new(),
4070 };
4071
4072 let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4074 msg_fields.insert(
4075 "nad.0".to_string(),
4076 FieldMapping::Simple("marktrolle".to_string()),
4077 );
4078 let msg_defs = vec![MappingDefinition {
4079 meta: MappingMeta {
4080 entity: "Marktteilnehmer".to_string(),
4081 bo4e_type: "Marktteilnehmer".to_string(),
4082 companion_type: None,
4083 source_group: "SG2".to_string(),
4084 source_path: None,
4085 discriminator: None,
4086 repeat_on_tag: None,
4087 },
4088 fields: msg_fields,
4089 companion_fields: None,
4090 complex_handlers: None,
4091 }];
4092
4093 let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4095 tx_fields.insert(
4096 "ide.1".to_string(),
4097 FieldMapping::Simple("vorgangId".to_string()),
4098 );
4099 let tx_defs = vec![MappingDefinition {
4100 meta: MappingMeta {
4101 entity: "Prozessdaten".to_string(),
4102 bo4e_type: "Prozessdaten".to_string(),
4103 companion_type: None,
4104 source_group: "SG4".to_string(),
4105 source_path: None,
4106 discriminator: None,
4107 repeat_on_tag: None,
4108 },
4109 fields: tx_fields,
4110 companion_fields: None,
4111 complex_handlers: None,
4112 }];
4113
4114 let msg_engine = MappingEngine::from_definitions(msg_defs);
4115 let tx_engine = MappingEngine::from_definitions(tx_defs);
4116
4117 let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
4118
4119 assert!(result.stammdaten["marktteilnehmer"].is_object());
4121 assert_eq!(
4122 result.stammdaten["marktteilnehmer"]["marktrolle"]
4123 .as_str()
4124 .unwrap(),
4125 "MS"
4126 );
4127
4128 assert_eq!(result.transaktionen.len(), 2);
4130 assert_eq!(
4131 result.transaktionen[0].stammdaten["prozessdaten"]["vorgangId"]
4132 .as_str()
4133 .unwrap(),
4134 "TX001"
4135 );
4136 assert_eq!(
4137 result.transaktionen[1].stammdaten["prozessdaten"]["vorgangId"]
4138 .as_str()
4139 .unwrap(),
4140 "TX002"
4141 );
4142 }
4143
4144 #[test]
4145 fn test_map_reverse_with_segment_structure_pads_trailing() {
4146 let mut fields = IndexMap::new();
4148 fields.insert(
4149 "sts.0".to_string(),
4150 FieldMapping::Structured(StructuredFieldMapping {
4151 target: String::new(),
4152 transform: None,
4153 when: None,
4154 default: Some("7".to_string()),
4155 enum_map: None,
4156 when_filled: None,
4157 also_target: None,
4158 also_enum_map: None,
4159 }),
4160 );
4161 fields.insert(
4162 "sts.2".to_string(),
4163 FieldMapping::Simple("grund".to_string()),
4164 );
4165
4166 let def = make_def(fields);
4167
4168 let mut counts = std::collections::HashMap::new();
4170 counts.insert("STS".to_string(), 5usize);
4171 let ss = SegmentStructure {
4172 element_counts: counts,
4173 };
4174
4175 let engine = MappingEngine::from_definitions(vec![]).with_segment_structure(ss);
4176
4177 let bo4e = serde_json::json!({ "grund": "E01" });
4178
4179 let instance = engine.map_reverse(&bo4e, &def);
4180 let sts = &instance.segments[0];
4181 assert_eq!(sts.elements.len(), 5);
4184 assert_eq!(sts.elements[0], vec!["7"]);
4185 assert_eq!(sts.elements[1], vec![""]);
4186 assert_eq!(sts.elements[2], vec!["E01"]);
4187 assert_eq!(sts.elements[3], vec![""]);
4188 assert_eq!(sts.elements[4], vec![""]);
4189 }
4190
4191 #[test]
4192 fn test_extract_companion_fields_with_code_enrichment() {
4193 use crate::code_lookup::CodeLookup;
4194 use mig_assembly::assembler::*;
4195
4196 let schema = serde_json::json!({
4197 "fields": {
4198 "sg4": {
4199 "children": {
4200 "sg8_z01": {
4201 "children": {
4202 "sg10": {
4203 "segments": [{
4204 "id": "CCI",
4205 "elements": [{
4206 "index": 2,
4207 "components": [{
4208 "sub_index": 0,
4209 "type": "code",
4210 "codes": [
4211 {"value": "Z15", "name": "Haushaltskunde"},
4212 {"value": "Z18", "name": "Kein Haushaltskunde"}
4213 ]
4214 }]
4215 }]
4216 }],
4217 "source_group": "SG10"
4218 }
4219 },
4220 "segments": [],
4221 "source_group": "SG8"
4222 }
4223 },
4224 "segments": [],
4225 "source_group": "SG4"
4226 }
4227 }
4228 });
4229
4230 let code_lookup = CodeLookup::from_schema_value(&schema);
4231
4232 let tree = AssembledTree {
4233 segments: vec![],
4234 groups: vec![AssembledGroup {
4235 group_id: "SG4".to_string(),
4236 repetitions: vec![AssembledGroupInstance {
4237 segments: vec![],
4238 child_groups: vec![AssembledGroup {
4239 group_id: "SG8".to_string(),
4240 repetitions: vec![AssembledGroupInstance {
4241 segments: vec![],
4242 child_groups: vec![AssembledGroup {
4243 group_id: "SG10".to_string(),
4244 repetitions: vec![AssembledGroupInstance {
4245 segments: vec![AssembledSegment {
4246 tag: "CCI".to_string(),
4247 elements: vec![vec![], vec![], vec!["Z15".to_string()]],
4248 }],
4249 child_groups: vec![],
4250 skipped_segments: vec![],
4251 }],
4252 }],
4253 skipped_segments: vec![],
4254 }],
4255 }],
4256 skipped_segments: vec![],
4257 }],
4258 }],
4259 post_group_start: 0,
4260 inter_group_segments: std::collections::BTreeMap::new(),
4261 };
4262
4263 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4264 companion_fields.insert(
4265 "cci.2".to_string(),
4266 FieldMapping::Simple("haushaltskunde".to_string()),
4267 );
4268
4269 let def = MappingDefinition {
4270 meta: MappingMeta {
4271 entity: "Marktlokation".to_string(),
4272 bo4e_type: "Marktlokation".to_string(),
4273 companion_type: Some("MarktlokationEdifact".to_string()),
4274 source_group: "SG4.SG8.SG10".to_string(),
4275 source_path: Some("sg4.sg8_z01.sg10".to_string()),
4276 discriminator: None,
4277 repeat_on_tag: None,
4278 },
4279 fields: IndexMap::new(),
4280 companion_fields: Some(companion_fields),
4281 complex_handlers: None,
4282 };
4283
4284 let engine_plain = MappingEngine::from_definitions(vec![]);
4286 let bo4e_plain = engine_plain.map_forward(&tree, &def, 0);
4287 assert_eq!(
4288 bo4e_plain["marktlokationEdifact"]["haushaltskunde"].as_str(),
4289 Some("Z15"),
4290 "Without code lookup, should be plain string"
4291 );
4292
4293 let engine_enriched = MappingEngine::from_definitions(vec![]).with_code_lookup(code_lookup);
4295 let bo4e_enriched = engine_enriched.map_forward(&tree, &def, 0);
4296 let hk = &bo4e_enriched["marktlokationEdifact"]["haushaltskunde"];
4297 assert_eq!(hk["code"].as_str(), Some("Z15"));
4298 assert_eq!(hk["meaning"].as_str(), Some("Haushaltskunde"));
4299 assert!(hk.get("enum").is_none());
4301 }
4302
4303 #[test]
4304 fn test_extract_companion_fields_with_enum_enrichment() {
4305 use crate::code_lookup::CodeLookup;
4306 use mig_assembly::assembler::*;
4307
4308 let schema = serde_json::json!({
4310 "fields": {
4311 "sg4": {
4312 "children": {
4313 "sg8_z01": {
4314 "children": {
4315 "sg10": {
4316 "segments": [{
4317 "id": "CCI",
4318 "elements": [{
4319 "index": 2,
4320 "components": [{
4321 "sub_index": 0,
4322 "type": "code",
4323 "codes": [
4324 {"value": "Z15", "name": "Haushaltskunde", "enum": "HAUSHALTSKUNDE"},
4325 {"value": "Z18", "name": "Kein Haushaltskunde", "enum": "KEIN_HAUSHALTSKUNDE"}
4326 ]
4327 }]
4328 }]
4329 }],
4330 "source_group": "SG10"
4331 }
4332 },
4333 "segments": [],
4334 "source_group": "SG8"
4335 }
4336 },
4337 "segments": [],
4338 "source_group": "SG4"
4339 }
4340 }
4341 });
4342
4343 let code_lookup = CodeLookup::from_schema_value(&schema);
4344
4345 let tree = AssembledTree {
4346 segments: vec![],
4347 groups: vec![AssembledGroup {
4348 group_id: "SG4".to_string(),
4349 repetitions: vec![AssembledGroupInstance {
4350 segments: vec![],
4351 child_groups: vec![AssembledGroup {
4352 group_id: "SG8".to_string(),
4353 repetitions: vec![AssembledGroupInstance {
4354 segments: vec![],
4355 child_groups: vec![AssembledGroup {
4356 group_id: "SG10".to_string(),
4357 repetitions: vec![AssembledGroupInstance {
4358 segments: vec![AssembledSegment {
4359 tag: "CCI".to_string(),
4360 elements: vec![vec![], vec![], vec!["Z15".to_string()]],
4361 }],
4362 child_groups: vec![],
4363 skipped_segments: vec![],
4364 }],
4365 }],
4366 skipped_segments: vec![],
4367 }],
4368 }],
4369 skipped_segments: vec![],
4370 }],
4371 }],
4372 post_group_start: 0,
4373 inter_group_segments: std::collections::BTreeMap::new(),
4374 };
4375
4376 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4377 companion_fields.insert(
4378 "cci.2".to_string(),
4379 FieldMapping::Simple("haushaltskunde".to_string()),
4380 );
4381
4382 let def = MappingDefinition {
4383 meta: MappingMeta {
4384 entity: "Marktlokation".to_string(),
4385 bo4e_type: "Marktlokation".to_string(),
4386 companion_type: Some("MarktlokationEdifact".to_string()),
4387 source_group: "SG4.SG8.SG10".to_string(),
4388 source_path: Some("sg4.sg8_z01.sg10".to_string()),
4389 discriminator: None,
4390 repeat_on_tag: None,
4391 },
4392 fields: IndexMap::new(),
4393 companion_fields: Some(companion_fields),
4394 complex_handlers: None,
4395 };
4396
4397 let engine = MappingEngine::from_definitions(vec![]).with_code_lookup(code_lookup);
4398 let bo4e = engine.map_forward(&tree, &def, 0);
4399 let hk = &bo4e["marktlokationEdifact"]["haushaltskunde"];
4400 assert_eq!(hk["code"].as_str(), Some("Z15"));
4401 assert_eq!(hk["meaning"].as_str(), Some("Haushaltskunde"));
4402 assert_eq!(
4403 hk["enum"].as_str(),
4404 Some("HAUSHALTSKUNDE"),
4405 "enum field should be present"
4406 );
4407 }
4408
4409 #[test]
4410 fn test_reverse_mapping_accepts_enriched_with_enum() {
4411 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4413 companion_fields.insert(
4414 "cci.2".to_string(),
4415 FieldMapping::Simple("haushaltskunde".to_string()),
4416 );
4417
4418 let def = MappingDefinition {
4419 meta: MappingMeta {
4420 entity: "Test".to_string(),
4421 bo4e_type: "Test".to_string(),
4422 companion_type: Some("TestEdifact".to_string()),
4423 source_group: "SG4".to_string(),
4424 source_path: None,
4425 discriminator: None,
4426 repeat_on_tag: None,
4427 },
4428 fields: IndexMap::new(),
4429 companion_fields: Some(companion_fields),
4430 complex_handlers: None,
4431 };
4432
4433 let engine = MappingEngine::from_definitions(vec![]);
4434
4435 let bo4e = serde_json::json!({
4436 "testEdifact": {
4437 "haushaltskunde": {
4438 "code": "Z15",
4439 "meaning": "Haushaltskunde",
4440 "enum": "HAUSHALTSKUNDE"
4441 }
4442 }
4443 });
4444 let instance = engine.map_reverse(&bo4e, &def);
4445 assert_eq!(instance.segments[0].elements[2], vec!["Z15"]);
4446 }
4447
4448 #[test]
4449 fn test_reverse_mapping_accepts_enriched_companion() {
4450 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4452 companion_fields.insert(
4453 "cci.2".to_string(),
4454 FieldMapping::Simple("haushaltskunde".to_string()),
4455 );
4456
4457 let def = MappingDefinition {
4458 meta: MappingMeta {
4459 entity: "Test".to_string(),
4460 bo4e_type: "Test".to_string(),
4461 companion_type: Some("TestEdifact".to_string()),
4462 source_group: "SG4".to_string(),
4463 source_path: None,
4464 discriminator: None,
4465 repeat_on_tag: None,
4466 },
4467 fields: IndexMap::new(),
4468 companion_fields: Some(companion_fields),
4469 complex_handlers: None,
4470 };
4471
4472 let engine = MappingEngine::from_definitions(vec![]);
4473
4474 let bo4e_plain = serde_json::json!({
4476 "testEdifact": {
4477 "haushaltskunde": "Z15"
4478 }
4479 });
4480 let instance_plain = engine.map_reverse(&bo4e_plain, &def);
4481 assert_eq!(instance_plain.segments[0].elements[2], vec!["Z15"]);
4482
4483 let bo4e_enriched = serde_json::json!({
4485 "testEdifact": {
4486 "haushaltskunde": {
4487 "code": "Z15",
4488 "meaning": "Haushaltskunde gem. EnWG"
4489 }
4490 }
4491 });
4492 let instance_enriched = engine.map_reverse(&bo4e_enriched, &def);
4493 assert_eq!(instance_enriched.segments[0].elements[2], vec!["Z15"]);
4494 }
4495
4496 #[test]
4497 fn test_resolve_child_relative_with_source_path() {
4498 let mut map: std::collections::HashMap<String, Vec<usize>> =
4499 std::collections::HashMap::new();
4500 map.insert("sg4.sg8_ze1".to_string(), vec![6]);
4501 map.insert("sg4.sg8_z98".to_string(), vec![0]);
4502
4503 assert_eq!(
4505 resolve_child_relative("SG8.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
4506 "SG8:6.SG10"
4507 );
4508
4509 assert_eq!(
4511 resolve_child_relative("SG8:3.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
4512 "SG8:3.SG10"
4513 );
4514
4515 assert_eq!(
4517 resolve_child_relative("SG8.SG10", Some("sg4.sg8_unknown.sg10"), &map, 0),
4518 "SG8.SG10"
4519 );
4520
4521 assert_eq!(
4523 resolve_child_relative("SG8.SG10", None, &map, 0),
4524 "SG8.SG10"
4525 );
4526
4527 assert_eq!(
4529 resolve_child_relative("SG8.SG9", Some("sg4.sg8_z98.sg9"), &map, 0),
4530 "SG8:0.SG9"
4531 );
4532
4533 map.insert("sg4.sg8_zf3".to_string(), vec![3, 4]);
4535 assert_eq!(
4536 resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 0),
4537 "SG8:3.SG10"
4538 );
4539 assert_eq!(
4540 resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 1),
4541 "SG8:4.SG10"
4542 );
4543 }
4544
4545 #[test]
4546 fn test_place_in_groups_returns_rep_index() {
4547 let mut groups: Vec<AssembledGroup> = Vec::new();
4548
4549 let instance = AssembledGroupInstance {
4551 segments: vec![],
4552 child_groups: vec![],
4553 skipped_segments: vec![],
4554 };
4555 assert_eq!(place_in_groups(&mut groups, "SG8", instance), 0);
4556
4557 let instance = AssembledGroupInstance {
4559 segments: vec![],
4560 child_groups: vec![],
4561 skipped_segments: vec![],
4562 };
4563 assert_eq!(place_in_groups(&mut groups, "SG8", instance), 1);
4564
4565 let instance = AssembledGroupInstance {
4567 segments: vec![],
4568 child_groups: vec![],
4569 skipped_segments: vec![],
4570 };
4571 assert_eq!(place_in_groups(&mut groups, "SG8:5", instance), 5);
4572 }
4573
4574 #[test]
4575 fn test_resolve_by_source_path() {
4576 use mig_assembly::assembler::*;
4577
4578 let tree = AssembledTree {
4580 segments: vec![],
4581 groups: vec![AssembledGroup {
4582 group_id: "SG4".to_string(),
4583 repetitions: vec![AssembledGroupInstance {
4584 segments: vec![],
4585 child_groups: vec![AssembledGroup {
4586 group_id: "SG8".to_string(),
4587 repetitions: vec![
4588 AssembledGroupInstance {
4589 segments: vec![AssembledSegment {
4590 tag: "SEQ".to_string(),
4591 elements: vec![vec!["Z98".to_string()]],
4592 }],
4593 child_groups: vec![AssembledGroup {
4594 group_id: "SG10".to_string(),
4595 repetitions: vec![AssembledGroupInstance {
4596 segments: vec![AssembledSegment {
4597 tag: "CCI".to_string(),
4598 elements: vec![vec![], vec![], vec!["ZB3".to_string()]],
4599 }],
4600 child_groups: vec![],
4601 skipped_segments: vec![],
4602 }],
4603 }],
4604 skipped_segments: vec![],
4605 },
4606 AssembledGroupInstance {
4607 segments: vec![AssembledSegment {
4608 tag: "SEQ".to_string(),
4609 elements: vec![vec!["ZD7".to_string()]],
4610 }],
4611 child_groups: vec![AssembledGroup {
4612 group_id: "SG10".to_string(),
4613 repetitions: vec![AssembledGroupInstance {
4614 segments: vec![AssembledSegment {
4615 tag: "CCI".to_string(),
4616 elements: vec![vec![], vec![], vec!["ZE6".to_string()]],
4617 }],
4618 child_groups: vec![],
4619 skipped_segments: vec![],
4620 }],
4621 }],
4622 skipped_segments: vec![],
4623 },
4624 ],
4625 }],
4626 skipped_segments: vec![],
4627 }],
4628 }],
4629 post_group_start: 0,
4630 inter_group_segments: std::collections::BTreeMap::new(),
4631 };
4632
4633 let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_z98.sg10");
4635 assert!(inst.is_some());
4636 assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
4637
4638 let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zd7.sg10");
4640 assert!(inst.is_some());
4641 assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZE6");
4642
4643 let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zzz.sg10");
4645 assert!(inst.is_none());
4646
4647 let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8.sg10");
4649 assert!(inst.is_some());
4650 assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
4651 }
4652
4653 #[test]
4654 fn test_parse_source_path_part() {
4655 assert_eq!(parse_source_path_part("sg4"), ("sg4", None));
4656 assert_eq!(parse_source_path_part("sg8_z98"), ("sg8", Some("z98")));
4657 assert_eq!(parse_source_path_part("sg10"), ("sg10", None));
4658 assert_eq!(parse_source_path_part("sg12_z04"), ("sg12", Some("z04")));
4659 }
4660
4661 #[test]
4662 fn test_has_source_path_qualifiers() {
4663 assert!(has_source_path_qualifiers("sg4.sg8_z98.sg10"));
4664 assert!(has_source_path_qualifiers("sg4.sg8_ze1.sg9"));
4665 assert!(!has_source_path_qualifiers("sg4.sg6"));
4666 assert!(!has_source_path_qualifiers("sg4.sg8.sg10"));
4667 }
4668
4669 #[test]
4670 fn test_companion_dotted_path_forward() {
4671 use mig_assembly::assembler::*;
4672
4673 let tree = AssembledTree {
4675 segments: vec![],
4676 groups: vec![AssembledGroup {
4677 group_id: "SG4".to_string(),
4678 repetitions: vec![AssembledGroupInstance {
4679 segments: vec![],
4680 child_groups: vec![AssembledGroup {
4681 group_id: "SG8".to_string(),
4682 repetitions: vec![AssembledGroupInstance {
4683 segments: vec![],
4684 child_groups: vec![AssembledGroup {
4685 group_id: "SG10".to_string(),
4686 repetitions: vec![AssembledGroupInstance {
4687 segments: vec![AssembledSegment {
4688 tag: "CCI".to_string(),
4689 elements: vec![
4690 vec!["11XAB-1234".to_string()],
4691 vec!["305".to_string()],
4692 ],
4693 }],
4694 child_groups: vec![],
4695 skipped_segments: vec![],
4696 }],
4697 }],
4698 skipped_segments: vec![],
4699 }],
4700 }],
4701 skipped_segments: vec![],
4702 }],
4703 }],
4704 post_group_start: 0,
4705 inter_group_segments: std::collections::BTreeMap::new(),
4706 };
4707
4708 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4710 companion_fields.insert(
4711 "cci.0".to_string(),
4712 FieldMapping::Simple("bilanzkreis.id".to_string()),
4713 );
4714 companion_fields.insert(
4715 "cci.1".to_string(),
4716 FieldMapping::Simple("bilanzkreis.codelist".to_string()),
4717 );
4718
4719 let def = MappingDefinition {
4720 meta: MappingMeta {
4721 entity: "Test".to_string(),
4722 bo4e_type: "Test".to_string(),
4723 companion_type: Some("TestEdifact".to_string()),
4724 source_group: "SG4.SG8.SG10".to_string(),
4725 source_path: Some("sg4.sg8_z01.sg10".to_string()),
4726 discriminator: None,
4727 repeat_on_tag: None,
4728 },
4729 fields: IndexMap::new(),
4730 companion_fields: Some(companion_fields),
4731 complex_handlers: None,
4732 };
4733
4734 let engine = MappingEngine::from_definitions(vec![]);
4735 let bo4e = engine.map_forward(&tree, &def, 0);
4736
4737 let companion = &bo4e["testEdifact"];
4739 assert!(
4740 companion.is_object(),
4741 "testEdifact should be an object, got: {companion}"
4742 );
4743 let bilanzkreis = &companion["bilanzkreis"];
4744 assert!(
4745 bilanzkreis.is_object(),
4746 "bilanzkreis should be a nested object, got: {bilanzkreis}"
4747 );
4748 assert_eq!(
4749 bilanzkreis["id"].as_str(),
4750 Some("11XAB-1234"),
4751 "bilanzkreis.id should be 11XAB-1234"
4752 );
4753 assert_eq!(
4754 bilanzkreis["codelist"].as_str(),
4755 Some("305"),
4756 "bilanzkreis.codelist should be 305"
4757 );
4758 }
4759
4760 #[test]
4761 fn test_companion_dotted_path_reverse() {
4762 let engine = MappingEngine::from_definitions(vec![]);
4764
4765 let companion_value = serde_json::json!({
4766 "bilanzkreis": {
4767 "id": "11XAB-1234",
4768 "codelist": "305"
4769 }
4770 });
4771
4772 assert_eq!(
4773 engine.populate_field(&companion_value, "bilanzkreis.id"),
4774 Some("11XAB-1234".to_string()),
4775 "dotted path bilanzkreis.id should resolve"
4776 );
4777 assert_eq!(
4778 engine.populate_field(&companion_value, "bilanzkreis.codelist"),
4779 Some("305".to_string()),
4780 "dotted path bilanzkreis.codelist should resolve"
4781 );
4782
4783 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4785 companion_fields.insert(
4786 "cci.0".to_string(),
4787 FieldMapping::Simple("bilanzkreis.id".to_string()),
4788 );
4789 companion_fields.insert(
4790 "cci.1".to_string(),
4791 FieldMapping::Simple("bilanzkreis.codelist".to_string()),
4792 );
4793
4794 let def = MappingDefinition {
4795 meta: MappingMeta {
4796 entity: "Test".to_string(),
4797 bo4e_type: "Test".to_string(),
4798 companion_type: Some("TestEdifact".to_string()),
4799 source_group: "SG4.SG8.SG10".to_string(),
4800 source_path: Some("sg4.sg8_z01.sg10".to_string()),
4801 discriminator: None,
4802 repeat_on_tag: None,
4803 },
4804 fields: IndexMap::new(),
4805 companion_fields: Some(companion_fields),
4806 complex_handlers: None,
4807 };
4808
4809 let bo4e = serde_json::json!({
4810 "testEdifact": {
4811 "bilanzkreis": {
4812 "id": "11XAB-1234",
4813 "codelist": "305"
4814 }
4815 }
4816 });
4817
4818 let instance = engine.map_reverse(&bo4e, &def);
4819 assert_eq!(instance.segments.len(), 1, "should produce one CCI segment");
4820 let cci = &instance.segments[0];
4821 assert_eq!(cci.tag, "CCI");
4822 assert_eq!(
4823 cci.elements[0],
4824 vec!["11XAB-1234"],
4825 "element 0 should contain bilanzkreis.id"
4826 );
4827 assert_eq!(
4828 cci.elements[1],
4829 vec!["305"],
4830 "element 1 should contain bilanzkreis.codelist"
4831 );
4832 }
4833
4834 #[test]
4835 fn test_when_filled_injects_when_field_present() {
4836 let toml_str = r#"
4837[meta]
4838entity = "Test"
4839bo4e_type = "Test"
4840companion_type = "TestEdifact"
4841source_group = "SG4.SG8.SG10"
4842
4843[fields]
4844
4845[companion_fields]
4846"cci.0.0" = { target = "", default = "Z83", when_filled = ["merkmalCode"] }
4847"cav.0.0" = "merkmalCode"
4848"#;
4849 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
4850
4851 let bo4e_with = serde_json::json!({
4853 "testEdifact": { "merkmalCode": "ZA7" }
4854 });
4855 let engine = MappingEngine::new_empty();
4856 let instance = engine.map_reverse(&bo4e_with, &def);
4857 let cci = instance
4858 .segments
4859 .iter()
4860 .find(|s| s.tag == "CCI")
4861 .expect("CCI should exist");
4862 assert_eq!(cci.elements[0][0], "Z83");
4863
4864 let bo4e_without = serde_json::json!({
4866 "testEdifact": {}
4867 });
4868 let instance2 = engine.map_reverse(&bo4e_without, &def);
4869 let cci2 = instance2.segments.iter().find(|s| s.tag == "CCI");
4870 assert!(
4871 cci2.is_none(),
4872 "CCI should not be emitted when merkmalCode is absent"
4873 );
4874 }
4875
4876 #[test]
4877 fn test_when_filled_checks_core_and_companion() {
4878 let toml_str = r#"
4879[meta]
4880entity = "Test"
4881bo4e_type = "Test"
4882companion_type = "TestEdifact"
4883source_group = "SG4.SG5"
4884
4885[fields]
4886"loc.1.0" = "marktlokationsId"
4887
4888[companion_fields]
4889"loc.0.0" = { target = "", default = "Z16", when_filled = ["marktlokationsId"] }
4890"#;
4891 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
4892
4893 let bo4e_with = serde_json::json!({
4895 "marktlokationsId": "51234567890"
4896 });
4897 let engine = MappingEngine::new_empty();
4898 let instance = engine.map_reverse(&bo4e_with, &def);
4899 let loc = instance
4900 .segments
4901 .iter()
4902 .find(|s| s.tag == "LOC")
4903 .expect("LOC should exist");
4904 assert_eq!(loc.elements[0][0], "Z16");
4905 assert_eq!(loc.elements[1][0], "51234567890");
4906
4907 let bo4e_without = serde_json::json!({});
4909 let instance2 = engine.map_reverse(&bo4e_without, &def);
4910 let loc2 = instance2.segments.iter().find(|s| s.tag == "LOC");
4911 assert!(loc2.is_none());
4912 }
4913
4914 #[test]
4915 fn test_extract_all_from_instance_collects_all_qualifier_matches() {
4916 use mig_assembly::assembler::*;
4917
4918 let instance = AssembledGroupInstance {
4920 segments: vec![
4921 AssembledSegment {
4922 tag: "SEQ".to_string(),
4923 elements: vec![vec!["ZD6".to_string()]],
4924 },
4925 AssembledSegment {
4926 tag: "RFF".to_string(),
4927 elements: vec![vec!["Z34".to_string(), "REF_A".to_string()]],
4928 },
4929 AssembledSegment {
4930 tag: "RFF".to_string(),
4931 elements: vec![vec!["Z34".to_string(), "REF_B".to_string()]],
4932 },
4933 AssembledSegment {
4934 tag: "RFF".to_string(),
4935 elements: vec![vec!["Z34".to_string(), "REF_C".to_string()]],
4936 },
4937 AssembledSegment {
4938 tag: "RFF".to_string(),
4939 elements: vec![vec!["Z35".to_string(), "OTHER".to_string()]],
4940 },
4941 ],
4942 child_groups: vec![],
4943 skipped_segments: vec![],
4944 };
4945
4946 let all = MappingEngine::extract_all_from_instance(&instance, "rff[Z34,*].0.1");
4948 assert_eq!(all, vec!["REF_A", "REF_B", "REF_C"]);
4949
4950 let single = MappingEngine::extract_from_instance(&instance, "rff[Z34].0.1");
4952 assert_eq!(single, Some("REF_A".to_string()));
4953
4954 let second = MappingEngine::extract_from_instance(&instance, "rff[Z34,1].0.1");
4955 assert_eq!(second, Some("REF_B".to_string()));
4956 }
4957
4958 #[test]
4959 fn test_forward_wildcard_collect_produces_json_array() {
4960 use mig_assembly::assembler::*;
4961
4962 let instance = AssembledGroupInstance {
4963 segments: vec![
4964 AssembledSegment {
4965 tag: "SEQ".to_string(),
4966 elements: vec![vec!["ZD6".to_string()]],
4967 },
4968 AssembledSegment {
4969 tag: "RFF".to_string(),
4970 elements: vec![vec!["Z34".to_string(), "REF_A".to_string()]],
4971 },
4972 AssembledSegment {
4973 tag: "RFF".to_string(),
4974 elements: vec![vec!["Z34".to_string(), "REF_B".to_string()]],
4975 },
4976 ],
4977 child_groups: vec![],
4978 skipped_segments: vec![],
4979 };
4980
4981 let toml_str = r#"
4982[meta]
4983entity = "Test"
4984bo4e_type = "Test"
4985companion_type = "TestEdifact"
4986source_group = "SG4.SG8"
4987
4988[fields]
4989
4990[companion_fields]
4991"rff[Z34,*].0.1" = "messlokationsIdRefs"
4992"#;
4993 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
4994 let engine = MappingEngine::new_empty();
4995
4996 let mut result = serde_json::Map::new();
4997 engine.extract_companion_fields(&instance, &def, &mut result, false);
4998
4999 let companion = result.get("testEdifact").unwrap().as_object().unwrap();
5000 let refs = companion
5001 .get("messlokationsIdRefs")
5002 .unwrap()
5003 .as_array()
5004 .unwrap();
5005 assert_eq!(refs.len(), 2);
5006 assert_eq!(refs[0].as_str().unwrap(), "REF_A");
5007 assert_eq!(refs[1].as_str().unwrap(), "REF_B");
5008 }
5009
5010 #[test]
5011 fn test_reverse_json_array_produces_multiple_segments() {
5012 let toml_str = r#"
5013[meta]
5014entity = "Test"
5015bo4e_type = "Test"
5016companion_type = "TestEdifact"
5017source_group = "SG4.SG8"
5018
5019[fields]
5020
5021[companion_fields]
5022"seq.0.0" = { target = "", default = "ZD6" }
5023"rff[Z34,*].0.1" = "messlokationsIdRefs"
5024"#;
5025 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5026 let engine = MappingEngine::new_empty();
5027
5028 let bo4e = serde_json::json!({
5029 "testEdifact": {
5030 "messlokationsIdRefs": ["REF_A", "REF_B", "REF_C"]
5031 }
5032 });
5033
5034 let instance = engine.map_reverse(&bo4e, &def);
5035
5036 let rff_segs: Vec<_> = instance
5038 .segments
5039 .iter()
5040 .filter(|s| s.tag == "RFF")
5041 .collect();
5042 assert_eq!(rff_segs.len(), 3);
5043 assert_eq!(rff_segs[0].elements[0][0], "Z34");
5044 assert_eq!(rff_segs[0].elements[0][1], "REF_A");
5045 assert_eq!(rff_segs[1].elements[0][0], "Z34");
5046 assert_eq!(rff_segs[1].elements[0][1], "REF_B");
5047 assert_eq!(rff_segs[2].elements[0][0], "Z34");
5048 assert_eq!(rff_segs[2].elements[0][1], "REF_C");
5049 }
5050
5051 #[test]
5052 fn test_when_filled_dotted_path() {
5053 let toml_str = r#"
5054[meta]
5055entity = "Test"
5056bo4e_type = "Test"
5057companion_type = "TestEdifact"
5058source_group = "SG4.SG8.SG10"
5059
5060[fields]
5061
5062[companion_fields]
5063"cci.0.0" = { target = "", default = "Z83", when_filled = ["merkmal.code"] }
5064"cav.0.0" = "merkmal.code"
5065"#;
5066 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5067
5068 let bo4e = serde_json::json!({
5069 "testEdifact": { "merkmal": { "code": "ZA7" } }
5070 });
5071 let engine = MappingEngine::new_empty();
5072 let instance = engine.map_reverse(&bo4e, &def);
5073 let cci = instance
5074 .segments
5075 .iter()
5076 .find(|s| s.tag == "CCI")
5077 .expect("CCI should exist");
5078 assert_eq!(cci.elements[0][0], "Z83");
5079 }
5080
5081 #[test]
5082 fn test_also_target_forward_extracts_both_fields() {
5083 use mig_assembly::assembler::*;
5084
5085 let instance = AssembledGroupInstance {
5086 segments: vec![AssembledSegment {
5087 tag: "NAD".to_string(),
5088 elements: vec![vec!["Z47".to_string()], vec!["12345".to_string()]],
5089 }],
5090 child_groups: vec![],
5091 skipped_segments: vec![],
5092 };
5093
5094 let toml_str = r#"
5095[meta]
5096entity = "Geschaeftspartner"
5097bo4e_type = "Geschaeftspartner"
5098companion_type = "GeschaeftspartnerEdifact"
5099source_group = "SG4.SG12"
5100
5101[fields]
5102"nad.1.0" = "identifikation"
5103
5104[companion_fields."nad.0.0"]
5105target = "partnerrolle"
5106enum_map = { "Z47" = "kundeDesLf", "Z48" = "kundeDesLf", "Z51" = "kundeDesNb", "Z52" = "kundeDesNb" }
5107also_target = "datenqualitaet"
5108also_enum_map = { "Z47" = "erwartet", "Z48" = "imSystemVorhanden", "Z51" = "erwartet", "Z52" = "imSystemVorhanden" }
5109"#;
5110 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5111 let engine = MappingEngine::new_empty();
5112
5113 let mut result = serde_json::Map::new();
5114 engine.extract_companion_fields(&instance, &def, &mut result, false);
5115
5116 let companion = result
5117 .get("geschaeftspartnerEdifact")
5118 .unwrap()
5119 .as_object()
5120 .unwrap();
5121 assert_eq!(
5122 companion.get("partnerrolle").unwrap().as_str().unwrap(),
5123 "kundeDesLf"
5124 );
5125 assert_eq!(
5126 companion.get("datenqualitaet").unwrap().as_str().unwrap(),
5127 "erwartet"
5128 );
5129 }
5130
5131 #[test]
5132 fn test_also_target_reverse_joint_lookup() {
5133 let toml_str = r#"
5134[meta]
5135entity = "Geschaeftspartner"
5136bo4e_type = "Geschaeftspartner"
5137companion_type = "GeschaeftspartnerEdifact"
5138source_group = "SG4.SG12"
5139
5140[fields]
5141
5142[companion_fields."nad.0.0"]
5143target = "partnerrolle"
5144enum_map = { "Z47" = "kundeDesLf", "Z48" = "kundeDesLf", "Z51" = "kundeDesNb", "Z52" = "kundeDesNb" }
5145also_target = "datenqualitaet"
5146also_enum_map = { "Z47" = "erwartet", "Z48" = "imSystemVorhanden", "Z51" = "erwartet", "Z52" = "imSystemVorhanden" }
5147"#;
5148 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5149 let engine = MappingEngine::new_empty();
5150
5151 let bo4e = serde_json::json!({
5153 "geschaeftspartnerEdifact": {
5154 "partnerrolle": "kundeDesLf",
5155 "datenqualitaet": "erwartet"
5156 }
5157 });
5158 let instance = engine.map_reverse(&bo4e, &def);
5159 let nad = instance
5160 .segments
5161 .iter()
5162 .find(|s| s.tag == "NAD")
5163 .expect("NAD");
5164 assert_eq!(nad.elements[0][0], "Z47");
5165
5166 let bo4e2 = serde_json::json!({
5168 "geschaeftspartnerEdifact": {
5169 "partnerrolle": "kundeDesNb",
5170 "datenqualitaet": "imSystemVorhanden"
5171 }
5172 });
5173 let instance2 = engine.map_reverse(&bo4e2, &def);
5174 let nad2 = instance2
5175 .segments
5176 .iter()
5177 .find(|s| s.tag == "NAD")
5178 .expect("NAD");
5179 assert_eq!(nad2.elements[0][0], "Z52");
5180 }
5181
5182 #[test]
5183 fn test_also_target_mixed_codes_unpaired_skips_datenqualitaet() {
5184 use mig_assembly::assembler::*;
5185
5186 let toml_str = r#"
5188[meta]
5189entity = "Geschaeftspartner"
5190bo4e_type = "Geschaeftspartner"
5191companion_type = "GeschaeftspartnerEdifact"
5192source_group = "SG4.SG12"
5193
5194[fields]
5195
5196[companion_fields."nad.0.0"]
5197target = "partnerrolle"
5198enum_map = { "Z09" = "kundeDesLf", "Z47" = "kundeDesLf", "Z48" = "kundeDesLf" }
5199also_target = "datenqualitaet"
5200also_enum_map = { "Z47" = "erwartet", "Z48" = "imSystemVorhanden" }
5201"#;
5202 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5203 let engine = MappingEngine::new_empty();
5204
5205 let instance_z09 = AssembledGroupInstance {
5207 segments: vec![AssembledSegment {
5208 tag: "NAD".to_string(),
5209 elements: vec![vec!["Z09".to_string()]],
5210 }],
5211 child_groups: vec![],
5212 skipped_segments: vec![],
5213 };
5214 let mut result = serde_json::Map::new();
5215 engine.extract_companion_fields(&instance_z09, &def, &mut result, false);
5216 let comp = result
5217 .get("geschaeftspartnerEdifact")
5218 .unwrap()
5219 .as_object()
5220 .unwrap();
5221 assert_eq!(
5222 comp.get("partnerrolle").unwrap().as_str().unwrap(),
5223 "kundeDesLf"
5224 );
5225 assert!(
5226 comp.get("datenqualitaet").is_none(),
5227 "Z09 should not set datenqualitaet"
5228 );
5229
5230 let bo4e = serde_json::json!({
5232 "geschaeftspartnerEdifact": { "partnerrolle": "kundeDesLf" }
5233 });
5234 let instance = engine.map_reverse(&bo4e, &def);
5235 let nad = instance
5236 .segments
5237 .iter()
5238 .find(|s| s.tag == "NAD")
5239 .expect("NAD");
5240 assert_eq!(nad.elements[0][0], "Z09");
5241
5242 let bo4e2 = serde_json::json!({
5244 "geschaeftspartnerEdifact": {
5245 "partnerrolle": "kundeDesLf",
5246 "datenqualitaet": "erwartet"
5247 }
5248 });
5249 let instance2 = engine.map_reverse(&bo4e2, &def);
5250 let nad2 = instance2
5251 .segments
5252 .iter()
5253 .find(|s| s.tag == "NAD")
5254 .expect("NAD");
5255 assert_eq!(nad2.elements[0][0], "Z47");
5256 }
5257}