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}
75
76impl AssembledGroupInstance {
77 pub fn as_assembled_tree(&self) -> AssembledTree {
84 AssembledTree {
85 segments: self.segments.clone(),
86 groups: self.child_groups.clone(),
87 post_group_start: self.segments.len(),
88 inter_group_segments: std::collections::BTreeMap::new(),
89 }
90 }
91}
92
93#[derive(Debug, Clone, Default)]
95pub struct AssemblerConfig {
96 pub skip_unknown_segments: bool,
103
104 pub qualifier_map: std::collections::HashMap<String, (usize, usize, String)>,
116
117 pub strict_code_matching: bool,
128}
129
130pub struct Assembler<'a> {
135 mig: &'a MigSchema,
136 config: AssemblerConfig,
137}
138
139impl<'a> Assembler<'a> {
140 pub fn new(mig: &'a MigSchema) -> Self {
141 Self {
142 mig,
143 config: AssemblerConfig::default(),
144 }
145 }
146
147 pub fn with_config(mig: &'a MigSchema, config: AssemblerConfig) -> Self {
148 Self { mig, config }
149 }
150
151 pub fn assemble_generic(
153 &self,
154 segments: &[OwnedSegment],
155 ) -> Result<AssembledTree, AssemblyError> {
156 let mut cursor = SegmentCursor::new(segments.len());
157 let mut tree = AssembledTree {
158 segments: Vec::new(),
159 groups: Vec::new(),
160 post_group_start: 0,
161 inter_group_segments: std::collections::BTreeMap::new(),
162 };
163
164 let mut matched_seg_indices = Vec::new();
166
167 for (i, mig_seg) in self.mig.segments.iter().enumerate() {
169 if cursor.is_exhausted() {
170 break;
171 }
172 if let Some(assembled) = self.try_consume_segment(segments, &mut cursor, mig_seg)? {
173 tree.segments.push(assembled);
174 matched_seg_indices.push(i);
175 }
176 }
177
178 let mut group_idx = 0;
187 while group_idx < self.mig.segment_groups.len() {
188 if cursor.is_exhausted() {
189 break;
190 }
191
192 let mig_group = &self.mig.segment_groups[group_idx];
193
194 let tree_group_idx = tree.groups.len();
196 for (i, mig_seg) in self.mig.segments.iter().enumerate() {
197 if cursor.is_exhausted() {
198 break;
199 }
200 if matched_seg_indices.contains(&i) {
201 continue;
202 }
203 if let Some(assembled) = self.try_consume_segment(segments, &mut cursor, mig_seg)? {
204 tree.inter_group_segments
205 .entry(tree_group_idx)
206 .or_default()
207 .push(assembled);
208 matched_seg_indices.push(i);
209 }
210 }
211
212 if mig_group.variant_code.is_some() {
214 let variant_count = self.mig.segment_groups[group_idx..]
215 .iter()
216 .take_while(|g| g.id == mig_group.id && g.variant_code.is_some())
217 .count();
218 let variant_end = group_idx + variant_count;
219
220 let variant_groups = &self.mig.segment_groups[group_idx..variant_end];
221 if let Some(combined) =
222 self.try_consume_variant_groups(segments, &mut cursor, variant_groups)?
223 {
224 tree.groups.push(combined);
225 }
226 group_idx = variant_end;
227 } else {
228 if let Some(assembled) = self.try_consume_group(segments, &mut cursor, mig_group)? {
229 tree.groups.push(assembled);
230 }
231 group_idx += 1;
232 }
233 }
234
235 tree.post_group_start = tree.segments.len();
237
238 for (i, mig_seg) in self.mig.segments.iter().enumerate() {
240 if cursor.is_exhausted() {
241 break;
242 }
243 if matched_seg_indices.contains(&i) {
244 continue;
245 }
246 if let Some(assembled) = self.try_consume_segment(segments, &mut cursor, mig_seg)? {
247 tree.segments.push(assembled);
248 }
249 }
250
251 Ok(tree)
252 }
253
254 fn try_consume_segment(
255 &self,
256 segments: &[OwnedSegment],
257 cursor: &mut SegmentCursor,
258 mig_seg: &MigSegment,
259 ) -> Result<Option<AssembledSegment>, AssemblyError> {
260 if cursor.is_exhausted() {
261 return Ok(None);
262 }
263 let seg = &segments[cursor.position()];
264 if matcher::matches_segment_tag(&seg.id, &mig_seg.id) {
265 if let Some(ref num) = mig_seg.number {
268 if let Some((el_idx, comp_idx, expected)) = self.config.qualifier_map.get(num) {
269 let actual = seg
270 .elements
271 .get(*el_idx)
272 .and_then(|e| e.get(*comp_idx))
273 .map(|s| s.as_str())
274 .unwrap_or("");
275 if actual != expected {
276 return Ok(None); }
278 }
279 }
280 let mut assembled = owned_to_assembled(seg);
288 assembled.mig_number = mig_seg.number.clone();
289 cursor.advance();
290 Ok(Some(assembled))
291 } else {
292 Ok(None) }
294 }
295
296 fn consume_entry_run_best_match(
306 &self,
307 segments: &[OwnedSegment],
308 cursor: &mut SegmentCursor,
309 entry_slots: &[MigSegment],
310 instance: &mut AssembledGroupInstance,
311 ) -> Result<(), AssemblyError> {
312 let mut used = vec![false; entry_slots.len()];
313 for _ in 0..entry_slots.len() {
314 if cursor.is_exhausted() {
315 break;
316 }
317 let seg = &segments[cursor.position()];
318 let mut strict_match: Option<usize> = None;
319 let mut tag_match: Option<usize> = None;
320 for (i, slot) in entry_slots.iter().enumerate() {
321 if used[i] {
322 continue;
323 }
324 if !matcher::matches_segment_tag(&seg.id, &slot.id) {
325 continue;
326 }
327 if !self.segment_passes_qualifier_map(seg, slot) {
328 continue;
329 }
330 if tag_match.is_none() {
331 tag_match = Some(i);
332 }
333 if strict_match.is_none() && segment_matches_mig_codes(seg, slot) {
334 strict_match = Some(i);
335 }
336 }
337 let Some(i) = strict_match.or(tag_match) else {
338 break;
339 };
340 used[i] = true;
341 let slot = &entry_slots[i];
342 let mut assembled = owned_to_assembled(seg);
343 assembled.mig_number = slot.number.clone();
344 instance.segments.push(assembled);
345 cursor.advance();
346 }
347 Ok(())
348 }
349
350 fn segment_passes_qualifier_map(&self, seg: &OwnedSegment, mig_seg: &MigSegment) -> bool {
351 let Some(ref num) = mig_seg.number else {
352 return true;
353 };
354 let Some((el_idx, comp_idx, expected)) = self.config.qualifier_map.get(num) else {
355 return true;
356 };
357 let actual = seg
358 .elements
359 .get(*el_idx)
360 .and_then(|e| e.get(*comp_idx))
361 .map(|s| s.as_str())
362 .unwrap_or("");
363 actual == expected
364 }
365
366 fn try_consume_group(
367 &self,
368 segments: &[OwnedSegment],
369 cursor: &mut SegmentCursor,
370 mig_group: &MigSegmentGroup,
371 ) -> Result<Option<AssembledGroup>, AssemblyError> {
372 let mut repetitions = Vec::new();
373 let entry_segment = mig_group.segments.first().ok_or_else(|| {
374 AssemblyError::ParseError(format!("Group {} has no segments", mig_group.id))
375 })?;
376
377 while !cursor.is_exhausted() {
379 let iter_start = cursor.position();
380 let seg = &segments[cursor.position()];
381 if !matcher::matches_segment_tag(&seg.id, &entry_segment.id) {
382 break; }
384
385 if !mig_group.variant_codes.is_empty() {
387 let (ei, ci) = mig_group.variant_qualifier_position.unwrap_or((0, 0));
388 let actual_qual = seg
389 .elements
390 .get(ei)
391 .and_then(|e| e.get(ci))
392 .map(|s| s.as_str())
393 .unwrap_or("");
394 if !mig_group
395 .variant_codes
396 .iter()
397 .any(|c| actual_qual.eq_ignore_ascii_case(c))
398 {
399 break;
400 }
401 } else if let Some(ref expected_code) = mig_group.variant_code {
402 let (ei, ci) = mig_group.variant_qualifier_position.unwrap_or((0, 0));
403 let actual_qual = seg
404 .elements
405 .get(ei)
406 .and_then(|e| e.get(ci))
407 .map(|s| s.as_str())
408 .unwrap_or("");
409 if !actual_qual.eq_ignore_ascii_case(expected_code) {
410 break;
411 }
412 }
413
414 let mut instance = AssembledGroupInstance {
415 segments: Vec::new(),
416 child_groups: Vec::new(),
417 entry_mig_number: entry_segment.number.clone(),
418 variant_mig_numbers: collect_mig_numbers(mig_group),
419 skipped_segments: Vec::new(),
420 };
421
422 let mut slot_idx = 0;
433 let mut is_entry_run = true;
434 while slot_idx < mig_group.segments.len() {
435 if cursor.is_exhausted() {
436 break;
437 }
438 let current_tag = &mig_group.segments[slot_idx].id;
439 let run_len = mig_group.segments[slot_idx..]
440 .iter()
441 .take_while(|s| s.id == *current_tag)
442 .count();
443
444 if is_entry_run {
445 let entry_slots = &mig_group.segments[slot_idx..slot_idx + run_len];
447 if self.config.strict_code_matching && run_len > 1 {
448 self.consume_entry_run_best_match(
454 segments,
455 cursor,
456 entry_slots,
457 &mut instance,
458 )?;
459 } else {
460 for slot in entry_slots {
461 if cursor.is_exhausted() {
462 break;
463 }
464 if let Some(assembled) =
465 self.try_consume_segment(segments, cursor, slot)?
466 {
467 instance.segments.push(assembled);
468 }
469 }
470 }
471 is_entry_run = false;
472 } else if matcher::matches_segment_tag(current_tag, &entry_segment.id) {
473 if cursor.is_exhausted() {
491 break;
492 }
493 let seg = &segments[cursor.position()];
494 if !matcher::matches_segment_tag(&seg.id, current_tag) {
495 break;
496 }
497 let has_following_non_entry = if cursor.position() + 1 < segments.len() {
500 let next = &segments[cursor.position() + 1];
501 !matcher::matches_segment_tag(&next.id, &entry_segment.id)
502 && mig_group.segments.iter().any(|s| {
503 matcher::matches_segment_tag(&next.id, &s.id)
504 && !matcher::matches_segment_tag(&s.id, &entry_segment.id)
505 })
506 } else {
507 false
508 };
509 if has_following_non_entry {
510 instance.segments.push(owned_to_assembled(seg));
512 cursor.advance();
513 } else {
514 break;
516 }
517 } else {
518 let slots = &mig_group.segments[slot_idx..slot_idx + run_len];
524 if self.config.strict_code_matching && run_len > 1 {
525 self.consume_entry_run_best_match(segments, cursor, slots, &mut instance)?;
526 } else {
527 for slot in slots {
528 if cursor.is_exhausted() {
529 break;
530 }
531 if let Some(assembled) =
532 self.try_consume_segment(segments, cursor, slot)?
533 {
534 instance.segments.push(assembled);
535 }
536 }
537 }
538 while !cursor.is_exhausted() {
540 let seg = &segments[cursor.position()];
541 if matcher::matches_segment_tag(&seg.id, current_tag) {
542 instance.segments.push(owned_to_assembled(seg));
543 cursor.advance();
544 } else {
545 break;
546 }
547 }
548 }
549
550 slot_idx += run_len;
551
552 if self.config.skip_unknown_segments {
557 while !cursor.is_exhausted() {
558 let seg = &segments[cursor.position()];
559 if matcher::matches_segment_tag(&seg.id, &entry_segment.id) {
561 break;
562 }
563 if mig_group.segments[slot_idx..]
565 .iter()
566 .any(|s| matcher::matches_segment_tag(&seg.id, &s.id))
567 {
568 break;
569 }
570 if mig_group.nested_groups.iter().any(|ng| {
572 ng.segments
573 .first()
574 .is_some_and(|es| matcher::matches_segment_tag(&seg.id, &es.id))
575 }) {
576 break;
577 }
578 instance.skipped_segments.push(owned_to_assembled(seg));
580 cursor.advance();
581 }
582 }
583 }
584
585 let mut nested_idx = 0;
587 while nested_idx < mig_group.nested_groups.len() {
588 if cursor.is_exhausted() {
589 break;
590 }
591 let nested = &mig_group.nested_groups[nested_idx];
592
593 if nested.variant_code.is_some() {
594 let variant_count = mig_group.nested_groups[nested_idx..]
596 .iter()
597 .take_while(|g| g.id == nested.id && g.variant_code.is_some())
598 .count();
599 let variant_end = nested_idx + variant_count;
600 let variant_groups = &mig_group.nested_groups[nested_idx..variant_end];
601 if let Some(combined) =
602 self.try_consume_variant_groups(segments, cursor, variant_groups)?
603 {
604 instance.child_groups.push(combined);
605 }
606 nested_idx = variant_end;
607 } else {
608 if let Some(assembled) = self.try_consume_group(segments, cursor, nested)? {
609 instance.child_groups.push(assembled);
610 }
611 nested_idx += 1;
612 }
613 }
614
615 if cursor.position() == iter_start {
621 break;
622 }
623 repetitions.push(instance);
624 }
625
626 if repetitions.is_empty() {
627 Ok(None)
628 } else {
629 Ok(Some(AssembledGroup {
630 group_id: mig_group.id.clone(),
631 repetitions,
632 }))
633 }
634 }
635
636 fn try_consume_variant_groups(
642 &self,
643 segments: &[OwnedSegment],
644 cursor: &mut SegmentCursor,
645 variants: &[MigSegmentGroup],
646 ) -> Result<Option<AssembledGroup>, AssemblyError> {
647 let group_id = variants[0].id.clone();
648 let entry_tag = variants[0]
649 .segments
650 .first()
651 .map(|s| s.id.as_str())
652 .unwrap_or("");
653 let mut all_reps = Vec::new();
654
655 while !cursor.is_exhausted() {
656 let seg = &segments[cursor.position()];
657 if !matcher::matches_segment_tag(&seg.id, entry_tag) {
658 break;
659 }
660
661 let matched = variants.iter().find(|v| {
665 let (ei, ci) = v.variant_qualifier_position.unwrap_or((0, 0));
666 let actual_qual = seg
667 .elements
668 .get(ei)
669 .and_then(|e| e.get(ci))
670 .map(|s| s.as_str())
671 .unwrap_or("");
672 if !v.variant_codes.is_empty() {
673 v.variant_codes
674 .iter()
675 .any(|c| actual_qual.eq_ignore_ascii_case(c))
676 } else if let Some(ref expected_code) = v.variant_code {
677 actual_qual.eq_ignore_ascii_case(expected_code)
678 } else {
679 false
680 }
681 });
682
683 if let Some(variant) = matched {
684 if let Some(group) = self.try_consume_group(segments, cursor, variant)? {
685 all_reps.extend(group.repetitions);
686 } else {
687 break;
688 }
689 } else {
690 if let Some(group) = self.try_consume_group(segments, cursor, &variants[0])? {
694 all_reps.extend(group.repetitions);
695 } else {
696 break;
697 }
698 }
699 }
700
701 if all_reps.is_empty() {
702 Ok(None)
703 } else {
704 Ok(Some(AssembledGroup {
705 group_id,
706 repetitions: all_reps,
707 }))
708 }
709 }
710
711 pub fn assemble_with_diagnostics(
716 &self,
717 segments: &[OwnedSegment],
718 ) -> (AssembledTree, Vec<StructureDiagnostic>) {
719 let mut diagnostics = Vec::new();
720
721 let tree = match self.assemble_generic(segments) {
722 Ok(tree) => tree,
723 Err(e) => {
724 diagnostics.push(StructureDiagnostic {
725 kind: StructureDiagnosticKind::UnexpectedSegment,
726 segment_id: String::new(),
727 position: 0,
728 message: format!("Assembly failed: {e}"),
729 });
730 return (
731 AssembledTree {
732 segments: Vec::new(),
733 groups: Vec::new(),
734 post_group_start: 0,
735 inter_group_segments: std::collections::BTreeMap::new(),
736 },
737 diagnostics,
738 );
739 }
740 };
741
742 let consumed = count_tree_segments(&tree);
744
745 for (i, seg) in segments.iter().enumerate().skip(consumed) {
747 diagnostics.push(StructureDiagnostic {
748 kind: StructureDiagnosticKind::UnexpectedSegment,
749 segment_id: seg.id.clone(),
750 position: i,
751 message: format!(
752 "Segment '{}' at position {} was not consumed by MIG-guided assembly",
753 seg.id, i
754 ),
755 });
756 }
757
758 (tree, diagnostics)
759 }
760}
761
762fn count_tree_segments(tree: &AssembledTree) -> usize {
763 let mut count = tree.segments.len();
764 for group in &tree.groups {
765 count += count_group_segments(group);
766 }
767 for segs in tree.inter_group_segments.values() {
769 count += segs.len();
770 }
771 count
772}
773
774fn count_group_segments(group: &AssembledGroup) -> usize {
775 let mut count = 0;
776 for rep in &group.repetitions {
777 count += rep.segments.len();
778 count += rep.skipped_segments.len();
779 for child in &rep.child_groups {
780 count += count_group_segments(child);
781 }
782 }
783 count
784}
785
786fn collect_mig_numbers(group: &MigSegmentGroup) -> Vec<String> {
791 let mut numbers = Vec::new();
792 for seg in &group.segments {
793 if let Some(ref num) = seg.number {
794 numbers.push(num.clone());
795 }
796 }
797 for nested in &group.nested_groups {
798 numbers.extend(collect_mig_numbers(nested));
799 }
800 numbers
801}
802
803pub fn owned_to_assembled(seg: &OwnedSegment) -> AssembledSegment {
804 AssembledSegment {
805 tag: seg.id.clone(),
806 elements: seg.elements.clone(),
807 mig_number: None,
808 }
809}
810
811fn segment_matches_mig_codes(seg: &OwnedSegment, mig_seg: &MigSegment) -> bool {
819 let actual_at = |el: usize, c: usize| -> &str {
820 seg.elements
821 .get(el)
822 .and_then(|e| e.get(c))
823 .map(|s| s.as_str())
824 .unwrap_or("")
825 };
826 for de in &mig_seg.data_elements {
827 if !de.codes.is_empty() {
828 let actual = actual_at(de.position, 0);
829 if !actual.is_empty() && !de.codes.iter().any(|c| c.value == actual) {
830 return false;
831 }
832 }
833 }
834 for comp in &mig_seg.composites {
835 for de in &comp.data_elements {
836 if !de.codes.is_empty() {
837 let actual = actual_at(comp.position, de.position);
838 if !actual.is_empty() && !de.codes.iter().any(|c| c.value == actual) {
839 return false;
840 }
841 }
842 }
843 }
844 true
845}
846
847#[cfg(test)]
848mod tests {
849 use super::*;
850 use crate::test_support::{make_mig_group, make_mig_group_with_variant, make_mig_segment};
851
852 fn make_owned_seg(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
853 OwnedSegment {
854 id: id.to_string(),
855 elements: elements
856 .into_iter()
857 .map(|e| e.into_iter().map(|c| c.to_string()).collect())
858 .collect(),
859 segment_number: 0,
860 }
861 }
862
863 fn make_mig_schema(segments: Vec<&str>, groups: Vec<MigSegmentGroup>) -> MigSchema {
864 MigSchema {
865 message_type: "UTILMD".to_string(),
866 variant: Some("Strom".to_string()),
867 version: "S2.1".to_string(),
868 publication_date: "2025-03-20".to_string(),
869 author: "BDEW".to_string(),
870 format_version: "FV2504".to_string(),
871 source_file: "test".to_string(),
872 segments: segments.into_iter().map(make_mig_segment).collect(),
873 segment_groups: groups,
874 }
875 }
876
877 #[test]
878 fn test_assembler_top_level_segments_only() {
879 let mig = make_mig_schema(vec!["UNH", "BGM", "DTM", "UNT"], vec![]);
880
881 let segments = vec![
882 make_owned_seg("UNH", vec![vec!["001", "UTILMD:D:11A:UN:S2.1"]]),
883 make_owned_seg("BGM", vec![vec!["E01", "DOC001"]]),
884 make_owned_seg("DTM", vec![vec!["137", "20250101", "102"]]),
885 make_owned_seg("UNT", vec![vec!["4", "001"]]),
886 ];
887
888 let assembler = Assembler::new(&mig);
889 let result = assembler.assemble_generic(&segments).unwrap();
890
891 assert_eq!(result.segments.len(), 4);
892 assert_eq!(result.segments[0].tag, "UNH");
893 assert_eq!(result.segments[1].tag, "BGM");
894 assert_eq!(result.segments[2].tag, "DTM");
895 assert_eq!(result.segments[3].tag, "UNT");
896 assert!(result.groups.is_empty());
897 }
898
899 #[test]
900 fn test_assembler_with_segment_group() {
901 let mig = make_mig_schema(
902 vec!["UNH", "BGM"],
903 vec![
904 make_mig_group("SG2", vec!["NAD"], vec![]),
905 make_mig_group("SG4", vec!["IDE", "STS"], vec![]),
906 ],
907 );
908
909 let segments = vec![
910 make_owned_seg("UNH", vec![vec!["001"]]),
911 make_owned_seg("BGM", vec![vec!["E01"]]),
912 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
913 make_owned_seg("NAD", vec![vec!["MR", "9900456"]]),
914 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
915 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
916 ];
917
918 let assembler = Assembler::new(&mig);
919 let result = assembler.assemble_generic(&segments).unwrap();
920
921 assert_eq!(result.segments.len(), 2);
923 assert_eq!(result.groups.len(), 2);
925 assert_eq!(result.groups[0].group_id, "SG2");
926 assert_eq!(result.groups[0].repetitions.len(), 2);
927 assert_eq!(result.groups[0].repetitions[0].segments[0].tag, "NAD");
928 assert_eq!(result.groups[0].repetitions[1].segments[0].tag, "NAD");
929 assert_eq!(result.groups[1].group_id, "SG4");
931 assert_eq!(result.groups[1].repetitions.len(), 1);
932 assert_eq!(result.groups[1].repetitions[0].segments.len(), 2);
933 }
934
935 #[test]
936 fn test_assembler_nested_groups() {
937 let sg3 = make_mig_group("SG3", vec!["CTA", "COM"], vec![]);
938 let mig = make_mig_schema(
939 vec!["UNH", "BGM"],
940 vec![make_mig_group("SG2", vec!["NAD"], vec![sg3])],
941 );
942
943 let segments = vec![
944 make_owned_seg("UNH", vec![vec!["001"]]),
945 make_owned_seg("BGM", vec![vec!["E01"]]),
946 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
947 make_owned_seg("CTA", vec![vec!["IC", "Kontakt"]]),
948 make_owned_seg("COM", vec![vec!["040@example.com", "EM"]]),
949 ];
950
951 let assembler = Assembler::new(&mig);
952 let result = assembler.assemble_generic(&segments).unwrap();
953
954 let sg2 = &result.groups[0];
956 assert_eq!(sg2.group_id, "SG2");
957 assert_eq!(sg2.repetitions.len(), 1);
958
959 let sg2_inst = &sg2.repetitions[0];
960 assert_eq!(sg2_inst.segments[0].tag, "NAD");
961
962 assert_eq!(sg2_inst.child_groups.len(), 1);
964 let sg3 = &sg2_inst.child_groups[0];
965 assert_eq!(sg3.group_id, "SG3");
966 assert_eq!(sg3.repetitions[0].segments.len(), 2);
967 assert_eq!(sg3.repetitions[0].segments[0].tag, "CTA");
968 assert_eq!(sg3.repetitions[0].segments[1].tag, "COM");
969 }
970
971 #[test]
972 fn test_assembler_optional_segments_skipped() {
973 let mig = make_mig_schema(vec!["UNH", "BGM", "DTM", "UNT"], vec![]);
975
976 let segments = vec![
977 make_owned_seg("UNH", vec![vec!["001"]]),
978 make_owned_seg("BGM", vec![vec!["E01"]]),
979 make_owned_seg("UNT", vec![vec!["2", "001"]]),
980 ];
981
982 let assembler = Assembler::new(&mig);
983 let result = assembler.assemble_generic(&segments).unwrap();
984
985 assert_eq!(result.segments.len(), 3);
987 assert_eq!(result.segments[0].tag, "UNH");
988 assert_eq!(result.segments[1].tag, "BGM");
989 assert_eq!(result.segments[2].tag, "UNT");
990 }
991
992 #[test]
993 fn test_assembler_empty_segments() {
994 let mig = make_mig_schema(vec!["UNH"], vec![]);
995 let assembler = Assembler::new(&mig);
996 let result = assembler.assemble_generic(&[]).unwrap();
997 assert!(result.segments.is_empty());
998 assert!(result.groups.is_empty());
999 }
1000
1001 #[test]
1002 fn test_assembler_preserves_element_data() {
1003 let mig = make_mig_schema(vec!["DTM"], vec![]);
1004
1005 let segments = vec![make_owned_seg(
1006 "DTM",
1007 vec![vec!["137", "202501010000+01", "303"]],
1008 )];
1009
1010 let assembler = Assembler::new(&mig);
1011 let result = assembler.assemble_generic(&segments).unwrap();
1012
1013 let dtm = &result.segments[0];
1014 assert_eq!(dtm.elements[0][0], "137");
1015 assert_eq!(dtm.elements[0][1], "202501010000+01");
1016 assert_eq!(dtm.elements[0][2], "303");
1017 }
1018
1019 #[test]
1020 fn test_group_instance_as_assembled_tree() {
1021 let sg5 = AssembledGroup {
1023 group_id: "SG5".to_string(),
1024 repetitions: vec![AssembledGroupInstance {
1025 segments: vec![AssembledSegment {
1026 tag: "LOC".to_string(),
1027 elements: vec![vec!["Z16".to_string(), "DE000111222333".to_string()]],
1028 mig_number: None,
1029 }],
1030 child_groups: vec![],
1031 entry_mig_number: None,
1032 variant_mig_numbers: vec![],
1033 skipped_segments: vec![],
1034 }],
1035 };
1036
1037 let sg4_instance = AssembledGroupInstance {
1038 segments: vec![
1039 AssembledSegment {
1040 tag: "IDE".to_string(),
1041 elements: vec![vec!["24".to_string(), "TX001".to_string()]],
1042 mig_number: None,
1043 },
1044 AssembledSegment {
1045 tag: "STS".to_string(),
1046 elements: vec![vec!["7".to_string()]],
1047 mig_number: None,
1048 },
1049 ],
1050 child_groups: vec![sg5],
1051 entry_mig_number: None,
1052 variant_mig_numbers: vec![],
1053 skipped_segments: vec![],
1054 };
1055
1056 let sub_tree = sg4_instance.as_assembled_tree();
1057
1058 assert_eq!(sub_tree.segments.len(), 2);
1060 assert_eq!(sub_tree.segments[0].tag, "IDE");
1061 assert_eq!(sub_tree.segments[1].tag, "STS");
1062
1063 assert_eq!(sub_tree.groups.len(), 1);
1065 assert_eq!(sub_tree.groups[0].group_id, "SG5");
1066
1067 assert_eq!(sub_tree.post_group_start, 2);
1069 }
1070
1071 #[test]
1072 fn test_assembler_from_parsed_edifact() {
1073 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'";
1075 let segments = crate::tokenize::parse_to_segments(input).unwrap();
1076
1077 let mig = make_mig_schema(vec!["UNB", "UNH", "BGM", "DTM", "UNT", "UNZ"], vec![]);
1078
1079 let assembler = Assembler::new(&mig);
1080 let result = assembler.assemble_generic(&segments).unwrap();
1081
1082 assert!(result.segments.iter().any(|s| s.tag == "UNH"));
1083 assert!(result.segments.iter().any(|s| s.tag == "BGM"));
1084 assert!(result.segments.iter().any(|s| s.tag == "DTM"));
1085 }
1086
1087 #[test]
1088 fn test_assemble_with_diagnostics_clean_input() {
1089 let mig = make_mig_schema(vec!["UNH", "BGM", "UNT"], vec![]);
1090 let segments = vec![
1091 make_owned_seg("UNH", vec![vec!["001"]]),
1092 make_owned_seg("BGM", vec![vec!["E01"]]),
1093 make_owned_seg("UNT", vec![vec!["2", "001"]]),
1094 ];
1095 let assembler = Assembler::new(&mig);
1096 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1097 assert_eq!(tree.segments.len(), 3);
1098 assert!(
1099 diagnostics.is_empty(),
1100 "Clean input should have no diagnostics"
1101 );
1102 }
1103
1104 #[test]
1105 fn test_assemble_with_diagnostics_unconsumed_segments() {
1106 let mig = make_mig_schema(vec!["UNH", "BGM"], vec![]);
1107 let segments = vec![
1108 make_owned_seg("UNH", vec![vec!["001"]]),
1109 make_owned_seg("BGM", vec![vec!["E01"]]),
1110 make_owned_seg("FTX", vec![vec!["AAA", "extra text"]]),
1111 ];
1112 let assembler = Assembler::new(&mig);
1113 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1114 assert_eq!(tree.segments.len(), 2);
1115 assert_eq!(diagnostics.len(), 1);
1116 assert_eq!(
1117 diagnostics[0].kind,
1118 StructureDiagnosticKind::UnexpectedSegment
1119 );
1120 assert_eq!(diagnostics[0].segment_id, "FTX");
1121 assert_eq!(diagnostics[0].position, 2);
1122 }
1123
1124 #[test]
1125 fn test_assemble_with_diagnostics_multiple_unconsumed() {
1126 let mig = make_mig_schema(vec!["UNH"], vec![]);
1127 let segments = vec![
1128 make_owned_seg("UNH", vec![vec!["001"]]),
1129 make_owned_seg("FOO", vec![]),
1130 make_owned_seg("BAR", vec![]),
1131 make_owned_seg("BAZ", vec![]),
1132 ];
1133 let assembler = Assembler::new(&mig);
1134 let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
1135 assert_eq!(tree.segments.len(), 1);
1136 assert_eq!(diagnostics.len(), 3);
1137 assert_eq!(diagnostics[0].segment_id, "FOO");
1138 assert_eq!(diagnostics[1].segment_id, "BAR");
1139 assert_eq!(diagnostics[2].segment_id, "BAZ");
1140 }
1141
1142 #[test]
1145 fn test_non_entry_segments_get_mig_number_from_bounded_slots() {
1146 use crate::test_support::make_mig_segment_numbered;
1150
1151 let sg4 = MigSegmentGroup {
1152 segments: vec![
1153 make_mig_segment_numbered("IDE", "00020"),
1154 make_mig_segment_numbered("DTM", "00023"),
1155 make_mig_segment_numbered("DTM", "00024"),
1156 make_mig_segment_numbered("STS", "00035"),
1157 ],
1158 ..make_mig_group("SG4", vec![], vec![])
1159 };
1160 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1161
1162 let segments = vec![
1163 make_owned_seg("UNH", vec![vec!["001"]]),
1164 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1165 make_owned_seg("DTM", vec![vec!["92", "202505312200+00", "303"]]),
1166 make_owned_seg("DTM", vec![vec!["93", "202512312300+00", "303"]]),
1167 make_owned_seg("STS", vec![vec!["7"], vec![], vec!["E01"]]),
1168 ];
1169
1170 let assembler = Assembler::new(&mig);
1171 let tree = assembler.assemble_generic(&segments).unwrap();
1172
1173 let sg4_instance = &tree.groups[0].repetitions[0];
1174
1175 assert_eq!(sg4_instance.segments[0].tag, "IDE");
1177 assert_eq!(sg4_instance.segments[0].mig_number.as_deref(), Some("00020"));
1178
1179 assert_eq!(sg4_instance.segments[1].tag, "DTM");
1181 assert_eq!(sg4_instance.segments[1].mig_number.as_deref(), Some("00023"));
1182
1183 assert_eq!(sg4_instance.segments[2].tag, "DTM");
1185 assert_eq!(sg4_instance.segments[2].mig_number.as_deref(), Some("00024"));
1186
1187 assert_eq!(sg4_instance.segments[3].tag, "STS");
1189 assert_eq!(sg4_instance.segments[3].mig_number.as_deref(), Some("00035"));
1190
1191 assert!(sg4_instance.variant_mig_numbers.contains(&"00020".to_string()));
1193 assert!(sg4_instance.variant_mig_numbers.contains(&"00023".to_string()));
1194 assert!(sg4_instance.variant_mig_numbers.contains(&"00024".to_string()));
1195 assert!(sg4_instance.variant_mig_numbers.contains(&"00035".to_string()));
1196 }
1197
1198 #[test]
1199 fn test_greedy_extra_segments_get_no_mig_number() {
1200 use crate::test_support::make_mig_segment_numbered;
1203
1204 let sg4 = MigSegmentGroup {
1205 segments: vec![
1206 make_mig_segment_numbered("IDE", "00020"),
1207 make_mig_segment_numbered("DTM", "00023"),
1208 ],
1209 ..make_mig_group("SG4", vec![], vec![])
1210 };
1211 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1212
1213 let segments = vec![
1214 make_owned_seg("UNH", vec![vec!["001"]]),
1215 make_owned_seg("IDE", vec![vec!["24"]]),
1216 make_owned_seg("DTM", vec![vec!["92", "20250531"]]),
1217 make_owned_seg("DTM", vec![vec!["93", "20251231"]]), ];
1219
1220 let assembler = Assembler::new(&mig);
1221 let tree = assembler.assemble_generic(&segments).unwrap();
1222
1223 let sg4_instance = &tree.groups[0].repetitions[0];
1224 assert_eq!(sg4_instance.segments.len(), 3); assert_eq!(sg4_instance.segments[1].mig_number.as_deref(), Some("00023"));
1228
1229 assert_eq!(sg4_instance.segments[2].mig_number, None);
1231 }
1232
1233 #[test]
1236 fn test_qualifier_map_prevents_wrong_slot_consumption() {
1237 use crate::test_support::make_mig_segment_numbered;
1242 use std::collections::HashMap;
1243
1244 let sg4 = MigSegmentGroup {
1245 segments: vec![
1246 make_mig_segment_numbered("IDE", "00020"),
1247 make_mig_segment_numbered("DTM", "00023"),
1248 make_mig_segment_numbered("DTM", "00024"),
1249 ],
1250 ..make_mig_group("SG4", vec![], vec![])
1251 };
1252 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1253
1254 let segments = vec![
1255 make_owned_seg("UNH", vec![vec!["001"]]),
1256 make_owned_seg("IDE", vec![vec!["24"]]),
1257 make_owned_seg("DTM", vec![vec!["93", "202512312300+00", "303"]]),
1258 ];
1259
1260 let mut qualifier_map = HashMap::new();
1261 qualifier_map.insert("00023".to_string(), (0, 0, "92".to_string()));
1262 qualifier_map.insert("00024".to_string(), (0, 0, "93".to_string()));
1263
1264 let config = AssemblerConfig {
1265 skip_unknown_segments: false,
1266 qualifier_map,
1267 ..Default::default()
1268 };
1269 let assembler = Assembler::with_config(&mig, config);
1270 let tree = assembler.assemble_generic(&segments).unwrap();
1271
1272 let sg4_instance = &tree.groups[0].repetitions[0];
1273
1274 assert_eq!(sg4_instance.segments.len(), 2); let dtm = &sg4_instance.segments[1];
1277 assert_eq!(dtm.tag, "DTM");
1278 assert_eq!(
1279 dtm.mig_number.as_deref(),
1280 Some("00024"),
1281 "DTM+93 should get mig_number 00024 (not 00023)"
1282 );
1283 }
1284
1285 #[test]
1286 fn test_group_entry_qualifier_mismatch_does_not_infinite_loop() {
1287 use crate::test_support::make_mig_segment_numbered;
1297 use std::collections::HashMap;
1298
1299 let sg5 = MigSegmentGroup {
1300 segments: vec![make_mig_segment_numbered("LOC", "00050")],
1301 ..make_mig_group("SG5", vec![], vec![])
1302 };
1303 let mig = make_mig_schema(vec!["UNH"], vec![sg5]);
1304
1305 let segments = vec![
1306 make_owned_seg("UNH", vec![vec!["001"]]),
1307 make_owned_seg("LOC", vec![vec!["172"], vec!["92003964705"]]),
1309 ];
1310
1311 let mut qualifier_map = HashMap::new();
1312 qualifier_map.insert("00050".to_string(), (0, 0, "Z16".to_string()));
1313
1314 let config = AssemblerConfig {
1315 skip_unknown_segments: false,
1316 qualifier_map,
1317 ..Default::default()
1318 };
1319 let assembler = Assembler::with_config(&mig, config);
1320
1321 let start = std::time::Instant::now();
1325 let tree = assembler.assemble_generic(&segments).unwrap();
1326 assert!(
1327 start.elapsed() < std::time::Duration::from_secs(5),
1328 "assembly took {:?} — suspected infinite-loop regression",
1329 start.elapsed()
1330 );
1331
1332 assert!(tree.groups.is_empty());
1336 }
1337
1338 #[test]
1341 fn test_skip_unknown_segment_between_slots() {
1342 let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
1346 let mig = make_mig_schema(vec!["UNH"], vec![sg8.clone()]);
1347
1348 let segments = vec![
1349 make_owned_seg("UNH", vec![vec!["001"]]),
1350 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1351 make_owned_seg("RFF", vec![vec!["Z38", "CROSSREF"]]),
1352 make_owned_seg("CCI", vec![vec!["Z30"]]),
1353 ];
1354
1355 let off = Assembler::new(&mig);
1357 let tree_off = off.assemble_generic(&segments).unwrap();
1358 let sg8_off = &tree_off.groups[0];
1359 assert_eq!(sg8_off.repetitions[0].segments.len(), 1); assert_eq!(sg8_off.repetitions[0].segments[0].tag, "SEQ");
1361
1362 let on = Assembler::with_config(
1364 &mig,
1365 AssemblerConfig {
1366 skip_unknown_segments: true,
1367 ..Default::default()
1368 },
1369 );
1370 let tree_on = on.assemble_generic(&segments).unwrap();
1371 let sg8_on = &tree_on.groups[0];
1372 assert_eq!(sg8_on.repetitions[0].segments.len(), 2); assert_eq!(sg8_on.repetitions[0].segments[0].tag, "SEQ");
1374 assert_eq!(sg8_on.repetitions[0].segments[1].tag, "CCI");
1375 }
1376
1377 #[test]
1378 fn test_skip_preserves_on_instance() {
1379 let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
1381 let mig = make_mig_schema(vec!["UNH"], vec![sg8]);
1382
1383 let segments = vec![
1384 make_owned_seg("UNH", vec![vec!["001"]]),
1385 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1386 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
1387 make_owned_seg("DTM", vec![vec!["92", "20250101"]]),
1388 make_owned_seg("CCI", vec![vec!["Z30"]]),
1389 ];
1390
1391 let assembler = Assembler::with_config(
1392 &mig,
1393 AssemblerConfig {
1394 skip_unknown_segments: true,
1395 ..Default::default()
1396 },
1397 );
1398 let tree = assembler.assemble_generic(&segments).unwrap();
1399 let instance = &tree.groups[0].repetitions[0];
1400
1401 assert_eq!(instance.segments.len(), 2); assert_eq!(instance.skipped_segments.len(), 2); assert_eq!(instance.skipped_segments[0].tag, "RFF");
1404 assert_eq!(instance.skipped_segments[1].tag, "DTM");
1405 }
1406
1407 #[test]
1408 fn test_skip_mode_off_default() {
1409 let mig = make_mig_schema(vec![], vec![]);
1411 let assembler = Assembler::new(&mig);
1412 assert!(!assembler.config.skip_unknown_segments);
1413 }
1414
1415 #[test]
1416 fn test_skip_does_not_consume_nested_group_entry() {
1417 let sg5 = make_mig_group("SG5", vec!["LOC"], vec![]);
1421 let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![sg5]);
1422 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1423
1424 let segments = vec![
1425 make_owned_seg("UNH", vec![vec!["001"]]),
1426 make_owned_seg("IDE", vec![vec!["24"]]),
1427 make_owned_seg("FOO", vec![vec!["unknown"]]),
1428 make_owned_seg("STS", vec![vec!["7"]]),
1429 make_owned_seg("LOC", vec![vec!["Z16"]]),
1430 ];
1431
1432 let assembler = Assembler::with_config(
1433 &mig,
1434 AssemblerConfig {
1435 skip_unknown_segments: true,
1436 ..Default::default()
1437 },
1438 );
1439 let tree = assembler.assemble_generic(&segments).unwrap();
1440 let sg4 = &tree.groups[0];
1441 let inst = &sg4.repetitions[0];
1442
1443 assert_eq!(inst.segments.len(), 2);
1445 assert_eq!(inst.segments[0].tag, "IDE");
1446 assert_eq!(inst.segments[1].tag, "STS");
1447 assert_eq!(inst.skipped_segments.len(), 1);
1448 assert_eq!(inst.skipped_segments[0].tag, "FOO");
1449
1450 assert_eq!(inst.child_groups.len(), 1);
1452 assert_eq!(inst.child_groups[0].group_id, "SG5");
1453 assert_eq!(inst.child_groups[0].repetitions[0].segments[0].tag, "LOC");
1454 }
1455
1456 #[test]
1457 fn test_roundtrip_with_skip() {
1458 use crate::disassembler::Disassembler;
1461 use crate::renderer::render_edifact;
1462
1463 let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
1464 let mig = make_mig_schema(vec!["UNH", "UNT"], vec![sg8]);
1465
1466 let segments = vec![
1467 make_owned_seg("UNH", vec![vec!["001"]]),
1468 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1469 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
1470 make_owned_seg("CCI", vec![vec!["Z30"]]),
1471 make_owned_seg("UNT", vec![vec!["4", "001"]]),
1472 ];
1473
1474 let assembler = Assembler::with_config(
1475 &mig,
1476 AssemblerConfig {
1477 skip_unknown_segments: true,
1478 ..Default::default()
1479 },
1480 );
1481 let tree = assembler.assemble_generic(&segments).unwrap();
1482
1483 let disassembler = Disassembler::new(&mig);
1484 let dis = disassembler.disassemble(&tree);
1485 let delimiters = edifact_primitives::EdifactDelimiters::default();
1486 let rendered = render_edifact(&dis, &delimiters);
1487
1488 assert_eq!(dis.len(), 5);
1493 assert_eq!(dis[0].tag, "UNH");
1494 assert_eq!(dis[1].tag, "SEQ");
1495 assert_eq!(dis[2].tag, "CCI");
1496 assert_eq!(dis[3].tag, "RFF"); assert_eq!(dis[4].tag, "UNT");
1498
1499 assert!(rendered.contains("UNH+001"));
1501 assert!(rendered.contains("SEQ+Z98"));
1502 assert!(rendered.contains("RFF+Z38:REF1"));
1503 assert!(rendered.contains("CCI+Z30"));
1504 assert!(rendered.contains("UNT+4:001"));
1505 }
1506
1507 #[test]
1510 fn test_variant_groups_interleaved_reps() {
1511 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
1515 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
1516
1517 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
1518
1519 let segments = vec![
1520 make_owned_seg("UNH", vec![vec!["001"]]),
1521 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
1522 make_owned_seg("CCI", vec![vec!["Z30"]]),
1523 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1524 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
1525 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
1526 make_owned_seg("CCI", vec![vec!["Z31"]]),
1527 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1528 make_owned_seg("RFF", vec![vec!["Z38", "REF2"]]),
1529 ];
1530
1531 let assembler = Assembler::new(&mig);
1532 let result = assembler.assemble_generic(&segments).unwrap();
1533
1534 assert_eq!(result.segments.len(), 1); assert_eq!(result.groups.len(), 1); let sg8 = &result.groups[0];
1537 assert_eq!(sg8.group_id, "SG8");
1538 assert_eq!(sg8.repetitions.len(), 4);
1539
1540 assert_eq!(sg8.repetitions[0].segments[0].elements[0][0], "ZD7");
1542 assert_eq!(sg8.repetitions[0].segments[1].tag, "CCI");
1543 assert_eq!(sg8.repetitions[1].segments[0].elements[0][0], "Z98");
1544 assert_eq!(sg8.repetitions[1].segments[1].tag, "RFF");
1545 assert_eq!(sg8.repetitions[2].segments[0].elements[0][0], "ZD7");
1546 assert_eq!(sg8.repetitions[3].segments[0].elements[0][0], "Z98");
1547 }
1548
1549 #[test]
1550 fn test_variant_groups_single_variant_type() {
1551 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
1553 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
1554
1555 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
1556
1557 let segments = vec![
1558 make_owned_seg("UNH", vec![vec!["001"]]),
1559 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1560 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
1561 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1562 make_owned_seg("RFF", vec![vec!["Z38", "REF2"]]),
1563 ];
1564
1565 let assembler = Assembler::new(&mig);
1566 let result = assembler.assemble_generic(&segments).unwrap();
1567
1568 assert_eq!(result.groups.len(), 1);
1569 assert_eq!(result.groups[0].repetitions.len(), 2);
1570 assert_eq!(
1571 result.groups[0].repetitions[0].segments[0].elements[0][0],
1572 "Z98"
1573 );
1574 assert_eq!(
1575 result.groups[0].repetitions[1].segments[0].elements[0][0],
1576 "Z98"
1577 );
1578 }
1579
1580 #[test]
1581 fn test_non_variant_groups_unchanged() {
1582 let sg2 = make_mig_group("SG2", vec!["NAD"], vec![]);
1584 let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![]);
1585
1586 let mig = make_mig_schema(vec!["UNH", "BGM"], vec![sg2, sg4]);
1587
1588 let segments = vec![
1589 make_owned_seg("UNH", vec![vec!["001"]]),
1590 make_owned_seg("BGM", vec![vec!["E01"]]),
1591 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1592 make_owned_seg("NAD", vec![vec!["MR", "9900456"]]),
1593 make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
1594 make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
1595 ];
1596
1597 let assembler = Assembler::new(&mig);
1598 let result = assembler.assemble_generic(&segments).unwrap();
1599
1600 assert_eq!(result.segments.len(), 2);
1601 assert_eq!(result.groups.len(), 2);
1602 assert_eq!(result.groups[0].group_id, "SG2");
1603 assert_eq!(result.groups[0].repetitions.len(), 2);
1604 assert_eq!(result.groups[1].group_id, "SG4");
1605 assert_eq!(result.groups[1].repetitions.len(), 1);
1606 }
1607
1608 #[test]
1609 fn test_variant_groups_with_nested_children() {
1610 let sg10 = make_mig_group("SG10", vec!["CCI", "CAV"], vec![]);
1612 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10.clone()], "ZD7");
1613 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10], "Z98");
1614
1615 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
1616
1617 let segments = vec![
1618 make_owned_seg("UNH", vec![vec!["001"]]),
1619 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
1620 make_owned_seg("CCI", vec![vec!["Z30"]]),
1621 make_owned_seg("CAV", vec![vec!["Z91", "Y"]]),
1622 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1623 make_owned_seg("CCI", vec![vec!["Z31"]]),
1624 make_owned_seg("CAV", vec![vec!["Z91", "N"]]),
1625 ];
1626
1627 let assembler = Assembler::new(&mig);
1628 let result = assembler.assemble_generic(&segments).unwrap();
1629
1630 assert_eq!(result.groups.len(), 1);
1631 let sg8 = &result.groups[0];
1632 assert_eq!(sg8.repetitions.len(), 2);
1633
1634 assert_eq!(sg8.repetitions[0].child_groups.len(), 1);
1636 assert_eq!(sg8.repetitions[0].child_groups[0].group_id, "SG10");
1637 assert_eq!(
1638 sg8.repetitions[0].child_groups[0].repetitions[0].segments[0].elements[0][0],
1639 "Z30"
1640 );
1641
1642 assert_eq!(sg8.repetitions[1].child_groups.len(), 1);
1644 assert_eq!(
1645 sg8.repetitions[1].child_groups[0].repetitions[0].segments[0].elements[0][0],
1646 "Z31"
1647 );
1648 }
1649
1650 #[test]
1651 fn test_variant_qualifier_check_prevents_wrong_variant_consumption() {
1652 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
1655
1656 let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7]);
1657
1658 let segments = vec![
1659 make_owned_seg("UNH", vec![vec!["001"]]),
1660 make_owned_seg("SEQ", vec![vec!["Z98"]]), make_owned_seg("CCI", vec![vec!["Z30"]]),
1662 ];
1663
1664 let assembler = Assembler::new(&mig);
1665 let result = assembler.assemble_generic(&segments).unwrap();
1666
1667 assert!(result.groups.is_empty());
1669 }
1670
1671 #[test]
1672 fn test_mixed_variant_and_non_variant_groups() {
1673 let sg2 = make_mig_group("SG2", vec!["NAD"], vec![]);
1675 let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
1676 let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
1677 let sg12 = make_mig_group("SG12", vec!["NAD"], vec![]);
1678
1679 let mig = make_mig_schema(vec!["UNH"], vec![sg2, sg8_zd7, sg8_z98, sg12]);
1680
1681 let segments = vec![
1682 make_owned_seg("UNH", vec![vec!["001"]]),
1683 make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
1684 make_owned_seg("SEQ", vec![vec!["ZD7"]]),
1685 make_owned_seg("CCI", vec![vec!["Z30"]]),
1686 make_owned_seg("SEQ", vec![vec!["Z98"]]),
1687 make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
1688 make_owned_seg("NAD", vec![vec!["Z65", "ID001"]]),
1689 ];
1690
1691 let assembler = Assembler::new(&mig);
1692 let result = assembler.assemble_generic(&segments).unwrap();
1693
1694 assert_eq!(result.groups.len(), 3); assert_eq!(result.groups[0].group_id, "SG2");
1696 assert_eq!(result.groups[0].repetitions.len(), 1);
1697 assert_eq!(result.groups[1].group_id, "SG8");
1698 assert_eq!(result.groups[1].repetitions.len(), 2);
1699 assert_eq!(result.groups[2].group_id, "SG12");
1700 assert_eq!(result.groups[2].repetitions.len(), 1);
1701 }
1702
1703 #[test]
1704 fn test_assembler_disambiguates_shared_qualifier_by_full_code_profile() {
1705 use mig_types::schema::common::CodeDefinition;
1713 use mig_types::schema::mig::{MigComposite, MigDataElement};
1714 use std::collections::HashMap;
1715
1716 fn code(value: &str) -> CodeDefinition {
1717 CodeDefinition {
1718 value: value.to_string(),
1719 name: value.to_string(),
1720 description: None,
1721 }
1722 }
1723
1724 fn pia_slot(number: &str, composite_code: &str) -> MigSegment {
1725 MigSegment {
1726 id: "PIA".to_string(),
1727 name: "PIA".to_string(),
1728 description: None,
1729 counter: None,
1730 level: 1,
1731 number: Some(number.to_string()),
1732 max_rep_std: 1,
1733 max_rep_spec: 1,
1734 status_std: Some("M".to_string()),
1735 status_spec: Some("M".to_string()),
1736 example: None,
1737 data_elements: vec![MigDataElement {
1738 id: "4347".to_string(),
1739 name: "Produkt-ID-Funktion".to_string(),
1740 description: None,
1741 status_std: Some("M".to_string()),
1742 status_spec: Some("M".to_string()),
1743 format_std: None,
1744 format_spec: None,
1745 codes: vec![code("5")],
1746 position: 0,
1747 }],
1748 composites: vec![MigComposite {
1749 id: "C212".to_string(),
1750 name: "Item Identifier".to_string(),
1751 description: None,
1752 status_std: Some("M".to_string()),
1753 status_spec: Some("M".to_string()),
1754 data_elements: vec![MigDataElement {
1755 id: "7143".to_string(),
1756 name: "Artikel/Dienstleistung-ID".to_string(),
1757 description: None,
1758 status_std: Some("M".to_string()),
1759 status_spec: Some("M".to_string()),
1760 format_std: None,
1761 format_spec: None,
1762 codes: vec![code(composite_code)],
1763 position: 0,
1764 }],
1765 position: 1,
1766 }],
1767 }
1768 }
1769
1770 let sg4 = MigSegmentGroup {
1771 segments: vec![
1772 crate::test_support::make_mig_segment_numbered("IDE", "00020"),
1773 pia_slot("00108", "Z12"),
1774 pia_slot("00197", "SRW"),
1775 ],
1776 ..make_mig_group("SG4", vec![], vec![])
1777 };
1778 let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
1779
1780 let segments = vec![
1781 make_owned_seg("UNH", vec![vec!["001"]]),
1782 make_owned_seg("IDE", vec![vec!["24"]]),
1783 make_owned_seg("PIA", vec![vec!["5"], vec!["SRW"]]),
1785 ];
1786
1787 let mut qualifier_map = HashMap::new();
1791 qualifier_map.insert("00108".to_string(), (0, 0, "5".to_string()));
1792 qualifier_map.insert("00197".to_string(), (0, 0, "5".to_string()));
1793
1794 let config = AssemblerConfig {
1795 skip_unknown_segments: false,
1796 qualifier_map,
1797 strict_code_matching: true,
1798 };
1799 let assembler = Assembler::with_config(&mig, config);
1800 let tree = assembler.assemble_generic(&segments).unwrap();
1801
1802 let sg4_instance = &tree.groups[0].repetitions[0];
1803 let pia = sg4_instance
1804 .segments
1805 .iter()
1806 .find(|s| s.tag == "PIA")
1807 .expect("PIA consumed into SG4");
1808 assert_eq!(
1809 pia.mig_number.as_deref(),
1810 Some("00197"),
1811 "PIA+5+:::SRW must be assigned the SRW variant (mig=00197), not the Z12 variant"
1812 );
1813 }
1814}