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}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct AssembledGroup {
48 pub group_id: String,
49 pub repetitions: Vec<AssembledGroupInstance>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct AssembledGroupInstance {
55 pub segments: Vec<AssembledSegment>,
56 pub child_groups: Vec<AssembledGroup>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub entry_mig_number: Option<String>,
60 #[serde(default, skip_serializing_if = "Vec::is_empty")]
68 pub variant_mig_numbers: Vec<String>,
69 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub skipped_segments: Vec<AssembledSegment>,
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub skipped_positions: Vec<usize>,
79}
80
81impl AssembledGroupInstance {
82 pub fn as_assembled_tree(&self) -> AssembledTree {
89 AssembledTree {
90 segments: self.segments.clone(),
91 groups: self.child_groups.clone(),
92 post_group_start: self.segments.len(),
93 inter_group_segments: std::collections::BTreeMap::new(),
94 }
95 }
96}
97
98#[derive(Debug, Clone, Default)]
100pub struct AssemblerConfig {
101 pub skip_unknown_segments: bool,
108
109 pub qualifier_map: std::collections::HashMap<String, (usize, usize, String)>,
121
122 pub strict_code_matching: bool,
133}
134
135pub struct Assembler<'a> {
140 mig: &'a MigSchema,
141 config: AssemblerConfig,
142}
143
144impl<'a> Assembler<'a> {
145 pub fn new(mig: &'a MigSchema) -> Self {
146 Self {
147 mig,
148 config: AssemblerConfig::default(),
149 }
150 }
151
152 pub fn with_config(mig: &'a MigSchema, config: AssemblerConfig) -> Self {
153 Self { mig, config }
154 }
155
156 fn top_level_enclosing_for_group(
170 &self,
171 current_group_idx: usize,
172 matched_seg_indices: &[usize],
173 ) -> std::collections::HashSet<String> {
174 if !self.config.skip_unknown_segments {
175 return std::collections::HashSet::new();
176 }
177 let mut tags: std::collections::HashSet<String> = self
178 .mig
179 .segments
180 .iter()
181 .enumerate()
182 .filter_map(|(i, s)| {
183 if matched_seg_indices.contains(&i) {
184 None
185 } else {
186 Some(s.id.clone())
187 }
188 })
189 .collect();
190 for (idx, group) in self.mig.segment_groups.iter().enumerate() {
191 if idx < current_group_idx {
192 continue;
193 }
194 if let Some(entry) = group.segments.first() {
195 tags.insert(entry.id.clone());
196 }
197 }
198 tags
199 }
200
201 pub fn assemble_generic(
203 &self,
204 segments: &[OwnedSegment],
205 ) -> Result<AssembledTree, AssemblyError> {
206 let mut cursor = SegmentCursor::new(segments.len());
207 let mut tree = AssembledTree {
208 segments: Vec::new(),
209 groups: Vec::new(),
210 post_group_start: 0,
211 inter_group_segments: std::collections::BTreeMap::new(),
212 };
213
214 let mut matched_seg_indices = Vec::new();
216
217 for (i, mig_seg) in self.mig.segments.iter().enumerate() {
219 if cursor.is_exhausted() {
220 break;
221 }
222 if let Some(assembled) = self.try_consume_segment(segments, &mut cursor, mig_seg)? {
223 tree.segments.push(assembled);
224 matched_seg_indices.push(i);
225 }
226 }
227
228 let mut group_idx = 0;
237 while group_idx < self.mig.segment_groups.len() {
238 if cursor.is_exhausted() {
239 break;
240 }
241
242 let mig_group = &self.mig.segment_groups[group_idx];
243
244 if self.config.skip_unknown_segments {
253 let tree_group_idx = tree.groups.len();
254 while !cursor.is_exhausted() {
255 let seg = &segments[cursor.position()];
256 let tag = &seg.id;
257 let is_unmatched_root_seg = self
258 .mig
259 .segments
260 .iter()
261 .enumerate()
262 .any(|(i, ms)| !matched_seg_indices.contains(&i) && ms.id == *tag);
263 let is_any_group_entry = self
264 .mig
265 .segment_groups
266 .iter()
267 .any(|g| g.segments.first().is_some_and(|s| s.id == *tag));
268 if is_unmatched_root_seg || is_any_group_entry {
269 break;
270 }
271 tree.inter_group_segments
272 .entry(tree_group_idx)
273 .or_default()
274 .push(owned_to_assembled(seg));
275 cursor.advance();
276 }
277 if cursor.is_exhausted() {
278 break;
279 }
280 }
281
282 let tree_group_idx = tree.groups.len();
284 for (i, mig_seg) in self.mig.segments.iter().enumerate() {
285 if cursor.is_exhausted() {
286 break;
287 }
288 if matched_seg_indices.contains(&i) {
289 continue;
290 }
291 if let Some(assembled) = self.try_consume_segment(segments, &mut cursor, mig_seg)? {
292 tree.inter_group_segments
293 .entry(tree_group_idx)
294 .or_default()
295 .push(assembled);
296 matched_seg_indices.push(i);
297 }
298 }
299
300 let top_enclosing =
306 self.top_level_enclosing_for_group(group_idx, &matched_seg_indices);
307
308 if mig_group.variant_code.is_some() {
310 let variant_count = self.mig.segment_groups[group_idx..]
311 .iter()
312 .take_while(|g| g.id == mig_group.id && g.variant_code.is_some())
313 .count();
314 let variant_end = group_idx + variant_count;
315
316 let variant_groups = &self.mig.segment_groups[group_idx..variant_end];
317 if let Some(combined) = self.try_consume_variant_groups(
318 segments,
319 &mut cursor,
320 variant_groups,
321 &top_enclosing,
322 )? {
323 tree.groups.push(combined);
324 }
325 group_idx = variant_end;
326 } else {
327 if let Some(assembled) = self.try_consume_group(
328 segments,
329 &mut cursor,
330 mig_group,
331 &top_enclosing,
332 )? {
333 tree.groups.push(assembled);
334 }
335 group_idx += 1;
336 }
337 }
338
339 tree.post_group_start = tree.segments.len();
341
342 for (i, mig_seg) in self.mig.segments.iter().enumerate() {
344 if cursor.is_exhausted() {
345 break;
346 }
347 if matched_seg_indices.contains(&i) {
348 continue;
349 }
350 if let Some(assembled) = self.try_consume_segment(segments, &mut cursor, mig_seg)? {
351 tree.segments.push(assembled);
352 }
353 }
354
355 Ok(tree)
356 }
357
358 fn try_consume_segment(
359 &self,
360 segments: &[OwnedSegment],
361 cursor: &mut SegmentCursor,
362 mig_seg: &MigSegment,
363 ) -> Result<Option<AssembledSegment>, AssemblyError> {
364 if cursor.is_exhausted() {
365 return Ok(None);
366 }
367 let seg = &segments[cursor.position()];
368 if matcher::matches_segment_tag(&seg.id, &mig_seg.id) {
369 if let Some(ref num) = mig_seg.number {
372 if let Some((el_idx, comp_idx, expected)) = self.config.qualifier_map.get(num) {
373 let actual = seg
374 .elements
375 .get(*el_idx)
376 .and_then(|e| e.get(*comp_idx))
377 .map(|s| s.as_str())
378 .unwrap_or("");
379 if actual != expected {
380 return Ok(None); }
382 }
383 }
384 let mut assembled = owned_to_assembled(seg);
392 assembled.mig_number = mig_seg.number.clone();
393 cursor.advance();
394 Ok(Some(assembled))
395 } else {
396 Ok(None) }
398 }
399
400 fn consume_entry_run_best_match(
410 &self,
411 segments: &[OwnedSegment],
412 cursor: &mut SegmentCursor,
413 entry_slots: &[MigSegment],
414 instance: &mut AssembledGroupInstance,
415 ) -> Result<(), AssemblyError> {
416 let mut used = vec![false; entry_slots.len()];
417 for _ in 0..entry_slots.len() {
418 if cursor.is_exhausted() {
419 break;
420 }
421 let seg = &segments[cursor.position()];
422 let mut strict_match: Option<usize> = None;
423 let mut tag_match: Option<usize> = None;
424 for (i, slot) in entry_slots.iter().enumerate() {
425 if used[i] {
426 continue;
427 }
428 if !matcher::matches_segment_tag(&seg.id, &slot.id) {
429 continue;
430 }
431 if !self.segment_passes_qualifier_map(seg, slot) {
432 continue;
433 }
434 if tag_match.is_none() {
435 tag_match = Some(i);
436 }
437 if strict_match.is_none() && segment_matches_mig_codes(seg, slot) {
438 strict_match = Some(i);
439 }
440 }
441 let Some(i) = strict_match.or(tag_match) else {
442 break;
443 };
444 used[i] = true;
445 let slot = &entry_slots[i];
446 let mut assembled = owned_to_assembled(seg);
447 assembled.mig_number = slot.number.clone();
448 instance.segments.push(assembled);
449 cursor.advance();
450 }
451 Ok(())
452 }
453
454 fn segment_passes_qualifier_map(&self, seg: &OwnedSegment, mig_seg: &MigSegment) -> bool {
455 let Some(ref num) = mig_seg.number else {
456 return true;
457 };
458 let Some((el_idx, comp_idx, expected)) = self.config.qualifier_map.get(num) else {
459 return true;
460 };
461 let actual = seg
462 .elements
463 .get(*el_idx)
464 .and_then(|e| e.get(*comp_idx))
465 .map(|s| s.as_str())
466 .unwrap_or("");
467 actual == expected
468 }
469
470 fn try_consume_group(
471 &self,
472 segments: &[OwnedSegment],
473 cursor: &mut SegmentCursor,
474 mig_group: &MigSegmentGroup,
475 enclosing: &std::collections::HashSet<String>,
476 ) -> Result<Option<AssembledGroup>, AssemblyError> {
477 let mut repetitions = Vec::new();
478 let entry_segment = mig_group.segments.first().ok_or_else(|| {
479 AssemblyError::ParseError(format!("Group {} has no segments", mig_group.id))
480 })?;
481
482 let nested_enclosing: std::collections::HashSet<String> =
487 if self.config.skip_unknown_segments {
488 let mut set = enclosing.clone();
489 set.extend(group_local_scope(mig_group));
490 set
491 } else {
492 std::collections::HashSet::new()
493 };
494
495 while !cursor.is_exhausted() {
497 let iter_start = cursor.position();
498 let seg = &segments[cursor.position()];
499 if !matcher::matches_segment_tag(&seg.id, &entry_segment.id) {
500 break; }
502
503 if !mig_group.variant_codes.is_empty() {
505 let (ei, ci) = mig_group.variant_qualifier_position.unwrap_or((0, 0));
506 let actual_qual = seg
507 .elements
508 .get(ei)
509 .and_then(|e| e.get(ci))
510 .map(|s| s.as_str())
511 .unwrap_or("");
512 if !mig_group
513 .variant_codes
514 .iter()
515 .any(|c| actual_qual.eq_ignore_ascii_case(c))
516 {
517 break;
518 }
519 } else if let Some(ref expected_code) = mig_group.variant_code {
520 let (ei, ci) = mig_group.variant_qualifier_position.unwrap_or((0, 0));
521 let actual_qual = seg
522 .elements
523 .get(ei)
524 .and_then(|e| e.get(ci))
525 .map(|s| s.as_str())
526 .unwrap_or("");
527 if !actual_qual.eq_ignore_ascii_case(expected_code) {
528 break;
529 }
530 }
531
532 let mut instance = AssembledGroupInstance {
533 segments: Vec::new(),
534 child_groups: Vec::new(),
535 entry_mig_number: entry_segment.number.clone(),
536 variant_mig_numbers: collect_mig_numbers(mig_group),
537 skipped_segments: Vec::new(),
538 skipped_positions: Vec::new(),
539 };
540
541 let mut slot_idx = 0;
552 let mut is_entry_run = true;
553 while slot_idx < mig_group.segments.len() {
554 if cursor.is_exhausted() {
555 break;
556 }
557 let current_tag = &mig_group.segments[slot_idx].id;
558 let run_len = mig_group.segments[slot_idx..]
559 .iter()
560 .take_while(|s| s.id == *current_tag)
561 .count();
562
563 if is_entry_run {
564 let entry_slots = &mig_group.segments[slot_idx..slot_idx + run_len];
566 if self.config.strict_code_matching && run_len > 1 {
567 self.consume_entry_run_best_match(
573 segments,
574 cursor,
575 entry_slots,
576 &mut instance,
577 )?;
578 } else {
579 for slot in entry_slots {
580 if cursor.is_exhausted() {
581 break;
582 }
583 if let Some(assembled) =
584 self.try_consume_segment(segments, cursor, slot)?
585 {
586 instance.segments.push(assembled);
587 }
588 }
589 }
590 is_entry_run = false;
591 } else if matcher::matches_segment_tag(current_tag, &entry_segment.id) {
592 if cursor.is_exhausted() {
610 break;
611 }
612 let seg = &segments[cursor.position()];
613 if !matcher::matches_segment_tag(&seg.id, current_tag) {
614 break;
615 }
616 let has_following_non_entry = if cursor.position() + 1 < segments.len() {
619 let next = &segments[cursor.position() + 1];
620 !matcher::matches_segment_tag(&next.id, &entry_segment.id)
621 && mig_group.segments.iter().any(|s| {
622 matcher::matches_segment_tag(&next.id, &s.id)
623 && !matcher::matches_segment_tag(&s.id, &entry_segment.id)
624 })
625 } else {
626 false
627 };
628 if has_following_non_entry {
629 instance.segments.push(owned_to_assembled(seg));
631 cursor.advance();
632 } else {
633 break;
635 }
636 } else {
637 let slots = &mig_group.segments[slot_idx..slot_idx + run_len];
643 if self.config.strict_code_matching && run_len > 1 {
644 self.consume_entry_run_best_match(segments, cursor, slots, &mut instance)?;
645 } else {
646 for slot in slots {
647 if cursor.is_exhausted() {
648 break;
649 }
650 if let Some(assembled) =
651 self.try_consume_segment(segments, cursor, slot)?
652 {
653 instance.segments.push(assembled);
654 }
655 }
656 }
657 while !cursor.is_exhausted() {
659 let seg = &segments[cursor.position()];
660 if matcher::matches_segment_tag(&seg.id, current_tag) {
661 instance.segments.push(owned_to_assembled(seg));
662 cursor.advance();
663 } else {
664 break;
665 }
666 }
667 }
668
669 slot_idx += run_len;
670
671 if self.config.skip_unknown_segments {
678 while !cursor.is_exhausted() {
679 let seg = &segments[cursor.position()];
680 if matcher::matches_segment_tag(&seg.id, &entry_segment.id) {
682 break;
683 }
684 if mig_group.segments[slot_idx..]
686 .iter()
687 .any(|s| matcher::matches_segment_tag(&seg.id, &s.id))
688 {
689 break;
690 }
691 if mig_group.nested_groups.iter().any(|ng| {
693 ng.segments
694 .first()
695 .is_some_and(|es| matcher::matches_segment_tag(&seg.id, &es.id))
696 }) {
697 break;
698 }
699 if enclosing.contains(&seg.id) {
705 break;
706 }
707 instance.skipped_positions.push(cursor.position());
709 instance.skipped_segments.push(owned_to_assembled(seg));
710 cursor.advance();
711 }
712 }
713 }
714
715 loop {
725 let pass_start = cursor.position();
726 let mut nested_idx = 0;
727 while nested_idx < mig_group.nested_groups.len() {
728 if cursor.is_exhausted() {
729 break;
730 }
731 let nested = &mig_group.nested_groups[nested_idx];
732
733 if nested.variant_code.is_some() {
734 let variant_count = mig_group.nested_groups[nested_idx..]
736 .iter()
737 .take_while(|g| g.id == nested.id && g.variant_code.is_some())
738 .count();
739 let variant_end = nested_idx + variant_count;
740 let variant_groups = &mig_group.nested_groups[nested_idx..variant_end];
741 if let Some(combined) = self.try_consume_variant_groups(
742 segments,
743 cursor,
744 variant_groups,
745 &nested_enclosing,
746 )? {
747 push_or_merge_child(&mut instance.child_groups, combined);
748 }
749 nested_idx = variant_end;
750 } else {
751 if let Some(assembled) =
752 self.try_consume_group(segments, cursor, nested, &nested_enclosing)?
753 {
754 push_or_merge_child(&mut instance.child_groups, assembled);
755 }
756 nested_idx += 1;
757 }
758 }
759
760 if !self.config.skip_unknown_segments || cursor.is_exhausted() {
761 break;
762 }
763 let seg = &segments[cursor.position()];
764 if enclosing.contains(&seg.id) {
779 break;
780 }
781 if cursor.position() == pass_start && !self.config.skip_unknown_segments {
785 break;
786 }
787
788 instance.skipped_positions.push(cursor.position());
789 instance.skipped_segments.push(owned_to_assembled(seg));
790 cursor.advance();
791 }
792
793 if cursor.position() == iter_start {
799 break;
800 }
801 repetitions.push(instance);
802 }
803
804 if repetitions.is_empty() {
805 Ok(None)
806 } else {
807 Ok(Some(AssembledGroup {
808 group_id: mig_group.id.clone(),
809 repetitions,
810 }))
811 }
812 }
813
814 fn try_consume_variant_groups(
820 &self,
821 segments: &[OwnedSegment],
822 cursor: &mut SegmentCursor,
823 variants: &[MigSegmentGroup],
824 enclosing: &std::collections::HashSet<String>,
825 ) -> Result<Option<AssembledGroup>, AssemblyError> {
826 let group_id = variants[0].id.clone();
827 let entry_tag = variants[0]
828 .segments
829 .first()
830 .map(|s| s.id.as_str())
831 .unwrap_or("");
832 let mut all_reps = Vec::new();
833
834 while !cursor.is_exhausted() {
835 let seg = &segments[cursor.position()];
836 if !matcher::matches_segment_tag(&seg.id, entry_tag) {
837 break;
838 }
839
840 let matched = variants.iter().find(|v| {
844 let (ei, ci) = v.variant_qualifier_position.unwrap_or((0, 0));
845 let actual_qual = seg
846 .elements
847 .get(ei)
848 .and_then(|e| e.get(ci))
849 .map(|s| s.as_str())
850 .unwrap_or("");
851 if !v.variant_codes.is_empty() {
852 v.variant_codes
853 .iter()
854 .any(|c| actual_qual.eq_ignore_ascii_case(c))
855 } else if let Some(ref expected_code) = v.variant_code {
856 actual_qual.eq_ignore_ascii_case(expected_code)
857 } else {
858 false
859 }
860 });
861
862 if let Some(variant) = matched {
863 if let Some(group) =
864 self.try_consume_group(segments, cursor, variant, enclosing)?
865 {
866 all_reps.extend(group.repetitions);
867 } else {
868 break;
869 }
870 } else {
871 if let Some(group) =
875 self.try_consume_group(segments, cursor, &variants[0], enclosing)?
876 {
877 all_reps.extend(group.repetitions);
878 } else {
879 break;
880 }
881 }
882 }
883
884 if all_reps.is_empty() {
885 Ok(None)
886 } else {
887 Ok(Some(AssembledGroup {
888 group_id,
889 repetitions: all_reps,
890 }))
891 }
892 }
893
894 pub fn assemble_with_diagnostics(
899 &self,
900 segments: &[OwnedSegment],
901 ) -> (AssembledTree, Vec<StructureDiagnostic>) {
902 let mut diagnostics = Vec::new();
903
904 let tree = match self.assemble_generic(segments) {
905 Ok(tree) => tree,
906 Err(e) => {
907 diagnostics.push(StructureDiagnostic {
908 kind: StructureDiagnosticKind::UnexpectedSegment,
909 segment_id: String::new(),
910 position: 0,
911 message: format!("Assembly failed: {e}"),
912 });
913 return (
914 AssembledTree {
915 segments: Vec::new(),
916 groups: Vec::new(),
917 post_group_start: 0,
918 inter_group_segments: std::collections::BTreeMap::new(),
919 },
920 diagnostics,
921 );
922 }
923 };
924
925 let consumed = count_tree_segments(&tree);
929
930 for (i, seg) in segments.iter().enumerate().skip(consumed) {
933 diagnostics.push(StructureDiagnostic {
934 kind: StructureDiagnosticKind::UnexpectedSegment,
935 segment_id: seg.id.clone(),
936 position: i,
937 message: format!(
938 "Segment '{}' at position {} was not consumed by MIG-guided assembly",
939 seg.id, i
940 ),
941 });
942 }
943
944 let mut skipped: Vec<(usize, String)> = Vec::new();
948 collect_skipped(&tree, &mut skipped);
949 skipped.sort_by_key(|(pos, _)| *pos);
950 for (pos, tag) in skipped {
951 diagnostics.push(StructureDiagnostic {
952 kind: StructureDiagnosticKind::SkippedUnknownSegment,
953 segment_id: tag.clone(),
954 position: pos,
955 message: format!(
956 "Segment '{tag}' at position {pos} is not defined in the PID-filtered MIG; the assembler advanced past it",
957 ),
958 });
959 }
960
961 (tree, diagnostics)
962 }
963}
964
965fn collect_skipped(tree: &AssembledTree, out: &mut Vec<(usize, String)>) {
966 for group in &tree.groups {
967 collect_skipped_from_group(group, out);
968 }
969}
970
971fn collect_skipped_from_group(group: &AssembledGroup, out: &mut Vec<(usize, String)>) {
972 for rep in &group.repetitions {
973 for (i, seg) in rep.skipped_segments.iter().enumerate() {
974 let pos = rep.skipped_positions.get(i).copied().unwrap_or(0);
975 out.push((pos, seg.tag.clone()));
976 }
977 for child in &rep.child_groups {
978 collect_skipped_from_group(child, out);
979 }
980 }
981}
982
983fn count_tree_segments(tree: &AssembledTree) -> usize {
984 let mut count = tree.segments.len();
985 for group in &tree.groups {
986 count += count_group_segments(group);
987 }
988 for segs in tree.inter_group_segments.values() {
990 count += segs.len();
991 }
992 count
993}
994
995fn count_group_segments(group: &AssembledGroup) -> usize {
996 let mut count = 0;
997 for rep in &group.repetitions {
998 count += rep.segments.len();
999 count += rep.skipped_segments.len();
1000 for child in &rep.child_groups {
1001 count += count_group_segments(child);
1002 }
1003 }
1004 count
1005}
1006
1007fn group_local_scope(mig_group: &MigSegmentGroup) -> std::collections::HashSet<String> {
1013 let mut tags = std::collections::HashSet::new();
1014 for seg in &mig_group.segments {
1015 tags.insert(seg.id.clone());
1016 }
1017 for nested in &mig_group.nested_groups {
1018 if let Some(entry) = nested.segments.first() {
1019 tags.insert(entry.id.clone());
1020 }
1021 }
1022 tags
1023}
1024
1025fn push_or_merge_child(child_groups: &mut Vec<AssembledGroup>, new: AssembledGroup) {
1030 if let Some(existing) = child_groups.iter_mut().find(|g| g.group_id == new.group_id) {
1031 existing.repetitions.extend(new.repetitions);
1032 } else {
1033 child_groups.push(new);
1034 }
1035}
1036
1037fn collect_mig_numbers(group: &MigSegmentGroup) -> Vec<String> {
1042 let mut numbers = Vec::new();
1043 for seg in &group.segments {
1044 if let Some(ref num) = seg.number {
1045 numbers.push(num.clone());
1046 }
1047 }
1048 for nested in &group.nested_groups {
1049 numbers.extend(collect_mig_numbers(nested));
1050 }
1051 numbers
1052}
1053
1054pub fn owned_to_assembled(seg: &OwnedSegment) -> AssembledSegment {
1055 AssembledSegment {
1056 tag: seg.id.clone(),
1057 elements: seg.elements.clone(),
1058 mig_number: None,
1059 }
1060}
1061
1062fn segment_matches_mig_codes(seg: &OwnedSegment, mig_seg: &MigSegment) -> bool {
1070 let actual_at = |el: usize, c: usize| -> &str {
1071 seg.elements
1072 .get(el)
1073 .and_then(|e| e.get(c))
1074 .map(|s| s.as_str())
1075 .unwrap_or("")
1076 };
1077 for de in &mig_seg.data_elements {
1078 if !de.codes.is_empty() {
1079 let actual = actual_at(de.position, 0);
1080 if !actual.is_empty() && !de.codes.iter().any(|c| c.value == actual) {
1081 return false;
1082 }
1083 }
1084 }
1085 for comp in &mig_seg.composites {
1086 for de in &comp.data_elements {
1087 if !de.codes.is_empty() {
1088 let actual = actual_at(comp.position, de.position);
1089 if !actual.is_empty() && !de.codes.iter().any(|c| c.value == actual) {
1090 return false;
1091 }
1092 }
1093 }
1094 }
1095 true
1096}
1097
1098#[cfg(test)]
1099mod tests {
1100 use super::*;
1101 use crate::test_support::{make_mig_group, make_mig_group_with_variant, make_mig_segment};
1102
1103 fn make_owned_seg(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
1104 OwnedSegment {
1105 id: id.to_string(),
1106 elements: elements
1107 .into_iter()
1108 .map(|e| e.into_iter().map(|c| c.to_string()).collect())
1109 .collect(),
1110 segment_number: 0,
1111 }
1112 }
1113
1114 fn make_mig_schema(segments: Vec<&str>, groups: Vec<MigSegmentGroup>) -> MigSchema {
1115 MigSchema {
1116 message_type: "UTILMD".to_string(),
1117 variant: Some("Strom".to_string()),
1118 version: "S2.1".to_string(),
1119 publication_date: "2025-03-20".to_string(),
1120 author: "BDEW".to_string(),
1121 format_version: "FV2504".to_string(),
1122 source_file: "test".to_string(),
1123 segments: segments.into_iter().map(make_mig_segment).collect(),
1124 segment_groups: groups,
1125 }
1126 }
1127
1128 #[test]
1129 fn test_assembler_top_level_segments_only() {
1130 let mig = make_mig_schema(vec!["UNH", "BGM", "DTM", "UNT"], vec![]);
1131
1132 let segments = vec![
1133 make_owned_seg("UNH", vec![vec!["001", "UTILMD:D:11A:UN:S2.1"]]),
1134 make_owned_seg("BGM", vec![vec!["E01", "DOC001"]]),
1135 make_owned_seg("DTM", vec![vec!["137", "20250101", "102"]]),
1136 make_owned_seg("UNT", vec![vec!["4", "001"]]),
1137 ];
1138
1139 let assembler = Assembler::new(&mig);
1140 let result = assembler.assemble_generic(&segments).unwrap();
1141
1142 assert_eq!(result.segments.len(), 4);
1143 assert_eq!(result.segments[0].tag, "UNH");
1144 assert_eq!(result.segments[1].tag, "BGM");
1145 assert_eq!(result.segments[2].tag, "DTM");
1146 assert_eq!(result.segments[3].tag, "UNT");
1147 assert!(result.groups.is_empty());
1148 }
1149
1150 #[test]
1151 fn test_assembler_with_segment_group() {
1152 let mig = make_mig_schema(
1153 vec!["UNH", "BGM"],
1154 vec![
1155 make_mig_group("SG2", vec!["NAD"], vec![]),
1156 make_mig_group("SG4", vec!["IDE", "STS"], vec![]),
1157 ],
1158 );
1159
1160 let segments = vec![
1161 make_owned_seg("UNH", vec![vec!["001"]]),
1162 make_owned_seg("BGM", vec![vec!["E01"]]),
1163 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1164 make_owned_seg("NAD", vec![vec!["MR", "9900456"]]),
1165 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1166 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
1167 ];
1168
1169 let assembler = Assembler::new(&mig);
1170 let result = assembler.assemble_generic(&segments).unwrap();
1171
1172 assert_eq!(result.segments.len(), 2);
1174 assert_eq!(result.groups.len(), 2);
1176 assert_eq!(result.groups[0].group_id, "SG2");
1177 assert_eq!(result.groups[0].repetitions.len(), 2);
1178 assert_eq!(result.groups[0].repetitions[0].segments[0].tag, "NAD");
1179 assert_eq!(result.groups[0].repetitions[1].segments[0].tag, "NAD");
1180 assert_eq!(result.groups[1].group_id, "SG4");
1182 assert_eq!(result.groups[1].repetitions.len(), 1);
1183 assert_eq!(result.groups[1].repetitions[0].segments.len(), 2);
1184 }
1185
1186 #[test]
1187 fn test_assembler_nested_groups() {
1188 let sg3 = make_mig_group("SG3", vec!["CTA", "COM"], vec![]);
1189 let mig = make_mig_schema(
1190 vec!["UNH", "BGM"],
1191 vec![make_mig_group("SG2", vec!["NAD"], vec![sg3])],
1192 );
1193
1194 let segments = vec![
1195 make_owned_seg("UNH", vec![vec!["001"]]),
1196 make_owned_seg("BGM", vec![vec!["E01"]]),
1197 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1198 make_owned_seg("CTA", vec![vec!["IC", "Kontakt"]]),
1199 make_owned_seg("COM", vec![vec!["040@example.com", "EM"]]),
1200 ];
1201
1202 let assembler = Assembler::new(&mig);
1203 let result = assembler.assemble_generic(&segments).unwrap();
1204
1205 let sg2 = &result.groups[0];
1207 assert_eq!(sg2.group_id, "SG2");
1208 assert_eq!(sg2.repetitions.len(), 1);
1209
1210 let sg2_inst = &sg2.repetitions[0];
1211 assert_eq!(sg2_inst.segments[0].tag, "NAD");
1212
1213 assert_eq!(sg2_inst.child_groups.len(), 1);
1215 let sg3 = &sg2_inst.child_groups[0];
1216 assert_eq!(sg3.group_id, "SG3");
1217 assert_eq!(sg3.repetitions[0].segments.len(), 2);
1218 assert_eq!(sg3.repetitions[0].segments[0].tag, "CTA");
1219 assert_eq!(sg3.repetitions[0].segments[1].tag, "COM");
1220 }
1221
1222 #[test]
1223 fn test_assembler_optional_segments_skipped() {
1224 let mig = make_mig_schema(vec!["UNH", "BGM", "DTM", "UNT"], vec![]);
1226
1227 let segments = vec![
1228 make_owned_seg("UNH", vec![vec!["001"]]),
1229 make_owned_seg("BGM", vec![vec!["E01"]]),
1230 make_owned_seg("UNT", vec![vec!["2", "001"]]),
1231 ];
1232
1233 let assembler = Assembler::new(&mig);
1234 let result = assembler.assemble_generic(&segments).unwrap();
1235
1236 assert_eq!(result.segments.len(), 3);
1238 assert_eq!(result.segments[0].tag, "UNH");
1239 assert_eq!(result.segments[1].tag, "BGM");
1240 assert_eq!(result.segments[2].tag, "UNT");
1241 }
1242
1243 #[test]
1244 fn test_assembler_empty_segments() {
1245 let mig = make_mig_schema(vec!["UNH"], vec![]);
1246 let assembler = Assembler::new(&mig);
1247 let result = assembler.assemble_generic(&[]).unwrap();
1248 assert!(result.segments.is_empty());
1249 assert!(result.groups.is_empty());
1250 }
1251
1252 #[test]
1253 fn test_assembler_preserves_element_data() {
1254 let mig = make_mig_schema(vec!["DTM"], vec![]);
1255
1256 let segments = vec![make_owned_seg(
1257 "DTM",
1258 vec![vec!["137", "202501010000+01", "303"]],
1259 )];
1260
1261 let assembler = Assembler::new(&mig);
1262 let result = assembler.assemble_generic(&segments).unwrap();
1263
1264 let dtm = &result.segments[0];
1265 assert_eq!(dtm.elements[0][0], "137");
1266 assert_eq!(dtm.elements[0][1], "202501010000+01");
1267 assert_eq!(dtm.elements[0][2], "303");
1268 }
1269
1270 #[test]
1271 fn test_group_instance_as_assembled_tree() {
1272 let sg5 = AssembledGroup {
1274 group_id: "SG5".to_string(),
1275 repetitions: vec![AssembledGroupInstance {
1276 segments: vec![AssembledSegment {
1277 tag: "LOC".to_string(),
1278 elements: vec![vec!["Z16".to_string(), "DE000111222333".to_string()]],
1279 mig_number: None,
1280 }],
1281 child_groups: vec![],
1282 entry_mig_number: None,
1283 variant_mig_numbers: vec![],
1284 skipped_segments: vec![],
1285 skipped_positions: Vec::new(),
1286 }],
1287 };
1288
1289 let sg4_instance = AssembledGroupInstance {
1290 segments: vec![
1291 AssembledSegment {
1292 tag: "IDE".to_string(),
1293 elements: vec![vec!["24".to_string(), "TX001".to_string()]],
1294 mig_number: None,
1295 },
1296 AssembledSegment {
1297 tag: "STS".to_string(),
1298 elements: vec![vec!["7".to_string()]],
1299 mig_number: None,
1300 },
1301 ],
1302 child_groups: vec![sg5],
1303 entry_mig_number: None,
1304 variant_mig_numbers: vec![],
1305 skipped_segments: vec![],
1306 skipped_positions: Vec::new(),
1307 };
1308
1309 let sub_tree = sg4_instance.as_assembled_tree();
1310
1311 assert_eq!(sub_tree.segments.len(), 2);
1313 assert_eq!(sub_tree.segments[0].tag, "IDE");
1314 assert_eq!(sub_tree.segments[1].tag, "STS");
1315
1316 assert_eq!(sub_tree.groups.len(), 1);
1318 assert_eq!(sub_tree.groups[0].group_id, "SG5");
1319
1320 assert_eq!(sub_tree.post_group_start, 2);
1322 }
1323
1324 #[test]
1325 fn test_assembler_from_parsed_edifact() {
1326 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'";
1328 let segments = crate::tokenize::parse_to_segments(input).unwrap();
1329
1330 let mig = make_mig_schema(vec!["UNB", "UNH", "BGM", "DTM", "UNT", "UNZ"], vec![]);
1331
1332 let assembler = Assembler::new(&mig);
1333 let result = assembler.assemble_generic(&segments).unwrap();
1334
1335 assert!(result.segments.iter().any(|s| s.tag == "UNH"));
1336 assert!(result.segments.iter().any(|s| s.tag == "BGM"));
1337 assert!(result.segments.iter().any(|s| s.tag == "DTM"));
1338 }
1339
1340 #[test]
1341 fn test_assemble_with_diagnostics_clean_input() {
1342 let mig = make_mig_schema(vec!["UNH", "BGM", "UNT"], vec![]);
1343 let segments = vec![
1344 make_owned_seg("UNH", vec![vec!["001"]]),
1345 make_owned_seg("BGM", vec![vec!["E01"]]),
1346 make_owned_seg("UNT", vec![vec!["2", "001"]]),
1347 ];
1348 let assembler = Assembler::new(&mig);
1349 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1350 assert_eq!(tree.segments.len(), 3);
1351 assert!(
1352 diagnostics.is_empty(),
1353 "Clean input should have no diagnostics"
1354 );
1355 }
1356
1357 #[test]
1358 fn test_assemble_with_diagnostics_unconsumed_segments() {
1359 let mig = make_mig_schema(vec!["UNH", "BGM"], vec![]);
1360 let segments = vec![
1361 make_owned_seg("UNH", vec![vec!["001"]]),
1362 make_owned_seg("BGM", vec![vec!["E01"]]),
1363 make_owned_seg("FTX", vec![vec!["AAA", "extra text"]]),
1364 ];
1365 let assembler = Assembler::new(&mig);
1366 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1367 assert_eq!(tree.segments.len(), 2);
1368 assert_eq!(diagnostics.len(), 1);
1369 assert_eq!(
1370 diagnostics[0].kind,
1371 StructureDiagnosticKind::UnexpectedSegment
1372 );
1373 assert_eq!(diagnostics[0].segment_id, "FTX");
1374 assert_eq!(diagnostics[0].position, 2);
1375 }
1376
1377 #[test]
1378 fn test_assemble_with_diagnostics_multiple_unconsumed() {
1379 let mig = make_mig_schema(vec!["UNH"], vec![]);
1380 let segments = vec![
1381 make_owned_seg("UNH", vec![vec!["001"]]),
1382 make_owned_seg("FOO", vec![]),
1383 make_owned_seg("BAR", vec![]),
1384 make_owned_seg("BAZ", vec![]),
1385 ];
1386 let assembler = Assembler::new(&mig);
1387 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1388 assert_eq!(tree.segments.len(), 1);
1389 assert_eq!(diagnostics.len(), 3);
1390 assert_eq!(diagnostics[0].segment_id, "FOO");
1391 assert_eq!(diagnostics[1].segment_id, "BAR");
1392 assert_eq!(diagnostics[2].segment_id, "BAZ");
1393 }
1394
1395 #[test]
1398 fn test_non_entry_segments_get_mig_number_from_bounded_slots() {
1399 use crate::test_support::make_mig_segment_numbered;
1403
1404 let sg4 = MigSegmentGroup {
1405 segments: vec![
1406 make_mig_segment_numbered("IDE", "00020"),
1407 make_mig_segment_numbered("DTM", "00023"),
1408 make_mig_segment_numbered("DTM", "00024"),
1409 make_mig_segment_numbered("STS", "00035"),
1410 ],
1411 ..make_mig_group("SG4", vec![], vec![])
1412 };
1413 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1414
1415 let segments = vec![
1416 make_owned_seg("UNH", vec![vec!["001"]]),
1417 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1418 make_owned_seg("DTM", vec![vec!["92", "202505312200+00", "303"]]),
1419 make_owned_seg("DTM", vec![vec!["93", "202512312300+00", "303"]]),
1420 make_owned_seg("STS", vec![vec!["7"], vec![], vec!["E01"]]),
1421 ];
1422
1423 let assembler = Assembler::new(&mig);
1424 let tree = assembler.assemble_generic(&segments).unwrap();
1425
1426 let sg4_instance = &tree.groups[0].repetitions[0];
1427
1428 assert_eq!(sg4_instance.segments[0].tag, "IDE");
1430 assert_eq!(sg4_instance.segments[0].mig_number.as_deref(), Some("00020"));
1431
1432 assert_eq!(sg4_instance.segments[1].tag, "DTM");
1434 assert_eq!(sg4_instance.segments[1].mig_number.as_deref(), Some("00023"));
1435
1436 assert_eq!(sg4_instance.segments[2].tag, "DTM");
1438 assert_eq!(sg4_instance.segments[2].mig_number.as_deref(), Some("00024"));
1439
1440 assert_eq!(sg4_instance.segments[3].tag, "STS");
1442 assert_eq!(sg4_instance.segments[3].mig_number.as_deref(), Some("00035"));
1443
1444 assert!(sg4_instance.variant_mig_numbers.contains(&"00020".to_string()));
1446 assert!(sg4_instance.variant_mig_numbers.contains(&"00023".to_string()));
1447 assert!(sg4_instance.variant_mig_numbers.contains(&"00024".to_string()));
1448 assert!(sg4_instance.variant_mig_numbers.contains(&"00035".to_string()));
1449 }
1450
1451 #[test]
1452 fn test_greedy_extra_segments_get_no_mig_number() {
1453 use crate::test_support::make_mig_segment_numbered;
1456
1457 let sg4 = MigSegmentGroup {
1458 segments: vec![
1459 make_mig_segment_numbered("IDE", "00020"),
1460 make_mig_segment_numbered("DTM", "00023"),
1461 ],
1462 ..make_mig_group("SG4", vec![], vec![])
1463 };
1464 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1465
1466 let segments = vec![
1467 make_owned_seg("UNH", vec![vec!["001"]]),
1468 make_owned_seg("IDE", vec![vec!["24"]]),
1469 make_owned_seg("DTM", vec![vec!["92", "20250531"]]),
1470 make_owned_seg("DTM", vec![vec!["93", "20251231"]]), ];
1472
1473 let assembler = Assembler::new(&mig);
1474 let tree = assembler.assemble_generic(&segments).unwrap();
1475
1476 let sg4_instance = &tree.groups[0].repetitions[0];
1477 assert_eq!(sg4_instance.segments.len(), 3); assert_eq!(sg4_instance.segments[1].mig_number.as_deref(), Some("00023"));
1481
1482 assert_eq!(sg4_instance.segments[2].mig_number, None);
1484 }
1485
1486 #[test]
1489 fn test_qualifier_map_prevents_wrong_slot_consumption() {
1490 use crate::test_support::make_mig_segment_numbered;
1495 use std::collections::HashMap;
1496
1497 let sg4 = MigSegmentGroup {
1498 segments: vec![
1499 make_mig_segment_numbered("IDE", "00020"),
1500 make_mig_segment_numbered("DTM", "00023"),
1501 make_mig_segment_numbered("DTM", "00024"),
1502 ],
1503 ..make_mig_group("SG4", vec![], vec![])
1504 };
1505 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1506
1507 let segments = vec![
1508 make_owned_seg("UNH", vec![vec!["001"]]),
1509 make_owned_seg("IDE", vec![vec!["24"]]),
1510 make_owned_seg("DTM", vec![vec!["93", "202512312300+00", "303"]]),
1511 ];
1512
1513 let mut qualifier_map = HashMap::new();
1514 qualifier_map.insert("00023".to_string(), (0, 0, "92".to_string()));
1515 qualifier_map.insert("00024".to_string(), (0, 0, "93".to_string()));
1516
1517 let config = AssemblerConfig {
1518 skip_unknown_segments: false,
1519 qualifier_map,
1520 ..Default::default()
1521 };
1522 let assembler = Assembler::with_config(&mig, config);
1523 let tree = assembler.assemble_generic(&segments).unwrap();
1524
1525 let sg4_instance = &tree.groups[0].repetitions[0];
1526
1527 assert_eq!(sg4_instance.segments.len(), 2); let dtm = &sg4_instance.segments[1];
1530 assert_eq!(dtm.tag, "DTM");
1531 assert_eq!(
1532 dtm.mig_number.as_deref(),
1533 Some("00024"),
1534 "DTM+93 should get mig_number 00024 (not 00023)"
1535 );
1536 }
1537
1538 #[test]
1539 fn test_group_entry_qualifier_mismatch_does_not_infinite_loop() {
1540 use crate::test_support::make_mig_segment_numbered;
1550 use std::collections::HashMap;
1551
1552 let sg5 = MigSegmentGroup {
1553 segments: vec![make_mig_segment_numbered("LOC", "00050")],
1554 ..make_mig_group("SG5", vec![], vec![])
1555 };
1556 let mig = make_mig_schema(vec!["UNH"], vec![sg5]);
1557
1558 let segments = vec![
1559 make_owned_seg("UNH", vec![vec!["001"]]),
1560 make_owned_seg("LOC", vec![vec!["172"], vec!["92003964705"]]),
1562 ];
1563
1564 let mut qualifier_map = HashMap::new();
1565 qualifier_map.insert("00050".to_string(), (0, 0, "Z16".to_string()));
1566
1567 let config = AssemblerConfig {
1568 skip_unknown_segments: false,
1569 qualifier_map,
1570 ..Default::default()
1571 };
1572 let assembler = Assembler::with_config(&mig, config);
1573
1574 let start = std::time::Instant::now();
1578 let tree = assembler.assemble_generic(&segments).unwrap();
1579 assert!(
1580 start.elapsed() < std::time::Duration::from_secs(5),
1581 "assembly took {:?} — suspected infinite-loop regression",
1582 start.elapsed()
1583 );
1584
1585 assert!(tree.groups.is_empty());
1589 }
1590
1591 #[test]
1594 fn test_skip_unknown_segment_between_slots() {
1595 let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
1599 let mig = make_mig_schema(vec!["UNH"], vec![sg8.clone()]);
1600
1601 let segments = vec![
1602 make_owned_seg("UNH", vec![vec!["001"]]),
1603 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1604 make_owned_seg("RFF", vec![vec!["Z38", "CROSSREF"]]),
1605 make_owned_seg("CCI", vec![vec!["Z30"]]),
1606 ];
1607
1608 let off = Assembler::new(&mig);
1610 let tree_off = off.assemble_generic(&segments).unwrap();
1611 let sg8_off = &tree_off.groups[0];
1612 assert_eq!(sg8_off.repetitions[0].segments.len(), 1); assert_eq!(sg8_off.repetitions[0].segments[0].tag, "SEQ");
1614
1615 let on = Assembler::with_config(
1617 &mig,
1618 AssemblerConfig {
1619 skip_unknown_segments: true,
1620 ..Default::default()
1621 },
1622 );
1623 let tree_on = on.assemble_generic(&segments).unwrap();
1624 let sg8_on = &tree_on.groups[0];
1625 assert_eq!(sg8_on.repetitions[0].segments.len(), 2); assert_eq!(sg8_on.repetitions[0].segments[0].tag, "SEQ");
1627 assert_eq!(sg8_on.repetitions[0].segments[1].tag, "CCI");
1628 }
1629
1630 #[test]
1631 fn test_skip_preserves_on_instance() {
1632 let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
1634 let mig = make_mig_schema(vec!["UNH"], vec![sg8]);
1635
1636 let segments = vec![
1637 make_owned_seg("UNH", vec![vec!["001"]]),
1638 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1639 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
1640 make_owned_seg("DTM", vec![vec!["92", "20250101"]]),
1641 make_owned_seg("CCI", vec![vec!["Z30"]]),
1642 ];
1643
1644 let assembler = Assembler::with_config(
1645 &mig,
1646 AssemblerConfig {
1647 skip_unknown_segments: true,
1648 ..Default::default()
1649 },
1650 );
1651 let tree = assembler.assemble_generic(&segments).unwrap();
1652 let instance = &tree.groups[0].repetitions[0];
1653
1654 assert_eq!(instance.segments.len(), 2); assert_eq!(instance.skipped_segments.len(), 2); assert_eq!(instance.skipped_segments[0].tag, "RFF");
1657 assert_eq!(instance.skipped_segments[1].tag, "DTM");
1658 }
1659
1660 #[test]
1661 fn test_skip_mode_off_default() {
1662 let mig = make_mig_schema(vec![], vec![]);
1664 let assembler = Assembler::new(&mig);
1665 assert!(!assembler.config.skip_unknown_segments);
1666 }
1667
1668 #[test]
1669 fn test_skip_does_not_consume_nested_group_entry() {
1670 let sg5 = make_mig_group("SG5", vec!["LOC"], vec![]);
1674 let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![sg5]);
1675 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1676
1677 let segments = vec![
1678 make_owned_seg("UNH", vec![vec!["001"]]),
1679 make_owned_seg("IDE", vec![vec!["24"]]),
1680 make_owned_seg("FOO", vec![vec!["unknown"]]),
1681 make_owned_seg("STS", vec![vec!["7"]]),
1682 make_owned_seg("LOC", vec![vec!["Z16"]]),
1683 ];
1684
1685 let assembler = Assembler::with_config(
1686 &mig,
1687 AssemblerConfig {
1688 skip_unknown_segments: true,
1689 ..Default::default()
1690 },
1691 );
1692 let tree = assembler.assemble_generic(&segments).unwrap();
1693 let sg4 = &tree.groups[0];
1694 let inst = &sg4.repetitions[0];
1695
1696 assert_eq!(inst.segments.len(), 2);
1698 assert_eq!(inst.segments[0].tag, "IDE");
1699 assert_eq!(inst.segments[1].tag, "STS");
1700 assert_eq!(inst.skipped_segments.len(), 1);
1701 assert_eq!(inst.skipped_segments[0].tag, "FOO");
1702
1703 assert_eq!(inst.child_groups.len(), 1);
1705 assert_eq!(inst.child_groups[0].group_id, "SG5");
1706 assert_eq!(inst.child_groups[0].repetitions[0].segments[0].tag, "LOC");
1707 }
1708
1709 #[test]
1710 fn test_skip_unknown_between_nested_group_reps() {
1711 let sg10 = make_mig_group("SG10", vec!["CCI"], vec![]);
1729 let sg8_zd7 =
1730 make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10.clone()], "ZD7");
1731 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![], "Z98");
1732 let sg12 = make_mig_group("SG12", vec!["NAD"], vec![]);
1733 let sg4 = make_mig_group(
1734 "SG4",
1735 vec!["IDE", "STS"],
1736 vec![sg8_zd7, sg8_z98, sg12],
1737 );
1738 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1739
1740 let segments = vec![
1741 make_owned_seg("UNH", vec![vec!["001"]]),
1742 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1743 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
1744 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
1745 make_owned_seg("CCI", vec![vec!["Z30"]]),
1746 make_owned_seg("FOO", vec![vec!["orphan"]]),
1747 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1748 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1749 ];
1750
1751 let assembler = Assembler::with_config(
1752 &mig,
1753 AssemblerConfig {
1754 skip_unknown_segments: true,
1755 ..Default::default()
1756 },
1757 );
1758 let tree = assembler.assemble_generic(&segments).unwrap();
1759
1760 assert_eq!(tree.groups.len(), 1);
1761 let sg4_inst = &tree.groups[0].repetitions[0];
1762 assert_eq!(sg4_inst.child_groups.len(), 2, "expected SG8 + SG12");
1764 let sg8_tree = &sg4_inst.child_groups[0];
1765 assert_eq!(sg8_tree.group_id, "SG8");
1766 assert_eq!(
1767 sg8_tree.repetitions.len(),
1768 2,
1769 "SG8 should have both ZD7 and Z98 reps after orphan skip"
1770 );
1771 assert_eq!(sg8_tree.repetitions[0].segments[0].elements[0][0], "ZD7");
1772 assert_eq!(sg8_tree.repetitions[1].segments[0].elements[0][0], "Z98");
1773
1774 let sg12_tree = &sg4_inst.child_groups[1];
1775 assert_eq!(sg12_tree.group_id, "SG12");
1776 assert_eq!(sg12_tree.repetitions.len(), 1);
1777
1778 fn count_foo(inst: &AssembledGroupInstance) -> usize {
1783 let mut n = inst
1784 .skipped_segments
1785 .iter()
1786 .filter(|s| s.tag == "FOO")
1787 .count();
1788 for child in &inst.child_groups {
1789 for rep in &child.repetitions {
1790 n += count_foo(rep);
1791 }
1792 }
1793 n
1794 }
1795 assert_eq!(count_foo(sg4_inst), 1, "FOO should be recorded exactly once");
1796 }
1797
1798 #[test]
1799 fn test_skip_off_preserves_cascade_behavior() {
1800 let sg10 = make_mig_group("SG10", vec!["CCI"], vec![]);
1804 let sg8_zd7 =
1805 make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10], "ZD7");
1806 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![], "Z98");
1807 let sg12 = make_mig_group("SG12", vec!["NAD"], vec![]);
1808 let sg4 = make_mig_group(
1809 "SG4",
1810 vec!["IDE", "STS"],
1811 vec![sg8_zd7, sg8_z98, sg12],
1812 );
1813 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1814
1815 let segments = vec![
1816 make_owned_seg("UNH", vec![vec!["001"]]),
1817 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1818 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
1819 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
1820 make_owned_seg("CCI", vec![vec!["Z30"]]),
1821 make_owned_seg("FOO", vec![vec!["orphan"]]),
1822 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1823 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1824 ];
1825
1826 let assembler = Assembler::new(&mig);
1827 let tree = assembler.assemble_generic(&segments).unwrap();
1828
1829 let sg4_inst = &tree.groups[0].repetitions[0];
1830 let sg8_tree = sg4_inst
1831 .child_groups
1832 .iter()
1833 .find(|g| g.group_id == "SG8")
1834 .expect("SG8 should still be present");
1835 assert_eq!(sg8_tree.repetitions.len(), 1);
1837 assert!(
1838 sg4_inst
1839 .skipped_segments
1840 .iter()
1841 .all(|s| s.tag != "FOO"),
1842 "FOO must not be skipped when skip mode is off"
1843 );
1844 }
1845
1846 #[test]
1847 fn test_roundtrip_with_skip() {
1848 use crate::disassembler::Disassembler;
1851 use crate::renderer::render_edifact;
1852
1853 let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
1854 let mig = make_mig_schema(vec!["UNH", "UNT"], vec![sg8]);
1855
1856 let segments = vec![
1857 make_owned_seg("UNH", vec![vec!["001"]]),
1858 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1859 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
1860 make_owned_seg("CCI", vec![vec!["Z30"]]),
1861 make_owned_seg("UNT", vec![vec!["4", "001"]]),
1862 ];
1863
1864 let assembler = Assembler::with_config(
1865 &mig,
1866 AssemblerConfig {
1867 skip_unknown_segments: true,
1868 ..Default::default()
1869 },
1870 );
1871 let tree = assembler.assemble_generic(&segments).unwrap();
1872
1873 let disassembler = Disassembler::new(&mig);
1874 let dis = disassembler.disassemble(&tree);
1875 let delimiters = edifact_primitives::EdifactDelimiters::default();
1876 let rendered = render_edifact(&dis, &delimiters);
1877
1878 assert_eq!(dis.len(), 5);
1883 assert_eq!(dis[0].tag, "UNH");
1884 assert_eq!(dis[1].tag, "SEQ");
1885 assert_eq!(dis[2].tag, "CCI");
1886 assert_eq!(dis[3].tag, "RFF"); assert_eq!(dis[4].tag, "UNT");
1888
1889 assert!(rendered.contains("UNH+001"));
1891 assert!(rendered.contains("SEQ+Z98"));
1892 assert!(rendered.contains("RFF+Z38:REF1"));
1893 assert!(rendered.contains("CCI+Z30"));
1894 assert!(rendered.contains("UNT+4:001"));
1895 }
1896
1897 #[test]
1900 fn test_variant_groups_interleaved_reps() {
1901 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
1905 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
1906
1907 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
1908
1909 let segments = vec![
1910 make_owned_seg("UNH", vec![vec!["001"]]),
1911 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
1912 make_owned_seg("CCI", vec![vec!["Z30"]]),
1913 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1914 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
1915 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
1916 make_owned_seg("CCI", vec![vec!["Z31"]]),
1917 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1918 make_owned_seg("RFF", vec![vec!["Z38", "REF2"]]),
1919 ];
1920
1921 let assembler = Assembler::new(&mig);
1922 let result = assembler.assemble_generic(&segments).unwrap();
1923
1924 assert_eq!(result.segments.len(), 1); assert_eq!(result.groups.len(), 1); let sg8 = &result.groups[0];
1927 assert_eq!(sg8.group_id, "SG8");
1928 assert_eq!(sg8.repetitions.len(), 4);
1929
1930 assert_eq!(sg8.repetitions[0].segments[0].elements[0][0], "ZD7");
1932 assert_eq!(sg8.repetitions[0].segments[1].tag, "CCI");
1933 assert_eq!(sg8.repetitions[1].segments[0].elements[0][0], "Z98");
1934 assert_eq!(sg8.repetitions[1].segments[1].tag, "RFF");
1935 assert_eq!(sg8.repetitions[2].segments[0].elements[0][0], "ZD7");
1936 assert_eq!(sg8.repetitions[3].segments[0].elements[0][0], "Z98");
1937 }
1938
1939 #[test]
1940 fn test_variant_groups_single_variant_type() {
1941 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
1943 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
1944
1945 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
1946
1947 let segments = vec![
1948 make_owned_seg("UNH", vec![vec!["001"]]),
1949 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1950 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
1951 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1952 make_owned_seg("RFF", vec![vec!["Z38", "REF2"]]),
1953 ];
1954
1955 let assembler = Assembler::new(&mig);
1956 let result = assembler.assemble_generic(&segments).unwrap();
1957
1958 assert_eq!(result.groups.len(), 1);
1959 assert_eq!(result.groups[0].repetitions.len(), 2);
1960 assert_eq!(
1961 result.groups[0].repetitions[0].segments[0].elements[0][0],
1962 "Z98"
1963 );
1964 assert_eq!(
1965 result.groups[0].repetitions[1].segments[0].elements[0][0],
1966 "Z98"
1967 );
1968 }
1969
1970 #[test]
1971 fn test_non_variant_groups_unchanged() {
1972 let sg2 = make_mig_group("SG2", vec!["NAD"], vec![]);
1974 let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![]);
1975
1976 let mig = make_mig_schema(vec!["UNH", "BGM"], vec![sg2, sg4]);
1977
1978 let segments = vec![
1979 make_owned_seg("UNH", vec![vec!["001"]]),
1980 make_owned_seg("BGM", vec![vec!["E01"]]),
1981 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1982 make_owned_seg("NAD", vec![vec!["MR", "9900456"]]),
1983 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1984 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
1985 ];
1986
1987 let assembler = Assembler::new(&mig);
1988 let result = assembler.assemble_generic(&segments).unwrap();
1989
1990 assert_eq!(result.segments.len(), 2);
1991 assert_eq!(result.groups.len(), 2);
1992 assert_eq!(result.groups[0].group_id, "SG2");
1993 assert_eq!(result.groups[0].repetitions.len(), 2);
1994 assert_eq!(result.groups[1].group_id, "SG4");
1995 assert_eq!(result.groups[1].repetitions.len(), 1);
1996 }
1997
1998 #[test]
1999 fn test_variant_groups_with_nested_children() {
2000 let sg10 = make_mig_group("SG10", vec!["CCI", "CAV"], vec![]);
2002 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10.clone()], "ZD7");
2003 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10], "Z98");
2004
2005 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
2006
2007 let segments = vec![
2008 make_owned_seg("UNH", vec![vec!["001"]]),
2009 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
2010 make_owned_seg("CCI", vec![vec!["Z30"]]),
2011 make_owned_seg("CAV", vec![vec!["Z91", "Y"]]),
2012 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2013 make_owned_seg("CCI", vec![vec!["Z31"]]),
2014 make_owned_seg("CAV", vec![vec!["Z91", "N"]]),
2015 ];
2016
2017 let assembler = Assembler::new(&mig);
2018 let result = assembler.assemble_generic(&segments).unwrap();
2019
2020 assert_eq!(result.groups.len(), 1);
2021 let sg8 = &result.groups[0];
2022 assert_eq!(sg8.repetitions.len(), 2);
2023
2024 assert_eq!(sg8.repetitions[0].child_groups.len(), 1);
2026 assert_eq!(sg8.repetitions[0].child_groups[0].group_id, "SG10");
2027 assert_eq!(
2028 sg8.repetitions[0].child_groups[0].repetitions[0].segments[0].elements[0][0],
2029 "Z30"
2030 );
2031
2032 assert_eq!(sg8.repetitions[1].child_groups.len(), 1);
2034 assert_eq!(
2035 sg8.repetitions[1].child_groups[0].repetitions[0].segments[0].elements[0][0],
2036 "Z31"
2037 );
2038 }
2039
2040 #[test]
2041 fn test_variant_qualifier_check_prevents_wrong_variant_consumption() {
2042 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
2045
2046 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7]);
2047
2048 let segments = vec![
2049 make_owned_seg("UNH", vec![vec!["001"]]),
2050 make_owned_seg("SEQ", vec![vec!["Z98"]]), make_owned_seg("CCI", vec![vec!["Z30"]]),
2052 ];
2053
2054 let assembler = Assembler::new(&mig);
2055 let result = assembler.assemble_generic(&segments).unwrap();
2056
2057 assert!(result.groups.is_empty());
2059 }
2060
2061 #[test]
2062 fn test_mixed_variant_and_non_variant_groups() {
2063 let sg2 = make_mig_group("SG2", vec!["NAD"], vec![]);
2065 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
2066 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
2067 let sg12 = make_mig_group("SG12", vec!["NAD"], vec![]);
2068
2069 let mig = make_mig_schema(vec!["UNH"], vec![sg2, sg8_zd7, sg8_z98, sg12]);
2070
2071 let segments = vec![
2072 make_owned_seg("UNH", vec![vec!["001"]]),
2073 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
2074 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
2075 make_owned_seg("CCI", vec![vec!["Z30"]]),
2076 make_owned_seg("SEQ", vec![vec!["Z98"]]),
2077 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
2078 make_owned_seg("NAD", vec![vec!["Z65", "ID001"]]),
2079 ];
2080
2081 let assembler = Assembler::new(&mig);
2082 let result = assembler.assemble_generic(&segments).unwrap();
2083
2084 assert_eq!(result.groups.len(), 3); assert_eq!(result.groups[0].group_id, "SG2");
2086 assert_eq!(result.groups[0].repetitions.len(), 1);
2087 assert_eq!(result.groups[1].group_id, "SG8");
2088 assert_eq!(result.groups[1].repetitions.len(), 2);
2089 assert_eq!(result.groups[2].group_id, "SG12");
2090 assert_eq!(result.groups[2].repetitions.len(), 1);
2091 }
2092
2093 #[test]
2094 fn test_assembler_disambiguates_shared_qualifier_by_full_code_profile() {
2095 use mig_types::schema::common::CodeDefinition;
2103 use mig_types::schema::mig::{MigComposite, MigDataElement};
2104 use std::collections::HashMap;
2105
2106 fn code(value: &str) -> CodeDefinition {
2107 CodeDefinition {
2108 value: value.to_string(),
2109 name: value.to_string(),
2110 description: None,
2111 }
2112 }
2113
2114 fn pia_slot(number: &str, composite_code: &str) -> MigSegment {
2115 MigSegment {
2116 id: "PIA".to_string(),
2117 name: "PIA".to_string(),
2118 description: None,
2119 counter: None,
2120 level: 1,
2121 number: Some(number.to_string()),
2122 max_rep_std: 1,
2123 max_rep_spec: 1,
2124 status_std: Some("M".to_string()),
2125 status_spec: Some("M".to_string()),
2126 example: None,
2127 data_elements: vec![MigDataElement {
2128 id: "4347".to_string(),
2129 name: "Produkt-ID-Funktion".to_string(),
2130 description: None,
2131 status_std: Some("M".to_string()),
2132 status_spec: Some("M".to_string()),
2133 format_std: None,
2134 format_spec: None,
2135 codes: vec![code("5")],
2136 position: 0,
2137 }],
2138 composites: vec![MigComposite {
2139 id: "C212".to_string(),
2140 name: "Item Identifier".to_string(),
2141 description: None,
2142 status_std: Some("M".to_string()),
2143 status_spec: Some("M".to_string()),
2144 data_elements: vec![MigDataElement {
2145 id: "7143".to_string(),
2146 name: "Artikel/Dienstleistung-ID".to_string(),
2147 description: None,
2148 status_std: Some("M".to_string()),
2149 status_spec: Some("M".to_string()),
2150 format_std: None,
2151 format_spec: None,
2152 codes: vec![code(composite_code)],
2153 position: 0,
2154 }],
2155 position: 1,
2156 }],
2157 }
2158 }
2159
2160 let sg4 = MigSegmentGroup {
2161 segments: vec![
2162 crate::test_support::make_mig_segment_numbered("IDE", "00020"),
2163 pia_slot("00108", "Z12"),
2164 pia_slot("00197", "SRW"),
2165 ],
2166 ..make_mig_group("SG4", vec![], vec![])
2167 };
2168 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
2169
2170 let segments = vec![
2171 make_owned_seg("UNH", vec![vec!["001"]]),
2172 make_owned_seg("IDE", vec![vec!["24"]]),
2173 make_owned_seg("PIA", vec![vec!["5"], vec!["SRW"]]),
2175 ];
2176
2177 let mut qualifier_map = HashMap::new();
2181 qualifier_map.insert("00108".to_string(), (0, 0, "5".to_string()));
2182 qualifier_map.insert("00197".to_string(), (0, 0, "5".to_string()));
2183
2184 let config = AssemblerConfig {
2185 skip_unknown_segments: false,
2186 qualifier_map,
2187 strict_code_matching: true,
2188 };
2189 let assembler = Assembler::with_config(&mig, config);
2190 let tree = assembler.assemble_generic(&segments).unwrap();
2191
2192 let sg4_instance = &tree.groups[0].repetitions[0];
2193 let pia = sg4_instance
2194 .segments
2195 .iter()
2196 .find(|s| s.tag == "PIA")
2197 .expect("PIA consumed into SG4");
2198 assert_eq!(
2199 pia.mig_number.as_deref(),
2200 Some("00197"),
2201 "PIA+5+:::SRW must be assigned the SRW variant (mig=00197), not the Z12 variant"
2202 );
2203 }
2204}