1use crate::cursor::SegmentCursor;
8use crate::diagnostic::{StructureDiagnostic, StructureDiagnosticKind};
9use crate::matcher;
10use crate::tokenize::OwnedSegment;
11use crate::AssemblyError;
12use mig_types::schema::mig::{MigSchema, MigSegment, MigSegmentGroup};
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct AssembledTree {
18 pub segments: Vec<AssembledSegment>,
19 pub groups: Vec<AssembledGroup>,
20 #[serde(default)]
23 pub post_group_start: usize,
24 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
29 pub inter_group_segments: std::collections::BTreeMap<usize, Vec<AssembledSegment>>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct AssembledSegment {
35 pub tag: String,
36 pub elements: Vec<Vec<String>>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub mig_number: Option<String>,
43 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub segment_number: Option<u32>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct AssembledGroup {
55 pub group_id: String,
56 pub repetitions: Vec<AssembledGroupInstance>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct AssembledGroupInstance {
62 pub segments: Vec<AssembledSegment>,
63 pub child_groups: Vec<AssembledGroup>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub entry_mig_number: Option<String>,
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
75 pub variant_mig_numbers: Vec<String>,
76 #[serde(default, skip_serializing_if = "Vec::is_empty")]
80 pub skipped_segments: Vec<AssembledSegment>,
81 #[serde(default, skip_serializing_if = "Vec::is_empty")]
85 pub skipped_positions: Vec<usize>,
86}
87
88impl AssembledGroupInstance {
89 pub fn as_assembled_tree(&self) -> AssembledTree {
96 AssembledTree {
97 segments: self.segments.clone(),
98 groups: self.child_groups.clone(),
99 post_group_start: self.segments.len(),
100 inter_group_segments: std::collections::BTreeMap::new(),
101 }
102 }
103}
104
105#[derive(Debug, Clone, Default)]
107pub struct AssemblerConfig {
108 pub skip_unknown_segments: bool,
115
116 pub qualifier_map: std::collections::HashMap<String, (usize, usize, String)>,
128
129 pub strict_code_matching: bool,
140}
141
142pub struct Assembler<'a> {
147 mig: &'a MigSchema,
148 config: AssemblerConfig,
149}
150
151impl<'a> Assembler<'a> {
152 pub fn new(mig: &'a MigSchema) -> Self {
153 Self {
154 mig,
155 config: AssemblerConfig::default(),
156 }
157 }
158
159 pub fn with_config(mig: &'a MigSchema, config: AssemblerConfig) -> Self {
160 Self { mig, config }
161 }
162
163 fn top_level_enclosing_for_group(
177 &self,
178 current_group_idx: usize,
179 matched_seg_indices: &[usize],
180 ) -> std::collections::HashSet<String> {
181 if !self.config.skip_unknown_segments {
182 return std::collections::HashSet::new();
183 }
184 let mut tags: std::collections::HashSet<String> = self
185 .mig
186 .segments
187 .iter()
188 .enumerate()
189 .filter_map(|(i, s)| {
190 if matched_seg_indices.contains(&i) {
191 None
192 } else {
193 Some(s.id.clone())
194 }
195 })
196 .collect();
197 for (idx, group) in self.mig.segment_groups.iter().enumerate() {
198 if idx < current_group_idx {
199 continue;
200 }
201 if let Some(entry) = group.segments.first() {
202 tags.insert(entry.id.clone());
203 }
204 }
205 tags
206 }
207
208 pub fn assemble_generic(
210 &self,
211 segments: &[OwnedSegment],
212 ) -> Result<AssembledTree, AssemblyError> {
213 let mut cursor = SegmentCursor::new(segments.len());
214 let mut tree = AssembledTree {
215 segments: Vec::new(),
216 groups: Vec::new(),
217 post_group_start: 0,
218 inter_group_segments: std::collections::BTreeMap::new(),
219 };
220
221 let mut matched_seg_indices = Vec::new();
223
224 for (i, mig_seg) in self.mig.segments.iter().enumerate() {
226 if cursor.is_exhausted() {
227 break;
228 }
229 if let Some(assembled) = self.try_consume_segment(segments, &mut cursor, mig_seg)? {
230 tree.segments.push(assembled);
231 matched_seg_indices.push(i);
232 }
233 }
234
235 let mut group_idx = 0;
244 while group_idx < self.mig.segment_groups.len() {
245 if cursor.is_exhausted() {
246 break;
247 }
248
249 let mig_group = &self.mig.segment_groups[group_idx];
250
251 if self.config.skip_unknown_segments {
260 let tree_group_idx = tree.groups.len();
261 while !cursor.is_exhausted() {
262 let seg = &segments[cursor.position()];
263 let tag = &seg.id;
264 let is_unmatched_root_seg = self
265 .mig
266 .segments
267 .iter()
268 .enumerate()
269 .any(|(i, ms)| !matched_seg_indices.contains(&i) && ms.id == *tag);
270 let is_any_group_entry = self
271 .mig
272 .segment_groups
273 .iter()
274 .any(|g| g.segments.first().is_some_and(|s| s.id == *tag));
275 if is_unmatched_root_seg || is_any_group_entry {
276 break;
277 }
278 tree.inter_group_segments
279 .entry(tree_group_idx)
280 .or_default()
281 .push(owned_to_assembled(seg));
282 cursor.advance();
283 }
284 if cursor.is_exhausted() {
285 break;
286 }
287 }
288
289 let tree_group_idx = tree.groups.len();
291 for (i, mig_seg) in self.mig.segments.iter().enumerate() {
292 if cursor.is_exhausted() {
293 break;
294 }
295 if matched_seg_indices.contains(&i) {
296 continue;
297 }
298 if let Some(assembled) = self.try_consume_segment(segments, &mut cursor, mig_seg)? {
299 tree.inter_group_segments
300 .entry(tree_group_idx)
301 .or_default()
302 .push(assembled);
303 matched_seg_indices.push(i);
304 }
305 }
306
307 let top_enclosing = self.top_level_enclosing_for_group(group_idx, &matched_seg_indices);
313
314 if mig_group.variant_code.is_some() {
316 let variant_count = self.mig.segment_groups[group_idx..]
317 .iter()
318 .take_while(|g| g.id == mig_group.id && g.variant_code.is_some())
319 .count();
320 let variant_end = group_idx + variant_count;
321
322 let variant_groups = &self.mig.segment_groups[group_idx..variant_end];
323 if let Some(combined) = self.try_consume_variant_groups(
324 segments,
325 &mut cursor,
326 variant_groups,
327 &top_enclosing,
328 )? {
329 tree.groups.push(combined);
330 }
331 group_idx = variant_end;
332 } else {
333 if let Some(assembled) =
334 self.try_consume_group(segments, &mut cursor, mig_group, &top_enclosing)?
335 {
336 tree.groups.push(assembled);
337 }
338 group_idx += 1;
339 }
340 }
341
342 tree.post_group_start = tree.segments.len();
344
345 for (i, mig_seg) in self.mig.segments.iter().enumerate() {
347 if cursor.is_exhausted() {
348 break;
349 }
350 if matched_seg_indices.contains(&i) {
351 continue;
352 }
353 if let Some(assembled) = self.try_consume_segment(segments, &mut cursor, mig_seg)? {
354 tree.segments.push(assembled);
355 }
356 }
357
358 Ok(tree)
359 }
360
361 fn try_consume_segment(
362 &self,
363 segments: &[OwnedSegment],
364 cursor: &mut SegmentCursor,
365 mig_seg: &MigSegment,
366 ) -> Result<Option<AssembledSegment>, AssemblyError> {
367 if cursor.is_exhausted() {
368 return Ok(None);
369 }
370 let seg = &segments[cursor.position()];
371 if matcher::matches_segment_tag(&seg.id, &mig_seg.id) {
372 if let Some(ref num) = mig_seg.number {
375 if let Some((el_idx, comp_idx, expected)) = self.config.qualifier_map.get(num) {
376 let actual = seg
377 .elements
378 .get(*el_idx)
379 .and_then(|e| e.get(*comp_idx))
380 .map(|s| s.as_str())
381 .unwrap_or("");
382 if actual != expected {
383 return Ok(None); }
385 }
386 }
387 let mut assembled = owned_to_assembled(seg);
395 assembled.mig_number = mig_seg.number.clone();
396 cursor.advance();
397 Ok(Some(assembled))
398 } else {
399 Ok(None) }
401 }
402
403 fn consume_entry_run_best_match(
413 &self,
414 segments: &[OwnedSegment],
415 cursor: &mut SegmentCursor,
416 entry_slots: &[MigSegment],
417 instance: &mut AssembledGroupInstance,
418 ) -> Result<(), AssemblyError> {
419 let mut used = vec![false; entry_slots.len()];
420 for _ in 0..entry_slots.len() {
421 if cursor.is_exhausted() {
422 break;
423 }
424 let seg = &segments[cursor.position()];
425 let mut strict_match: Option<usize> = None;
426 let mut tag_match: Option<usize> = None;
427 for (i, slot) in entry_slots.iter().enumerate() {
428 if used[i] {
429 continue;
430 }
431 if !matcher::matches_segment_tag(&seg.id, &slot.id) {
432 continue;
433 }
434 if !self.segment_passes_qualifier_map(seg, slot) {
435 continue;
436 }
437 if tag_match.is_none() {
438 tag_match = Some(i);
439 }
440 if strict_match.is_none() && segment_matches_mig_codes(seg, slot) {
441 strict_match = Some(i);
442 }
443 }
444 let Some(i) = strict_match.or(tag_match) else {
445 break;
446 };
447 used[i] = true;
448 let slot = &entry_slots[i];
449 let mut assembled = owned_to_assembled(seg);
450 assembled.mig_number = slot.number.clone();
451 instance.segments.push(assembled);
452 cursor.advance();
453 }
454 Ok(())
455 }
456
457 fn segment_passes_qualifier_map(&self, seg: &OwnedSegment, mig_seg: &MigSegment) -> bool {
458 let Some(ref num) = mig_seg.number else {
459 return true;
460 };
461 let Some((el_idx, comp_idx, expected)) = self.config.qualifier_map.get(num) else {
462 return true;
463 };
464 let actual = seg
465 .elements
466 .get(*el_idx)
467 .and_then(|e| e.get(*comp_idx))
468 .map(|s| s.as_str())
469 .unwrap_or("");
470 actual == expected
471 }
472
473 fn try_consume_group(
474 &self,
475 segments: &[OwnedSegment],
476 cursor: &mut SegmentCursor,
477 mig_group: &MigSegmentGroup,
478 enclosing: &std::collections::HashSet<String>,
479 ) -> Result<Option<AssembledGroup>, AssemblyError> {
480 let mut repetitions = Vec::new();
481 let entry_segment = mig_group.segments.first().ok_or_else(|| {
482 AssemblyError::ParseError(format!("Group {} has no segments", mig_group.id))
483 })?;
484
485 let nested_enclosing: std::collections::HashSet<String> =
490 if self.config.skip_unknown_segments {
491 let mut set = enclosing.clone();
492 set.extend(group_local_scope(mig_group));
493 set
494 } else {
495 std::collections::HashSet::new()
496 };
497
498 while !cursor.is_exhausted() {
500 let iter_start = cursor.position();
501 let seg = &segments[cursor.position()];
502 if !matcher::matches_segment_tag(&seg.id, &entry_segment.id) {
503 break; }
505
506 if !mig_group.variant_codes.is_empty() {
508 let (ei, ci) = mig_group.variant_qualifier_position.unwrap_or((0, 0));
509 let actual_qual = seg
510 .elements
511 .get(ei)
512 .and_then(|e| e.get(ci))
513 .map(|s| s.as_str())
514 .unwrap_or("");
515 if !mig_group
516 .variant_codes
517 .iter()
518 .any(|c| actual_qual.eq_ignore_ascii_case(c))
519 {
520 break;
521 }
522 } else if let Some(ref expected_code) = mig_group.variant_code {
523 let (ei, ci) = mig_group.variant_qualifier_position.unwrap_or((0, 0));
524 let actual_qual = seg
525 .elements
526 .get(ei)
527 .and_then(|e| e.get(ci))
528 .map(|s| s.as_str())
529 .unwrap_or("");
530 if !actual_qual.eq_ignore_ascii_case(expected_code) {
531 break;
532 }
533 }
534
535 let mut instance = AssembledGroupInstance {
536 segments: Vec::new(),
537 child_groups: Vec::new(),
538 entry_mig_number: entry_segment.number.clone(),
539 variant_mig_numbers: collect_mig_numbers(mig_group),
540 skipped_segments: Vec::new(),
541 skipped_positions: Vec::new(),
542 };
543
544 let mut slot_idx = 0;
555 let mut is_entry_run = true;
556 while slot_idx < mig_group.segments.len() {
557 if cursor.is_exhausted() {
558 break;
559 }
560 let current_tag = &mig_group.segments[slot_idx].id;
561 let run_len = mig_group.segments[slot_idx..]
562 .iter()
563 .take_while(|s| s.id == *current_tag)
564 .count();
565
566 if is_entry_run {
567 let entry_slots = &mig_group.segments[slot_idx..slot_idx + run_len];
569 if self.config.strict_code_matching && run_len > 1 {
570 self.consume_entry_run_best_match(
576 segments,
577 cursor,
578 entry_slots,
579 &mut instance,
580 )?;
581 } else {
582 for slot in entry_slots {
583 if cursor.is_exhausted() {
584 break;
585 }
586 if let Some(assembled) =
587 self.try_consume_segment(segments, cursor, slot)?
588 {
589 instance.segments.push(assembled);
590 }
591 }
592 }
593 is_entry_run = false;
594 } else if matcher::matches_segment_tag(current_tag, &entry_segment.id) {
595 if cursor.is_exhausted() {
613 break;
614 }
615 let seg = &segments[cursor.position()];
616 if !matcher::matches_segment_tag(&seg.id, current_tag) {
617 break;
618 }
619 let has_following_non_entry = if cursor.position() + 1 < segments.len() {
622 let next = &segments[cursor.position() + 1];
623 !matcher::matches_segment_tag(&next.id, &entry_segment.id)
624 && mig_group.segments.iter().any(|s| {
625 matcher::matches_segment_tag(&next.id, &s.id)
626 && !matcher::matches_segment_tag(&s.id, &entry_segment.id)
627 })
628 } else {
629 false
630 };
631 if has_following_non_entry {
632 instance.segments.push(owned_to_assembled(seg));
634 cursor.advance();
635 } else {
636 break;
638 }
639 } else {
640 let slots = &mig_group.segments[slot_idx..slot_idx + run_len];
646 if self.config.strict_code_matching && run_len > 1 {
647 self.consume_entry_run_best_match(segments, cursor, slots, &mut instance)?;
648 } else {
649 for slot in slots {
650 if cursor.is_exhausted() {
651 break;
652 }
653 if let Some(assembled) =
654 self.try_consume_segment(segments, cursor, slot)?
655 {
656 instance.segments.push(assembled);
657 }
658 }
659 }
660 while !cursor.is_exhausted() {
662 let seg = &segments[cursor.position()];
663 if matcher::matches_segment_tag(&seg.id, current_tag) {
664 instance.segments.push(owned_to_assembled(seg));
665 cursor.advance();
666 } else {
667 break;
668 }
669 }
670 }
671
672 slot_idx += run_len;
673
674 if self.config.skip_unknown_segments {
681 while !cursor.is_exhausted() {
682 let seg = &segments[cursor.position()];
683 if matcher::matches_segment_tag(&seg.id, &entry_segment.id) {
685 break;
686 }
687 if mig_group.segments[slot_idx..]
689 .iter()
690 .any(|s| matcher::matches_segment_tag(&seg.id, &s.id))
691 {
692 break;
693 }
694 if mig_group.nested_groups.iter().any(|ng| {
696 ng.segments
697 .first()
698 .is_some_and(|es| matcher::matches_segment_tag(&seg.id, &es.id))
699 }) {
700 break;
701 }
702 if enclosing.contains(&seg.id) {
708 break;
709 }
710 instance.skipped_positions.push(cursor.position());
712 instance.skipped_segments.push(owned_to_assembled(seg));
713 cursor.advance();
714 }
715 }
716 }
717
718 loop {
728 let pass_start = cursor.position();
729 let mut nested_idx = 0;
730 while nested_idx < mig_group.nested_groups.len() {
731 if cursor.is_exhausted() {
732 break;
733 }
734 let nested = &mig_group.nested_groups[nested_idx];
735
736 if nested.variant_code.is_some() {
737 let variant_count = mig_group.nested_groups[nested_idx..]
739 .iter()
740 .take_while(|g| g.id == nested.id && g.variant_code.is_some())
741 .count();
742 let variant_end = nested_idx + variant_count;
743 let variant_groups = &mig_group.nested_groups[nested_idx..variant_end];
744 if let Some(combined) = self.try_consume_variant_groups(
745 segments,
746 cursor,
747 variant_groups,
748 &nested_enclosing,
749 )? {
750 push_or_merge_child(&mut instance.child_groups, combined);
751 }
752 nested_idx = variant_end;
753 } else {
754 if let Some(assembled) =
755 self.try_consume_group(segments, cursor, nested, &nested_enclosing)?
756 {
757 push_or_merge_child(&mut instance.child_groups, assembled);
758 }
759 nested_idx += 1;
760 }
761 }
762
763 if !self.config.skip_unknown_segments || cursor.is_exhausted() {
764 break;
765 }
766 let seg = &segments[cursor.position()];
767 if enclosing.contains(&seg.id) {
782 break;
783 }
784 if cursor.position() == pass_start && !self.config.skip_unknown_segments {
788 break;
789 }
790
791 instance.skipped_positions.push(cursor.position());
792 instance.skipped_segments.push(owned_to_assembled(seg));
793 cursor.advance();
794 }
795
796 if cursor.position() == iter_start {
802 break;
803 }
804 repetitions.push(instance);
805 }
806
807 if repetitions.is_empty() {
808 Ok(None)
809 } else {
810 Ok(Some(AssembledGroup {
811 group_id: mig_group.id.clone(),
812 repetitions,
813 }))
814 }
815 }
816
817 fn try_consume_variant_groups(
823 &self,
824 segments: &[OwnedSegment],
825 cursor: &mut SegmentCursor,
826 variants: &[MigSegmentGroup],
827 enclosing: &std::collections::HashSet<String>,
828 ) -> Result<Option<AssembledGroup>, AssemblyError> {
829 let group_id = variants[0].id.clone();
830 let entry_tag = variants[0]
831 .segments
832 .first()
833 .map(|s| s.id.as_str())
834 .unwrap_or("");
835 let mut all_reps = Vec::new();
836
837 while !cursor.is_exhausted() {
838 let seg = &segments[cursor.position()];
839 if !matcher::matches_segment_tag(&seg.id, entry_tag) {
840 break;
841 }
842
843 let matched = variants.iter().find(|v| {
847 let (ei, ci) = v.variant_qualifier_position.unwrap_or((0, 0));
848 let actual_qual = seg
849 .elements
850 .get(ei)
851 .and_then(|e| e.get(ci))
852 .map(|s| s.as_str())
853 .unwrap_or("");
854 if !v.variant_codes.is_empty() {
855 v.variant_codes
856 .iter()
857 .any(|c| actual_qual.eq_ignore_ascii_case(c))
858 } else if let Some(ref expected_code) = v.variant_code {
859 actual_qual.eq_ignore_ascii_case(expected_code)
860 } else {
861 false
862 }
863 });
864
865 if let Some(variant) = matched {
866 if let Some(group) = self.try_consume_group(segments, cursor, variant, enclosing)? {
867 all_reps.extend(group.repetitions);
868 } else {
869 break;
870 }
871 } else {
872 if let Some(group) =
876 self.try_consume_group(segments, cursor, &variants[0], enclosing)?
877 {
878 all_reps.extend(group.repetitions);
879 } else {
880 break;
881 }
882 }
883 }
884
885 if all_reps.is_empty() {
886 Ok(None)
887 } else {
888 Ok(Some(AssembledGroup {
889 group_id,
890 repetitions: all_reps,
891 }))
892 }
893 }
894
895 pub fn assemble_with_diagnostics(
900 &self,
901 segments: &[OwnedSegment],
902 ) -> (AssembledTree, Vec<StructureDiagnostic>) {
903 let mut diagnostics = Vec::new();
904
905 let tree = match self.assemble_generic(segments) {
906 Ok(tree) => tree,
907 Err(e) => {
908 diagnostics.push(StructureDiagnostic {
909 kind: StructureDiagnosticKind::UnexpectedSegment,
910 segment_id: String::new(),
911 position: 0,
912 message: format!("Assembly failed: {e}"),
913 });
914 return (
915 AssembledTree {
916 segments: Vec::new(),
917 groups: Vec::new(),
918 post_group_start: 0,
919 inter_group_segments: std::collections::BTreeMap::new(),
920 },
921 diagnostics,
922 );
923 }
924 };
925
926 let consumed = count_tree_segments(&tree);
930
931 for (i, seg) in segments.iter().enumerate().skip(consumed) {
934 diagnostics.push(StructureDiagnostic {
935 kind: StructureDiagnosticKind::UnexpectedSegment,
936 segment_id: seg.id.clone(),
937 position: i,
938 message: format!(
939 "Segment '{}' at position {} was not consumed by MIG-guided assembly",
940 seg.id, i
941 ),
942 });
943 }
944
945 let mut skipped: Vec<SkippedSegment> = Vec::new();
949 let top_level: Vec<&MigSegmentGroup> = self.mig.segment_groups.iter().collect();
950 for group in &tree.groups {
951 collect_skipped_from_group(group, &top_level, &mut skipped);
952 }
953 skipped.sort_by_key(|s| s.position);
954 for SkippedSegment {
955 position: pos,
956 tag,
957 orphaned_in,
958 } in skipped
959 {
960 diagnostics.push(match orphaned_in {
961 Some((group_id, entry)) => StructureDiagnostic {
964 kind: StructureDiagnosticKind::OrphanedGroupSegment,
965 segment_id: tag.clone(),
966 position: pos,
967 message: format!(
968 "Segment '{tag}' at position {pos} belongs to group {group_id}, but that group's entry segment '{entry}' is missing; the assembler advanced past it and its content is lost",
969 ),
970 },
971 None if mig_defines_tag(self.mig, &tag) => StructureDiagnostic {
972 kind: StructureDiagnosticKind::SkippedUnknownSegment,
973 segment_id: tag.clone(),
974 position: pos,
975 message: format!(
976 "Segment '{tag}' at position {pos} has no slot at this point of the PID-filtered MIG; the assembler advanced past it",
977 ),
978 },
979 None => StructureDiagnostic {
980 kind: StructureDiagnosticKind::SkippedUnknownSegment,
981 segment_id: tag.clone(),
982 position: pos,
983 message: format!(
984 "Segment '{tag}' at position {pos} is not defined in the PID-filtered MIG; the assembler advanced past it",
985 ),
986 },
987 });
988 }
989
990 (tree, diagnostics)
991 }
992}
993
994struct SkippedSegment {
997 position: usize,
998 tag: String,
999 orphaned_in: Option<(String, String)>,
1001}
1002
1003fn collect_skipped_from_group(
1006 group: &AssembledGroup,
1007 mig_candidates: &[&MigSegmentGroup],
1008 out: &mut Vec<SkippedSegment>,
1009) {
1010 let defs: Vec<&MigSegmentGroup> = mig_candidates
1011 .iter()
1012 .copied()
1013 .filter(|g| g.id == group.group_id)
1014 .collect();
1015 let nested: Vec<&MigSegmentGroup> = defs.iter().flat_map(|g| &g.nested_groups).collect();
1016 for rep in &group.repetitions {
1017 for (i, seg) in rep.skipped_segments.iter().enumerate() {
1018 let pos = rep.skipped_positions.get(i).copied().unwrap_or(0);
1019 out.push(SkippedSegment {
1020 position: pos,
1021 tag: seg.tag.clone(),
1022 orphaned_in: orphaning_group(&nested, &seg.tag),
1023 });
1024 }
1025 for child in &rep.child_groups {
1026 collect_skipped_from_group(child, &nested, out);
1027 }
1028 }
1029}
1030
1031fn orphaning_group(nested: &[&MigSegmentGroup], tag: &str) -> Option<(String, String)> {
1037 if nested
1038 .iter()
1039 .any(|g| g.segments.first().is_some_and(|e| e.id == tag))
1040 {
1041 return None;
1042 }
1043 nested.iter().find_map(|g| {
1044 let (entry, rest) = g.segments.split_first()?;
1045 rest.iter()
1046 .any(|s| s.id == tag)
1047 .then(|| (g.id.clone(), entry.id.clone()))
1048 })
1049}
1050
1051fn mig_defines_tag(mig: &MigSchema, tag: &str) -> bool {
1053 fn in_group(g: &MigSegmentGroup, tag: &str) -> bool {
1054 g.segments.iter().any(|s| s.id == tag) || g.nested_groups.iter().any(|n| in_group(n, tag))
1055 }
1056 mig.segments.iter().any(|s| s.id == tag) || mig.segment_groups.iter().any(|g| in_group(g, tag))
1057}
1058
1059fn count_tree_segments(tree: &AssembledTree) -> usize {
1060 let mut count = tree.segments.len();
1061 for group in &tree.groups {
1062 count += count_group_segments(group);
1063 }
1064 for segs in tree.inter_group_segments.values() {
1066 count += segs.len();
1067 }
1068 count
1069}
1070
1071fn count_group_segments(group: &AssembledGroup) -> usize {
1072 let mut count = 0;
1073 for rep in &group.repetitions {
1074 count += rep.segments.len();
1075 count += rep.skipped_segments.len();
1076 for child in &rep.child_groups {
1077 count += count_group_segments(child);
1078 }
1079 }
1080 count
1081}
1082
1083fn group_local_scope(mig_group: &MigSegmentGroup) -> std::collections::HashSet<String> {
1089 let mut tags = std::collections::HashSet::new();
1090 for seg in &mig_group.segments {
1091 tags.insert(seg.id.clone());
1092 }
1093 for nested in &mig_group.nested_groups {
1094 if let Some(entry) = nested.segments.first() {
1095 tags.insert(entry.id.clone());
1096 }
1097 }
1098 tags
1099}
1100
1101fn push_or_merge_child(child_groups: &mut Vec<AssembledGroup>, new: AssembledGroup) {
1106 if let Some(existing) = child_groups.iter_mut().find(|g| g.group_id == new.group_id) {
1107 existing.repetitions.extend(new.repetitions);
1108 } else {
1109 child_groups.push(new);
1110 }
1111}
1112
1113fn collect_mig_numbers(group: &MigSegmentGroup) -> Vec<String> {
1118 let mut numbers = Vec::new();
1119 for seg in &group.segments {
1120 if let Some(ref num) = seg.number {
1121 numbers.push(num.clone());
1122 }
1123 }
1124 for nested in &group.nested_groups {
1125 numbers.extend(collect_mig_numbers(nested));
1126 }
1127 numbers
1128}
1129
1130pub fn owned_to_assembled(seg: &OwnedSegment) -> AssembledSegment {
1131 AssembledSegment {
1132 tag: seg.id.clone(),
1133 elements: seg.elements.clone(),
1134 mig_number: None,
1135 segment_number: Some(seg.segment_number),
1136 }
1137}
1138
1139fn segment_matches_mig_codes(seg: &OwnedSegment, mig_seg: &MigSegment) -> bool {
1147 let actual_at = |el: usize, c: usize| -> &str {
1148 seg.elements
1149 .get(el)
1150 .and_then(|e| e.get(c))
1151 .map(|s| s.as_str())
1152 .unwrap_or("")
1153 };
1154 for de in &mig_seg.data_elements {
1155 if !de.codes.is_empty() {
1156 let actual = actual_at(de.position, 0);
1157 if !actual.is_empty() && !de.codes.iter().any(|c| c.value == actual) {
1158 return false;
1159 }
1160 }
1161 }
1162 for comp in &mig_seg.composites {
1163 for de in &comp.data_elements {
1164 if !de.codes.is_empty() {
1165 let actual = actual_at(comp.position, de.position);
1166 if !actual.is_empty() && !de.codes.iter().any(|c| c.value == actual) {
1167 return false;
1168 }
1169 }
1170 }
1171 }
1172 true
1173}
1174
1175#[cfg(test)]
1176mod tests {
1177 use super::*;
1178 use crate::test_support::{make_mig_group, make_mig_group_with_variant, make_mig_segment};
1179
1180 fn make_owned_seg(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
1181 OwnedSegment {
1182 id: id.to_string(),
1183 elements: elements
1184 .into_iter()
1185 .map(|e| e.into_iter().map(|c| c.to_string()).collect())
1186 .collect(),
1187 segment_number: 0,
1188 }
1189 }
1190
1191 fn make_mig_schema(segments: Vec<&str>, groups: Vec<MigSegmentGroup>) -> MigSchema {
1192 MigSchema {
1193 message_type: "UTILMD".to_string(),
1194 variant: Some("Strom".to_string()),
1195 version: "S2.1".to_string(),
1196 publication_date: "2025-03-20".to_string(),
1197 author: "BDEW".to_string(),
1198 format_version: "FV2504".to_string(),
1199 source_file: "test".to_string(),
1200 segments: segments.into_iter().map(make_mig_segment).collect(),
1201 segment_groups: groups,
1202 }
1203 }
1204
1205 #[test]
1206 fn test_assembler_top_level_segments_only() {
1207 let mig = make_mig_schema(vec!["UNH", "BGM", "DTM", "UNT"], vec![]);
1208
1209 let segments = vec![
1210 make_owned_seg("UNH", vec![vec!["001", "UTILMD:D:11A:UN:S2.1"]]),
1211 make_owned_seg("BGM", vec![vec!["E01", "DOC001"]]),
1212 make_owned_seg("DTM", vec![vec!["137", "20250101", "102"]]),
1213 make_owned_seg("UNT", vec![vec!["4", "001"]]),
1214 ];
1215
1216 let assembler = Assembler::new(&mig);
1217 let result = assembler.assemble_generic(&segments).unwrap();
1218
1219 assert_eq!(result.segments.len(), 4);
1220 assert_eq!(result.segments[0].tag, "UNH");
1221 assert_eq!(result.segments[1].tag, "BGM");
1222 assert_eq!(result.segments[2].tag, "DTM");
1223 assert_eq!(result.segments[3].tag, "UNT");
1224 assert!(result.groups.is_empty());
1225 }
1226
1227 #[test]
1228 fn test_assembler_with_segment_group() {
1229 let mig = make_mig_schema(
1230 vec!["UNH", "BGM"],
1231 vec![
1232 make_mig_group("SG2", vec!["NAD"], vec![]),
1233 make_mig_group("SG4", vec!["IDE", "STS"], vec![]),
1234 ],
1235 );
1236
1237 let segments = vec![
1238 make_owned_seg("UNH", vec![vec!["001"]]),
1239 make_owned_seg("BGM", vec![vec!["E01"]]),
1240 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1241 make_owned_seg("NAD", vec![vec!["MR", "9900456"]]),
1242 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1243 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
1244 ];
1245
1246 let assembler = Assembler::new(&mig);
1247 let result = assembler.assemble_generic(&segments).unwrap();
1248
1249 assert_eq!(result.segments.len(), 2);
1251 assert_eq!(result.groups.len(), 2);
1253 assert_eq!(result.groups[0].group_id, "SG2");
1254 assert_eq!(result.groups[0].repetitions.len(), 2);
1255 assert_eq!(result.groups[0].repetitions[0].segments[0].tag, "NAD");
1256 assert_eq!(result.groups[0].repetitions[1].segments[0].tag, "NAD");
1257 assert_eq!(result.groups[1].group_id, "SG4");
1259 assert_eq!(result.groups[1].repetitions.len(), 1);
1260 assert_eq!(result.groups[1].repetitions[0].segments.len(), 2);
1261 }
1262
1263 #[test]
1264 fn test_assembler_nested_groups() {
1265 let sg3 = make_mig_group("SG3", vec!["CTA", "COM"], vec![]);
1266 let mig = make_mig_schema(
1267 vec!["UNH", "BGM"],
1268 vec![make_mig_group("SG2", vec!["NAD"], vec![sg3])],
1269 );
1270
1271 let segments = vec![
1272 make_owned_seg("UNH", vec![vec!["001"]]),
1273 make_owned_seg("BGM", vec![vec!["E01"]]),
1274 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1275 make_owned_seg("CTA", vec![vec!["IC", "Kontakt"]]),
1276 make_owned_seg("COM", vec![vec!["040@example.com", "EM"]]),
1277 ];
1278
1279 let assembler = Assembler::new(&mig);
1280 let result = assembler.assemble_generic(&segments).unwrap();
1281
1282 let sg2 = &result.groups[0];
1284 assert_eq!(sg2.group_id, "SG2");
1285 assert_eq!(sg2.repetitions.len(), 1);
1286
1287 let sg2_inst = &sg2.repetitions[0];
1288 assert_eq!(sg2_inst.segments[0].tag, "NAD");
1289
1290 assert_eq!(sg2_inst.child_groups.len(), 1);
1292 let sg3 = &sg2_inst.child_groups[0];
1293 assert_eq!(sg3.group_id, "SG3");
1294 assert_eq!(sg3.repetitions[0].segments.len(), 2);
1295 assert_eq!(sg3.repetitions[0].segments[0].tag, "CTA");
1296 assert_eq!(sg3.repetitions[0].segments[1].tag, "COM");
1297 }
1298
1299 #[test]
1300 fn test_assembler_optional_segments_skipped() {
1301 let mig = make_mig_schema(vec!["UNH", "BGM", "DTM", "UNT"], vec![]);
1303
1304 let segments = vec![
1305 make_owned_seg("UNH", vec![vec!["001"]]),
1306 make_owned_seg("BGM", vec![vec!["E01"]]),
1307 make_owned_seg("UNT", vec![vec!["2", "001"]]),
1308 ];
1309
1310 let assembler = Assembler::new(&mig);
1311 let result = assembler.assemble_generic(&segments).unwrap();
1312
1313 assert_eq!(result.segments.len(), 3);
1315 assert_eq!(result.segments[0].tag, "UNH");
1316 assert_eq!(result.segments[1].tag, "BGM");
1317 assert_eq!(result.segments[2].tag, "UNT");
1318 }
1319
1320 #[test]
1321 fn test_assembler_empty_segments() {
1322 let mig = make_mig_schema(vec!["UNH"], vec![]);
1323 let assembler = Assembler::new(&mig);
1324 let result = assembler.assemble_generic(&[]).unwrap();
1325 assert!(result.segments.is_empty());
1326 assert!(result.groups.is_empty());
1327 }
1328
1329 #[test]
1330 fn test_assembler_preserves_element_data() {
1331 let mig = make_mig_schema(vec!["DTM"], vec![]);
1332
1333 let segments = vec![make_owned_seg(
1334 "DTM",
1335 vec![vec!["137", "202501010000+01", "303"]],
1336 )];
1337
1338 let assembler = Assembler::new(&mig);
1339 let result = assembler.assemble_generic(&segments).unwrap();
1340
1341 let dtm = &result.segments[0];
1342 assert_eq!(dtm.elements[0][0], "137");
1343 assert_eq!(dtm.elements[0][1], "202501010000+01");
1344 assert_eq!(dtm.elements[0][2], "303");
1345 }
1346
1347 #[test]
1348 fn test_group_instance_as_assembled_tree() {
1349 let sg5 = AssembledGroup {
1351 group_id: "SG5".to_string(),
1352 repetitions: vec![AssembledGroupInstance {
1353 segments: vec![AssembledSegment {
1354 tag: "LOC".to_string(),
1355 elements: vec![vec!["Z16".to_string(), "DE000111222333".to_string()]],
1356 mig_number: None,
1357 segment_number: None,
1358 }],
1359 child_groups: vec![],
1360 entry_mig_number: None,
1361 variant_mig_numbers: vec![],
1362 skipped_segments: vec![],
1363 skipped_positions: Vec::new(),
1364 }],
1365 };
1366
1367 let sg4_instance = AssembledGroupInstance {
1368 segments: vec![
1369 AssembledSegment {
1370 tag: "IDE".to_string(),
1371 elements: vec![vec!["24".to_string(), "TX001".to_string()]],
1372 mig_number: None,
1373 segment_number: None,
1374 },
1375 AssembledSegment {
1376 tag: "STS".to_string(),
1377 elements: vec![vec!["7".to_string()]],
1378 mig_number: None,
1379 segment_number: None,
1380 },
1381 ],
1382 child_groups: vec![sg5],
1383 entry_mig_number: None,
1384 variant_mig_numbers: vec![],
1385 skipped_segments: vec![],
1386 skipped_positions: Vec::new(),
1387 };
1388
1389 let sub_tree = sg4_instance.as_assembled_tree();
1390
1391 assert_eq!(sub_tree.segments.len(), 2);
1393 assert_eq!(sub_tree.segments[0].tag, "IDE");
1394 assert_eq!(sub_tree.segments[1].tag, "STS");
1395
1396 assert_eq!(sub_tree.groups.len(), 1);
1398 assert_eq!(sub_tree.groups[0].group_id, "SG5");
1399
1400 assert_eq!(sub_tree.post_group_start, 2);
1402 }
1403
1404 #[test]
1405 fn test_assembler_from_parsed_edifact() {
1406 let input = b"UNA:+.? 'UNB+UNOC:3+SENDER+RECEIVER+210101:1200+REF001'UNH+MSG001+UTILMD:D:11A:UN:S2.1'BGM+E01+DOC001+9'DTM+137:20250101:102'UNT+3+MSG001'UNZ+1+REF001'";
1408 let segments = crate::tokenize::parse_to_segments(input).unwrap();
1409
1410 let mig = make_mig_schema(vec!["UNB", "UNH", "BGM", "DTM", "UNT", "UNZ"], vec![]);
1411
1412 let assembler = Assembler::new(&mig);
1413 let result = assembler.assemble_generic(&segments).unwrap();
1414
1415 assert!(result.segments.iter().any(|s| s.tag == "UNH"));
1416 assert!(result.segments.iter().any(|s| s.tag == "BGM"));
1417 assert!(result.segments.iter().any(|s| s.tag == "DTM"));
1418 }
1419
1420 #[test]
1421 fn test_assemble_with_diagnostics_clean_input() {
1422 let mig = make_mig_schema(vec!["UNH", "BGM", "UNT"], vec![]);
1423 let segments = vec![
1424 make_owned_seg("UNH", vec![vec!["001"]]),
1425 make_owned_seg("BGM", vec![vec!["E01"]]),
1426 make_owned_seg("UNT", vec![vec!["2", "001"]]),
1427 ];
1428 let assembler = Assembler::new(&mig);
1429 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1430 assert_eq!(tree.segments.len(), 3);
1431 assert!(
1432 diagnostics.is_empty(),
1433 "Clean input should have no diagnostics"
1434 );
1435 }
1436
1437 #[test]
1438 fn test_assemble_with_diagnostics_unconsumed_segments() {
1439 let mig = make_mig_schema(vec!["UNH", "BGM"], vec![]);
1440 let segments = vec![
1441 make_owned_seg("UNH", vec![vec!["001"]]),
1442 make_owned_seg("BGM", vec![vec!["E01"]]),
1443 make_owned_seg("FTX", vec![vec!["AAA", "extra text"]]),
1444 ];
1445 let assembler = Assembler::new(&mig);
1446 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1447 assert_eq!(tree.segments.len(), 2);
1448 assert_eq!(diagnostics.len(), 1);
1449 assert_eq!(
1450 diagnostics[0].kind,
1451 StructureDiagnosticKind::UnexpectedSegment
1452 );
1453 assert_eq!(diagnostics[0].segment_id, "FTX");
1454 assert_eq!(diagnostics[0].position, 2);
1455 }
1456
1457 #[test]
1458 fn test_assemble_with_diagnostics_multiple_unconsumed() {
1459 let mig = make_mig_schema(vec!["UNH"], vec![]);
1460 let segments = vec![
1461 make_owned_seg("UNH", vec![vec!["001"]]),
1462 make_owned_seg("FOO", vec![]),
1463 make_owned_seg("BAR", vec![]),
1464 make_owned_seg("BAZ", vec![]),
1465 ];
1466 let assembler = Assembler::new(&mig);
1467 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1468 assert_eq!(tree.segments.len(), 1);
1469 assert_eq!(diagnostics.len(), 3);
1470 assert_eq!(diagnostics[0].segment_id, "FOO");
1471 assert_eq!(diagnostics[1].segment_id, "BAR");
1472 assert_eq!(diagnostics[2].segment_id, "BAZ");
1473 }
1474
1475 #[test]
1478 fn test_non_entry_segments_get_mig_number_from_bounded_slots() {
1479 use crate::test_support::make_mig_segment_numbered;
1483
1484 let sg4 = MigSegmentGroup {
1485 segments: vec![
1486 make_mig_segment_numbered("IDE", "00020"),
1487 make_mig_segment_numbered("DTM", "00023"),
1488 make_mig_segment_numbered("DTM", "00024"),
1489 make_mig_segment_numbered("STS", "00035"),
1490 ],
1491 ..make_mig_group("SG4", vec![], vec![])
1492 };
1493 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1494
1495 let segments = vec![
1496 make_owned_seg("UNH", vec![vec!["001"]]),
1497 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1498 make_owned_seg("DTM", vec![vec!["92", "202505312200+00", "303"]]),
1499 make_owned_seg("DTM", vec![vec!["93", "202512312300+00", "303"]]),
1500 make_owned_seg("STS", vec![vec!["7"], vec![], vec!["E01"]]),
1501 ];
1502
1503 let assembler = Assembler::new(&mig);
1504 let tree = assembler.assemble_generic(&segments).unwrap();
1505
1506 let sg4_instance = &tree.groups[0].repetitions[0];
1507
1508 assert_eq!(sg4_instance.segments[0].tag, "IDE");
1510 assert_eq!(
1511 sg4_instance.segments[0].mig_number.as_deref(),
1512 Some("00020")
1513 );
1514
1515 assert_eq!(sg4_instance.segments[1].tag, "DTM");
1517 assert_eq!(
1518 sg4_instance.segments[1].mig_number.as_deref(),
1519 Some("00023")
1520 );
1521
1522 assert_eq!(sg4_instance.segments[2].tag, "DTM");
1524 assert_eq!(
1525 sg4_instance.segments[2].mig_number.as_deref(),
1526 Some("00024")
1527 );
1528
1529 assert_eq!(sg4_instance.segments[3].tag, "STS");
1531 assert_eq!(
1532 sg4_instance.segments[3].mig_number.as_deref(),
1533 Some("00035")
1534 );
1535
1536 assert!(sg4_instance
1538 .variant_mig_numbers
1539 .contains(&"00020".to_string()));
1540 assert!(sg4_instance
1541 .variant_mig_numbers
1542 .contains(&"00023".to_string()));
1543 assert!(sg4_instance
1544 .variant_mig_numbers
1545 .contains(&"00024".to_string()));
1546 assert!(sg4_instance
1547 .variant_mig_numbers
1548 .contains(&"00035".to_string()));
1549 }
1550
1551 #[test]
1552 fn test_greedy_extra_segments_get_no_mig_number() {
1553 use crate::test_support::make_mig_segment_numbered;
1556
1557 let sg4 = MigSegmentGroup {
1558 segments: vec![
1559 make_mig_segment_numbered("IDE", "00020"),
1560 make_mig_segment_numbered("DTM", "00023"),
1561 ],
1562 ..make_mig_group("SG4", vec![], vec![])
1563 };
1564 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1565
1566 let segments = vec![
1567 make_owned_seg("UNH", vec![vec!["001"]]),
1568 make_owned_seg("IDE", vec![vec!["24"]]),
1569 make_owned_seg("DTM", vec![vec!["92", "20250531"]]),
1570 make_owned_seg("DTM", vec![vec!["93", "20251231"]]), ];
1572
1573 let assembler = Assembler::new(&mig);
1574 let tree = assembler.assemble_generic(&segments).unwrap();
1575
1576 let sg4_instance = &tree.groups[0].repetitions[0];
1577 assert_eq!(sg4_instance.segments.len(), 3); assert_eq!(
1581 sg4_instance.segments[1].mig_number.as_deref(),
1582 Some("00023")
1583 );
1584
1585 assert_eq!(sg4_instance.segments[2].mig_number, None);
1587 }
1588
1589 #[test]
1592 fn test_qualifier_map_prevents_wrong_slot_consumption() {
1593 use crate::test_support::make_mig_segment_numbered;
1598 use std::collections::HashMap;
1599
1600 let sg4 = MigSegmentGroup {
1601 segments: vec![
1602 make_mig_segment_numbered("IDE", "00020"),
1603 make_mig_segment_numbered("DTM", "00023"),
1604 make_mig_segment_numbered("DTM", "00024"),
1605 ],
1606 ..make_mig_group("SG4", vec![], vec![])
1607 };
1608 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1609
1610 let segments = vec![
1611 make_owned_seg("UNH", vec![vec!["001"]]),
1612 make_owned_seg("IDE", vec![vec!["24"]]),
1613 make_owned_seg("DTM", vec![vec!["93", "202512312300+00", "303"]]),
1614 ];
1615
1616 let mut qualifier_map = HashMap::new();
1617 qualifier_map.insert("00023".to_string(), (0, 0, "92".to_string()));
1618 qualifier_map.insert("00024".to_string(), (0, 0, "93".to_string()));
1619
1620 let config = AssemblerConfig {
1621 skip_unknown_segments: false,
1622 qualifier_map,
1623 ..Default::default()
1624 };
1625 let assembler = Assembler::with_config(&mig, config);
1626 let tree = assembler.assemble_generic(&segments).unwrap();
1627
1628 let sg4_instance = &tree.groups[0].repetitions[0];
1629
1630 assert_eq!(sg4_instance.segments.len(), 2); let dtm = &sg4_instance.segments[1];
1633 assert_eq!(dtm.tag, "DTM");
1634 assert_eq!(
1635 dtm.mig_number.as_deref(),
1636 Some("00024"),
1637 "DTM+93 should get mig_number 00024 (not 00023)"
1638 );
1639 }
1640
1641 #[test]
1642 fn test_group_entry_qualifier_mismatch_does_not_infinite_loop() {
1643 use crate::test_support::make_mig_segment_numbered;
1653 use std::collections::HashMap;
1654
1655 let sg5 = MigSegmentGroup {
1656 segments: vec![make_mig_segment_numbered("LOC", "00050")],
1657 ..make_mig_group("SG5", vec![], vec![])
1658 };
1659 let mig = make_mig_schema(vec!["UNH"], vec![sg5]);
1660
1661 let segments = vec![
1662 make_owned_seg("UNH", vec![vec!["001"]]),
1663 make_owned_seg("LOC", vec![vec!["172"], vec!["92003964705"]]),
1665 ];
1666
1667 let mut qualifier_map = HashMap::new();
1668 qualifier_map.insert("00050".to_string(), (0, 0, "Z16".to_string()));
1669
1670 let config = AssemblerConfig {
1671 skip_unknown_segments: false,
1672 qualifier_map,
1673 ..Default::default()
1674 };
1675 let assembler = Assembler::with_config(&mig, config);
1676
1677 let start = std::time::Instant::now();
1681 let tree = assembler.assemble_generic(&segments).unwrap();
1682 assert!(
1683 start.elapsed() < std::time::Duration::from_secs(5),
1684 "assembly took {:?} — suspected infinite-loop regression",
1685 start.elapsed()
1686 );
1687
1688 assert!(tree.groups.is_empty());
1692 }
1693
1694 #[test]
1697 fn test_skip_unknown_segment_between_slots() {
1698 let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
1702 let mig = make_mig_schema(vec!["UNH"], vec![sg8.clone()]);
1703
1704 let segments = vec![
1705 make_owned_seg("UNH", vec![vec!["001"]]),
1706 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1707 make_owned_seg("RFF", vec![vec!["Z38", "CROSSREF"]]),
1708 make_owned_seg("CCI", vec![vec!["Z30"]]),
1709 ];
1710
1711 let off = Assembler::new(&mig);
1713 let tree_off = off.assemble_generic(&segments).unwrap();
1714 let sg8_off = &tree_off.groups[0];
1715 assert_eq!(sg8_off.repetitions[0].segments.len(), 1); assert_eq!(sg8_off.repetitions[0].segments[0].tag, "SEQ");
1717
1718 let on = Assembler::with_config(
1720 &mig,
1721 AssemblerConfig {
1722 skip_unknown_segments: true,
1723 ..Default::default()
1724 },
1725 );
1726 let tree_on = on.assemble_generic(&segments).unwrap();
1727 let sg8_on = &tree_on.groups[0];
1728 assert_eq!(sg8_on.repetitions[0].segments.len(), 2); assert_eq!(sg8_on.repetitions[0].segments[0].tag, "SEQ");
1730 assert_eq!(sg8_on.repetitions[0].segments[1].tag, "CCI");
1731 }
1732
1733 #[test]
1734 fn test_skip_preserves_on_instance() {
1735 let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
1737 let mig = make_mig_schema(vec!["UNH"], vec![sg8]);
1738
1739 let segments = vec![
1740 make_owned_seg("UNH", vec![vec!["001"]]),
1741 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1742 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
1743 make_owned_seg("DTM", vec![vec!["92", "20250101"]]),
1744 make_owned_seg("CCI", vec![vec!["Z30"]]),
1745 ];
1746
1747 let assembler = Assembler::with_config(
1748 &mig,
1749 AssemblerConfig {
1750 skip_unknown_segments: true,
1751 ..Default::default()
1752 },
1753 );
1754 let tree = assembler.assemble_generic(&segments).unwrap();
1755 let instance = &tree.groups[0].repetitions[0];
1756
1757 assert_eq!(instance.segments.len(), 2); assert_eq!(instance.skipped_segments.len(), 2); assert_eq!(instance.skipped_segments[0].tag, "RFF");
1760 assert_eq!(instance.skipped_segments[1].tag, "DTM");
1761 }
1762
1763 #[test]
1764 fn test_skip_mode_off_default() {
1765 let mig = make_mig_schema(vec![], vec![]);
1767 let assembler = Assembler::new(&mig);
1768 assert!(!assembler.config.skip_unknown_segments);
1769 }
1770
1771 #[test]
1772 fn test_skip_does_not_consume_nested_group_entry() {
1773 let sg5 = make_mig_group("SG5", vec!["LOC"], vec![]);
1777 let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![sg5]);
1778 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1779
1780 let segments = vec![
1781 make_owned_seg("UNH", vec![vec!["001"]]),
1782 make_owned_seg("IDE", vec![vec!["24"]]),
1783 make_owned_seg("FOO", vec![vec!["unknown"]]),
1784 make_owned_seg("STS", vec![vec!["7"]]),
1785 make_owned_seg("LOC", vec![vec!["Z16"]]),
1786 ];
1787
1788 let assembler = Assembler::with_config(
1789 &mig,
1790 AssemblerConfig {
1791 skip_unknown_segments: true,
1792 ..Default::default()
1793 },
1794 );
1795 let tree = assembler.assemble_generic(&segments).unwrap();
1796 let sg4 = &tree.groups[0];
1797 let inst = &sg4.repetitions[0];
1798
1799 assert_eq!(inst.segments.len(), 2);
1801 assert_eq!(inst.segments[0].tag, "IDE");
1802 assert_eq!(inst.segments[1].tag, "STS");
1803 assert_eq!(inst.skipped_segments.len(), 1);
1804 assert_eq!(inst.skipped_segments[0].tag, "FOO");
1805
1806 assert_eq!(inst.child_groups.len(), 1);
1808 assert_eq!(inst.child_groups[0].group_id, "SG5");
1809 assert_eq!(inst.child_groups[0].repetitions[0].segments[0].tag, "LOC");
1810 }
1811
1812 #[test]
1817 fn test_diagnostics_orphaned_group_segment_without_entry() {
1818 let sg10 = make_mig_group("SG10", vec!["CCI", "CAV"], vec![]);
1819 let sg8 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10], "Z03");
1820 let sg12 = make_mig_group("SG12", vec!["NAD"], vec![]);
1821 let sg4 = make_mig_group("SG4", vec!["IDE"], vec![sg8, sg12]);
1822 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1823
1824 let segments = vec![
1825 make_owned_seg("UNH", vec![vec!["001"]]),
1826 make_owned_seg("IDE", vec![vec!["24"]]),
1827 make_owned_seg("SEQ", vec![vec!["Z03"]]),
1828 make_owned_seg("CAV", vec![vec!["Z30", "", "", "X"]]),
1829 make_owned_seg("FOO", vec![vec!["unknown"]]),
1830 make_owned_seg("NAD", vec![vec!["Z07"]]),
1831 ];
1832
1833 let assembler = Assembler::with_config(
1834 &mig,
1835 AssemblerConfig {
1836 skip_unknown_segments: true,
1837 ..Default::default()
1838 },
1839 );
1840 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1841
1842 let sg4 = &tree.groups[0].repetitions[0];
1844 assert!(sg4.child_groups.iter().any(|g| g.group_id == "SG12"));
1845
1846 let orphaned: Vec<_> = diagnostics
1847 .iter()
1848 .filter(|d| d.kind == StructureDiagnosticKind::OrphanedGroupSegment)
1849 .collect();
1850 assert_eq!(orphaned.len(), 1, "{diagnostics:?}");
1851 assert_eq!(orphaned[0].segment_id, "CAV");
1852 assert_eq!(orphaned[0].position, 3);
1853 assert!(
1854 orphaned[0].message.contains("SG10") && orphaned[0].message.contains("'CCI'"),
1855 "message must name the group and its missing entry segment: {}",
1856 orphaned[0].message
1857 );
1858 assert!(
1859 !orphaned[0].message.contains("not defined"),
1860 "CAV is defined in the MIG: {}",
1861 orphaned[0].message
1862 );
1863
1864 let skipped: Vec<_> = diagnostics
1866 .iter()
1867 .filter(|d| d.kind == StructureDiagnosticKind::SkippedUnknownSegment)
1868 .collect();
1869 assert_eq!(skipped.len(), 1, "{diagnostics:?}");
1870 assert_eq!(skipped[0].segment_id, "FOO");
1871 }
1872
1873 #[test]
1874 fn test_skip_unknown_between_nested_group_reps() {
1875 let sg10 = make_mig_group("SG10", vec!["CCI"], vec![]);
1893 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10.clone()], "ZD7");
1894 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![], "Z98");
1895 let sg12 = make_mig_group("SG12", vec!["NAD"], vec![]);
1896 let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![sg8_zd7, sg8_z98, sg12]);
1897 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1898
1899 let segments = vec![
1900 make_owned_seg("UNH", vec![vec!["001"]]),
1901 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1902 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
1903 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
1904 make_owned_seg("CCI", vec![vec!["Z30"]]),
1905 make_owned_seg("FOO", vec![vec!["orphan"]]),
1906 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1907 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1908 ];
1909
1910 let assembler = Assembler::with_config(
1911 &mig,
1912 AssemblerConfig {
1913 skip_unknown_segments: true,
1914 ..Default::default()
1915 },
1916 );
1917 let tree = assembler.assemble_generic(&segments).unwrap();
1918
1919 assert_eq!(tree.groups.len(), 1);
1920 let sg4_inst = &tree.groups[0].repetitions[0];
1921 assert_eq!(sg4_inst.child_groups.len(), 2, "expected SG8 + SG12");
1923 let sg8_tree = &sg4_inst.child_groups[0];
1924 assert_eq!(sg8_tree.group_id, "SG8");
1925 assert_eq!(
1926 sg8_tree.repetitions.len(),
1927 2,
1928 "SG8 should have both ZD7 and Z98 reps after orphan skip"
1929 );
1930 assert_eq!(sg8_tree.repetitions[0].segments[0].elements[0][0], "ZD7");
1931 assert_eq!(sg8_tree.repetitions[1].segments[0].elements[0][0], "Z98");
1932
1933 let sg12_tree = &sg4_inst.child_groups[1];
1934 assert_eq!(sg12_tree.group_id, "SG12");
1935 assert_eq!(sg12_tree.repetitions.len(), 1);
1936
1937 fn count_foo(inst: &AssembledGroupInstance) -> usize {
1942 let mut n = inst
1943 .skipped_segments
1944 .iter()
1945 .filter(|s| s.tag == "FOO")
1946 .count();
1947 for child in &inst.child_groups {
1948 for rep in &child.repetitions {
1949 n += count_foo(rep);
1950 }
1951 }
1952 n
1953 }
1954 assert_eq!(
1955 count_foo(sg4_inst),
1956 1,
1957 "FOO should be recorded exactly once"
1958 );
1959 }
1960
1961 #[test]
1962 fn test_skip_off_preserves_cascade_behavior() {
1963 let sg10 = make_mig_group("SG10", vec!["CCI"], vec![]);
1967 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10], "ZD7");
1968 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![], "Z98");
1969 let sg12 = make_mig_group("SG12", vec!["NAD"], vec![]);
1970 let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![sg8_zd7, sg8_z98, sg12]);
1971 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1972
1973 let segments = vec![
1974 make_owned_seg("UNH", vec![vec!["001"]]),
1975 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1976 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
1977 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
1978 make_owned_seg("CCI", vec![vec!["Z30"]]),
1979 make_owned_seg("FOO", vec![vec!["orphan"]]),
1980 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1981 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1982 ];
1983
1984 let assembler = Assembler::new(&mig);
1985 let tree = assembler.assemble_generic(&segments).unwrap();
1986
1987 let sg4_inst = &tree.groups[0].repetitions[0];
1988 let sg8_tree = sg4_inst
1989 .child_groups
1990 .iter()
1991 .find(|g| g.group_id == "SG8")
1992 .expect("SG8 should still be present");
1993 assert_eq!(sg8_tree.repetitions.len(), 1);
1995 assert!(
1996 sg4_inst.skipped_segments.iter().all(|s| s.tag != "FOO"),
1997 "FOO must not be skipped when skip mode is off"
1998 );
1999 }
2000
2001 #[test]
2002 fn test_roundtrip_with_skip() {
2003 use crate::disassembler::Disassembler;
2006 use crate::renderer::render_edifact;
2007
2008 let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
2009 let mig = make_mig_schema(vec!["UNH", "UNT"], vec![sg8]);
2010
2011 let segments = vec![
2012 make_owned_seg("UNH", vec![vec!["001"]]),
2013 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2014 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
2015 make_owned_seg("CCI", vec![vec!["Z30"]]),
2016 make_owned_seg("UNT", vec![vec!["4", "001"]]),
2017 ];
2018
2019 let assembler = Assembler::with_config(
2020 &mig,
2021 AssemblerConfig {
2022 skip_unknown_segments: true,
2023 ..Default::default()
2024 },
2025 );
2026 let tree = assembler.assemble_generic(&segments).unwrap();
2027
2028 let disassembler = Disassembler::new(&mig);
2029 let dis = disassembler.disassemble(&tree);
2030 let delimiters = edifact_primitives::EdifactDelimiters::default();
2031 let rendered = render_edifact(&dis, &delimiters);
2032
2033 assert_eq!(dis.len(), 5);
2038 assert_eq!(dis[0].tag, "UNH");
2039 assert_eq!(dis[1].tag, "SEQ");
2040 assert_eq!(dis[2].tag, "CCI");
2041 assert_eq!(dis[3].tag, "RFF"); assert_eq!(dis[4].tag, "UNT");
2043
2044 assert!(rendered.contains("UNH+001"));
2046 assert!(rendered.contains("SEQ+Z98"));
2047 assert!(rendered.contains("RFF+Z38:REF1"));
2048 assert!(rendered.contains("CCI+Z30"));
2049 assert!(rendered.contains("UNT+4:001"));
2050 }
2051
2052 #[test]
2055 fn test_variant_groups_interleaved_reps() {
2056 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
2060 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
2061
2062 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
2063
2064 let segments = vec![
2065 make_owned_seg("UNH", vec![vec!["001"]]),
2066 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
2067 make_owned_seg("CCI", vec![vec!["Z30"]]),
2068 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2069 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
2070 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
2071 make_owned_seg("CCI", vec![vec!["Z31"]]),
2072 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2073 make_owned_seg("RFF", vec![vec!["Z38", "REF2"]]),
2074 ];
2075
2076 let assembler = Assembler::new(&mig);
2077 let result = assembler.assemble_generic(&segments).unwrap();
2078
2079 assert_eq!(result.segments.len(), 1); assert_eq!(result.groups.len(), 1); let sg8 = &result.groups[0];
2082 assert_eq!(sg8.group_id, "SG8");
2083 assert_eq!(sg8.repetitions.len(), 4);
2084
2085 assert_eq!(sg8.repetitions[0].segments[0].elements[0][0], "ZD7");
2087 assert_eq!(sg8.repetitions[0].segments[1].tag, "CCI");
2088 assert_eq!(sg8.repetitions[1].segments[0].elements[0][0], "Z98");
2089 assert_eq!(sg8.repetitions[1].segments[1].tag, "RFF");
2090 assert_eq!(sg8.repetitions[2].segments[0].elements[0][0], "ZD7");
2091 assert_eq!(sg8.repetitions[3].segments[0].elements[0][0], "Z98");
2092 }
2093
2094 #[test]
2095 fn test_variant_groups_single_variant_type() {
2096 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
2098 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
2099
2100 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
2101
2102 let segments = vec![
2103 make_owned_seg("UNH", vec![vec!["001"]]),
2104 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2105 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
2106 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2107 make_owned_seg("RFF", vec![vec!["Z38", "REF2"]]),
2108 ];
2109
2110 let assembler = Assembler::new(&mig);
2111 let result = assembler.assemble_generic(&segments).unwrap();
2112
2113 assert_eq!(result.groups.len(), 1);
2114 assert_eq!(result.groups[0].repetitions.len(), 2);
2115 assert_eq!(
2116 result.groups[0].repetitions[0].segments[0].elements[0][0],
2117 "Z98"
2118 );
2119 assert_eq!(
2120 result.groups[0].repetitions[1].segments[0].elements[0][0],
2121 "Z98"
2122 );
2123 }
2124
2125 #[test]
2126 fn test_non_variant_groups_unchanged() {
2127 let sg2 = make_mig_group("SG2", vec!["NAD"], vec![]);
2129 let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![]);
2130
2131 let mig = make_mig_schema(vec!["UNH", "BGM"], vec![sg2, sg4]);
2132
2133 let segments = vec![
2134 make_owned_seg("UNH", vec![vec!["001"]]),
2135 make_owned_seg("BGM", vec![vec!["E01"]]),
2136 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
2137 make_owned_seg("NAD", vec![vec!["MR", "9900456"]]),
2138 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
2139 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
2140 ];
2141
2142 let assembler = Assembler::new(&mig);
2143 let result = assembler.assemble_generic(&segments).unwrap();
2144
2145 assert_eq!(result.segments.len(), 2);
2146 assert_eq!(result.groups.len(), 2);
2147 assert_eq!(result.groups[0].group_id, "SG2");
2148 assert_eq!(result.groups[0].repetitions.len(), 2);
2149 assert_eq!(result.groups[1].group_id, "SG4");
2150 assert_eq!(result.groups[1].repetitions.len(), 1);
2151 }
2152
2153 #[test]
2154 fn test_variant_groups_with_nested_children() {
2155 let sg10 = make_mig_group("SG10", vec!["CCI", "CAV"], vec![]);
2157 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10.clone()], "ZD7");
2158 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10], "Z98");
2159
2160 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
2161
2162 let segments = vec![
2163 make_owned_seg("UNH", vec![vec!["001"]]),
2164 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
2165 make_owned_seg("CCI", vec![vec!["Z30"]]),
2166 make_owned_seg("CAV", vec![vec!["Z91", "Y"]]),
2167 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2168 make_owned_seg("CCI", vec![vec!["Z31"]]),
2169 make_owned_seg("CAV", vec![vec!["Z91", "N"]]),
2170 ];
2171
2172 let assembler = Assembler::new(&mig);
2173 let result = assembler.assemble_generic(&segments).unwrap();
2174
2175 assert_eq!(result.groups.len(), 1);
2176 let sg8 = &result.groups[0];
2177 assert_eq!(sg8.repetitions.len(), 2);
2178
2179 assert_eq!(sg8.repetitions[0].child_groups.len(), 1);
2181 assert_eq!(sg8.repetitions[0].child_groups[0].group_id, "SG10");
2182 assert_eq!(
2183 sg8.repetitions[0].child_groups[0].repetitions[0].segments[0].elements[0][0],
2184 "Z30"
2185 );
2186
2187 assert_eq!(sg8.repetitions[1].child_groups.len(), 1);
2189 assert_eq!(
2190 sg8.repetitions[1].child_groups[0].repetitions[0].segments[0].elements[0][0],
2191 "Z31"
2192 );
2193 }
2194
2195 #[test]
2196 fn test_variant_qualifier_check_prevents_wrong_variant_consumption() {
2197 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
2200
2201 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7]);
2202
2203 let segments = vec![
2204 make_owned_seg("UNH", vec![vec!["001"]]),
2205 make_owned_seg("SEQ", vec![vec!["Z98"]]), make_owned_seg("CCI", vec![vec!["Z30"]]),
2207 ];
2208
2209 let assembler = Assembler::new(&mig);
2210 let result = assembler.assemble_generic(&segments).unwrap();
2211
2212 assert!(result.groups.is_empty());
2214 }
2215
2216 #[test]
2217 fn test_mixed_variant_and_non_variant_groups() {
2218 let sg2 = make_mig_group("SG2", vec!["NAD"], vec![]);
2220 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
2221 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
2222 let sg12 = make_mig_group("SG12", vec!["NAD"], vec![]);
2223
2224 let mig = make_mig_schema(vec!["UNH"], vec![sg2, sg8_zd7, sg8_z98, sg12]);
2225
2226 let segments = vec![
2227 make_owned_seg("UNH", vec![vec!["001"]]),
2228 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
2229 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
2230 make_owned_seg("CCI", vec![vec!["Z30"]]),
2231 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2232 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
2233 make_owned_seg("NAD", vec![vec!["Z65", "ID001"]]),
2234 ];
2235
2236 let assembler = Assembler::new(&mig);
2237 let result = assembler.assemble_generic(&segments).unwrap();
2238
2239 assert_eq!(result.groups.len(), 3); assert_eq!(result.groups[0].group_id, "SG2");
2241 assert_eq!(result.groups[0].repetitions.len(), 1);
2242 assert_eq!(result.groups[1].group_id, "SG8");
2243 assert_eq!(result.groups[1].repetitions.len(), 2);
2244 assert_eq!(result.groups[2].group_id, "SG12");
2245 assert_eq!(result.groups[2].repetitions.len(), 1);
2246 }
2247
2248 #[test]
2249 fn test_assembler_disambiguates_shared_qualifier_by_full_code_profile() {
2250 use mig_types::schema::common::CodeDefinition;
2258 use mig_types::schema::mig::{MigComposite, MigDataElement};
2259 use std::collections::HashMap;
2260
2261 fn code(value: &str) -> CodeDefinition {
2262 CodeDefinition {
2263 value: value.to_string(),
2264 name: value.to_string(),
2265 description: None,
2266 }
2267 }
2268
2269 fn pia_slot(number: &str, composite_code: &str) -> MigSegment {
2270 MigSegment {
2271 id: "PIA".to_string(),
2272 name: "PIA".to_string(),
2273 description: None,
2274 counter: None,
2275 level: 1,
2276 number: Some(number.to_string()),
2277 max_rep_std: 1,
2278 max_rep_spec: 1,
2279 status_std: Some("M".to_string()),
2280 status_spec: Some("M".to_string()),
2281 example: None,
2282 data_elements: vec![MigDataElement {
2283 id: "4347".to_string(),
2284 name: "Produkt-ID-Funktion".to_string(),
2285 description: None,
2286 status_std: Some("M".to_string()),
2287 status_spec: Some("M".to_string()),
2288 format_std: None,
2289 format_spec: None,
2290 codes: vec![code("5")],
2291 position: 0,
2292 }],
2293 composites: vec![MigComposite {
2294 id: "C212".to_string(),
2295 name: "Item Identifier".to_string(),
2296 description: None,
2297 status_std: Some("M".to_string()),
2298 status_spec: Some("M".to_string()),
2299 data_elements: vec![MigDataElement {
2300 id: "7143".to_string(),
2301 name: "Artikel/Dienstleistung-ID".to_string(),
2302 description: None,
2303 status_std: Some("M".to_string()),
2304 status_spec: Some("M".to_string()),
2305 format_std: None,
2306 format_spec: None,
2307 codes: vec![code(composite_code)],
2308 position: 0,
2309 }],
2310 position: 1,
2311 }],
2312 }
2313 }
2314
2315 let sg4 = MigSegmentGroup {
2316 segments: vec![
2317 crate::test_support::make_mig_segment_numbered("IDE", "00020"),
2318 pia_slot("00108", "Z12"),
2319 pia_slot("00197", "SRW"),
2320 ],
2321 ..make_mig_group("SG4", vec![], vec![])
2322 };
2323 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
2324
2325 let segments = vec![
2326 make_owned_seg("UNH", vec![vec!["001"]]),
2327 make_owned_seg("IDE", vec![vec!["24"]]),
2328 make_owned_seg("PIA", vec![vec!["5"], vec!["SRW"]]),
2330 ];
2331
2332 let mut qualifier_map = HashMap::new();
2336 qualifier_map.insert("00108".to_string(), (0, 0, "5".to_string()));
2337 qualifier_map.insert("00197".to_string(), (0, 0, "5".to_string()));
2338
2339 let config = AssemblerConfig {
2340 skip_unknown_segments: false,
2341 qualifier_map,
2342 strict_code_matching: true,
2343 };
2344 let assembler = Assembler::with_config(&mig, config);
2345 let tree = assembler.assemble_generic(&segments).unwrap();
2346
2347 let sg4_instance = &tree.groups[0].repetitions[0];
2348 let pia = sg4_instance
2349 .segments
2350 .iter()
2351 .find(|s| s.tag == "PIA")
2352 .expect("PIA consumed into SG4");
2353 assert_eq!(
2354 pia.mig_number.as_deref(),
2355 Some("00197"),
2356 "PIA+5+:::SRW must be assigned the SRW variant (mig=00197), not the Z12 variant"
2357 );
2358 }
2359}