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 {
1244 let raw_key = def.meta.companion_type.as_deref().unwrap_or("_companion");
1245 let companion_key = to_camel_case(raw_key);
1246 let companion_value = bo4e_value
1247 .get(&companion_key)
1248 .unwrap_or(bo4e_value);
1249
1250 for (path, field_mapping) in companion_fields {
1251 let (target, default, enum_map, when_filled, also_target, also_enum_map) =
1252 match field_mapping {
1253 FieldMapping::Simple(t) => (t.as_str(), None, None, None, None, None),
1254 FieldMapping::Structured(s) => (
1255 s.target.as_str(),
1256 s.default.as_ref(),
1257 s.enum_map.as_ref(),
1258 s.when_filled.as_ref(),
1259 s.also_target.as_deref(),
1260 s.also_enum_map.as_ref(),
1261 ),
1262 FieldMapping::Nested(_) => continue,
1263 };
1264
1265 let parts: Vec<&str> = path.split('.').collect();
1266 if parts.len() < 2 {
1267 continue;
1268 }
1269
1270 let (seg_tag, qualifier, _occ) = parse_tag_qualifier(parts[0]);
1271 let seg_key = parts[0].to_uppercase();
1272 let sub_path = &parts[1..];
1273
1274 let (element_idx, component_idx) = if let Ok(ei) = sub_path[0].parse::<usize>() {
1275 let ci = if sub_path.len() > 1 {
1276 sub_path[1].parse::<usize>().unwrap_or(0)
1277 } else {
1278 0
1279 };
1280 (ei, ci)
1281 } else {
1282 match sub_path.len() {
1283 1 => (0, 0),
1284 2 => (1, 0),
1285 _ => continue,
1286 }
1287 };
1288
1289 if is_collect_all_path(path) && !target.is_empty() {
1291 if let Some(arr) = self
1292 .populate_field_json(companion_value, target)
1293 .and_then(|v| v.as_array().cloned())
1294 {
1295 has_data_fields = true;
1296 if !arr.is_empty() {
1297 has_real_data = true;
1298 }
1299 for (idx, item) in arr.iter().enumerate() {
1300 if let Some(val_str) = item.as_str() {
1301 let mapped = if let Some(map) = enum_map {
1302 map.iter()
1303 .find(|(_, bo4e_v)| *bo4e_v == val_str)
1304 .map(|(edifact_k, _)| edifact_k.clone())
1305 .unwrap_or_else(|| val_str.to_string())
1306 } else {
1307 val_str.to_string()
1308 };
1309 let occ_key = if let Some(q) = qualifier {
1310 format!("{}[{},{}]", seg_tag, q, idx)
1311 } else {
1312 format!("{}[*,{}]", seg_tag, idx)
1313 };
1314 field_values.push((
1315 occ_key.clone(),
1316 seg_tag.clone(),
1317 element_idx,
1318 component_idx,
1319 mapped,
1320 ));
1321 if let Some(q) = qualifier {
1323 if injected_qualifiers.insert(occ_key.clone()) {
1324 field_values.push((
1325 occ_key,
1326 seg_tag.clone(),
1327 0,
1328 0,
1329 q.to_string(),
1330 ));
1331 }
1332 }
1333 }
1334 }
1335 }
1336 continue;
1337 }
1338
1339 let val = if target.is_empty() {
1340 match (default, when_filled) {
1341 (Some(d), Some(fields)) => {
1342 let any_filled = fields.iter().any(|f| {
1343 self.populate_field(bo4e_value, f).is_some()
1344 || self.populate_field(companion_value, f).is_some()
1345 });
1346 if any_filled {
1347 has_real_data = true;
1348 Some(d.clone())
1349 } else {
1350 None
1351 }
1352 }
1353 (Some(d), None) => Some(d.clone()),
1354 (None, _) => None,
1355 }
1356 } else {
1357 has_data_fields = true;
1358 seg_has_data_field.insert(seg_key.clone());
1359 let bo4e_val = self.populate_field(companion_value, target);
1360 if bo4e_val.is_some() {
1361 has_real_data = true;
1362 seg_has_real_data.insert(seg_key.clone());
1363 }
1364 let mapped_val = match (bo4e_val, enum_map) {
1365 (Some(v), Some(map)) => {
1366 if let (Some(at), Some(am)) = (also_target, also_enum_map) {
1367 let also_val = self.populate_field(companion_value, at);
1368 if let Some(av) = also_val.as_deref() {
1369 map.iter()
1371 .find(|(edifact_k, bo4e_v)| {
1372 *bo4e_v == &v
1373 && am.get(*edifact_k).is_some_and(|am_v| am_v == av)
1374 })
1375 .map(|(edifact_k, _)| edifact_k.clone())
1376 .or(Some(v))
1377 } else {
1378 map.iter()
1381 .find(|(edifact_k, bo4e_v)| {
1382 *bo4e_v == &v && !am.contains_key(*edifact_k)
1383 })
1384 .or_else(|| {
1385 map.iter().find(|(_, bo4e_v)| *bo4e_v == &v)
1387 })
1388 .map(|(edifact_k, _)| edifact_k.clone())
1389 .or(Some(v))
1390 }
1391 } else {
1392 map.iter()
1393 .find(|(_, bo4e_v)| *bo4e_v == &v)
1394 .map(|(edifact_k, _)| edifact_k.clone())
1395 .or(Some(v))
1396 }
1397 }
1398 (v, _) => v,
1399 };
1400 mapped_val.or_else(|| default.cloned())
1401 };
1402
1403 if let Some(val) = val {
1404 field_values.push((
1405 seg_key.clone(),
1406 seg_tag.clone(),
1407 element_idx,
1408 component_idx,
1409 val,
1410 ));
1411 }
1412
1413 if let Some(q) = qualifier {
1414 if injected_qualifiers.insert(seg_key.clone()) {
1415 field_values.push((seg_key, seg_tag, 0, 0, q.to_string()));
1416 }
1417 }
1418 }
1419 }
1420
1421 field_values.retain(|(seg_key, _, _, _, _)| {
1429 if !seg_key.contains('[') {
1430 return true; }
1432 !seg_has_data_field.contains(seg_key) || seg_has_real_data.contains(seg_key)
1433 });
1434
1435 if has_data_fields && !has_real_data {
1440 return AssembledGroupInstance {
1441 segments: vec![],
1442 child_groups: vec![],
1443 skipped_segments: Vec::new(),
1444 };
1445 }
1446
1447 let mut segments: Vec<AssembledSegment> = Vec::with_capacity(field_values.len());
1450 let mut seen_keys: HashMap<String, usize> = HashMap::new();
1451
1452 for (seg_key, seg_tag, element_idx, component_idx, val) in &field_values {
1453 let seg = if let Some(&pos) = seen_keys.get(seg_key) {
1454 &mut segments[pos]
1455 } else {
1456 let pos = segments.len();
1457 seen_keys.insert(seg_key.clone(), pos);
1458 segments.push(AssembledSegment {
1459 tag: seg_tag.clone(),
1460 elements: vec![],
1461 });
1462 &mut segments[pos]
1463 };
1464
1465 while seg.elements.len() <= *element_idx {
1466 seg.elements.push(vec![]);
1467 }
1468 while seg.elements[*element_idx].len() <= *component_idx {
1469 seg.elements[*element_idx].push(String::new());
1470 }
1471 seg.elements[*element_idx][*component_idx] = val.clone();
1472 }
1473
1474 for seg in &mut segments {
1477 let last_populated = seg.elements.iter().rposition(|e| !e.is_empty());
1478 if let Some(last_idx) = last_populated {
1479 for i in 0..last_idx {
1480 if seg.elements[i].is_empty() {
1481 seg.elements[i] = vec![String::new()];
1482 }
1483 }
1484 }
1485 }
1486
1487 if let Some(ref ss) = self.segment_structure {
1489 for seg in &mut segments {
1490 if let Some(expected) = ss.element_count(&seg.tag) {
1491 while seg.elements.len() < expected {
1492 seg.elements.push(vec![String::new()]);
1493 }
1494 }
1495 }
1496 }
1497
1498 AssembledGroupInstance {
1499 segments,
1500 child_groups: vec![],
1501 skipped_segments: Vec::new(),
1502 }
1503 }
1504
1505 fn resolve_field_path(segment: &AssembledSegment, path: &[&str]) -> Option<String> {
1518 if path.is_empty() {
1519 return None;
1520 }
1521
1522 if let Ok(element_idx) = path[0].parse::<usize>() {
1524 let component_idx = if path.len() > 1 {
1525 path[1].parse::<usize>().unwrap_or(0)
1526 } else {
1527 0
1528 };
1529 return segment
1530 .elements
1531 .get(element_idx)?
1532 .get(component_idx)
1533 .filter(|v| !v.is_empty())
1534 .cloned();
1535 }
1536
1537 match path.len() {
1539 1 => segment
1540 .elements
1541 .first()?
1542 .first()
1543 .filter(|v| !v.is_empty())
1544 .cloned(),
1545 2 => segment
1546 .elements
1547 .get(1)?
1548 .first()
1549 .filter(|v| !v.is_empty())
1550 .cloned(),
1551 _ => None,
1552 }
1553 }
1554
1555 fn parse_element_component(parts: &[&str]) -> (usize, usize) {
1558 if parts.is_empty() {
1559 return (0, 0);
1560 }
1561 let element_idx = parts[0].parse::<usize>().unwrap_or(0);
1562 let component_idx = if parts.len() > 1 {
1563 parts[1].parse::<usize>().unwrap_or(0)
1564 } else {
1565 0
1566 };
1567 (element_idx, component_idx)
1568 }
1569
1570 pub fn populate_field(
1573 &self,
1574 bo4e_value: &serde_json::Value,
1575 target_field: &str,
1576 ) -> Option<String> {
1577 let mut current = bo4e_value;
1578 for part in target_field.split('.') {
1579 current = current.get(part)?;
1580 }
1581 if let Some(code) = current.get("code").and_then(|v| v.as_str()) {
1583 return Some(code.to_string());
1584 }
1585 current.as_str().map(|s| s.to_string())
1586 }
1587
1588 fn populate_field_json<'a>(
1591 &self,
1592 bo4e_value: &'a serde_json::Value,
1593 target_field: &str,
1594 ) -> Option<&'a serde_json::Value> {
1595 let mut current = bo4e_value;
1596 for part in target_field.split('.') {
1597 current = current.get(part)?;
1598 }
1599 Some(current)
1600 }
1601
1602 pub fn build_segment_from_bo4e(
1604 &self,
1605 bo4e_value: &serde_json::Value,
1606 segment_tag: &str,
1607 target_field: &str,
1608 ) -> AssembledSegment {
1609 let value = self.populate_field(bo4e_value, target_field);
1610 let elements = if let Some(val) = value {
1611 vec![vec![val]]
1612 } else {
1613 vec![]
1614 };
1615 AssembledSegment {
1616 tag: segment_tag.to_uppercase(),
1617 elements,
1618 }
1619 }
1620
1621 pub fn resolve_repetition(
1630 tree: &AssembledTree,
1631 group_path: &str,
1632 discriminator: &str,
1633 ) -> Option<usize> {
1634 let (spec, expected) = discriminator.split_once('=')?;
1635 let parts: Vec<&str> = spec.split('.').collect();
1636 if parts.len() != 3 {
1637 return None;
1638 }
1639 let tag = parts[0];
1640 let element_idx: usize = parts[1].parse().ok()?;
1641 let component_idx: usize = parts[2].parse().ok()?;
1642
1643 let path_parts: Vec<&str> = group_path.split('.').collect();
1645
1646 let leaf_group = if path_parts.len() == 1 {
1647 let (group_id, _) = parse_group_spec(path_parts[0]);
1648 tree.groups.iter().find(|g| g.group_id == group_id)?
1649 } else {
1650 let parent_parts = &path_parts[..path_parts.len() - 1];
1652 let mut current_instance = {
1653 let (first_id, first_rep) = parse_group_spec(parent_parts[0]);
1654 let first_group = tree.groups.iter().find(|g| g.group_id == first_id)?;
1655 first_group.repetitions.get(first_rep.unwrap_or(0))?
1656 };
1657 for part in &parent_parts[1..] {
1658 let (group_id, explicit_rep) = parse_group_spec(part);
1659 let child_group = current_instance
1660 .child_groups
1661 .iter()
1662 .find(|g| g.group_id == group_id)?;
1663 current_instance = child_group.repetitions.get(explicit_rep.unwrap_or(0))?;
1664 }
1665 let (leaf_id, _) = parse_group_spec(path_parts.last()?);
1666 current_instance
1667 .child_groups
1668 .iter()
1669 .find(|g| g.group_id == leaf_id)?
1670 };
1671
1672 let expected_values: Vec<&str> = expected.split('|').collect();
1674 for (rep_idx, instance) in leaf_group.repetitions.iter().enumerate() {
1675 let matches = instance.segments.iter().any(|s| {
1676 s.tag.eq_ignore_ascii_case(tag)
1677 && s.elements
1678 .get(element_idx)
1679 .and_then(|e| e.get(component_idx))
1680 .map(|v| expected_values.iter().any(|ev| v == ev))
1681 .unwrap_or(false)
1682 });
1683 if matches {
1684 return Some(rep_idx);
1685 }
1686 }
1687
1688 None
1689 }
1690
1691 pub fn resolve_all_repetitions(
1696 tree: &AssembledTree,
1697 group_path: &str,
1698 discriminator: &str,
1699 ) -> Vec<usize> {
1700 let Some((spec, expected)) = discriminator.split_once('=') else {
1701 return Vec::new();
1702 };
1703 let parts: Vec<&str> = spec.split('.').collect();
1704 if parts.len() != 3 {
1705 return Vec::new();
1706 }
1707 let tag = parts[0];
1708 let element_idx: usize = match parts[1].parse() {
1709 Ok(v) => v,
1710 Err(_) => return Vec::new(),
1711 };
1712 let component_idx: usize = match parts[2].parse() {
1713 Ok(v) => v,
1714 Err(_) => return Vec::new(),
1715 };
1716
1717 let path_parts: Vec<&str> = group_path.split('.').collect();
1719
1720 let leaf_group = if path_parts.len() == 1 {
1721 let (group_id, _) = parse_group_spec(path_parts[0]);
1722 match tree.groups.iter().find(|g| g.group_id == group_id) {
1723 Some(g) => g,
1724 None => return Vec::new(),
1725 }
1726 } else {
1727 let parent_parts = &path_parts[..path_parts.len() - 1];
1728 let mut current_instance = {
1729 let (first_id, first_rep) = parse_group_spec(parent_parts[0]);
1730 let first_group = match tree.groups.iter().find(|g| g.group_id == first_id) {
1731 Some(g) => g,
1732 None => return Vec::new(),
1733 };
1734 match first_group.repetitions.get(first_rep.unwrap_or(0)) {
1735 Some(i) => i,
1736 None => return Vec::new(),
1737 }
1738 };
1739 for part in &parent_parts[1..] {
1740 let (group_id, explicit_rep) = parse_group_spec(part);
1741 let child_group = match current_instance
1742 .child_groups
1743 .iter()
1744 .find(|g| g.group_id == group_id)
1745 {
1746 Some(g) => g,
1747 None => return Vec::new(),
1748 };
1749 current_instance = match child_group.repetitions.get(explicit_rep.unwrap_or(0)) {
1750 Some(i) => i,
1751 None => return Vec::new(),
1752 };
1753 }
1754 let (leaf_id, _) = match path_parts.last() {
1755 Some(p) => parse_group_spec(p),
1756 None => return Vec::new(),
1757 };
1758 match current_instance
1759 .child_groups
1760 .iter()
1761 .find(|g| g.group_id == leaf_id)
1762 {
1763 Some(g) => g,
1764 None => return Vec::new(),
1765 }
1766 };
1767
1768 let (expected_raw, occurrence) = parse_discriminator_occurrence(expected);
1770
1771 let expected_values: Vec<&str> = expected_raw.split('|').collect();
1773 let mut result = Vec::new();
1774 for (rep_idx, instance) in leaf_group.repetitions.iter().enumerate() {
1775 let matches = instance.segments.iter().any(|s| {
1776 s.tag.eq_ignore_ascii_case(tag)
1777 && s.elements
1778 .get(element_idx)
1779 .and_then(|e| e.get(component_idx))
1780 .map(|v| expected_values.iter().any(|ev| v == ev))
1781 .unwrap_or(false)
1782 });
1783 if matches {
1784 result.push(rep_idx);
1785 }
1786 }
1787
1788 if let Some(occ) = occurrence {
1790 result.into_iter().nth(occ).into_iter().collect()
1791 } else {
1792 result
1793 }
1794 }
1795
1796 pub fn map_all_forward(&self, tree: &AssembledTree) -> serde_json::Value {
1818 self.map_all_forward_inner(tree, true).0
1819 }
1820
1821 pub fn map_all_forward_enriched(
1825 &self,
1826 tree: &AssembledTree,
1827 enrich_codes: bool,
1828 ) -> serde_json::Value {
1829 self.map_all_forward_inner(tree, enrich_codes).0
1830 }
1831
1832 fn map_all_forward_inner(
1839 &self,
1840 tree: &AssembledTree,
1841 enrich_codes: bool,
1842 ) -> (
1843 serde_json::Value,
1844 std::collections::HashMap<String, Vec<usize>>,
1845 ) {
1846 let mut result = serde_json::Map::new();
1847 let mut nesting_info: std::collections::HashMap<String, Vec<usize>> =
1848 std::collections::HashMap::new();
1849
1850 for def in &self.definitions {
1851 let entity = &def.meta.entity;
1852
1853 let bo4e = if let Some(ref disc) = def.meta.discriminator {
1854 let use_source_path = def
1859 .meta
1860 .source_path
1861 .as_ref()
1862 .is_some_and(|sp| has_source_path_qualifiers(sp));
1863 if use_source_path {
1864 let sp = def.meta.source_path.as_deref().unwrap();
1866 let all_instances = Self::resolve_all_by_source_path(tree, sp);
1867 let instances: Vec<_> = if let Some(matcher) = DiscriminatorMatcher::parse(disc)
1869 {
1870 matcher.filter_instances(all_instances)
1871 } else {
1872 all_instances
1873 };
1874 let extract = |instance: &AssembledGroupInstance| {
1875 let mut r = serde_json::Map::new();
1876 self.extract_fields_from_instance(instance, def, &mut r, enrich_codes);
1877 self.extract_companion_fields(instance, def, &mut r, enrich_codes);
1878 serde_json::Value::Object(r)
1879 };
1880 match instances.len() {
1881 0 => None,
1882 1 => Some(extract(instances[0])),
1883 _ => Some(serde_json::Value::Array(
1884 instances.iter().map(|i| extract(i)).collect(),
1885 )),
1886 }
1887 } else {
1888 let reps = Self::resolve_all_repetitions(tree, &def.meta.source_group, disc);
1889 match reps.len() {
1890 0 => None,
1891 1 => Some(self.map_forward_inner(tree, def, reps[0], enrich_codes)),
1892 _ => Some(serde_json::Value::Array(
1893 reps.iter()
1894 .map(|&rep| self.map_forward_inner(tree, def, rep, enrich_codes))
1895 .collect(),
1896 )),
1897 }
1898 }
1899 } else if def.meta.source_group.is_empty() {
1900 Some(self.map_forward_inner(tree, def, 0, enrich_codes))
1902 } else if def.meta.source_path.as_ref().is_some_and(|sp| {
1903 has_source_path_qualifiers(sp) || def.meta.source_group.contains('.')
1904 }) {
1905 let sp = def.meta.source_path.as_deref().unwrap();
1910 let mut indexed = Self::resolve_all_with_parent_indices(tree, sp);
1911
1912 if let Some(last_part) = sp.rsplit('.').next() {
1917 if !last_part.contains('_') {
1918 let base_prefix = if let Some(parent) = sp.rsplit_once('.') {
1922 format!("{}.", parent.0)
1923 } else {
1924 String::new()
1925 };
1926 let sibling_qualifiers: Vec<String> = self
1927 .definitions
1928 .iter()
1929 .filter_map(|d| d.meta.source_path.as_deref())
1930 .filter(|other_sp| {
1931 *other_sp != sp
1932 && other_sp.starts_with(&base_prefix)
1933 && other_sp.split('.').count() == sp.split('.').count()
1934 })
1935 .filter_map(|other_sp| {
1936 let other_last = other_sp.rsplit('.').next()?;
1937 let (base, q) = other_last.split_once('_')?;
1940 if base == last_part {
1941 Some(q.to_string())
1942 } else {
1943 None
1944 }
1945 })
1946 .collect();
1947
1948 if !sibling_qualifiers.is_empty() {
1949 indexed.retain(|(_, inst)| {
1950 let entry_qual = inst
1951 .segments
1952 .first()
1953 .and_then(|seg| seg.elements.first())
1954 .and_then(|el| el.first())
1955 .map(|v| v.to_lowercase());
1956 !entry_qual.is_some_and(|q| {
1959 sibling_qualifiers.iter().any(|sq| {
1960 sq.split('_').any(|part| part.eq_ignore_ascii_case(&q))
1961 })
1962 })
1963 });
1964 }
1965 }
1966 }
1967 let extract = |instance: &AssembledGroupInstance| {
1968 let mut r = serde_json::Map::new();
1969 self.extract_fields_from_instance(instance, def, &mut r, enrich_codes);
1970 self.extract_companion_fields(instance, def, &mut r, enrich_codes);
1971 serde_json::Value::Object(r)
1972 };
1973 if def.meta.source_group.contains('.') && !indexed.is_empty() {
1978 if let Some(sp) = &def.meta.source_path {
1979 let parent_indices: Vec<usize> =
1980 indexed.iter().map(|(idx, _)| *idx).collect();
1981 nesting_info.entry(sp.clone()).or_insert(parent_indices);
1982
1983 let child_key = format!("{sp}#child");
1986 if let std::collections::hash_map::Entry::Vacant(e) =
1987 nesting_info.entry(child_key)
1988 {
1989 let child_indices: Vec<usize> =
1990 Self::compute_child_indices(tree, sp, &indexed);
1991 if !child_indices.is_empty() {
1992 e.insert(child_indices);
1993 }
1994 }
1995 }
1996 }
1997 match indexed.len() {
1998 0 => None,
1999 1 => Some(extract(indexed[0].1)),
2000 _ => Some(serde_json::Value::Array(
2001 indexed.iter().map(|(_, i)| extract(i)).collect(),
2002 )),
2003 }
2004 } else {
2005 let num_reps = Self::count_repetitions(tree, &def.meta.source_group);
2006 if num_reps <= 1 {
2007 Some(self.map_forward_inner(tree, def, 0, enrich_codes))
2008 } else {
2009 let mut items = Vec::with_capacity(num_reps);
2011 for rep in 0..num_reps {
2012 items.push(self.map_forward_inner(tree, def, rep, enrich_codes));
2013 }
2014 Some(serde_json::Value::Array(items))
2015 }
2016 };
2017
2018 if let Some(bo4e) = bo4e {
2019 let bo4e = inject_bo4e_metadata(bo4e, &def.meta.bo4e_type);
2020 let key = to_camel_case(entity);
2021 deep_merge_insert(&mut result, &key, bo4e);
2022 }
2023 }
2024
2025 (serde_json::Value::Object(result), nesting_info)
2026 }
2027
2028 pub fn map_all_reverse(
2037 &self,
2038 entities: &serde_json::Value,
2039 nesting_info: Option<&std::collections::HashMap<String, Vec<usize>>>,
2040 ) -> AssembledTree {
2041 let mut root_segments: Vec<AssembledSegment> = Vec::new();
2042 let mut groups: Vec<AssembledGroup> = Vec::new();
2043
2044 for def in &self.definitions {
2045 let entity_key = to_camel_case(&def.meta.entity);
2046
2047 let entity_value = entities.get(&entity_key);
2049
2050 if entity_value.is_none() {
2051 continue;
2052 }
2053 let entity_value = entity_value.unwrap();
2054
2055 let unwrapped: Option<serde_json::Value>;
2063 let entity_value = if entity_value.is_object() && !entity_value.is_array() {
2064 if let Some(disc_value) = def
2065 .meta
2066 .discriminator
2067 .as_deref()
2068 .and_then(|d| d.split_once('='))
2069 .map(|(_, v)| v)
2070 {
2071 if let Some(inner) = entity_value.get(disc_value) {
2073 let mut injected = inner.clone();
2074 if let Some(ref cf) = def.companion_fields {
2077 let disc_path = def
2078 .meta
2079 .discriminator
2080 .as_deref()
2081 .unwrap()
2082 .split_once('=')
2083 .unwrap()
2084 .0
2085 .to_lowercase();
2086 for (path, mapping) in cf {
2087 let cf_path = path.to_lowercase();
2091 let matches = cf_path == disc_path
2092 || format!("{}.0", cf_path) == disc_path;
2093 if matches {
2094 let target = match mapping {
2095 FieldMapping::Simple(t) => t.as_str(),
2096 FieldMapping::Structured(s) => s.target.as_str(),
2097 FieldMapping::Nested(_) => continue,
2098 };
2099 if !target.is_empty() {
2100 if let Some(obj) = injected.as_object_mut() {
2101 obj.entry(target.to_string()).or_insert_with(|| {
2102 serde_json::Value::String(
2103 disc_value.to_string(),
2104 )
2105 });
2106 }
2107 }
2108 break;
2109 }
2110 }
2111 }
2112 unwrapped = Some(injected);
2113 unwrapped.as_ref().unwrap()
2114 } else {
2115 entity_value
2116 }
2117 } else if is_map_keyed_object(entity_value) {
2118 let map = entity_value.as_object().unwrap();
2123 let arr: Vec<serde_json::Value> = map
2124 .iter()
2125 .map(|(key, val)| {
2126 let mut item = val.clone();
2127 if let Some(obj) = item.as_object_mut() {
2130 if let Some(qualifier_field) =
2131 find_qualifier_companion_field(&self.definitions, &def.meta.entity)
2132 {
2133 obj.entry(qualifier_field).or_insert_with(|| {
2134 serde_json::Value::String(key.clone())
2135 });
2136 }
2137 }
2138 item
2139 })
2140 .collect();
2141 unwrapped = Some(serde_json::Value::Array(arr));
2142 unwrapped.as_ref().unwrap()
2143 } else {
2144 entity_value
2145 }
2146 } else {
2147 entity_value
2148 };
2149
2150 let leaf_group = def
2152 .meta
2153 .source_group
2154 .rsplit('.')
2155 .next()
2156 .unwrap_or(&def.meta.source_group);
2157
2158 if def.meta.source_group.is_empty() {
2159 let instance = self.map_reverse(entity_value, def);
2161 root_segments.extend(instance.segments);
2162 } else if entity_value.is_array() {
2163 let arr = entity_value.as_array().unwrap();
2165 let reps: Vec<_> = arr.iter().map(|item| self.map_reverse(item, def)).collect();
2166
2167 if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
2169 existing.repetitions.extend(reps);
2170 } else {
2171 groups.push(AssembledGroup {
2172 group_id: leaf_group.to_string(),
2173 repetitions: reps,
2174 });
2175 }
2176 } else {
2177 let instance = self.map_reverse(entity_value, def);
2179
2180 if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
2181 existing.repetitions.push(instance);
2182 } else {
2183 groups.push(AssembledGroup {
2184 group_id: leaf_group.to_string(),
2185 repetitions: vec![instance],
2186 });
2187 }
2188 }
2189 }
2190
2191 let nested_specs: Vec<(String, String)> = self
2197 .definitions
2198 .iter()
2199 .filter_map(|def| {
2200 let parts: Vec<&str> = def.meta.source_group.split('.').collect();
2201 if parts.len() > 1 {
2202 Some((parts[0].to_string(), parts[parts.len() - 1].to_string()))
2203 } else {
2204 None
2205 }
2206 })
2207 .collect();
2208 for (parent_id, child_id) in &nested_specs {
2209 let has_parent = groups.iter().any(|g| g.group_id == *parent_id);
2211 let has_child = groups.iter().any(|g| g.group_id == *child_id);
2212 if has_parent && has_child {
2213 let child_idx = groups.iter().position(|g| g.group_id == *child_id).unwrap();
2214 let child_group = groups.remove(child_idx);
2215 let parent = groups
2216 .iter_mut()
2217 .find(|g| g.group_id == *parent_id)
2218 .unwrap();
2219 let child_source_path = self
2223 .definitions
2224 .iter()
2225 .find(|d| {
2226 let parts: Vec<&str> = d.meta.source_group.split('.').collect();
2227 parts.len() > 1 && parts[parts.len() - 1] == *child_id
2228 })
2229 .and_then(|d| d.meta.source_path.as_deref());
2230 let distribution =
2231 child_source_path.and_then(|key| nesting_info.and_then(|ni| ni.get(key)));
2232 for (i, child_rep) in child_group.repetitions.into_iter().enumerate() {
2233 let target_idx = distribution
2234 .and_then(|dist| dist.get(i))
2235 .copied()
2236 .unwrap_or(0);
2237
2238 if let Some(target_rep) = parent.repetitions.get_mut(target_idx) {
2239 if let Some(existing) = target_rep
2240 .child_groups
2241 .iter_mut()
2242 .find(|g| g.group_id == *child_id)
2243 {
2244 existing.repetitions.push(child_rep);
2245 } else {
2246 target_rep.child_groups.push(AssembledGroup {
2247 group_id: child_id.clone(),
2248 repetitions: vec![child_rep],
2249 });
2250 }
2251 }
2252 }
2253 }
2254 }
2255
2256 let post_group_start = root_segments.len();
2257 AssembledTree {
2258 segments: root_segments,
2259 groups,
2260 post_group_start,
2261 inter_group_segments: std::collections::BTreeMap::new(),
2262 }
2263 }
2264
2265 fn count_repetitions(tree: &AssembledTree, group_path: &str) -> usize {
2267 let parts: Vec<&str> = group_path.split('.').collect();
2268
2269 let (first_id, first_rep) = parse_group_spec(parts[0]);
2270 let first_group = match tree.groups.iter().find(|g| g.group_id == first_id) {
2271 Some(g) => g,
2272 None => return 0,
2273 };
2274
2275 if parts.len() == 1 {
2276 return first_group.repetitions.len();
2277 }
2278
2279 let mut current_instance = match first_group.repetitions.get(first_rep.unwrap_or(0)) {
2281 Some(i) => i,
2282 None => return 0,
2283 };
2284
2285 for (i, part) in parts[1..].iter().enumerate() {
2286 let (group_id, explicit_rep) = parse_group_spec(part);
2287 let child_group = match current_instance
2288 .child_groups
2289 .iter()
2290 .find(|g| g.group_id == group_id)
2291 {
2292 Some(g) => g,
2293 None => return 0,
2294 };
2295
2296 if i == parts.len() - 2 {
2297 return child_group.repetitions.len();
2299 }
2300 current_instance = match child_group.repetitions.get(explicit_rep.unwrap_or(0)) {
2301 Some(i) => i,
2302 None => return 0,
2303 };
2304 }
2305
2306 0
2307 }
2308
2309 pub fn map_interchange(
2318 msg_engine: &MappingEngine,
2319 tx_engine: &MappingEngine,
2320 tree: &AssembledTree,
2321 transaction_group: &str,
2322 enrich_codes: bool,
2323 ) -> crate::model::MappedMessage {
2324 let (stammdaten, nesting_info) = msg_engine.map_all_forward_inner(tree, enrich_codes);
2326
2327 let transaktionen = tree
2329 .groups
2330 .iter()
2331 .find(|g| g.group_id == transaction_group)
2332 .map(|sg| {
2333 sg.repetitions
2334 .iter()
2335 .map(|instance| {
2336 let wrapped_tree = AssembledTree {
2339 segments: vec![],
2340 groups: vec![AssembledGroup {
2341 group_id: transaction_group.to_string(),
2342 repetitions: vec![instance.clone()],
2343 }],
2344 post_group_start: 0,
2345 inter_group_segments: std::collections::BTreeMap::new(),
2346 };
2347
2348 let (tx_result, tx_nesting) =
2349 tx_engine.map_all_forward_inner(&wrapped_tree, enrich_codes);
2350
2351 crate::model::MappedTransaktion {
2352 stammdaten: tx_result,
2353 nesting_info: tx_nesting,
2354 }
2355 })
2356 .collect()
2357 })
2358 .unwrap_or_default();
2359
2360 crate::model::MappedMessage {
2361 stammdaten,
2362 transaktionen,
2363 nesting_info,
2364 }
2365 }
2366
2367 pub fn map_interchange_reverse(
2377 msg_engine: &MappingEngine,
2378 tx_engine: &MappingEngine,
2379 mapped: &crate::model::MappedMessage,
2380 transaction_group: &str,
2381 filtered_mig: Option<&MigSchema>,
2382 ) -> AssembledTree {
2383 let msg_tree = msg_engine.map_all_reverse(
2385 &mapped.stammdaten,
2386 if mapped.nesting_info.is_empty() {
2387 None
2388 } else {
2389 Some(&mapped.nesting_info)
2390 },
2391 );
2392
2393 let mut sg4_reps: Vec<AssembledGroupInstance> = Vec::new();
2395
2396 struct DefWithMeta<'a> {
2400 def: &'a MappingDefinition,
2401 relative: String,
2402 depth: usize,
2403 }
2404
2405 let mut sorted_defs: Vec<DefWithMeta> = tx_engine
2406 .definitions
2407 .iter()
2408 .map(|def| {
2409 let relative = strip_tx_group_prefix(&def.meta.source_group, transaction_group);
2410 let depth = if relative.is_empty() {
2411 0
2412 } else {
2413 relative.chars().filter(|c| *c == '.').count() + 1
2414 };
2415 DefWithMeta {
2416 def,
2417 relative,
2418 depth,
2419 }
2420 })
2421 .collect();
2422
2423 let mut parent_rep_map: std::collections::HashMap<String, usize> =
2427 std::collections::HashMap::new();
2428 for dm in &sorted_defs {
2429 if dm.depth >= 2 {
2430 let parts: Vec<&str> = dm.relative.split('.').collect();
2431 let (_, parent_rep) = parse_group_spec(parts[0]);
2432 if let Some(rep_idx) = parent_rep {
2433 if let Some(sp) = &dm.def.meta.source_path {
2434 if let Some((parent_path, _)) = sp.rsplit_once('.') {
2435 parent_rep_map
2436 .entry(parent_path.to_string())
2437 .or_insert(rep_idx);
2438 }
2439 }
2440 }
2441 }
2442 }
2443
2444 for dm in &mut sorted_defs {
2447 if dm.depth == 1 && !dm.relative.contains(':') {
2448 if let Some(sp) = &dm.def.meta.source_path {
2449 if let Some(rep_idx) = parent_rep_map.get(sp.as_str()) {
2450 dm.relative = format!("{}:{}", dm.relative, rep_idx);
2451 }
2452 }
2453 }
2454 }
2455
2456 if let Some(mig) = filtered_mig {
2463 let mig_order = build_reverse_mig_group_order(mig, transaction_group);
2464 sorted_defs.sort_by(|a, b| {
2465 a.depth.cmp(&b.depth).then_with(|| {
2466 let a_id = a.relative.split(':').next().unwrap_or(&a.relative);
2467 let b_id = b.relative.split(':').next().unwrap_or(&b.relative);
2468 let a_pos = variant_mig_position(a.def, a_id, &mig_order);
2470 let b_pos = variant_mig_position(b.def, b_id, &mig_order);
2471 a_pos.cmp(&b_pos).then(a.relative.cmp(&b.relative))
2472 })
2473 });
2474 } else {
2475 sorted_defs.sort_by(|a, b| a.depth.cmp(&b.depth).then(a.relative.cmp(&b.relative)));
2476 }
2477
2478 for tx in &mapped.transaktionen {
2479 let mut root_segs: Vec<AssembledSegment> = Vec::new();
2480 let mut child_groups: Vec<AssembledGroup> = Vec::new();
2481
2482 let mut source_path_to_rep: std::collections::HashMap<String, Vec<usize>> =
2487 std::collections::HashMap::new();
2488
2489 for dm in &sorted_defs {
2490 let entity_key = to_camel_case(&dm.def.meta.entity);
2492 let bo4e_value = match tx.stammdaten.get(&entity_key) {
2493 Some(v) => v,
2494 None => continue,
2495 };
2496
2497 let unwrapped_value: Option<serde_json::Value>;
2499 let bo4e_value = if bo4e_value.is_object() && !bo4e_value.is_array() {
2500 if let Some(disc_value) = dm
2501 .def
2502 .meta
2503 .discriminator
2504 .as_deref()
2505 .and_then(|d| d.split_once('='))
2506 .map(|(_, v)| v)
2507 {
2508 if let Some(inner) = bo4e_value.get(disc_value) {
2509 let mut injected = inner.clone();
2510 if let Some(ref cf) = dm.def.companion_fields {
2511 let disc_path = dm
2512 .def
2513 .meta
2514 .discriminator
2515 .as_deref()
2516 .unwrap()
2517 .split_once('=')
2518 .unwrap()
2519 .0
2520 .to_lowercase();
2521 for (path, mapping) in cf {
2522 let cf_path = path.to_lowercase();
2523 let matches = cf_path == disc_path
2524 || format!("{}.0", cf_path) == disc_path;
2525 if matches {
2526 let target = match mapping {
2527 FieldMapping::Simple(t) => t.as_str(),
2528 FieldMapping::Structured(s) => s.target.as_str(),
2529 FieldMapping::Nested(_) => continue,
2530 };
2531 if !target.is_empty() {
2532 if let Some(obj) = injected.as_object_mut() {
2533 obj.entry(target.to_string()).or_insert_with(
2534 || {
2535 serde_json::Value::String(
2536 disc_value.to_string(),
2537 )
2538 },
2539 );
2540 }
2541 }
2542 break;
2543 }
2544 }
2545 }
2546 unwrapped_value = Some(injected);
2547 unwrapped_value.as_ref().unwrap()
2548 } else {
2549 bo4e_value
2550 }
2551 } else if is_map_keyed_object(bo4e_value) {
2552 let map = bo4e_value.as_object().unwrap();
2553 let arr: Vec<serde_json::Value> = map
2554 .iter()
2555 .map(|(key, val)| {
2556 let mut item = val.clone();
2557 if let Some(obj) = item.as_object_mut() {
2558 if let Some(qualifier_field) =
2559 find_qualifier_companion_field(
2560 &tx_engine.definitions,
2561 &dm.def.meta.entity,
2562 )
2563 {
2564 obj.entry(qualifier_field).or_insert_with(|| {
2565 serde_json::Value::String(key.clone())
2566 });
2567 }
2568 }
2569 item
2570 })
2571 .collect();
2572 unwrapped_value = Some(serde_json::Value::Array(arr));
2573 unwrapped_value.as_ref().unwrap()
2574 } else {
2575 bo4e_value
2576 }
2577 } else {
2578 bo4e_value
2579 };
2580
2581 let items: Vec<&serde_json::Value> = if bo4e_value.is_array() {
2585 bo4e_value.as_array().unwrap().iter().collect()
2586 } else {
2587 vec![bo4e_value]
2588 };
2589
2590 for (item_idx, item) in items.iter().enumerate() {
2591 let instance = tx_engine.map_reverse(item, dm.def);
2592
2593 if instance.segments.is_empty() && instance.child_groups.is_empty() {
2595 continue;
2596 }
2597
2598 if dm.relative.is_empty() {
2599 root_segs.extend(instance.segments);
2600 } else {
2601 let effective_relative = if dm.depth >= 2 {
2605 let rel = if items.len() > 1 {
2608 strip_all_rep_indices(&dm.relative)
2609 } else {
2610 dm.relative.clone()
2611 };
2612 let skip_nesting = dm
2619 .def
2620 .meta
2621 .source_path
2622 .as_ref()
2623 .and_then(|sp| sp.rsplit_once('.'))
2624 .and_then(|(parent_path, _)| {
2625 source_path_to_rep.get(parent_path)
2626 })
2627 .is_some_and(|reps| reps.len() == 1);
2628 let nesting_idx = if items.len() > 1 && !skip_nesting {
2629 dm.def
2630 .meta
2631 .source_path
2632 .as_ref()
2633 .and_then(|sp| tx.nesting_info.get(sp))
2634 .and_then(|dist| dist.get(item_idx))
2635 .copied()
2636 } else {
2637 None
2638 };
2639 if let Some(parent_rep) = nesting_idx {
2640 let parts: Vec<&str> = rel.split('.').collect();
2642 let parent_id = parts[0].split(':').next().unwrap_or(parts[0]);
2643 let rest = parts[1..].join(".");
2644 format!("{}:{}.{}", parent_id, parent_rep, rest)
2645 } else {
2646 resolve_child_relative(
2647 &rel,
2648 dm.def.meta.source_path.as_deref(),
2649 &source_path_to_rep,
2650 item_idx,
2651 )
2652 }
2653 } else if dm.depth == 1 {
2654 let child_key = dm
2657 .def
2658 .meta
2659 .source_path
2660 .as_ref()
2661 .map(|sp| format!("{sp}#child"));
2662 if let Some(child_indices) =
2663 child_key.as_ref().and_then(|ck| tx.nesting_info.get(ck))
2664 {
2665 if let Some(&target) = child_indices.get(item_idx) {
2666 if target != usize::MAX {
2667 let base =
2668 dm.relative.split(':').next().unwrap_or(&dm.relative);
2669 format!("{}:{}", base, target)
2670 } else {
2671 dm.relative.clone()
2672 }
2673 } else if items.len() > 1 && item_idx > 0 {
2674 strip_rep_index(&dm.relative)
2675 } else {
2676 dm.relative.clone()
2677 }
2678 } else if items.len() > 1 && item_idx > 0 {
2679 strip_rep_index(&dm.relative)
2680 } else {
2681 dm.relative.clone()
2682 }
2683 } else if items.len() > 1 && item_idx > 0 {
2684 strip_rep_index(&dm.relative)
2687 } else {
2688 dm.relative.clone()
2689 };
2690
2691 let rep_used =
2692 place_in_groups(&mut child_groups, &effective_relative, instance);
2693
2694 if dm.depth == 1 {
2696 if let Some(sp) = &dm.def.meta.source_path {
2697 source_path_to_rep
2698 .entry(sp.clone())
2699 .or_default()
2700 .push(rep_used);
2701 }
2702 }
2703 }
2704 }
2705 }
2706
2707 if let Some(mig) = filtered_mig {
2712 sort_variant_reps_by_mig(&mut child_groups, mig, transaction_group);
2713 }
2714
2715 sg4_reps.push(AssembledGroupInstance {
2716 segments: root_segs,
2717 child_groups,
2718 skipped_segments: Vec::new(),
2719 });
2720 }
2721
2722 let mut root_segments = Vec::new();
2729 let mut uns_segments = Vec::new();
2730 let mut uns_is_summary = false;
2731 let mut found_uns = false;
2732 for seg in msg_tree.segments {
2733 if seg.tag == "UNS" {
2734 uns_is_summary = seg
2736 .elements
2737 .first()
2738 .and_then(|el| el.first())
2739 .map(|v| v == "S")
2740 .unwrap_or(false);
2741 uns_segments.push(seg);
2742 found_uns = true;
2743 } else if found_uns {
2744 uns_segments.push(seg);
2746 } else {
2747 root_segments.push(seg);
2748 }
2749 }
2750
2751 let pre_group_count = root_segments.len();
2752 let mut all_groups = msg_tree.groups;
2753 let mut inter_group = msg_tree.inter_group_segments;
2754
2755 let sg_num = |id: &str| -> usize {
2757 id.strip_prefix("SG")
2758 .and_then(|n| n.parse::<usize>().ok())
2759 .unwrap_or(0)
2760 };
2761
2762 if !sg4_reps.is_empty() {
2763 if uns_is_summary {
2764 all_groups.push(AssembledGroup {
2766 group_id: transaction_group.to_string(),
2767 repetitions: sg4_reps,
2768 });
2769 if !uns_segments.is_empty() {
2770 all_groups.sort_by_key(|g| sg_num(&g.group_id));
2775 let tx_num = sg_num(transaction_group);
2776 let uns_pos = all_groups
2777 .iter()
2778 .rposition(|g| sg_num(&g.group_id) <= tx_num)
2779 .map(|i| i + 1)
2780 .unwrap_or(all_groups.len());
2781 inter_group.insert(uns_pos, uns_segments);
2782 }
2783 } else {
2784 if !uns_segments.is_empty() {
2786 inter_group.insert(all_groups.len(), uns_segments);
2787 }
2788 all_groups.push(AssembledGroup {
2789 group_id: transaction_group.to_string(),
2790 repetitions: sg4_reps,
2791 });
2792 }
2793 } else if !uns_segments.is_empty() {
2794 if transaction_group.is_empty() {
2795 all_groups.sort_by_key(|g| sg_num(&g.group_id));
2800 if uns_is_summary {
2801 inter_group.insert(all_groups.len(), uns_segments);
2802 } else {
2803 inter_group.insert(0, uns_segments);
2804 }
2805 } else {
2806 all_groups.sort_by_key(|g| sg_num(&g.group_id));
2810 let tx_num = sg_num(transaction_group);
2811 let uns_pos = all_groups
2812 .iter()
2813 .rposition(|g| sg_num(&g.group_id) <= tx_num)
2814 .map(|i| i + 1)
2815 .unwrap_or(all_groups.len());
2816 inter_group.insert(uns_pos, uns_segments);
2817 }
2818 }
2819
2820 AssembledTree {
2821 segments: root_segments,
2822 groups: all_groups,
2823 post_group_start: pre_group_count,
2824 inter_group_segments: inter_group,
2825 }
2826 }
2827
2828 pub fn build_group_from_bo4e(
2830 &self,
2831 bo4e_value: &serde_json::Value,
2832 def: &MappingDefinition,
2833 ) -> AssembledGroup {
2834 let instance = self.map_reverse(bo4e_value, def);
2835 let leaf_group = def
2836 .meta
2837 .source_group
2838 .rsplit('.')
2839 .next()
2840 .unwrap_or(&def.meta.source_group);
2841
2842 AssembledGroup {
2843 group_id: leaf_group.to_string(),
2844 repetitions: vec![instance],
2845 }
2846 }
2847
2848 pub fn map_interchange_typed<M, T>(
2856 msg_engine: &MappingEngine,
2857 tx_engine: &MappingEngine,
2858 tree: &AssembledTree,
2859 tx_group: &str,
2860 enrich_codes: bool,
2861 nachrichtendaten: crate::model::Nachrichtendaten,
2862 interchangedaten: crate::model::Interchangedaten,
2863 ) -> Result<crate::model::Interchange<M, T>, serde_json::Error>
2864 where
2865 M: serde::de::DeserializeOwned,
2866 T: serde::de::DeserializeOwned,
2867 {
2868 let mapped = Self::map_interchange(msg_engine, tx_engine, tree, tx_group, enrich_codes);
2869 let nachricht = mapped.into_dynamic_nachricht(nachrichtendaten);
2870 let dynamic = crate::model::DynamicInterchange {
2871 interchangedaten,
2872 nachrichten: vec![nachricht],
2873 };
2874 let value = serde_json::to_value(&dynamic)?;
2875 serde_json::from_value(value)
2876 }
2877
2878 pub fn map_interchange_reverse_typed<M, T>(
2885 msg_engine: &MappingEngine,
2886 tx_engine: &MappingEngine,
2887 nachricht: &crate::model::Nachricht<M, T>,
2888 tx_group: &str,
2889 ) -> Result<AssembledTree, serde_json::Error>
2890 where
2891 M: serde::Serialize,
2892 T: serde::Serialize,
2893 {
2894 let stammdaten = serde_json::to_value(&nachricht.stammdaten)?;
2895 let transaktionen: Vec<crate::model::MappedTransaktion> = nachricht
2896 .transaktionen
2897 .iter()
2898 .map(|t| {
2899 Ok(crate::model::MappedTransaktion {
2900 stammdaten: serde_json::to_value(t)?,
2901 nesting_info: Default::default(),
2902 })
2903 })
2904 .collect::<Result<Vec<_>, serde_json::Error>>()?;
2905 let mapped = crate::model::MappedMessage {
2906 stammdaten,
2907 transaktionen,
2908 nesting_info: Default::default(),
2909 };
2910 Ok(Self::map_interchange_reverse(
2911 msg_engine, tx_engine, &mapped, tx_group, None,
2912 ))
2913 }
2914}
2915
2916fn parse_source_path_part(part: &str) -> (&str, Option<&str>) {
2923 if let Some(pos) = part.find('_') {
2927 let group = &part[..pos];
2928 let qualifier = &part[pos + 1..];
2929 if !qualifier.is_empty() {
2930 return (group, Some(qualifier));
2931 }
2932 }
2933 (part, None)
2934}
2935
2936fn build_reverse_mig_group_order(mig: &MigSchema, tx_group_id: &str) -> HashMap<String, usize> {
2944 let mut order = HashMap::new();
2945 if let Some(tg) = mig.segment_groups.iter().find(|g| g.id == tx_group_id) {
2946 for (i, nested) in tg.nested_groups.iter().enumerate() {
2947 if let Some(ref vc) = nested.variant_code {
2949 let variant_key = format!("{}_{}", nested.id, vc.to_uppercase());
2950 order.insert(variant_key, i);
2951 }
2952 order.entry(nested.id.clone()).or_insert(i);
2954 }
2955 }
2956 order
2957}
2958
2959fn variant_mig_position(
2965 def: &MappingDefinition,
2966 base_group_id: &str,
2967 mig_order: &HashMap<String, usize>,
2968) -> usize {
2969 if let Some(ref sp) = def.meta.source_path {
2972 let base_lower = base_group_id.to_lowercase();
2974 for part in sp.split('.') {
2975 if part.starts_with(&base_lower)
2976 || part.starts_with(base_group_id.to_lowercase().as_str())
2977 {
2978 if let Some(underscore_pos) = part.find('_') {
2980 let qualifier = &part[underscore_pos + 1..];
2981 let variant_key = format!("{}_{}", base_group_id, qualifier.to_uppercase());
2982 if let Some(&pos) = mig_order.get(&variant_key) {
2983 return pos;
2984 }
2985 }
2986 }
2987 }
2988 }
2989 mig_order.get(base_group_id).copied().unwrap_or(usize::MAX)
2991}
2992
2993fn find_rep_by_entry_qualifier<'a>(
2998 reps: &'a [AssembledGroupInstance],
2999 qualifier: &str,
3000) -> Option<&'a AssembledGroupInstance> {
3001 let parts: Vec<&str> = qualifier.split('_').collect();
3003 reps.iter().find(|inst| {
3004 inst.segments.first().is_some_and(|seg| {
3005 seg.elements
3006 .first()
3007 .and_then(|e| e.first())
3008 .is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
3009 })
3010 })
3011}
3012
3013fn find_all_reps_by_entry_qualifier<'a>(
3015 reps: &'a [AssembledGroupInstance],
3016 qualifier: &str,
3017) -> Vec<&'a AssembledGroupInstance> {
3018 let parts: Vec<&str> = qualifier.split('_').collect();
3020 reps.iter()
3021 .filter(|inst| {
3022 inst.segments.first().is_some_and(|seg| {
3023 seg.elements
3024 .first()
3025 .and_then(|e| e.first())
3026 .is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
3027 })
3028 })
3029 .collect()
3030}
3031
3032fn has_source_path_qualifiers(source_path: &str) -> bool {
3034 source_path.split('.').any(|part| {
3035 if let Some(pos) = part.find('_') {
3036 pos < part.len() - 1
3037 } else {
3038 false
3039 }
3040 })
3041}
3042
3043fn parse_group_spec(part: &str) -> (&str, Option<usize>) {
3044 if let Some(colon_pos) = part.find(':') {
3045 let id = &part[..colon_pos];
3046 let rep = part[colon_pos + 1..].parse::<usize>().ok();
3047 (id, rep)
3048 } else {
3049 (part, None)
3050 }
3051}
3052
3053fn strip_tx_group_prefix(source_group: &str, tx_group: &str) -> String {
3059 if source_group == tx_group || source_group.is_empty() {
3060 String::new()
3061 } else if let Some(rest) = source_group.strip_prefix(tx_group) {
3062 rest.strip_prefix('.').unwrap_or(rest).to_string()
3063 } else {
3064 source_group.to_string()
3065 }
3066}
3067
3068fn place_in_groups(
3076 groups: &mut Vec<AssembledGroup>,
3077 relative_path: &str,
3078 instance: AssembledGroupInstance,
3079) -> usize {
3080 let parts: Vec<&str> = relative_path.split('.').collect();
3081
3082 if parts.len() == 1 {
3083 let (id, rep) = parse_group_spec(parts[0]);
3085
3086 let group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == id) {
3088 g
3089 } else {
3090 groups.push(AssembledGroup {
3091 group_id: id.to_string(),
3092 repetitions: vec![],
3093 });
3094 groups.last_mut().unwrap()
3095 };
3096
3097 if let Some(rep_idx) = rep {
3098 while group.repetitions.len() <= rep_idx {
3100 group.repetitions.push(AssembledGroupInstance {
3101 segments: vec![],
3102 child_groups: vec![],
3103 skipped_segments: Vec::new(),
3104 });
3105 }
3106 group.repetitions[rep_idx]
3107 .segments
3108 .extend(instance.segments);
3109 group.repetitions[rep_idx]
3110 .child_groups
3111 .extend(instance.child_groups);
3112 rep_idx
3113 } else {
3114 let pos = group.repetitions.len();
3116 group.repetitions.push(instance);
3117 pos
3118 }
3119 } else {
3120 let (parent_id, parent_rep) = parse_group_spec(parts[0]);
3122 let rep_idx = parent_rep.unwrap_or(0);
3123
3124 let parent_group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == parent_id) {
3126 g
3127 } else {
3128 groups.push(AssembledGroup {
3129 group_id: parent_id.to_string(),
3130 repetitions: vec![],
3131 });
3132 groups.last_mut().unwrap()
3133 };
3134
3135 while parent_group.repetitions.len() <= rep_idx {
3137 parent_group.repetitions.push(AssembledGroupInstance {
3138 segments: vec![],
3139 child_groups: vec![],
3140 skipped_segments: Vec::new(),
3141 });
3142 }
3143
3144 let remaining = parts[1..].join(".");
3145 place_in_groups(
3146 &mut parent_group.repetitions[rep_idx].child_groups,
3147 &remaining,
3148 instance,
3149 );
3150 rep_idx
3151 }
3152}
3153
3154fn resolve_child_relative(
3166 relative: &str,
3167 source_path: Option<&str>,
3168 source_path_to_rep: &std::collections::HashMap<String, Vec<usize>>,
3169 item_idx: usize,
3170) -> String {
3171 let parts: Vec<&str> = relative.split('.').collect();
3172 if parts.is_empty() {
3173 return relative.to_string();
3174 }
3175
3176 let (parent_id, parent_rep) = parse_group_spec(parts[0]);
3178 if parent_rep.is_some() {
3179 return relative.to_string();
3180 }
3181
3182 if let Some(sp) = source_path {
3184 if let Some((parent_path, _child)) = sp.rsplit_once('.') {
3185 if let Some(rep_indices) = source_path_to_rep.get(parent_path) {
3186 let rep_idx = rep_indices
3188 .get(item_idx)
3189 .or_else(|| rep_indices.last())
3190 .copied()
3191 .unwrap_or(0);
3192 let rest = parts[1..].join(".");
3193 return format!("{}:{}.{}", parent_id, rep_idx, rest);
3194 }
3195 }
3196 }
3197
3198 relative.to_string()
3200}
3201
3202struct DiscriminatorMatcher<'a> {
3209 tag: &'a str,
3210 element_idx: usize,
3211 component_idx: usize,
3212 expected_values: Vec<&'a str>,
3213 occurrence: Option<usize>,
3215}
3216
3217impl<'a> DiscriminatorMatcher<'a> {
3218 fn parse(disc: &'a str) -> Option<Self> {
3219 let (spec, expected) = disc.split_once('=')?;
3220 let parts: Vec<&str> = spec.split('.').collect();
3221 if parts.len() != 3 {
3222 return None;
3223 }
3224 let (expected_raw, occurrence) = parse_discriminator_occurrence(expected);
3225 Some(Self {
3226 tag: parts[0],
3227 element_idx: parts[1].parse().ok()?,
3228 component_idx: parts[2].parse().ok()?,
3229 expected_values: expected_raw.split('|').collect(),
3230 occurrence,
3231 })
3232 }
3233
3234 fn matches(&self, instance: &AssembledGroupInstance) -> bool {
3235 instance.segments.iter().any(|s| {
3236 s.tag.eq_ignore_ascii_case(self.tag)
3237 && s.elements
3238 .get(self.element_idx)
3239 .and_then(|e| e.get(self.component_idx))
3240 .map(|v| self.expected_values.iter().any(|ev| v == ev))
3241 .unwrap_or(false)
3242 })
3243 }
3244
3245 fn filter_instances<'b>(
3247 &self,
3248 instances: Vec<&'b AssembledGroupInstance>,
3249 ) -> Vec<&'b AssembledGroupInstance> {
3250 let matching: Vec<_> = instances
3251 .into_iter()
3252 .filter(|inst| self.matches(inst))
3253 .collect();
3254 if let Some(occ) = self.occurrence {
3255 matching.into_iter().nth(occ).into_iter().collect()
3256 } else {
3257 matching
3258 }
3259 }
3260}
3261
3262fn parse_discriminator_occurrence(expected: &str) -> (&str, Option<usize>) {
3268 if let Some(hash_pos) = expected.rfind('#') {
3269 if let Ok(occ) = expected[hash_pos + 1..].parse::<usize>() {
3270 return (&expected[..hash_pos], Some(occ));
3271 }
3272 }
3273 (expected, None)
3274}
3275
3276fn strip_rep_index(relative: &str) -> String {
3280 let (id, _) = parse_group_spec(relative);
3281 id.to_string()
3282}
3283
3284fn strip_all_rep_indices(relative: &str) -> String {
3289 relative
3290 .split('.')
3291 .map(|part| {
3292 let (id, _) = parse_group_spec(part);
3293 id
3294 })
3295 .collect::<Vec<_>>()
3296 .join(".")
3297}
3298
3299fn is_collect_all_path(path: &str) -> bool {
3304 let tag_part = path.split('.').next().unwrap_or("");
3305 if let Some(bracket_start) = tag_part.find('[') {
3306 let inner = tag_part[bracket_start + 1..].trim_end_matches(']');
3307 if let Some(comma_pos) = inner.find(',') {
3308 let qualifier = &inner[..comma_pos];
3309 let occ = &inner[comma_pos + 1..];
3310 qualifier != "*" && occ == "*"
3312 } else {
3313 false
3314 }
3315 } else {
3316 false
3317 }
3318}
3319
3320fn parse_tag_qualifier(tag_part: &str) -> (String, Option<&str>, usize) {
3327 if let Some(bracket_start) = tag_part.find('[') {
3328 let tag = tag_part[..bracket_start].to_uppercase();
3329 let inner = tag_part[bracket_start + 1..].trim_end_matches(']');
3330 if let Some(comma_pos) = inner.find(',') {
3331 let qualifier = &inner[..comma_pos];
3332 let index = inner[comma_pos + 1..].parse::<usize>().unwrap_or(0);
3333 if qualifier == "*" {
3335 (tag, None, index)
3336 } else {
3337 (tag, Some(qualifier), index)
3338 }
3339 } else {
3340 (tag, Some(inner), 0)
3341 }
3342 } else {
3343 (tag_part.to_uppercase(), None, 0)
3344 }
3345}
3346
3347fn inject_bo4e_metadata(mut value: serde_json::Value, bo4e_type: &str) -> serde_json::Value {
3352 match &mut value {
3353 serde_json::Value::Object(map) => {
3354 map.entry("boTyp")
3355 .or_insert_with(|| serde_json::Value::String(bo4e_type.to_uppercase()));
3356 map.entry("versionStruktur")
3357 .or_insert_with(|| serde_json::Value::String("1".to_string()));
3358 }
3359 serde_json::Value::Array(items) => {
3360 for item in items {
3361 if let serde_json::Value::Object(map) = item {
3362 map.entry("boTyp")
3363 .or_insert_with(|| serde_json::Value::String(bo4e_type.to_uppercase()));
3364 map.entry("versionStruktur")
3365 .or_insert_with(|| serde_json::Value::String("1".to_string()));
3366 }
3367 }
3368 }
3369 _ => {}
3370 }
3371 value
3372}
3373
3374fn deep_merge_insert(
3380 result: &mut serde_json::Map<String, serde_json::Value>,
3381 entity: &str,
3382 bo4e: serde_json::Value,
3383) {
3384 if let Some(existing) = result.get_mut(entity) {
3385 if let (Some(existing_arr), Some(new_arr)) =
3388 (existing.as_array().map(|a| a.len()), bo4e.as_array())
3389 {
3390 if existing_arr == new_arr.len() {
3391 let existing_arr = existing.as_array_mut().unwrap();
3392 for (existing_elem, new_elem) in existing_arr.iter_mut().zip(new_arr) {
3393 if let (Some(existing_map), Some(new_map)) =
3394 (existing_elem.as_object_mut(), new_elem.as_object())
3395 {
3396 for (k, v) in new_map {
3397 if let Some(existing_v) = existing_map.get_mut(k) {
3398 if let (Some(existing_inner), Some(new_inner)) =
3399 (existing_v.as_object_mut(), v.as_object())
3400 {
3401 for (ik, iv) in new_inner {
3402 existing_inner
3403 .entry(ik.clone())
3404 .or_insert_with(|| iv.clone());
3405 }
3406 }
3407 } else {
3408 existing_map.insert(k.clone(), v.clone());
3409 }
3410 }
3411 }
3412 }
3413 return;
3414 }
3415 }
3416 if let (Some(existing_map), serde_json::Value::Object(new_map)) =
3418 (existing.as_object_mut(), &bo4e)
3419 {
3420 for (k, v) in new_map {
3421 if let Some(existing_v) = existing_map.get_mut(k) {
3422 if let (Some(existing_inner), Some(new_inner)) =
3424 (existing_v.as_object_mut(), v.as_object())
3425 {
3426 for (ik, iv) in new_inner {
3427 existing_inner
3428 .entry(ik.clone())
3429 .or_insert_with(|| iv.clone());
3430 }
3431 }
3432 } else {
3434 existing_map.insert(k.clone(), v.clone());
3435 }
3436 }
3437 return;
3438 }
3439 }
3440 result.insert(entity.to_string(), bo4e);
3441}
3442
3443fn is_map_keyed_object(value: &serde_json::Value) -> bool {
3454 let Some(obj) = value.as_object() else {
3455 return false;
3456 };
3457 if obj.is_empty() {
3458 return false;
3459 }
3460 obj.iter().all(|(k, v)| {
3462 k.len() <= 5
3463 && k.chars()
3464 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
3465 && v.is_object()
3466 })
3467}
3468
3469fn find_qualifier_companion_field(
3478 definitions: &[crate::definition::MappingDefinition],
3479 entity: &str,
3480) -> Option<String> {
3481 for def in definitions {
3482 if def.meta.entity != *entity {
3483 continue;
3484 }
3485 let disc = def.meta.discriminator.as_deref()?;
3486 let (disc_path, _) = disc.split_once('=')?;
3487 let disc_path_lower = disc_path.to_lowercase();
3488
3489 if let Some(ref cf) = def.companion_fields {
3490 for (path, mapping) in cf {
3491 let cf_path = path.to_lowercase();
3492 let matches = cf_path == disc_path_lower
3493 || format!("{}.0", cf_path) == disc_path_lower;
3494 if matches {
3495 let target = match mapping {
3496 FieldMapping::Simple(t) => t.as_str(),
3497 FieldMapping::Structured(s) => s.target.as_str(),
3498 FieldMapping::Nested(_) => continue,
3499 };
3500 if !target.is_empty() {
3501 return Some(target.to_string());
3502 }
3503 }
3504 }
3505 }
3506 }
3507 None
3508}
3509
3510fn to_camel_case(name: &str) -> String {
3511 let mut chars = name.chars();
3512 match chars.next() {
3513 Some(c) => c.to_lowercase().to_string() + chars.as_str(),
3514 None => String::new(),
3515 }
3516}
3517
3518fn set_nested_value(map: &mut serde_json::Map<String, serde_json::Value>, path: &str, val: String) {
3521 set_nested_value_json(map, path, serde_json::Value::String(val));
3522}
3523
3524fn set_nested_value_json(
3526 map: &mut serde_json::Map<String, serde_json::Value>,
3527 path: &str,
3528 val: serde_json::Value,
3529) {
3530 if let Some((prefix, leaf)) = path.rsplit_once('.') {
3531 let mut current = map;
3532 for part in prefix.split('.') {
3533 let entry = current
3534 .entry(part.to_string())
3535 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
3536 current = entry.as_object_mut().expect("expected object in path");
3537 }
3538 current.insert(leaf.to_string(), val);
3539 } else {
3540 map.insert(path.to_string(), val);
3541 }
3542}
3543
3544#[derive(serde::Serialize, serde::Deserialize)]
3549pub struct VariantCache {
3550 pub message_defs: Vec<MappingDefinition>,
3552 pub transaction_defs: HashMap<String, Vec<MappingDefinition>>,
3554 pub combined_defs: HashMap<String, Vec<MappingDefinition>>,
3556 #[serde(default)]
3558 pub code_lookups: HashMap<String, crate::code_lookup::CodeLookup>,
3559 #[serde(default)]
3561 pub mig_schema: Option<mig_types::schema::mig::MigSchema>,
3562 #[serde(default)]
3564 pub segment_structure: Option<crate::segment_structure::SegmentStructure>,
3565 #[serde(default)]
3568 pub pid_segment_numbers: HashMap<String, Vec<String>>,
3569 #[serde(default)]
3572 pub pid_requirements: HashMap<String, crate::pid_requirements::PidRequirements>,
3573 #[serde(default)]
3577 pub tx_groups: HashMap<String, String>,
3578}
3579
3580impl VariantCache {
3581 pub fn save(&self, path: &Path) -> Result<(), MappingError> {
3583 let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
3584 path: path.display().to_string(),
3585 message: e.to_string(),
3586 })?;
3587 if let Some(parent) = path.parent() {
3588 std::fs::create_dir_all(parent)?;
3589 }
3590 std::fs::write(path, encoded)?;
3591 Ok(())
3592 }
3593
3594 pub fn load(path: &Path) -> Result<Self, MappingError> {
3596 let bytes = std::fs::read(path)?;
3597 serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
3598 path: path.display().to_string(),
3599 message: e.to_string(),
3600 })
3601 }
3602
3603 pub fn tx_group(&self, pid: &str) -> Option<&str> {
3607 self.tx_groups
3608 .get(&format!("pid_{pid}"))
3609 .map(|s| s.as_str())
3610 }
3611
3612 pub fn msg_engine(&self) -> MappingEngine {
3614 MappingEngine::from_definitions(self.message_defs.clone())
3615 }
3616
3617 pub fn tx_engine(&self, pid: &str) -> Option<MappingEngine> {
3620 self.transaction_defs
3621 .get(&format!("pid_{pid}"))
3622 .map(|defs| MappingEngine::from_definitions(defs.clone()))
3623 }
3624
3625 pub fn filtered_mig(&self, pid: &str) -> Option<mig_types::schema::mig::MigSchema> {
3628 let mig = self.mig_schema.as_ref()?;
3629 let numbers = self.pid_segment_numbers.get(&format!("pid_{pid}"))?;
3630 let number_set: std::collections::HashSet<String> = numbers.iter().cloned().collect();
3631 Some(mig_assembly::pid_filter::filter_mig_for_pid(
3632 mig,
3633 &number_set,
3634 ))
3635 }
3636}
3637
3638#[derive(serde::Serialize, serde::Deserialize)]
3643pub struct DataBundle {
3644 pub format_version: String,
3645 pub bundle_version: u32,
3646 pub variants: HashMap<String, VariantCache>,
3647}
3648
3649impl DataBundle {
3650 pub const CURRENT_VERSION: u32 = 2;
3651
3652 pub fn variant(&self, name: &str) -> Option<&VariantCache> {
3653 self.variants.get(name)
3654 }
3655
3656 pub fn write_to<W: std::io::Write>(&self, writer: &mut W) -> Result<(), MappingError> {
3657 let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
3658 path: "<stream>".to_string(),
3659 message: e.to_string(),
3660 })?;
3661 writer.write_all(&encoded).map_err(MappingError::Io)
3662 }
3663
3664 pub fn read_from<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
3665 let mut bytes = Vec::new();
3666 reader.read_to_end(&mut bytes).map_err(MappingError::Io)?;
3667 serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
3668 path: "<stream>".to_string(),
3669 message: e.to_string(),
3670 })
3671 }
3672
3673 pub fn read_from_checked<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
3674 let bundle = Self::read_from(reader)?;
3675 if bundle.bundle_version != Self::CURRENT_VERSION {
3676 return Err(MappingError::CacheRead {
3677 path: "<stream>".to_string(),
3678 message: format!(
3679 "Incompatible bundle version {}, expected version {}. \
3680 Run `edifact-data update` to fetch compatible bundles.",
3681 bundle.bundle_version,
3682 Self::CURRENT_VERSION
3683 ),
3684 });
3685 }
3686 Ok(bundle)
3687 }
3688
3689 pub fn save(&self, path: &Path) -> Result<(), MappingError> {
3690 if let Some(parent) = path.parent() {
3691 std::fs::create_dir_all(parent)?;
3692 }
3693 let mut file = std::fs::File::create(path).map_err(MappingError::Io)?;
3694 self.write_to(&mut file)
3695 }
3696
3697 pub fn load(path: &Path) -> Result<Self, MappingError> {
3698 let mut file = std::fs::File::open(path).map_err(MappingError::Io)?;
3699 Self::read_from_checked(&mut file)
3700 }
3701}
3702
3703fn sort_variant_reps_by_mig(
3716 child_groups: &mut [AssembledGroup],
3717 mig: &MigSchema,
3718 transaction_group: &str,
3719) {
3720 let tx_def = match mig
3721 .segment_groups
3722 .iter()
3723 .find(|sg| sg.id == transaction_group)
3724 {
3725 Some(d) => d,
3726 None => return,
3727 };
3728
3729 for cg in child_groups.iter_mut() {
3730 if cg.repetitions.len() <= 1 {
3731 continue;
3732 }
3733
3734 let variant_defs: Vec<(usize, &mig_types::schema::mig::MigSegmentGroup)> = tx_def
3736 .nested_groups
3737 .iter()
3738 .enumerate()
3739 .filter(|(_, ng)| ng.id == cg.group_id && ng.variant_code.is_some())
3740 .collect();
3741
3742 if variant_defs.is_empty() {
3743 continue;
3744 }
3745
3746 cg.repetitions.sort_by_key(|rep| {
3749 let entry_seg = rep.segments.first();
3750 for &(mig_pos, variant_def) in &variant_defs {
3751 let (ei, ci) = variant_def.variant_qualifier_position.unwrap_or((0, 0));
3752 let actual_qual = entry_seg
3753 .and_then(|s| s.elements.get(ei))
3754 .and_then(|e| e.get(ci))
3755 .map(|s| s.as_str())
3756 .unwrap_or("");
3757 let matches = if !variant_def.variant_codes.is_empty() {
3758 variant_def
3759 .variant_codes
3760 .iter()
3761 .any(|c| actual_qual.eq_ignore_ascii_case(c))
3762 } else if let Some(ref expected_code) = variant_def.variant_code {
3763 actual_qual.eq_ignore_ascii_case(expected_code)
3764 } else {
3765 false
3766 };
3767 if matches {
3768 return mig_pos;
3769 }
3770 }
3771 usize::MAX });
3773 }
3774}
3775
3776#[cfg(test)]
3777mod variant_cache_helper_tests {
3778 use super::*;
3779
3780 fn make_test_cache() -> VariantCache {
3781 let mut tx_groups = HashMap::new();
3782 tx_groups.insert("pid_55001".to_string(), "SG4".to_string());
3783 tx_groups.insert("pid_21007".to_string(), "SG14".to_string());
3784
3785 let mut transaction_defs = HashMap::new();
3786 transaction_defs.insert("pid_55001".to_string(), vec![]);
3787 transaction_defs.insert("pid_21007".to_string(), vec![]);
3788
3789 VariantCache {
3790 message_defs: vec![],
3791 transaction_defs,
3792 combined_defs: HashMap::new(),
3793 code_lookups: HashMap::new(),
3794 mig_schema: None,
3795 segment_structure: None,
3796 pid_segment_numbers: HashMap::new(),
3797 pid_requirements: HashMap::new(),
3798 tx_groups,
3799 }
3800 }
3801
3802 #[test]
3803 fn test_tx_group_returns_correct_group() {
3804 let vc = make_test_cache();
3805 assert_eq!(vc.tx_group("55001").unwrap(), "SG4");
3806 assert_eq!(vc.tx_group("21007").unwrap(), "SG14");
3807 }
3808
3809 #[test]
3810 fn test_tx_group_unknown_pid_returns_none() {
3811 let vc = make_test_cache();
3812 assert!(vc.tx_group("99999").is_none());
3813 }
3814
3815 #[test]
3816 fn test_msg_engine_returns_engine() {
3817 let vc = make_test_cache();
3818 let engine = vc.msg_engine();
3819 assert_eq!(engine.definitions().len(), 0);
3820 }
3821
3822 #[test]
3823 fn test_tx_engine_returns_engine_for_known_pid() {
3824 let vc = make_test_cache();
3825 assert!(vc.tx_engine("55001").is_some());
3826 }
3827
3828 #[test]
3829 fn test_tx_engine_returns_none_for_unknown_pid() {
3830 let vc = make_test_cache();
3831 assert!(vc.tx_engine("99999").is_none());
3832 }
3833}
3834
3835#[cfg(test)]
3836mod tests {
3837 use super::*;
3838 use crate::definition::{MappingDefinition, MappingMeta, StructuredFieldMapping};
3839 use indexmap::IndexMap;
3840
3841 fn make_def(fields: IndexMap<String, FieldMapping>) -> MappingDefinition {
3842 MappingDefinition {
3843 meta: MappingMeta {
3844 entity: "Test".to_string(),
3845 bo4e_type: "Test".to_string(),
3846 companion_type: None,
3847 source_group: "SG4".to_string(),
3848 source_path: None,
3849 discriminator: None,
3850 repeat_on_tag: None,
3851 },
3852 fields,
3853 companion_fields: None,
3854 complex_handlers: None,
3855 }
3856 }
3857
3858 #[test]
3859 fn test_map_interchange_single_transaction_backward_compat() {
3860 use mig_assembly::assembler::*;
3861
3862 let tree = AssembledTree {
3864 segments: vec![
3865 AssembledSegment {
3866 tag: "UNH".to_string(),
3867 elements: vec![vec!["001".to_string()]],
3868 },
3869 AssembledSegment {
3870 tag: "BGM".to_string(),
3871 elements: vec![vec!["E01".to_string()], vec!["DOC001".to_string()]],
3872 },
3873 ],
3874 groups: vec![
3875 AssembledGroup {
3876 group_id: "SG2".to_string(),
3877 repetitions: vec![AssembledGroupInstance {
3878 segments: vec![AssembledSegment {
3879 tag: "NAD".to_string(),
3880 elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
3881 }],
3882 child_groups: vec![],
3883 skipped_segments: vec![],
3884 }],
3885 },
3886 AssembledGroup {
3887 group_id: "SG4".to_string(),
3888 repetitions: vec![AssembledGroupInstance {
3889 segments: vec![AssembledSegment {
3890 tag: "IDE".to_string(),
3891 elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
3892 }],
3893 child_groups: vec![AssembledGroup {
3894 group_id: "SG5".to_string(),
3895 repetitions: vec![AssembledGroupInstance {
3896 segments: vec![AssembledSegment {
3897 tag: "LOC".to_string(),
3898 elements: vec![
3899 vec!["Z16".to_string()],
3900 vec!["DE000111222333".to_string()],
3901 ],
3902 }],
3903 child_groups: vec![],
3904 skipped_segments: vec![],
3905 }],
3906 }],
3907 skipped_segments: vec![],
3908 }],
3909 },
3910 ],
3911 post_group_start: 2,
3912 inter_group_segments: std::collections::BTreeMap::new(),
3913 };
3914
3915 let msg_engine = MappingEngine::from_definitions(vec![]);
3917
3918 let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
3920 tx_fields.insert(
3921 "ide.1".to_string(),
3922 FieldMapping::Simple("vorgangId".to_string()),
3923 );
3924 let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
3925 malo_fields.insert(
3926 "loc.1".to_string(),
3927 FieldMapping::Simple("marktlokationsId".to_string()),
3928 );
3929
3930 let tx_engine = MappingEngine::from_definitions(vec![
3931 MappingDefinition {
3932 meta: MappingMeta {
3933 entity: "Prozessdaten".to_string(),
3934 bo4e_type: "Prozessdaten".to_string(),
3935 companion_type: None,
3936 source_group: "SG4".to_string(),
3937 source_path: None,
3938 discriminator: None,
3939 repeat_on_tag: None,
3940 },
3941 fields: tx_fields,
3942 companion_fields: None,
3943 complex_handlers: None,
3944 },
3945 MappingDefinition {
3946 meta: MappingMeta {
3947 entity: "Marktlokation".to_string(),
3948 bo4e_type: "Marktlokation".to_string(),
3949 companion_type: None,
3950 source_group: "SG4.SG5".to_string(),
3951 source_path: None,
3952 discriminator: None,
3953 repeat_on_tag: None,
3954 },
3955 fields: malo_fields,
3956 companion_fields: None,
3957 complex_handlers: None,
3958 },
3959 ]);
3960
3961 let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
3962
3963 assert_eq!(result.transaktionen.len(), 1);
3964 assert_eq!(
3965 result.transaktionen[0].stammdaten["prozessdaten"]["vorgangId"]
3966 .as_str()
3967 .unwrap(),
3968 "TX001"
3969 );
3970 assert_eq!(
3971 result.transaktionen[0].stammdaten["marktlokation"]["marktlokationsId"]
3972 .as_str()
3973 .unwrap(),
3974 "DE000111222333"
3975 );
3976 }
3977
3978 #[test]
3979 fn test_map_reverse_pads_intermediate_empty_elements() {
3980 let mut fields = IndexMap::new();
3982 fields.insert(
3983 "nad.0".to_string(),
3984 FieldMapping::Structured(StructuredFieldMapping {
3985 target: String::new(),
3986 transform: None,
3987 when: None,
3988 default: Some("Z09".to_string()),
3989 enum_map: None,
3990 when_filled: None,
3991 also_target: None,
3992 also_enum_map: None,
3993 }),
3994 );
3995 fields.insert(
3996 "nad.3.0".to_string(),
3997 FieldMapping::Simple("name".to_string()),
3998 );
3999 fields.insert(
4000 "nad.3.1".to_string(),
4001 FieldMapping::Simple("vorname".to_string()),
4002 );
4003
4004 let def = make_def(fields);
4005 let engine = MappingEngine::from_definitions(vec![]);
4006
4007 let bo4e = serde_json::json!({
4008 "name": "Muster",
4009 "vorname": "Max"
4010 });
4011
4012 let instance = engine.map_reverse(&bo4e, &def);
4013 assert_eq!(instance.segments.len(), 1);
4014
4015 let nad = &instance.segments[0];
4016 assert_eq!(nad.tag, "NAD");
4017 assert_eq!(nad.elements.len(), 4);
4018 assert_eq!(nad.elements[0], vec!["Z09"]);
4019 assert_eq!(nad.elements[1], vec![""]);
4021 assert_eq!(nad.elements[2], vec![""]);
4022 assert_eq!(nad.elements[3][0], "Muster");
4023 assert_eq!(nad.elements[3][1], "Max");
4024 }
4025
4026 #[test]
4027 fn test_map_reverse_no_padding_when_contiguous() {
4028 let mut fields = IndexMap::new();
4030 fields.insert(
4031 "dtm.0.0".to_string(),
4032 FieldMapping::Structured(StructuredFieldMapping {
4033 target: String::new(),
4034 transform: None,
4035 when: None,
4036 default: Some("92".to_string()),
4037 enum_map: None,
4038 when_filled: None,
4039 also_target: None,
4040 also_enum_map: None,
4041 }),
4042 );
4043 fields.insert(
4044 "dtm.0.1".to_string(),
4045 FieldMapping::Simple("value".to_string()),
4046 );
4047 fields.insert(
4048 "dtm.0.2".to_string(),
4049 FieldMapping::Structured(StructuredFieldMapping {
4050 target: String::new(),
4051 transform: None,
4052 when: None,
4053 default: Some("303".to_string()),
4054 enum_map: None,
4055 when_filled: None,
4056 also_target: None,
4057 also_enum_map: None,
4058 }),
4059 );
4060
4061 let def = make_def(fields);
4062 let engine = MappingEngine::from_definitions(vec![]);
4063
4064 let bo4e = serde_json::json!({ "value": "20250531" });
4065
4066 let instance = engine.map_reverse(&bo4e, &def);
4067 let dtm = &instance.segments[0];
4068 assert_eq!(dtm.elements.len(), 1);
4070 assert_eq!(dtm.elements[0], vec!["92", "20250531", "303"]);
4071 }
4072
4073 #[test]
4074 fn test_map_message_level_extracts_sg2_only() {
4075 use mig_assembly::assembler::*;
4076
4077 let tree = AssembledTree {
4079 segments: vec![
4080 AssembledSegment {
4081 tag: "UNH".to_string(),
4082 elements: vec![vec!["001".to_string()]],
4083 },
4084 AssembledSegment {
4085 tag: "BGM".to_string(),
4086 elements: vec![vec!["E01".to_string()]],
4087 },
4088 ],
4089 groups: vec![
4090 AssembledGroup {
4091 group_id: "SG2".to_string(),
4092 repetitions: vec![AssembledGroupInstance {
4093 segments: vec![AssembledSegment {
4094 tag: "NAD".to_string(),
4095 elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
4096 }],
4097 child_groups: vec![],
4098 skipped_segments: vec![],
4099 }],
4100 },
4101 AssembledGroup {
4102 group_id: "SG4".to_string(),
4103 repetitions: vec![AssembledGroupInstance {
4104 segments: vec![AssembledSegment {
4105 tag: "IDE".to_string(),
4106 elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
4107 }],
4108 child_groups: vec![],
4109 skipped_segments: vec![],
4110 }],
4111 },
4112 ],
4113 post_group_start: 2,
4114 inter_group_segments: std::collections::BTreeMap::new(),
4115 };
4116
4117 let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4119 msg_fields.insert(
4120 "nad.0".to_string(),
4121 FieldMapping::Simple("marktrolle".to_string()),
4122 );
4123 msg_fields.insert(
4124 "nad.1".to_string(),
4125 FieldMapping::Simple("rollencodenummer".to_string()),
4126 );
4127 let msg_def = MappingDefinition {
4128 meta: MappingMeta {
4129 entity: "Marktteilnehmer".to_string(),
4130 bo4e_type: "Marktteilnehmer".to_string(),
4131 companion_type: None,
4132 source_group: "SG2".to_string(),
4133 source_path: None,
4134 discriminator: None,
4135 repeat_on_tag: None,
4136 },
4137 fields: msg_fields,
4138 companion_fields: None,
4139 complex_handlers: None,
4140 };
4141
4142 let engine = MappingEngine::from_definitions(vec![msg_def.clone()]);
4143 let result = engine.map_all_forward(&tree);
4144
4145 assert!(result.get("marktteilnehmer").is_some());
4147 let mt = &result["marktteilnehmer"];
4148 assert_eq!(mt["marktrolle"].as_str().unwrap(), "MS");
4149 assert_eq!(mt["rollencodenummer"].as_str().unwrap(), "9900123");
4150 }
4151
4152 #[test]
4153 fn test_map_transaction_scoped_to_sg4_instance() {
4154 use mig_assembly::assembler::*;
4155
4156 let tree = AssembledTree {
4158 segments: vec![
4159 AssembledSegment {
4160 tag: "UNH".to_string(),
4161 elements: vec![vec!["001".to_string()]],
4162 },
4163 AssembledSegment {
4164 tag: "BGM".to_string(),
4165 elements: vec![vec!["E01".to_string()]],
4166 },
4167 ],
4168 groups: vec![AssembledGroup {
4169 group_id: "SG4".to_string(),
4170 repetitions: vec![AssembledGroupInstance {
4171 segments: vec![AssembledSegment {
4172 tag: "IDE".to_string(),
4173 elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
4174 }],
4175 child_groups: vec![AssembledGroup {
4176 group_id: "SG5".to_string(),
4177 repetitions: vec![AssembledGroupInstance {
4178 segments: vec![AssembledSegment {
4179 tag: "LOC".to_string(),
4180 elements: vec![
4181 vec!["Z16".to_string()],
4182 vec!["DE000111222333".to_string()],
4183 ],
4184 }],
4185 child_groups: vec![],
4186 skipped_segments: vec![],
4187 }],
4188 }],
4189 skipped_segments: vec![],
4190 }],
4191 }],
4192 post_group_start: 2,
4193 inter_group_segments: std::collections::BTreeMap::new(),
4194 };
4195
4196 let mut proz_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4198 proz_fields.insert(
4199 "ide.1".to_string(),
4200 FieldMapping::Simple("vorgangId".to_string()),
4201 );
4202 let proz_def = MappingDefinition {
4203 meta: MappingMeta {
4204 entity: "Prozessdaten".to_string(),
4205 bo4e_type: "Prozessdaten".to_string(),
4206 companion_type: None,
4207 source_group: "".to_string(), source_path: None,
4209 discriminator: None,
4210 repeat_on_tag: None,
4211 },
4212 fields: proz_fields,
4213 companion_fields: None,
4214 complex_handlers: None,
4215 };
4216
4217 let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4218 malo_fields.insert(
4219 "loc.1".to_string(),
4220 FieldMapping::Simple("marktlokationsId".to_string()),
4221 );
4222 let malo_def = MappingDefinition {
4223 meta: MappingMeta {
4224 entity: "Marktlokation".to_string(),
4225 bo4e_type: "Marktlokation".to_string(),
4226 companion_type: None,
4227 source_group: "SG5".to_string(), source_path: None,
4229 discriminator: None,
4230 repeat_on_tag: None,
4231 },
4232 fields: malo_fields,
4233 companion_fields: None,
4234 complex_handlers: None,
4235 };
4236
4237 let tx_engine = MappingEngine::from_definitions(vec![proz_def, malo_def]);
4238
4239 let sg4 = &tree.groups[0]; let sg4_instance = &sg4.repetitions[0];
4242 let sub_tree = sg4_instance.as_assembled_tree();
4243
4244 let result = tx_engine.map_all_forward(&sub_tree);
4245
4246 assert_eq!(
4248 result["prozessdaten"]["vorgangId"].as_str().unwrap(),
4249 "TX001"
4250 );
4251
4252 assert_eq!(
4254 result["marktlokation"]["marktlokationsId"]
4255 .as_str()
4256 .unwrap(),
4257 "DE000111222333"
4258 );
4259 }
4260
4261 #[test]
4262 fn test_map_interchange_produces_full_hierarchy() {
4263 use mig_assembly::assembler::*;
4264
4265 let tree = AssembledTree {
4267 segments: vec![
4268 AssembledSegment {
4269 tag: "UNH".to_string(),
4270 elements: vec![vec!["001".to_string()]],
4271 },
4272 AssembledSegment {
4273 tag: "BGM".to_string(),
4274 elements: vec![vec!["E01".to_string()]],
4275 },
4276 ],
4277 groups: vec![
4278 AssembledGroup {
4279 group_id: "SG2".to_string(),
4280 repetitions: vec![AssembledGroupInstance {
4281 segments: vec![AssembledSegment {
4282 tag: "NAD".to_string(),
4283 elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
4284 }],
4285 child_groups: vec![],
4286 skipped_segments: vec![],
4287 }],
4288 },
4289 AssembledGroup {
4290 group_id: "SG4".to_string(),
4291 repetitions: vec![
4292 AssembledGroupInstance {
4293 segments: vec![AssembledSegment {
4294 tag: "IDE".to_string(),
4295 elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
4296 }],
4297 child_groups: vec![],
4298 skipped_segments: vec![],
4299 },
4300 AssembledGroupInstance {
4301 segments: vec![AssembledSegment {
4302 tag: "IDE".to_string(),
4303 elements: vec![vec!["24".to_string()], vec!["TX002".to_string()]],
4304 }],
4305 child_groups: vec![],
4306 skipped_segments: vec![],
4307 },
4308 ],
4309 },
4310 ],
4311 post_group_start: 2,
4312 inter_group_segments: std::collections::BTreeMap::new(),
4313 };
4314
4315 let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4317 msg_fields.insert(
4318 "nad.0".to_string(),
4319 FieldMapping::Simple("marktrolle".to_string()),
4320 );
4321 let msg_defs = vec![MappingDefinition {
4322 meta: MappingMeta {
4323 entity: "Marktteilnehmer".to_string(),
4324 bo4e_type: "Marktteilnehmer".to_string(),
4325 companion_type: None,
4326 source_group: "SG2".to_string(),
4327 source_path: None,
4328 discriminator: None,
4329 repeat_on_tag: None,
4330 },
4331 fields: msg_fields,
4332 companion_fields: None,
4333 complex_handlers: None,
4334 }];
4335
4336 let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4338 tx_fields.insert(
4339 "ide.1".to_string(),
4340 FieldMapping::Simple("vorgangId".to_string()),
4341 );
4342 let tx_defs = vec![MappingDefinition {
4343 meta: MappingMeta {
4344 entity: "Prozessdaten".to_string(),
4345 bo4e_type: "Prozessdaten".to_string(),
4346 companion_type: None,
4347 source_group: "SG4".to_string(),
4348 source_path: None,
4349 discriminator: None,
4350 repeat_on_tag: None,
4351 },
4352 fields: tx_fields,
4353 companion_fields: None,
4354 complex_handlers: None,
4355 }];
4356
4357 let msg_engine = MappingEngine::from_definitions(msg_defs);
4358 let tx_engine = MappingEngine::from_definitions(tx_defs);
4359
4360 let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
4361
4362 assert!(result.stammdaten["marktteilnehmer"].is_object());
4364 assert_eq!(
4365 result.stammdaten["marktteilnehmer"]["marktrolle"]
4366 .as_str()
4367 .unwrap(),
4368 "MS"
4369 );
4370
4371 assert_eq!(result.transaktionen.len(), 2);
4373 assert_eq!(
4374 result.transaktionen[0].stammdaten["prozessdaten"]["vorgangId"]
4375 .as_str()
4376 .unwrap(),
4377 "TX001"
4378 );
4379 assert_eq!(
4380 result.transaktionen[1].stammdaten["prozessdaten"]["vorgangId"]
4381 .as_str()
4382 .unwrap(),
4383 "TX002"
4384 );
4385 }
4386
4387 #[test]
4388 fn test_map_reverse_with_segment_structure_pads_trailing() {
4389 let mut fields = IndexMap::new();
4391 fields.insert(
4392 "sts.0".to_string(),
4393 FieldMapping::Structured(StructuredFieldMapping {
4394 target: String::new(),
4395 transform: None,
4396 when: None,
4397 default: Some("7".to_string()),
4398 enum_map: None,
4399 when_filled: None,
4400 also_target: None,
4401 also_enum_map: None,
4402 }),
4403 );
4404 fields.insert(
4405 "sts.2".to_string(),
4406 FieldMapping::Simple("grund".to_string()),
4407 );
4408
4409 let def = make_def(fields);
4410
4411 let mut counts = std::collections::HashMap::new();
4413 counts.insert("STS".to_string(), 5usize);
4414 let ss = SegmentStructure {
4415 element_counts: counts,
4416 };
4417
4418 let engine = MappingEngine::from_definitions(vec![]).with_segment_structure(ss);
4419
4420 let bo4e = serde_json::json!({ "grund": "E01" });
4421
4422 let instance = engine.map_reverse(&bo4e, &def);
4423 let sts = &instance.segments[0];
4424 assert_eq!(sts.elements.len(), 5);
4427 assert_eq!(sts.elements[0], vec!["7"]);
4428 assert_eq!(sts.elements[1], vec![""]);
4429 assert_eq!(sts.elements[2], vec!["E01"]);
4430 assert_eq!(sts.elements[3], vec![""]);
4431 assert_eq!(sts.elements[4], vec![""]);
4432 }
4433
4434 #[test]
4435 fn test_extract_companion_fields_with_code_enrichment() {
4436 use crate::code_lookup::CodeLookup;
4437 use mig_assembly::assembler::*;
4438
4439 let schema = serde_json::json!({
4440 "fields": {
4441 "sg4": {
4442 "children": {
4443 "sg8_z01": {
4444 "children": {
4445 "sg10": {
4446 "segments": [{
4447 "id": "CCI",
4448 "elements": [{
4449 "index": 2,
4450 "components": [{
4451 "sub_index": 0,
4452 "type": "code",
4453 "codes": [
4454 {"value": "Z15", "name": "Haushaltskunde"},
4455 {"value": "Z18", "name": "Kein Haushaltskunde"}
4456 ]
4457 }]
4458 }]
4459 }],
4460 "source_group": "SG10"
4461 }
4462 },
4463 "segments": [],
4464 "source_group": "SG8"
4465 }
4466 },
4467 "segments": [],
4468 "source_group": "SG4"
4469 }
4470 }
4471 });
4472
4473 let code_lookup = CodeLookup::from_schema_value(&schema);
4474
4475 let tree = AssembledTree {
4476 segments: vec![],
4477 groups: vec![AssembledGroup {
4478 group_id: "SG4".to_string(),
4479 repetitions: vec![AssembledGroupInstance {
4480 segments: vec![],
4481 child_groups: vec![AssembledGroup {
4482 group_id: "SG8".to_string(),
4483 repetitions: vec![AssembledGroupInstance {
4484 segments: vec![],
4485 child_groups: vec![AssembledGroup {
4486 group_id: "SG10".to_string(),
4487 repetitions: vec![AssembledGroupInstance {
4488 segments: vec![AssembledSegment {
4489 tag: "CCI".to_string(),
4490 elements: vec![vec![], vec![], vec!["Z15".to_string()]],
4491 }],
4492 child_groups: vec![],
4493 skipped_segments: vec![],
4494 }],
4495 }],
4496 skipped_segments: vec![],
4497 }],
4498 }],
4499 skipped_segments: vec![],
4500 }],
4501 }],
4502 post_group_start: 0,
4503 inter_group_segments: std::collections::BTreeMap::new(),
4504 };
4505
4506 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4507 companion_fields.insert(
4508 "cci.2".to_string(),
4509 FieldMapping::Simple("haushaltskunde".to_string()),
4510 );
4511
4512 let def = MappingDefinition {
4513 meta: MappingMeta {
4514 entity: "Marktlokation".to_string(),
4515 bo4e_type: "Marktlokation".to_string(),
4516 companion_type: Some("MarktlokationEdifact".to_string()),
4517 source_group: "SG4.SG8.SG10".to_string(),
4518 source_path: Some("sg4.sg8_z01.sg10".to_string()),
4519 discriminator: None,
4520 repeat_on_tag: None,
4521 },
4522 fields: IndexMap::new(),
4523 companion_fields: Some(companion_fields),
4524 complex_handlers: None,
4525 };
4526
4527 let engine_plain = MappingEngine::from_definitions(vec![]);
4529 let bo4e_plain = engine_plain.map_forward(&tree, &def, 0);
4530 assert_eq!(
4531 bo4e_plain["marktlokationEdifact"]["haushaltskunde"].as_str(),
4532 Some("Z15"),
4533 "Without code lookup, should be plain string"
4534 );
4535
4536 let engine_enriched = MappingEngine::from_definitions(vec![]).with_code_lookup(code_lookup);
4538 let bo4e_enriched = engine_enriched.map_forward(&tree, &def, 0);
4539 let hk = &bo4e_enriched["marktlokationEdifact"]["haushaltskunde"];
4540 assert_eq!(hk["code"].as_str(), Some("Z15"));
4541 assert_eq!(hk["meaning"].as_str(), Some("Haushaltskunde"));
4542 assert!(hk.get("enum").is_none());
4544 }
4545
4546 #[test]
4547 fn test_extract_companion_fields_with_enum_enrichment() {
4548 use crate::code_lookup::CodeLookup;
4549 use mig_assembly::assembler::*;
4550
4551 let schema = serde_json::json!({
4553 "fields": {
4554 "sg4": {
4555 "children": {
4556 "sg8_z01": {
4557 "children": {
4558 "sg10": {
4559 "segments": [{
4560 "id": "CCI",
4561 "elements": [{
4562 "index": 2,
4563 "components": [{
4564 "sub_index": 0,
4565 "type": "code",
4566 "codes": [
4567 {"value": "Z15", "name": "Haushaltskunde", "enum": "HAUSHALTSKUNDE"},
4568 {"value": "Z18", "name": "Kein Haushaltskunde", "enum": "KEIN_HAUSHALTSKUNDE"}
4569 ]
4570 }]
4571 }]
4572 }],
4573 "source_group": "SG10"
4574 }
4575 },
4576 "segments": [],
4577 "source_group": "SG8"
4578 }
4579 },
4580 "segments": [],
4581 "source_group": "SG4"
4582 }
4583 }
4584 });
4585
4586 let code_lookup = CodeLookup::from_schema_value(&schema);
4587
4588 let tree = AssembledTree {
4589 segments: vec![],
4590 groups: vec![AssembledGroup {
4591 group_id: "SG4".to_string(),
4592 repetitions: vec![AssembledGroupInstance {
4593 segments: vec![],
4594 child_groups: vec![AssembledGroup {
4595 group_id: "SG8".to_string(),
4596 repetitions: vec![AssembledGroupInstance {
4597 segments: vec![],
4598 child_groups: vec![AssembledGroup {
4599 group_id: "SG10".to_string(),
4600 repetitions: vec![AssembledGroupInstance {
4601 segments: vec![AssembledSegment {
4602 tag: "CCI".to_string(),
4603 elements: vec![vec![], vec![], vec!["Z15".to_string()]],
4604 }],
4605 child_groups: vec![],
4606 skipped_segments: vec![],
4607 }],
4608 }],
4609 skipped_segments: vec![],
4610 }],
4611 }],
4612 skipped_segments: vec![],
4613 }],
4614 }],
4615 post_group_start: 0,
4616 inter_group_segments: std::collections::BTreeMap::new(),
4617 };
4618
4619 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4620 companion_fields.insert(
4621 "cci.2".to_string(),
4622 FieldMapping::Simple("haushaltskunde".to_string()),
4623 );
4624
4625 let def = MappingDefinition {
4626 meta: MappingMeta {
4627 entity: "Marktlokation".to_string(),
4628 bo4e_type: "Marktlokation".to_string(),
4629 companion_type: Some("MarktlokationEdifact".to_string()),
4630 source_group: "SG4.SG8.SG10".to_string(),
4631 source_path: Some("sg4.sg8_z01.sg10".to_string()),
4632 discriminator: None,
4633 repeat_on_tag: None,
4634 },
4635 fields: IndexMap::new(),
4636 companion_fields: Some(companion_fields),
4637 complex_handlers: None,
4638 };
4639
4640 let engine = MappingEngine::from_definitions(vec![]).with_code_lookup(code_lookup);
4641 let bo4e = engine.map_forward(&tree, &def, 0);
4642 let hk = &bo4e["marktlokationEdifact"]["haushaltskunde"];
4643 assert_eq!(hk["code"].as_str(), Some("Z15"));
4644 assert_eq!(hk["meaning"].as_str(), Some("Haushaltskunde"));
4645 assert_eq!(
4646 hk["enum"].as_str(),
4647 Some("HAUSHALTSKUNDE"),
4648 "enum field should be present"
4649 );
4650 }
4651
4652 #[test]
4653 fn test_reverse_mapping_accepts_enriched_with_enum() {
4654 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4656 companion_fields.insert(
4657 "cci.2".to_string(),
4658 FieldMapping::Simple("haushaltskunde".to_string()),
4659 );
4660
4661 let def = MappingDefinition {
4662 meta: MappingMeta {
4663 entity: "Test".to_string(),
4664 bo4e_type: "Test".to_string(),
4665 companion_type: Some("TestEdifact".to_string()),
4666 source_group: "SG4".to_string(),
4667 source_path: None,
4668 discriminator: None,
4669 repeat_on_tag: None,
4670 },
4671 fields: IndexMap::new(),
4672 companion_fields: Some(companion_fields),
4673 complex_handlers: None,
4674 };
4675
4676 let engine = MappingEngine::from_definitions(vec![]);
4677
4678 let bo4e = serde_json::json!({
4679 "testEdifact": {
4680 "haushaltskunde": {
4681 "code": "Z15",
4682 "meaning": "Haushaltskunde",
4683 "enum": "HAUSHALTSKUNDE"
4684 }
4685 }
4686 });
4687 let instance = engine.map_reverse(&bo4e, &def);
4688 assert_eq!(instance.segments[0].elements[2], vec!["Z15"]);
4689 }
4690
4691 #[test]
4692 fn test_reverse_mapping_accepts_enriched_companion() {
4693 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4695 companion_fields.insert(
4696 "cci.2".to_string(),
4697 FieldMapping::Simple("haushaltskunde".to_string()),
4698 );
4699
4700 let def = MappingDefinition {
4701 meta: MappingMeta {
4702 entity: "Test".to_string(),
4703 bo4e_type: "Test".to_string(),
4704 companion_type: Some("TestEdifact".to_string()),
4705 source_group: "SG4".to_string(),
4706 source_path: None,
4707 discriminator: None,
4708 repeat_on_tag: None,
4709 },
4710 fields: IndexMap::new(),
4711 companion_fields: Some(companion_fields),
4712 complex_handlers: None,
4713 };
4714
4715 let engine = MappingEngine::from_definitions(vec![]);
4716
4717 let bo4e_plain = serde_json::json!({
4719 "testEdifact": {
4720 "haushaltskunde": "Z15"
4721 }
4722 });
4723 let instance_plain = engine.map_reverse(&bo4e_plain, &def);
4724 assert_eq!(instance_plain.segments[0].elements[2], vec!["Z15"]);
4725
4726 let bo4e_enriched = serde_json::json!({
4728 "testEdifact": {
4729 "haushaltskunde": {
4730 "code": "Z15",
4731 "meaning": "Haushaltskunde gem. EnWG"
4732 }
4733 }
4734 });
4735 let instance_enriched = engine.map_reverse(&bo4e_enriched, &def);
4736 assert_eq!(instance_enriched.segments[0].elements[2], vec!["Z15"]);
4737 }
4738
4739 #[test]
4740 fn test_resolve_child_relative_with_source_path() {
4741 let mut map: std::collections::HashMap<String, Vec<usize>> =
4742 std::collections::HashMap::new();
4743 map.insert("sg4.sg8_ze1".to_string(), vec![6]);
4744 map.insert("sg4.sg8_z98".to_string(), vec![0]);
4745
4746 assert_eq!(
4748 resolve_child_relative("SG8.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
4749 "SG8:6.SG10"
4750 );
4751
4752 assert_eq!(
4754 resolve_child_relative("SG8:3.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
4755 "SG8:3.SG10"
4756 );
4757
4758 assert_eq!(
4760 resolve_child_relative("SG8.SG10", Some("sg4.sg8_unknown.sg10"), &map, 0),
4761 "SG8.SG10"
4762 );
4763
4764 assert_eq!(
4766 resolve_child_relative("SG8.SG10", None, &map, 0),
4767 "SG8.SG10"
4768 );
4769
4770 assert_eq!(
4772 resolve_child_relative("SG8.SG9", Some("sg4.sg8_z98.sg9"), &map, 0),
4773 "SG8:0.SG9"
4774 );
4775
4776 map.insert("sg4.sg8_zf3".to_string(), vec![3, 4]);
4778 assert_eq!(
4779 resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 0),
4780 "SG8:3.SG10"
4781 );
4782 assert_eq!(
4783 resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 1),
4784 "SG8:4.SG10"
4785 );
4786 }
4787
4788 #[test]
4789 fn test_place_in_groups_returns_rep_index() {
4790 let mut groups: Vec<AssembledGroup> = Vec::new();
4791
4792 let instance = AssembledGroupInstance {
4794 segments: vec![],
4795 child_groups: vec![],
4796 skipped_segments: vec![],
4797 };
4798 assert_eq!(place_in_groups(&mut groups, "SG8", instance), 0);
4799
4800 let instance = AssembledGroupInstance {
4802 segments: vec![],
4803 child_groups: vec![],
4804 skipped_segments: vec![],
4805 };
4806 assert_eq!(place_in_groups(&mut groups, "SG8", instance), 1);
4807
4808 let instance = AssembledGroupInstance {
4810 segments: vec![],
4811 child_groups: vec![],
4812 skipped_segments: vec![],
4813 };
4814 assert_eq!(place_in_groups(&mut groups, "SG8:5", instance), 5);
4815 }
4816
4817 #[test]
4818 fn test_resolve_by_source_path() {
4819 use mig_assembly::assembler::*;
4820
4821 let tree = AssembledTree {
4823 segments: vec![],
4824 groups: vec![AssembledGroup {
4825 group_id: "SG4".to_string(),
4826 repetitions: vec![AssembledGroupInstance {
4827 segments: vec![],
4828 child_groups: vec![AssembledGroup {
4829 group_id: "SG8".to_string(),
4830 repetitions: vec![
4831 AssembledGroupInstance {
4832 segments: vec![AssembledSegment {
4833 tag: "SEQ".to_string(),
4834 elements: vec![vec!["Z98".to_string()]],
4835 }],
4836 child_groups: vec![AssembledGroup {
4837 group_id: "SG10".to_string(),
4838 repetitions: vec![AssembledGroupInstance {
4839 segments: vec![AssembledSegment {
4840 tag: "CCI".to_string(),
4841 elements: vec![vec![], vec![], vec!["ZB3".to_string()]],
4842 }],
4843 child_groups: vec![],
4844 skipped_segments: vec![],
4845 }],
4846 }],
4847 skipped_segments: vec![],
4848 },
4849 AssembledGroupInstance {
4850 segments: vec![AssembledSegment {
4851 tag: "SEQ".to_string(),
4852 elements: vec![vec!["ZD7".to_string()]],
4853 }],
4854 child_groups: vec![AssembledGroup {
4855 group_id: "SG10".to_string(),
4856 repetitions: vec![AssembledGroupInstance {
4857 segments: vec![AssembledSegment {
4858 tag: "CCI".to_string(),
4859 elements: vec![vec![], vec![], vec!["ZE6".to_string()]],
4860 }],
4861 child_groups: vec![],
4862 skipped_segments: vec![],
4863 }],
4864 }],
4865 skipped_segments: vec![],
4866 },
4867 ],
4868 }],
4869 skipped_segments: vec![],
4870 }],
4871 }],
4872 post_group_start: 0,
4873 inter_group_segments: std::collections::BTreeMap::new(),
4874 };
4875
4876 let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_z98.sg10");
4878 assert!(inst.is_some());
4879 assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
4880
4881 let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zd7.sg10");
4883 assert!(inst.is_some());
4884 assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZE6");
4885
4886 let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zzz.sg10");
4888 assert!(inst.is_none());
4889
4890 let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8.sg10");
4892 assert!(inst.is_some());
4893 assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
4894 }
4895
4896 #[test]
4897 fn test_parse_source_path_part() {
4898 assert_eq!(parse_source_path_part("sg4"), ("sg4", None));
4899 assert_eq!(parse_source_path_part("sg8_z98"), ("sg8", Some("z98")));
4900 assert_eq!(parse_source_path_part("sg10"), ("sg10", None));
4901 assert_eq!(parse_source_path_part("sg12_z04"), ("sg12", Some("z04")));
4902 }
4903
4904 #[test]
4905 fn test_has_source_path_qualifiers() {
4906 assert!(has_source_path_qualifiers("sg4.sg8_z98.sg10"));
4907 assert!(has_source_path_qualifiers("sg4.sg8_ze1.sg9"));
4908 assert!(!has_source_path_qualifiers("sg4.sg6"));
4909 assert!(!has_source_path_qualifiers("sg4.sg8.sg10"));
4910 }
4911
4912 #[test]
4913 fn test_companion_dotted_path_forward() {
4914 use mig_assembly::assembler::*;
4915
4916 let tree = AssembledTree {
4918 segments: vec![],
4919 groups: vec![AssembledGroup {
4920 group_id: "SG4".to_string(),
4921 repetitions: vec![AssembledGroupInstance {
4922 segments: vec![],
4923 child_groups: vec![AssembledGroup {
4924 group_id: "SG8".to_string(),
4925 repetitions: vec![AssembledGroupInstance {
4926 segments: vec![],
4927 child_groups: vec![AssembledGroup {
4928 group_id: "SG10".to_string(),
4929 repetitions: vec![AssembledGroupInstance {
4930 segments: vec![AssembledSegment {
4931 tag: "CCI".to_string(),
4932 elements: vec![
4933 vec!["11XAB-1234".to_string()],
4934 vec!["305".to_string()],
4935 ],
4936 }],
4937 child_groups: vec![],
4938 skipped_segments: vec![],
4939 }],
4940 }],
4941 skipped_segments: vec![],
4942 }],
4943 }],
4944 skipped_segments: vec![],
4945 }],
4946 }],
4947 post_group_start: 0,
4948 inter_group_segments: std::collections::BTreeMap::new(),
4949 };
4950
4951 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4953 companion_fields.insert(
4954 "cci.0".to_string(),
4955 FieldMapping::Simple("bilanzkreis.id".to_string()),
4956 );
4957 companion_fields.insert(
4958 "cci.1".to_string(),
4959 FieldMapping::Simple("bilanzkreis.codelist".to_string()),
4960 );
4961
4962 let def = MappingDefinition {
4963 meta: MappingMeta {
4964 entity: "Test".to_string(),
4965 bo4e_type: "Test".to_string(),
4966 companion_type: Some("TestEdifact".to_string()),
4967 source_group: "SG4.SG8.SG10".to_string(),
4968 source_path: Some("sg4.sg8_z01.sg10".to_string()),
4969 discriminator: None,
4970 repeat_on_tag: None,
4971 },
4972 fields: IndexMap::new(),
4973 companion_fields: Some(companion_fields),
4974 complex_handlers: None,
4975 };
4976
4977 let engine = MappingEngine::from_definitions(vec![]);
4978 let bo4e = engine.map_forward(&tree, &def, 0);
4979
4980 let companion = &bo4e["testEdifact"];
4982 assert!(
4983 companion.is_object(),
4984 "testEdifact should be an object, got: {companion}"
4985 );
4986 let bilanzkreis = &companion["bilanzkreis"];
4987 assert!(
4988 bilanzkreis.is_object(),
4989 "bilanzkreis should be a nested object, got: {bilanzkreis}"
4990 );
4991 assert_eq!(
4992 bilanzkreis["id"].as_str(),
4993 Some("11XAB-1234"),
4994 "bilanzkreis.id should be 11XAB-1234"
4995 );
4996 assert_eq!(
4997 bilanzkreis["codelist"].as_str(),
4998 Some("305"),
4999 "bilanzkreis.codelist should be 305"
5000 );
5001 }
5002
5003 #[test]
5004 fn test_companion_dotted_path_reverse() {
5005 let engine = MappingEngine::from_definitions(vec![]);
5007
5008 let companion_value = serde_json::json!({
5009 "bilanzkreis": {
5010 "id": "11XAB-1234",
5011 "codelist": "305"
5012 }
5013 });
5014
5015 assert_eq!(
5016 engine.populate_field(&companion_value, "bilanzkreis.id"),
5017 Some("11XAB-1234".to_string()),
5018 "dotted path bilanzkreis.id should resolve"
5019 );
5020 assert_eq!(
5021 engine.populate_field(&companion_value, "bilanzkreis.codelist"),
5022 Some("305".to_string()),
5023 "dotted path bilanzkreis.codelist should resolve"
5024 );
5025
5026 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
5028 companion_fields.insert(
5029 "cci.0".to_string(),
5030 FieldMapping::Simple("bilanzkreis.id".to_string()),
5031 );
5032 companion_fields.insert(
5033 "cci.1".to_string(),
5034 FieldMapping::Simple("bilanzkreis.codelist".to_string()),
5035 );
5036
5037 let def = MappingDefinition {
5038 meta: MappingMeta {
5039 entity: "Test".to_string(),
5040 bo4e_type: "Test".to_string(),
5041 companion_type: Some("TestEdifact".to_string()),
5042 source_group: "SG4.SG8.SG10".to_string(),
5043 source_path: Some("sg4.sg8_z01.sg10".to_string()),
5044 discriminator: None,
5045 repeat_on_tag: None,
5046 },
5047 fields: IndexMap::new(),
5048 companion_fields: Some(companion_fields),
5049 complex_handlers: None,
5050 };
5051
5052 let bo4e = serde_json::json!({
5053 "testEdifact": {
5054 "bilanzkreis": {
5055 "id": "11XAB-1234",
5056 "codelist": "305"
5057 }
5058 }
5059 });
5060
5061 let instance = engine.map_reverse(&bo4e, &def);
5062 assert_eq!(instance.segments.len(), 1, "should produce one CCI segment");
5063 let cci = &instance.segments[0];
5064 assert_eq!(cci.tag, "CCI");
5065 assert_eq!(
5066 cci.elements[0],
5067 vec!["11XAB-1234"],
5068 "element 0 should contain bilanzkreis.id"
5069 );
5070 assert_eq!(
5071 cci.elements[1],
5072 vec!["305"],
5073 "element 1 should contain bilanzkreis.codelist"
5074 );
5075 }
5076
5077 #[test]
5078 fn test_when_filled_injects_when_field_present() {
5079 let toml_str = r#"
5080[meta]
5081entity = "Test"
5082bo4e_type = "Test"
5083companion_type = "TestEdifact"
5084source_group = "SG4.SG8.SG10"
5085
5086[fields]
5087
5088[companion_fields]
5089"cci.0.0" = { target = "", default = "Z83", when_filled = ["merkmalCode"] }
5090"cav.0.0" = "merkmalCode"
5091"#;
5092 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5093
5094 let bo4e_with = serde_json::json!({
5096 "testEdifact": { "merkmalCode": "ZA7" }
5097 });
5098 let engine = MappingEngine::new_empty();
5099 let instance = engine.map_reverse(&bo4e_with, &def);
5100 let cci = instance
5101 .segments
5102 .iter()
5103 .find(|s| s.tag == "CCI")
5104 .expect("CCI should exist");
5105 assert_eq!(cci.elements[0][0], "Z83");
5106
5107 let bo4e_without = serde_json::json!({
5109 "testEdifact": {}
5110 });
5111 let instance2 = engine.map_reverse(&bo4e_without, &def);
5112 let cci2 = instance2.segments.iter().find(|s| s.tag == "CCI");
5113 assert!(
5114 cci2.is_none(),
5115 "CCI should not be emitted when merkmalCode is absent"
5116 );
5117 }
5118
5119 #[test]
5120 fn test_when_filled_checks_core_and_companion() {
5121 let toml_str = r#"
5122[meta]
5123entity = "Test"
5124bo4e_type = "Test"
5125companion_type = "TestEdifact"
5126source_group = "SG4.SG5"
5127
5128[fields]
5129"loc.1.0" = "marktlokationsId"
5130
5131[companion_fields]
5132"loc.0.0" = { target = "", default = "Z16", when_filled = ["marktlokationsId"] }
5133"#;
5134 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5135
5136 let bo4e_with = serde_json::json!({
5138 "marktlokationsId": "51234567890"
5139 });
5140 let engine = MappingEngine::new_empty();
5141 let instance = engine.map_reverse(&bo4e_with, &def);
5142 let loc = instance
5143 .segments
5144 .iter()
5145 .find(|s| s.tag == "LOC")
5146 .expect("LOC should exist");
5147 assert_eq!(loc.elements[0][0], "Z16");
5148 assert_eq!(loc.elements[1][0], "51234567890");
5149
5150 let bo4e_without = serde_json::json!({});
5152 let instance2 = engine.map_reverse(&bo4e_without, &def);
5153 let loc2 = instance2.segments.iter().find(|s| s.tag == "LOC");
5154 assert!(loc2.is_none());
5155 }
5156
5157 #[test]
5158 fn test_extract_all_from_instance_collects_all_qualifier_matches() {
5159 use mig_assembly::assembler::*;
5160
5161 let instance = AssembledGroupInstance {
5163 segments: vec![
5164 AssembledSegment {
5165 tag: "SEQ".to_string(),
5166 elements: vec![vec!["ZD6".to_string()]],
5167 },
5168 AssembledSegment {
5169 tag: "RFF".to_string(),
5170 elements: vec![vec!["Z34".to_string(), "REF_A".to_string()]],
5171 },
5172 AssembledSegment {
5173 tag: "RFF".to_string(),
5174 elements: vec![vec!["Z34".to_string(), "REF_B".to_string()]],
5175 },
5176 AssembledSegment {
5177 tag: "RFF".to_string(),
5178 elements: vec![vec!["Z34".to_string(), "REF_C".to_string()]],
5179 },
5180 AssembledSegment {
5181 tag: "RFF".to_string(),
5182 elements: vec![vec!["Z35".to_string(), "OTHER".to_string()]],
5183 },
5184 ],
5185 child_groups: vec![],
5186 skipped_segments: vec![],
5187 };
5188
5189 let all = MappingEngine::extract_all_from_instance(&instance, "rff[Z34,*].0.1");
5191 assert_eq!(all, vec!["REF_A", "REF_B", "REF_C"]);
5192
5193 let single = MappingEngine::extract_from_instance(&instance, "rff[Z34].0.1");
5195 assert_eq!(single, Some("REF_A".to_string()));
5196
5197 let second = MappingEngine::extract_from_instance(&instance, "rff[Z34,1].0.1");
5198 assert_eq!(second, Some("REF_B".to_string()));
5199 }
5200
5201 #[test]
5202 fn test_forward_wildcard_collect_produces_json_array() {
5203 use mig_assembly::assembler::*;
5204
5205 let instance = AssembledGroupInstance {
5206 segments: vec![
5207 AssembledSegment {
5208 tag: "SEQ".to_string(),
5209 elements: vec![vec!["ZD6".to_string()]],
5210 },
5211 AssembledSegment {
5212 tag: "RFF".to_string(),
5213 elements: vec![vec!["Z34".to_string(), "REF_A".to_string()]],
5214 },
5215 AssembledSegment {
5216 tag: "RFF".to_string(),
5217 elements: vec![vec!["Z34".to_string(), "REF_B".to_string()]],
5218 },
5219 ],
5220 child_groups: vec![],
5221 skipped_segments: vec![],
5222 };
5223
5224 let toml_str = r#"
5225[meta]
5226entity = "Test"
5227bo4e_type = "Test"
5228companion_type = "TestEdifact"
5229source_group = "SG4.SG8"
5230
5231[fields]
5232
5233[companion_fields]
5234"rff[Z34,*].0.1" = "messlokationsIdRefs"
5235"#;
5236 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5237 let engine = MappingEngine::new_empty();
5238
5239 let mut result = serde_json::Map::new();
5240 engine.extract_companion_fields(&instance, &def, &mut result, false);
5241
5242 let companion = result.get("testEdifact").unwrap().as_object().unwrap();
5243 let refs = companion
5244 .get("messlokationsIdRefs")
5245 .unwrap()
5246 .as_array()
5247 .unwrap();
5248 assert_eq!(refs.len(), 2);
5249 assert_eq!(refs[0].as_str().unwrap(), "REF_A");
5250 assert_eq!(refs[1].as_str().unwrap(), "REF_B");
5251 }
5252
5253 #[test]
5254 fn test_reverse_json_array_produces_multiple_segments() {
5255 let toml_str = r#"
5256[meta]
5257entity = "Test"
5258bo4e_type = "Test"
5259companion_type = "TestEdifact"
5260source_group = "SG4.SG8"
5261
5262[fields]
5263
5264[companion_fields]
5265"seq.0.0" = { target = "", default = "ZD6" }
5266"rff[Z34,*].0.1" = "messlokationsIdRefs"
5267"#;
5268 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5269 let engine = MappingEngine::new_empty();
5270
5271 let bo4e = serde_json::json!({
5272 "testEdifact": {
5273 "messlokationsIdRefs": ["REF_A", "REF_B", "REF_C"]
5274 }
5275 });
5276
5277 let instance = engine.map_reverse(&bo4e, &def);
5278
5279 let rff_segs: Vec<_> = instance
5281 .segments
5282 .iter()
5283 .filter(|s| s.tag == "RFF")
5284 .collect();
5285 assert_eq!(rff_segs.len(), 3);
5286 assert_eq!(rff_segs[0].elements[0][0], "Z34");
5287 assert_eq!(rff_segs[0].elements[0][1], "REF_A");
5288 assert_eq!(rff_segs[1].elements[0][0], "Z34");
5289 assert_eq!(rff_segs[1].elements[0][1], "REF_B");
5290 assert_eq!(rff_segs[2].elements[0][0], "Z34");
5291 assert_eq!(rff_segs[2].elements[0][1], "REF_C");
5292 }
5293
5294 #[test]
5295 fn test_when_filled_dotted_path() {
5296 let toml_str = r#"
5297[meta]
5298entity = "Test"
5299bo4e_type = "Test"
5300companion_type = "TestEdifact"
5301source_group = "SG4.SG8.SG10"
5302
5303[fields]
5304
5305[companion_fields]
5306"cci.0.0" = { target = "", default = "Z83", when_filled = ["merkmal.code"] }
5307"cav.0.0" = "merkmal.code"
5308"#;
5309 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5310
5311 let bo4e = serde_json::json!({
5312 "testEdifact": { "merkmal": { "code": "ZA7" } }
5313 });
5314 let engine = MappingEngine::new_empty();
5315 let instance = engine.map_reverse(&bo4e, &def);
5316 let cci = instance
5317 .segments
5318 .iter()
5319 .find(|s| s.tag == "CCI")
5320 .expect("CCI should exist");
5321 assert_eq!(cci.elements[0][0], "Z83");
5322 }
5323
5324 #[test]
5325 fn test_also_target_forward_extracts_both_fields() {
5326 use mig_assembly::assembler::*;
5327
5328 let instance = AssembledGroupInstance {
5329 segments: vec![AssembledSegment {
5330 tag: "NAD".to_string(),
5331 elements: vec![vec!["Z47".to_string()], vec!["12345".to_string()]],
5332 }],
5333 child_groups: vec![],
5334 skipped_segments: vec![],
5335 };
5336
5337 let toml_str = r#"
5338[meta]
5339entity = "Geschaeftspartner"
5340bo4e_type = "Geschaeftspartner"
5341companion_type = "GeschaeftspartnerEdifact"
5342source_group = "SG4.SG12"
5343
5344[fields]
5345"nad.1.0" = "identifikation"
5346
5347[companion_fields."nad.0.0"]
5348target = "partnerrolle"
5349enum_map = { "Z47" = "kundeDesLf", "Z48" = "kundeDesLf", "Z51" = "kundeDesNb", "Z52" = "kundeDesNb" }
5350also_target = "datenqualitaet"
5351also_enum_map = { "Z47" = "erwartet", "Z48" = "imSystemVorhanden", "Z51" = "erwartet", "Z52" = "imSystemVorhanden" }
5352"#;
5353 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5354 let engine = MappingEngine::new_empty();
5355
5356 let mut result = serde_json::Map::new();
5357 engine.extract_companion_fields(&instance, &def, &mut result, false);
5358
5359 let companion = result
5360 .get("geschaeftspartnerEdifact")
5361 .unwrap()
5362 .as_object()
5363 .unwrap();
5364 assert_eq!(
5365 companion.get("partnerrolle").unwrap().as_str().unwrap(),
5366 "kundeDesLf"
5367 );
5368 assert_eq!(
5369 companion.get("datenqualitaet").unwrap().as_str().unwrap(),
5370 "erwartet"
5371 );
5372 }
5373
5374 #[test]
5375 fn test_also_target_reverse_joint_lookup() {
5376 let toml_str = r#"
5377[meta]
5378entity = "Geschaeftspartner"
5379bo4e_type = "Geschaeftspartner"
5380companion_type = "GeschaeftspartnerEdifact"
5381source_group = "SG4.SG12"
5382
5383[fields]
5384
5385[companion_fields."nad.0.0"]
5386target = "partnerrolle"
5387enum_map = { "Z47" = "kundeDesLf", "Z48" = "kundeDesLf", "Z51" = "kundeDesNb", "Z52" = "kundeDesNb" }
5388also_target = "datenqualitaet"
5389also_enum_map = { "Z47" = "erwartet", "Z48" = "imSystemVorhanden", "Z51" = "erwartet", "Z52" = "imSystemVorhanden" }
5390"#;
5391 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5392 let engine = MappingEngine::new_empty();
5393
5394 let bo4e = serde_json::json!({
5396 "geschaeftspartnerEdifact": {
5397 "partnerrolle": "kundeDesLf",
5398 "datenqualitaet": "erwartet"
5399 }
5400 });
5401 let instance = engine.map_reverse(&bo4e, &def);
5402 let nad = instance
5403 .segments
5404 .iter()
5405 .find(|s| s.tag == "NAD")
5406 .expect("NAD");
5407 assert_eq!(nad.elements[0][0], "Z47");
5408
5409 let bo4e2 = serde_json::json!({
5411 "geschaeftspartnerEdifact": {
5412 "partnerrolle": "kundeDesNb",
5413 "datenqualitaet": "imSystemVorhanden"
5414 }
5415 });
5416 let instance2 = engine.map_reverse(&bo4e2, &def);
5417 let nad2 = instance2
5418 .segments
5419 .iter()
5420 .find(|s| s.tag == "NAD")
5421 .expect("NAD");
5422 assert_eq!(nad2.elements[0][0], "Z52");
5423 }
5424
5425 #[test]
5426 fn test_also_target_mixed_codes_unpaired_skips_datenqualitaet() {
5427 use mig_assembly::assembler::*;
5428
5429 let toml_str = r#"
5431[meta]
5432entity = "Geschaeftspartner"
5433bo4e_type = "Geschaeftspartner"
5434companion_type = "GeschaeftspartnerEdifact"
5435source_group = "SG4.SG12"
5436
5437[fields]
5438
5439[companion_fields."nad.0.0"]
5440target = "partnerrolle"
5441enum_map = { "Z09" = "kundeDesLf", "Z47" = "kundeDesLf", "Z48" = "kundeDesLf" }
5442also_target = "datenqualitaet"
5443also_enum_map = { "Z47" = "erwartet", "Z48" = "imSystemVorhanden" }
5444"#;
5445 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5446 let engine = MappingEngine::new_empty();
5447
5448 let instance_z09 = AssembledGroupInstance {
5450 segments: vec![AssembledSegment {
5451 tag: "NAD".to_string(),
5452 elements: vec![vec!["Z09".to_string()]],
5453 }],
5454 child_groups: vec![],
5455 skipped_segments: vec![],
5456 };
5457 let mut result = serde_json::Map::new();
5458 engine.extract_companion_fields(&instance_z09, &def, &mut result, false);
5459 let comp = result
5460 .get("geschaeftspartnerEdifact")
5461 .unwrap()
5462 .as_object()
5463 .unwrap();
5464 assert_eq!(
5465 comp.get("partnerrolle").unwrap().as_str().unwrap(),
5466 "kundeDesLf"
5467 );
5468 assert!(
5469 comp.get("datenqualitaet").is_none(),
5470 "Z09 should not set datenqualitaet"
5471 );
5472
5473 let bo4e = serde_json::json!({
5475 "geschaeftspartnerEdifact": { "partnerrolle": "kundeDesLf" }
5476 });
5477 let instance = engine.map_reverse(&bo4e, &def);
5478 let nad = instance
5479 .segments
5480 .iter()
5481 .find(|s| s.tag == "NAD")
5482 .expect("NAD");
5483 assert_eq!(nad.elements[0][0], "Z09");
5484
5485 let bo4e2 = serde_json::json!({
5487 "geschaeftspartnerEdifact": {
5488 "partnerrolle": "kundeDesLf",
5489 "datenqualitaet": "erwartet"
5490 }
5491 });
5492 let instance2 = engine.map_reverse(&bo4e2, &def);
5493 let nad2 = instance2
5494 .segments
5495 .iter()
5496 .find(|s| s.tag == "NAD")
5497 .expect("NAD");
5498 assert_eq!(nad2.elements[0][0], "Z47");
5499 }
5500}