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