use crate::cursor::SegmentCursor;
use crate::diagnostic::{StructureDiagnostic, StructureDiagnosticKind};
use crate::matcher;
use crate::tokenize::OwnedSegment;
use crate::AssemblyError;
use mig_types::schema::mig::{MigSchema, MigSegment, MigSegmentGroup};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssembledTree {
pub segments: Vec<AssembledSegment>,
pub groups: Vec<AssembledGroup>,
#[serde(default)]
pub post_group_start: usize,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub inter_group_segments: std::collections::BTreeMap<usize, Vec<AssembledSegment>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssembledSegment {
pub tag: String,
pub elements: Vec<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssembledGroup {
pub group_id: String,
pub repetitions: Vec<AssembledGroupInstance>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssembledGroupInstance {
pub segments: Vec<AssembledSegment>,
pub child_groups: Vec<AssembledGroup>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skipped_segments: Vec<AssembledSegment>,
}
impl AssembledGroupInstance {
pub fn as_assembled_tree(&self) -> AssembledTree {
AssembledTree {
segments: self.segments.clone(),
groups: self.child_groups.clone(),
post_group_start: self.segments.len(),
inter_group_segments: std::collections::BTreeMap::new(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AssemblerConfig {
pub skip_unknown_segments: bool,
}
pub struct Assembler<'a> {
mig: &'a MigSchema,
config: AssemblerConfig,
}
impl<'a> Assembler<'a> {
pub fn new(mig: &'a MigSchema) -> Self {
Self {
mig,
config: AssemblerConfig::default(),
}
}
pub fn with_config(mig: &'a MigSchema, config: AssemblerConfig) -> Self {
Self { mig, config }
}
pub fn assemble_generic(
&self,
segments: &[OwnedSegment],
) -> Result<AssembledTree, AssemblyError> {
let mut cursor = SegmentCursor::new(segments.len());
let mut tree = AssembledTree {
segments: Vec::new(),
groups: Vec::new(),
post_group_start: 0,
inter_group_segments: std::collections::BTreeMap::new(),
};
let mut matched_seg_indices = Vec::new();
for (i, mig_seg) in self.mig.segments.iter().enumerate() {
if cursor.is_exhausted() {
break;
}
if let Some(assembled) = self.try_consume_segment(segments, &mut cursor, mig_seg)? {
tree.segments.push(assembled);
matched_seg_indices.push(i);
}
}
let mut group_idx = 0;
while group_idx < self.mig.segment_groups.len() {
if cursor.is_exhausted() {
break;
}
let mig_group = &self.mig.segment_groups[group_idx];
let tree_group_idx = tree.groups.len();
for (i, mig_seg) in self.mig.segments.iter().enumerate() {
if cursor.is_exhausted() {
break;
}
if matched_seg_indices.contains(&i) {
continue;
}
if let Some(assembled) = self.try_consume_segment(segments, &mut cursor, mig_seg)? {
tree.inter_group_segments
.entry(tree_group_idx)
.or_default()
.push(assembled);
matched_seg_indices.push(i);
}
}
if mig_group.variant_code.is_some() {
let variant_count = self.mig.segment_groups[group_idx..]
.iter()
.take_while(|g| g.id == mig_group.id && g.variant_code.is_some())
.count();
let variant_end = group_idx + variant_count;
let variant_groups = &self.mig.segment_groups[group_idx..variant_end];
if let Some(combined) =
self.try_consume_variant_groups(segments, &mut cursor, variant_groups)?
{
tree.groups.push(combined);
}
group_idx = variant_end;
} else {
if let Some(assembled) = self.try_consume_group(segments, &mut cursor, mig_group)? {
tree.groups.push(assembled);
}
group_idx += 1;
}
}
tree.post_group_start = tree.segments.len();
for (i, mig_seg) in self.mig.segments.iter().enumerate() {
if cursor.is_exhausted() {
break;
}
if matched_seg_indices.contains(&i) {
continue;
}
if let Some(assembled) = self.try_consume_segment(segments, &mut cursor, mig_seg)? {
tree.segments.push(assembled);
}
}
Ok(tree)
}
fn try_consume_segment(
&self,
segments: &[OwnedSegment],
cursor: &mut SegmentCursor,
mig_seg: &MigSegment,
) -> Result<Option<AssembledSegment>, AssemblyError> {
if cursor.is_exhausted() {
return Ok(None);
}
let seg = &segments[cursor.position()];
if matcher::matches_segment_tag(&seg.id, &mig_seg.id) {
let assembled = owned_to_assembled(seg);
cursor.advance();
Ok(Some(assembled))
} else {
Ok(None) }
}
fn try_consume_group(
&self,
segments: &[OwnedSegment],
cursor: &mut SegmentCursor,
mig_group: &MigSegmentGroup,
) -> Result<Option<AssembledGroup>, AssemblyError> {
let mut repetitions = Vec::new();
let entry_segment = mig_group.segments.first().ok_or_else(|| {
AssemblyError::ParseError(format!("Group {} has no segments", mig_group.id))
})?;
while !cursor.is_exhausted() {
let seg = &segments[cursor.position()];
if !matcher::matches_segment_tag(&seg.id, &entry_segment.id) {
break; }
if !mig_group.variant_codes.is_empty() {
let (ei, ci) = mig_group.variant_qualifier_position.unwrap_or((0, 0));
let actual_qual = seg
.elements
.get(ei)
.and_then(|e| e.get(ci))
.map(|s| s.as_str())
.unwrap_or("");
if !mig_group
.variant_codes
.iter()
.any(|c| actual_qual.eq_ignore_ascii_case(c))
{
break;
}
} else if let Some(ref expected_code) = mig_group.variant_code {
let (ei, ci) = mig_group.variant_qualifier_position.unwrap_or((0, 0));
let actual_qual = seg
.elements
.get(ei)
.and_then(|e| e.get(ci))
.map(|s| s.as_str())
.unwrap_or("");
if !actual_qual.eq_ignore_ascii_case(expected_code) {
break;
}
}
let mut instance = AssembledGroupInstance {
segments: Vec::new(),
child_groups: Vec::new(),
skipped_segments: Vec::new(),
};
let mut slot_idx = 0;
let mut is_entry_run = true;
while slot_idx < mig_group.segments.len() {
if cursor.is_exhausted() {
break;
}
let current_tag = &mig_group.segments[slot_idx].id;
let run_len = mig_group.segments[slot_idx..]
.iter()
.take_while(|s| s.id == *current_tag)
.count();
if is_entry_run {
for slot in &mig_group.segments[slot_idx..slot_idx + run_len] {
if cursor.is_exhausted() {
break;
}
if let Some(assembled) = self.try_consume_segment(segments, cursor, slot)? {
instance.segments.push(assembled);
}
}
is_entry_run = false;
} else if matcher::matches_segment_tag(current_tag, &entry_segment.id) {
if cursor.is_exhausted() {
break;
}
let seg = &segments[cursor.position()];
if !matcher::matches_segment_tag(&seg.id, current_tag) {
break;
}
let has_following_non_entry = if cursor.position() + 1 < segments.len() {
let next = &segments[cursor.position() + 1];
!matcher::matches_segment_tag(&next.id, &entry_segment.id)
&& mig_group.segments.iter().any(|s| {
matcher::matches_segment_tag(&next.id, &s.id)
&& !matcher::matches_segment_tag(&s.id, &entry_segment.id)
})
} else {
false
};
if has_following_non_entry {
instance.segments.push(owned_to_assembled(seg));
cursor.advance();
} else {
break;
}
} else {
while !cursor.is_exhausted() {
let seg = &segments[cursor.position()];
if matcher::matches_segment_tag(&seg.id, current_tag) {
instance.segments.push(owned_to_assembled(seg));
cursor.advance();
} else {
break;
}
}
}
slot_idx += run_len;
if self.config.skip_unknown_segments {
while !cursor.is_exhausted() {
let seg = &segments[cursor.position()];
if matcher::matches_segment_tag(&seg.id, &entry_segment.id) {
break;
}
if mig_group.segments[slot_idx..]
.iter()
.any(|s| matcher::matches_segment_tag(&seg.id, &s.id))
{
break;
}
if mig_group.nested_groups.iter().any(|ng| {
ng.segments
.first()
.is_some_and(|es| matcher::matches_segment_tag(&seg.id, &es.id))
}) {
break;
}
instance.skipped_segments.push(owned_to_assembled(seg));
cursor.advance();
}
}
}
let mut nested_idx = 0;
while nested_idx < mig_group.nested_groups.len() {
if cursor.is_exhausted() {
break;
}
let nested = &mig_group.nested_groups[nested_idx];
if nested.variant_code.is_some() {
let variant_count = mig_group.nested_groups[nested_idx..]
.iter()
.take_while(|g| g.id == nested.id && g.variant_code.is_some())
.count();
let variant_end = nested_idx + variant_count;
let variant_groups = &mig_group.nested_groups[nested_idx..variant_end];
if let Some(combined) =
self.try_consume_variant_groups(segments, cursor, variant_groups)?
{
instance.child_groups.push(combined);
}
nested_idx = variant_end;
} else {
if let Some(assembled) = self.try_consume_group(segments, cursor, nested)? {
instance.child_groups.push(assembled);
}
nested_idx += 1;
}
}
repetitions.push(instance);
}
if repetitions.is_empty() {
Ok(None)
} else {
Ok(Some(AssembledGroup {
group_id: mig_group.id.clone(),
repetitions,
}))
}
}
fn try_consume_variant_groups(
&self,
segments: &[OwnedSegment],
cursor: &mut SegmentCursor,
variants: &[MigSegmentGroup],
) -> Result<Option<AssembledGroup>, AssemblyError> {
let group_id = variants[0].id.clone();
let entry_tag = variants[0]
.segments
.first()
.map(|s| s.id.as_str())
.unwrap_or("");
let mut all_reps = Vec::new();
while !cursor.is_exhausted() {
let seg = &segments[cursor.position()];
if !matcher::matches_segment_tag(&seg.id, entry_tag) {
break;
}
let matched = variants.iter().find(|v| {
let (ei, ci) = v.variant_qualifier_position.unwrap_or((0, 0));
let actual_qual = seg
.elements
.get(ei)
.and_then(|e| e.get(ci))
.map(|s| s.as_str())
.unwrap_or("");
if !v.variant_codes.is_empty() {
v.variant_codes
.iter()
.any(|c| actual_qual.eq_ignore_ascii_case(c))
} else if let Some(ref expected_code) = v.variant_code {
actual_qual.eq_ignore_ascii_case(expected_code)
} else {
false
}
});
if let Some(variant) = matched {
if let Some(group) = self.try_consume_group(segments, cursor, variant)? {
all_reps.extend(group.repetitions);
} else {
break;
}
} else {
if let Some(group) = self.try_consume_group(segments, cursor, &variants[0])? {
all_reps.extend(group.repetitions);
} else {
break;
}
}
}
if all_reps.is_empty() {
Ok(None)
} else {
Ok(Some(AssembledGroup {
group_id,
repetitions: all_reps,
}))
}
}
pub fn assemble_with_diagnostics(
&self,
segments: &[OwnedSegment],
) -> (AssembledTree, Vec<StructureDiagnostic>) {
let mut diagnostics = Vec::new();
let tree = match self.assemble_generic(segments) {
Ok(tree) => tree,
Err(e) => {
diagnostics.push(StructureDiagnostic {
kind: StructureDiagnosticKind::UnexpectedSegment,
segment_id: String::new(),
position: 0,
message: format!("Assembly failed: {e}"),
});
return (
AssembledTree {
segments: Vec::new(),
groups: Vec::new(),
post_group_start: 0,
inter_group_segments: std::collections::BTreeMap::new(),
},
diagnostics,
);
}
};
let consumed = count_tree_segments(&tree);
for (i, seg) in segments.iter().enumerate().skip(consumed) {
diagnostics.push(StructureDiagnostic {
kind: StructureDiagnosticKind::UnexpectedSegment,
segment_id: seg.id.clone(),
position: i,
message: format!(
"Segment '{}' at position {} was not consumed by MIG-guided assembly",
seg.id, i
),
});
}
(tree, diagnostics)
}
}
fn count_tree_segments(tree: &AssembledTree) -> usize {
let mut count = tree.segments.len();
for group in &tree.groups {
count += count_group_segments(group);
}
for segs in tree.inter_group_segments.values() {
count += segs.len();
}
count
}
fn count_group_segments(group: &AssembledGroup) -> usize {
let mut count = 0;
for rep in &group.repetitions {
count += rep.segments.len();
count += rep.skipped_segments.len();
for child in &rep.child_groups {
count += count_group_segments(child);
}
}
count
}
pub fn owned_to_assembled(seg: &OwnedSegment) -> AssembledSegment {
AssembledSegment {
tag: seg.id.clone(),
elements: seg.elements.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{make_mig_group, make_mig_group_with_variant, make_mig_segment};
fn make_owned_seg(id: &str, elements: Vec<Vec<&str>>) -> OwnedSegment {
OwnedSegment {
id: id.to_string(),
elements: elements
.into_iter()
.map(|e| e.into_iter().map(|c| c.to_string()).collect())
.collect(),
segment_number: 0,
}
}
fn make_mig_schema(segments: Vec<&str>, groups: Vec<MigSegmentGroup>) -> MigSchema {
MigSchema {
message_type: "UTILMD".to_string(),
variant: Some("Strom".to_string()),
version: "S2.1".to_string(),
publication_date: "2025-03-20".to_string(),
author: "BDEW".to_string(),
format_version: "FV2504".to_string(),
source_file: "test".to_string(),
segments: segments.into_iter().map(make_mig_segment).collect(),
segment_groups: groups,
}
}
#[test]
fn test_assembler_top_level_segments_only() {
let mig = make_mig_schema(vec!["UNH", "BGM", "DTM", "UNT"], vec![]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001", "UTILMD:D:11A:UN:S2.1"]]),
make_owned_seg("BGM", vec![vec!["E01", "DOC001"]]),
make_owned_seg("DTM", vec![vec!["137", "20250101", "102"]]),
make_owned_seg("UNT", vec![vec!["4", "001"]]),
];
let assembler = Assembler::new(&mig);
let result = assembler.assemble_generic(&segments).unwrap();
assert_eq!(result.segments.len(), 4);
assert_eq!(result.segments[0].tag, "UNH");
assert_eq!(result.segments[1].tag, "BGM");
assert_eq!(result.segments[2].tag, "DTM");
assert_eq!(result.segments[3].tag, "UNT");
assert!(result.groups.is_empty());
}
#[test]
fn test_assembler_with_segment_group() {
let mig = make_mig_schema(
vec!["UNH", "BGM"],
vec![
make_mig_group("SG2", vec!["NAD"], vec![]),
make_mig_group("SG4", vec!["IDE", "STS"], vec![]),
],
);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("BGM", vec![vec!["E01"]]),
make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
make_owned_seg("NAD", vec![vec!["MR", "9900456"]]),
make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
];
let assembler = Assembler::new(&mig);
let result = assembler.assemble_generic(&segments).unwrap();
assert_eq!(result.segments.len(), 2);
assert_eq!(result.groups.len(), 2);
assert_eq!(result.groups[0].group_id, "SG2");
assert_eq!(result.groups[0].repetitions.len(), 2);
assert_eq!(result.groups[0].repetitions[0].segments[0].tag, "NAD");
assert_eq!(result.groups[0].repetitions[1].segments[0].tag, "NAD");
assert_eq!(result.groups[1].group_id, "SG4");
assert_eq!(result.groups[1].repetitions.len(), 1);
assert_eq!(result.groups[1].repetitions[0].segments.len(), 2);
}
#[test]
fn test_assembler_nested_groups() {
let sg3 = make_mig_group("SG3", vec!["CTA", "COM"], vec![]);
let mig = make_mig_schema(
vec!["UNH", "BGM"],
vec![make_mig_group("SG2", vec!["NAD"], vec![sg3])],
);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("BGM", vec![vec!["E01"]]),
make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
make_owned_seg("CTA", vec![vec!["IC", "Kontakt"]]),
make_owned_seg("COM", vec![vec!["040@example.com", "EM"]]),
];
let assembler = Assembler::new(&mig);
let result = assembler.assemble_generic(&segments).unwrap();
let sg2 = &result.groups[0];
assert_eq!(sg2.group_id, "SG2");
assert_eq!(sg2.repetitions.len(), 1);
let sg2_inst = &sg2.repetitions[0];
assert_eq!(sg2_inst.segments[0].tag, "NAD");
assert_eq!(sg2_inst.child_groups.len(), 1);
let sg3 = &sg2_inst.child_groups[0];
assert_eq!(sg3.group_id, "SG3");
assert_eq!(sg3.repetitions[0].segments.len(), 2);
assert_eq!(sg3.repetitions[0].segments[0].tag, "CTA");
assert_eq!(sg3.repetitions[0].segments[1].tag, "COM");
}
#[test]
fn test_assembler_optional_segments_skipped() {
let mig = make_mig_schema(vec!["UNH", "BGM", "DTM", "UNT"], vec![]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("BGM", vec![vec!["E01"]]),
make_owned_seg("UNT", vec![vec!["2", "001"]]),
];
let assembler = Assembler::new(&mig);
let result = assembler.assemble_generic(&segments).unwrap();
assert_eq!(result.segments.len(), 3);
assert_eq!(result.segments[0].tag, "UNH");
assert_eq!(result.segments[1].tag, "BGM");
assert_eq!(result.segments[2].tag, "UNT");
}
#[test]
fn test_assembler_empty_segments() {
let mig = make_mig_schema(vec!["UNH"], vec![]);
let assembler = Assembler::new(&mig);
let result = assembler.assemble_generic(&[]).unwrap();
assert!(result.segments.is_empty());
assert!(result.groups.is_empty());
}
#[test]
fn test_assembler_preserves_element_data() {
let mig = make_mig_schema(vec!["DTM"], vec![]);
let segments = vec![make_owned_seg(
"DTM",
vec![vec!["137", "202501010000+01", "303"]],
)];
let assembler = Assembler::new(&mig);
let result = assembler.assemble_generic(&segments).unwrap();
let dtm = &result.segments[0];
assert_eq!(dtm.elements[0][0], "137");
assert_eq!(dtm.elements[0][1], "202501010000+01");
assert_eq!(dtm.elements[0][2], "303");
}
#[test]
fn test_group_instance_as_assembled_tree() {
let sg5 = AssembledGroup {
group_id: "SG5".to_string(),
repetitions: vec![AssembledGroupInstance {
segments: vec![AssembledSegment {
tag: "LOC".to_string(),
elements: vec![vec!["Z16".to_string(), "DE000111222333".to_string()]],
}],
child_groups: vec![],
skipped_segments: vec![],
}],
};
let sg4_instance = AssembledGroupInstance {
segments: vec![
AssembledSegment {
tag: "IDE".to_string(),
elements: vec![vec!["24".to_string(), "TX001".to_string()]],
},
AssembledSegment {
tag: "STS".to_string(),
elements: vec![vec!["7".to_string()]],
},
],
child_groups: vec![sg5],
skipped_segments: vec![],
};
let sub_tree = sg4_instance.as_assembled_tree();
assert_eq!(sub_tree.segments.len(), 2);
assert_eq!(sub_tree.segments[0].tag, "IDE");
assert_eq!(sub_tree.segments[1].tag, "STS");
assert_eq!(sub_tree.groups.len(), 1);
assert_eq!(sub_tree.groups[0].group_id, "SG5");
assert_eq!(sub_tree.post_group_start, 2);
}
#[test]
fn test_assembler_from_parsed_edifact() {
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'";
let segments = crate::tokenize::parse_to_segments(input).unwrap();
let mig = make_mig_schema(vec!["UNB", "UNH", "BGM", "DTM", "UNT", "UNZ"], vec![]);
let assembler = Assembler::new(&mig);
let result = assembler.assemble_generic(&segments).unwrap();
assert!(result.segments.iter().any(|s| s.tag == "UNH"));
assert!(result.segments.iter().any(|s| s.tag == "BGM"));
assert!(result.segments.iter().any(|s| s.tag == "DTM"));
}
#[test]
fn test_assemble_with_diagnostics_clean_input() {
let mig = make_mig_schema(vec!["UNH", "BGM", "UNT"], vec![]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("BGM", vec![vec!["E01"]]),
make_owned_seg("UNT", vec![vec!["2", "001"]]),
];
let assembler = Assembler::new(&mig);
let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
assert_eq!(tree.segments.len(), 3);
assert!(
diagnostics.is_empty(),
"Clean input should have no diagnostics"
);
}
#[test]
fn test_assemble_with_diagnostics_unconsumed_segments() {
let mig = make_mig_schema(vec!["UNH", "BGM"], vec![]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("BGM", vec![vec!["E01"]]),
make_owned_seg("FTX", vec![vec!["AAA", "extra text"]]),
];
let assembler = Assembler::new(&mig);
let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
assert_eq!(tree.segments.len(), 2);
assert_eq!(diagnostics.len(), 1);
assert_eq!(
diagnostics[0].kind,
StructureDiagnosticKind::UnexpectedSegment
);
assert_eq!(diagnostics[0].segment_id, "FTX");
assert_eq!(diagnostics[0].position, 2);
}
#[test]
fn test_assemble_with_diagnostics_multiple_unconsumed() {
let mig = make_mig_schema(vec!["UNH"], vec![]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("FOO", vec![]),
make_owned_seg("BAR", vec![]),
make_owned_seg("BAZ", vec![]),
];
let assembler = Assembler::new(&mig);
let (tree, diagnostics) = assembler.assemble_with_diagnostics(&segments);
assert_eq!(tree.segments.len(), 1);
assert_eq!(diagnostics.len(), 3);
assert_eq!(diagnostics[0].segment_id, "FOO");
assert_eq!(diagnostics[1].segment_id, "BAR");
assert_eq!(diagnostics[2].segment_id, "BAZ");
}
#[test]
fn test_skip_unknown_segment_between_slots() {
let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
let mig = make_mig_schema(vec!["UNH"], vec![sg8.clone()]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("SEQ", vec![vec!["Z98"]]),
make_owned_seg("RFF", vec![vec!["Z38", "CROSSREF"]]),
make_owned_seg("CCI", vec![vec!["Z30"]]),
];
let off = Assembler::new(&mig);
let tree_off = off.assemble_generic(&segments).unwrap();
let sg8_off = &tree_off.groups[0];
assert_eq!(sg8_off.repetitions[0].segments.len(), 1); assert_eq!(sg8_off.repetitions[0].segments[0].tag, "SEQ");
let on = Assembler::with_config(
&mig,
AssemblerConfig {
skip_unknown_segments: true,
},
);
let tree_on = on.assemble_generic(&segments).unwrap();
let sg8_on = &tree_on.groups[0];
assert_eq!(sg8_on.repetitions[0].segments.len(), 2); assert_eq!(sg8_on.repetitions[0].segments[0].tag, "SEQ");
assert_eq!(sg8_on.repetitions[0].segments[1].tag, "CCI");
}
#[test]
fn test_skip_preserves_on_instance() {
let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
let mig = make_mig_schema(vec!["UNH"], vec![sg8]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("SEQ", vec![vec!["Z98"]]),
make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
make_owned_seg("DTM", vec![vec!["92", "20250101"]]),
make_owned_seg("CCI", vec![vec!["Z30"]]),
];
let assembler = Assembler::with_config(
&mig,
AssemblerConfig {
skip_unknown_segments: true,
},
);
let tree = assembler.assemble_generic(&segments).unwrap();
let instance = &tree.groups[0].repetitions[0];
assert_eq!(instance.segments.len(), 2); assert_eq!(instance.skipped_segments.len(), 2); assert_eq!(instance.skipped_segments[0].tag, "RFF");
assert_eq!(instance.skipped_segments[1].tag, "DTM");
}
#[test]
fn test_skip_mode_off_default() {
let mig = make_mig_schema(vec![], vec![]);
let assembler = Assembler::new(&mig);
assert!(!assembler.config.skip_unknown_segments);
}
#[test]
fn test_skip_does_not_consume_nested_group_entry() {
let sg5 = make_mig_group("SG5", vec!["LOC"], vec![]);
let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![sg5]);
let mig = make_mig_schema(vec!["UNH"], vec![sg4]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("IDE", vec![vec!["24"]]),
make_owned_seg("FOO", vec![vec!["unknown"]]),
make_owned_seg("STS", vec![vec!["7"]]),
make_owned_seg("LOC", vec![vec!["Z16"]]),
];
let assembler = Assembler::with_config(
&mig,
AssemblerConfig {
skip_unknown_segments: true,
},
);
let tree = assembler.assemble_generic(&segments).unwrap();
let sg4 = &tree.groups[0];
let inst = &sg4.repetitions[0];
assert_eq!(inst.segments.len(), 2);
assert_eq!(inst.segments[0].tag, "IDE");
assert_eq!(inst.segments[1].tag, "STS");
assert_eq!(inst.skipped_segments.len(), 1);
assert_eq!(inst.skipped_segments[0].tag, "FOO");
assert_eq!(inst.child_groups.len(), 1);
assert_eq!(inst.child_groups[0].group_id, "SG5");
assert_eq!(inst.child_groups[0].repetitions[0].segments[0].tag, "LOC");
}
#[test]
fn test_roundtrip_with_skip() {
use crate::disassembler::Disassembler;
use crate::renderer::render_edifact;
let sg8 = make_mig_group("SG8", vec!["SEQ", "CCI"], vec![]);
let mig = make_mig_schema(vec!["UNH", "UNT"], vec![sg8]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("SEQ", vec![vec!["Z98"]]),
make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
make_owned_seg("CCI", vec![vec!["Z30"]]),
make_owned_seg("UNT", vec![vec!["4", "001"]]),
];
let assembler = Assembler::with_config(
&mig,
AssemblerConfig {
skip_unknown_segments: true,
},
);
let tree = assembler.assemble_generic(&segments).unwrap();
let disassembler = Disassembler::new(&mig);
let dis = disassembler.disassemble(&tree);
let delimiters = edifact_primitives::EdifactDelimiters::default();
let rendered = render_edifact(&dis, &delimiters);
assert_eq!(dis.len(), 5);
assert_eq!(dis[0].tag, "UNH");
assert_eq!(dis[1].tag, "SEQ");
assert_eq!(dis[2].tag, "CCI");
assert_eq!(dis[3].tag, "RFF"); assert_eq!(dis[4].tag, "UNT");
assert!(rendered.contains("UNH+001"));
assert!(rendered.contains("SEQ+Z98"));
assert!(rendered.contains("RFF+Z38:REF1"));
assert!(rendered.contains("CCI+Z30"));
assert!(rendered.contains("UNT+4:001"));
}
#[test]
fn test_variant_groups_interleaved_reps() {
let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("SEQ", vec![vec!["ZD7"]]),
make_owned_seg("CCI", vec![vec!["Z30"]]),
make_owned_seg("SEQ", vec![vec!["Z98"]]),
make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
make_owned_seg("SEQ", vec![vec!["ZD7"]]),
make_owned_seg("CCI", vec![vec!["Z31"]]),
make_owned_seg("SEQ", vec![vec!["Z98"]]),
make_owned_seg("RFF", vec![vec!["Z38", "REF2"]]),
];
let assembler = Assembler::new(&mig);
let result = assembler.assemble_generic(&segments).unwrap();
assert_eq!(result.segments.len(), 1); assert_eq!(result.groups.len(), 1); let sg8 = &result.groups[0];
assert_eq!(sg8.group_id, "SG8");
assert_eq!(sg8.repetitions.len(), 4);
assert_eq!(sg8.repetitions[0].segments[0].elements[0][0], "ZD7");
assert_eq!(sg8.repetitions[0].segments[1].tag, "CCI");
assert_eq!(sg8.repetitions[1].segments[0].elements[0][0], "Z98");
assert_eq!(sg8.repetitions[1].segments[1].tag, "RFF");
assert_eq!(sg8.repetitions[2].segments[0].elements[0][0], "ZD7");
assert_eq!(sg8.repetitions[3].segments[0].elements[0][0], "Z98");
}
#[test]
fn test_variant_groups_single_variant_type() {
let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("SEQ", vec![vec!["Z98"]]),
make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
make_owned_seg("SEQ", vec![vec!["Z98"]]),
make_owned_seg("RFF", vec![vec!["Z38", "REF2"]]),
];
let assembler = Assembler::new(&mig);
let result = assembler.assemble_generic(&segments).unwrap();
assert_eq!(result.groups.len(), 1);
assert_eq!(result.groups[0].repetitions.len(), 2);
assert_eq!(
result.groups[0].repetitions[0].segments[0].elements[0][0],
"Z98"
);
assert_eq!(
result.groups[0].repetitions[1].segments[0].elements[0][0],
"Z98"
);
}
#[test]
fn test_non_variant_groups_unchanged() {
let sg2 = make_mig_group("SG2", vec!["NAD"], vec![]);
let sg4 = make_mig_group("SG4", vec!["IDE", "STS"], vec![]);
let mig = make_mig_schema(vec!["UNH", "BGM"], vec![sg2, sg4]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("BGM", vec![vec!["E01"]]),
make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
make_owned_seg("NAD", vec![vec!["MR", "9900456"]]),
make_owned_seg("IDE", vec![vec!["24", "TX001"]]),
make_owned_seg("STS", vec![vec!["7"], vec!["Z33"]]),
];
let assembler = Assembler::new(&mig);
let result = assembler.assemble_generic(&segments).unwrap();
assert_eq!(result.segments.len(), 2);
assert_eq!(result.groups.len(), 2);
assert_eq!(result.groups[0].group_id, "SG2");
assert_eq!(result.groups[0].repetitions.len(), 2);
assert_eq!(result.groups[1].group_id, "SG4");
assert_eq!(result.groups[1].repetitions.len(), 1);
}
#[test]
fn test_variant_groups_with_nested_children() {
let sg10 = make_mig_group("SG10", vec!["CCI", "CAV"], vec![]);
let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10.clone()], "ZD7");
let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ"], vec![sg10], "Z98");
let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7, sg8_z98]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("SEQ", vec![vec!["ZD7"]]),
make_owned_seg("CCI", vec![vec!["Z30"]]),
make_owned_seg("CAV", vec![vec!["Z91", "Y"]]),
make_owned_seg("SEQ", vec![vec!["Z98"]]),
make_owned_seg("CCI", vec![vec!["Z31"]]),
make_owned_seg("CAV", vec![vec!["Z91", "N"]]),
];
let assembler = Assembler::new(&mig);
let result = assembler.assemble_generic(&segments).unwrap();
assert_eq!(result.groups.len(), 1);
let sg8 = &result.groups[0];
assert_eq!(sg8.repetitions.len(), 2);
assert_eq!(sg8.repetitions[0].child_groups.len(), 1);
assert_eq!(sg8.repetitions[0].child_groups[0].group_id, "SG10");
assert_eq!(
sg8.repetitions[0].child_groups[0].repetitions[0].segments[0].elements[0][0],
"Z30"
);
assert_eq!(sg8.repetitions[1].child_groups.len(), 1);
assert_eq!(
sg8.repetitions[1].child_groups[0].repetitions[0].segments[0].elements[0][0],
"Z31"
);
}
#[test]
fn test_variant_qualifier_check_prevents_wrong_variant_consumption() {
let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
let mig = make_mig_schema(vec!["UNH"], vec![sg8_zd7]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("SEQ", vec![vec!["Z98"]]), make_owned_seg("CCI", vec![vec!["Z30"]]),
];
let assembler = Assembler::new(&mig);
let result = assembler.assemble_generic(&segments).unwrap();
assert!(result.groups.is_empty());
}
#[test]
fn test_mixed_variant_and_non_variant_groups() {
let sg2 = make_mig_group("SG2", vec!["NAD"], vec![]);
let sg8_zd7 = make_mig_group_with_variant("SG8", vec!["SEQ", "CCI"], vec![], "ZD7");
let sg8_z98 = make_mig_group_with_variant("SG8", vec!["SEQ", "RFF"], vec![], "Z98");
let sg12 = make_mig_group("SG12", vec!["NAD"], vec![]);
let mig = make_mig_schema(vec!["UNH"], vec![sg2, sg8_zd7, sg8_z98, sg12]);
let segments = vec![
make_owned_seg("UNH", vec![vec!["001"]]),
make_owned_seg("NAD", vec![vec!["MS", "9900123"]]),
make_owned_seg("SEQ", vec![vec!["ZD7"]]),
make_owned_seg("CCI", vec![vec!["Z30"]]),
make_owned_seg("SEQ", vec![vec!["Z98"]]),
make_owned_seg("RFF", vec![vec!["Z38", "REF1"]]),
make_owned_seg("NAD", vec![vec!["Z65", "ID001"]]),
];
let assembler = Assembler::new(&mig);
let result = assembler.assemble_generic(&segments).unwrap();
assert_eq!(result.groups.len(), 3); assert_eq!(result.groups[0].group_id, "SG2");
assert_eq!(result.groups[0].repetitions.len(), 1);
assert_eq!(result.groups[1].group_id, "SG8");
assert_eq!(result.groups[1].repetitions.len(), 2);
assert_eq!(result.groups[2].group_id, "SG12");
assert_eq!(result.groups[2].repetitions.len(), 1);
}
}