use crate::{error::EdifactError, model::Segment};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InterchangeEnvelope {
pub syntax_identifier: String,
pub sender_id: String,
pub recipient_id: String,
pub datetime: String,
pub control_ref: String,
pub declared_message_count: u32,
pub actual_message_count: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MessageEnvelope {
pub message_ref: String,
pub message_type: String,
pub version: String,
pub release: String,
pub controlling_agency: String,
pub association_code: String,
pub declared_segment_count: u32,
pub actual_segment_count: u32,
}
pub fn validate_envelope(
segments: &[Segment<'_>],
) -> Result<(InterchangeEnvelope, Vec<MessageEnvelope>), EdifactError> {
let mut interchange_env = extract_interchange(segments)?;
let message_envs = extract_messages(segments)?;
interchange_env.actual_message_count = message_envs.len() as u32;
if interchange_env.declared_message_count != interchange_env.actual_message_count {
return Err(EdifactError::MessageCountMismatch {
expected: interchange_env.declared_message_count,
actual: interchange_env.actual_message_count,
});
}
for msg in &message_envs {
if msg.declared_segment_count != msg.actual_segment_count {
return Err(EdifactError::SegmentCountMismatch {
expected: msg.declared_segment_count,
actual: msg.actual_segment_count,
message_ref: msg.message_ref.clone(),
});
}
}
Ok((interchange_env, message_envs))
}
fn extract_interchange(segments: &[Segment<'_>]) -> Result<InterchangeEnvelope, EdifactError> {
if segments.first().map(|segment| segment.tag) != Some("UNB") {
return Err(EdifactError::MissingSegment {
tag: "UNB".to_owned(),
expected_position: "first segment of interchange".to_owned(),
});
}
if segments.last().map(|segment| segment.tag) != Some("UNZ") {
return Err(EdifactError::MissingSegment {
tag: "UNZ".to_owned(),
expected_position: "last segment of interchange".to_owned(),
});
}
let unb = &segments[0];
let unz = &segments[segments.len() - 1];
let syntax_identifier = required_component(unb, "UNB", 0, 0)?.to_owned();
let sender_id = required_component(unb, "UNB", 1, 0)?.to_owned();
let recipient_id = required_component(unb, "UNB", 2, 0)?.to_owned();
let date = required_component(unb, "UNB", 3, 0)?;
let time = unb
.get_element(3)
.and_then(|e| e.get_component(1))
.unwrap_or("");
let datetime = if time.is_empty() {
date.to_owned()
} else {
format!("{date}:{time}")
};
let control_ref = required_component(unb, "UNB", 4, 0)?.to_owned();
let unz_control_ref = required_component(unz, "UNZ", 1, 0)?;
if unz_control_ref != control_ref {
return Err(EdifactError::QualifierMismatch {
tag: "UNZ".to_owned(),
actual: unz_control_ref.to_owned(),
expected: control_ref,
offset: unz.span.start,
});
}
let declared_message_count: u32 = required_component(unz, "UNZ", 0, 0)?
.parse()
.map_err(|_| EdifactError::InvalidText {
offset: unz.span.start,
})?;
Ok(InterchangeEnvelope {
syntax_identifier,
sender_id,
recipient_id,
datetime,
control_ref,
declared_message_count,
actual_message_count: 0,
})
}
#[inline]
fn required_component<'a>(
segment: &'a Segment<'_>,
_tag: &'static str,
element_index: usize,
component_index: usize,
) -> Result<&'a str, EdifactError> {
crate::de::required_component(segment, element_index, component_index)
}
fn extract_messages(segments: &[Segment<'_>]) -> Result<Vec<MessageEnvelope>, EdifactError> {
let mut messages: Vec<MessageEnvelope> = Vec::new();
let mut in_message = false;
let mut msg_start_idx: usize = 0;
let mut current_unh: Option<&Segment<'_>> = None;
for (i, seg) in segments[1..segments.len() - 1].iter().enumerate() {
match seg.tag {
"UNH" => {
if in_message {
return Err(EdifactError::UnexpectedEof {
offset: seg.span.start,
});
}
in_message = true;
msg_start_idx = i;
current_unh = Some(seg);
}
"UNT" if in_message => {
let unh = current_unh
.take()
.ok_or(EdifactError::UnexpectedEof {
offset: seg.span.start,
})?;
let message_ref = required_component(unh, "UNH", 0, 0)?.to_owned();
let message_type = required_component(unh, "UNH", 1, 0)?.to_owned();
let version = required_component(unh, "UNH", 1, 1)?.to_owned();
let release = required_component(unh, "UNH", 1, 2)?.to_owned();
let controlling_agency = required_component(unh, "UNH", 1, 3)?.to_owned();
let association_code = unh
.get_element(1)
.and_then(|e| e.get_component(4))
.unwrap_or("")
.to_owned();
let declared_segment_count: u32 = required_component(seg, "UNT", 0, 0)?
.parse()
.map_err(|_| EdifactError::InvalidText {
offset: seg.span.start,
})?;
let unt_ref = required_component(seg, "UNT", 1, 0)?;
if unt_ref != message_ref {
return Err(EdifactError::QualifierMismatch {
tag: "UNT".to_owned(),
actual: unt_ref.to_owned(),
expected: message_ref.clone(),
offset: seg.span.start,
});
}
let actual_segment_count = (i - msg_start_idx + 1) as u32;
in_message = false;
messages.push(MessageEnvelope {
message_ref,
message_type,
version,
release,
controlling_agency,
association_code,
declared_segment_count,
actual_segment_count,
});
}
"UNT" => {
return Err(EdifactError::InvalidSegmentForMessage {
tag: "UNT".to_owned(),
message_type: "ENVELOPE".to_owned(),
offset: seg.span.start,
});
}
"UNB" | "UNZ" if in_message => {
return Err(EdifactError::InvalidSegmentForMessage {
tag: seg.tag.to_owned(),
message_type: "ENVELOPE".to_owned(),
offset: seg.span.start,
});
}
_ if !in_message => {
return Err(EdifactError::InvalidSegmentForMessage {
tag: seg.tag.to_owned(),
message_type: "ENVELOPE".to_owned(),
offset: seg.span.start,
});
}
_ => {}
}
}
if in_message {
return Err(EdifactError::MissingSegment {
tag: "UNT".to_owned(),
expected_position: "end of message group".to_owned(),
});
}
Ok(messages)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::from_bytes;
fn parse(input: &[u8]) -> Vec<Segment<'static>> {
let leaked: &'static [u8] = Box::leak(input.to_vec().into_boxed_slice());
from_bytes(leaked)
.collect::<Result<Vec<_>, _>>()
.expect("parse failed")
}
const VALID_INTERCHANGE: &[u8] =
b"UNA:+.? 'UNB+UNOA:3+SENDER::293+RECEIVER::293+230401:0900+00001'UNH+00001+ORDERS:D:11A:UN:EAN010'BGM+220+PO-4711+9'DTM+137:20230401:102'UNT+4+00001'UNZ+1+00001'";
#[test]
fn valid_envelope_parses_ok() {
let segs = parse(VALID_INTERCHANGE);
let (interchange, messages) = validate_envelope(&segs).expect("envelope should be valid");
assert_eq!(interchange.sender_id, "SENDER");
assert_eq!(interchange.recipient_id, "RECEIVER");
assert_eq!(interchange.control_ref, "00001");
assert_eq!(interchange.declared_message_count, 1);
assert_eq!(interchange.actual_message_count, 1);
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].message_type, "ORDERS");
assert_eq!(messages[0].association_code, "EAN010");
assert_eq!(messages[0].declared_segment_count, 4);
assert_eq!(messages[0].actual_segment_count, 4); }
#[test]
fn unt_count_mismatch_returns_err() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'DTM+137:20200101:102'UNT+99+1'UNZ+1+1'";
let segs = parse(input);
let result = validate_envelope(&segs);
assert!(
matches!(
result,
Err(EdifactError::SegmentCountMismatch { expected: 99, .. })
),
"expected SegmentCountMismatch, got {result:?}"
);
}
#[test]
fn unz_count_mismatch_returns_err() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+2+1'";
let segs = parse(input);
let result = validate_envelope(&segs);
assert!(
matches!(
result,
Err(EdifactError::MessageCountMismatch {
expected: 2,
actual: 1
})
),
"expected MessageCountMismatch(2,1), got {result:?}"
);
}
#[test]
fn missing_unb_returns_err() {
let input = b"UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
let segs = parse(input);
let result = validate_envelope(&segs);
assert!(result.is_err());
}
#[test]
fn extracts_una_interchange_correctly() {
let segs = parse(VALID_INTERCHANGE);
let (env, _) = validate_envelope(&segs).unwrap();
assert_eq!(env.syntax_identifier, "UNOA");
assert_eq!(env.datetime, "230401:0900");
}
#[test]
fn dangling_unh_without_unt_returns_err() {
let input =
b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNZ+1+1'";
let segs = parse(input);
let result = validate_envelope(&segs);
assert!(matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNT"));
}
#[test]
fn stray_segment_outside_message_returns_err() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'BGM+999+PO-2+9'UNZ+1+1'";
let segs = parse(input);
let result = validate_envelope(&segs);
assert!(matches!(result, Err(EdifactError::InvalidSegmentForMessage { .. })));
}
#[test]
fn missing_unb_sender_component_returns_err() {
let input = b"UNB+UNOA:3++R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
let segs = parse(input);
let result = validate_envelope(&segs);
assert!(matches!(result, Err(EdifactError::MissingRequiredElement { tag, .. }) if tag == "UNB"));
}
#[test]
fn nested_unh_without_closing_previous_message_returns_err() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNH+2+ORDERS:D:11A:UN:EAN010'UNT+3+2'UNZ+1+1'";
let segs = parse(input);
let result = validate_envelope(&segs);
assert!(matches!(result, Err(EdifactError::UnexpectedEof { .. })));
}
#[test]
fn unt_message_reference_must_match_unh() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+999'UNZ+1+1'";
let segs = parse(input);
let result = validate_envelope(&segs);
assert!(matches!(result, Err(EdifactError::QualifierMismatch { tag, .. }) if tag == "UNT"));
}
#[test]
fn unz_control_reference_must_match_unb() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+999'";
let segs = parse(input);
let result = validate_envelope(&segs);
assert!(matches!(result, Err(EdifactError::QualifierMismatch { tag, .. }) if tag == "UNZ"));
}
#[test]
fn missing_unh_message_type_components_return_err() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
let segs = parse(input);
let result = validate_envelope(&segs);
assert!(matches!(result, Err(EdifactError::MissingRequiredElement { tag, .. }) if tag == "UNH"));
}
#[test]
fn nested_unz_inside_message_returns_err() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'UNZ+1+1'UNT+2+1'UNZ+1+1'";
let segs = parse(input);
let result = validate_envelope(&segs);
assert!(matches!(result, Err(EdifactError::InvalidSegmentForMessage { tag, .. }) if tag == "UNZ"));
}
}