use crate::envelope::{
FunctionalGroupEnvelope, InterchangeEnvelope, MessageEnvelope, ValidatedInterchange,
};
use crate::model::{OwnedElement, OwnedSegment, Segment};
use crate::report::{ValidationIssue, ValidationReport};
use crate::{EdifactError, Writer};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Action {
Rejected,
Acknowledged,
Received,
}
impl Action {
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::Rejected => "4",
Self::Acknowledged => "7",
Self::Received => "8",
}
}
}
impl std::fmt::Display for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.code())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ReportingLevel {
Interchange,
Group,
Message,
Segment,
DataElement,
}
impl ReportingLevel {
#[must_use]
pub const fn tag(self) -> &'static str {
match self {
Self::Interchange => "UCI",
Self::Group => "UCF",
Self::Message => "UCM",
Self::Segment => "UCS",
Self::DataElement => "UCD",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SyntaxError {
SyntaxVersionNotSupported,
NotActualRecipient,
InvalidValue,
Missing,
ValueNotSupportedHere,
NotSupportedHere,
TooManyConstituents,
NoAgreement,
Unspecified,
InvalidAsServiceCharacter,
InvalidCharacters,
InvalidServiceCharacters,
UnknownSender,
TooOld,
TestIndicatorNotSupported,
DuplicateDetected,
ReferencesDoNotMatch,
ControlCountMismatch,
GroupsAndMessagesMixed,
LowerLevelEmpty,
InvalidOccurrenceOutsideMessage,
TooManyRepetitions,
TooManyGroupRepetitions,
InvalidCharacterType,
DataElementTooLong,
DataElementTooShort,
TrailingSeparator,
CharacterSetNotSupported,
EnvelopeFunctionalityNotSupported,
}
impl SyntaxError {
#[must_use]
pub const fn code(self) -> &'static str {
match self {
Self::SyntaxVersionNotSupported => "2",
Self::NotActualRecipient => "7",
Self::InvalidValue => "12",
Self::Missing => "13",
Self::ValueNotSupportedHere => "14",
Self::NotSupportedHere => "15",
Self::TooManyConstituents => "16",
Self::NoAgreement => "17",
Self::Unspecified => "18",
Self::InvalidAsServiceCharacter => "20",
Self::InvalidCharacters => "21",
Self::InvalidServiceCharacters => "22",
Self::UnknownSender => "23",
Self::TooOld => "24",
Self::TestIndicatorNotSupported => "25",
Self::DuplicateDetected => "26",
Self::ReferencesDoNotMatch => "28",
Self::ControlCountMismatch => "29",
Self::GroupsAndMessagesMixed => "30",
Self::LowerLevelEmpty => "32",
Self::InvalidOccurrenceOutsideMessage => "33",
Self::TooManyRepetitions => "35",
Self::TooManyGroupRepetitions => "36",
Self::InvalidCharacterType => "37",
Self::DataElementTooLong => "39",
Self::DataElementTooShort => "40",
Self::TrailingSeparator => "45",
Self::CharacterSetNotSupported => "46",
Self::EnvelopeFunctionalityNotSupported => "47",
}
}
#[must_use]
pub const fn description(self) -> &'static str {
match self {
Self::SyntaxVersionNotSupported => "syntax version or level not supported",
Self::NotActualRecipient => "interchange recipient not actual recipient",
Self::InvalidValue => "invalid value",
Self::Missing => "missing",
Self::ValueNotSupportedHere => "value not supported in this position",
Self::NotSupportedHere => "not supported in this position",
Self::TooManyConstituents => "too many constituents",
Self::NoAgreement => "no agreement",
Self::Unspecified => "unspecified error",
Self::InvalidAsServiceCharacter => "character invalid as service character",
Self::InvalidCharacters => "invalid character(s)",
Self::InvalidServiceCharacters => "invalid service character(s)",
Self::UnknownSender => "unknown interchange sender",
Self::TooOld => "too old",
Self::TestIndicatorNotSupported => "test indicator not supported",
Self::DuplicateDetected => "duplicate detected",
Self::ReferencesDoNotMatch => "references do not match",
Self::ControlCountMismatch => {
"control or octet count does not match number of instances received"
}
Self::GroupsAndMessagesMixed => "groups and messages/packages mixed",
Self::LowerLevelEmpty => "lower level empty",
Self::InvalidOccurrenceOutsideMessage => {
"invalid occurrence outside message, package or group"
}
Self::TooManyRepetitions => "too many repetitions",
Self::TooManyGroupRepetitions => "too many segment group repetitions",
Self::InvalidCharacterType => "invalid type of character(s)",
Self::DataElementTooLong => "data element too long",
Self::DataElementTooShort => "data element too short",
Self::TrailingSeparator => "trailing separator",
Self::CharacterSetNotSupported => "character set not supported",
Self::EnvelopeFunctionalityNotSupported => "envelope functionality not supported",
}
}
#[must_use]
pub const fn permitted_at(self, level: ReportingLevel) -> bool {
use ReportingLevel as L;
match self {
Self::SyntaxVersionNotSupported
| Self::NotActualRecipient
| Self::InvalidAsServiceCharacter
| Self::UnknownSender
| Self::CharacterSetNotSupported => matches!(l_of(level), L::Interchange),
Self::InvalidValue
| Self::Missing
| Self::ValueNotSupportedHere
| Self::NotSupportedHere
| Self::TooManyConstituents
| Self::Unspecified
| Self::InvalidCharacters => true,
Self::NoAgreement | Self::TestIndicatorNotSupported => matches!(
l_of(level),
L::Interchange | L::Group | L::Message | L::Segment
),
Self::InvalidServiceCharacters => true,
Self::TooOld => matches!(l_of(level), L::Interchange | L::Group),
Self::DuplicateDetected
| Self::ReferencesDoNotMatch
| Self::ControlCountMismatch
| Self::GroupsAndMessagesMixed => {
matches!(l_of(level), L::Interchange | L::Group | L::Message)
}
Self::LowerLevelEmpty | Self::InvalidOccurrenceOutsideMessage => {
matches!(l_of(level), L::Interchange | L::Group)
}
Self::TooManyRepetitions => {
matches!(l_of(level), L::Message | L::Segment | L::DataElement)
}
Self::TooManyGroupRepetitions => matches!(l_of(level), L::Segment),
Self::InvalidCharacterType | Self::DataElementTooLong | Self::DataElementTooShort => {
matches!(
l_of(level),
L::Interchange | L::Group | L::Message | L::DataElement
)
}
Self::TrailingSeparator => matches!(
l_of(level),
L::Interchange | L::Group | L::Message | L::Segment
),
Self::EnvelopeFunctionalityNotSupported => matches!(l_of(level), L::Group | L::Message),
}
}
#[must_use]
pub fn for_error(error: &EdifactError) -> Self {
use EdifactError as E;
match error {
E::MessageCountMismatch { .. } | E::SegmentCountMismatch { .. } => {
Self::ControlCountMismatch
}
E::QualifierMismatch { .. } => Self::ReferencesDoNotMatch,
E::DuplicateReference { .. } => Self::DuplicateDetected,
E::MissingRequiredElement { .. }
| E::MissingRequiredComponent { .. }
| E::MissingSegment { .. } => Self::Missing,
E::InvalidCodeValue { .. } | E::InvalidFieldValue { .. } => Self::InvalidValue,
E::InvalidSegmentForMessage { .. } | E::ConditionalRequirementNotMet { .. } => {
Self::NotSupportedHere
}
E::InvalidElementCount { .. } | E::InvalidComponentCount { .. } => {
Self::TooManyConstituents
}
E::UnrecognisedSyntaxIdentifier(_) | E::UnsupportedCharset { .. } => {
Self::CharacterSetNotSupported
}
E::CharacterNotInRepertoire { .. } | E::InvalidText { .. } => Self::InvalidCharacters,
E::InvalidUna | E::InvalidDelimiter { .. } | E::InvalidReleaseSequence { .. } => {
Self::InvalidServiceCharacters
}
E::EmptyInterchange { .. } | E::EmptyMessage { .. } => Self::LowerLevelEmpty,
E::SegmentWithoutDataElements { .. } => Self::Missing,
E::BlankDataElementValue { .. } => Self::InvalidValue,
E::PackageNotSupported { .. } => Self::EnvelopeFunctionalityNotSupported,
E::SegmentTooLong { .. } | E::DataElementTooLong { .. } => Self::DataElementTooLong,
E::DataElementTooShort { .. } => Self::DataElementTooShort,
E::InvalidCharacterType { .. } => Self::InvalidCharacterType,
E::TooManyRepetitions { .. } => Self::TooManyRepetitions,
E::TrailingSeparator { .. } => Self::TrailingSeparator,
E::GroupsAndMessagesMixed { .. } => Self::GroupsAndMessagesMixed,
E::InsignificantCharacters { .. } => Self::InvalidValue,
E::UnexpectedDataToken { .. } | E::InvalidSegmentTag(_) => {
Self::InvalidOccurrenceOutsideMessage
}
_ => Self::Unspecified,
}
}
#[must_use]
pub fn for_issue(issue: &ValidationIssue) -> Self {
match issue.error_code() {
Some("E004" | "E005") => Self::ControlCountMismatch,
Some("E016") => Self::ReferencesDoNotMatch,
Some("E032") => Self::DuplicateDetected,
Some("E008" | "E015" | "E021" | "E046") => Self::Missing,
Some("E014" | "E027" | "E045") => Self::InvalidValue,
Some("E011" | "E017") => Self::NotSupportedHere,
Some("E012" | "E013") => Self::TooManyConstituents,
Some("E031" | "E039") => Self::CharacterSetNotSupported,
Some("E003" | "E038") => Self::InvalidCharacters,
Some("E002" | "E007" | "E019") => Self::InvalidServiceCharacters,
Some("E042" | "E043") => Self::LowerLevelEmpty,
Some("E044") => Self::EnvelopeFunctionalityNotSupported,
Some("E020" | "E049") => Self::DataElementTooLong,
Some("E050") => Self::DataElementTooShort,
Some("E048") => Self::InvalidCharacterType,
Some("E047") => Self::TooManyRepetitions,
Some("E051") => Self::TrailingSeparator,
Some("E052") => Self::GroupsAndMessagesMixed,
Some("E053") => Self::InvalidValue,
Some("E006" | "E028") => Self::InvalidOccurrenceOutsideMessage,
_ => Self::Unspecified,
}
}
}
#[inline]
const fn l_of(level: ReportingLevel) -> ReportingLevel {
level
}
impl std::fmt::Display for SyntaxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.code(), self.description())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Scope {
Group(usize),
Message {
index: usize,
segment_position: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Finding {
scope: Scope,
element_position: Option<u32>,
component_position: Option<u32>,
rejects: bool,
error: SyntaxError,
}
impl Finding {
fn level(&self) -> ReportingLevel {
if let Scope::Group(_) = self.scope {
return ReportingLevel::Group;
}
if self.element_position.is_some() && self.error.permitted_at(ReportingLevel::DataElement) {
return ReportingLevel::DataElement;
}
if self.error.permitted_at(ReportingLevel::Segment) {
return ReportingLevel::Segment;
}
ReportingLevel::Message
}
fn message_index(&self) -> Option<usize> {
match self.scope {
Scope::Message { index, .. } => Some(index),
Scope::Group(_) => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Contrl {
control_ref: String,
sender: String,
sender_qualifier: String,
recipient: String,
recipient_qualifier: String,
action: Action,
interchange_error: Option<SyntaxError>,
message_ref: Option<String>,
messages: Vec<(usize, MessageEnvelope, Action)>,
groups: Vec<GroupReport>,
findings: Vec<Finding>,
}
#[derive(Debug, Clone)]
struct GroupReport {
envelope: FunctionalGroupEnvelope,
action: Action,
error: Option<SyntaxError>,
messages: Vec<(usize, MessageEnvelope, Action)>,
}
impl Contrl {
fn base(interchange: &InterchangeEnvelope, action: Action) -> Self {
Self {
control_ref: interchange.control_ref.clone(),
sender: interchange.sender_id.clone(),
sender_qualifier: interchange.sender_qualifier.clone(),
recipient: interchange.recipient_id.clone(),
recipient_qualifier: interchange.recipient_qualifier.clone(),
action,
interchange_error: None,
message_ref: None,
messages: Vec::new(),
groups: Vec::new(),
findings: Vec::new(),
}
}
#[must_use]
pub fn receipt(interchange: &InterchangeEnvelope) -> Self {
Self::base(interchange, Action::Received)
}
#[must_use]
pub fn acknowledgement(subject: &ValidatedInterchange) -> Self {
Self::base(&subject.interchange, Action::Acknowledged)
}
#[must_use]
pub fn from_report(
subject: &ValidatedInterchange,
segments: &[Segment<'_>],
report: &ValidationReport,
) -> Self {
let mut contrl = Self::base(&subject.interchange, Action::Acknowledged);
let messages = message_boundaries(segments);
let groups = group_boundaries(segments);
for (issue, rejects) in report
.errors()
.iter()
.map(|i| (i, true))
.chain(report.warnings().iter().map(|i| (i, false)))
{
let error = SyntaxError::for_issue(issue);
let located = issue
.span
.and_then(|span| locate(segments, &messages, &groups, span.start));
let Some(scope) = located else {
if rejects {
contrl.action = Action::Rejected;
}
if contrl.interchange_error.is_none()
&& error.permitted_at(ReportingLevel::Interchange)
{
contrl.interchange_error = Some(error);
}
continue;
};
contrl.findings.push(Finding {
scope,
element_position: issue.element_index.map(|i| u32::from(i) + 2),
component_position: issue.component_index.map(|i| u32::from(i) + 1),
rejects,
error,
});
}
if contrl.action == Action::Rejected {
contrl.findings.clear();
return contrl;
}
if subject.functional_groups.is_empty() {
contrl.messages = contrl.reported_messages(subject.messages.iter().enumerate());
} else {
contrl.build_group_reports(subject);
}
contrl
}
fn reported_messages<'m>(
&self,
candidates: impl Iterator<Item = (usize, &'m MessageEnvelope)>,
) -> Vec<(usize, MessageEnvelope, Action)> {
let mut out = Vec::new();
for (index, message) in candidates {
let mut has_finding = false;
let mut rejected = false;
for finding in &self.findings {
if finding.message_index() == Some(index) {
has_finding = true;
rejected |= finding.rejects;
}
}
if !has_finding {
continue;
}
out.push((
index,
message.clone(),
if rejected {
Action::Rejected
} else {
Action::Acknowledged
},
));
}
out
}
fn build_group_reports(&mut self, subject: &ValidatedInterchange) {
let mut flat = 0usize;
let mut reports = Vec::new();
for (group_index, group) in subject.functional_groups.iter().enumerate() {
let start = flat;
flat += group.messages.len();
let group_fault = self
.findings
.iter()
.find(|f| f.scope == Scope::Group(group_index));
let rejected = group_fault.is_some_and(|f| f.rejects);
let messages = if rejected {
Vec::new()
} else {
self.reported_messages(
group
.messages
.iter()
.enumerate()
.map(|(offset, message)| (start + offset, message)),
)
};
if group_fault.is_none() && messages.is_empty() {
continue; }
reports.push(GroupReport {
envelope: group.clone(),
action: if rejected {
Action::Rejected
} else {
Action::Acknowledged
},
error: group_fault
.map(|f| f.error)
.filter(|e| e.permitted_at(ReportingLevel::Group)),
messages,
});
}
self.groups = reports;
}
#[must_use]
pub fn with_message_reference(mut self, reference: impl Into<String>) -> Self {
self.message_ref = Some(reference.into());
self
}
#[must_use]
pub fn with_interchange_error(mut self, error: SyntaxError) -> Self {
if error.permitted_at(ReportingLevel::Interchange) {
self.interchange_error = Some(error);
}
self
}
#[must_use]
pub const fn action(&self) -> Action {
self.action
}
#[must_use]
pub fn message_reference(&self) -> &str {
self.message_ref.as_deref().unwrap_or(&self.control_ref)
}
#[must_use]
pub fn segments(&self) -> Vec<OwnedSegment> {
let mut out = Vec::new();
let reference = self.message_reference().to_owned();
out.push(OwnedSegment::new(
"UNH",
vec![
OwnedElement::of(&[reference.as_str()]),
OwnedElement::of(&["CONTRL", "4", "1", "UN"]),
],
));
let mut uci = vec![
OwnedElement::of(&[self.control_ref.as_str()]),
party(&self.sender, &self.sender_qualifier),
party(&self.recipient, &self.recipient_qualifier),
OwnedElement::of(&[self.action.code()]),
];
if let Some(error) = self.interchange_error {
uci.push(OwnedElement::of(&[error.code()]));
}
out.push(OwnedSegment::new("UCI", uci));
for (index, message, action) in &self.messages {
out.extend(self.message_report(*index, message, *action));
}
for group in &self.groups {
let mut ucf = vec![
OwnedElement::of(&[group.envelope.group_ref.as_str()]),
party(
&group.envelope.app_sender,
&group.envelope.app_sender_qualifier,
),
party(
&group.envelope.app_recipient,
&group.envelope.app_recipient_qualifier,
),
OwnedElement::of(&[group.action.code()]),
];
if let Some(error) = group.error {
ucf.push(OwnedElement::of(&[error.code()]));
}
out.push(OwnedSegment::new("UCF", ucf));
for (index, message, action) in &group.messages {
out.extend(self.message_report(*index, message, *action));
}
}
let count = (out.len() + 1).to_string();
out.push(OwnedSegment::new(
"UNT",
vec![
OwnedElement::of(&[count.as_str()]),
OwnedElement::of(&[reference.as_str()]),
],
));
out
}
fn message_report(
&self,
index: usize,
message: &MessageEnvelope,
action: Action,
) -> Vec<OwnedSegment> {
let mut ucm = vec![
OwnedElement::of(&[message.message_ref.as_str()]),
OwnedElement::of(&[
message.message_type.as_str(),
message.version.as_str(),
message.release.as_str(),
message.controlling_agency.as_str(),
]),
OwnedElement::of(&[action.code()]),
];
if let Some(finding) = self
.findings
.iter()
.find(|f| f.message_index() == Some(index) && f.level() == ReportingLevel::Message)
{
ucm.push(OwnedElement::of(&[finding.error.code()]));
}
let mut out = vec![OwnedSegment::new("UCM", ucm)];
out.extend(self.segment_reports(index));
out
}
fn segment_reports(&self, message_index: usize) -> Vec<OwnedSegment> {
let mut out = Vec::new();
let mut reported_positions: Vec<u32> = Vec::new();
for finding in self
.findings
.iter()
.filter(|f| f.message_index() == Some(message_index))
{
let level = finding.level();
let Scope::Message {
segment_position: position,
..
} = finding.scope
else {
continue;
};
if level == ReportingLevel::Message {
continue;
}
if !reported_positions.contains(&position) {
reported_positions.push(position);
let position_text = position.to_string();
let mut ucs = vec![OwnedElement::of(&[position_text.as_str()])];
if level == ReportingLevel::Segment {
ucs.push(OwnedElement::of(&[finding.error.code()]));
}
out.push(OwnedSegment::new("UCS", ucs));
}
if level == ReportingLevel::DataElement {
let element_text = finding
.element_position
.expect("DataElement level implies a known element position")
.to_string();
let mut identification = vec![element_text];
if let Some(component) = finding.component_position {
identification.push(component.to_string());
}
out.push(OwnedSegment::new(
"UCD",
vec![
OwnedElement::of(&[finding.error.code()]),
OwnedElement::of(&identification),
],
));
}
}
out
}
pub fn to_bytes(&self) -> Result<Vec<u8>, EdifactError> {
crate::segments_to_bytes_owned(&self.segments())
}
pub fn to_edifact_string(&self) -> Result<String, EdifactError> {
String::from_utf8(self.to_bytes()?).map_err(|_| EdifactError::InvalidUtf8)
}
pub fn to_interchange_bytes(
&self,
syntax_identifier: &str,
syntax_version: &str,
date: &str,
time: &str,
control_reference: &str,
) -> Result<Vec<u8>, EdifactError> {
let mut writer = Writer::new(Vec::new());
writer.begin_interchange(
syntax_identifier,
syntax_version,
&self.recipient,
&self.sender,
date,
time,
control_reference,
)?;
for segment in self.segments() {
writer.write_segment(&segment.as_borrowed())?;
}
writer.end_interchange(1, control_reference)?;
writer.finish()
}
pub fn to_interchange_string(
&self,
syntax_identifier: &str,
syntax_version: &str,
date: &str,
time: &str,
control_reference: &str,
) -> Result<String, EdifactError> {
let bytes = self.to_interchange_bytes(
syntax_identifier,
syntax_version,
date,
time,
control_reference,
)?;
String::from_utf8(bytes).map_err(|_| EdifactError::InvalidUtf8)
}
}
fn party(id: &str, qualifier: &str) -> OwnedElement {
if qualifier.is_empty() {
OwnedElement::of(&[id])
} else {
OwnedElement::of(&[id, qualifier])
}
}
fn message_boundaries(segments: &[Segment<'_>]) -> Vec<(usize, usize)> {
spans_between(segments, "UNH", "UNT")
}
fn group_boundaries(segments: &[Segment<'_>]) -> Vec<(usize, usize)> {
spans_between(segments, "UNG", "UNE")
}
fn spans_between(segments: &[Segment<'_>], open: &str, close: &str) -> Vec<(usize, usize)> {
let mut out = Vec::new();
let mut start: Option<usize> = None;
for (index, segment) in segments.iter().enumerate() {
if segment.tag == open {
start = Some(index);
} else if segment.tag == close {
if let Some(from) = start.take() {
out.push((from, index));
}
}
}
out
}
fn locate(
segments: &[Segment<'_>],
messages: &[(usize, usize)],
groups: &[(usize, usize)],
offset: usize,
) -> Option<Scope> {
let index = match segments.binary_search_by(|segment| segment.span.start.cmp(&offset)) {
Ok(exact) => exact,
Err(0) => return None,
Err(next) => next - 1,
};
if let Some((message_index, (start, _))) = messages
.iter()
.enumerate()
.find(|(_, (start, end))| (*start..=*end).contains(&index))
{
return u32::try_from(index - start + 1)
.ok()
.map(|segment_position| Scope::Message {
index: message_index,
segment_position,
});
}
groups
.iter()
.position(|(start, end)| (*start..=*end).contains(&index))
.map(Scope::Group)
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(input: &[u8]) -> Vec<OwnedSegment> {
crate::from_bytes_owned(input)
.collect::<Result<Vec<_>, _>>()
.expect("parse")
}
#[test]
fn annex_a_permits_a_code_only_where_the_table_says() {
assert!(SyntaxError::NotActualRecipient.permitted_at(ReportingLevel::Interchange));
assert!(!SyntaxError::NotActualRecipient.permitted_at(ReportingLevel::Group));
assert!(SyntaxError::TooManyGroupRepetitions.permitted_at(ReportingLevel::Segment));
assert!(!SyntaxError::TooManyGroupRepetitions.permitted_at(ReportingLevel::DataElement));
assert!(SyntaxError::LowerLevelEmpty.permitted_at(ReportingLevel::Group));
assert!(!SyntaxError::LowerLevelEmpty.permitted_at(ReportingLevel::Message));
assert!(SyntaxError::EnvelopeFunctionalityNotSupported.permitted_at(ReportingLevel::Group));
assert!(
!SyntaxError::EnvelopeFunctionalityNotSupported
.permitted_at(ReportingLevel::Interchange)
);
for level in [
ReportingLevel::Interchange,
ReportingLevel::Group,
ReportingLevel::Message,
ReportingLevel::Segment,
ReportingLevel::DataElement,
] {
assert!(SyntaxError::InvalidValue.permitted_at(level), "{level:?}");
}
}
#[test]
fn every_code_is_permitted_somewhere() {
for error in ALL_ERRORS {
assert!(
[
ReportingLevel::Interchange,
ReportingLevel::Group,
ReportingLevel::Message,
ReportingLevel::Segment,
ReportingLevel::DataElement,
]
.iter()
.any(|level| error.permitted_at(*level)),
"{error:?} is permitted nowhere"
);
}
}
#[test]
fn codes_are_unique() {
let mut codes: Vec<&str> = ALL_ERRORS.iter().map(|e| e.code()).collect();
codes.sort_unstable();
let before = codes.len();
codes.dedup();
assert_eq!(before, codes.len(), "duplicate DE 0085 code");
}
const ALL_ERRORS: &[SyntaxError] = &[
SyntaxError::SyntaxVersionNotSupported,
SyntaxError::NotActualRecipient,
SyntaxError::InvalidValue,
SyntaxError::Missing,
SyntaxError::ValueNotSupportedHere,
SyntaxError::NotSupportedHere,
SyntaxError::TooManyConstituents,
SyntaxError::NoAgreement,
SyntaxError::Unspecified,
SyntaxError::InvalidAsServiceCharacter,
SyntaxError::InvalidCharacters,
SyntaxError::InvalidServiceCharacters,
SyntaxError::UnknownSender,
SyntaxError::TooOld,
SyntaxError::TestIndicatorNotSupported,
SyntaxError::DuplicateDetected,
SyntaxError::ReferencesDoNotMatch,
SyntaxError::ControlCountMismatch,
SyntaxError::GroupsAndMessagesMixed,
SyntaxError::LowerLevelEmpty,
SyntaxError::InvalidOccurrenceOutsideMessage,
SyntaxError::TooManyRepetitions,
SyntaxError::TooManyGroupRepetitions,
SyntaxError::InvalidCharacterType,
SyntaxError::DataElementTooLong,
SyntaxError::DataElementTooShort,
SyntaxError::TrailingSeparator,
SyntaxError::CharacterSetNotSupported,
SyntaxError::EnvelopeFunctionalityNotSupported,
];
#[test]
fn an_acknowledgement_validates_against_the_shipped_layouts() {
let raw = b"UNB+UNOC:3+SENDER:14+RECEIVER:14+260101:0900+IC4711'\
UNH+MSG1+ORDERS:D:96A:UN'BGM+220+PO-1+9'UNT+3+MSG1'\
UNZ+1+IC4711'";
let owned = parse(raw);
let segments: Vec<_> = owned.iter().map(OwnedSegment::as_borrowed).collect();
let validated = crate::validate_envelope(&segments).expect("valid subject");
let contrl = Contrl::acknowledgement(&validated).with_message_reference("ACK1");
let wire = contrl.to_edifact_string().expect("render");
assert_eq!(
wire,
"UNH+ACK1+CONTRL:4:1:UN'UCI+IC4711+SENDER:14+RECEIVER:14+7'UNT+3+ACK1'"
);
let reparsed = parse(wire.as_bytes());
let borrowed: Vec<_> = reparsed.iter().map(OwnedSegment::as_borrowed).collect();
let validator = crate::DirectoryValidator::new(
"iso-9735-4",
crate::service::lookup,
|_, _| true,
|_, _| None,
|_, _| None,
None,
);
let report = crate::ValidationContext::builder()
.with_validator(crate::ValidationLayer::Structure, validator)
.build()
.validate_lenient(&borrowed);
assert!(!report.has_errors(), "{:#?}", report.errors());
}
#[test]
fn a_receipt_carries_action_8_and_nothing_else() {
let raw =
b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'UNZ+1+IC1'";
let owned = parse(raw);
let segments: Vec<_> = owned.iter().map(OwnedSegment::as_borrowed).collect();
let validated = crate::validate_envelope(&segments).expect("valid subject");
let wire = Contrl::receipt(&validated.interchange)
.to_edifact_string()
.expect("render");
assert!(wire.contains("UCI+IC1+S+R+8'"), "{wire}");
assert_eq!(
Contrl::receipt(&validated.interchange).action(),
Action::Received
);
}
#[test]
fn the_message_reference_defaults_to_the_subject_control_reference() {
let raw = b"UNB+UNOC:3+S+R+260101:0900+IC-42'UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'UNZ+1+IC-42'";
let owned = parse(raw);
let segments: Vec<_> = owned.iter().map(OwnedSegment::as_borrowed).collect();
let validated = crate::validate_envelope(&segments).expect("valid subject");
assert_eq!(
Contrl::acknowledgement(&validated).message_reference(),
"IC-42"
);
}
#[test]
fn a_forbidden_interchange_level_code_is_not_recorded() {
let raw =
b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'UNZ+1+IC1'";
let owned = parse(raw);
let segments: Vec<_> = owned.iter().map(OwnedSegment::as_borrowed).collect();
let validated = crate::validate_envelope(&segments).expect("valid subject");
let wire = Contrl::acknowledgement(&validated)
.with_interchange_error(SyntaxError::TooManyGroupRepetitions)
.to_edifact_string()
.expect("render");
assert!(wire.contains("UCI+IC1+S+R+7'"), "{wire}");
}
fn report_for(raw: &[u8]) -> Contrl {
let owned = parse(raw);
let segments: Vec<_> = owned.iter().map(OwnedSegment::as_borrowed).collect();
let validated = crate::validate_envelope_lenient(&segments)
.interchange
.expect("subject must be structurally interpretable");
let report = crate::ValidationContext::builder()
.with_envelope_validation()
.with_syntax_validation()
.build()
.validate_lenient(&segments);
Contrl::from_report(&validated, &segments, &report)
}
#[test]
fn a_data_element_fault_is_reported_at_the_ucd_level() {
let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
UNH+M1+ORDERS:D:96A:UN'FTX+ 'UNT+3+M1'\
UNZ+1+IC1'";
let contrl = report_for(raw);
assert_eq!(contrl.action(), Action::Acknowledged);
let wire = contrl.to_edifact_string().expect("render");
assert!(wire.contains("UCM+M1+ORDERS:D:96A:UN+7'"), "{wire}");
assert!(wire.contains("UCS+2'"), "{wire}");
assert!(wire.contains("UCD+12+2:1'"), "{wire}");
}
#[test]
fn a_rejected_message_is_named_by_its_ucm() {
let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
UNH+M1+ORDERS:D:96A:UN'BGM+221'UNT+3+M1'\
UNZ+2+IC1'";
let contrl = report_for(raw);
assert_eq!(contrl.action(), Action::Acknowledged);
let wire = contrl.to_edifact_string().expect("render");
assert_eq!(
wire,
"UNH+IC1+CONTRL:4:1:UN'\
UCI+IC1+S+R+7'\
UCM+M1+ORDERS:D:96A:UN+4+26'\
UNT+4+IC1'"
);
}
#[test]
fn an_envelope_fault_rejects_the_interchange_and_emits_no_ucm() {
let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
UNZ+1+IC-OTHER'";
let contrl = report_for(raw);
assert_eq!(contrl.action(), Action::Rejected);
let wire = contrl.to_edifact_string().expect("render");
assert_eq!(wire, "UNH+IC1+CONTRL:4:1:UN'UCI+IC1+S+R+4+28'UNT+3+IC1'");
}
#[test]
fn a_count_mismatch_is_reported_on_the_message_that_got_it_wrong() {
let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+9+M1'\
UNZ+1+IC1'";
let contrl = report_for(raw);
assert_eq!(contrl.action(), Action::Acknowledged);
let wire = contrl.to_edifact_string().expect("render");
assert_eq!(
wire,
"UNH+IC1+CONTRL:4:1:UN'\
UCI+IC1+S+R+7'\
UCM+M1+ORDERS:D:96A:UN+4+29'\
UNT+4+IC1'"
);
}
#[test]
fn a_finding_lands_at_the_lowest_level_annex_a_permits() {
let duplicate = Finding {
scope: Scope::Message {
index: 0,
segment_position: 1,
},
element_position: Some(2),
component_position: None,
rejects: true,
error: SyntaxError::DuplicateDetected,
};
assert_eq!(duplicate.level(), ReportingLevel::Message);
let invalid = Finding {
error: SyntaxError::InvalidValue,
..duplicate.clone()
};
assert_eq!(invalid.level(), ReportingLevel::DataElement);
let segment_only = Finding {
element_position: None,
..invalid.clone()
};
assert_eq!(segment_only.level(), ReportingLevel::Segment);
let group_repetitions = Finding {
error: SyntaxError::TooManyGroupRepetitions,
..duplicate
};
assert_eq!(group_repetitions.level(), ReportingLevel::Segment);
}
#[test]
fn a_generated_contrl_reparses_and_counts_its_own_segments() {
let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
UNH+M1+ORDERS:D:96A:UN'FTX+ 'UNT+3+M1'\
UNZ+1+IC1'";
let contrl = report_for(raw);
let wire = contrl
.to_interchange_string("UNOC", "3", "260101", "0930", "ACK-1")
.expect("render");
let segments: Vec<_> = crate::from_bytes(wire.as_bytes())
.collect::<Result<Vec<_>, _>>()
.expect("generated CONTRL must reparse");
let validated =
crate::validate_envelope(&segments).expect("generated CONTRL must validate");
assert_eq!(validated.messages.len(), 1);
assert_eq!(validated.messages[0].message_type, "CONTRL");
assert_eq!(validated.messages[0].version, "4");
assert_eq!(validated.messages[0].release, "1");
assert_eq!(
validated.messages[0].declared_segment_count,
validated.messages[0].actual_segment_count
);
}
#[test]
fn a_grouped_interchange_is_reported_through_ucf() {
let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
UNG+ORDERS+SND:14+RCV:14+260101:0900+GRP1+UN+D:96A'\
UNH+M1+ORDERS:D:96A:UN'FTX+ 'UNT+3+M1'\
UNE+1+GRP1'\
UNZ+1+IC1'";
let contrl = report_for(raw);
assert_eq!(contrl.action(), Action::Acknowledged);
let wire = contrl.to_edifact_string().expect("render");
assert_eq!(
wire,
"UNH+IC1+CONTRL:4:1:UN'\
UCI+IC1+S+R+7'\
UCF+GRP1+SND:14+RCV:14+7'\
UCM+M1+ORDERS:D:96A:UN+7'\
UCS+2'\
UCD+12+2:1'\
UNT+7+IC1'"
);
}
#[test]
fn a_group_envelope_fault_rejects_only_that_group() {
let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
UNG+ORDERS+SND+RCV+260101:0900+GRP1+UN+D:96A'\
UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
UNE+1+GRP-OTHER'\
UNZ+1+IC1'";
let contrl = report_for(raw);
assert_eq!(contrl.action(), Action::Acknowledged);
let wire = contrl.to_edifact_string().expect("render");
assert_eq!(
wire,
"UNH+IC1+CONTRL:4:1:UN'\
UCI+IC1+S+R+7'\
UCF+GRP1+SND+RCV+4+28'\
UNT+4+IC1'"
);
}
#[test]
fn a_clean_grouped_interchange_needs_no_ucf() {
let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
UNG+ORDERS+SND+RCV+260101:0900+GRP1+UN+D:96A'\
UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
UNE+1+GRP1'\
UNZ+1+IC1'";
let contrl = report_for(raw);
assert_eq!(
contrl.to_edifact_string().expect("render"),
"UNH+IC1+CONTRL:4:1:UN'UCI+IC1+S+R+7'UNT+3+IC1'"
);
}
#[test]
fn error_mapping_picks_the_narrowest_annex_a_code() {
use EdifactError as E;
let cases: [(EdifactError, SyntaxError); 6] = [
(
E::MessageCountMismatch {
expected: 1,
actual: 2,
},
SyntaxError::ControlCountMismatch,
),
(
E::DuplicateReference {
tag: "UNH".to_owned(),
reference: "1".to_owned(),
span: crate::Span::new(0, 1),
},
SyntaxError::DuplicateDetected,
),
(
E::UnsupportedCharset {
syntax_identifier: "UNOX".to_owned(),
},
SyntaxError::CharacterSetNotSupported,
),
(E::InvalidUna, SyntaxError::InvalidServiceCharacters),
(
E::EmptyInterchange {
control_ref: "IC1".to_owned(),
},
SyntaxError::LowerLevelEmpty,
),
(
E::PackageNotSupported {
tag: "UNO".to_owned(),
span: crate::Span::new(0, 1),
},
SyntaxError::EnvelopeFunctionalityNotSupported,
),
];
for (error, expected) in cases {
assert_eq!(SyntaxError::for_error(&error), expected, "{error:?}");
}
}
}