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 let entry = obj.entry(target.to_string())
2102 .or_insert(serde_json::Value::Null);
2103 if entry.is_null() {
2104 *entry = serde_json::Value::String(
2105 disc_value.to_string(),
2106 );
2107 }
2108 }
2109 }
2110 break;
2111 }
2112 }
2113 }
2114 unwrapped = Some(injected);
2115 unwrapped.as_ref().unwrap()
2116 } else {
2117 entity_value
2118 }
2119 } else if is_map_keyed_object(entity_value) {
2120 let map = entity_value.as_object().unwrap();
2125 let arr: Vec<serde_json::Value> = map
2126 .iter()
2127 .map(|(key, val)| {
2128 let mut item = val.clone();
2129 if let Some(obj) = item.as_object_mut() {
2132 if let Some(qualifier_field) =
2133 find_qualifier_companion_field(&self.definitions, &def.meta.entity)
2134 {
2135 let entry = obj.entry(qualifier_field).or_insert(serde_json::Value::Null);
2136 if entry.is_null() {
2137 *entry = serde_json::Value::String(key.clone());
2138 }
2139 }
2140 }
2141 item
2142 })
2143 .collect();
2144 unwrapped = Some(serde_json::Value::Array(arr));
2145 unwrapped.as_ref().unwrap()
2146 } else {
2147 entity_value
2148 }
2149 } else {
2150 entity_value
2151 };
2152
2153 let leaf_group = def
2155 .meta
2156 .source_group
2157 .rsplit('.')
2158 .next()
2159 .unwrap_or(&def.meta.source_group);
2160
2161 if def.meta.source_group.is_empty() {
2162 let instance = self.map_reverse(entity_value, def);
2164 root_segments.extend(instance.segments);
2165 } else if entity_value.is_array() {
2166 let arr = entity_value.as_array().unwrap();
2168 let reps: Vec<_> = arr.iter().map(|item| self.map_reverse(item, def)).collect();
2169
2170 if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
2172 existing.repetitions.extend(reps);
2173 } else {
2174 groups.push(AssembledGroup {
2175 group_id: leaf_group.to_string(),
2176 repetitions: reps,
2177 });
2178 }
2179 } else {
2180 let instance = self.map_reverse(entity_value, def);
2182
2183 if let Some(existing) = groups.iter_mut().find(|g| g.group_id == leaf_group) {
2184 existing.repetitions.push(instance);
2185 } else {
2186 groups.push(AssembledGroup {
2187 group_id: leaf_group.to_string(),
2188 repetitions: vec![instance],
2189 });
2190 }
2191 }
2192 }
2193
2194 let nested_specs: Vec<(String, String)> = self
2200 .definitions
2201 .iter()
2202 .filter_map(|def| {
2203 let parts: Vec<&str> = def.meta.source_group.split('.').collect();
2204 if parts.len() > 1 {
2205 Some((parts[0].to_string(), parts[parts.len() - 1].to_string()))
2206 } else {
2207 None
2208 }
2209 })
2210 .collect();
2211 for (parent_id, child_id) in &nested_specs {
2212 let has_parent = groups.iter().any(|g| g.group_id == *parent_id);
2214 let has_child = groups.iter().any(|g| g.group_id == *child_id);
2215 if has_parent && has_child {
2216 let child_idx = groups.iter().position(|g| g.group_id == *child_id).unwrap();
2217 let child_group = groups.remove(child_idx);
2218 let parent = groups
2219 .iter_mut()
2220 .find(|g| g.group_id == *parent_id)
2221 .unwrap();
2222 let child_source_path = self
2226 .definitions
2227 .iter()
2228 .find(|d| {
2229 let parts: Vec<&str> = d.meta.source_group.split('.').collect();
2230 parts.len() > 1 && parts[parts.len() - 1] == *child_id
2231 })
2232 .and_then(|d| d.meta.source_path.as_deref());
2233 let distribution =
2234 child_source_path.and_then(|key| nesting_info.and_then(|ni| ni.get(key)));
2235 for (i, child_rep) in child_group.repetitions.into_iter().enumerate() {
2236 let target_idx = distribution
2237 .and_then(|dist| dist.get(i))
2238 .copied()
2239 .unwrap_or(0);
2240
2241 if let Some(target_rep) = parent.repetitions.get_mut(target_idx) {
2242 if let Some(existing) = target_rep
2243 .child_groups
2244 .iter_mut()
2245 .find(|g| g.group_id == *child_id)
2246 {
2247 existing.repetitions.push(child_rep);
2248 } else {
2249 target_rep.child_groups.push(AssembledGroup {
2250 group_id: child_id.clone(),
2251 repetitions: vec![child_rep],
2252 });
2253 }
2254 }
2255 }
2256 }
2257 }
2258
2259 let post_group_start = root_segments.len();
2260 AssembledTree {
2261 segments: root_segments,
2262 groups,
2263 post_group_start,
2264 inter_group_segments: std::collections::BTreeMap::new(),
2265 }
2266 }
2267
2268 fn count_repetitions(tree: &AssembledTree, group_path: &str) -> usize {
2270 let parts: Vec<&str> = group_path.split('.').collect();
2271
2272 let (first_id, first_rep) = parse_group_spec(parts[0]);
2273 let first_group = match tree.groups.iter().find(|g| g.group_id == first_id) {
2274 Some(g) => g,
2275 None => return 0,
2276 };
2277
2278 if parts.len() == 1 {
2279 return first_group.repetitions.len();
2280 }
2281
2282 let mut current_instance = match first_group.repetitions.get(first_rep.unwrap_or(0)) {
2284 Some(i) => i,
2285 None => return 0,
2286 };
2287
2288 for (i, part) in parts[1..].iter().enumerate() {
2289 let (group_id, explicit_rep) = parse_group_spec(part);
2290 let child_group = match current_instance
2291 .child_groups
2292 .iter()
2293 .find(|g| g.group_id == group_id)
2294 {
2295 Some(g) => g,
2296 None => return 0,
2297 };
2298
2299 if i == parts.len() - 2 {
2300 return child_group.repetitions.len();
2302 }
2303 current_instance = match child_group.repetitions.get(explicit_rep.unwrap_or(0)) {
2304 Some(i) => i,
2305 None => return 0,
2306 };
2307 }
2308
2309 0
2310 }
2311
2312 pub fn map_interchange(
2321 msg_engine: &MappingEngine,
2322 tx_engine: &MappingEngine,
2323 tree: &AssembledTree,
2324 transaction_group: &str,
2325 enrich_codes: bool,
2326 ) -> crate::model::MappedMessage {
2327 let (stammdaten, nesting_info) = msg_engine.map_all_forward_inner(tree, enrich_codes);
2329
2330 let transaktionen = tree
2332 .groups
2333 .iter()
2334 .find(|g| g.group_id == transaction_group)
2335 .map(|sg| {
2336 sg.repetitions
2337 .iter()
2338 .map(|instance| {
2339 let wrapped_tree = AssembledTree {
2342 segments: vec![],
2343 groups: vec![AssembledGroup {
2344 group_id: transaction_group.to_string(),
2345 repetitions: vec![instance.clone()],
2346 }],
2347 post_group_start: 0,
2348 inter_group_segments: std::collections::BTreeMap::new(),
2349 };
2350
2351 let (tx_result, tx_nesting) =
2352 tx_engine.map_all_forward_inner(&wrapped_tree, enrich_codes);
2353
2354 crate::model::MappedTransaktion {
2355 stammdaten: tx_result,
2356 nesting_info: tx_nesting,
2357 }
2358 })
2359 .collect()
2360 })
2361 .unwrap_or_default();
2362
2363 crate::model::MappedMessage {
2364 stammdaten,
2365 transaktionen,
2366 nesting_info,
2367 }
2368 }
2369
2370 pub fn map_interchange_reverse(
2380 msg_engine: &MappingEngine,
2381 tx_engine: &MappingEngine,
2382 mapped: &crate::model::MappedMessage,
2383 transaction_group: &str,
2384 filtered_mig: Option<&MigSchema>,
2385 ) -> AssembledTree {
2386 let msg_tree = msg_engine.map_all_reverse(
2388 &mapped.stammdaten,
2389 if mapped.nesting_info.is_empty() {
2390 None
2391 } else {
2392 Some(&mapped.nesting_info)
2393 },
2394 );
2395
2396 let mut sg4_reps: Vec<AssembledGroupInstance> = Vec::new();
2398
2399 struct DefWithMeta<'a> {
2403 def: &'a MappingDefinition,
2404 relative: String,
2405 depth: usize,
2406 }
2407
2408 let mut sorted_defs: Vec<DefWithMeta> = tx_engine
2409 .definitions
2410 .iter()
2411 .map(|def| {
2412 let relative = strip_tx_group_prefix(&def.meta.source_group, transaction_group);
2413 let depth = if relative.is_empty() {
2414 0
2415 } else {
2416 relative.chars().filter(|c| *c == '.').count() + 1
2417 };
2418 DefWithMeta {
2419 def,
2420 relative,
2421 depth,
2422 }
2423 })
2424 .collect();
2425
2426 let mut parent_rep_map: std::collections::HashMap<String, usize> =
2430 std::collections::HashMap::new();
2431 for dm in &sorted_defs {
2432 if dm.depth >= 2 {
2433 let parts: Vec<&str> = dm.relative.split('.').collect();
2434 let (_, parent_rep) = parse_group_spec(parts[0]);
2435 if let Some(rep_idx) = parent_rep {
2436 if let Some(sp) = &dm.def.meta.source_path {
2437 if let Some((parent_path, _)) = sp.rsplit_once('.') {
2438 parent_rep_map
2439 .entry(parent_path.to_string())
2440 .or_insert(rep_idx);
2441 }
2442 }
2443 }
2444 }
2445 }
2446
2447 for dm in &mut sorted_defs {
2450 if dm.depth == 1 && !dm.relative.contains(':') {
2451 if let Some(sp) = &dm.def.meta.source_path {
2452 if let Some(rep_idx) = parent_rep_map.get(sp.as_str()) {
2453 dm.relative = format!("{}:{}", dm.relative, rep_idx);
2454 }
2455 }
2456 }
2457 }
2458
2459 if let Some(mig) = filtered_mig {
2466 let mig_order = build_reverse_mig_group_order(mig, transaction_group);
2467 sorted_defs.sort_by(|a, b| {
2468 a.depth.cmp(&b.depth).then_with(|| {
2469 let a_id = a.relative.split(':').next().unwrap_or(&a.relative);
2470 let b_id = b.relative.split(':').next().unwrap_or(&b.relative);
2471 let a_pos = variant_mig_position(a.def, a_id, &mig_order);
2473 let b_pos = variant_mig_position(b.def, b_id, &mig_order);
2474 a_pos.cmp(&b_pos).then(a.relative.cmp(&b.relative))
2475 })
2476 });
2477 } else {
2478 sorted_defs.sort_by(|a, b| a.depth.cmp(&b.depth).then(a.relative.cmp(&b.relative)));
2479 }
2480
2481 for tx in &mapped.transaktionen {
2482 let mut root_segs: Vec<AssembledSegment> = Vec::new();
2483 let mut child_groups: Vec<AssembledGroup> = Vec::new();
2484
2485 let mut source_path_to_rep: std::collections::HashMap<String, Vec<usize>> =
2490 std::collections::HashMap::new();
2491
2492 for dm in &sorted_defs {
2493 let entity_key = to_camel_case(&dm.def.meta.entity);
2495 let bo4e_value = match tx.stammdaten.get(&entity_key) {
2496 Some(v) => v,
2497 None => continue,
2498 };
2499
2500 let unwrapped_value: Option<serde_json::Value>;
2502 let bo4e_value = if bo4e_value.is_object() && !bo4e_value.is_array() {
2503 if let Some(disc_value) = dm
2504 .def
2505 .meta
2506 .discriminator
2507 .as_deref()
2508 .and_then(|d| d.split_once('='))
2509 .map(|(_, v)| v)
2510 {
2511 if let Some(inner) = bo4e_value.get(disc_value) {
2512 let mut injected = inner.clone();
2513 if let Some(ref cf) = dm.def.companion_fields {
2514 let disc_path = dm
2515 .def
2516 .meta
2517 .discriminator
2518 .as_deref()
2519 .unwrap()
2520 .split_once('=')
2521 .unwrap()
2522 .0
2523 .to_lowercase();
2524 for (path, mapping) in cf {
2525 let cf_path = path.to_lowercase();
2526 let matches = cf_path == disc_path
2527 || format!("{}.0", cf_path) == disc_path;
2528 if matches {
2529 let target = match mapping {
2530 FieldMapping::Simple(t) => t.as_str(),
2531 FieldMapping::Structured(s) => s.target.as_str(),
2532 FieldMapping::Nested(_) => continue,
2533 };
2534 if !target.is_empty() {
2535 if let Some(obj) = injected.as_object_mut() {
2536 obj.entry(target.to_string()).or_insert_with(
2537 || {
2538 serde_json::Value::String(
2539 disc_value.to_string(),
2540 )
2541 },
2542 );
2543 }
2544 }
2545 break;
2546 }
2547 }
2548 }
2549 unwrapped_value = Some(injected);
2550 unwrapped_value.as_ref().unwrap()
2551 } else {
2552 bo4e_value
2553 }
2554 } else if is_map_keyed_object(bo4e_value) {
2555 let map = bo4e_value.as_object().unwrap();
2556 let arr: Vec<serde_json::Value> = map
2557 .iter()
2558 .map(|(key, val)| {
2559 let mut item = val.clone();
2560 if let Some(obj) = item.as_object_mut() {
2561 if let Some(qualifier_field) =
2562 find_qualifier_companion_field(
2563 &tx_engine.definitions,
2564 &dm.def.meta.entity,
2565 )
2566 {
2567 let entry = obj.entry(qualifier_field).or_insert(serde_json::Value::Null);
2568 if entry.is_null() {
2569 *entry = serde_json::Value::String(key.clone());
2570 }
2571 }
2572 }
2573 item
2574 })
2575 .collect();
2576 unwrapped_value = Some(serde_json::Value::Array(arr));
2577 unwrapped_value.as_ref().unwrap()
2578 } else {
2579 bo4e_value
2580 }
2581 } else {
2582 bo4e_value
2583 };
2584
2585 let items: Vec<&serde_json::Value> = if bo4e_value.is_array() {
2589 bo4e_value.as_array().unwrap().iter().collect()
2590 } else {
2591 vec![bo4e_value]
2592 };
2593
2594 for (item_idx, item) in items.iter().enumerate() {
2595 let instance = tx_engine.map_reverse(item, dm.def);
2596
2597 if instance.segments.is_empty() && instance.child_groups.is_empty() {
2599 continue;
2600 }
2601
2602 if dm.relative.is_empty() {
2603 root_segs.extend(instance.segments);
2604 } else {
2605 let effective_relative = if dm.depth >= 2 {
2609 let rel = if items.len() > 1 {
2612 strip_all_rep_indices(&dm.relative)
2613 } else {
2614 dm.relative.clone()
2615 };
2616 let skip_nesting = dm
2623 .def
2624 .meta
2625 .source_path
2626 .as_ref()
2627 .and_then(|sp| sp.rsplit_once('.'))
2628 .and_then(|(parent_path, _)| {
2629 source_path_to_rep.get(parent_path)
2630 })
2631 .is_some_and(|reps| reps.len() == 1);
2632 let nesting_idx = if items.len() > 1 && !skip_nesting {
2633 dm.def
2634 .meta
2635 .source_path
2636 .as_ref()
2637 .and_then(|sp| tx.nesting_info.get(sp))
2638 .and_then(|dist| dist.get(item_idx))
2639 .copied()
2640 } else {
2641 None
2642 };
2643 if let Some(parent_rep) = nesting_idx {
2644 let parts: Vec<&str> = rel.split('.').collect();
2646 let parent_id = parts[0].split(':').next().unwrap_or(parts[0]);
2647 let rest = parts[1..].join(".");
2648 format!("{}:{}.{}", parent_id, parent_rep, rest)
2649 } else {
2650 resolve_child_relative(
2651 &rel,
2652 dm.def.meta.source_path.as_deref(),
2653 &source_path_to_rep,
2654 item_idx,
2655 )
2656 }
2657 } else if dm.depth == 1 {
2658 let child_key = dm
2661 .def
2662 .meta
2663 .source_path
2664 .as_ref()
2665 .map(|sp| format!("{sp}#child"));
2666 if let Some(child_indices) =
2667 child_key.as_ref().and_then(|ck| tx.nesting_info.get(ck))
2668 {
2669 if let Some(&target) = child_indices.get(item_idx) {
2670 if target != usize::MAX {
2671 let base =
2672 dm.relative.split(':').next().unwrap_or(&dm.relative);
2673 format!("{}:{}", base, target)
2674 } else {
2675 dm.relative.clone()
2676 }
2677 } else if items.len() > 1 && item_idx > 0 {
2678 strip_rep_index(&dm.relative)
2679 } else {
2680 dm.relative.clone()
2681 }
2682 } else if items.len() > 1 && item_idx > 0 {
2683 strip_rep_index(&dm.relative)
2684 } else {
2685 dm.relative.clone()
2686 }
2687 } else if items.len() > 1 && item_idx > 0 {
2688 strip_rep_index(&dm.relative)
2691 } else {
2692 dm.relative.clone()
2693 };
2694
2695 let rep_used =
2696 place_in_groups(&mut child_groups, &effective_relative, instance);
2697
2698 if dm.depth == 1 {
2700 if let Some(sp) = &dm.def.meta.source_path {
2701 source_path_to_rep
2702 .entry(sp.clone())
2703 .or_default()
2704 .push(rep_used);
2705 }
2706 }
2707 }
2708 }
2709 }
2710
2711 if let Some(mig) = filtered_mig {
2716 sort_variant_reps_by_mig(&mut child_groups, mig, transaction_group);
2717 }
2718
2719 sg4_reps.push(AssembledGroupInstance {
2720 segments: root_segs,
2721 child_groups,
2722 skipped_segments: Vec::new(),
2723 });
2724 }
2725
2726 let mut root_segments = Vec::new();
2733 let mut uns_segments = Vec::new();
2734 let mut uns_is_summary = false;
2735 let mut found_uns = false;
2736 for seg in msg_tree.segments {
2737 if seg.tag == "UNS" {
2738 uns_is_summary = seg
2740 .elements
2741 .first()
2742 .and_then(|el| el.first())
2743 .map(|v| v == "S")
2744 .unwrap_or(false);
2745 uns_segments.push(seg);
2746 found_uns = true;
2747 } else if found_uns {
2748 uns_segments.push(seg);
2750 } else {
2751 root_segments.push(seg);
2752 }
2753 }
2754
2755 let pre_group_count = root_segments.len();
2756 let mut all_groups = msg_tree.groups;
2757 let mut inter_group = msg_tree.inter_group_segments;
2758
2759 let sg_num = |id: &str| -> usize {
2761 id.strip_prefix("SG")
2762 .and_then(|n| n.parse::<usize>().ok())
2763 .unwrap_or(0)
2764 };
2765
2766 if !sg4_reps.is_empty() {
2767 if uns_is_summary {
2768 all_groups.push(AssembledGroup {
2770 group_id: transaction_group.to_string(),
2771 repetitions: sg4_reps,
2772 });
2773 if !uns_segments.is_empty() {
2774 all_groups.sort_by_key(|g| sg_num(&g.group_id));
2779 let tx_num = sg_num(transaction_group);
2780 let uns_pos = all_groups
2781 .iter()
2782 .rposition(|g| sg_num(&g.group_id) <= tx_num)
2783 .map(|i| i + 1)
2784 .unwrap_or(all_groups.len());
2785 inter_group.insert(uns_pos, uns_segments);
2786 }
2787 } else {
2788 if !uns_segments.is_empty() {
2790 inter_group.insert(all_groups.len(), uns_segments);
2791 }
2792 all_groups.push(AssembledGroup {
2793 group_id: transaction_group.to_string(),
2794 repetitions: sg4_reps,
2795 });
2796 }
2797 } else if !uns_segments.is_empty() {
2798 if transaction_group.is_empty() {
2799 all_groups.sort_by_key(|g| sg_num(&g.group_id));
2804 if uns_is_summary {
2805 inter_group.insert(all_groups.len(), uns_segments);
2806 } else {
2807 inter_group.insert(0, uns_segments);
2808 }
2809 } else {
2810 all_groups.sort_by_key(|g| sg_num(&g.group_id));
2814 let tx_num = sg_num(transaction_group);
2815 let uns_pos = all_groups
2816 .iter()
2817 .rposition(|g| sg_num(&g.group_id) <= tx_num)
2818 .map(|i| i + 1)
2819 .unwrap_or(all_groups.len());
2820 inter_group.insert(uns_pos, uns_segments);
2821 }
2822 }
2823
2824 AssembledTree {
2825 segments: root_segments,
2826 groups: all_groups,
2827 post_group_start: pre_group_count,
2828 inter_group_segments: inter_group,
2829 }
2830 }
2831
2832 pub fn build_group_from_bo4e(
2834 &self,
2835 bo4e_value: &serde_json::Value,
2836 def: &MappingDefinition,
2837 ) -> AssembledGroup {
2838 let instance = self.map_reverse(bo4e_value, def);
2839 let leaf_group = def
2840 .meta
2841 .source_group
2842 .rsplit('.')
2843 .next()
2844 .unwrap_or(&def.meta.source_group);
2845
2846 AssembledGroup {
2847 group_id: leaf_group.to_string(),
2848 repetitions: vec![instance],
2849 }
2850 }
2851
2852 pub fn map_interchange_typed<M, T>(
2860 msg_engine: &MappingEngine,
2861 tx_engine: &MappingEngine,
2862 tree: &AssembledTree,
2863 tx_group: &str,
2864 enrich_codes: bool,
2865 nachrichtendaten: crate::model::Nachrichtendaten,
2866 interchangedaten: crate::model::Interchangedaten,
2867 ) -> Result<crate::model::Interchange<M, T>, serde_json::Error>
2868 where
2869 M: serde::de::DeserializeOwned,
2870 T: serde::de::DeserializeOwned,
2871 {
2872 let mapped = Self::map_interchange(msg_engine, tx_engine, tree, tx_group, enrich_codes);
2873 let nachricht = mapped.into_dynamic_nachricht(nachrichtendaten);
2874 let dynamic = crate::model::DynamicInterchange {
2875 interchangedaten,
2876 nachrichten: vec![nachricht],
2877 };
2878 let value = serde_json::to_value(&dynamic)?;
2879 serde_json::from_value(value)
2880 }
2881
2882 pub fn map_interchange_reverse_typed<M, T>(
2889 msg_engine: &MappingEngine,
2890 tx_engine: &MappingEngine,
2891 nachricht: &crate::model::Nachricht<M, T>,
2892 tx_group: &str,
2893 ) -> Result<AssembledTree, serde_json::Error>
2894 where
2895 M: serde::Serialize,
2896 T: serde::Serialize,
2897 {
2898 let stammdaten = serde_json::to_value(&nachricht.stammdaten)?;
2899 let transaktionen: Vec<crate::model::MappedTransaktion> = nachricht
2900 .transaktionen
2901 .iter()
2902 .map(|t| {
2903 Ok(crate::model::MappedTransaktion {
2904 stammdaten: serde_json::to_value(t)?,
2905 nesting_info: Default::default(),
2906 })
2907 })
2908 .collect::<Result<Vec<_>, serde_json::Error>>()?;
2909 let mapped = crate::model::MappedMessage {
2910 stammdaten,
2911 transaktionen,
2912 nesting_info: Default::default(),
2913 };
2914 Ok(Self::map_interchange_reverse(
2915 msg_engine, tx_engine, &mapped, tx_group, None,
2916 ))
2917 }
2918}
2919
2920fn parse_source_path_part(part: &str) -> (&str, Option<&str>) {
2927 if let Some(pos) = part.find('_') {
2931 let group = &part[..pos];
2932 let qualifier = &part[pos + 1..];
2933 if !qualifier.is_empty() {
2934 return (group, Some(qualifier));
2935 }
2936 }
2937 (part, None)
2938}
2939
2940fn build_reverse_mig_group_order(mig: &MigSchema, tx_group_id: &str) -> HashMap<String, usize> {
2948 let mut order = HashMap::new();
2949 if let Some(tg) = mig.segment_groups.iter().find(|g| g.id == tx_group_id) {
2950 for (i, nested) in tg.nested_groups.iter().enumerate() {
2951 if let Some(ref vc) = nested.variant_code {
2953 let variant_key = format!("{}_{}", nested.id, vc.to_uppercase());
2954 order.insert(variant_key, i);
2955 }
2956 order.entry(nested.id.clone()).or_insert(i);
2958 }
2959 }
2960 order
2961}
2962
2963fn variant_mig_position(
2969 def: &MappingDefinition,
2970 base_group_id: &str,
2971 mig_order: &HashMap<String, usize>,
2972) -> usize {
2973 if let Some(ref sp) = def.meta.source_path {
2976 let base_lower = base_group_id.to_lowercase();
2978 for part in sp.split('.') {
2979 if part.starts_with(&base_lower)
2980 || part.starts_with(base_group_id.to_lowercase().as_str())
2981 {
2982 if let Some(underscore_pos) = part.find('_') {
2984 let qualifier = &part[underscore_pos + 1..];
2985 let variant_key = format!("{}_{}", base_group_id, qualifier.to_uppercase());
2986 if let Some(&pos) = mig_order.get(&variant_key) {
2987 return pos;
2988 }
2989 }
2990 }
2991 }
2992 }
2993 mig_order.get(base_group_id).copied().unwrap_or(usize::MAX)
2995}
2996
2997fn find_rep_by_entry_qualifier<'a>(
3002 reps: &'a [AssembledGroupInstance],
3003 qualifier: &str,
3004) -> Option<&'a AssembledGroupInstance> {
3005 let parts: Vec<&str> = qualifier.split('_').collect();
3007 reps.iter().find(|inst| {
3008 inst.segments.first().is_some_and(|seg| {
3009 seg.elements
3010 .first()
3011 .and_then(|e| e.first())
3012 .is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
3013 })
3014 })
3015}
3016
3017fn find_all_reps_by_entry_qualifier<'a>(
3019 reps: &'a [AssembledGroupInstance],
3020 qualifier: &str,
3021) -> Vec<&'a AssembledGroupInstance> {
3022 let parts: Vec<&str> = qualifier.split('_').collect();
3024 reps.iter()
3025 .filter(|inst| {
3026 inst.segments.first().is_some_and(|seg| {
3027 seg.elements
3028 .first()
3029 .and_then(|e| e.first())
3030 .is_some_and(|v| parts.iter().any(|part| v.eq_ignore_ascii_case(part)))
3031 })
3032 })
3033 .collect()
3034}
3035
3036fn has_source_path_qualifiers(source_path: &str) -> bool {
3038 source_path.split('.').any(|part| {
3039 if let Some(pos) = part.find('_') {
3040 pos < part.len() - 1
3041 } else {
3042 false
3043 }
3044 })
3045}
3046
3047fn parse_group_spec(part: &str) -> (&str, Option<usize>) {
3048 if let Some(colon_pos) = part.find(':') {
3049 let id = &part[..colon_pos];
3050 let rep = part[colon_pos + 1..].parse::<usize>().ok();
3051 (id, rep)
3052 } else {
3053 (part, None)
3054 }
3055}
3056
3057fn strip_tx_group_prefix(source_group: &str, tx_group: &str) -> String {
3063 if source_group == tx_group || source_group.is_empty() {
3064 String::new()
3065 } else if let Some(rest) = source_group.strip_prefix(tx_group) {
3066 rest.strip_prefix('.').unwrap_or(rest).to_string()
3067 } else {
3068 source_group.to_string()
3069 }
3070}
3071
3072fn place_in_groups(
3080 groups: &mut Vec<AssembledGroup>,
3081 relative_path: &str,
3082 instance: AssembledGroupInstance,
3083) -> usize {
3084 let parts: Vec<&str> = relative_path.split('.').collect();
3085
3086 if parts.len() == 1 {
3087 let (id, rep) = parse_group_spec(parts[0]);
3089
3090 let group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == id) {
3092 g
3093 } else {
3094 groups.push(AssembledGroup {
3095 group_id: id.to_string(),
3096 repetitions: vec![],
3097 });
3098 groups.last_mut().unwrap()
3099 };
3100
3101 if let Some(rep_idx) = rep {
3102 while group.repetitions.len() <= rep_idx {
3104 group.repetitions.push(AssembledGroupInstance {
3105 segments: vec![],
3106 child_groups: vec![],
3107 skipped_segments: Vec::new(),
3108 });
3109 }
3110 group.repetitions[rep_idx]
3111 .segments
3112 .extend(instance.segments);
3113 group.repetitions[rep_idx]
3114 .child_groups
3115 .extend(instance.child_groups);
3116 rep_idx
3117 } else {
3118 let pos = group.repetitions.len();
3120 group.repetitions.push(instance);
3121 pos
3122 }
3123 } else {
3124 let (parent_id, parent_rep) = parse_group_spec(parts[0]);
3126 let rep_idx = parent_rep.unwrap_or(0);
3127
3128 let parent_group = if let Some(g) = groups.iter_mut().find(|g| g.group_id == parent_id) {
3130 g
3131 } else {
3132 groups.push(AssembledGroup {
3133 group_id: parent_id.to_string(),
3134 repetitions: vec![],
3135 });
3136 groups.last_mut().unwrap()
3137 };
3138
3139 while parent_group.repetitions.len() <= rep_idx {
3141 parent_group.repetitions.push(AssembledGroupInstance {
3142 segments: vec![],
3143 child_groups: vec![],
3144 skipped_segments: Vec::new(),
3145 });
3146 }
3147
3148 let remaining = parts[1..].join(".");
3149 place_in_groups(
3150 &mut parent_group.repetitions[rep_idx].child_groups,
3151 &remaining,
3152 instance,
3153 );
3154 rep_idx
3155 }
3156}
3157
3158fn resolve_child_relative(
3170 relative: &str,
3171 source_path: Option<&str>,
3172 source_path_to_rep: &std::collections::HashMap<String, Vec<usize>>,
3173 item_idx: usize,
3174) -> String {
3175 let parts: Vec<&str> = relative.split('.').collect();
3176 if parts.is_empty() {
3177 return relative.to_string();
3178 }
3179
3180 let (parent_id, parent_rep) = parse_group_spec(parts[0]);
3182 if parent_rep.is_some() {
3183 return relative.to_string();
3184 }
3185
3186 if let Some(sp) = source_path {
3188 if let Some((parent_path, _child)) = sp.rsplit_once('.') {
3189 if let Some(rep_indices) = source_path_to_rep.get(parent_path) {
3190 let rep_idx = rep_indices
3192 .get(item_idx)
3193 .or_else(|| rep_indices.last())
3194 .copied()
3195 .unwrap_or(0);
3196 let rest = parts[1..].join(".");
3197 return format!("{}:{}.{}", parent_id, rep_idx, rest);
3198 }
3199 }
3200 }
3201
3202 relative.to_string()
3204}
3205
3206struct DiscriminatorMatcher<'a> {
3213 tag: &'a str,
3214 element_idx: usize,
3215 component_idx: usize,
3216 expected_values: Vec<&'a str>,
3217 occurrence: Option<usize>,
3219}
3220
3221impl<'a> DiscriminatorMatcher<'a> {
3222 fn parse(disc: &'a str) -> Option<Self> {
3223 let (spec, expected) = disc.split_once('=')?;
3224 let parts: Vec<&str> = spec.split('.').collect();
3225 if parts.len() != 3 {
3226 return None;
3227 }
3228 let (expected_raw, occurrence) = parse_discriminator_occurrence(expected);
3229 Some(Self {
3230 tag: parts[0],
3231 element_idx: parts[1].parse().ok()?,
3232 component_idx: parts[2].parse().ok()?,
3233 expected_values: expected_raw.split('|').collect(),
3234 occurrence,
3235 })
3236 }
3237
3238 fn matches(&self, instance: &AssembledGroupInstance) -> bool {
3239 instance.segments.iter().any(|s| {
3240 s.tag.eq_ignore_ascii_case(self.tag)
3241 && s.elements
3242 .get(self.element_idx)
3243 .and_then(|e| e.get(self.component_idx))
3244 .map(|v| self.expected_values.iter().any(|ev| v == ev))
3245 .unwrap_or(false)
3246 })
3247 }
3248
3249 fn filter_instances<'b>(
3251 &self,
3252 instances: Vec<&'b AssembledGroupInstance>,
3253 ) -> Vec<&'b AssembledGroupInstance> {
3254 let matching: Vec<_> = instances
3255 .into_iter()
3256 .filter(|inst| self.matches(inst))
3257 .collect();
3258 if let Some(occ) = self.occurrence {
3259 matching.into_iter().nth(occ).into_iter().collect()
3260 } else {
3261 matching
3262 }
3263 }
3264}
3265
3266fn parse_discriminator_occurrence(expected: &str) -> (&str, Option<usize>) {
3272 if let Some(hash_pos) = expected.rfind('#') {
3273 if let Ok(occ) = expected[hash_pos + 1..].parse::<usize>() {
3274 return (&expected[..hash_pos], Some(occ));
3275 }
3276 }
3277 (expected, None)
3278}
3279
3280fn strip_rep_index(relative: &str) -> String {
3284 let (id, _) = parse_group_spec(relative);
3285 id.to_string()
3286}
3287
3288fn strip_all_rep_indices(relative: &str) -> String {
3293 relative
3294 .split('.')
3295 .map(|part| {
3296 let (id, _) = parse_group_spec(part);
3297 id
3298 })
3299 .collect::<Vec<_>>()
3300 .join(".")
3301}
3302
3303fn is_collect_all_path(path: &str) -> bool {
3308 let tag_part = path.split('.').next().unwrap_or("");
3309 if let Some(bracket_start) = tag_part.find('[') {
3310 let inner = tag_part[bracket_start + 1..].trim_end_matches(']');
3311 if let Some(comma_pos) = inner.find(',') {
3312 let qualifier = &inner[..comma_pos];
3313 let occ = &inner[comma_pos + 1..];
3314 qualifier != "*" && occ == "*"
3316 } else {
3317 false
3318 }
3319 } else {
3320 false
3321 }
3322}
3323
3324fn parse_tag_qualifier(tag_part: &str) -> (String, Option<&str>, usize) {
3331 if let Some(bracket_start) = tag_part.find('[') {
3332 let tag = tag_part[..bracket_start].to_uppercase();
3333 let inner = tag_part[bracket_start + 1..].trim_end_matches(']');
3334 if let Some(comma_pos) = inner.find(',') {
3335 let qualifier = &inner[..comma_pos];
3336 let index = inner[comma_pos + 1..].parse::<usize>().unwrap_or(0);
3337 if qualifier == "*" {
3339 (tag, None, index)
3340 } else {
3341 (tag, Some(qualifier), index)
3342 }
3343 } else {
3344 (tag, Some(inner), 0)
3345 }
3346 } else {
3347 (tag_part.to_uppercase(), None, 0)
3348 }
3349}
3350
3351fn inject_bo4e_metadata(mut value: serde_json::Value, bo4e_type: &str) -> serde_json::Value {
3356 match &mut value {
3357 serde_json::Value::Object(map) => {
3358 map.entry("boTyp")
3359 .or_insert_with(|| serde_json::Value::String(bo4e_type.to_uppercase()));
3360 map.entry("versionStruktur")
3361 .or_insert_with(|| serde_json::Value::String("1".to_string()));
3362 }
3363 serde_json::Value::Array(items) => {
3364 for item in items {
3365 if let serde_json::Value::Object(map) = item {
3366 map.entry("boTyp")
3367 .or_insert_with(|| serde_json::Value::String(bo4e_type.to_uppercase()));
3368 map.entry("versionStruktur")
3369 .or_insert_with(|| serde_json::Value::String("1".to_string()));
3370 }
3371 }
3372 }
3373 _ => {}
3374 }
3375 value
3376}
3377
3378fn deep_merge_insert(
3384 result: &mut serde_json::Map<String, serde_json::Value>,
3385 entity: &str,
3386 bo4e: serde_json::Value,
3387) {
3388 if let Some(existing) = result.get_mut(entity) {
3389 if let (Some(existing_arr), Some(new_arr)) =
3392 (existing.as_array().map(|a| a.len()), bo4e.as_array())
3393 {
3394 if existing_arr == new_arr.len() {
3395 let existing_arr = existing.as_array_mut().unwrap();
3396 for (existing_elem, new_elem) in existing_arr.iter_mut().zip(new_arr) {
3397 if let (Some(existing_map), Some(new_map)) =
3398 (existing_elem.as_object_mut(), new_elem.as_object())
3399 {
3400 for (k, v) in new_map {
3401 if let Some(existing_v) = existing_map.get_mut(k) {
3402 if let (Some(existing_inner), Some(new_inner)) =
3403 (existing_v.as_object_mut(), v.as_object())
3404 {
3405 for (ik, iv) in new_inner {
3406 existing_inner
3407 .entry(ik.clone())
3408 .or_insert_with(|| iv.clone());
3409 }
3410 }
3411 } else {
3412 existing_map.insert(k.clone(), v.clone());
3413 }
3414 }
3415 }
3416 }
3417 return;
3418 }
3419 }
3420 if let (Some(existing_map), serde_json::Value::Object(new_map)) =
3422 (existing.as_object_mut(), &bo4e)
3423 {
3424 for (k, v) in new_map {
3425 if let Some(existing_v) = existing_map.get_mut(k) {
3426 if let (Some(existing_inner), Some(new_inner)) =
3428 (existing_v.as_object_mut(), v.as_object())
3429 {
3430 for (ik, iv) in new_inner {
3431 existing_inner
3432 .entry(ik.clone())
3433 .or_insert_with(|| iv.clone());
3434 }
3435 }
3436 } else {
3438 existing_map.insert(k.clone(), v.clone());
3439 }
3440 }
3441 return;
3442 }
3443 }
3444 result.insert(entity.to_string(), bo4e);
3445}
3446
3447fn is_map_keyed_object(value: &serde_json::Value) -> bool {
3458 let Some(obj) = value.as_object() else {
3459 return false;
3460 };
3461 if obj.is_empty() {
3462 return false;
3463 }
3464 obj.iter().all(|(k, v)| {
3466 k.len() <= 5
3467 && k.chars()
3468 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
3469 && v.is_object()
3470 })
3471}
3472
3473fn find_qualifier_companion_field(
3482 definitions: &[crate::definition::MappingDefinition],
3483 entity: &str,
3484) -> Option<String> {
3485 for def in definitions {
3486 if def.meta.entity != *entity {
3487 continue;
3488 }
3489 let disc = def.meta.discriminator.as_deref()?;
3490 let (disc_path, _) = disc.split_once('=')?;
3491 let disc_path_lower = disc_path.to_lowercase();
3492
3493 let sections: Vec<&indexmap::IndexMap<String, FieldMapping>> = [
3496 def.companion_fields.as_ref(),
3497 Some(&def.fields),
3498 ]
3499 .into_iter()
3500 .flatten()
3501 .collect();
3502
3503 for section in sections {
3504 for (path, mapping) in section {
3505 let cf_path = path.to_lowercase();
3506 let matches = cf_path == disc_path_lower
3507 || format!("{}.0", cf_path) == disc_path_lower;
3508 if matches {
3509 let target = match mapping {
3510 FieldMapping::Simple(t) => t.as_str(),
3511 FieldMapping::Structured(s) => s.target.as_str(),
3512 FieldMapping::Nested(_) => continue,
3513 };
3514 if !target.is_empty() {
3515 return Some(target.to_string());
3516 }
3517 }
3518 }
3519 }
3520 }
3521 None
3522}
3523
3524fn to_camel_case(name: &str) -> String {
3525 let mut chars = name.chars();
3526 match chars.next() {
3527 Some(c) => c.to_lowercase().to_string() + chars.as_str(),
3528 None => String::new(),
3529 }
3530}
3531
3532fn set_nested_value(map: &mut serde_json::Map<String, serde_json::Value>, path: &str, val: String) {
3535 set_nested_value_json(map, path, serde_json::Value::String(val));
3536}
3537
3538fn set_nested_value_json(
3540 map: &mut serde_json::Map<String, serde_json::Value>,
3541 path: &str,
3542 val: serde_json::Value,
3543) {
3544 if let Some((prefix, leaf)) = path.rsplit_once('.') {
3545 let mut current = map;
3546 for part in prefix.split('.') {
3547 let entry = current
3548 .entry(part.to_string())
3549 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
3550 current = entry.as_object_mut().expect("expected object in path");
3551 }
3552 current.insert(leaf.to_string(), val);
3553 } else {
3554 map.insert(path.to_string(), val);
3555 }
3556}
3557
3558#[derive(serde::Serialize, serde::Deserialize)]
3563pub struct VariantCache {
3564 pub message_defs: Vec<MappingDefinition>,
3566 pub transaction_defs: HashMap<String, Vec<MappingDefinition>>,
3568 pub combined_defs: HashMap<String, Vec<MappingDefinition>>,
3570 #[serde(default)]
3572 pub code_lookups: HashMap<String, crate::code_lookup::CodeLookup>,
3573 #[serde(default)]
3575 pub mig_schema: Option<mig_types::schema::mig::MigSchema>,
3576 #[serde(default)]
3578 pub segment_structure: Option<crate::segment_structure::SegmentStructure>,
3579 #[serde(default)]
3582 pub pid_segment_numbers: HashMap<String, Vec<String>>,
3583 #[serde(default)]
3586 pub pid_requirements: HashMap<String, crate::pid_requirements::PidRequirements>,
3587 #[serde(default)]
3591 pub tx_groups: HashMap<String, String>,
3592}
3593
3594impl VariantCache {
3595 pub fn save(&self, path: &Path) -> Result<(), MappingError> {
3597 let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
3598 path: path.display().to_string(),
3599 message: e.to_string(),
3600 })?;
3601 if let Some(parent) = path.parent() {
3602 std::fs::create_dir_all(parent)?;
3603 }
3604 std::fs::write(path, encoded)?;
3605 Ok(())
3606 }
3607
3608 pub fn load(path: &Path) -> Result<Self, MappingError> {
3610 let bytes = std::fs::read(path)?;
3611 serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
3612 path: path.display().to_string(),
3613 message: e.to_string(),
3614 })
3615 }
3616
3617 pub fn tx_group(&self, pid: &str) -> Option<&str> {
3621 self.tx_groups
3622 .get(&format!("pid_{pid}"))
3623 .map(|s| s.as_str())
3624 }
3625
3626 pub fn msg_engine(&self) -> MappingEngine {
3628 MappingEngine::from_definitions(self.message_defs.clone())
3629 }
3630
3631 pub fn tx_engine(&self, pid: &str) -> Option<MappingEngine> {
3634 self.transaction_defs
3635 .get(&format!("pid_{pid}"))
3636 .map(|defs| MappingEngine::from_definitions(defs.clone()))
3637 }
3638
3639 pub fn filtered_mig(&self, pid: &str) -> Option<mig_types::schema::mig::MigSchema> {
3642 let mig = self.mig_schema.as_ref()?;
3643 let numbers = self.pid_segment_numbers.get(&format!("pid_{pid}"))?;
3644 let number_set: std::collections::HashSet<String> = numbers.iter().cloned().collect();
3645 Some(mig_assembly::pid_filter::filter_mig_for_pid(
3646 mig,
3647 &number_set,
3648 ))
3649 }
3650}
3651
3652#[derive(serde::Serialize, serde::Deserialize)]
3657pub struct DataBundle {
3658 pub format_version: String,
3659 pub bundle_version: u32,
3660 pub variants: HashMap<String, VariantCache>,
3661}
3662
3663impl DataBundle {
3664 pub const CURRENT_VERSION: u32 = 2;
3665
3666 pub fn variant(&self, name: &str) -> Option<&VariantCache> {
3667 self.variants.get(name)
3668 }
3669
3670 pub fn write_to<W: std::io::Write>(&self, writer: &mut W) -> Result<(), MappingError> {
3671 let encoded = serde_json::to_vec(self).map_err(|e| MappingError::CacheWrite {
3672 path: "<stream>".to_string(),
3673 message: e.to_string(),
3674 })?;
3675 writer.write_all(&encoded).map_err(MappingError::Io)
3676 }
3677
3678 pub fn read_from<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
3679 let mut bytes = Vec::new();
3680 reader.read_to_end(&mut bytes).map_err(MappingError::Io)?;
3681 serde_json::from_slice(&bytes).map_err(|e| MappingError::CacheRead {
3682 path: "<stream>".to_string(),
3683 message: e.to_string(),
3684 })
3685 }
3686
3687 pub fn read_from_checked<R: std::io::Read>(reader: &mut R) -> Result<Self, MappingError> {
3688 let bundle = Self::read_from(reader)?;
3689 if bundle.bundle_version != Self::CURRENT_VERSION {
3690 return Err(MappingError::CacheRead {
3691 path: "<stream>".to_string(),
3692 message: format!(
3693 "Incompatible bundle version {}, expected version {}. \
3694 Run `edifact-data update` to fetch compatible bundles.",
3695 bundle.bundle_version,
3696 Self::CURRENT_VERSION
3697 ),
3698 });
3699 }
3700 Ok(bundle)
3701 }
3702
3703 pub fn save(&self, path: &Path) -> Result<(), MappingError> {
3704 if let Some(parent) = path.parent() {
3705 std::fs::create_dir_all(parent)?;
3706 }
3707 let mut file = std::fs::File::create(path).map_err(MappingError::Io)?;
3708 self.write_to(&mut file)
3709 }
3710
3711 pub fn load(path: &Path) -> Result<Self, MappingError> {
3712 let mut file = std::fs::File::open(path).map_err(MappingError::Io)?;
3713 Self::read_from_checked(&mut file)
3714 }
3715}
3716
3717fn sort_variant_reps_by_mig(
3730 child_groups: &mut [AssembledGroup],
3731 mig: &MigSchema,
3732 transaction_group: &str,
3733) {
3734 let tx_def = match mig
3735 .segment_groups
3736 .iter()
3737 .find(|sg| sg.id == transaction_group)
3738 {
3739 Some(d) => d,
3740 None => return,
3741 };
3742
3743 for cg in child_groups.iter_mut() {
3744 if cg.repetitions.len() <= 1 {
3745 continue;
3746 }
3747
3748 let variant_defs: Vec<(usize, &mig_types::schema::mig::MigSegmentGroup)> = tx_def
3750 .nested_groups
3751 .iter()
3752 .enumerate()
3753 .filter(|(_, ng)| ng.id == cg.group_id && ng.variant_code.is_some())
3754 .collect();
3755
3756 if variant_defs.is_empty() {
3757 continue;
3758 }
3759
3760 cg.repetitions.sort_by_key(|rep| {
3763 let entry_seg = rep.segments.first();
3764 for &(mig_pos, variant_def) in &variant_defs {
3765 let (ei, ci) = variant_def.variant_qualifier_position.unwrap_or((0, 0));
3766 let actual_qual = entry_seg
3767 .and_then(|s| s.elements.get(ei))
3768 .and_then(|e| e.get(ci))
3769 .map(|s| s.as_str())
3770 .unwrap_or("");
3771 let matches = if !variant_def.variant_codes.is_empty() {
3772 variant_def
3773 .variant_codes
3774 .iter()
3775 .any(|c| actual_qual.eq_ignore_ascii_case(c))
3776 } else if let Some(ref expected_code) = variant_def.variant_code {
3777 actual_qual.eq_ignore_ascii_case(expected_code)
3778 } else {
3779 false
3780 };
3781 if matches {
3782 return mig_pos;
3783 }
3784 }
3785 usize::MAX });
3787 }
3788}
3789
3790#[cfg(test)]
3791mod variant_cache_helper_tests {
3792 use super::*;
3793
3794 fn make_test_cache() -> VariantCache {
3795 let mut tx_groups = HashMap::new();
3796 tx_groups.insert("pid_55001".to_string(), "SG4".to_string());
3797 tx_groups.insert("pid_21007".to_string(), "SG14".to_string());
3798
3799 let mut transaction_defs = HashMap::new();
3800 transaction_defs.insert("pid_55001".to_string(), vec![]);
3801 transaction_defs.insert("pid_21007".to_string(), vec![]);
3802
3803 VariantCache {
3804 message_defs: vec![],
3805 transaction_defs,
3806 combined_defs: HashMap::new(),
3807 code_lookups: HashMap::new(),
3808 mig_schema: None,
3809 segment_structure: None,
3810 pid_segment_numbers: HashMap::new(),
3811 pid_requirements: HashMap::new(),
3812 tx_groups,
3813 }
3814 }
3815
3816 #[test]
3817 fn test_tx_group_returns_correct_group() {
3818 let vc = make_test_cache();
3819 assert_eq!(vc.tx_group("55001").unwrap(), "SG4");
3820 assert_eq!(vc.tx_group("21007").unwrap(), "SG14");
3821 }
3822
3823 #[test]
3824 fn test_tx_group_unknown_pid_returns_none() {
3825 let vc = make_test_cache();
3826 assert!(vc.tx_group("99999").is_none());
3827 }
3828
3829 #[test]
3830 fn test_msg_engine_returns_engine() {
3831 let vc = make_test_cache();
3832 let engine = vc.msg_engine();
3833 assert_eq!(engine.definitions().len(), 0);
3834 }
3835
3836 #[test]
3837 fn test_tx_engine_returns_engine_for_known_pid() {
3838 let vc = make_test_cache();
3839 assert!(vc.tx_engine("55001").is_some());
3840 }
3841
3842 #[test]
3843 fn test_tx_engine_returns_none_for_unknown_pid() {
3844 let vc = make_test_cache();
3845 assert!(vc.tx_engine("99999").is_none());
3846 }
3847}
3848
3849#[cfg(test)]
3850mod tests {
3851 use super::*;
3852 use crate::definition::{MappingDefinition, MappingMeta, StructuredFieldMapping};
3853 use indexmap::IndexMap;
3854
3855 fn make_def(fields: IndexMap<String, FieldMapping>) -> MappingDefinition {
3856 MappingDefinition {
3857 meta: MappingMeta {
3858 entity: "Test".to_string(),
3859 bo4e_type: "Test".to_string(),
3860 companion_type: None,
3861 source_group: "SG4".to_string(),
3862 source_path: None,
3863 discriminator: None,
3864 repeat_on_tag: None,
3865 },
3866 fields,
3867 companion_fields: None,
3868 complex_handlers: None,
3869 }
3870 }
3871
3872 #[test]
3873 fn test_map_interchange_single_transaction_backward_compat() {
3874 use mig_assembly::assembler::*;
3875
3876 let tree = AssembledTree {
3878 segments: vec![
3879 AssembledSegment {
3880 tag: "UNH".to_string(),
3881 elements: vec![vec!["001".to_string()]],
3882 },
3883 AssembledSegment {
3884 tag: "BGM".to_string(),
3885 elements: vec![vec!["E01".to_string()], vec!["DOC001".to_string()]],
3886 },
3887 ],
3888 groups: vec![
3889 AssembledGroup {
3890 group_id: "SG2".to_string(),
3891 repetitions: vec![AssembledGroupInstance {
3892 segments: vec![AssembledSegment {
3893 tag: "NAD".to_string(),
3894 elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
3895 }],
3896 child_groups: vec![],
3897 skipped_segments: vec![],
3898 }],
3899 },
3900 AssembledGroup {
3901 group_id: "SG4".to_string(),
3902 repetitions: vec![AssembledGroupInstance {
3903 segments: vec![AssembledSegment {
3904 tag: "IDE".to_string(),
3905 elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
3906 }],
3907 child_groups: vec![AssembledGroup {
3908 group_id: "SG5".to_string(),
3909 repetitions: vec![AssembledGroupInstance {
3910 segments: vec![AssembledSegment {
3911 tag: "LOC".to_string(),
3912 elements: vec![
3913 vec!["Z16".to_string()],
3914 vec!["DE000111222333".to_string()],
3915 ],
3916 }],
3917 child_groups: vec![],
3918 skipped_segments: vec![],
3919 }],
3920 }],
3921 skipped_segments: vec![],
3922 }],
3923 },
3924 ],
3925 post_group_start: 2,
3926 inter_group_segments: std::collections::BTreeMap::new(),
3927 };
3928
3929 let msg_engine = MappingEngine::from_definitions(vec![]);
3931
3932 let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
3934 tx_fields.insert(
3935 "ide.1".to_string(),
3936 FieldMapping::Simple("vorgangId".to_string()),
3937 );
3938 let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
3939 malo_fields.insert(
3940 "loc.1".to_string(),
3941 FieldMapping::Simple("marktlokationsId".to_string()),
3942 );
3943
3944 let tx_engine = MappingEngine::from_definitions(vec![
3945 MappingDefinition {
3946 meta: MappingMeta {
3947 entity: "Prozessdaten".to_string(),
3948 bo4e_type: "Prozessdaten".to_string(),
3949 companion_type: None,
3950 source_group: "SG4".to_string(),
3951 source_path: None,
3952 discriminator: None,
3953 repeat_on_tag: None,
3954 },
3955 fields: tx_fields,
3956 companion_fields: None,
3957 complex_handlers: None,
3958 },
3959 MappingDefinition {
3960 meta: MappingMeta {
3961 entity: "Marktlokation".to_string(),
3962 bo4e_type: "Marktlokation".to_string(),
3963 companion_type: None,
3964 source_group: "SG4.SG5".to_string(),
3965 source_path: None,
3966 discriminator: None,
3967 repeat_on_tag: None,
3968 },
3969 fields: malo_fields,
3970 companion_fields: None,
3971 complex_handlers: None,
3972 },
3973 ]);
3974
3975 let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
3976
3977 assert_eq!(result.transaktionen.len(), 1);
3978 assert_eq!(
3979 result.transaktionen[0].stammdaten["prozessdaten"]["vorgangId"]
3980 .as_str()
3981 .unwrap(),
3982 "TX001"
3983 );
3984 assert_eq!(
3985 result.transaktionen[0].stammdaten["marktlokation"]["marktlokationsId"]
3986 .as_str()
3987 .unwrap(),
3988 "DE000111222333"
3989 );
3990 }
3991
3992 #[test]
3993 fn test_map_reverse_pads_intermediate_empty_elements() {
3994 let mut fields = IndexMap::new();
3996 fields.insert(
3997 "nad.0".to_string(),
3998 FieldMapping::Structured(StructuredFieldMapping {
3999 target: String::new(),
4000 transform: None,
4001 when: None,
4002 default: Some("Z09".to_string()),
4003 enum_map: None,
4004 when_filled: None,
4005 also_target: None,
4006 also_enum_map: None,
4007 }),
4008 );
4009 fields.insert(
4010 "nad.3.0".to_string(),
4011 FieldMapping::Simple("name".to_string()),
4012 );
4013 fields.insert(
4014 "nad.3.1".to_string(),
4015 FieldMapping::Simple("vorname".to_string()),
4016 );
4017
4018 let def = make_def(fields);
4019 let engine = MappingEngine::from_definitions(vec![]);
4020
4021 let bo4e = serde_json::json!({
4022 "name": "Muster",
4023 "vorname": "Max"
4024 });
4025
4026 let instance = engine.map_reverse(&bo4e, &def);
4027 assert_eq!(instance.segments.len(), 1);
4028
4029 let nad = &instance.segments[0];
4030 assert_eq!(nad.tag, "NAD");
4031 assert_eq!(nad.elements.len(), 4);
4032 assert_eq!(nad.elements[0], vec!["Z09"]);
4033 assert_eq!(nad.elements[1], vec![""]);
4035 assert_eq!(nad.elements[2], vec![""]);
4036 assert_eq!(nad.elements[3][0], "Muster");
4037 assert_eq!(nad.elements[3][1], "Max");
4038 }
4039
4040 #[test]
4041 fn test_map_reverse_no_padding_when_contiguous() {
4042 let mut fields = IndexMap::new();
4044 fields.insert(
4045 "dtm.0.0".to_string(),
4046 FieldMapping::Structured(StructuredFieldMapping {
4047 target: String::new(),
4048 transform: None,
4049 when: None,
4050 default: Some("92".to_string()),
4051 enum_map: None,
4052 when_filled: None,
4053 also_target: None,
4054 also_enum_map: None,
4055 }),
4056 );
4057 fields.insert(
4058 "dtm.0.1".to_string(),
4059 FieldMapping::Simple("value".to_string()),
4060 );
4061 fields.insert(
4062 "dtm.0.2".to_string(),
4063 FieldMapping::Structured(StructuredFieldMapping {
4064 target: String::new(),
4065 transform: None,
4066 when: None,
4067 default: Some("303".to_string()),
4068 enum_map: None,
4069 when_filled: None,
4070 also_target: None,
4071 also_enum_map: None,
4072 }),
4073 );
4074
4075 let def = make_def(fields);
4076 let engine = MappingEngine::from_definitions(vec![]);
4077
4078 let bo4e = serde_json::json!({ "value": "20250531" });
4079
4080 let instance = engine.map_reverse(&bo4e, &def);
4081 let dtm = &instance.segments[0];
4082 assert_eq!(dtm.elements.len(), 1);
4084 assert_eq!(dtm.elements[0], vec!["92", "20250531", "303"]);
4085 }
4086
4087 #[test]
4088 fn test_map_message_level_extracts_sg2_only() {
4089 use mig_assembly::assembler::*;
4090
4091 let tree = AssembledTree {
4093 segments: vec![
4094 AssembledSegment {
4095 tag: "UNH".to_string(),
4096 elements: vec![vec!["001".to_string()]],
4097 },
4098 AssembledSegment {
4099 tag: "BGM".to_string(),
4100 elements: vec![vec!["E01".to_string()]],
4101 },
4102 ],
4103 groups: vec![
4104 AssembledGroup {
4105 group_id: "SG2".to_string(),
4106 repetitions: vec![AssembledGroupInstance {
4107 segments: vec![AssembledSegment {
4108 tag: "NAD".to_string(),
4109 elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
4110 }],
4111 child_groups: vec![],
4112 skipped_segments: vec![],
4113 }],
4114 },
4115 AssembledGroup {
4116 group_id: "SG4".to_string(),
4117 repetitions: vec![AssembledGroupInstance {
4118 segments: vec![AssembledSegment {
4119 tag: "IDE".to_string(),
4120 elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
4121 }],
4122 child_groups: vec![],
4123 skipped_segments: vec![],
4124 }],
4125 },
4126 ],
4127 post_group_start: 2,
4128 inter_group_segments: std::collections::BTreeMap::new(),
4129 };
4130
4131 let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4133 msg_fields.insert(
4134 "nad.0".to_string(),
4135 FieldMapping::Simple("marktrolle".to_string()),
4136 );
4137 msg_fields.insert(
4138 "nad.1".to_string(),
4139 FieldMapping::Simple("rollencodenummer".to_string()),
4140 );
4141 let msg_def = MappingDefinition {
4142 meta: MappingMeta {
4143 entity: "Marktteilnehmer".to_string(),
4144 bo4e_type: "Marktteilnehmer".to_string(),
4145 companion_type: None,
4146 source_group: "SG2".to_string(),
4147 source_path: None,
4148 discriminator: None,
4149 repeat_on_tag: None,
4150 },
4151 fields: msg_fields,
4152 companion_fields: None,
4153 complex_handlers: None,
4154 };
4155
4156 let engine = MappingEngine::from_definitions(vec![msg_def.clone()]);
4157 let result = engine.map_all_forward(&tree);
4158
4159 assert!(result.get("marktteilnehmer").is_some());
4161 let mt = &result["marktteilnehmer"];
4162 assert_eq!(mt["marktrolle"].as_str().unwrap(), "MS");
4163 assert_eq!(mt["rollencodenummer"].as_str().unwrap(), "9900123");
4164 }
4165
4166 #[test]
4167 fn test_map_transaction_scoped_to_sg4_instance() {
4168 use mig_assembly::assembler::*;
4169
4170 let tree = AssembledTree {
4172 segments: vec![
4173 AssembledSegment {
4174 tag: "UNH".to_string(),
4175 elements: vec![vec!["001".to_string()]],
4176 },
4177 AssembledSegment {
4178 tag: "BGM".to_string(),
4179 elements: vec![vec!["E01".to_string()]],
4180 },
4181 ],
4182 groups: vec![AssembledGroup {
4183 group_id: "SG4".to_string(),
4184 repetitions: vec![AssembledGroupInstance {
4185 segments: vec![AssembledSegment {
4186 tag: "IDE".to_string(),
4187 elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
4188 }],
4189 child_groups: vec![AssembledGroup {
4190 group_id: "SG5".to_string(),
4191 repetitions: vec![AssembledGroupInstance {
4192 segments: vec![AssembledSegment {
4193 tag: "LOC".to_string(),
4194 elements: vec![
4195 vec!["Z16".to_string()],
4196 vec!["DE000111222333".to_string()],
4197 ],
4198 }],
4199 child_groups: vec![],
4200 skipped_segments: vec![],
4201 }],
4202 }],
4203 skipped_segments: vec![],
4204 }],
4205 }],
4206 post_group_start: 2,
4207 inter_group_segments: std::collections::BTreeMap::new(),
4208 };
4209
4210 let mut proz_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4212 proz_fields.insert(
4213 "ide.1".to_string(),
4214 FieldMapping::Simple("vorgangId".to_string()),
4215 );
4216 let proz_def = MappingDefinition {
4217 meta: MappingMeta {
4218 entity: "Prozessdaten".to_string(),
4219 bo4e_type: "Prozessdaten".to_string(),
4220 companion_type: None,
4221 source_group: "".to_string(), source_path: None,
4223 discriminator: None,
4224 repeat_on_tag: None,
4225 },
4226 fields: proz_fields,
4227 companion_fields: None,
4228 complex_handlers: None,
4229 };
4230
4231 let mut malo_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4232 malo_fields.insert(
4233 "loc.1".to_string(),
4234 FieldMapping::Simple("marktlokationsId".to_string()),
4235 );
4236 let malo_def = MappingDefinition {
4237 meta: MappingMeta {
4238 entity: "Marktlokation".to_string(),
4239 bo4e_type: "Marktlokation".to_string(),
4240 companion_type: None,
4241 source_group: "SG5".to_string(), source_path: None,
4243 discriminator: None,
4244 repeat_on_tag: None,
4245 },
4246 fields: malo_fields,
4247 companion_fields: None,
4248 complex_handlers: None,
4249 };
4250
4251 let tx_engine = MappingEngine::from_definitions(vec![proz_def, malo_def]);
4252
4253 let sg4 = &tree.groups[0]; let sg4_instance = &sg4.repetitions[0];
4256 let sub_tree = sg4_instance.as_assembled_tree();
4257
4258 let result = tx_engine.map_all_forward(&sub_tree);
4259
4260 assert_eq!(
4262 result["prozessdaten"]["vorgangId"].as_str().unwrap(),
4263 "TX001"
4264 );
4265
4266 assert_eq!(
4268 result["marktlokation"]["marktlokationsId"]
4269 .as_str()
4270 .unwrap(),
4271 "DE000111222333"
4272 );
4273 }
4274
4275 #[test]
4276 fn test_map_interchange_produces_full_hierarchy() {
4277 use mig_assembly::assembler::*;
4278
4279 let tree = AssembledTree {
4281 segments: vec![
4282 AssembledSegment {
4283 tag: "UNH".to_string(),
4284 elements: vec![vec!["001".to_string()]],
4285 },
4286 AssembledSegment {
4287 tag: "BGM".to_string(),
4288 elements: vec![vec!["E01".to_string()]],
4289 },
4290 ],
4291 groups: vec![
4292 AssembledGroup {
4293 group_id: "SG2".to_string(),
4294 repetitions: vec![AssembledGroupInstance {
4295 segments: vec![AssembledSegment {
4296 tag: "NAD".to_string(),
4297 elements: vec![vec!["MS".to_string()], vec!["9900123".to_string()]],
4298 }],
4299 child_groups: vec![],
4300 skipped_segments: vec![],
4301 }],
4302 },
4303 AssembledGroup {
4304 group_id: "SG4".to_string(),
4305 repetitions: vec![
4306 AssembledGroupInstance {
4307 segments: vec![AssembledSegment {
4308 tag: "IDE".to_string(),
4309 elements: vec![vec!["24".to_string()], vec!["TX001".to_string()]],
4310 }],
4311 child_groups: vec![],
4312 skipped_segments: vec![],
4313 },
4314 AssembledGroupInstance {
4315 segments: vec![AssembledSegment {
4316 tag: "IDE".to_string(),
4317 elements: vec![vec!["24".to_string()], vec!["TX002".to_string()]],
4318 }],
4319 child_groups: vec![],
4320 skipped_segments: vec![],
4321 },
4322 ],
4323 },
4324 ],
4325 post_group_start: 2,
4326 inter_group_segments: std::collections::BTreeMap::new(),
4327 };
4328
4329 let mut msg_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4331 msg_fields.insert(
4332 "nad.0".to_string(),
4333 FieldMapping::Simple("marktrolle".to_string()),
4334 );
4335 let msg_defs = vec![MappingDefinition {
4336 meta: MappingMeta {
4337 entity: "Marktteilnehmer".to_string(),
4338 bo4e_type: "Marktteilnehmer".to_string(),
4339 companion_type: None,
4340 source_group: "SG2".to_string(),
4341 source_path: None,
4342 discriminator: None,
4343 repeat_on_tag: None,
4344 },
4345 fields: msg_fields,
4346 companion_fields: None,
4347 complex_handlers: None,
4348 }];
4349
4350 let mut tx_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4352 tx_fields.insert(
4353 "ide.1".to_string(),
4354 FieldMapping::Simple("vorgangId".to_string()),
4355 );
4356 let tx_defs = vec![MappingDefinition {
4357 meta: MappingMeta {
4358 entity: "Prozessdaten".to_string(),
4359 bo4e_type: "Prozessdaten".to_string(),
4360 companion_type: None,
4361 source_group: "SG4".to_string(),
4362 source_path: None,
4363 discriminator: None,
4364 repeat_on_tag: None,
4365 },
4366 fields: tx_fields,
4367 companion_fields: None,
4368 complex_handlers: None,
4369 }];
4370
4371 let msg_engine = MappingEngine::from_definitions(msg_defs);
4372 let tx_engine = MappingEngine::from_definitions(tx_defs);
4373
4374 let result = MappingEngine::map_interchange(&msg_engine, &tx_engine, &tree, "SG4", true);
4375
4376 assert!(result.stammdaten["marktteilnehmer"].is_object());
4378 assert_eq!(
4379 result.stammdaten["marktteilnehmer"]["marktrolle"]
4380 .as_str()
4381 .unwrap(),
4382 "MS"
4383 );
4384
4385 assert_eq!(result.transaktionen.len(), 2);
4387 assert_eq!(
4388 result.transaktionen[0].stammdaten["prozessdaten"]["vorgangId"]
4389 .as_str()
4390 .unwrap(),
4391 "TX001"
4392 );
4393 assert_eq!(
4394 result.transaktionen[1].stammdaten["prozessdaten"]["vorgangId"]
4395 .as_str()
4396 .unwrap(),
4397 "TX002"
4398 );
4399 }
4400
4401 #[test]
4402 fn test_map_reverse_with_segment_structure_pads_trailing() {
4403 let mut fields = IndexMap::new();
4405 fields.insert(
4406 "sts.0".to_string(),
4407 FieldMapping::Structured(StructuredFieldMapping {
4408 target: String::new(),
4409 transform: None,
4410 when: None,
4411 default: Some("7".to_string()),
4412 enum_map: None,
4413 when_filled: None,
4414 also_target: None,
4415 also_enum_map: None,
4416 }),
4417 );
4418 fields.insert(
4419 "sts.2".to_string(),
4420 FieldMapping::Simple("grund".to_string()),
4421 );
4422
4423 let def = make_def(fields);
4424
4425 let mut counts = std::collections::HashMap::new();
4427 counts.insert("STS".to_string(), 5usize);
4428 let ss = SegmentStructure {
4429 element_counts: counts,
4430 };
4431
4432 let engine = MappingEngine::from_definitions(vec![]).with_segment_structure(ss);
4433
4434 let bo4e = serde_json::json!({ "grund": "E01" });
4435
4436 let instance = engine.map_reverse(&bo4e, &def);
4437 let sts = &instance.segments[0];
4438 assert_eq!(sts.elements.len(), 5);
4441 assert_eq!(sts.elements[0], vec!["7"]);
4442 assert_eq!(sts.elements[1], vec![""]);
4443 assert_eq!(sts.elements[2], vec!["E01"]);
4444 assert_eq!(sts.elements[3], vec![""]);
4445 assert_eq!(sts.elements[4], vec![""]);
4446 }
4447
4448 #[test]
4449 fn test_extract_companion_fields_with_code_enrichment() {
4450 use crate::code_lookup::CodeLookup;
4451 use mig_assembly::assembler::*;
4452
4453 let schema = serde_json::json!({
4454 "fields": {
4455 "sg4": {
4456 "children": {
4457 "sg8_z01": {
4458 "children": {
4459 "sg10": {
4460 "segments": [{
4461 "id": "CCI",
4462 "elements": [{
4463 "index": 2,
4464 "components": [{
4465 "sub_index": 0,
4466 "type": "code",
4467 "codes": [
4468 {"value": "Z15", "name": "Haushaltskunde"},
4469 {"value": "Z18", "name": "Kein Haushaltskunde"}
4470 ]
4471 }]
4472 }]
4473 }],
4474 "source_group": "SG10"
4475 }
4476 },
4477 "segments": [],
4478 "source_group": "SG8"
4479 }
4480 },
4481 "segments": [],
4482 "source_group": "SG4"
4483 }
4484 }
4485 });
4486
4487 let code_lookup = CodeLookup::from_schema_value(&schema);
4488
4489 let tree = AssembledTree {
4490 segments: vec![],
4491 groups: vec![AssembledGroup {
4492 group_id: "SG4".to_string(),
4493 repetitions: vec![AssembledGroupInstance {
4494 segments: vec![],
4495 child_groups: vec![AssembledGroup {
4496 group_id: "SG8".to_string(),
4497 repetitions: vec![AssembledGroupInstance {
4498 segments: vec![],
4499 child_groups: vec![AssembledGroup {
4500 group_id: "SG10".to_string(),
4501 repetitions: vec![AssembledGroupInstance {
4502 segments: vec![AssembledSegment {
4503 tag: "CCI".to_string(),
4504 elements: vec![vec![], vec![], vec!["Z15".to_string()]],
4505 }],
4506 child_groups: vec![],
4507 skipped_segments: vec![],
4508 }],
4509 }],
4510 skipped_segments: vec![],
4511 }],
4512 }],
4513 skipped_segments: vec![],
4514 }],
4515 }],
4516 post_group_start: 0,
4517 inter_group_segments: std::collections::BTreeMap::new(),
4518 };
4519
4520 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4521 companion_fields.insert(
4522 "cci.2".to_string(),
4523 FieldMapping::Simple("haushaltskunde".to_string()),
4524 );
4525
4526 let def = MappingDefinition {
4527 meta: MappingMeta {
4528 entity: "Marktlokation".to_string(),
4529 bo4e_type: "Marktlokation".to_string(),
4530 companion_type: Some("MarktlokationEdifact".to_string()),
4531 source_group: "SG4.SG8.SG10".to_string(),
4532 source_path: Some("sg4.sg8_z01.sg10".to_string()),
4533 discriminator: None,
4534 repeat_on_tag: None,
4535 },
4536 fields: IndexMap::new(),
4537 companion_fields: Some(companion_fields),
4538 complex_handlers: None,
4539 };
4540
4541 let engine_plain = MappingEngine::from_definitions(vec![]);
4543 let bo4e_plain = engine_plain.map_forward(&tree, &def, 0);
4544 assert_eq!(
4545 bo4e_plain["marktlokationEdifact"]["haushaltskunde"].as_str(),
4546 Some("Z15"),
4547 "Without code lookup, should be plain string"
4548 );
4549
4550 let engine_enriched = MappingEngine::from_definitions(vec![]).with_code_lookup(code_lookup);
4552 let bo4e_enriched = engine_enriched.map_forward(&tree, &def, 0);
4553 let hk = &bo4e_enriched["marktlokationEdifact"]["haushaltskunde"];
4554 assert_eq!(hk["code"].as_str(), Some("Z15"));
4555 assert_eq!(hk["meaning"].as_str(), Some("Haushaltskunde"));
4556 assert!(hk.get("enum").is_none());
4558 }
4559
4560 #[test]
4561 fn test_extract_companion_fields_with_enum_enrichment() {
4562 use crate::code_lookup::CodeLookup;
4563 use mig_assembly::assembler::*;
4564
4565 let schema = serde_json::json!({
4567 "fields": {
4568 "sg4": {
4569 "children": {
4570 "sg8_z01": {
4571 "children": {
4572 "sg10": {
4573 "segments": [{
4574 "id": "CCI",
4575 "elements": [{
4576 "index": 2,
4577 "components": [{
4578 "sub_index": 0,
4579 "type": "code",
4580 "codes": [
4581 {"value": "Z15", "name": "Haushaltskunde", "enum": "HAUSHALTSKUNDE"},
4582 {"value": "Z18", "name": "Kein Haushaltskunde", "enum": "KEIN_HAUSHALTSKUNDE"}
4583 ]
4584 }]
4585 }]
4586 }],
4587 "source_group": "SG10"
4588 }
4589 },
4590 "segments": [],
4591 "source_group": "SG8"
4592 }
4593 },
4594 "segments": [],
4595 "source_group": "SG4"
4596 }
4597 }
4598 });
4599
4600 let code_lookup = CodeLookup::from_schema_value(&schema);
4601
4602 let tree = AssembledTree {
4603 segments: vec![],
4604 groups: vec![AssembledGroup {
4605 group_id: "SG4".to_string(),
4606 repetitions: vec![AssembledGroupInstance {
4607 segments: vec![],
4608 child_groups: vec![AssembledGroup {
4609 group_id: "SG8".to_string(),
4610 repetitions: vec![AssembledGroupInstance {
4611 segments: vec![],
4612 child_groups: vec![AssembledGroup {
4613 group_id: "SG10".to_string(),
4614 repetitions: vec![AssembledGroupInstance {
4615 segments: vec![AssembledSegment {
4616 tag: "CCI".to_string(),
4617 elements: vec![vec![], vec![], vec!["Z15".to_string()]],
4618 }],
4619 child_groups: vec![],
4620 skipped_segments: vec![],
4621 }],
4622 }],
4623 skipped_segments: vec![],
4624 }],
4625 }],
4626 skipped_segments: vec![],
4627 }],
4628 }],
4629 post_group_start: 0,
4630 inter_group_segments: std::collections::BTreeMap::new(),
4631 };
4632
4633 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4634 companion_fields.insert(
4635 "cci.2".to_string(),
4636 FieldMapping::Simple("haushaltskunde".to_string()),
4637 );
4638
4639 let def = MappingDefinition {
4640 meta: MappingMeta {
4641 entity: "Marktlokation".to_string(),
4642 bo4e_type: "Marktlokation".to_string(),
4643 companion_type: Some("MarktlokationEdifact".to_string()),
4644 source_group: "SG4.SG8.SG10".to_string(),
4645 source_path: Some("sg4.sg8_z01.sg10".to_string()),
4646 discriminator: None,
4647 repeat_on_tag: None,
4648 },
4649 fields: IndexMap::new(),
4650 companion_fields: Some(companion_fields),
4651 complex_handlers: None,
4652 };
4653
4654 let engine = MappingEngine::from_definitions(vec![]).with_code_lookup(code_lookup);
4655 let bo4e = engine.map_forward(&tree, &def, 0);
4656 let hk = &bo4e["marktlokationEdifact"]["haushaltskunde"];
4657 assert_eq!(hk["code"].as_str(), Some("Z15"));
4658 assert_eq!(hk["meaning"].as_str(), Some("Haushaltskunde"));
4659 assert_eq!(
4660 hk["enum"].as_str(),
4661 Some("HAUSHALTSKUNDE"),
4662 "enum field should be present"
4663 );
4664 }
4665
4666 #[test]
4667 fn test_reverse_mapping_accepts_enriched_with_enum() {
4668 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4670 companion_fields.insert(
4671 "cci.2".to_string(),
4672 FieldMapping::Simple("haushaltskunde".to_string()),
4673 );
4674
4675 let def = MappingDefinition {
4676 meta: MappingMeta {
4677 entity: "Test".to_string(),
4678 bo4e_type: "Test".to_string(),
4679 companion_type: Some("TestEdifact".to_string()),
4680 source_group: "SG4".to_string(),
4681 source_path: None,
4682 discriminator: None,
4683 repeat_on_tag: None,
4684 },
4685 fields: IndexMap::new(),
4686 companion_fields: Some(companion_fields),
4687 complex_handlers: None,
4688 };
4689
4690 let engine = MappingEngine::from_definitions(vec![]);
4691
4692 let bo4e = serde_json::json!({
4693 "testEdifact": {
4694 "haushaltskunde": {
4695 "code": "Z15",
4696 "meaning": "Haushaltskunde",
4697 "enum": "HAUSHALTSKUNDE"
4698 }
4699 }
4700 });
4701 let instance = engine.map_reverse(&bo4e, &def);
4702 assert_eq!(instance.segments[0].elements[2], vec!["Z15"]);
4703 }
4704
4705 #[test]
4706 fn test_reverse_mapping_accepts_enriched_companion() {
4707 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4709 companion_fields.insert(
4710 "cci.2".to_string(),
4711 FieldMapping::Simple("haushaltskunde".to_string()),
4712 );
4713
4714 let def = MappingDefinition {
4715 meta: MappingMeta {
4716 entity: "Test".to_string(),
4717 bo4e_type: "Test".to_string(),
4718 companion_type: Some("TestEdifact".to_string()),
4719 source_group: "SG4".to_string(),
4720 source_path: None,
4721 discriminator: None,
4722 repeat_on_tag: None,
4723 },
4724 fields: IndexMap::new(),
4725 companion_fields: Some(companion_fields),
4726 complex_handlers: None,
4727 };
4728
4729 let engine = MappingEngine::from_definitions(vec![]);
4730
4731 let bo4e_plain = serde_json::json!({
4733 "testEdifact": {
4734 "haushaltskunde": "Z15"
4735 }
4736 });
4737 let instance_plain = engine.map_reverse(&bo4e_plain, &def);
4738 assert_eq!(instance_plain.segments[0].elements[2], vec!["Z15"]);
4739
4740 let bo4e_enriched = serde_json::json!({
4742 "testEdifact": {
4743 "haushaltskunde": {
4744 "code": "Z15",
4745 "meaning": "Haushaltskunde gem. EnWG"
4746 }
4747 }
4748 });
4749 let instance_enriched = engine.map_reverse(&bo4e_enriched, &def);
4750 assert_eq!(instance_enriched.segments[0].elements[2], vec!["Z15"]);
4751 }
4752
4753 #[test]
4754 fn test_resolve_child_relative_with_source_path() {
4755 let mut map: std::collections::HashMap<String, Vec<usize>> =
4756 std::collections::HashMap::new();
4757 map.insert("sg4.sg8_ze1".to_string(), vec![6]);
4758 map.insert("sg4.sg8_z98".to_string(), vec![0]);
4759
4760 assert_eq!(
4762 resolve_child_relative("SG8.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
4763 "SG8:6.SG10"
4764 );
4765
4766 assert_eq!(
4768 resolve_child_relative("SG8:3.SG10", Some("sg4.sg8_ze1.sg10"), &map, 0),
4769 "SG8:3.SG10"
4770 );
4771
4772 assert_eq!(
4774 resolve_child_relative("SG8.SG10", Some("sg4.sg8_unknown.sg10"), &map, 0),
4775 "SG8.SG10"
4776 );
4777
4778 assert_eq!(
4780 resolve_child_relative("SG8.SG10", None, &map, 0),
4781 "SG8.SG10"
4782 );
4783
4784 assert_eq!(
4786 resolve_child_relative("SG8.SG9", Some("sg4.sg8_z98.sg9"), &map, 0),
4787 "SG8:0.SG9"
4788 );
4789
4790 map.insert("sg4.sg8_zf3".to_string(), vec![3, 4]);
4792 assert_eq!(
4793 resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 0),
4794 "SG8:3.SG10"
4795 );
4796 assert_eq!(
4797 resolve_child_relative("SG8.SG10", Some("sg4.sg8_zf3.sg10"), &map, 1),
4798 "SG8:4.SG10"
4799 );
4800 }
4801
4802 #[test]
4803 fn test_place_in_groups_returns_rep_index() {
4804 let mut groups: Vec<AssembledGroup> = Vec::new();
4805
4806 let instance = AssembledGroupInstance {
4808 segments: vec![],
4809 child_groups: vec![],
4810 skipped_segments: vec![],
4811 };
4812 assert_eq!(place_in_groups(&mut groups, "SG8", instance), 0);
4813
4814 let instance = AssembledGroupInstance {
4816 segments: vec![],
4817 child_groups: vec![],
4818 skipped_segments: vec![],
4819 };
4820 assert_eq!(place_in_groups(&mut groups, "SG8", instance), 1);
4821
4822 let instance = AssembledGroupInstance {
4824 segments: vec![],
4825 child_groups: vec![],
4826 skipped_segments: vec![],
4827 };
4828 assert_eq!(place_in_groups(&mut groups, "SG8:5", instance), 5);
4829 }
4830
4831 #[test]
4832 fn test_resolve_by_source_path() {
4833 use mig_assembly::assembler::*;
4834
4835 let tree = AssembledTree {
4837 segments: vec![],
4838 groups: vec![AssembledGroup {
4839 group_id: "SG4".to_string(),
4840 repetitions: vec![AssembledGroupInstance {
4841 segments: vec![],
4842 child_groups: vec![AssembledGroup {
4843 group_id: "SG8".to_string(),
4844 repetitions: vec![
4845 AssembledGroupInstance {
4846 segments: vec![AssembledSegment {
4847 tag: "SEQ".to_string(),
4848 elements: vec![vec!["Z98".to_string()]],
4849 }],
4850 child_groups: vec![AssembledGroup {
4851 group_id: "SG10".to_string(),
4852 repetitions: vec![AssembledGroupInstance {
4853 segments: vec![AssembledSegment {
4854 tag: "CCI".to_string(),
4855 elements: vec![vec![], vec![], vec!["ZB3".to_string()]],
4856 }],
4857 child_groups: vec![],
4858 skipped_segments: vec![],
4859 }],
4860 }],
4861 skipped_segments: vec![],
4862 },
4863 AssembledGroupInstance {
4864 segments: vec![AssembledSegment {
4865 tag: "SEQ".to_string(),
4866 elements: vec![vec!["ZD7".to_string()]],
4867 }],
4868 child_groups: vec![AssembledGroup {
4869 group_id: "SG10".to_string(),
4870 repetitions: vec![AssembledGroupInstance {
4871 segments: vec![AssembledSegment {
4872 tag: "CCI".to_string(),
4873 elements: vec![vec![], vec![], vec!["ZE6".to_string()]],
4874 }],
4875 child_groups: vec![],
4876 skipped_segments: vec![],
4877 }],
4878 }],
4879 skipped_segments: vec![],
4880 },
4881 ],
4882 }],
4883 skipped_segments: vec![],
4884 }],
4885 }],
4886 post_group_start: 0,
4887 inter_group_segments: std::collections::BTreeMap::new(),
4888 };
4889
4890 let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_z98.sg10");
4892 assert!(inst.is_some());
4893 assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
4894
4895 let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zd7.sg10");
4897 assert!(inst.is_some());
4898 assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZE6");
4899
4900 let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8_zzz.sg10");
4902 assert!(inst.is_none());
4903
4904 let inst = MappingEngine::resolve_by_source_path(&tree, "sg4.sg8.sg10");
4906 assert!(inst.is_some());
4907 assert_eq!(inst.unwrap().segments[0].elements[2][0], "ZB3");
4908 }
4909
4910 #[test]
4911 fn test_parse_source_path_part() {
4912 assert_eq!(parse_source_path_part("sg4"), ("sg4", None));
4913 assert_eq!(parse_source_path_part("sg8_z98"), ("sg8", Some("z98")));
4914 assert_eq!(parse_source_path_part("sg10"), ("sg10", None));
4915 assert_eq!(parse_source_path_part("sg12_z04"), ("sg12", Some("z04")));
4916 }
4917
4918 #[test]
4919 fn test_has_source_path_qualifiers() {
4920 assert!(has_source_path_qualifiers("sg4.sg8_z98.sg10"));
4921 assert!(has_source_path_qualifiers("sg4.sg8_ze1.sg9"));
4922 assert!(!has_source_path_qualifiers("sg4.sg6"));
4923 assert!(!has_source_path_qualifiers("sg4.sg8.sg10"));
4924 }
4925
4926 #[test]
4927 fn test_companion_dotted_path_forward() {
4928 use mig_assembly::assembler::*;
4929
4930 let tree = AssembledTree {
4932 segments: vec![],
4933 groups: vec![AssembledGroup {
4934 group_id: "SG4".to_string(),
4935 repetitions: vec![AssembledGroupInstance {
4936 segments: vec![],
4937 child_groups: vec![AssembledGroup {
4938 group_id: "SG8".to_string(),
4939 repetitions: vec![AssembledGroupInstance {
4940 segments: vec![],
4941 child_groups: vec![AssembledGroup {
4942 group_id: "SG10".to_string(),
4943 repetitions: vec![AssembledGroupInstance {
4944 segments: vec![AssembledSegment {
4945 tag: "CCI".to_string(),
4946 elements: vec![
4947 vec!["11XAB-1234".to_string()],
4948 vec!["305".to_string()],
4949 ],
4950 }],
4951 child_groups: vec![],
4952 skipped_segments: vec![],
4953 }],
4954 }],
4955 skipped_segments: vec![],
4956 }],
4957 }],
4958 skipped_segments: vec![],
4959 }],
4960 }],
4961 post_group_start: 0,
4962 inter_group_segments: std::collections::BTreeMap::new(),
4963 };
4964
4965 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
4967 companion_fields.insert(
4968 "cci.0".to_string(),
4969 FieldMapping::Simple("bilanzkreis.id".to_string()),
4970 );
4971 companion_fields.insert(
4972 "cci.1".to_string(),
4973 FieldMapping::Simple("bilanzkreis.codelist".to_string()),
4974 );
4975
4976 let def = MappingDefinition {
4977 meta: MappingMeta {
4978 entity: "Test".to_string(),
4979 bo4e_type: "Test".to_string(),
4980 companion_type: Some("TestEdifact".to_string()),
4981 source_group: "SG4.SG8.SG10".to_string(),
4982 source_path: Some("sg4.sg8_z01.sg10".to_string()),
4983 discriminator: None,
4984 repeat_on_tag: None,
4985 },
4986 fields: IndexMap::new(),
4987 companion_fields: Some(companion_fields),
4988 complex_handlers: None,
4989 };
4990
4991 let engine = MappingEngine::from_definitions(vec![]);
4992 let bo4e = engine.map_forward(&tree, &def, 0);
4993
4994 let companion = &bo4e["testEdifact"];
4996 assert!(
4997 companion.is_object(),
4998 "testEdifact should be an object, got: {companion}"
4999 );
5000 let bilanzkreis = &companion["bilanzkreis"];
5001 assert!(
5002 bilanzkreis.is_object(),
5003 "bilanzkreis should be a nested object, got: {bilanzkreis}"
5004 );
5005 assert_eq!(
5006 bilanzkreis["id"].as_str(),
5007 Some("11XAB-1234"),
5008 "bilanzkreis.id should be 11XAB-1234"
5009 );
5010 assert_eq!(
5011 bilanzkreis["codelist"].as_str(),
5012 Some("305"),
5013 "bilanzkreis.codelist should be 305"
5014 );
5015 }
5016
5017 #[test]
5018 fn test_companion_dotted_path_reverse() {
5019 let engine = MappingEngine::from_definitions(vec![]);
5021
5022 let companion_value = serde_json::json!({
5023 "bilanzkreis": {
5024 "id": "11XAB-1234",
5025 "codelist": "305"
5026 }
5027 });
5028
5029 assert_eq!(
5030 engine.populate_field(&companion_value, "bilanzkreis.id"),
5031 Some("11XAB-1234".to_string()),
5032 "dotted path bilanzkreis.id should resolve"
5033 );
5034 assert_eq!(
5035 engine.populate_field(&companion_value, "bilanzkreis.codelist"),
5036 Some("305".to_string()),
5037 "dotted path bilanzkreis.codelist should resolve"
5038 );
5039
5040 let mut companion_fields: IndexMap<String, FieldMapping> = IndexMap::new();
5042 companion_fields.insert(
5043 "cci.0".to_string(),
5044 FieldMapping::Simple("bilanzkreis.id".to_string()),
5045 );
5046 companion_fields.insert(
5047 "cci.1".to_string(),
5048 FieldMapping::Simple("bilanzkreis.codelist".to_string()),
5049 );
5050
5051 let def = MappingDefinition {
5052 meta: MappingMeta {
5053 entity: "Test".to_string(),
5054 bo4e_type: "Test".to_string(),
5055 companion_type: Some("TestEdifact".to_string()),
5056 source_group: "SG4.SG8.SG10".to_string(),
5057 source_path: Some("sg4.sg8_z01.sg10".to_string()),
5058 discriminator: None,
5059 repeat_on_tag: None,
5060 },
5061 fields: IndexMap::new(),
5062 companion_fields: Some(companion_fields),
5063 complex_handlers: None,
5064 };
5065
5066 let bo4e = serde_json::json!({
5067 "testEdifact": {
5068 "bilanzkreis": {
5069 "id": "11XAB-1234",
5070 "codelist": "305"
5071 }
5072 }
5073 });
5074
5075 let instance = engine.map_reverse(&bo4e, &def);
5076 assert_eq!(instance.segments.len(), 1, "should produce one CCI segment");
5077 let cci = &instance.segments[0];
5078 assert_eq!(cci.tag, "CCI");
5079 assert_eq!(
5080 cci.elements[0],
5081 vec!["11XAB-1234"],
5082 "element 0 should contain bilanzkreis.id"
5083 );
5084 assert_eq!(
5085 cci.elements[1],
5086 vec!["305"],
5087 "element 1 should contain bilanzkreis.codelist"
5088 );
5089 }
5090
5091 #[test]
5092 fn test_when_filled_injects_when_field_present() {
5093 let toml_str = r#"
5094[meta]
5095entity = "Test"
5096bo4e_type = "Test"
5097companion_type = "TestEdifact"
5098source_group = "SG4.SG8.SG10"
5099
5100[fields]
5101
5102[companion_fields]
5103"cci.0.0" = { target = "", default = "Z83", when_filled = ["merkmalCode"] }
5104"cav.0.0" = "merkmalCode"
5105"#;
5106 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5107
5108 let bo4e_with = serde_json::json!({
5110 "testEdifact": { "merkmalCode": "ZA7" }
5111 });
5112 let engine = MappingEngine::new_empty();
5113 let instance = engine.map_reverse(&bo4e_with, &def);
5114 let cci = instance
5115 .segments
5116 .iter()
5117 .find(|s| s.tag == "CCI")
5118 .expect("CCI should exist");
5119 assert_eq!(cci.elements[0][0], "Z83");
5120
5121 let bo4e_without = serde_json::json!({
5123 "testEdifact": {}
5124 });
5125 let instance2 = engine.map_reverse(&bo4e_without, &def);
5126 let cci2 = instance2.segments.iter().find(|s| s.tag == "CCI");
5127 assert!(
5128 cci2.is_none(),
5129 "CCI should not be emitted when merkmalCode is absent"
5130 );
5131 }
5132
5133 #[test]
5134 fn test_when_filled_checks_core_and_companion() {
5135 let toml_str = r#"
5136[meta]
5137entity = "Test"
5138bo4e_type = "Test"
5139companion_type = "TestEdifact"
5140source_group = "SG4.SG5"
5141
5142[fields]
5143"loc.1.0" = "marktlokationsId"
5144
5145[companion_fields]
5146"loc.0.0" = { target = "", default = "Z16", when_filled = ["marktlokationsId"] }
5147"#;
5148 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5149
5150 let bo4e_with = serde_json::json!({
5152 "marktlokationsId": "51234567890"
5153 });
5154 let engine = MappingEngine::new_empty();
5155 let instance = engine.map_reverse(&bo4e_with, &def);
5156 let loc = instance
5157 .segments
5158 .iter()
5159 .find(|s| s.tag == "LOC")
5160 .expect("LOC should exist");
5161 assert_eq!(loc.elements[0][0], "Z16");
5162 assert_eq!(loc.elements[1][0], "51234567890");
5163
5164 let bo4e_without = serde_json::json!({});
5166 let instance2 = engine.map_reverse(&bo4e_without, &def);
5167 let loc2 = instance2.segments.iter().find(|s| s.tag == "LOC");
5168 assert!(loc2.is_none());
5169 }
5170
5171 #[test]
5172 fn test_extract_all_from_instance_collects_all_qualifier_matches() {
5173 use mig_assembly::assembler::*;
5174
5175 let instance = AssembledGroupInstance {
5177 segments: vec![
5178 AssembledSegment {
5179 tag: "SEQ".to_string(),
5180 elements: vec![vec!["ZD6".to_string()]],
5181 },
5182 AssembledSegment {
5183 tag: "RFF".to_string(),
5184 elements: vec![vec!["Z34".to_string(), "REF_A".to_string()]],
5185 },
5186 AssembledSegment {
5187 tag: "RFF".to_string(),
5188 elements: vec![vec!["Z34".to_string(), "REF_B".to_string()]],
5189 },
5190 AssembledSegment {
5191 tag: "RFF".to_string(),
5192 elements: vec![vec!["Z34".to_string(), "REF_C".to_string()]],
5193 },
5194 AssembledSegment {
5195 tag: "RFF".to_string(),
5196 elements: vec![vec!["Z35".to_string(), "OTHER".to_string()]],
5197 },
5198 ],
5199 child_groups: vec![],
5200 skipped_segments: vec![],
5201 };
5202
5203 let all = MappingEngine::extract_all_from_instance(&instance, "rff[Z34,*].0.1");
5205 assert_eq!(all, vec!["REF_A", "REF_B", "REF_C"]);
5206
5207 let single = MappingEngine::extract_from_instance(&instance, "rff[Z34].0.1");
5209 assert_eq!(single, Some("REF_A".to_string()));
5210
5211 let second = MappingEngine::extract_from_instance(&instance, "rff[Z34,1].0.1");
5212 assert_eq!(second, Some("REF_B".to_string()));
5213 }
5214
5215 #[test]
5216 fn test_forward_wildcard_collect_produces_json_array() {
5217 use mig_assembly::assembler::*;
5218
5219 let instance = AssembledGroupInstance {
5220 segments: vec![
5221 AssembledSegment {
5222 tag: "SEQ".to_string(),
5223 elements: vec![vec!["ZD6".to_string()]],
5224 },
5225 AssembledSegment {
5226 tag: "RFF".to_string(),
5227 elements: vec![vec!["Z34".to_string(), "REF_A".to_string()]],
5228 },
5229 AssembledSegment {
5230 tag: "RFF".to_string(),
5231 elements: vec![vec!["Z34".to_string(), "REF_B".to_string()]],
5232 },
5233 ],
5234 child_groups: vec![],
5235 skipped_segments: vec![],
5236 };
5237
5238 let toml_str = r#"
5239[meta]
5240entity = "Test"
5241bo4e_type = "Test"
5242companion_type = "TestEdifact"
5243source_group = "SG4.SG8"
5244
5245[fields]
5246
5247[companion_fields]
5248"rff[Z34,*].0.1" = "messlokationsIdRefs"
5249"#;
5250 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5251 let engine = MappingEngine::new_empty();
5252
5253 let mut result = serde_json::Map::new();
5254 engine.extract_companion_fields(&instance, &def, &mut result, false);
5255
5256 let companion = result.get("testEdifact").unwrap().as_object().unwrap();
5257 let refs = companion
5258 .get("messlokationsIdRefs")
5259 .unwrap()
5260 .as_array()
5261 .unwrap();
5262 assert_eq!(refs.len(), 2);
5263 assert_eq!(refs[0].as_str().unwrap(), "REF_A");
5264 assert_eq!(refs[1].as_str().unwrap(), "REF_B");
5265 }
5266
5267 #[test]
5268 fn test_reverse_json_array_produces_multiple_segments() {
5269 let toml_str = r#"
5270[meta]
5271entity = "Test"
5272bo4e_type = "Test"
5273companion_type = "TestEdifact"
5274source_group = "SG4.SG8"
5275
5276[fields]
5277
5278[companion_fields]
5279"seq.0.0" = { target = "", default = "ZD6" }
5280"rff[Z34,*].0.1" = "messlokationsIdRefs"
5281"#;
5282 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5283 let engine = MappingEngine::new_empty();
5284
5285 let bo4e = serde_json::json!({
5286 "testEdifact": {
5287 "messlokationsIdRefs": ["REF_A", "REF_B", "REF_C"]
5288 }
5289 });
5290
5291 let instance = engine.map_reverse(&bo4e, &def);
5292
5293 let rff_segs: Vec<_> = instance
5295 .segments
5296 .iter()
5297 .filter(|s| s.tag == "RFF")
5298 .collect();
5299 assert_eq!(rff_segs.len(), 3);
5300 assert_eq!(rff_segs[0].elements[0][0], "Z34");
5301 assert_eq!(rff_segs[0].elements[0][1], "REF_A");
5302 assert_eq!(rff_segs[1].elements[0][0], "Z34");
5303 assert_eq!(rff_segs[1].elements[0][1], "REF_B");
5304 assert_eq!(rff_segs[2].elements[0][0], "Z34");
5305 assert_eq!(rff_segs[2].elements[0][1], "REF_C");
5306 }
5307
5308 #[test]
5309 fn test_when_filled_dotted_path() {
5310 let toml_str = r#"
5311[meta]
5312entity = "Test"
5313bo4e_type = "Test"
5314companion_type = "TestEdifact"
5315source_group = "SG4.SG8.SG10"
5316
5317[fields]
5318
5319[companion_fields]
5320"cci.0.0" = { target = "", default = "Z83", when_filled = ["merkmal.code"] }
5321"cav.0.0" = "merkmal.code"
5322"#;
5323 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5324
5325 let bo4e = serde_json::json!({
5326 "testEdifact": { "merkmal": { "code": "ZA7" } }
5327 });
5328 let engine = MappingEngine::new_empty();
5329 let instance = engine.map_reverse(&bo4e, &def);
5330 let cci = instance
5331 .segments
5332 .iter()
5333 .find(|s| s.tag == "CCI")
5334 .expect("CCI should exist");
5335 assert_eq!(cci.elements[0][0], "Z83");
5336 }
5337
5338 #[test]
5339 fn test_also_target_forward_extracts_both_fields() {
5340 use mig_assembly::assembler::*;
5341
5342 let instance = AssembledGroupInstance {
5343 segments: vec![AssembledSegment {
5344 tag: "NAD".to_string(),
5345 elements: vec![vec!["Z47".to_string()], vec!["12345".to_string()]],
5346 }],
5347 child_groups: vec![],
5348 skipped_segments: vec![],
5349 };
5350
5351 let toml_str = r#"
5352[meta]
5353entity = "Geschaeftspartner"
5354bo4e_type = "Geschaeftspartner"
5355companion_type = "GeschaeftspartnerEdifact"
5356source_group = "SG4.SG12"
5357
5358[fields]
5359"nad.1.0" = "identifikation"
5360
5361[companion_fields."nad.0.0"]
5362target = "partnerrolle"
5363enum_map = { "Z47" = "kundeDesLf", "Z48" = "kundeDesLf", "Z51" = "kundeDesNb", "Z52" = "kundeDesNb" }
5364also_target = "datenqualitaet"
5365also_enum_map = { "Z47" = "erwartet", "Z48" = "imSystemVorhanden", "Z51" = "erwartet", "Z52" = "imSystemVorhanden" }
5366"#;
5367 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5368 let engine = MappingEngine::new_empty();
5369
5370 let mut result = serde_json::Map::new();
5371 engine.extract_companion_fields(&instance, &def, &mut result, false);
5372
5373 let companion = result
5374 .get("geschaeftspartnerEdifact")
5375 .unwrap()
5376 .as_object()
5377 .unwrap();
5378 assert_eq!(
5379 companion.get("partnerrolle").unwrap().as_str().unwrap(),
5380 "kundeDesLf"
5381 );
5382 assert_eq!(
5383 companion.get("datenqualitaet").unwrap().as_str().unwrap(),
5384 "erwartet"
5385 );
5386 }
5387
5388 #[test]
5389 fn test_also_target_reverse_joint_lookup() {
5390 let toml_str = r#"
5391[meta]
5392entity = "Geschaeftspartner"
5393bo4e_type = "Geschaeftspartner"
5394companion_type = "GeschaeftspartnerEdifact"
5395source_group = "SG4.SG12"
5396
5397[fields]
5398
5399[companion_fields."nad.0.0"]
5400target = "partnerrolle"
5401enum_map = { "Z47" = "kundeDesLf", "Z48" = "kundeDesLf", "Z51" = "kundeDesNb", "Z52" = "kundeDesNb" }
5402also_target = "datenqualitaet"
5403also_enum_map = { "Z47" = "erwartet", "Z48" = "imSystemVorhanden", "Z51" = "erwartet", "Z52" = "imSystemVorhanden" }
5404"#;
5405 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5406 let engine = MappingEngine::new_empty();
5407
5408 let bo4e = serde_json::json!({
5410 "geschaeftspartnerEdifact": {
5411 "partnerrolle": "kundeDesLf",
5412 "datenqualitaet": "erwartet"
5413 }
5414 });
5415 let instance = engine.map_reverse(&bo4e, &def);
5416 let nad = instance
5417 .segments
5418 .iter()
5419 .find(|s| s.tag == "NAD")
5420 .expect("NAD");
5421 assert_eq!(nad.elements[0][0], "Z47");
5422
5423 let bo4e2 = serde_json::json!({
5425 "geschaeftspartnerEdifact": {
5426 "partnerrolle": "kundeDesNb",
5427 "datenqualitaet": "imSystemVorhanden"
5428 }
5429 });
5430 let instance2 = engine.map_reverse(&bo4e2, &def);
5431 let nad2 = instance2
5432 .segments
5433 .iter()
5434 .find(|s| s.tag == "NAD")
5435 .expect("NAD");
5436 assert_eq!(nad2.elements[0][0], "Z52");
5437 }
5438
5439 #[test]
5440 fn test_also_target_mixed_codes_unpaired_skips_datenqualitaet() {
5441 use mig_assembly::assembler::*;
5442
5443 let toml_str = r#"
5445[meta]
5446entity = "Geschaeftspartner"
5447bo4e_type = "Geschaeftspartner"
5448companion_type = "GeschaeftspartnerEdifact"
5449source_group = "SG4.SG12"
5450
5451[fields]
5452
5453[companion_fields."nad.0.0"]
5454target = "partnerrolle"
5455enum_map = { "Z09" = "kundeDesLf", "Z47" = "kundeDesLf", "Z48" = "kundeDesLf" }
5456also_target = "datenqualitaet"
5457also_enum_map = { "Z47" = "erwartet", "Z48" = "imSystemVorhanden" }
5458"#;
5459 let def: MappingDefinition = toml::from_str(toml_str).unwrap();
5460 let engine = MappingEngine::new_empty();
5461
5462 let instance_z09 = AssembledGroupInstance {
5464 segments: vec![AssembledSegment {
5465 tag: "NAD".to_string(),
5466 elements: vec![vec!["Z09".to_string()]],
5467 }],
5468 child_groups: vec![],
5469 skipped_segments: vec![],
5470 };
5471 let mut result = serde_json::Map::new();
5472 engine.extract_companion_fields(&instance_z09, &def, &mut result, false);
5473 let comp = result
5474 .get("geschaeftspartnerEdifact")
5475 .unwrap()
5476 .as_object()
5477 .unwrap();
5478 assert_eq!(
5479 comp.get("partnerrolle").unwrap().as_str().unwrap(),
5480 "kundeDesLf"
5481 );
5482 assert!(
5483 comp.get("datenqualitaet").is_none(),
5484 "Z09 should not set datenqualitaet"
5485 );
5486
5487 let bo4e = serde_json::json!({
5489 "geschaeftspartnerEdifact": { "partnerrolle": "kundeDesLf" }
5490 });
5491 let instance = engine.map_reverse(&bo4e, &def);
5492 let nad = instance
5493 .segments
5494 .iter()
5495 .find(|s| s.tag == "NAD")
5496 .expect("NAD");
5497 assert_eq!(nad.elements[0][0], "Z09");
5498
5499 let bo4e2 = serde_json::json!({
5501 "geschaeftspartnerEdifact": {
5502 "partnerrolle": "kundeDesLf",
5503 "datenqualitaet": "erwartet"
5504 }
5505 });
5506 let instance2 = engine.map_reverse(&bo4e2, &def);
5507 let nad2 = instance2
5508 .segments
5509 .iter()
5510 .find(|s| s.tag == "NAD")
5511 .expect("NAD");
5512 assert_eq!(nad2.elements[0][0], "Z47");
5513 }
5514}