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, PartialEq, 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, PartialEq, Serialize, Deserialize)]
54pub struct AssembledGroup {
55 pub group_id: String,
56 pub repetitions: Vec<AssembledGroupInstance>,
57}
58
59#[derive(Debug, Clone, PartialEq, 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 for q in &nested.variant_entry_qualifiers {
1129 for n in &q.mig_numbers {
1130 if !numbers.contains(n) {
1131 numbers.push(n.clone());
1132 }
1133 }
1134 }
1135 }
1136 numbers
1137}
1138
1139pub fn owned_to_assembled(seg: &OwnedSegment) -> AssembledSegment {
1140 AssembledSegment {
1141 tag: seg.id.clone(),
1142 elements: seg.elements.clone(),
1143 mig_number: None,
1144 segment_number: Some(seg.segment_number),
1145 }
1146}
1147
1148fn segment_matches_mig_codes(seg: &OwnedSegment, mig_seg: &MigSegment) -> bool {
1156 let actual_at = |el: usize, c: usize| -> &str {
1157 seg.elements
1158 .get(el)
1159 .and_then(|e| e.get(c))
1160 .map(|s| s.as_str())
1161 .unwrap_or("")
1162 };
1163 for de in &mig_seg.data_elements {
1164 if !de.codes.is_empty() {
1165 let actual = actual_at(de.position, 0);
1166 if !actual.is_empty() && !de.codes.iter().any(|c| c.value == actual) {
1167 return false;
1168 }
1169 }
1170 }
1171 for comp in &mig_seg.composites {
1172 for de in &comp.data_elements {
1173 if !de.codes.is_empty() {
1174 let actual = actual_at(comp.position, de.position);
1175 if !actual.is_empty() && !de.codes.iter().any(|c| c.value == actual) {
1176 return false;
1177 }
1178 }
1179 }
1180 }
1181 true
1182}
1183
1184#[cfg(test)]
1185mod tests {
1186 use super::*;
1187 use crate::test_support::{make_mig_group, make_mig_group_with_variant, make_mig_segment};
1188
1189 fn make_owned_seg(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
1190 OwnedSegment {
1191 id: id.to_string(),
1192 elements: elements
1193 .into_iter()
1194 .map(|e| e.into_iter().map(|c| c.to_string()).collect())
1195 .collect(),
1196 segment_number: 0,
1197 }
1198 }
1199
1200 fn make_mig_schema(segments: Vec<&str>, groups: Vec<MigSegmentGroup>) -> MigSchema {
1201 MigSchema {
1202 message_type: "UTILMD".to_string(),
1203 variant: Some("Strom".to_string()),
1204 version: "S2.1".to_string(),
1205 publication_date: "2025-03-20".to_string(),
1206 author: "BDEW".to_string(),
1207 format_version: "FV2504".to_string(),
1208 source_file: "test".to_string(),
1209 segments: segments.into_iter().map(make_mig_segment).collect(),
1210 segment_groups: groups,
1211 }
1212 }
1213
1214 #[test]
1215 fn test_assembler_top_level_segments_only() {
1216 let mig = make_mig_schema(vec!["UNH", "BGM", "DTM", "UNT"], vec![]);
1217
1218 let segments = vec![
1219 make_owned_seg("UNH", vec![vec!["001", "UTILMD:D:11A:UN:S2.1"]]),
1220 make_owned_seg("BGM", vec![vec!["E01", "DOC001"]]),
1221 make_owned_seg("DTM", vec![vec!["137", "20250101", "102"]]),
1222 make_owned_seg("UNT", vec![vec!["4", "001"]]),
1223 ];
1224
1225 let assembler = Assembler::new(&mig);
1226 let result = assembler.assemble_generic(&segments).unwrap();
1227
1228 assert_eq!(result.segments.len(), 4);
1229 assert_eq!(result.segments[0].tag, "UNH");
1230 assert_eq!(result.segments[1].tag, "BGM");
1231 assert_eq!(result.segments[2].tag, "DTM");
1232 assert_eq!(result.segments[3].tag, "UNT");
1233 assert!(result.groups.is_empty());
1234 }
1235
1236 #[test]
1237 fn test_assembler_with_segment_group() {
1238 let mig = make_mig_schema(
1239 vec!["UNH", "BGM"],
1240 vec![
1241 make_mig_group("SG2", vec!["NAD"], vec![]),
1242 make_mig_group("SG4", vec!["IDE", "STS"], vec![]),
1243 ],
1244 );
1245
1246 let segments = vec![
1247 make_owned_seg("UNH", vec![vec!["001"]]),
1248 make_owned_seg("BGM", vec![vec!["E01"]]),
1249 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1250 make_owned_seg("NAD", vec![vec!["MR", "9900456"]]),
1251 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1252 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
1253 ];
1254
1255 let assembler = Assembler::new(&mig);
1256 let result = assembler.assemble_generic(&segments).unwrap();
1257
1258 assert_eq!(result.segments.len(), 2);
1260 assert_eq!(result.groups.len(), 2);
1262 assert_eq!(result.groups[0].group_id, "SG2");
1263 assert_eq!(result.groups[0].repetitions.len(), 2);
1264 assert_eq!(result.groups[0].repetitions[0].segments[0].tag, "NAD");
1265 assert_eq!(result.groups[0].repetitions[1].segments[0].tag, "NAD");
1266 assert_eq!(result.groups[1].group_id, "SG4");
1268 assert_eq!(result.groups[1].repetitions.len(), 1);
1269 assert_eq!(result.groups[1].repetitions[0].segments.len(), 2);
1270 }
1271
1272 #[test]
1273 fn test_assembler_nested_groups() {
1274 let sg3 = make_mig_group("SG3", vec!["CTA", "COM"], vec![]);
1275 let mig = make_mig_schema(
1276 vec!["UNH", "BGM"],
1277 vec![make_mig_group("SG2", vec!["NAD"], vec![sg3])],
1278 );
1279
1280 let segments = vec![
1281 make_owned_seg("UNH", vec![vec!["001"]]),
1282 make_owned_seg("BGM", vec![vec!["E01"]]),
1283 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1284 make_owned_seg("CTA", vec![vec!["IC", "Kontakt"]]),
1285 make_owned_seg("COM", vec![vec!["040@example.com", "EM"]]),
1286 ];
1287
1288 let assembler = Assembler::new(&mig);
1289 let result = assembler.assemble_generic(&segments).unwrap();
1290
1291 let sg2 = &result.groups[0];
1293 assert_eq!(sg2.group_id, "SG2");
1294 assert_eq!(sg2.repetitions.len(), 1);
1295
1296 let sg2_inst = &sg2.repetitions[0];
1297 assert_eq!(sg2_inst.segments[0].tag, "NAD");
1298
1299 assert_eq!(sg2_inst.child_groups.len(), 1);
1301 let sg3 = &sg2_inst.child_groups[0];
1302 assert_eq!(sg3.group_id, "SG3");
1303 assert_eq!(sg3.repetitions[0].segments.len(), 2);
1304 assert_eq!(sg3.repetitions[0].segments[0].tag, "CTA");
1305 assert_eq!(sg3.repetitions[0].segments[1].tag, "COM");
1306 }
1307
1308 #[test]
1309 fn test_assembler_optional_segments_skipped() {
1310 let mig = make_mig_schema(vec!["UNH", "BGM", "DTM", "UNT"], vec![]);
1312
1313 let segments = vec![
1314 make_owned_seg("UNH", vec![vec!["001"]]),
1315 make_owned_seg("BGM", vec![vec!["E01"]]),
1316 make_owned_seg("UNT", vec![vec!["2", "001"]]),
1317 ];
1318
1319 let assembler = Assembler::new(&mig);
1320 let result = assembler.assemble_generic(&segments).unwrap();
1321
1322 assert_eq!(result.segments.len(), 3);
1324 assert_eq!(result.segments[0].tag, "UNH");
1325 assert_eq!(result.segments[1].tag, "BGM");
1326 assert_eq!(result.segments[2].tag, "UNT");
1327 }
1328
1329 #[test]
1330 fn test_assembler_empty_segments() {
1331 let mig = make_mig_schema(vec!["UNH"], vec![]);
1332 let assembler = Assembler::new(&mig);
1333 let result = assembler.assemble_generic(&[]).unwrap();
1334 assert!(result.segments.is_empty());
1335 assert!(result.groups.is_empty());
1336 }
1337
1338 #[test]
1339 fn test_assembler_preserves_element_data() {
1340 let mig = make_mig_schema(vec!["DTM"], vec![]);
1341
1342 let segments = vec![make_owned_seg(
1343 "DTM",
1344 vec![vec!["137", "202501010000+01", "303"]],
1345 )];
1346
1347 let assembler = Assembler::new(&mig);
1348 let result = assembler.assemble_generic(&segments).unwrap();
1349
1350 let dtm = &result.segments[0];
1351 assert_eq!(dtm.elements[0][0], "137");
1352 assert_eq!(dtm.elements[0][1], "202501010000+01");
1353 assert_eq!(dtm.elements[0][2], "303");
1354 }
1355
1356 #[test]
1357 fn test_group_instance_as_assembled_tree() {
1358 let sg5 = AssembledGroup {
1360 group_id: "SG5".to_string(),
1361 repetitions: vec![AssembledGroupInstance {
1362 segments: vec![AssembledSegment {
1363 tag: "LOC".to_string(),
1364 elements: vec![vec!["Z16".to_string(), "DE000111222333".to_string()]],
1365 mig_number: None,
1366 segment_number: None,
1367 }],
1368 child_groups: vec![],
1369 entry_mig_number: None,
1370 variant_mig_numbers: vec![],
1371 skipped_segments: vec![],
1372 skipped_positions: Vec::new(),
1373 }],
1374 };
1375
1376 let sg4_instance = AssembledGroupInstance {
1377 segments: vec![
1378 AssembledSegment {
1379 tag: "IDE".to_string(),
1380 elements: vec![vec!["24".to_string(), "TX001".to_string()]],
1381 mig_number: None,
1382 segment_number: None,
1383 },
1384 AssembledSegment {
1385 tag: "STS".to_string(),
1386 elements: vec![vec!["7".to_string()]],
1387 mig_number: None,
1388 segment_number: None,
1389 },
1390 ],
1391 child_groups: vec![sg5],
1392 entry_mig_number: None,
1393 variant_mig_numbers: vec![],
1394 skipped_segments: vec![],
1395 skipped_positions: Vec::new(),
1396 };
1397
1398 let sub_tree = sg4_instance.as_assembled_tree();
1399
1400 assert_eq!(sub_tree.segments.len(), 2);
1402 assert_eq!(sub_tree.segments[0].tag, "IDE");
1403 assert_eq!(sub_tree.segments[1].tag, "STS");
1404
1405 assert_eq!(sub_tree.groups.len(), 1);
1407 assert_eq!(sub_tree.groups[0].group_id, "SG5");
1408
1409 assert_eq!(sub_tree.post_group_start, 2);
1411 }
1412
1413 #[test]
1414 fn test_assembler_from_parsed_edifact() {
1415 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'";
1417 let segments = crate::tokenize::parse_to_segments(input).unwrap();
1418
1419 let mig = make_mig_schema(vec!["UNB", "UNH", "BGM", "DTM", "UNT", "UNZ"], vec![]);
1420
1421 let assembler = Assembler::new(&mig);
1422 let result = assembler.assemble_generic(&segments).unwrap();
1423
1424 assert!(result.segments.iter().any(|s| s.tag == "UNH"));
1425 assert!(result.segments.iter().any(|s| s.tag == "BGM"));
1426 assert!(result.segments.iter().any(|s| s.tag == "DTM"));
1427 }
1428
1429 #[test]
1430 fn test_assemble_with_diagnostics_clean_input() {
1431 let mig = make_mig_schema(vec!["UNH", "BGM", "UNT"], vec![]);
1432 let segments = vec![
1433 make_owned_seg("UNH", vec![vec!["001"]]),
1434 make_owned_seg("BGM", vec![vec!["E01"]]),
1435 make_owned_seg("UNT", vec![vec!["2", "001"]]),
1436 ];
1437 let assembler = Assembler::new(&mig);
1438 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1439 assert_eq!(tree.segments.len(), 3);
1440 assert!(
1441 diagnostics.is_empty(),
1442 "Clean input should have no diagnostics"
1443 );
1444 }
1445
1446 #[test]
1447 fn test_assemble_with_diagnostics_unconsumed_segments() {
1448 let mig = make_mig_schema(vec!["UNH", "BGM"], vec![]);
1449 let segments = vec![
1450 make_owned_seg("UNH", vec![vec!["001"]]),
1451 make_owned_seg("BGM", vec![vec!["E01"]]),
1452 make_owned_seg("FTX", vec![vec!["AAA", "extra text"]]),
1453 ];
1454 let assembler = Assembler::new(&mig);
1455 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1456 assert_eq!(tree.segments.len(), 2);
1457 assert_eq!(diagnostics.len(), 1);
1458 assert_eq!(
1459 diagnostics[0].kind,
1460 StructureDiagnosticKind::UnexpectedSegment
1461 );
1462 assert_eq!(diagnostics[0].segment_id, "FTX");
1463 assert_eq!(diagnostics[0].position, 2);
1464 }
1465
1466 #[test]
1467 fn test_assemble_with_diagnostics_multiple_unconsumed() {
1468 let mig = make_mig_schema(vec!["UNH"], vec![]);
1469 let segments = vec![
1470 make_owned_seg("UNH", vec![vec!["001"]]),
1471 make_owned_seg("FOO", vec![]),
1472 make_owned_seg("BAR", vec![]),
1473 make_owned_seg("BAZ", vec![]),
1474 ];
1475 let assembler = Assembler::new(&mig);
1476 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1477 assert_eq!(tree.segments.len(), 1);
1478 assert_eq!(diagnostics.len(), 3);
1479 assert_eq!(diagnostics[0].segment_id, "FOO");
1480 assert_eq!(diagnostics[1].segment_id, "BAR");
1481 assert_eq!(diagnostics[2].segment_id, "BAZ");
1482 }
1483
1484 #[test]
1487 fn test_non_entry_segments_get_mig_number_from_bounded_slots() {
1488 use crate::test_support::make_mig_segment_numbered;
1492
1493 let sg4 = MigSegmentGroup {
1494 segments: vec![
1495 make_mig_segment_numbered("IDE", "00020"),
1496 make_mig_segment_numbered("DTM", "00023"),
1497 make_mig_segment_numbered("DTM", "00024"),
1498 make_mig_segment_numbered("STS", "00035"),
1499 ],
1500 ..make_mig_group("SG4", vec![], vec![])
1501 };
1502 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1503
1504 let segments = vec![
1505 make_owned_seg("UNH", vec![vec!["001"]]),
1506 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1507 make_owned_seg("DTM", vec![vec!["92", "202505312200+00", "303"]]),
1508 make_owned_seg("DTM", vec![vec!["93", "202512312300+00", "303"]]),
1509 make_owned_seg("STS", vec![vec!["7"], vec![], vec!["E01"]]),
1510 ];
1511
1512 let assembler = Assembler::new(&mig);
1513 let tree = assembler.assemble_generic(&segments).unwrap();
1514
1515 let sg4_instance = &tree.groups[0].repetitions[0];
1516
1517 assert_eq!(sg4_instance.segments[0].tag, "IDE");
1519 assert_eq!(
1520 sg4_instance.segments[0].mig_number.as_deref(),
1521 Some("00020")
1522 );
1523
1524 assert_eq!(sg4_instance.segments[1].tag, "DTM");
1526 assert_eq!(
1527 sg4_instance.segments[1].mig_number.as_deref(),
1528 Some("00023")
1529 );
1530
1531 assert_eq!(sg4_instance.segments[2].tag, "DTM");
1533 assert_eq!(
1534 sg4_instance.segments[2].mig_number.as_deref(),
1535 Some("00024")
1536 );
1537
1538 assert_eq!(sg4_instance.segments[3].tag, "STS");
1540 assert_eq!(
1541 sg4_instance.segments[3].mig_number.as_deref(),
1542 Some("00035")
1543 );
1544
1545 assert!(sg4_instance
1547 .variant_mig_numbers
1548 .contains(&"00020".to_string()));
1549 assert!(sg4_instance
1550 .variant_mig_numbers
1551 .contains(&"00023".to_string()));
1552 assert!(sg4_instance
1553 .variant_mig_numbers
1554 .contains(&"00024".to_string()));
1555 assert!(sg4_instance
1556 .variant_mig_numbers
1557 .contains(&"00035".to_string()));
1558 }
1559
1560 #[test]
1561 fn test_greedy_extra_segments_get_no_mig_number() {
1562 use crate::test_support::make_mig_segment_numbered;
1565
1566 let sg4 = MigSegmentGroup {
1567 segments: vec![
1568 make_mig_segment_numbered("IDE", "00020"),
1569 make_mig_segment_numbered("DTM", "00023"),
1570 ],
1571 ..make_mig_group("SG4", vec![], vec![])
1572 };
1573 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1574
1575 let segments = vec![
1576 make_owned_seg("UNH", vec![vec!["001"]]),
1577 make_owned_seg("IDE", vec![vec!["24"]]),
1578 make_owned_seg("DTM", vec![vec!["92", "20250531"]]),
1579 make_owned_seg("DTM", vec![vec!["93", "20251231"]]), ];
1581
1582 let assembler = Assembler::new(&mig);
1583 let tree = assembler.assemble_generic(&segments).unwrap();
1584
1585 let sg4_instance = &tree.groups[0].repetitions[0];
1586 assert_eq!(sg4_instance.segments.len(), 3); assert_eq!(
1590 sg4_instance.segments[1].mig_number.as_deref(),
1591 Some("00023")
1592 );
1593
1594 assert_eq!(sg4_instance.segments[2].mig_number, None);
1596 }
1597
1598 #[test]
1601 fn test_qualifier_map_prevents_wrong_slot_consumption() {
1602 use crate::test_support::make_mig_segment_numbered;
1607 use std::collections::HashMap;
1608
1609 let sg4 = MigSegmentGroup {
1610 segments: vec![
1611 make_mig_segment_numbered("IDE", "00020"),
1612 make_mig_segment_numbered("DTM", "00023"),
1613 make_mig_segment_numbered("DTM", "00024"),
1614 ],
1615 ..make_mig_group("SG4", vec![], vec![])
1616 };
1617 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1618
1619 let segments = vec![
1620 make_owned_seg("UNH", vec![vec!["001"]]),
1621 make_owned_seg("IDE", vec![vec!["24"]]),
1622 make_owned_seg("DTM", vec![vec!["93", "202512312300+00", "303"]]),
1623 ];
1624
1625 let mut qualifier_map = HashMap::new();
1626 qualifier_map.insert("00023".to_string(), (0, 0, "92".to_string()));
1627 qualifier_map.insert("00024".to_string(), (0, 0, "93".to_string()));
1628
1629 let config = AssemblerConfig {
1630 skip_unknown_segments: false,
1631 qualifier_map,
1632 ..Default::default()
1633 };
1634 let assembler = Assembler::with_config(&mig, config);
1635 let tree = assembler.assemble_generic(&segments).unwrap();
1636
1637 let sg4_instance = &tree.groups[0].repetitions[0];
1638
1639 assert_eq!(sg4_instance.segments.len(), 2); let dtm = &sg4_instance.segments[1];
1642 assert_eq!(dtm.tag, "DTM");
1643 assert_eq!(
1644 dtm.mig_number.as_deref(),
1645 Some("00024"),
1646 "DTM+93 should get mig_number 00024 (not 00023)"
1647 );
1648 }
1649
1650 #[test]
1651 fn test_group_entry_qualifier_mismatch_does_not_infinite_loop() {
1652 use crate::test_support::make_mig_segment_numbered;
1662 use std::collections::HashMap;
1663
1664 let sg5 = MigSegmentGroup {
1665 segments: vec![make_mig_segment_numbered("LOC", "00050")],
1666 ..make_mig_group("SG5", vec![], vec![])
1667 };
1668 let mig = make_mig_schema(vec!["UNH"], vec![sg5]);
1669
1670 let segments = vec![
1671 make_owned_seg("UNH", vec![vec!["001"]]),
1672 make_owned_seg("LOC", vec![vec!["172"], vec!["92003964705"]]),
1674 ];
1675
1676 let mut qualifier_map = HashMap::new();
1677 qualifier_map.insert("00050".to_string(), (0, 0, "Z16".to_string()));
1678
1679 let config = AssemblerConfig {
1680 skip_unknown_segments: false,
1681 qualifier_map,
1682 ..Default::default()
1683 };
1684 let assembler = Assembler::with_config(&mig, config);
1685
1686 let start = std::time::Instant::now();
1690 let tree = assembler.assemble_generic(&segments).unwrap();
1691 assert!(
1692 start.elapsed() < std::time::Duration::from_secs(5),
1693 "assembly took {:?} — suspected infinite-loop regression",
1694 start.elapsed()
1695 );
1696
1697 assert!(tree.groups.is_empty());
1701 }
1702
1703 #[test]
1706 fn test_skip_unknown_segment_between_slots() {
1707 let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
1711 let mig = make_mig_schema(vec!["UNH"], vec![sg8.clone()]);
1712
1713 let segments = vec![
1714 make_owned_seg("UNH", vec![vec!["001"]]),
1715 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1716 make_owned_seg("RFF", vec![vec!["Z38", "CROSSREF"]]),
1717 make_owned_seg("CCI", vec![vec!["Z30"]]),
1718 ];
1719
1720 let off = Assembler::new(&mig);
1722 let tree_off = off.assemble_generic(&segments).unwrap();
1723 let sg8_off = &tree_off.groups[0];
1724 assert_eq!(sg8_off.repetitions[0].segments.len(), 1); assert_eq!(sg8_off.repetitions[0].segments[0].tag, "SEQ");
1726
1727 let on = Assembler::with_config(
1729 &mig,
1730 AssemblerConfig {
1731 skip_unknown_segments: true,
1732 ..Default::default()
1733 },
1734 );
1735 let tree_on = on.assemble_generic(&segments).unwrap();
1736 let sg8_on = &tree_on.groups[0];
1737 assert_eq!(sg8_on.repetitions[0].segments.len(), 2); assert_eq!(sg8_on.repetitions[0].segments[0].tag, "SEQ");
1739 assert_eq!(sg8_on.repetitions[0].segments[1].tag, "CCI");
1740 }
1741
1742 #[test]
1743 fn test_skip_preserves_on_instance() {
1744 let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
1746 let mig = make_mig_schema(vec!["UNH"], vec![sg8]);
1747
1748 let segments = vec![
1749 make_owned_seg("UNH", vec![vec!["001"]]),
1750 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1751 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
1752 make_owned_seg("DTM", vec![vec!["92", "20250101"]]),
1753 make_owned_seg("CCI", vec![vec!["Z30"]]),
1754 ];
1755
1756 let assembler = Assembler::with_config(
1757 &mig,
1758 AssemblerConfig {
1759 skip_unknown_segments: true,
1760 ..Default::default()
1761 },
1762 );
1763 let tree = assembler.assemble_generic(&segments).unwrap();
1764 let instance = &tree.groups[0].repetitions[0];
1765
1766 assert_eq!(instance.segments.len(), 2); assert_eq!(instance.skipped_segments.len(), 2); assert_eq!(instance.skipped_segments[0].tag, "RFF");
1769 assert_eq!(instance.skipped_segments[1].tag, "DTM");
1770 }
1771
1772 #[test]
1773 fn test_skip_mode_off_default() {
1774 let mig = make_mig_schema(vec![], vec![]);
1776 let assembler = Assembler::new(&mig);
1777 assert!(!assembler.config.skip_unknown_segments);
1778 }
1779
1780 #[test]
1781 fn test_skip_does_not_consume_nested_group_entry() {
1782 let sg5 = make_mig_group("SG5", vec!["LOC"], vec![]);
1786 let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![sg5]);
1787 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1788
1789 let segments = vec![
1790 make_owned_seg("UNH", vec![vec!["001"]]),
1791 make_owned_seg("IDE", vec![vec!["24"]]),
1792 make_owned_seg("FOO", vec![vec!["unknown"]]),
1793 make_owned_seg("STS", vec![vec!["7"]]),
1794 make_owned_seg("LOC", vec![vec!["Z16"]]),
1795 ];
1796
1797 let assembler = Assembler::with_config(
1798 &mig,
1799 AssemblerConfig {
1800 skip_unknown_segments: true,
1801 ..Default::default()
1802 },
1803 );
1804 let tree = assembler.assemble_generic(&segments).unwrap();
1805 let sg4 = &tree.groups[0];
1806 let inst = &sg4.repetitions[0];
1807
1808 assert_eq!(inst.segments.len(), 2);
1810 assert_eq!(inst.segments[0].tag, "IDE");
1811 assert_eq!(inst.segments[1].tag, "STS");
1812 assert_eq!(inst.skipped_segments.len(), 1);
1813 assert_eq!(inst.skipped_segments[0].tag, "FOO");
1814
1815 assert_eq!(inst.child_groups.len(), 1);
1817 assert_eq!(inst.child_groups[0].group_id, "SG5");
1818 assert_eq!(inst.child_groups[0].repetitions[0].segments[0].tag, "LOC");
1819 }
1820
1821 #[test]
1826 fn test_diagnostics_orphaned_group_segment_without_entry() {
1827 let sg10 = make_mig_group("SG10", vec!["CCI", "CAV"], vec![]);
1828 let sg8 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10], "Z03");
1829 let sg12 = make_mig_group("SG12", vec!["NAD"], vec![]);
1830 let sg4 = make_mig_group("SG4", vec!["IDE"], vec![sg8, sg12]);
1831 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1832
1833 let segments = vec![
1834 make_owned_seg("UNH", vec![vec!["001"]]),
1835 make_owned_seg("IDE", vec![vec!["24"]]),
1836 make_owned_seg("SEQ", vec![vec!["Z03"]]),
1837 make_owned_seg("CAV", vec![vec!["Z30", "", "", "X"]]),
1838 make_owned_seg("FOO", vec![vec!["unknown"]]),
1839 make_owned_seg("NAD", vec![vec!["Z07"]]),
1840 ];
1841
1842 let assembler = Assembler::with_config(
1843 &mig,
1844 AssemblerConfig {
1845 skip_unknown_segments: true,
1846 ..Default::default()
1847 },
1848 );
1849 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1850
1851 let sg4 = &tree.groups[0].repetitions[0];
1853 assert!(sg4.child_groups.iter().any(|g| g.group_id == "SG12"));
1854
1855 let orphaned: Vec<_> = diagnostics
1856 .iter()
1857 .filter(|d| d.kind == StructureDiagnosticKind::OrphanedGroupSegment)
1858 .collect();
1859 assert_eq!(orphaned.len(), 1, "{diagnostics:?}");
1860 assert_eq!(orphaned[0].segment_id, "CAV");
1861 assert_eq!(orphaned[0].position, 3);
1862 assert!(
1863 orphaned[0].message.contains("SG10") && orphaned[0].message.contains("'CCI'"),
1864 "message must name the group and its missing entry segment: {}",
1865 orphaned[0].message
1866 );
1867 assert!(
1868 !orphaned[0].message.contains("not defined"),
1869 "CAV is defined in the MIG: {}",
1870 orphaned[0].message
1871 );
1872
1873 let skipped: Vec<_> = diagnostics
1875 .iter()
1876 .filter(|d| d.kind == StructureDiagnosticKind::SkippedUnknownSegment)
1877 .collect();
1878 assert_eq!(skipped.len(), 1, "{diagnostics:?}");
1879 assert_eq!(skipped[0].segment_id, "FOO");
1880 }
1881
1882 #[test]
1883 fn test_skip_unknown_between_nested_group_reps() {
1884 let sg10 = make_mig_group("SG10", vec!["CCI"], vec![]);
1902 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10.clone()], "ZD7");
1903 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![], "Z98");
1904 let sg12 = make_mig_group("SG12", vec!["NAD"], vec![]);
1905 let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![sg8_zd7, sg8_z98, sg12]);
1906 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1907
1908 let segments = vec![
1909 make_owned_seg("UNH", vec![vec!["001"]]),
1910 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1911 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
1912 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
1913 make_owned_seg("CCI", vec![vec!["Z30"]]),
1914 make_owned_seg("FOO", vec![vec!["orphan"]]),
1915 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1916 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1917 ];
1918
1919 let assembler = Assembler::with_config(
1920 &mig,
1921 AssemblerConfig {
1922 skip_unknown_segments: true,
1923 ..Default::default()
1924 },
1925 );
1926 let tree = assembler.assemble_generic(&segments).unwrap();
1927
1928 assert_eq!(tree.groups.len(), 1);
1929 let sg4_inst = &tree.groups[0].repetitions[0];
1930 assert_eq!(sg4_inst.child_groups.len(), 2, "expected SG8 + SG12");
1932 let sg8_tree = &sg4_inst.child_groups[0];
1933 assert_eq!(sg8_tree.group_id, "SG8");
1934 assert_eq!(
1935 sg8_tree.repetitions.len(),
1936 2,
1937 "SG8 should have both ZD7 and Z98 reps after orphan skip"
1938 );
1939 assert_eq!(sg8_tree.repetitions[0].segments[0].elements[0][0], "ZD7");
1940 assert_eq!(sg8_tree.repetitions[1].segments[0].elements[0][0], "Z98");
1941
1942 let sg12_tree = &sg4_inst.child_groups[1];
1943 assert_eq!(sg12_tree.group_id, "SG12");
1944 assert_eq!(sg12_tree.repetitions.len(), 1);
1945
1946 fn count_foo(inst: &AssembledGroupInstance) -> usize {
1951 let mut n = inst
1952 .skipped_segments
1953 .iter()
1954 .filter(|s| s.tag == "FOO")
1955 .count();
1956 for child in &inst.child_groups {
1957 for rep in &child.repetitions {
1958 n += count_foo(rep);
1959 }
1960 }
1961 n
1962 }
1963 assert_eq!(
1964 count_foo(sg4_inst),
1965 1,
1966 "FOO should be recorded exactly once"
1967 );
1968 }
1969
1970 #[test]
1971 fn test_skip_off_preserves_cascade_behavior() {
1972 let sg10 = make_mig_group("SG10", vec!["CCI"], vec![]);
1976 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10], "ZD7");
1977 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![], "Z98");
1978 let sg12 = make_mig_group("SG12", vec!["NAD"], vec![]);
1979 let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![sg8_zd7, sg8_z98, sg12]);
1980 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1981
1982 let segments = vec![
1983 make_owned_seg("UNH", vec![vec!["001"]]),
1984 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1985 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
1986 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
1987 make_owned_seg("CCI", vec![vec!["Z30"]]),
1988 make_owned_seg("FOO", vec![vec!["orphan"]]),
1989 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1990 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1991 ];
1992
1993 let assembler = Assembler::new(&mig);
1994 let tree = assembler.assemble_generic(&segments).unwrap();
1995
1996 let sg4_inst = &tree.groups[0].repetitions[0];
1997 let sg8_tree = sg4_inst
1998 .child_groups
1999 .iter()
2000 .find(|g| g.group_id == "SG8")
2001 .expect("SG8 should still be present");
2002 assert_eq!(sg8_tree.repetitions.len(), 1);
2004 assert!(
2005 sg4_inst.skipped_segments.iter().all(|s| s.tag != "FOO"),
2006 "FOO must not be skipped when skip mode is off"
2007 );
2008 }
2009
2010 #[test]
2011 fn test_roundtrip_with_skip() {
2012 use crate::disassembler::Disassembler;
2015 use crate::renderer::render_edifact;
2016
2017 let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
2018 let mig = make_mig_schema(vec!["UNH", "UNT"], vec![sg8]);
2019
2020 let segments = vec![
2021 make_owned_seg("UNH", vec![vec!["001"]]),
2022 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2023 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
2024 make_owned_seg("CCI", vec![vec!["Z30"]]),
2025 make_owned_seg("UNT", vec![vec!["4", "001"]]),
2026 ];
2027
2028 let assembler = Assembler::with_config(
2029 &mig,
2030 AssemblerConfig {
2031 skip_unknown_segments: true,
2032 ..Default::default()
2033 },
2034 );
2035 let tree = assembler.assemble_generic(&segments).unwrap();
2036
2037 let disassembler = Disassembler::new(&mig);
2038 let dis = disassembler.disassemble(&tree);
2039 let delimiters = edifact_primitives::EdifactDelimiters::default();
2040 let rendered = render_edifact(&dis, &delimiters);
2041
2042 assert_eq!(dis.len(), 5);
2047 assert_eq!(dis[0].tag, "UNH");
2048 assert_eq!(dis[1].tag, "SEQ");
2049 assert_eq!(dis[2].tag, "CCI");
2050 assert_eq!(dis[3].tag, "RFF"); assert_eq!(dis[4].tag, "UNT");
2052
2053 assert!(rendered.contains("UNH+001"));
2055 assert!(rendered.contains("SEQ+Z98"));
2056 assert!(rendered.contains("RFF+Z38:REF1"));
2057 assert!(rendered.contains("CCI+Z30"));
2058 assert!(rendered.contains("UNT+4:001"));
2059 }
2060
2061 #[test]
2064 fn test_variant_groups_interleaved_reps() {
2065 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
2069 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
2070
2071 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
2072
2073 let segments = vec![
2074 make_owned_seg("UNH", vec![vec!["001"]]),
2075 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
2076 make_owned_seg("CCI", vec![vec!["Z30"]]),
2077 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2078 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
2079 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
2080 make_owned_seg("CCI", vec![vec!["Z31"]]),
2081 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2082 make_owned_seg("RFF", vec![vec!["Z38", "REF2"]]),
2083 ];
2084
2085 let assembler = Assembler::new(&mig);
2086 let result = assembler.assemble_generic(&segments).unwrap();
2087
2088 assert_eq!(result.segments.len(), 1); assert_eq!(result.groups.len(), 1); let sg8 = &result.groups[0];
2091 assert_eq!(sg8.group_id, "SG8");
2092 assert_eq!(sg8.repetitions.len(), 4);
2093
2094 assert_eq!(sg8.repetitions[0].segments[0].elements[0][0], "ZD7");
2096 assert_eq!(sg8.repetitions[0].segments[1].tag, "CCI");
2097 assert_eq!(sg8.repetitions[1].segments[0].elements[0][0], "Z98");
2098 assert_eq!(sg8.repetitions[1].segments[1].tag, "RFF");
2099 assert_eq!(sg8.repetitions[2].segments[0].elements[0][0], "ZD7");
2100 assert_eq!(sg8.repetitions[3].segments[0].elements[0][0], "Z98");
2101 }
2102
2103 #[test]
2104 fn test_variant_groups_single_variant_type() {
2105 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
2107 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
2108
2109 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
2110
2111 let segments = vec![
2112 make_owned_seg("UNH", vec![vec!["001"]]),
2113 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2114 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
2115 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2116 make_owned_seg("RFF", vec![vec!["Z38", "REF2"]]),
2117 ];
2118
2119 let assembler = Assembler::new(&mig);
2120 let result = assembler.assemble_generic(&segments).unwrap();
2121
2122 assert_eq!(result.groups.len(), 1);
2123 assert_eq!(result.groups[0].repetitions.len(), 2);
2124 assert_eq!(
2125 result.groups[0].repetitions[0].segments[0].elements[0][0],
2126 "Z98"
2127 );
2128 assert_eq!(
2129 result.groups[0].repetitions[1].segments[0].elements[0][0],
2130 "Z98"
2131 );
2132 }
2133
2134 #[test]
2135 fn test_non_variant_groups_unchanged() {
2136 let sg2 = make_mig_group("SG2", vec!["NAD"], vec![]);
2138 let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![]);
2139
2140 let mig = make_mig_schema(vec!["UNH", "BGM"], vec![sg2, sg4]);
2141
2142 let segments = vec![
2143 make_owned_seg("UNH", vec![vec!["001"]]),
2144 make_owned_seg("BGM", vec![vec!["E01"]]),
2145 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
2146 make_owned_seg("NAD", vec![vec!["MR", "9900456"]]),
2147 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
2148 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
2149 ];
2150
2151 let assembler = Assembler::new(&mig);
2152 let result = assembler.assemble_generic(&segments).unwrap();
2153
2154 assert_eq!(result.segments.len(), 2);
2155 assert_eq!(result.groups.len(), 2);
2156 assert_eq!(result.groups[0].group_id, "SG2");
2157 assert_eq!(result.groups[0].repetitions.len(), 2);
2158 assert_eq!(result.groups[1].group_id, "SG4");
2159 assert_eq!(result.groups[1].repetitions.len(), 1);
2160 }
2161
2162 #[test]
2163 fn test_variant_groups_with_nested_children() {
2164 let sg10 = make_mig_group("SG10", vec!["CCI", "CAV"], vec![]);
2166 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10.clone()], "ZD7");
2167 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10], "Z98");
2168
2169 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
2170
2171 let segments = vec![
2172 make_owned_seg("UNH", vec![vec!["001"]]),
2173 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
2174 make_owned_seg("CCI", vec![vec!["Z30"]]),
2175 make_owned_seg("CAV", vec![vec!["Z91", "Y"]]),
2176 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2177 make_owned_seg("CCI", vec![vec!["Z31"]]),
2178 make_owned_seg("CAV", vec![vec!["Z91", "N"]]),
2179 ];
2180
2181 let assembler = Assembler::new(&mig);
2182 let result = assembler.assemble_generic(&segments).unwrap();
2183
2184 assert_eq!(result.groups.len(), 1);
2185 let sg8 = &result.groups[0];
2186 assert_eq!(sg8.repetitions.len(), 2);
2187
2188 assert_eq!(sg8.repetitions[0].child_groups.len(), 1);
2190 assert_eq!(sg8.repetitions[0].child_groups[0].group_id, "SG10");
2191 assert_eq!(
2192 sg8.repetitions[0].child_groups[0].repetitions[0].segments[0].elements[0][0],
2193 "Z30"
2194 );
2195
2196 assert_eq!(sg8.repetitions[1].child_groups.len(), 1);
2198 assert_eq!(
2199 sg8.repetitions[1].child_groups[0].repetitions[0].segments[0].elements[0][0],
2200 "Z31"
2201 );
2202 }
2203
2204 #[test]
2205 fn test_variant_qualifier_check_prevents_wrong_variant_consumption() {
2206 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
2209
2210 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7]);
2211
2212 let segments = vec![
2213 make_owned_seg("UNH", vec![vec!["001"]]),
2214 make_owned_seg("SEQ", vec![vec!["Z98"]]), make_owned_seg("CCI", vec![vec!["Z30"]]),
2216 ];
2217
2218 let assembler = Assembler::new(&mig);
2219 let result = assembler.assemble_generic(&segments).unwrap();
2220
2221 assert!(result.groups.is_empty());
2223 }
2224
2225 #[test]
2226 fn test_mixed_variant_and_non_variant_groups() {
2227 let sg2 = make_mig_group("SG2", vec!["NAD"], vec![]);
2229 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
2230 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
2231 let sg12 = make_mig_group("SG12", vec!["NAD"], vec![]);
2232
2233 let mig = make_mig_schema(vec!["UNH"], vec![sg2, sg8_zd7, sg8_z98, sg12]);
2234
2235 let segments = vec![
2236 make_owned_seg("UNH", vec![vec!["001"]]),
2237 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
2238 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
2239 make_owned_seg("CCI", vec![vec!["Z30"]]),
2240 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2241 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
2242 make_owned_seg("NAD", vec![vec!["Z65", "ID001"]]),
2243 ];
2244
2245 let assembler = Assembler::new(&mig);
2246 let result = assembler.assemble_generic(&segments).unwrap();
2247
2248 assert_eq!(result.groups.len(), 3); assert_eq!(result.groups[0].group_id, "SG2");
2250 assert_eq!(result.groups[0].repetitions.len(), 1);
2251 assert_eq!(result.groups[1].group_id, "SG8");
2252 assert_eq!(result.groups[1].repetitions.len(), 2);
2253 assert_eq!(result.groups[2].group_id, "SG12");
2254 assert_eq!(result.groups[2].repetitions.len(), 1);
2255 }
2256
2257 #[test]
2258 fn test_assembler_disambiguates_shared_qualifier_by_full_code_profile() {
2259 use mig_types::schema::common::CodeDefinition;
2267 use mig_types::schema::mig::{MigComposite, MigDataElement};
2268 use std::collections::HashMap;
2269
2270 fn code(value: &str) -> CodeDefinition {
2271 CodeDefinition {
2272 value: value.to_string(),
2273 name: value.to_string(),
2274 description: None,
2275 }
2276 }
2277
2278 fn pia_slot(number: &str, composite_code: &str) -> MigSegment {
2279 MigSegment {
2280 id: "PIA".to_string(),
2281 name: "PIA".to_string(),
2282 description: None,
2283 counter: None,
2284 level: 1,
2285 number: Some(number.to_string()),
2286 max_rep_std: 1,
2287 max_rep_spec: 1,
2288 status_std: Some("M".to_string()),
2289 status_spec: Some("M".to_string()),
2290 example: None,
2291 data_elements: vec![MigDataElement {
2292 id: "4347".to_string(),
2293 name: "Produkt-ID-Funktion".to_string(),
2294 description: None,
2295 status_std: Some("M".to_string()),
2296 status_spec: Some("M".to_string()),
2297 format_std: None,
2298 format_spec: None,
2299 codes: vec![code("5")],
2300 position: 0,
2301 }],
2302 composites: vec![MigComposite {
2303 id: "C212".to_string(),
2304 name: "Item Identifier".to_string(),
2305 description: None,
2306 status_std: Some("M".to_string()),
2307 status_spec: Some("M".to_string()),
2308 data_elements: vec![MigDataElement {
2309 id: "7143".to_string(),
2310 name: "Artikel/Dienstleistung-ID".to_string(),
2311 description: None,
2312 status_std: Some("M".to_string()),
2313 status_spec: Some("M".to_string()),
2314 format_std: None,
2315 format_spec: None,
2316 codes: vec![code(composite_code)],
2317 position: 0,
2318 }],
2319 position: 1,
2320 }],
2321 }
2322 }
2323
2324 let sg4 = MigSegmentGroup {
2325 segments: vec![
2326 crate::test_support::make_mig_segment_numbered("IDE", "00020"),
2327 pia_slot("00108", "Z12"),
2328 pia_slot("00197", "SRW"),
2329 ],
2330 ..make_mig_group("SG4", vec![], vec![])
2331 };
2332 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
2333
2334 let segments = vec![
2335 make_owned_seg("UNH", vec![vec!["001"]]),
2336 make_owned_seg("IDE", vec![vec!["24"]]),
2337 make_owned_seg("PIA", vec![vec!["5"], vec!["SRW"]]),
2339 ];
2340
2341 let mut qualifier_map = HashMap::new();
2345 qualifier_map.insert("00108".to_string(), (0, 0, "5".to_string()));
2346 qualifier_map.insert("00197".to_string(), (0, 0, "5".to_string()));
2347
2348 let config = AssemblerConfig {
2349 skip_unknown_segments: false,
2350 qualifier_map,
2351 strict_code_matching: true,
2352 };
2353 let assembler = Assembler::with_config(&mig, config);
2354 let tree = assembler.assemble_generic(&segments).unwrap();
2355
2356 let sg4_instance = &tree.groups[0].repetitions[0];
2357 let pia = sg4_instance
2358 .segments
2359 .iter()
2360 .find(|s| s.tag == "PIA")
2361 .expect("PIA consumed into SG4");
2362 assert_eq!(
2363 pia.mig_number.as_deref(),
2364 Some("00197"),
2365 "PIA+5+:::SRW must be assigned the SRW variant (mig=00197), not the Z12 variant"
2366 );
2367 }
2368}