use crate::{EdifactError, Segment};
use std::io::Read;
use std::str::FromStr;
pub trait EdifactDeserialize: Sized {
fn edifact_deserialize(segments: &[Segment<'_>]) -> Result<Self, EdifactError>;
fn edifact_deserialize_owned(segments: &[crate::OwnedSegment]) -> Result<Self, EdifactError> {
let borrowed: Vec<Segment<'_>> = segments.iter().map(|s| s.as_borrowed()).collect();
Self::edifact_deserialize(&borrowed)
}
}
pub trait EdifactCompositeDeserialize: Sized {
fn edifact_deserialize_composite(composite: CompositeElement<'_>)
-> Result<Self, EdifactError>;
}
impl EdifactCompositeDeserialize for Vec<String> {
fn edifact_deserialize_composite(
composite: CompositeElement<'_>,
) -> Result<Self, EdifactError> {
Ok(composite.iter().map(str::to_owned).collect())
}
}
pub trait EdifactSegmentTag {
const SEGMENT_TAG: &'static str;
const QUALIFIER_PATTERN: Option<&'static str> = None;
fn matches_qualifier(seg: &Segment<'_>) -> bool {
match Self::QUALIFIER_PATTERN {
Some(pattern) => seg
.element_str(0)
.is_some_and(|q| qualifier_matches_pattern(q, pattern)),
None => true,
}
}
fn matches_segment(seg: &Segment<'_>) -> bool {
seg.tag == Self::SEGMENT_TAG && Self::matches_qualifier(seg)
}
fn matches_owned_segment(seg: &crate::OwnedSegment) -> bool {
if seg.tag != Self::SEGMENT_TAG {
return false;
}
match Self::QUALIFIER_PATTERN {
None => true,
Some(pattern) => {
let q = seg
.elements
.first()
.and_then(|e| e.components.first())
.map(|c| c.as_str())
.unwrap_or("");
qualifier_matches_pattern(q, pattern)
}
}
}
}
impl<T> EdifactDeserialize for Vec<T>
where
T: EdifactDeserialize + EdifactSegmentTag,
{
fn edifact_deserialize(segments: &[Segment<'_>]) -> Result<Self, EdifactError> {
segments
.iter()
.filter(|s| T::matches_segment(s))
.map(|seg| T::edifact_deserialize(std::slice::from_ref(seg)))
.collect()
}
fn edifact_deserialize_owned(segments: &[crate::OwnedSegment]) -> Result<Self, EdifactError> {
segments
.iter()
.filter(|s| T::matches_owned_segment(s))
.map(|seg| T::edifact_deserialize_owned(std::slice::from_ref(seg)))
.collect()
}
}
pub fn deserialize<T: EdifactDeserialize>(input: &[u8]) -> Result<T, EdifactError> {
let segments: Vec<Segment<'_>> = crate::from_bytes(input).collect::<Result<_, _>>()?;
T::edifact_deserialize(&segments)
}
pub fn deserialize_first_streaming<T>(input: &[u8]) -> Result<T, EdifactError>
where
T: EdifactDeserialize + EdifactSegmentTag,
{
for segment in crate::from_bytes(input) {
let segment = segment?;
if T::matches_segment(&segment) {
return T::edifact_deserialize(std::slice::from_ref(&segment));
}
}
Err(EdifactError::MissingSegment {
tag: T::SEGMENT_TAG.to_owned(),
expected_position: "any position in input".to_owned(),
})
}
pub fn deserialize_all_streaming<T>(input: &[u8]) -> Result<Vec<T>, EdifactError>
where
T: EdifactDeserialize + EdifactSegmentTag,
{
let mut out = Vec::new();
for segment in crate::from_bytes(input) {
let segment = segment?;
if T::matches_segment(&segment) {
out.push(T::edifact_deserialize(std::slice::from_ref(&segment))?);
}
}
Ok(out)
}
pub fn deserialize_first_from_reader<T, R>(reader: R) -> Result<T, EdifactError>
where
T: EdifactDeserialize + EdifactSegmentTag,
R: Read,
{
for segment in crate::from_reader_iter(reader) {
let segment = segment?;
if !T::matches_owned_segment(&segment) {
continue;
}
let borrowed = segment.as_borrowed();
return T::edifact_deserialize(std::slice::from_ref(&borrowed));
}
Err(EdifactError::MissingSegment {
tag: T::SEGMENT_TAG.to_owned(),
expected_position: "any position in input".to_owned(),
})
}
pub fn deserialize_all_from_reader<T, R>(reader: R) -> Result<Vec<T>, EdifactError>
where
T: EdifactDeserialize + EdifactSegmentTag,
R: Read,
{
let mut out = Vec::new();
for segment in crate::from_reader_iter(reader) {
let segment = segment?;
if !T::matches_owned_segment(&segment) {
continue;
}
let borrowed = segment.as_borrowed();
out.push(T::edifact_deserialize(std::slice::from_ref(&borrowed))?);
}
Ok(out)
}
pub fn deserialize_str<T: EdifactDeserialize>(input: &str) -> Result<T, EdifactError> {
deserialize(input.as_bytes())
}
pub fn find_segment<'s, 'd>(segments: &'s [Segment<'d>], tag: &str) -> Option<&'s Segment<'d>> {
segments.iter().find(|s| s.tag == tag)
}
pub fn find_segments_iter<'s, 'd: 's>(
segments: &'s [Segment<'d>],
tag: &'s str,
) -> impl Iterator<Item = &'s Segment<'d>> {
segments.iter().filter(move |s| s.tag == tag)
}
pub fn find_qualified_segment<'s, 'd>(
segments: &'s [Segment<'d>],
tag: &str,
qualifier: &str,
) -> Option<&'s Segment<'d>> {
segments
.iter()
.find(|s| s.tag == tag && s.element_str(0).unwrap_or("") == qualifier)
}
pub fn find_segment_typed<'s, 'd, T>(segments: &'s [Segment<'d>]) -> Option<&'s Segment<'d>>
where
T: EdifactSegmentTag,
{
segments.iter().find(|s| T::matches_segment(s))
}
pub fn find_segments_typed<'s, 'd, T>(
segments: &'s [Segment<'d>],
) -> Vec<&'s Segment<'d>>
where
T: EdifactSegmentTag,
{
segments.iter().filter(|s| T::matches_segment(s)).collect()
}
pub fn contiguous_groups_by_qualifier<'s, 'd, T>(
segments: &'s [Segment<'d>],
) -> Vec<&'s [Segment<'d>]>
where
T: EdifactSegmentTag,
{
let mut groups = Vec::new();
let mut idx = 0;
while idx < segments.len() {
if T::matches_segment(&segments[idx]) {
let start = idx;
idx += 1;
while idx < segments.len() && T::matches_segment(&segments[idx]) {
idx += 1;
}
groups.push(&segments[start..idx]);
} else {
idx += 1;
}
}
groups
}
pub fn groups_are_contiguous_by_qualifier<T>(segments: &[Segment<'_>]) -> bool
where
T: EdifactSegmentTag,
{
let mut seen_match = false;
let mut seen_gap_after_match = false;
for seg in segments {
if T::matches_segment(seg) {
if seen_gap_after_match {
return false;
}
seen_match = true;
} else if seen_match {
seen_gap_after_match = true;
}
}
true
}
pub fn qualifier_matches_pattern(value: &str, pattern: &str) -> bool {
if pattern.is_empty() {
return value.is_empty();
}
if !pattern.contains('*') {
return value == pattern;
}
if let Some((prefix, suffix)) = pattern.split_once('*') {
if !pattern[prefix.len() + 1..].contains('*') {
return value.len() >= prefix.len() + suffix.len()
&& value.starts_with(prefix)
&& value.ends_with(suffix)
&& {
let mid_start = prefix.len();
let mid_end = value.len().saturating_sub(suffix.len());
mid_start <= mid_end
};
}
}
let parts: smallvec::SmallVec<[&str; 4]> = pattern.split('*').collect();
let prefix = parts[0];
let suffix = parts[parts.len() - 1];
if !value.starts_with(prefix) || !value.ends_with(suffix) {
return false;
}
let mid_start = prefix.len();
let mid_end = value.len().saturating_sub(suffix.len());
if mid_start > mid_end {
return parts[1..parts.len() - 1].iter().all(|p| p.is_empty());
}
let mut remaining = &value[mid_start..mid_end];
for part in &parts[1..parts.len() - 1] {
if part.is_empty() {
continue;
}
match remaining.find(part) {
Some(idx) => remaining = &remaining[idx + part.len()..],
None => return false,
}
}
true
}
#[inline]
pub fn element_str<'s>(seg: &'s Segment<'_>, idx: usize) -> &'s str {
seg.element_str(idx).unwrap_or("")
}
pub fn required_element<'a>(seg: &'a Segment<'_>, idx: usize) -> Result<&'a str, EdifactError> {
seg.element_str(idx)
.filter(|s| !s.is_empty())
.ok_or_else(|| EdifactError::MissingRequiredElement {
tag: seg.tag.to_owned(),
element_index: idx,
})
}
pub fn optional_element<'a>(seg: &'a Segment<'_>, idx: usize) -> Option<&'a str> {
seg.element_str(idx)
.filter(|s| !s.is_empty())
}
pub fn required_component<'a>(
seg: &'a Segment<'_>,
elem_idx: usize,
comp_idx: usize,
) -> Result<&'a str, EdifactError> {
seg.elements
.get(elem_idx)
.and_then(|elem| elem.get_component(comp_idx))
.filter(|s| !s.is_empty())
.ok_or_else(|| EdifactError::MissingRequiredElement {
tag: seg.tag.to_owned(),
element_index: elem_idx,
})
}
pub fn optional_component<'a>(seg: &'a Segment<'_>, elem_idx: usize, comp_idx: usize) -> Option<&'a str> {
seg.elements
.get(elem_idx)
.and_then(|elem| elem.get_component(comp_idx))
.filter(|s| !s.is_empty())
}
pub fn get_components_iter<'a>(
seg: &'a Segment<'_>,
idx: usize,
) -> impl Iterator<Item = &'a str> {
seg.elements
.get(idx)
.into_iter()
.flat_map(|elem| elem.components.iter().map(|c| c.as_ref()))
}
pub struct CompositeElement<'a> {
components: &'a [std::borrow::Cow<'a, str>],
}
impl<'a> CompositeElement<'a> {
pub fn get(&self, i: usize) -> Option<&'a str> {
self.components.get(i).map(|c| c.as_ref())
}
pub fn get_or_empty(&self, i: usize) -> &'a str {
self.get(i).unwrap_or("")
}
pub fn len(&self) -> usize {
self.components.len()
}
pub fn is_empty(&self) -> bool {
self.components.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &'a str> {
self.components.iter().map(|c| c.as_ref())
}
pub fn from_slice(components: &'a [std::borrow::Cow<'a, str>]) -> Self {
Self { components }
}
}
pub fn composite_element<'a>(seg: &'a Segment<'_>, idx: usize) -> Option<CompositeElement<'a>> {
seg.elements.get(idx).map(|elem| CompositeElement {
components: &elem.components,
})
}
pub fn find_segment_owned<'s>(
segments: &'s [crate::OwnedSegment],
tag: &str,
) -> Option<&'s crate::OwnedSegment> {
segments.iter().find(|s| s.tag == tag)
}
pub fn find_qualified_segment_owned<'s>(
segments: &'s [crate::OwnedSegment],
tag: &str,
qualifier: &str,
) -> Option<&'s crate::OwnedSegment> {
segments.iter().find(|s| {
s.tag == tag && s.element_str(0).unwrap_or("") == qualifier
})
}
pub trait SegmentAccessor<'a> {
fn get_element(&'a self, idx: usize) -> Option<&'a str>;
fn get_component(&'a self, elem: usize, comp: usize) -> Option<&'a str>;
fn get_composite(&'a self, idx: usize) -> Option<CompositeElement<'a>>;
fn text_element(&'a self, idx: usize) -> Result<&'a str, EdifactError>;
fn optional_element(&'a self, idx: usize) -> Option<&'a str>;
fn code_element<T: FromStr>(&'a self, idx: usize) -> Result<T, EdifactError>;
fn required_composite(&'a self, elem: usize, comp: usize) -> Result<&'a str, EdifactError>;
fn repeating_components(
&'a self,
elem: usize,
start_idx: usize,
count: usize,
) -> Result<Vec<&'a str>, EdifactError>;
fn repeating_components_iter(
&'a self,
elem: usize,
start_idx: usize,
count: usize,
) -> impl Iterator<Item = Result<&'a str, EdifactError>> + 'a;
}
impl<'s, 'd> SegmentAccessor<'s> for Segment<'d>
where
'd: 's,
{
fn get_element(&'s self, idx: usize) -> Option<&'s str> {
self.element_str(idx).filter(|s| !s.is_empty())
}
fn get_component(&'s self, elem: usize, comp: usize) -> Option<&'s str> {
self.elements
.get(elem)
.and_then(|e| e.get_component(comp))
.filter(|s| !s.is_empty())
}
fn get_composite(&'s self, idx: usize) -> Option<CompositeElement<'s>> {
composite_element(self, idx)
}
fn text_element(&'s self, idx: usize) -> Result<&'s str, EdifactError> {
<Self as SegmentAccessor>::get_element(self, idx).ok_or_else(|| {
EdifactError::MissingRequiredElement {
tag: self.tag.to_owned(),
element_index: idx,
}
})
}
fn optional_element(&'s self, idx: usize) -> Option<&'s str> {
<Self as SegmentAccessor>::get_element(self, idx)
}
fn code_element<T: FromStr>(&'s self, idx: usize) -> Result<T, EdifactError> {
let raw = self.text_element(idx)?;
raw.parse::<T>().map_err(|_| EdifactError::InvalidText {
offset: self.element_span(idx).map(|s| s.start).unwrap_or(self.span.start),
})
}
fn required_composite(&'s self, elem: usize, comp: usize) -> Result<&'s str, EdifactError> {
<Self as SegmentAccessor>::get_component(self, elem, comp).ok_or_else(|| {
EdifactError::MissingRequiredElement {
tag: self.tag.to_owned(),
element_index: elem,
}
})
}
fn repeating_components(
&'s self,
elem: usize,
start_idx: usize,
count: usize,
) -> Result<Vec<&'s str>, EdifactError> {
let comp =
self.get_composite(elem)
.ok_or_else(|| EdifactError::MissingRequiredElement {
tag: self.tag.to_owned(),
element_index: elem,
})?;
(start_idx..start_idx + count)
.map(|idx| {
comp.get(idx).filter(|s| !s.is_empty()).ok_or_else(|| {
EdifactError::MissingRequiredElement {
tag: self.tag.to_owned(),
element_index: elem,
}
})
})
.collect()
}
fn repeating_components_iter(
&'s self,
elem: usize,
start_idx: usize,
count: usize,
) -> impl Iterator<Item = Result<&'s str, EdifactError>> + 's {
let tag = self.tag;
let components = self
.elements
.get(elem)
.map(|e| e.components.as_slice())
.unwrap_or(&[]);
(start_idx..start_idx + count).map(move |idx| {
components
.get(idx)
.map(|c| c.as_ref())
.filter(|s| !s.is_empty())
.ok_or_else(|| EdifactError::MissingRequiredElement {
tag: tag.to_owned(),
element_index: elem,
})
})
}
}
pub struct MessageWindowsSliceIter<'a> {
inner: crate::FromBytesIter<'a>,
buf: Vec<crate::Segment<'a>>,
in_message: bool,
done: bool,
}
impl<'a> MessageWindowsSliceIter<'a> {
fn new(inner: crate::FromBytesIter<'a>) -> Self {
Self {
inner,
buf: Vec::new(),
in_message: false,
done: false,
}
}
}
impl<'a> Iterator for MessageWindowsSliceIter<'a> {
type Item = Result<Vec<crate::Segment<'a>>, EdifactError>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
loop {
let segment = match self.inner.next() {
Some(Ok(s)) => s,
Some(Err(e)) => {
self.done = true;
return Some(Err(e));
}
None => {
self.done = true;
if self.in_message && !self.buf.is_empty() {
self.in_message = false;
let offset = self.buf.last().map(|s| s.span.end).unwrap_or(0);
return Some(Err(EdifactError::UnexpectedEof { offset }));
}
return None;
}
};
match segment.tag {
"UNH" => {
if self.in_message {
self.buf.clear();
self.in_message = false;
self.done = true;
return Some(Err(EdifactError::ValidationFailed {
error_count: 1,
first_message:
"UNH seen while a message window is already open (missing UNT)"
.to_owned(),
}));
}
self.buf.clear();
self.in_message = true;
self.buf.push(segment);
}
"UNT" if self.in_message => {
self.buf.push(segment);
self.in_message = false;
return Some(Ok(std::mem::take(&mut self.buf)));
}
_ if self.in_message => {
self.buf.push(segment);
}
_ => {
}
}
}
}
}
pub struct MessageWindowsIter<I> {
inner: I,
buf: Vec<crate::OwnedSegment>,
in_message: bool,
done: bool,
}
impl<I: Iterator<Item = Result<crate::OwnedSegment, EdifactError>>> MessageWindowsIter<I> {
pub fn new(inner: I) -> Self {
Self {
inner,
buf: Vec::new(),
in_message: false,
done: false,
}
}
}
impl<I: Iterator<Item = Result<crate::OwnedSegment, EdifactError>>> Iterator
for MessageWindowsIter<I>
{
type Item = Result<Vec<crate::OwnedSegment>, EdifactError>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
loop {
let segment = match self.inner.next() {
Some(Ok(s)) => s,
Some(Err(e)) => {
self.done = true;
return Some(Err(e));
}
None => {
self.done = true;
if self.in_message && !self.buf.is_empty() {
self.in_message = false;
let offset = self.buf.last().map(|s| s.span.end).unwrap_or(0);
return Some(Err(EdifactError::UnexpectedEof { offset }));
}
return None;
}
};
match segment.tag.as_str() {
"UNH" => {
if self.in_message {
self.buf.clear();
self.in_message = false;
self.done = true;
return Some(Err(EdifactError::ValidationFailed {
error_count: 1,
first_message:
"UNH seen while a message window is already open (missing UNT)"
.to_owned(),
}));
}
self.buf.clear();
self.in_message = true;
self.buf.push(segment);
}
"UNT" if self.in_message => {
self.buf.push(segment);
self.in_message = false;
return Some(Ok(std::mem::take(&mut self.buf)));
}
_ if self.in_message => {
self.buf.push(segment);
}
_ => {
}
}
}
}
}
pub fn message_windows_bytes(input: &[u8]) -> MessageWindowsSliceIter<'_> {
MessageWindowsSliceIter::new(crate::from_bytes(input))
}
pub fn message_windows_from_reader<R: Read>(
reader: R,
) -> MessageWindowsIter<crate::FromReaderIter<R>> {
MessageWindowsIter::new(crate::from_reader_iter(reader))
}
pub fn deserialize_messages_from_reader<T, R>(
reader: R,
) -> impl Iterator<Item = Result<T, EdifactError>>
where
T: EdifactDeserialize,
R: Read,
{
message_windows_from_reader(reader).map(|window| {
let window = window?;
T::edifact_deserialize_owned(&window)
})
}
pub fn deserialize_messages_bytes<T>(
input: &[u8],
) -> impl Iterator<Item = Result<T, EdifactError>> + '_
where
T: EdifactDeserialize,
{
message_windows_bytes(input).map(|window| {
let window = window?;
T::edifact_deserialize(&window)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, PartialEq)]
struct BgmSegment {
doc_name_code: String,
pruef_id: String,
msg_function: Option<String>,
}
impl EdifactSegmentTag for BgmSegment {
const SEGMENT_TAG: &'static str = "BGM";
}
struct NadM;
impl EdifactSegmentTag for NadM {
const SEGMENT_TAG: &'static str = "NAD";
const QUALIFIER_PATTERN: Option<&'static str> = Some("M*");
}
struct NadWildcard;
impl EdifactSegmentTag for NadWildcard {
const SEGMENT_TAG: &'static str = "NAD";
const QUALIFIER_PATTERN: Option<&'static str> = Some("M*");
}
impl EdifactDeserialize for BgmSegment {
fn edifact_deserialize(segments: &[Segment<'_>]) -> Result<Self, EdifactError> {
let seg = find_segment(segments, "BGM").ok_or_else(|| {
EdifactError::MissingRequiredElement {
tag: "BGM".to_owned(),
element_index: 0,
}
})?;
Ok(Self {
doc_name_code: element_str(seg, 0).to_owned(),
pruef_id: element_str(seg, 1).to_owned(),
msg_function: seg
.element_str(2)
.filter(|s| !s.is_empty())
.map(str::to_owned),
})
}
}
#[test]
fn deserialize_single_segment() {
let input = b"BGM+E03+11042+9'";
let bgm: BgmSegment = deserialize(input).unwrap();
assert_eq!(bgm.doc_name_code, "E03");
assert_eq!(bgm.pruef_id, "11042");
assert_eq!(bgm.msg_function, Some("9".to_owned()));
}
#[test]
fn streaming_deserialize_first_from_bytes() {
let input = b"UNH+1+ORDERS:D:11A:UN'BGM+E03+11042+9'UNT+3+1'";
let bgm: BgmSegment = deserialize_first_streaming(input).unwrap();
assert_eq!(bgm.pruef_id, "11042");
}
#[test]
fn streaming_deserialize_all_from_bytes() {
let input = b"BGM+E03+11042+9'RFF+AA:1'BGM+E01+11043+9'";
let bgms: Vec<BgmSegment> = deserialize_all_streaming(input).unwrap();
assert_eq!(bgms.len(), 2);
assert_eq!(bgms[0].pruef_id, "11042");
assert_eq!(bgms[1].pruef_id, "11043");
}
#[test]
fn streaming_deserialize_first_from_reader() {
let input = std::io::Cursor::new(b"UNH+1+ORDERS:D:11A:UN'BGM+E03+11042+9'UNT+3+1'".to_vec());
let bgm: BgmSegment = deserialize_first_from_reader(input).unwrap();
assert_eq!(bgm.pruef_id, "11042");
}
#[test]
fn streaming_deserialize_all_from_reader() {
let input = std::io::Cursor::new(b"BGM+E03+11042+9'BGM+E01+11043+9'".to_vec());
let bgms: Vec<BgmSegment> = deserialize_all_from_reader(input).unwrap();
assert_eq!(bgms.len(), 2);
assert_eq!(bgms[0].pruef_id, "11042");
assert_eq!(bgms[1].pruef_id, "11043");
}
#[test]
fn missing_segment_returns_error() {
let input = b"DTM+137:20230401:102'";
let result: Result<BgmSegment, _> = deserialize(input);
assert!(result.is_err());
}
#[test]
fn vec_collects_all_matching_segments() {
let input = b"DTM+137:20230401:102'BGM+E03+11042+9'BGM+E01+11043+9'";
let bgms: Vec<BgmSegment> = deserialize(input).unwrap();
assert_eq!(bgms.len(), 2);
assert_eq!(bgms[0].pruef_id, "11042");
assert_eq!(bgms[1].pruef_id, "11043");
}
#[test]
fn find_qualified_segment_matches_qualifier() {
let input = b"NAD+MS+9900001+293'NAD+MR+9900002+293'";
let segments: Vec<Segment<'_>> =
crate::from_bytes(input).collect::<Result<_, _>>().unwrap();
let nad_ms = find_qualified_segment(&segments, "NAD", "MS");
let nad_mr = find_qualified_segment(&segments, "NAD", "MR");
assert!(nad_ms.is_some());
assert!(nad_mr.is_some());
assert_eq!(element_str(nad_ms.unwrap(), 0), "MS");
assert_eq!(element_str(nad_mr.unwrap(), 0), "MR");
}
#[test]
fn round_trip_str_api() {
let input = "BGM+E03+11042+9'";
let bgm: BgmSegment = deserialize_str(input).unwrap();
assert_eq!(bgm.pruef_id, "11042");
}
#[test]
fn required_element_extraction() {
let input = b"BGM+E03+11042+9'";
let segments: Vec<Segment<'_>> =
crate::from_bytes(input).collect::<Result<_, _>>().unwrap();
let seg = &segments[0];
assert_eq!(required_element(seg, 0).unwrap(), "E03");
assert_eq!(required_element(seg, 1).unwrap(), "11042");
assert!(required_element(seg, 5).is_err());
}
#[test]
fn optional_element_extraction() {
let input = b"BGM+E03+11042+9'BGM+E01++absent'";
let segments: Vec<Segment<'_>> =
crate::from_bytes(input).collect::<Result<_, _>>().unwrap();
assert_eq!(optional_element(&segments[0], 0), Some("E03"));
assert_eq!(optional_element(&segments[0], 1), Some("11042"));
assert_eq!(optional_element(&segments[0], 5), None);
assert_eq!(optional_element(&segments[1], 1), None);
}
#[test]
fn component_extraction() {
let input = b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+1'";
let segments: Vec<Segment<'_>> =
crate::from_bytes(input).collect::<Result<_, _>>().unwrap();
let seg = &segments[0];
assert_eq!(required_component(seg, 0, 0).unwrap(), "UNOA");
assert_eq!(required_component(seg, 0, 1).unwrap(), "1");
assert!(required_component(seg, 0, 5).is_err());
}
#[test]
fn composite_element_helper() {
let input = b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+1'";
let segments: Vec<Segment<'_>> =
crate::from_bytes(input).collect::<Result<_, _>>().unwrap();
let seg = &segments[0];
let comp = composite_element(seg, 0).unwrap();
assert_eq!(comp.len(), 2);
assert_eq!(comp.get(0), Some("UNOA"));
assert_eq!(comp.get(1), Some("1"));
assert_eq!(comp.get(5), None);
assert_eq!(comp.get_or_empty(5), "");
}
#[test]
fn get_all_components() {
let input = b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+1'";
let segments: Vec<Segment<'_>> =
crate::from_bytes(input).collect::<Result<_, _>>().unwrap();
let seg = &segments[0];
let comps: Vec<&str> = get_components_iter(seg, 0).collect(); assert!(!comps.is_empty(), "Expected components but got empty");
assert_eq!(comps.len(), 2);
assert_eq!(comps[0], "UNOA");
assert_eq!(comps[1], "1");
}
#[test]
fn qualifier_pattern_matching_supports_exact_and_wildcard() {
assert!(qualifier_matches_pattern("MS", "MS"));
assert!(!qualifier_matches_pattern("MS", "M")); assert!(qualifier_matches_pattern("MS", "M*"));
assert!(qualifier_matches_pattern("MRY", "M*Y"));
assert!(!qualifier_matches_pattern("AB", "M*"));
}
#[test]
fn qualifier_matches_pattern_table() {
let cases: &[(&str, &str, bool)] = &[
("", "", true), ("", "*", true), ("A", "", false), ("", "A", false), ("MS", "MS", true),
("BY", "BY", true),
("ms", "MS", false), ("MSX", "MS", false), ("M", "MS", false), ("MS", "M*", true),
("MULTI", "MUL*", true),
("AB", "M*", false),
("", "M*", false), ("MSG", "*G", true),
("G", "*G", true),
("MSG", "*X", false),
("", "*G", false),
("MRY", "M*Y", true),
("MAY", "M*Y", true),
("MY", "M*Y", true), ("MYY", "M*Y", true), ("MAYZ", "M*Y", false), ("AB", "M*Y", false),
("*", "*", true), ("anything", "*", true),
("", "*", true),
("ABCDE", "A*C*E", true),
("ACE", "A*C*E", true), ("AXCYE", "A*C*E", true),
("ABCDF", "A*C*E", false),
("AB", "A**B", true), ("AB", "A*B*C", false),
("XMS", "MS", false),
];
for (value, pattern, expected) in cases {
let got = qualifier_matches_pattern(value, pattern);
assert_eq!(
got, *expected,
"qualifier_matches_pattern({value:?}, {pattern:?}) expected {expected} but got {got}"
);
}
}
#[test]
fn typed_qualifier_helpers_work() {
let input = b"NAD+MS+9900001+293'NAD+MR+9900002+293'";
let segments: Vec<Segment<'_>> =
crate::from_bytes(input).collect::<Result<_, _>>().unwrap();
let first = find_segment_typed::<NadM>(&segments).unwrap();
assert_eq!(first.element_str(0), Some("MS"));
let all = find_segments_typed::<NadWildcard>(&segments);
assert_eq!(all.len(), 2);
}
#[test]
fn segment_accessor_trait_methods_work() {
let input = b"UNB+UNOA:1+SENDER+RECEIVER+200101:0900+1'";
let segments: Vec<Segment<'_>> =
crate::from_bytes(input).collect::<Result<_, _>>().unwrap();
let seg = &segments[0];
assert_eq!(SegmentAccessor::get_element(seg, 1), Some("SENDER"));
assert_eq!(SegmentAccessor::required_composite(seg, 0, 1).unwrap(), "1");
let parsed: i32 = SegmentAccessor::code_element(seg, 4).unwrap();
assert_eq!(parsed, 1);
let reps = SegmentAccessor::repeating_components(seg, 3, 0, 2).unwrap();
assert_eq!(reps, vec!["200101", "0900"]);
}
#[test]
fn group_helpers_detect_contiguity() {
struct NadAny;
impl EdifactSegmentTag for NadAny {
const SEGMENT_TAG: &'static str = "NAD";
}
let contiguous_input = b"NAD+MS+1'NAD+MR+2'RFF+AA:1'";
let contiguous_segments: Vec<Segment<'_>> = crate::from_bytes(contiguous_input)
.collect::<Result<_, _>>()
.unwrap();
assert!(groups_are_contiguous_by_qualifier::<NadAny>(
&contiguous_segments
));
let non_contiguous_input = b"NAD+MS+1'RFF+AA:1'NAD+MR+2'";
let non_contiguous_segments: Vec<Segment<'_>> = crate::from_bytes(non_contiguous_input)
.collect::<Result<_, _>>()
.unwrap();
assert!(!groups_are_contiguous_by_qualifier::<NadAny>(
&non_contiguous_segments
));
}
#[test]
fn group_helpers_collect_contiguous_groups() {
struct NadAny;
impl EdifactSegmentTag for NadAny {
const SEGMENT_TAG: &'static str = "NAD";
}
let input = b"NAD+MS+1'NAD+MR+2'RFF+AA:1'NAD+BY+3'";
let segments: Vec<Segment<'_>> =
crate::from_bytes(input).collect::<Result<_, _>>().unwrap();
let groups = contiguous_groups_by_qualifier::<NadAny>(&segments);
assert_eq!(groups.len(), 2);
assert_eq!(groups[0].len(), 2);
assert_eq!(groups[1].len(), 1);
}
#[test]
fn message_windows_bytes_yields_complete_windows() {
let input = b"UNB+UNOA:1+S+R+200101:0900+1'\
UNH+1+ORDERS:D:96A:UN'\
BGM+220+PO-001+9'\
UNT+3+1'\
UNZ+1+1'";
let windows: Vec<_> = message_windows_bytes(input)
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(windows.len(), 1);
assert_eq!(windows[0][0].tag, "UNH");
assert_eq!(windows[0].last().unwrap().tag, "UNT");
}
#[test]
fn message_windows_truncated_stream_returns_error() {
let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO-001+9'";
let results: Vec<_> = message_windows_bytes(input).collect();
assert_eq!(results.len(), 1);
assert!(
matches!(results[0], Err(EdifactError::UnexpectedEof { .. })),
"expected UnexpectedEof for truncated window, got: {:?}",
results[0]
);
}
#[test]
fn message_windows_subsequent_calls_return_none_after_truncation() {
let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO-001+9'";
let mut iter = message_windows_bytes(input);
assert!(matches!(
iter.next(),
Some(Err(EdifactError::UnexpectedEof { .. }))
));
assert!(iter.next().is_none());
}
#[test]
fn message_windows_unh_without_unt_before_next_unh_returns_error() {
let input = b"UNH+1+ORDERS:D:96A:UN'BGM+220+PO-001+9'\
UNH+2+ORDERS:D:96A:UN'BGM+220+PO-002+9'UNT+3+2'";
let results: Vec<_> = message_windows_bytes(input).collect();
assert!(
matches!(results[0], Err(EdifactError::ValidationFailed { .. })),
"expected ValidationFailed, got: {:?}",
results[0]
);
}
}