use std::io::{BufReader, Read};
use edifact_rs::{MessageWindows, OwnedSegment, ReaderConfig, from_bufread_with_config};
use crate::{AnyMessage, Error, MessageType};
#[cfg(feature = "tracing")]
fn sanitize_release_code(s: &str) -> std::borrow::Cow<'_, str> {
const MAX_LEN: usize = 16;
if s.len() <= MAX_LEN && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '.') {
std::borrow::Cow::Borrowed(s)
} else {
std::borrow::Cow::Owned(format!("<invalid-code:{} bytes>", s.len()))
}
}
fn decode_repertoire(input: &[u8]) -> Result<std::borrow::Cow<'_, [u8]>, Error> {
edifact_rs::decode_interchange(input).map_err(Error::Parse)
}
fn resolve_pid_source(
msg_type_code: &str,
assoc_code: &str,
registry: &crate::registry::ReleaseRegistry,
) -> crate::registry::PidSource {
MessageType::from_unh_code(msg_type_code)
.and_then(|mt| {
let rel = crate::release::Release::new(assoc_code);
registry.profile(mt, &rel).ok()
})
.map(super::registry::Profile::pid_source)
.unwrap_or_default()
}
pub(crate) fn resolve_pid_source_pub(
msg_type_code: &str,
assoc_code: &str,
registry: &crate::registry::ReleaseRegistry,
) -> crate::registry::PidSource {
resolve_pid_source(msg_type_code, assoc_code, registry)
}
pub const DEFAULT_MAX_SEGMENT_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, Copy)]
pub struct ParseConfig {
pub max_segment_bytes: usize,
pub max_segments: Option<usize>,
pub max_input_bytes: Option<usize>,
pub max_messages_per_interchange: Option<usize>,
pub max_messages: Option<usize>,
pub max_segments_per_message: Option<usize>,
pub reference_date: Option<time::Date>,
}
impl Default for ParseConfig {
fn default() -> Self {
Self {
max_segment_bytes: DEFAULT_MAX_SEGMENT_BYTES,
max_segments: Some(10_000),
max_input_bytes: Some(10 * 1024 * 1024),
max_messages_per_interchange: Some(1_000),
max_segments_per_message: Some(500),
max_messages: None,
reference_date: None,
}
}
}
impl ParseConfig {
#[must_use]
pub fn with_reference_date(mut self, date: time::Date) -> Self {
self.reference_date = Some(date);
self
}
pub(crate) fn to_reader_config(self) -> ReaderConfig {
let mut cfg = ReaderConfig::default().max_segment_bytes(self.max_segment_bytes);
if let Some(n) = self.max_segments {
cfg = cfg.max_segments(n);
}
if let Some(n) = self.max_input_bytes {
cfg = cfg.max_input_bytes(n as u64);
}
if let Some(n) = self.max_messages {
cfg = cfg.max_messages(n);
}
cfg
}
}
pub fn parse_envelope_only(input: &[u8]) -> Result<crate::light_message::LightMessage, Error> {
let cfg = ParseConfig::default().to_reader_config();
let input = decode_repertoire(input)?;
let segments: Vec<OwnedSegment> = edifact_rs::from_reader_with_config(input.as_ref(), cfg)
.collect::<Result<_, _>>()
.map_err(Error::Parse)?;
crate::light_message::LightMessage::from_segments(
segments,
crate::registry::ReleaseRegistry::global(),
)
}
pub fn parse(input: &[u8]) -> Result<AnyMessage, Error> {
parse_with_registry(
input,
ParseConfig::default(),
crate::registry::ReleaseRegistry::global(),
)
}
pub(crate) fn check_interchange_party_identity(
unb_sender: &str,
unb_receiver: &str,
message: &AnyMessage,
message_index: usize,
) -> Result<(), Error> {
for (unb_id, nad_id, qualifier, nad_qualifier) in [
(unb_sender, message.nad_sender(), "DE0004", "MS"),
(unb_receiver, message.nad_receiver(), "DE0010", "MR"),
] {
let Some(nad_id) = nad_id else { continue };
if unb_id.is_empty() || nad_id.is_empty() || unb_id == nad_id {
continue;
}
return Err(Error::InterchangePartyMismatch {
qualifier,
nad_qualifier,
unb_id: unb_id.to_owned(),
nad_id: nad_id.to_owned(),
message_index,
});
}
Ok(())
}
pub(crate) fn parse_with_registry(
input: &[u8],
config: ParseConfig,
registry: &crate::registry::ReleaseRegistry,
) -> Result<AnyMessage, Error> {
let per_msg_limit = config.max_segments_per_message;
let cfg = config.to_reader_config();
let input = decode_repertoire(input)?;
let segments: Vec<OwnedSegment> = edifact_rs::from_reader_with_config(input.as_ref(), cfg)
.collect::<Result<_, _>>()
.map_err(Error::Parse)?;
if let Some(lim) = per_msg_limit {
let actual = segments.len();
if actual > lim {
return Err(Error::TooManySegmentsInMessage { limit: lim, actual });
}
}
let (unb_sender, unb_receiver) = segments
.iter()
.find(|s| s.tag == "UNB")
.map(|unb| {
(
unb.component_str(1, 0).unwrap_or("").to_owned(),
unb.component_str(2, 0).unwrap_or("").to_owned(),
)
})
.unwrap_or_default();
let message = dispatch_message_on_date(segments, registry, config.reference_date)?;
check_interchange_party_identity(&unb_sender, &unb_receiver, &message, 0)?;
Ok(message)
}
pub fn parse_interchange(reader: impl Read) -> impl Iterator<Item = Result<AnyMessage, Error>> {
parse_interchange_with_registry(reader, ParseConfig::default())
}
pub(crate) fn parse_interchange_with_registry(
reader: impl Read,
config: ParseConfig,
) -> impl Iterator<Item = Result<AnyMessage, Error>> {
parse_interchange_impl(
reader,
config,
std::sync::Arc::clone(crate::registry::ReleaseRegistry::global_arc()),
)
}
pub(crate) fn parse_interchange_impl(
reader: impl Read,
config: ParseConfig,
registry: std::sync::Arc<crate::registry::ReleaseRegistry>,
) -> impl Iterator<Item = Result<AnyMessage, Error>> {
let limit = config.max_messages_per_interchange;
let per_msg_limit = config.max_segments_per_message;
let reference_date = config.reference_date;
let cfg = config.to_reader_config();
let (decoded, fatal) = match edifact_rs::decode_reader(BufReader::new(reader)) {
Ok(d) => (Some(d), None),
Err(e) => (None, Some(Error::Parse(e))),
};
fatal
.map(Err)
.into_iter()
.chain(decoded.into_iter().flat_map(move |d| {
let registry = std::sync::Arc::clone(®istry);
MessageWindows::new(from_bufread_with_config(BufReader::new(d), cfg))
.enumerate()
.map(move |(index, window)| {
if let Some(lim) = limit
&& index >= lim
{
return Err(Error::TooManyMessages { limit: lim });
}
let window = window.map_err(Error::Parse)?;
if let Some(lim) = per_msg_limit {
let actual = window.segments.len();
if actual > lim {
return Err(Error::TooManySegmentsInMessage { limit: lim, actual });
}
}
dispatch_message_on_date(window.segments, ®istry, reference_date)
})
}))
}
pub(crate) fn parse_interchange_with_arc_registry(
reader: impl Read,
config: ParseConfig,
registry: std::sync::Arc<crate::registry::ReleaseRegistry>,
) -> impl Iterator<Item = Result<AnyMessage, Error>> {
parse_interchange_impl(reader, config, registry)
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(skip(reader, config, registry))
)]
pub(crate) fn parse_interchange_buffered_impl(
reader: impl Read,
config: ParseConfig,
registry: std::sync::Arc<crate::registry::ReleaseRegistry>,
) -> Result<(crate::interchange::InterchangeHeader, InterchangeIter), Error> {
let cfg = config.to_reader_config();
let decoded = edifact_rs::decode_reader(BufReader::new(reader)).map_err(Error::Parse)?;
let segments: Vec<OwnedSegment> = from_bufread_with_config(BufReader::new(decoded), cfg)
.collect::<Result<_, _>>()
.map_err(Error::Parse)?;
let header = parse_interchange_header_from_segments(&segments)?;
let unz_ref = segments
.iter()
.rfind(|s| s.tag == "UNZ")
.and_then(|unz| unz.element_str(1))
.map(str::to_owned);
let declared_count = segments
.iter()
.rfind(|s| s.tag == "UNZ")
.and_then(|unz| unz.element_str(0))
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
let msg_iter = MessageWindows::new(segments.into_iter().map(
Ok::<_, edifact_rs::EdifactError>
as fn(OwnedSegment) -> Result<OwnedSegment, edifact_rs::EdifactError>,
));
let iter = InterchangeIter {
inner: msg_iter,
header: header.clone(),
registry,
limit: config.max_messages_per_interchange,
index: 0,
actual_count: 0,
declared_count,
unz_ref,
unz_checked: false,
done: false,
reference_date: config.reference_date,
};
Ok((header, iter))
}
#[derive(Debug, Clone)]
pub struct Parser {
config: ParseConfig,
}
impl Default for Parser {
fn default() -> Self {
Self::new()
}
}
impl Parser {
#[must_use]
pub fn new() -> Self {
Self {
config: ParseConfig::default(),
}
}
#[must_use]
pub fn with_config(config: ParseConfig) -> Self {
Self { config }
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(skip(self, input), fields(bytes = input.len()))
)]
pub fn parse(&self, input: &[u8]) -> Result<AnyMessage, Error> {
parse_with_registry(
input,
self.config,
crate::registry::ReleaseRegistry::global(),
)
}
pub fn parse_reader(&self, reader: impl Read) -> Result<AnyMessage, Error> {
let cfg = self.config.to_reader_config();
let decoded = edifact_rs::decode_reader(BufReader::new(reader)).map_err(Error::Parse)?;
let segments: Vec<OwnedSegment> = from_bufread_with_config(BufReader::new(decoded), cfg)
.collect::<Result<_, _>>()
.map_err(Error::Parse)?;
dispatch_message_on_date(
segments,
crate::registry::ReleaseRegistry::global(),
self.config.reference_date,
)
}
pub fn parse_envelope_only(
&self,
input: &[u8],
) -> Result<crate::light_message::LightMessage, Error> {
let cfg = self.config.to_reader_config();
let input = decode_repertoire(input)?;
let segments: Vec<OwnedSegment> = edifact_rs::from_reader_with_config(input.as_ref(), cfg)
.collect::<Result<_, _>>()
.map_err(Error::Parse)?;
crate::light_message::LightMessage::from_segments(
segments,
crate::registry::ReleaseRegistry::global(),
)
}
pub fn parse_interchange(
&self,
reader: impl Read,
) -> impl Iterator<Item = Result<AnyMessage, Error>> {
parse_interchange_with_registry(reader, self.config)
}
pub fn parse_interchange_buffered(
&self,
reader: impl Read,
) -> Result<(crate::interchange::InterchangeHeader, InterchangeIter), Error> {
parse_interchange_buffered_impl(
reader,
self.config,
std::sync::Arc::clone(crate::registry::ReleaseRegistry::global_arc()),
)
}
pub fn parse_interchange_full(
&self,
reader: impl Read,
) -> Result<crate::interchange::ParsedInterchange, Error> {
let reader_cfg = self.config.to_reader_config();
let decoded = edifact_rs::decode_reader(BufReader::new(reader)).map_err(Error::Parse)?;
let segments: Vec<OwnedSegment> =
from_bufread_with_config(BufReader::new(decoded), reader_cfg)
.collect::<Result<_, _>>()
.map_err(Error::Parse)?;
parse_interchange_full_from_segments(segments, &self.config)
}
}
pub struct InterchangeIter {
#[expect(clippy::type_complexity)]
inner: MessageWindows<
'static,
std::iter::Map<
std::vec::IntoIter<OwnedSegment>,
fn(OwnedSegment) -> Result<OwnedSegment, edifact_rs::EdifactError>,
>,
>,
header: crate::interchange::InterchangeHeader,
registry: std::sync::Arc<crate::registry::ReleaseRegistry>,
limit: Option<usize>,
index: usize,
actual_count: usize,
declared_count: usize,
unz_ref: Option<String>,
unz_checked: bool,
done: bool,
reference_date: Option<time::Date>,
}
impl Iterator for InterchangeIter {
type Item = Result<crate::interchange::MessageEnvelope, Error>;
fn next(&mut self) -> Option<Self::Item> {
if self.done {
return None;
}
if let Some(window_result) = self.inner.next() {
let index = self.index;
self.index += 1;
self.actual_count += 1;
if let Some(lim) = self.limit
&& index >= lim
{
self.done = true;
return Some(Err(Error::TooManyMessages { limit: lim }));
}
let result = (|| {
let window = window_result.map_err(Error::Parse)?;
let message =
dispatch_message_on_date(window.segments, &self.registry, self.reference_date)?;
check_interchange_party_identity(
self.header.sender_id.as_ref(),
self.header.receiver_id.as_ref(),
&message,
index,
)?;
Ok(crate::interchange::MessageEnvelope {
message,
header: self.header.clone(),
message_index: index,
})
})();
Some(result)
} else {
if self.unz_checked {
self.done = true;
return None;
}
self.unz_checked = true;
if let Some(ref uref) = self.unz_ref
&& !uref.is_empty()
&& uref.as_str() != self.header.control_ref.as_ref()
{
self.done = true;
return Some(Err(Error::InterchangeRefMismatch {
unb_ref: self.header.control_ref.to_string(),
unz_ref: uref.clone(),
}));
}
if self.declared_count != 0 && self.declared_count != self.actual_count {
self.done = true;
return Some(Err(Error::InterchangeCountMismatch {
declared: self.declared_count,
actual: self.actual_count,
}));
}
self.done = true;
None
}
}
}
fn parse_interchange_header_from_segments(
segments: &[OwnedSegment],
) -> Result<crate::interchange::InterchangeHeader, Error> {
use crate::interchange::InterchangeHeader;
let unb = segments
.iter()
.find(|s| s.tag == "UNB")
.ok_or(Error::MissingSegment("UNB"))?;
let syntax_id = unb.component_str(0, 0).unwrap_or("UNOC").to_owned();
let syntax_version: u8 = unb
.component_str(0, 1)
.and_then(|s| s.parse().ok())
.unwrap_or(3);
let sender_id = unb.component_str(1, 0).unwrap_or("").to_owned();
let sender_qualifier = unb.component_str(1, 1).unwrap_or("").to_owned();
let receiver_id = unb.component_str(2, 0).unwrap_or("").to_owned();
let receiver_qualifier = unb.component_str(2, 1).unwrap_or("").to_owned();
let transmission_datetime = parse_unb_datetime(
unb.component_str(3, 0).unwrap_or(""),
unb.component_str(3, 1).unwrap_or(""),
);
let control_ref = unb.element_str(4).unwrap_or("").to_owned();
let test_indicator = unb.element_str(10).is_some_and(|v| v.trim() == "1");
Ok(InterchangeHeader {
sender_id: sender_id.into_boxed_str(),
sender_qualifier: sender_qualifier.into_boxed_str(),
receiver_id: receiver_id.into_boxed_str(),
receiver_qualifier: receiver_qualifier.into_boxed_str(),
transmission_datetime,
control_ref: control_ref.into_boxed_str(),
syntax_id: syntax_id.into_boxed_str(),
syntax_version,
test_indicator,
})
}
fn parse_interchange_full_from_segments(
segments: Vec<OwnedSegment>,
config: &ParseConfig,
) -> Result<crate::interchange::ParsedInterchange, Error> {
parse_interchange_full_from_segments_with_registry(
segments,
config,
std::sync::Arc::clone(crate::registry::ReleaseRegistry::global_arc()),
)
}
pub(crate) fn parse_interchange_full_with_arc_registry(
data: &[u8],
config: ParseConfig,
registry: std::sync::Arc<crate::registry::ReleaseRegistry>,
) -> Result<crate::interchange::ParsedInterchange, Error> {
let cfg = config.to_reader_config();
let data = decode_repertoire(data)?;
let segments: Vec<OwnedSegment> = edifact_rs::from_reader_with_config(data.as_ref(), cfg)
.collect::<Result<_, _>>()
.map_err(Error::Parse)?;
parse_interchange_full_from_segments_with_registry(segments, &config, registry)
}
#[expect(clippy::needless_pass_by_value)]
fn parse_interchange_full_from_segments_with_registry(
segments: Vec<OwnedSegment>,
config: &ParseConfig,
registry: std::sync::Arc<crate::registry::ReleaseRegistry>,
) -> Result<crate::interchange::ParsedInterchange, Error> {
use crate::interchange::{InterchangeHeader, MessageEnvelope, ParsedInterchange};
let unb = segments
.iter()
.find(|s| s.tag == "UNB")
.ok_or(Error::MissingSegment("UNB"))?;
let syntax_id = unb.component_str(0, 0).unwrap_or("UNOC").to_owned();
let syntax_version: u8 = unb
.component_str(0, 1)
.and_then(|s| s.parse().ok())
.unwrap_or(3);
let sender_id = unb.component_str(1, 0).unwrap_or("").to_owned();
let sender_qualifier = unb.component_str(1, 1).unwrap_or("").to_owned();
let receiver_id = unb.component_str(2, 0).unwrap_or("").to_owned();
let receiver_qualifier = unb.component_str(2, 1).unwrap_or("").to_owned();
let transmission_datetime = parse_unb_datetime(
unb.component_str(3, 0).unwrap_or(""),
unb.component_str(3, 1).unwrap_or(""),
);
let control_ref = unb.element_str(4).unwrap_or("").to_owned();
#[cfg(feature = "tracing")]
let _span = tracing::debug_span!(
"parse_interchange",
sender = %sender_id,
receiver = %receiver_id,
control_ref = %control_ref,
segment_count = segments.len(),
)
.entered();
let test_indicator = unb.element_str(10).is_some_and(|v| v.trim() == "1");
let header = InterchangeHeader {
sender_id: sender_id.into_boxed_str(),
sender_qualifier: sender_qualifier.into_boxed_str(),
receiver_id: receiver_id.into_boxed_str(),
receiver_qualifier: receiver_qualifier.into_boxed_str(),
transmission_datetime,
control_ref: control_ref.into_boxed_str(),
syntax_id: syntax_id.into_boxed_str(),
syntax_version,
test_indicator,
};
let unz_seg = segments.iter().rfind(|s| s.tag == "UNZ");
let (trailer_ref, declared_message_count) = match unz_seg {
Some(unz_seg) => {
let count: usize = unz_seg
.element_str(0)
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let tref = unz_seg.element_str(1).unwrap_or("").to_owned();
(tref.into_boxed_str(), count)
}
None => ("".into(), 0),
};
let msg_iter = edifact_rs::MessageWindows::new(
segments.into_iter().map(Ok::<_, edifact_rs::EdifactError>),
);
let mut messages: Vec<MessageEnvelope> = Vec::new();
for (index, window_result) in msg_iter.enumerate() {
if let Some(limit) = config.max_messages_per_interchange
&& index >= limit
{
return Err(Error::TooManyMessages { limit });
}
let window = window_result.map_err(Error::Parse)?;
let message = dispatch_message_on_date(window.segments, ®istry, config.reference_date)?;
messages.push(MessageEnvelope {
message,
header: header.clone(),
message_index: index,
});
}
for (index, envelope) in messages.iter().enumerate() {
check_interchange_party_identity(
header.sender_id.as_ref(),
header.receiver_id.as_ref(),
&envelope.message,
index,
)?;
}
if !trailer_ref.is_empty() && trailer_ref.as_ref() != header.control_ref.as_ref() {
return Err(Error::InterchangeRefMismatch {
unb_ref: header.control_ref.to_string(),
unz_ref: trailer_ref.to_string(),
});
}
if declared_message_count != 0 && declared_message_count != messages.len() {
return Err(Error::InterchangeCountMismatch {
declared: declared_message_count,
actual: messages.len(),
});
}
Ok(ParsedInterchange {
header,
messages,
trailer_ref,
declared_message_count,
})
}
pub(crate) fn parse_unb_datetime(date: &str, time: &str) -> Option<time::OffsetDateTime> {
use time::{Date, Month, PrimitiveDateTime, Time, UtcOffset};
fn digits(s: &str, len: usize) -> Option<&str> {
(s.len() == len && s.bytes().all(|b| b.is_ascii_digit())).then_some(s)
}
let (year, month_n, day) = match date.len() {
6 => {
let d = digits(date, 6)?;
(
2000 + d[0..2].parse::<i32>().ok()?,
d[2..4].parse::<u8>().ok()?,
d[4..6].parse::<u8>().ok()?,
)
}
8 => {
let d = digits(date, 8)?;
(
d[0..4].parse::<i32>().ok()?,
d[4..6].parse::<u8>().ok()?,
d[6..8].parse::<u8>().ok()?,
)
}
_ => return None,
};
let d = Date::from_calendar_date(year, Month::try_from(month_n).ok()?, day).ok()?;
let (hh, mi, ss) = match time.len() {
4 => {
let t = digits(time, 4)?;
(
t[0..2].parse::<u8>().ok()?,
t[2..4].parse::<u8>().ok()?,
0u8,
)
}
6 => {
let t = digits(time, 6)?;
(
t[0..2].parse::<u8>().ok()?,
t[2..4].parse::<u8>().ok()?,
t[4..6].parse::<u8>().ok()?,
)
}
_ => return None,
};
let t = Time::from_hms(hh, mi, ss).ok()?;
Some(PrimitiveDateTime::new(d, t).assume_offset(UtcOffset::UTC))
}
pub(crate) fn dispatch_message(
segments: Vec<OwnedSegment>,
registry: &crate::registry::ReleaseRegistry,
) -> Result<AnyMessage, Error> {
dispatch_message_on_date(segments, registry, None)
}
pub(crate) fn dispatch_message_on_date(
segments: Vec<OwnedSegment>,
registry: &crate::registry::ReleaseRegistry,
reference_date: Option<time::Date>,
) -> Result<AnyMessage, Error> {
let (message_ref, msg_type_code, assoc_code) = {
let unh = segments
.iter()
.find(|s| s.tag == "UNH")
.ok_or(Error::MissingSegment("UNH"))?;
let message_ref = unh.element_str(0).unwrap_or_default().to_owned();
let msg_type_code = unh
.component_str(1, 0)
.ok_or(Error::MalformedSegment("UNH"))?
.to_owned();
let assoc_code = unh.component_str(1, 4).unwrap_or_default().to_owned();
(message_ref, msg_type_code, assoc_code)
};
let pruefidentifikator: Option<u32> = crate::pid_scan::detect(
&segments,
resolve_pid_source(&msg_type_code, &assoc_code, registry),
);
if matches!(
crate::release::Release::new(&assoc_code).kind(),
crate::release::ReleaseKind::Opaque(_)
) {
#[cfg(feature = "tracing")]
{
let safe_code = sanitize_release_code(&assoc_code);
tracing::warn!(
release = %safe_code,
"unrecognised EDI@Energy release code — validate() will return ProfileNotFound"
);
}
} else {
#[cfg(feature = "tracing")]
tracing::debug!(
message_type = %msg_type_code,
release = %assoc_code,
segment_count = segments.len(),
"parsed EDIFACT message"
);
}
let mut message = dispatch_by_type(
&msg_type_code,
segments,
message_ref,
assoc_code,
pruefidentifikator,
)?;
if let Some(date) = reference_date {
message.set_reference_date(date);
}
Ok(message)
}
#[allow(unused_variables)] #[allow(clippy::too_many_lines)]
fn dispatch_by_type(
msg_type_code: &str,
segments: Vec<OwnedSegment>,
message_ref: String,
assoc_code: String,
pruefidentifikator: Option<u32>,
) -> Result<AnyMessage, Error> {
match msg_type_code {
#[cfg(feature = "utilmd")]
"UTILMD" => Ok(AnyMessage::Utilmd(
crate::messages::utilmd::UtilmdMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "mscons")]
"MSCONS" => Ok(AnyMessage::Mscons(
crate::messages::mscons::MsconsMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "aperak")]
"APERAK" => Ok(AnyMessage::Aperak(
crate::messages::aperak::AperakMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "contrl")]
"CONTRL" => Ok(AnyMessage::Contrl(
crate::messages::contrl::ContrlMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "invoic")]
"INVOIC" => Ok(AnyMessage::Invoic(
crate::messages::invoic::InvoicMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "remadv")]
"REMADV" => Ok(AnyMessage::Remadv(
crate::messages::remadv::RemadvMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "orders")]
"ORDERS" => Ok(AnyMessage::Orders(
crate::messages::orders::OrdersMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "iftsta")]
"IFTSTA" => Ok(AnyMessage::Iftsta(
crate::messages::iftsta::IftstaMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "insrpt")]
"INSRPT" => Ok(AnyMessage::Insrpt(
crate::messages::insrpt::InsrptMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "reqote")]
"REQOTE" => Ok(AnyMessage::Reqote(
crate::messages::reqote::ReqoteMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "partin")]
"PARTIN" => Ok(AnyMessage::Partin(
crate::messages::partin::PartinMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "ordchg")]
"ORDCHG" => Ok(AnyMessage::Ordchg(
crate::messages::ordchg::OrdchgMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "ordrsp")]
"ORDRSP" => Ok(AnyMessage::Ordrsp(
crate::messages::ordrsp::OrdrespMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "quotes")]
"QUOTES" => Ok(AnyMessage::Quotes(
crate::messages::quotes::QuotesMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "comdis")]
"COMDIS" => Ok(AnyMessage::Comdis(
crate::messages::comdis::ComdisMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "pricat")]
"PRICAT" => Ok(AnyMessage::Pricat(
crate::messages::pricat::PricatMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
#[cfg(feature = "utilts")]
"UTILTS" => Ok(AnyMessage::Utilts(
crate::messages::utilts::UtiltsMessage::from_parts(
segments,
message_ref,
assoc_code,
pruefidentifikator,
),
)),
other => {
if let Some(mt) =
MessageType::from_unh_code(other).filter(|mt| !mt.is_feature_enabled())
{
return Err(Error::FeatureNotEnabled {
message_type: other.to_owned(),
feature: mt.feature_name().to_owned(),
});
}
Ok(AnyMessage::Unknown {
message_type_code: other.into(),
release: crate::Release::new(&assoc_code),
message_ref: message_ref.into(),
segments,
})
}
}
}
#[cfg(test)]
mod unb_datetime_tests {
use super::parse_unb_datetime;
use time::macros::datetime;
#[test]
fn parses_both_edifact_widths() {
assert_eq!(
parse_unb_datetime("260101", "0900"),
Some(datetime!(2026-01-01 09:00 UTC))
);
assert_eq!(
parse_unb_datetime("20260101", "090059"),
Some(datetime!(2026-01-01 09:00:59 UTC))
);
}
#[test]
fn non_ascii_and_malformed_values_are_rejected() {
assert_eq!(parse_unb_datetime("aé345", "0900"), None);
assert_eq!(parse_unb_datetime("260101", "ü900"), None);
assert_eq!(parse_unb_datetime("261301", "0900"), None, "month 13");
assert_eq!(parse_unb_datetime("260101", "2500"), None, "hour 25");
assert_eq!(parse_unb_datetime("", ""), None);
}
}