use crate::{OwnedSegment, error::EdifactError, model::Segment};
pub(crate) trait SegmentReader: sealed::Sealed {
fn tag(&self) -> &str;
fn span_start(&self) -> usize;
fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str>;
fn required_component_field(
&self,
elem_idx: usize,
comp_idx: usize,
) -> Result<&str, EdifactError> {
self.component(elem_idx, comp_idx)
.filter(|s| !s.is_empty())
.ok_or_else(|| EdifactError::MissingRequiredComponent {
tag: self.tag().to_owned(),
element_index: elem_idx,
component_index: comp_idx,
})
}
}
mod sealed {
pub trait Sealed {}
impl Sealed for crate::model::Segment<'_> {}
impl Sealed for crate::OwnedSegment {}
}
impl SegmentReader for Segment<'_> {
#[inline]
fn tag(&self) -> &str {
self.tag
}
#[inline]
fn span_start(&self) -> usize {
self.span.start
}
#[inline]
fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str> {
self.get_element(elem_idx)?.get_component(comp_idx)
}
}
impl SegmentReader for OwnedSegment {
#[inline]
fn tag(&self) -> &str {
&self.tag
}
#[inline]
fn span_start(&self) -> usize {
self.span.start
}
#[inline]
fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str> {
self.component_str(elem_idx, comp_idx)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct InterchangeEnvelope {
pub syntax_identifier: String,
pub syntax_version: String,
pub sender_id: String,
pub sender_qualifier: String,
pub sender_routing_address: Option<String>,
pub recipient_id: String,
pub recipient_qualifier: String,
pub recipient_routing_address: Option<String>,
pub date: String,
pub time: Option<String>,
pub control_ref: String,
pub recipient_password: Option<String>,
pub recipient_password_qualifier: Option<String>,
pub app_ref: Option<String>,
pub processing_priority: Option<String>,
pub acknowledgement_request: bool,
pub communications_agreement_id: Option<String>,
pub test_indicator: bool,
pub declared_unit_count: u32,
pub actual_unit_count: u32,
}
impl InterchangeEnvelope {
#[inline]
#[must_use]
pub fn is_test(&self) -> bool {
self.test_indicator
}
#[inline]
#[must_use]
pub fn ack_requested(&self) -> bool {
self.acknowledgement_request
}
}
impl std::fmt::Display for InterchangeEnvelope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{sender} -> {recipient} [{ctrl}] ({syntax}:{ver})",
sender = self.sender_id,
recipient = self.recipient_id,
ctrl = self.control_ref,
syntax = self.syntax_identifier,
ver = self.syntax_version,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
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 common_access_ref: Option<String>,
pub sequence_of_transfers: Option<u32>,
pub transfer_position: Option<String>,
pub declared_segment_count: u32,
pub actual_segment_count: u32,
}
impl std::fmt::Display for MessageEnvelope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{msg_type}:{ver}:{rel} ref={msg_ref} seg={actual}/{declared}",
msg_type = self.message_type,
ver = self.version,
rel = self.release,
msg_ref = self.message_ref,
actual = self.actual_segment_count,
declared = self.declared_segment_count,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FunctionalGroupEnvelope {
pub group_id: String,
pub app_sender: String,
pub app_sender_qualifier: String,
pub app_recipient: String,
pub app_recipient_qualifier: String,
pub date: String,
pub time: Option<String>,
pub group_ref: String,
pub controlling_agency: String,
pub version: String,
pub release: String,
pub declared_message_count: u32,
pub actual_message_count: u32,
pub messages: Vec<MessageEnvelope>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ValidatedInterchange {
pub interchange: InterchangeEnvelope,
pub functional_groups: Vec<FunctionalGroupEnvelope>,
pub messages: Vec<MessageEnvelope>,
}
impl ValidatedInterchange {
#[inline]
#[must_use]
pub fn has_functional_groups(&self) -> bool {
!self.functional_groups.is_empty()
}
#[inline]
#[must_use]
pub fn message_count(&self) -> usize {
self.messages.len()
}
#[inline]
#[must_use]
pub fn iter_messages(&self) -> impl Iterator<Item = &MessageEnvelope> {
self.messages.iter()
}
#[inline]
#[must_use]
pub fn find_message(&self, reference: &str) -> Option<&MessageEnvelope> {
self.messages.iter().find(|m| m.message_ref == reference)
}
#[must_use]
pub fn messages_by_type(&self, message_type: &str) -> Vec<&MessageEnvelope> {
self.messages
.iter()
.filter(|m| m.message_type == message_type)
.collect()
}
#[inline]
#[must_use]
pub fn iter_messages_by_type<'s, 'q: 's>(
&'s self,
message_type: &'q str,
) -> impl Iterator<Item = &'s MessageEnvelope> + 's {
self.messages
.iter()
.filter(move |m| m.message_type == message_type)
}
}
impl std::fmt::Display for ValidatedInterchange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{ic} messages={n}",
ic = self.interchange,
n = self.messages.len(),
)
}
}
impl std::fmt::Display for FunctionalGroupEnvelope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{gid} sender={sender} recipient={recip} [{gref}] ({agency}) msgs={actual}/{declared}",
gid = self.group_id,
sender = self.app_sender,
recip = self.app_recipient,
gref = self.group_ref,
agency = self.controlling_agency,
actual = self.actual_message_count,
declared = self.declared_message_count,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MessageIdentifier<'a> {
pub message_ref: &'a str,
pub message_type: &'a str,
pub version: &'a str,
pub release: &'a str,
pub controlling_agency: &'a str,
pub association_code: &'a str,
}
pub fn parse_unh<'a>(unh: &'a Segment<'a>) -> Result<MessageIdentifier<'a>, EdifactError> {
let message_ref = unh
.get_element(0)
.and_then(|e| e.get_component(0))
.filter(|s| !s.is_empty())
.ok_or_else(|| EdifactError::MissingRequiredComponent {
tag: "UNH".to_owned(),
element_index: 0,
component_index: 0,
})?;
let elem = unh
.get_element(1)
.ok_or_else(|| EdifactError::MissingRequiredElement {
tag: "UNH".to_owned(),
element_index: 1,
})?;
let message_type =
elem.get_component(0)
.ok_or_else(|| EdifactError::MissingRequiredComponent {
tag: "UNH".to_owned(),
element_index: 1,
component_index: 0,
})?;
Ok(MessageIdentifier {
message_ref,
message_type,
version: elem.get_component(1).unwrap_or(""),
release: elem.get_component(2).unwrap_or(""),
controlling_agency: elem.get_component(3).unwrap_or(""),
association_code: elem.get_component(4).unwrap_or(""),
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GroupIdentifier<'a> {
pub group_id: &'a str,
pub app_sender: &'a str,
pub app_sender_qualifier: &'a str,
pub app_recipient: &'a str,
pub app_recipient_qualifier: &'a str,
pub group_ref: &'a str,
pub controlling_agency: &'a str,
pub version: &'a str,
pub release: &'a str,
}
pub fn parse_ung<'a>(ung: &'a Segment<'a>) -> Result<GroupIdentifier<'a>, EdifactError> {
let group_id = ung
.get_element(0)
.and_then(|e| e.get_component(0))
.unwrap_or("");
let app_sender = ung
.get_element(1)
.and_then(|e| e.get_component(0))
.unwrap_or("");
let app_sender_qualifier = ung
.get_element(1)
.and_then(|e| e.get_component(1))
.unwrap_or("");
let app_recipient = ung
.get_element(2)
.and_then(|e| e.get_component(0))
.unwrap_or("");
let app_recipient_qualifier = ung
.get_element(2)
.and_then(|e| e.get_component(1))
.unwrap_or("");
let group_ref = ung
.get_element(4)
.and_then(|e| e.get_component(0))
.ok_or_else(|| EdifactError::MissingRequiredComponent {
tag: "UNG".to_owned(),
element_index: 4,
component_index: 0,
})?;
let controlling_agency = ung
.get_element(5)
.and_then(|e| e.get_component(0))
.unwrap_or("");
let s008 = ung.get_element(6);
let version = s008.as_ref().and_then(|e| e.get_component(0)).unwrap_or("");
let release = s008.as_ref().and_then(|e| e.get_component(1)).unwrap_or("");
Ok(GroupIdentifier {
group_id,
app_sender,
app_sender_qualifier,
app_recipient,
app_recipient_qualifier,
group_ref,
controlling_agency,
version,
release,
})
}
pub fn validate_envelope(segments: &[Segment<'_>]) -> Result<ValidatedInterchange, EdifactError> {
validate_envelope_impl(segments)
}
pub fn validate_envelope_from_owned(
segments: &[OwnedSegment],
) -> Result<ValidatedInterchange, EdifactError> {
validate_envelope_impl(segments)
}
#[derive(Debug)]
#[non_exhaustive]
pub struct LenientResult {
pub interchange: Option<ValidatedInterchange>,
pub errors: Vec<EdifactError>,
}
impl LenientResult {
#[inline]
#[must_use]
pub fn is_valid(&self) -> bool {
self.errors.is_empty()
}
#[inline]
#[must_use]
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
#[must_use]
pub fn into_strict(self) -> Result<ValidatedInterchange, Vec<EdifactError>> {
if self.errors.is_empty() {
Ok(self.interchange.expect(
"LenientResult: no errors but interchange is None — this is a bug in edifact-rs",
))
} else {
Err(self.errors)
}
}
}
pub fn validate_envelope_lenient(segments: &[Segment<'_>]) -> LenientResult {
validate_envelope_lenient_impl(segments)
}
pub fn validate_envelope_lenient_from_owned(segments: &[OwnedSegment]) -> LenientResult {
validate_envelope_lenient_impl(segments)
}
fn validate_envelope_impl<S: SegmentReader>(
segments: &[S],
) -> Result<ValidatedInterchange, EdifactError> {
let mut interchange_env = extract_interchange(segments)?;
let inner = if segments.len() >= 2 {
&segments[1..segments.len() - 1]
} else {
&[]
};
let (functional_groups, messages) = extract_content(inner)?;
let actual_unit_count = if functional_groups.is_empty() {
messages.len()
} else {
functional_groups.len()
};
interchange_env.actual_unit_count =
u32::try_from(actual_unit_count).map_err(|_| EdifactError::InterchangeTooLarge {
count: actual_unit_count as u64,
})?;
if interchange_env.declared_unit_count != interchange_env.actual_unit_count {
return Err(EdifactError::MessageCountMismatch {
expected: interchange_env.declared_unit_count,
actual: interchange_env.actual_unit_count,
});
}
for msg in &messages {
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(ValidatedInterchange {
interchange: interchange_env,
functional_groups,
messages,
})
}
fn validate_envelope_lenient_impl<S: SegmentReader>(segments: &[S]) -> LenientResult {
match validate_envelope_impl(segments) {
Ok(result) => {
return LenientResult {
interchange: Some(result),
errors: Vec::new(),
};
}
Err(_) => {}
}
let mut ie = match extract_interchange(segments) {
Ok(ie) => ie,
Err(e) => {
return LenientResult {
interchange: None,
errors: vec![e],
};
}
};
let inner = if segments.len() >= 2 {
&segments[1..segments.len() - 1]
} else {
&[]
};
let (functional_groups, messages) = match extract_content(inner) {
Ok(pair) => pair,
Err(e) => {
return LenientResult {
interchange: None,
errors: vec![e],
};
}
};
let mut errors: Vec<EdifactError> = Vec::new();
let actual_unit_count = if functional_groups.is_empty() {
messages.len()
} else {
functional_groups.len()
};
ie.actual_unit_count = u32::try_from(actual_unit_count).unwrap_or(u32::MAX);
if ie.declared_unit_count != ie.actual_unit_count {
errors.push(EdifactError::MessageCountMismatch {
expected: ie.declared_unit_count,
actual: ie.actual_unit_count,
});
}
for msg in &messages {
if msg.declared_segment_count != msg.actual_segment_count {
errors.push(EdifactError::SegmentCountMismatch {
expected: msg.declared_segment_count,
actual: msg.actual_segment_count,
message_ref: msg.message_ref.clone(),
});
}
}
let result = ValidatedInterchange {
interchange: ie,
functional_groups,
messages,
};
LenientResult {
interchange: Some(result),
errors,
}
}
fn extract_interchange<S: SegmentReader>(
segments: &[S],
) -> Result<InterchangeEnvelope, EdifactError> {
if segments.first().map(|s| s.tag()) != Some("UNB") {
return Err(EdifactError::MissingSegment {
tag: "UNB".to_owned(),
expected_position: "first segment of interchange".to_owned(),
});
}
if segments.last().map(|s| s.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 = unb.required_component_field(0, 0)?.to_owned();
let syntax_version = unb.component(0, 1).unwrap_or("").to_owned();
const VALID_SYNTAX_IDS: &[&str] = &["UNOA", "UNOB", "UNOC", "UNOD", "UNOE", "UNOF", "KECA"];
if !VALID_SYNTAX_IDS.contains(&syntax_identifier.as_str()) {
return Err(EdifactError::UnrecognisedSyntaxIdentifier(
syntax_identifier,
));
}
let sender_id = unb.required_component_field(1, 0)?.to_owned();
let sender_qualifier = unb.component(1, 1).unwrap_or("").to_owned();
let sender_routing_address = unb
.component(1, 2)
.filter(|s| !s.is_empty())
.map(str::to_owned);
let recipient_id = unb.required_component_field(2, 0)?.to_owned();
let recipient_qualifier = unb.component(2, 1).unwrap_or("").to_owned();
let recipient_routing_address = unb
.component(2, 2)
.filter(|s| !s.is_empty())
.map(str::to_owned);
let date = unb.required_component_field(3, 0)?.to_owned();
let time_raw = unb.component(3, 1).unwrap_or("");
let time = if time_raw.is_empty() {
None
} else {
Some(time_raw.to_owned())
};
let control_ref = unb.required_component_field(4, 0)?.to_owned();
let recipient_password = unb
.component(5, 0)
.filter(|s| !s.is_empty())
.map(str::to_owned);
let recipient_password_qualifier = unb
.component(5, 1)
.filter(|s| !s.is_empty())
.map(str::to_owned);
let app_ref = unb
.component(6, 0)
.filter(|s| !s.is_empty())
.map(str::to_owned);
let processing_priority = unb
.component(7, 0)
.filter(|s| !s.is_empty())
.map(str::to_owned);
let acknowledgement_request = unb.component(8, 0).map_or(false, |v| v == "1");
let communications_agreement_id = unb
.component(9, 0)
.filter(|s| !s.is_empty())
.map(str::to_owned);
let test_indicator = unb.component(10, 0).map_or(false, |v| v == "1");
let unz_control_ref = unz.required_component_field(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_unit_count: u32 =
unz.required_component_field(0, 0)?
.parse()
.map_err(|_| EdifactError::InvalidText {
offset: unz.span_start(),
})?;
Ok(InterchangeEnvelope {
syntax_identifier,
syntax_version,
sender_id,
sender_qualifier,
sender_routing_address,
recipient_id,
recipient_qualifier,
recipient_routing_address,
date,
time,
control_ref,
recipient_password,
recipient_password_qualifier,
app_ref,
processing_priority,
acknowledgement_request,
communications_agreement_id,
test_indicator,
declared_unit_count,
actual_unit_count: 0,
})
}
fn extract_content<S: SegmentReader>(
inner: &[S],
) -> Result<(Vec<FunctionalGroupEnvelope>, Vec<MessageEnvelope>), EdifactError> {
if inner.first().map_or(false, |s| s.tag() == "UNG") {
let groups = extract_with_groups(inner)?;
let messages = groups
.iter()
.flat_map(|g| g.messages.iter().cloned())
.collect();
Ok((groups, messages))
} else {
let messages = extract_messages_flat(inner)?;
Ok((vec![], messages))
}
}
fn find_matching_une<S: SegmentReader>(
segments: &[S],
start: usize,
) -> Result<usize, EdifactError> {
for i in start..segments.len() {
match segments[i].tag() {
"UNE" => return Ok(i),
"UNG" => {
return Err(EdifactError::InvalidSegmentForMessage {
tag: "UNG".to_owned(),
message_type: "ENVELOPE".to_owned(),
offset: segments[i].span_start(),
});
}
_ => {}
}
}
Err(EdifactError::MissingSegment {
tag: "UNE".to_owned(),
expected_position: "end of functional group".to_owned(),
})
}
fn extract_with_groups<S: SegmentReader>(
inner: &[S],
) -> Result<Vec<FunctionalGroupEnvelope>, EdifactError> {
let mut groups: Vec<FunctionalGroupEnvelope> = Vec::new();
let mut i = 0;
while i < inner.len() {
let seg = &inner[i];
match seg.tag() {
"UNG" => {
let ung_idx = i;
let une_idx = find_matching_une(inner, ung_idx + 1)?;
let ung = &inner[ung_idx];
let group_id = ung.component(0, 0).unwrap_or("").to_owned();
let app_sender = ung.component(1, 0).unwrap_or("").to_owned();
let app_sender_qualifier = ung.component(1, 1).unwrap_or("").to_owned();
let app_recipient = ung.component(2, 0).unwrap_or("").to_owned();
let app_recipient_qualifier = ung.component(2, 1).unwrap_or("").to_owned();
let date = ung.component(3, 0).unwrap_or("").to_owned();
let time_raw = ung.component(3, 1).unwrap_or("");
let time = if time_raw.is_empty() {
None
} else {
Some(time_raw.to_owned())
};
let group_ref = ung.required_component_field(4, 0)?.to_owned();
let controlling_agency = ung.component(5, 0).unwrap_or("").to_owned();
let version = ung.component(6, 0).unwrap_or("").to_owned();
let release = ung.component(6, 1).unwrap_or("").to_owned();
let une = &inner[une_idx];
let declared_str = une.required_component_field(0, 0)?;
let declared_message_count: u32 =
declared_str
.parse()
.map_err(|_| EdifactError::InvalidText {
offset: une.span_start(),
})?;
let une_ref = une.required_component_field(1, 0)?;
if une_ref != group_ref {
return Err(EdifactError::QualifierMismatch {
tag: "UNE".to_owned(),
actual: une_ref.to_owned(),
expected: group_ref.clone(),
offset: une.span_start(),
});
}
let group_content = &inner[ung_idx + 1..une_idx];
let messages = extract_messages_flat(group_content)?;
let actual_message_count = u32::try_from(messages.len()).map_err(|_| {
EdifactError::InterchangeTooLarge {
count: messages.len() as u64,
}
})?;
if declared_message_count != actual_message_count {
return Err(EdifactError::MessageCountMismatch {
expected: declared_message_count,
actual: actual_message_count,
});
}
groups.push(FunctionalGroupEnvelope {
group_id,
app_sender,
app_sender_qualifier,
app_recipient,
app_recipient_qualifier,
date,
time,
group_ref,
controlling_agency,
version,
release,
declared_message_count,
actual_message_count,
messages,
});
i = une_idx + 1;
}
"UNE" => {
return Err(EdifactError::InvalidSegmentForMessage {
tag: "UNE".to_owned(),
message_type: "ENVELOPE".to_owned(),
offset: seg.span_start(),
});
}
"UNH" => {
return Err(EdifactError::InvalidSegmentForMessage {
tag: "UNH".to_owned(),
message_type: "ENVELOPE".to_owned(),
offset: seg.span_start(),
});
}
_ => {
return Err(EdifactError::InvalidSegmentForMessage {
tag: seg.tag().to_owned(),
message_type: "ENVELOPE".to_owned(),
offset: seg.span_start(),
});
}
}
}
Ok(groups)
}
fn extract_messages_flat<S: SegmentReader>(
segments: &[S],
) -> 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 unh_idx: Option<usize> = None;
for (i, seg) in segments.iter().enumerate() {
match seg.tag() {
"UNH" => {
if in_message {
return Err(EdifactError::InvalidSegmentForMessage {
tag: "UNH".to_owned(),
message_type: "ENVELOPE".to_owned(),
offset: seg.span_start(),
});
}
in_message = true;
msg_start_idx = i;
unh_idx = Some(i);
}
"UNT" if in_message => {
let u_idx = unh_idx.take().unwrap();
let unh = &segments[u_idx];
let message_ref = unh.required_component_field(0, 0)?.to_owned();
let message_type = unh.required_component_field(1, 0)?.to_owned();
let version = unh.required_component_field(1, 1)?.to_owned();
let release = unh.required_component_field(1, 2)?.to_owned();
let controlling_agency = unh.required_component_field(1, 3)?.to_owned();
let association_code = unh.component(1, 4).unwrap_or("").to_owned();
let common_access_ref = unh
.component(2, 0)
.filter(|s| !s.is_empty())
.map(str::to_owned);
let sequence_of_transfers = unh
.component(3, 0)
.filter(|s| !s.is_empty())
.and_then(|s| s.parse::<u32>().ok());
let transfer_position = unh
.component(3, 1)
.filter(|s| !s.is_empty())
.map(str::to_owned);
let declared_segment_count: u32 = seg
.required_component_field(0, 0)?
.parse()
.map_err(|_| EdifactError::InvalidText {
offset: seg.span_start(),
})?;
let unt_ref = seg.required_component_field(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 = u32::try_from(i - msg_start_idx + 1).map_err(|_| {
EdifactError::InterchangeTooLarge {
count: u64::try_from(i - msg_start_idx + 1).unwrap_or(u64::MAX),
}
})?;
in_message = false;
messages.push(MessageEnvelope {
message_ref,
message_type,
version,
release,
controlling_agency,
association_code,
common_access_ref,
sequence_of_transfers,
transfer_position,
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" | "UNG" | "UNE" 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::*;
fn parse(input: &[u8]) -> Vec<crate::OwnedSegment> {
crate::from_reader_collect(std::io::Cursor::new(input)).expect("parse failed")
}
fn parse_and_validate(input: &[u8]) -> Result<ValidatedInterchange, EdifactError> {
let owned = parse(input);
let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
validate_envelope(&segs)
}
fn parse_and_validate_owned(input: &[u8]) -> Result<ValidatedInterchange, EdifactError> {
validate_envelope_from_owned(&parse(input))
}
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 result = parse_and_validate(VALID_INTERCHANGE).expect("envelope should be valid");
assert_eq!(result.interchange.sender_id, "SENDER");
assert_eq!(result.interchange.sender_qualifier, ""); assert_eq!(result.interchange.recipient_id, "RECEIVER");
assert_eq!(result.interchange.recipient_qualifier, "");
assert_eq!(result.interchange.syntax_identifier, "UNOA");
assert_eq!(result.interchange.syntax_version, "3");
assert_eq!(result.interchange.control_ref, "00001");
assert_eq!(result.interchange.declared_unit_count, 1);
assert_eq!(result.interchange.actual_unit_count, 1);
assert!(!result.interchange.is_test());
assert!(!result.has_functional_groups());
assert_eq!(result.message_count(), 1);
assert_eq!(result.messages[0].message_type, "ORDERS");
assert_eq!(result.messages[0].association_code, "EAN010");
assert_eq!(result.messages[0].declared_segment_count, 4);
assert_eq!(result.messages[0].actual_segment_count, 4);
}
#[test]
fn valid_envelope_parses_ok_owned_path() {
let result = parse_and_validate_owned(VALID_INTERCHANGE).expect("envelope should be valid");
assert_eq!(result.interchange.sender_id, "SENDER");
assert_eq!(result.interchange.actual_unit_count, 1);
assert_eq!(result.messages[0].declared_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 result = parse_and_validate(input);
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 result = parse_and_validate(input);
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'";
assert!(parse_and_validate(input).is_err());
}
#[test]
fn extracts_una_interchange_correctly() {
let result = parse_and_validate(VALID_INTERCHANGE).unwrap();
assert_eq!(result.interchange.syntax_identifier, "UNOA");
assert_eq!(result.interchange.syntax_version, "3");
assert_eq!(result.interchange.date, "230401");
assert_eq!(result.interchange.time.as_deref(), Some("0900"));
}
#[test]
fn sender_and_recipient_qualifiers_extracted() {
let input = b"UNB+UNOA:3+1234567890123:14+9876543210987:14+200101:0900+1'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("GLN-qualified UNB must parse ok");
assert_eq!(r.interchange.sender_id, "1234567890123");
assert_eq!(r.interchange.sender_qualifier, "14");
assert_eq!(r.interchange.recipient_id, "9876543210987");
assert_eq!(r.interchange.recipient_qualifier, "14");
}
#[test]
fn test_indicator_parsed_from_unb() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1++++++1'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("test-flagged UNB must parse ok");
assert!(
r.interchange.test_indicator,
"test_indicator should be true"
);
assert!(r.interchange.is_test(), "is_test() convenience must agree");
}
#[test]
fn no_test_indicator_defaults_false() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
assert!(!r.interchange.test_indicator);
assert!(!r.interchange.is_test());
}
#[test]
fn app_ref_extracted_when_present() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1++MYAPP'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("UNB with app_ref must parse ok");
assert_eq!(r.interchange.app_ref.as_deref(), Some("MYAPP"));
}
#[test]
fn app_ref_is_none_when_absent() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
assert!(r.interchange.app_ref.is_none());
}
#[test]
fn recipient_password_extracted_when_present() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1+MYPASS'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("UNB with password must parse ok");
assert_eq!(r.interchange.recipient_password.as_deref(), Some("MYPASS"));
}
#[test]
fn recipient_password_is_none_when_absent() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
assert!(r.interchange.recipient_password.is_none());
}
#[test]
fn acknowledgement_request_flag_parsed() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1++++1'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("UNB with ack-request must parse ok");
assert!(r.interchange.acknowledgement_request);
assert!(r.interchange.ack_requested());
}
#[test]
fn acknowledgement_request_defaults_false() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
assert!(!r.interchange.acknowledgement_request);
assert!(!r.interchange.ack_requested());
}
#[test]
fn communications_agreement_id_extracted() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1+++++EANCOM'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("UNB with comms-agreement must parse ok");
assert_eq!(
r.interchange.communications_agreement_id.as_deref(),
Some("EANCOM")
);
}
#[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 result = parse_and_validate(input);
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'";
assert!(matches!(
parse_and_validate(input),
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 result = parse_and_validate(input);
assert!(
matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 0 }) if tag == "UNB"),
"expected MissingRequiredComponent for empty sender, got: {result:?}"
);
}
#[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 result = parse_and_validate(input);
assert!(
matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNH"),
"expected InvalidSegmentForMessage(UNH), got {result:?}"
);
}
#[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 result = parse_and_validate(input);
assert!(
matches!(result, Err(EdifactError::QualifierMismatch { ref 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 result = parse_and_validate(input);
assert!(
matches!(result, Err(EdifactError::QualifierMismatch { ref 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 result = parse_and_validate(input);
assert!(
matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 3 }) if tag == "UNH"),
"got: {result:?}"
);
}
#[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 result = parse_and_validate(input);
assert!(
matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNZ")
);
}
#[test]
fn lenient_returns_partial_result_on_count_mismatch() {
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 owned = parse(input);
let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
let lenient = validate_envelope_lenient(&segs);
let result = lenient.interchange;
let errors = lenient.errors;
assert!(
result.is_some(),
"lenient mode must return Some even on count mismatch"
);
assert_eq!(errors.len(), 1);
assert!(
matches!(
&errors[0],
EdifactError::MessageCountMismatch {
expected: 2,
actual: 1
}
),
"expected MessageCountMismatch(2,1), got {:?}",
errors[0]
);
let partial = result.unwrap();
assert_eq!(partial.messages.len(), 1);
assert_eq!(partial.interchange.actual_unit_count, 1);
assert_eq!(partial.interchange.declared_unit_count, 2);
}
#[test]
fn lenient_returns_none_on_structural_error() {
let input = b"UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
let owned = parse(input);
let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
let lenient = validate_envelope_lenient(&segs);
let result = lenient.interchange;
let errors = lenient.errors;
assert!(result.is_none(), "missing UNB must yield None");
assert!(!errors.is_empty());
}
#[test]
fn message_count_convenience_method() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
assert_eq!(r.message_count(), r.messages.len());
assert_eq!(r.message_count(), 1);
}
#[test]
fn interchange_with_single_functional_group_parses_ok() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNE+1+1'\
UNZ+1+1'";
let result = parse_and_validate(input).expect("single-group interchange must parse ok");
assert!(result.has_functional_groups());
assert_eq!(result.functional_groups.len(), 1);
let g = &result.functional_groups[0];
assert_eq!(g.group_id, "ORDERS");
assert_eq!(g.group_ref, "1");
assert_eq!(g.controlling_agency, "UN");
assert_eq!(g.declared_message_count, 1);
assert_eq!(g.actual_message_count, 1);
assert_eq!(result.messages.len(), 1);
assert_eq!(result.messages[0].message_type, "ORDERS");
assert_eq!(result.interchange.declared_unit_count, 1);
assert_eq!(result.interchange.actual_unit_count, 1);
}
#[test]
fn interchange_with_multi_message_group_parses_ok() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNH+2+ORDERS:D:96A:UN'\
BGM+220+PO-002+9'\
UNT+3+2'\
UNE+2+1'\
UNZ+1+1'";
let result = parse_and_validate(input).expect("multi-message group must parse ok");
assert!(result.has_functional_groups());
assert_eq!(result.functional_groups.len(), 1);
assert_eq!(result.functional_groups[0].actual_message_count, 2);
assert_eq!(result.messages.len(), 2);
assert_eq!(result.interchange.actual_unit_count, 1);
assert_eq!(result.interchange.declared_unit_count, 1);
}
#[test]
fn interchange_with_multiple_groups_parses_ok() {
let input = b"UNB+UNOA:3+S+R+200101:0900+2'\
UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNE+1+1'\
UNG+INVOIC+S+R+200101:0900+2+UN+D:96A'\
UNH+2+INVOIC:D:96A:UN'\
BGM+380+INV-001+9'\
UNT+3+2'\
UNE+1+2'\
UNZ+2+2'";
let result = parse_and_validate(input).expect("multi-group interchange must parse ok");
assert_eq!(result.functional_groups.len(), 2);
assert_eq!(result.functional_groups[0].group_id, "ORDERS");
assert_eq!(result.functional_groups[1].group_id, "INVOIC");
assert_eq!(result.messages.len(), 2);
assert_eq!(result.interchange.declared_unit_count, 2);
assert_eq!(result.interchange.actual_unit_count, 2);
}
#[test]
fn ung_une_count_mismatch_returns_err() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNE+2+1'\
UNZ+1+1'";
let result = parse_and_validate(input);
assert!(
matches!(
result,
Err(EdifactError::MessageCountMismatch {
expected: 2,
actual: 1
})
),
"expected MessageCountMismatch(2,1), got {result:?}"
);
}
#[test]
fn une_without_ung_returns_err() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNE+1+1'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let result = parse_and_validate(input);
assert!(
matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNE"),
"expected InvalidSegmentForMessage(UNE), got {result:?}"
);
}
#[test]
fn ung_without_une_returns_err() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let result = parse_and_validate(input);
assert!(
matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNE"),
"expected MissingSegment(UNE), got {result:?}"
);
}
#[test]
fn une_group_ref_must_match_ung() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNE+1+999'\
UNZ+1+1'";
let result = parse_and_validate(input);
assert!(
matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNE"),
"expected QualifierMismatch(UNE), got {result:?}"
);
}
#[test]
fn processing_priority_extracted_when_present() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1+++A'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("UNB with processing_priority must parse ok");
assert_eq!(r.interchange.processing_priority.as_deref(), Some("A"));
}
#[test]
fn processing_priority_is_none_when_absent() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
assert!(r.interchange.processing_priority.is_none());
}
#[test]
fn sender_routing_address_extracted_when_present() {
let input = b"UNB+UNOA:3+SENDER:14:ROUTEA+RECIP+200101:0900+1'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("UNB with sender routing must parse ok");
assert_eq!(
r.interchange.sender_routing_address.as_deref(),
Some("ROUTEA")
);
assert!(r.interchange.recipient_routing_address.is_none());
}
#[test]
fn recipient_routing_address_extracted_when_present() {
let input = b"UNB+UNOA:3+SENDER+RECIP:14:ROUTEB+200101:0900+1'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("UNB with recipient routing must parse ok");
assert!(r.interchange.sender_routing_address.is_none());
assert_eq!(
r.interchange.recipient_routing_address.as_deref(),
Some("ROUTEB")
);
}
#[test]
fn routing_address_is_none_when_absent() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let r = parse_and_validate(input).unwrap();
assert!(r.interchange.sender_routing_address.is_none());
assert!(r.interchange.recipient_routing_address.is_none());
}
#[test]
fn common_access_ref_extracted_when_present() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNH+1+ORDERS:D:96A:UN+COMREF1'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("UNH with common_access_ref must parse ok");
assert_eq!(r.messages[0].common_access_ref.as_deref(), Some("COMREF1"));
}
#[test]
fn common_access_ref_is_none_when_absent() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
assert!(r.messages[0].common_access_ref.is_none());
}
#[test]
fn parse_unh_includes_message_ref() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNH+REF42+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+REF42'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("must parse ok");
assert_eq!(r.messages[0].message_ref, "REF42");
assert_eq!(r.messages[0].message_type, "ORDERS");
}
#[test]
fn iter_messages_by_type_returns_matching_messages() {
let input = b"UNB+UNOA:3+S+R+200101:0900+CTRL2'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNH+2+INVOIC:D:96A:UN'\
BGM+380+INV-001+9'\
UNT+3+2'\
UNZ+2+CTRL2'";
let r = parse_and_validate(input).expect("two-message interchange must parse ok");
let orders: Vec<_> = r.iter_messages_by_type("ORDERS").collect();
assert_eq!(orders.len(), 1);
assert_eq!(orders[0].message_ref, "1");
let invoices: Vec<_> = r.iter_messages_by_type("INVOIC").collect();
assert_eq!(invoices.len(), 1);
assert_eq!(invoices[0].message_ref, "2");
let none: Vec<_> = r.iter_messages_by_type("DESADV").collect();
assert!(none.is_empty());
}
#[test]
fn display_interchange_envelope() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
let s = r.interchange.to_string();
assert!(s.contains("SENDER"), "Display must include sender_id");
assert!(s.contains("UNOA"), "Display must include syntax_identifier");
}
#[test]
fn display_message_envelope() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
let s = r.messages[0].to_string();
assert!(s.contains("ORDERS"), "Display must include message_type");
assert!(s.contains("ref="), "Display must include message ref label");
}
#[test]
fn display_validated_interchange() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
let s = r.to_string();
assert!(
s.contains("messages="),
"Display must include message count label"
);
}
#[test]
fn unrecognised_syntax_identifier_returns_err() {
let input = b"UNB+XXXX:3+S+R+200101:0900+1'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let result = parse_and_validate(input);
assert!(
matches!(result, Err(EdifactError::UnrecognisedSyntaxIdentifier(ref id)) if id == "XXXX"),
"expected UnrecognisedSyntaxIdentifier(\"XXXX\"), got {result:?}"
);
}
#[test]
fn all_valid_syntax_identifiers_accepted() {
for id in &["UNOA", "UNOB", "UNOC", "UNOD", "UNOE", "UNOF", "KECA"] {
let input = format!(
"UNB+{id}:3+S+R+200101:0900+1'UNH+1+ORDERS:D:96A:UN'BGM+220+PO-001+9'UNT+3+1'UNZ+1+1'"
);
let result = parse_and_validate(input.as_bytes());
assert!(
result.is_ok(),
"syntax id '{id}' should be accepted, got {result:?}"
);
}
}
#[test]
fn recipient_password_qualifier_extracted_when_present() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1+MYPASS:AA'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("UNB with password+qualifier must parse ok");
assert_eq!(r.interchange.recipient_password.as_deref(), Some("MYPASS"));
assert_eq!(
r.interchange.recipient_password_qualifier.as_deref(),
Some("AA")
);
}
#[test]
fn recipient_password_qualifier_is_none_when_absent() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
assert!(r.interchange.recipient_password_qualifier.is_none());
}
#[test]
fn sequence_of_transfers_extracted_when_present() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNH+1+ORDERS:D:96A:UN++2:C'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("UNH with S010 must parse ok");
assert_eq!(r.messages[0].sequence_of_transfers, Some(2));
assert_eq!(r.messages[0].transfer_position.as_deref(), Some("C"));
}
#[test]
fn sequence_of_transfers_none_when_absent() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
assert!(r.messages[0].sequence_of_transfers.is_none());
assert!(r.messages[0].transfer_position.is_none());
}
#[test]
fn ung_app_sender_and_recipient_qualifiers_extracted() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNG+ORDERS+APPSEND:ZZZ+APPRECV:14+200101:0900+1+UN+D:96A'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNE+1+1'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("UNG with qualifiers must parse ok");
let g = &r.functional_groups[0];
assert_eq!(g.app_sender, "APPSEND");
assert_eq!(g.app_sender_qualifier, "ZZZ");
assert_eq!(g.app_recipient, "APPRECV");
assert_eq!(g.app_recipient_qualifier, "14");
}
#[test]
fn ung_qualifiers_empty_when_absent() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNE+1+1'\
UNZ+1+1'";
let r = parse_and_validate(input).unwrap();
let g = &r.functional_groups[0];
assert_eq!(g.app_sender_qualifier, "");
assert_eq!(g.app_recipient_qualifier, "");
}
#[test]
fn lenient_result_is_valid_true_on_clean_interchange() {
let owned = parse(VALID_INTERCHANGE);
let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
let r = validate_envelope_lenient(&segs);
assert!(r.is_valid());
assert!(r.errors.is_empty());
assert!(r.interchange.is_some());
}
#[test]
fn lenient_result_into_strict_ok_path() {
let owned = parse(VALID_INTERCHANGE);
let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
let r = validate_envelope_lenient(&segs);
let strict = r.into_strict();
assert!(
strict.is_ok(),
"into_strict() should succeed for valid interchange"
);
assert_eq!(strict.unwrap().messages.len(), 1);
}
#[test]
fn lenient_result_into_strict_err_path() {
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 owned = parse(input);
let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
let r = validate_envelope_lenient(&segs);
assert!(!r.is_valid());
let strict = r.into_strict();
assert!(strict.is_err());
let errors = strict.unwrap_err();
assert_eq!(errors.len(), 1);
assert!(matches!(
&errors[0],
EdifactError::MessageCountMismatch {
expected: 2,
actual: 1
}
));
}
#[test]
fn parse_unh_direct_extracts_all_s009_fields() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNH+REF99+ORDERS:D:96A:UN:EAN010'\
BGM+220+PO-001+9'\
UNT+3+REF99'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("must parse ok");
let msg = &r.messages[0];
assert_eq!(msg.message_ref, "REF99");
assert_eq!(msg.message_type, "ORDERS");
assert_eq!(msg.version, "D");
assert_eq!(msg.release, "96A");
assert_eq!(msg.controlling_agency, "UN");
assert_eq!(msg.association_code, "EAN010");
}
#[test]
fn parse_ung_direct_extracts_identifier_fields() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNG+ORDERS+APPSEND:ZZZ+APPRECV:14+200101:0900+GRP01+UN+D:96A'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNE+1+GRP01'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("must parse ok");
let g = &r.functional_groups[0];
assert_eq!(g.group_id, "ORDERS");
assert_eq!(g.app_sender, "APPSEND");
assert_eq!(g.app_sender_qualifier, "ZZZ");
assert_eq!(g.app_recipient, "APPRECV");
assert_eq!(g.app_recipient_qualifier, "14");
assert_eq!(g.group_ref, "GRP01");
assert_eq!(g.controlling_agency, "UN");
assert_eq!(g.version, "D");
assert_eq!(g.release, "96A");
}
#[test]
fn find_message_returns_correct_message_by_ref() {
let input = b"UNB+UNOA:3+S+R+200101:0900+CTRL2'\
UNH+REF-A+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+REF-A'\
UNH+REF-B+INVOIC:D:96A:UN'\
BGM+380+INV-001+9'\
UNT+3+REF-B'\
UNZ+2+CTRL2'";
let r = parse_and_validate(input).expect("two-message interchange must parse ok");
let msg_a = r.find_message("REF-A");
assert!(msg_a.is_some());
assert_eq!(msg_a.unwrap().message_type, "ORDERS");
let msg_b = r.find_message("REF-B");
assert!(msg_b.is_some());
assert_eq!(msg_b.unwrap().message_type, "INVOIC");
assert!(r.find_message("MISSING").is_none());
}
#[test]
fn ung_missing_group_ref_returns_err() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNG+ORDERS+S+R+200101:0900++UN+D:96A'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNE+1+'\
UNZ+1+1'";
let result = parse_and_validate(input);
assert!(
matches!(result,
Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 4, component_index: 0 })
if tag == "UNG"
),
"expected MissingRequiredComponent for empty UNG group_ref, got {result:?}"
);
}
#[test]
fn empty_segment_list_returns_missing_unb() {
let result = validate_envelope(&[]);
assert!(
matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNB"),
"expected MissingSegment(UNB) for empty input, got {result:?}"
);
}
#[test]
fn single_segment_only_unb_returns_missing_unz() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'";
let owned = parse(input);
let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
let result = validate_envelope(&segs);
assert!(
matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNZ"),
"expected MissingSegment(UNZ) for UNB-only input, got {result:?}"
);
}
#[test]
fn display_validated_interchange_includes_count() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
let s = r.to_string();
assert!(
s.contains("messages=1"),
"Display must include count '1': {s}"
);
}
#[test]
fn display_interchange_envelope_contains_arrow() {
let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
let s = r.interchange.to_string();
assert!(s.contains("->"), "Display must use ASCII '->' arrow: {s}");
assert!(
!s.contains('\u{2192}'),
"Display must not use Unicode → arrow: {s}"
);
}
#[test]
fn display_functional_group_envelope() {
let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
UNG+ORDERS+APPSEND+APPRECV+200101:0900+GRP01+UN+D:96A'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNE+1+GRP01'\
UNZ+1+1'";
let r = parse_and_validate(input).expect("must parse ok");
let g = &r.functional_groups[0];
let s = g.to_string();
assert!(s.contains("ORDERS"), "Display must include group_id: {s}");
assert!(
s.contains("APPSEND"),
"Display must include app_sender: {s}"
);
assert!(
s.contains("APPRECV"),
"Display must include app_recipient: {s}"
);
assert!(s.contains("GRP01"), "Display must include group_ref: {s}");
assert!(
s.contains("msgs="),
"Display must include message count label: {s}"
);
assert!(
s.contains("1/1"),
"Display must include actual/declared counts: {s}"
);
}
#[test]
fn lenient_has_errors_is_inverse_of_is_valid() {
let owned = parse(VALID_INTERCHANGE);
let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
let valid = validate_envelope_lenient(&segs);
assert!(valid.is_valid());
assert!(!valid.has_errors());
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 owned2 = parse(input);
let segs2: Vec<Segment<'_>> = owned2
.iter()
.map(crate::OwnedSegment::as_borrowed)
.collect();
let invalid = validate_envelope_lenient(&segs2);
assert!(!invalid.is_valid());
assert!(invalid.has_errors());
}
}