1use crate::envelope::{
61 FunctionalGroupEnvelope, InterchangeEnvelope, MessageEnvelope, ValidatedInterchange,
62};
63use crate::model::{OwnedElement, OwnedSegment, Segment};
64use crate::report::{ValidationIssue, ValidationReport};
65use crate::{EdifactError, Writer};
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74#[non_exhaustive]
75pub enum Action {
76 Rejected,
78 Acknowledged,
81 Received,
83}
84
85impl Action {
86 #[must_use]
88 pub const fn code(self) -> &'static str {
89 match self {
90 Self::Rejected => "4",
91 Self::Acknowledged => "7",
92 Self::Received => "8",
93 }
94 }
95}
96
97impl std::fmt::Display for Action {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 f.write_str(self.code())
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110#[non_exhaustive]
111pub enum ReportingLevel {
112 Interchange,
114 Group,
116 Message,
118 Segment,
120 DataElement,
122}
123
124impl ReportingLevel {
125 #[must_use]
127 pub const fn tag(self) -> &'static str {
128 match self {
129 Self::Interchange => "UCI",
130 Self::Group => "UCF",
131 Self::Message => "UCM",
132 Self::Segment => "UCS",
133 Self::DataElement => "UCD",
134 }
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
148#[non_exhaustive]
149pub enum SyntaxError {
150 SyntaxVersionNotSupported,
152 NotActualRecipient,
154 InvalidValue,
156 Missing,
158 ValueNotSupportedHere,
160 NotSupportedHere,
162 TooManyConstituents,
164 NoAgreement,
166 Unspecified,
168 InvalidAsServiceCharacter,
170 InvalidCharacters,
172 InvalidServiceCharacters,
174 UnknownSender,
176 TooOld,
178 TestIndicatorNotSupported,
180 DuplicateDetected,
182 ReferencesDoNotMatch,
184 ControlCountMismatch,
186 GroupsAndMessagesMixed,
188 LowerLevelEmpty,
190 InvalidOccurrenceOutsideMessage,
192 TooManyRepetitions,
194 TooManyGroupRepetitions,
196 InvalidCharacterType,
198 DataElementTooLong,
200 DataElementTooShort,
202 TrailingSeparator,
204 CharacterSetNotSupported,
206 EnvelopeFunctionalityNotSupported,
208}
209
210impl SyntaxError {
211 #[must_use]
213 pub const fn code(self) -> &'static str {
214 match self {
215 Self::SyntaxVersionNotSupported => "2",
216 Self::NotActualRecipient => "7",
217 Self::InvalidValue => "12",
218 Self::Missing => "13",
219 Self::ValueNotSupportedHere => "14",
220 Self::NotSupportedHere => "15",
221 Self::TooManyConstituents => "16",
222 Self::NoAgreement => "17",
223 Self::Unspecified => "18",
224 Self::InvalidAsServiceCharacter => "20",
225 Self::InvalidCharacters => "21",
226 Self::InvalidServiceCharacters => "22",
227 Self::UnknownSender => "23",
228 Self::TooOld => "24",
229 Self::TestIndicatorNotSupported => "25",
230 Self::DuplicateDetected => "26",
231 Self::ReferencesDoNotMatch => "28",
232 Self::ControlCountMismatch => "29",
233 Self::GroupsAndMessagesMixed => "30",
234 Self::LowerLevelEmpty => "32",
235 Self::InvalidOccurrenceOutsideMessage => "33",
236 Self::TooManyRepetitions => "35",
237 Self::TooManyGroupRepetitions => "36",
238 Self::InvalidCharacterType => "37",
239 Self::DataElementTooLong => "39",
240 Self::DataElementTooShort => "40",
241 Self::TrailingSeparator => "45",
242 Self::CharacterSetNotSupported => "46",
243 Self::EnvelopeFunctionalityNotSupported => "47",
244 }
245 }
246
247 #[must_use]
249 pub const fn description(self) -> &'static str {
250 match self {
251 Self::SyntaxVersionNotSupported => "syntax version or level not supported",
252 Self::NotActualRecipient => "interchange recipient not actual recipient",
253 Self::InvalidValue => "invalid value",
254 Self::Missing => "missing",
255 Self::ValueNotSupportedHere => "value not supported in this position",
256 Self::NotSupportedHere => "not supported in this position",
257 Self::TooManyConstituents => "too many constituents",
258 Self::NoAgreement => "no agreement",
259 Self::Unspecified => "unspecified error",
260 Self::InvalidAsServiceCharacter => "character invalid as service character",
261 Self::InvalidCharacters => "invalid character(s)",
262 Self::InvalidServiceCharacters => "invalid service character(s)",
263 Self::UnknownSender => "unknown interchange sender",
264 Self::TooOld => "too old",
265 Self::TestIndicatorNotSupported => "test indicator not supported",
266 Self::DuplicateDetected => "duplicate detected",
267 Self::ReferencesDoNotMatch => "references do not match",
268 Self::ControlCountMismatch => {
269 "control or octet count does not match number of instances received"
270 }
271 Self::GroupsAndMessagesMixed => "groups and messages/packages mixed",
272 Self::LowerLevelEmpty => "lower level empty",
273 Self::InvalidOccurrenceOutsideMessage => {
274 "invalid occurrence outside message, package or group"
275 }
276 Self::TooManyRepetitions => "too many repetitions",
277 Self::TooManyGroupRepetitions => "too many segment group repetitions",
278 Self::InvalidCharacterType => "invalid type of character(s)",
279 Self::DataElementTooLong => "data element too long",
280 Self::DataElementTooShort => "data element too short",
281 Self::TrailingSeparator => "trailing separator",
282 Self::CharacterSetNotSupported => "character set not supported",
283 Self::EnvelopeFunctionalityNotSupported => "envelope functionality not supported",
284 }
285 }
286
287 #[must_use]
303 pub const fn permitted_at(self, level: ReportingLevel) -> bool {
304 use ReportingLevel as L;
305 match self {
306 Self::SyntaxVersionNotSupported
308 | Self::NotActualRecipient
309 | Self::InvalidAsServiceCharacter
310 | Self::UnknownSender
311 | Self::CharacterSetNotSupported => matches!(l_of(level), L::Interchange),
312 Self::InvalidValue
314 | Self::Missing
315 | Self::ValueNotSupportedHere
316 | Self::NotSupportedHere
317 | Self::TooManyConstituents
318 | Self::Unspecified
319 | Self::InvalidCharacters => true,
320 Self::NoAgreement | Self::TestIndicatorNotSupported => matches!(
322 l_of(level),
323 L::Interchange | L::Group | L::Message | L::Segment
324 ),
325 Self::InvalidServiceCharacters => true,
326 Self::TooOld => matches!(l_of(level), L::Interchange | L::Group),
327 Self::DuplicateDetected
328 | Self::ReferencesDoNotMatch
329 | Self::ControlCountMismatch
330 | Self::GroupsAndMessagesMixed => {
331 matches!(l_of(level), L::Interchange | L::Group | L::Message)
332 }
333 Self::LowerLevelEmpty | Self::InvalidOccurrenceOutsideMessage => {
334 matches!(l_of(level), L::Interchange | L::Group)
335 }
336 Self::TooManyRepetitions => {
337 matches!(l_of(level), L::Message | L::Segment | L::DataElement)
338 }
339 Self::TooManyGroupRepetitions => matches!(l_of(level), L::Segment),
340 Self::InvalidCharacterType | Self::DataElementTooLong | Self::DataElementTooShort => {
341 matches!(
342 l_of(level),
343 L::Interchange | L::Group | L::Message | L::DataElement
344 )
345 }
346 Self::TrailingSeparator => matches!(
347 l_of(level),
348 L::Interchange | L::Group | L::Message | L::Segment
349 ),
350 Self::EnvelopeFunctionalityNotSupported => matches!(l_of(level), L::Group | L::Message),
351 }
352 }
353
354 #[must_use]
373 pub fn for_error(error: &EdifactError) -> Self {
374 use EdifactError as E;
375 match error {
376 E::MessageCountMismatch { .. } | E::SegmentCountMismatch { .. } => {
377 Self::ControlCountMismatch
378 }
379 E::QualifierMismatch { .. } => Self::ReferencesDoNotMatch,
380 E::DuplicateReference { .. } => Self::DuplicateDetected,
381 E::MissingRequiredElement { .. }
382 | E::MissingRequiredComponent { .. }
383 | E::MissingSegment { .. } => Self::Missing,
384 E::InvalidCodeValue { .. } | E::InvalidFieldValue { .. } => Self::InvalidValue,
385 E::InvalidSegmentForMessage { .. } | E::ConditionalRequirementNotMet { .. } => {
386 Self::NotSupportedHere
387 }
388 E::InvalidElementCount { .. } | E::InvalidComponentCount { .. } => {
389 Self::TooManyConstituents
390 }
391 E::UnrecognisedSyntaxIdentifier(_) | E::UnsupportedCharset { .. } => {
392 Self::CharacterSetNotSupported
393 }
394 E::CharacterNotInRepertoire { .. } | E::InvalidText { .. } => Self::InvalidCharacters,
395 E::InvalidUna | E::InvalidDelimiter { .. } | E::InvalidReleaseSequence { .. } => {
396 Self::InvalidServiceCharacters
397 }
398 E::EmptyInterchange { .. } | E::EmptyMessage { .. } => Self::LowerLevelEmpty,
399 E::SegmentWithoutDataElements { .. } => Self::Missing,
400 E::BlankDataElementValue { .. } => Self::InvalidValue,
401 E::PackageNotSupported { .. } => Self::EnvelopeFunctionalityNotSupported,
402 E::SegmentTooLong { .. } | E::DataElementTooLong { .. } => Self::DataElementTooLong,
403 E::DataElementTooShort { .. } => Self::DataElementTooShort,
404 E::InvalidCharacterType { .. } => Self::InvalidCharacterType,
405 E::TooManyRepetitions { .. } => Self::TooManyRepetitions,
406 E::TrailingSeparator { .. } => Self::TrailingSeparator,
407 E::GroupsAndMessagesMixed { .. } => Self::GroupsAndMessagesMixed,
408 E::InsignificantCharacters { .. } => Self::InvalidValue,
409 E::UnexpectedDataToken { .. } | E::InvalidSegmentTag(_) => {
410 Self::InvalidOccurrenceOutsideMessage
411 }
412 _ => Self::Unspecified,
413 }
414 }
415
416 #[must_use]
424 pub fn for_issue(issue: &ValidationIssue) -> Self {
425 match issue.error_code() {
426 Some("E004" | "E005") => Self::ControlCountMismatch,
427 Some("E016") => Self::ReferencesDoNotMatch,
428 Some("E032") => Self::DuplicateDetected,
429 Some("E008" | "E015" | "E021" | "E046") => Self::Missing,
430 Some("E014" | "E027" | "E045") => Self::InvalidValue,
431 Some("E011" | "E017") => Self::NotSupportedHere,
432 Some("E012" | "E013") => Self::TooManyConstituents,
433 Some("E031" | "E039") => Self::CharacterSetNotSupported,
434 Some("E003" | "E038") => Self::InvalidCharacters,
435 Some("E002" | "E007" | "E019") => Self::InvalidServiceCharacters,
436 Some("E042" | "E043") => Self::LowerLevelEmpty,
437 Some("E044") => Self::EnvelopeFunctionalityNotSupported,
438 Some("E020" | "E049") => Self::DataElementTooLong,
439 Some("E050") => Self::DataElementTooShort,
440 Some("E048") => Self::InvalidCharacterType,
441 Some("E047") => Self::TooManyRepetitions,
442 Some("E051") => Self::TrailingSeparator,
443 Some("E052") => Self::GroupsAndMessagesMixed,
444 Some("E053") => Self::InvalidValue,
445 Some("E006" | "E028") => Self::InvalidOccurrenceOutsideMessage,
446 _ => Self::Unspecified,
447 }
448 }
449}
450
451#[inline]
458const fn l_of(level: ReportingLevel) -> ReportingLevel {
459 level
460}
461
462impl std::fmt::Display for SyntaxError {
463 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 write!(f, "{} ({})", self.code(), self.description())
465 }
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq)]
477enum Scope {
478 Group(usize),
480 Message {
482 index: usize,
483 segment_position: u32,
485 },
486}
487
488#[derive(Debug, Clone, PartialEq, Eq)]
490struct Finding {
491 scope: Scope,
492 element_position: Option<u32>,
494 component_position: Option<u32>,
496 rejects: bool,
501 error: SyntaxError,
502}
503
504impl Finding {
505 fn level(&self) -> ReportingLevel {
514 if let Scope::Group(_) = self.scope {
515 return ReportingLevel::Group;
516 }
517 if self.element_position.is_some() && self.error.permitted_at(ReportingLevel::DataElement) {
518 return ReportingLevel::DataElement;
519 }
520 if self.error.permitted_at(ReportingLevel::Segment) {
521 return ReportingLevel::Segment;
522 }
523 ReportingLevel::Message
524 }
525
526 fn message_index(&self) -> Option<usize> {
528 match self.scope {
529 Scope::Message { index, .. } => Some(index),
530 Scope::Group(_) => None,
531 }
532 }
533}
534
535#[derive(Debug, Clone)]
546pub struct Contrl {
547 control_ref: String,
548 sender: String,
549 sender_qualifier: String,
550 recipient: String,
551 recipient_qualifier: String,
552 action: Action,
553 interchange_error: Option<SyntaxError>,
554 message_ref: Option<String>,
555 messages: Vec<(usize, MessageEnvelope, Action)>,
561 groups: Vec<GroupReport>,
565 findings: Vec<Finding>,
566}
567
568#[derive(Debug, Clone)]
570struct GroupReport {
571 envelope: FunctionalGroupEnvelope,
572 action: Action,
573 error: Option<SyntaxError>,
574 messages: Vec<(usize, MessageEnvelope, Action)>,
576}
577
578impl Contrl {
579 fn base(interchange: &InterchangeEnvelope, action: Action) -> Self {
580 Self {
581 control_ref: interchange.control_ref.clone(),
582 sender: interchange.sender_id.clone(),
583 sender_qualifier: interchange.sender_qualifier.clone(),
584 recipient: interchange.recipient_id.clone(),
585 recipient_qualifier: interchange.recipient_qualifier.clone(),
586 action,
587 interchange_error: None,
588 message_ref: None,
589 messages: Vec::new(),
590 groups: Vec::new(),
591 findings: Vec::new(),
592 }
593 }
594
595 #[must_use]
600 pub fn receipt(interchange: &InterchangeEnvelope) -> Self {
601 Self::base(interchange, Action::Received)
602 }
603
604 #[must_use]
610 pub fn acknowledgement(subject: &ValidatedInterchange) -> Self {
611 Self::base(&subject.interchange, Action::Acknowledged)
612 }
613
614 #[must_use]
644 pub fn from_report(
645 subject: &ValidatedInterchange,
646 segments: &[Segment<'_>],
647 report: &ValidationReport,
648 ) -> Self {
649 let mut contrl = Self::base(&subject.interchange, Action::Acknowledged);
650
651 let messages = message_boundaries(segments);
654 let groups = group_boundaries(segments);
655
656 for (issue, rejects) in report
657 .errors()
658 .iter()
659 .map(|i| (i, true))
660 .chain(report.warnings().iter().map(|i| (i, false)))
661 {
662 let error = SyntaxError::for_issue(issue);
663 let located = issue
664 .span
665 .and_then(|span| locate(segments, &messages, &groups, span.start));
666
667 let Some(scope) = located else {
671 if rejects {
672 contrl.action = Action::Rejected;
673 }
674 if contrl.interchange_error.is_none()
675 && error.permitted_at(ReportingLevel::Interchange)
676 {
677 contrl.interchange_error = Some(error);
678 }
679 continue;
680 };
681
682 contrl.findings.push(Finding {
683 scope,
684 element_position: issue.element_index.map(|i| u32::from(i) + 2),
687 component_position: issue.component_index.map(|i| u32::from(i) + 1),
688 rejects,
689 error,
690 });
691 }
692
693 if contrl.action == Action::Rejected {
696 contrl.findings.clear();
697 return contrl;
698 }
699
700 if subject.functional_groups.is_empty() {
701 contrl.messages = contrl.reported_messages(subject.messages.iter().enumerate());
702 } else {
703 contrl.build_group_reports(subject);
704 }
705
706 contrl
707 }
708
709 fn reported_messages<'m>(
714 &self,
715 candidates: impl Iterator<Item = (usize, &'m MessageEnvelope)>,
716 ) -> Vec<(usize, MessageEnvelope, Action)> {
717 let mut out = Vec::new();
718 for (index, message) in candidates {
719 let mut has_finding = false;
720 let mut rejected = false;
721 for finding in &self.findings {
722 if finding.message_index() == Some(index) {
723 has_finding = true;
724 rejected |= finding.rejects;
725 }
726 }
727 if !has_finding {
728 continue;
729 }
730 out.push((
731 index,
732 message.clone(),
733 if rejected {
734 Action::Rejected
735 } else {
736 Action::Acknowledged
737 },
738 ));
739 }
740 out
741 }
742
743 fn build_group_reports(&mut self, subject: &ValidatedInterchange) {
745 let mut flat = 0usize;
749 let mut reports = Vec::new();
750
751 for (group_index, group) in subject.functional_groups.iter().enumerate() {
752 let start = flat;
753 flat += group.messages.len();
754
755 let group_fault = self
756 .findings
757 .iter()
758 .find(|f| f.scope == Scope::Group(group_index));
759 let rejected = group_fault.is_some_and(|f| f.rejects);
760
761 let messages = if rejected {
764 Vec::new()
765 } else {
766 self.reported_messages(
767 group
768 .messages
769 .iter()
770 .enumerate()
771 .map(|(offset, message)| (start + offset, message)),
772 )
773 };
774
775 if group_fault.is_none() && messages.is_empty() {
776 continue; }
778
779 reports.push(GroupReport {
780 envelope: group.clone(),
781 action: if rejected {
782 Action::Rejected
783 } else {
784 Action::Acknowledged
785 },
786 error: group_fault
787 .map(|f| f.error)
788 .filter(|e| e.permitted_at(ReportingLevel::Group)),
789 messages,
790 });
791 }
792 self.groups = reports;
793 }
794
795 #[must_use]
799 pub fn with_message_reference(mut self, reference: impl Into<String>) -> Self {
800 self.message_ref = Some(reference.into());
801 self
802 }
803
804 #[must_use]
810 pub fn with_interchange_error(mut self, error: SyntaxError) -> Self {
811 if error.permitted_at(ReportingLevel::Interchange) {
812 self.interchange_error = Some(error);
813 }
814 self
815 }
816
817 #[must_use]
819 pub const fn action(&self) -> Action {
820 self.action
821 }
822
823 #[must_use]
825 pub fn message_reference(&self) -> &str {
826 self.message_ref.as_deref().unwrap_or(&self.control_ref)
827 }
828
829 #[must_use]
834 pub fn segments(&self) -> Vec<OwnedSegment> {
835 let mut out = Vec::new();
836 let reference = self.message_reference().to_owned();
837
838 out.push(OwnedSegment::new(
840 "UNH",
841 vec![
842 OwnedElement::of(std::slice::from_ref(&reference)),
843 OwnedElement::of(&["CONTRL", "4", "1", "UN"]),
844 ],
845 ));
846
847 let mut uci = vec![
849 OwnedElement::of(std::slice::from_ref(&self.control_ref)),
850 party(&self.sender, &self.sender_qualifier),
851 party(&self.recipient, &self.recipient_qualifier),
852 OwnedElement::of(&[self.action.code()]),
853 ];
854 if let Some(error) = self.interchange_error {
855 uci.push(OwnedElement::of(&[error.code()]));
856 }
857 out.push(OwnedSegment::new("UCI", uci));
858
859 for (index, message, action) in &self.messages {
861 out.extend(self.message_report(*index, message, *action));
862 }
863 for group in &self.groups {
864 let mut ucf = vec![
865 OwnedElement::of(std::slice::from_ref(&group.envelope.group_ref)),
866 party(
867 &group.envelope.app_sender,
868 &group.envelope.app_sender_qualifier,
869 ),
870 party(
871 &group.envelope.app_recipient,
872 &group.envelope.app_recipient_qualifier,
873 ),
874 OwnedElement::of(&[group.action.code()]),
875 ];
876 if let Some(error) = group.error {
877 ucf.push(OwnedElement::of(&[error.code()]));
878 }
879 out.push(OwnedSegment::new("UCF", ucf));
880 for (index, message, action) in &group.messages {
881 out.extend(self.message_report(*index, message, *action));
882 }
883 }
884
885 let count = (out.len() + 1).to_string();
886 out.push(OwnedSegment::new(
887 "UNT",
888 vec![OwnedElement::of(&[count]), OwnedElement::of(&[reference])],
889 ));
890 out
891 }
892
893 fn message_report(
895 &self,
896 index: usize,
897 message: &MessageEnvelope,
898 action: Action,
899 ) -> Vec<OwnedSegment> {
900 let mut ucm = vec![
901 OwnedElement::of(std::slice::from_ref(&message.message_ref)),
902 OwnedElement::of(&[
903 message.message_type.clone(),
904 message.version.clone(),
905 message.release.clone(),
906 message.controlling_agency.clone(),
907 ]),
908 OwnedElement::of(&[action.code()]),
909 ];
910 if let Some(finding) = self
912 .findings
913 .iter()
914 .find(|f| f.message_index() == Some(index) && f.level() == ReportingLevel::Message)
915 {
916 ucm.push(OwnedElement::of(&[finding.error.code()]));
917 }
918 let mut out = vec![OwnedSegment::new("UCM", ucm)];
919 out.extend(self.segment_reports(index));
920 out
921 }
922
923 fn segment_reports(&self, message_index: usize) -> Vec<OwnedSegment> {
928 let mut out = Vec::new();
929 let mut reported_positions: Vec<u32> = Vec::new();
930
931 for finding in self
932 .findings
933 .iter()
934 .filter(|f| f.message_index() == Some(message_index))
935 {
936 let level = finding.level();
937 let Scope::Message {
938 segment_position: position,
939 ..
940 } = finding.scope
941 else {
942 continue;
943 };
944 if level == ReportingLevel::Message {
945 continue;
946 }
947 if !reported_positions.contains(&position) {
951 reported_positions.push(position);
952 let position_text = position.to_string();
953 let mut ucs = vec![OwnedElement::of(&[position_text])];
954 if level == ReportingLevel::Segment {
957 ucs.push(OwnedElement::of(&[finding.error.code()]));
958 }
959 out.push(OwnedSegment::new("UCS", ucs));
960 }
961
962 if level == ReportingLevel::DataElement {
963 let element_text = finding
964 .element_position
965 .expect("DataElement level implies a known element position")
966 .to_string();
967 let mut identification = vec![element_text];
968 if let Some(component) = finding.component_position {
969 identification.push(component.to_string());
970 }
971 out.push(OwnedSegment::new(
972 "UCD",
973 vec![
974 OwnedElement::of(&[finding.error.code()]),
975 OwnedElement::of(&identification),
976 ],
977 ));
978 }
979 }
980 out
981 }
982
983 pub fn to_bytes(&self) -> Result<Vec<u8>, EdifactError> {
989 crate::segments_to_bytes(&self.segments())
990 }
991
992 pub fn to_edifact_string(&self) -> Result<String, EdifactError> {
998 String::from_utf8(self.to_bytes()?).map_err(|_| EdifactError::InvalidUtf8)
999 }
1000
1001 pub fn to_interchange_bytes(
1028 &self,
1029 syntax_identifier: &str,
1030 syntax_version: &str,
1031 date: &str,
1032 time: &str,
1033 control_reference: &str,
1034 ) -> Result<Vec<u8>, EdifactError> {
1035 let mut writer = Writer::new(Vec::new());
1036 writer.begin_interchange(
1037 syntax_identifier,
1038 syntax_version,
1039 &self.recipient,
1041 &self.sender,
1042 date,
1043 time,
1044 control_reference,
1045 )?;
1046 for segment in self.segments() {
1047 writer.write_segment(&segment)?;
1048 }
1049 writer.end_interchange(1, control_reference)?;
1050 writer.finish()
1051 }
1052
1053 pub fn to_interchange_string(
1060 &self,
1061 syntax_identifier: &str,
1062 syntax_version: &str,
1063 date: &str,
1064 time: &str,
1065 control_reference: &str,
1066 ) -> Result<String, EdifactError> {
1067 let bytes = self.to_interchange_bytes(
1068 syntax_identifier,
1069 syntax_version,
1070 date,
1071 time,
1072 control_reference,
1073 )?;
1074 String::from_utf8(bytes).map_err(|_| EdifactError::InvalidUtf8)
1075 }
1076}
1077
1078fn party(id: &str, qualifier: &str) -> OwnedElement {
1080 if qualifier.is_empty() {
1081 OwnedElement::of(&[id.to_owned()])
1082 } else {
1083 OwnedElement::of(&[id.to_owned(), qualifier.to_owned()])
1084 }
1085}
1086
1087fn message_boundaries(segments: &[Segment<'_>]) -> Vec<(usize, usize)> {
1092 spans_between(segments, "UNH", "UNT")
1093}
1094
1095fn group_boundaries(segments: &[Segment<'_>]) -> Vec<(usize, usize)> {
1099 spans_between(segments, "UNG", "UNE")
1100}
1101
1102fn spans_between(segments: &[Segment<'_>], open: &str, close: &str) -> Vec<(usize, usize)> {
1104 let mut out = Vec::new();
1105 let mut start: Option<usize> = None;
1106 for (index, segment) in segments.iter().enumerate() {
1107 if segment.tag == open {
1108 start = Some(index);
1109 } else if segment.tag == close {
1110 if let Some(from) = start.take() {
1111 out.push((from, index));
1112 }
1113 }
1114 }
1115 out
1116}
1117
1118fn locate(
1132 segments: &[Segment<'_>],
1133 messages: &[(usize, usize)],
1134 groups: &[(usize, usize)],
1135 offset: usize,
1136) -> Option<Scope> {
1137 let index = match segments.binary_search_by(|segment| segment.span.start.cmp(&offset)) {
1140 Ok(exact) => exact,
1141 Err(0) => return None,
1142 Err(next) => next - 1,
1143 };
1144 if let Some((message_index, (start, _))) = messages
1145 .iter()
1146 .enumerate()
1147 .find(|(_, (start, end))| (*start..=*end).contains(&index))
1148 {
1149 return u32::try_from(index - start + 1)
1150 .ok()
1151 .map(|segment_position| Scope::Message {
1152 index: message_index,
1153 segment_position,
1154 });
1155 }
1156 groups
1157 .iter()
1158 .position(|(start, end)| (*start..=*end).contains(&index))
1159 .map(Scope::Group)
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164 use super::*;
1165
1166 fn parse(input: &[u8]) -> Vec<OwnedSegment> {
1167 crate::from_bytes(input)
1168 .map(|r| r.map(|s| s.into_owned()))
1169 .collect::<Result<Vec<_>, _>>()
1170 .expect("parse")
1171 }
1172
1173 #[test]
1174 fn annex_a_permits_a_code_only_where_the_table_says() {
1175 assert!(SyntaxError::NotActualRecipient.permitted_at(ReportingLevel::Interchange));
1177 assert!(!SyntaxError::NotActualRecipient.permitted_at(ReportingLevel::Group));
1178 assert!(SyntaxError::TooManyGroupRepetitions.permitted_at(ReportingLevel::Segment));
1179 assert!(!SyntaxError::TooManyGroupRepetitions.permitted_at(ReportingLevel::DataElement));
1180 assert!(SyntaxError::LowerLevelEmpty.permitted_at(ReportingLevel::Group));
1181 assert!(!SyntaxError::LowerLevelEmpty.permitted_at(ReportingLevel::Message));
1182 assert!(SyntaxError::EnvelopeFunctionalityNotSupported.permitted_at(ReportingLevel::Group));
1183 assert!(
1184 !SyntaxError::EnvelopeFunctionalityNotSupported
1185 .permitted_at(ReportingLevel::Interchange)
1186 );
1187 for level in [
1189 ReportingLevel::Interchange,
1190 ReportingLevel::Group,
1191 ReportingLevel::Message,
1192 ReportingLevel::Segment,
1193 ReportingLevel::DataElement,
1194 ] {
1195 assert!(SyntaxError::InvalidValue.permitted_at(level), "{level:?}");
1196 }
1197 }
1198
1199 #[test]
1200 fn every_code_is_permitted_somewhere() {
1201 for error in ALL_ERRORS {
1204 assert!(
1205 [
1206 ReportingLevel::Interchange,
1207 ReportingLevel::Group,
1208 ReportingLevel::Message,
1209 ReportingLevel::Segment,
1210 ReportingLevel::DataElement,
1211 ]
1212 .iter()
1213 .any(|level| error.permitted_at(*level)),
1214 "{error:?} is permitted nowhere"
1215 );
1216 }
1217 }
1218
1219 #[test]
1220 fn codes_are_unique() {
1221 let mut codes: Vec<&str> = ALL_ERRORS.iter().map(|e| e.code()).collect();
1222 codes.sort_unstable();
1223 let before = codes.len();
1224 codes.dedup();
1225 assert_eq!(before, codes.len(), "duplicate DE 0085 code");
1226 }
1227
1228 const ALL_ERRORS: &[SyntaxError] = &[
1229 SyntaxError::SyntaxVersionNotSupported,
1230 SyntaxError::NotActualRecipient,
1231 SyntaxError::InvalidValue,
1232 SyntaxError::Missing,
1233 SyntaxError::ValueNotSupportedHere,
1234 SyntaxError::NotSupportedHere,
1235 SyntaxError::TooManyConstituents,
1236 SyntaxError::NoAgreement,
1237 SyntaxError::Unspecified,
1238 SyntaxError::InvalidAsServiceCharacter,
1239 SyntaxError::InvalidCharacters,
1240 SyntaxError::InvalidServiceCharacters,
1241 SyntaxError::UnknownSender,
1242 SyntaxError::TooOld,
1243 SyntaxError::TestIndicatorNotSupported,
1244 SyntaxError::DuplicateDetected,
1245 SyntaxError::ReferencesDoNotMatch,
1246 SyntaxError::ControlCountMismatch,
1247 SyntaxError::GroupsAndMessagesMixed,
1248 SyntaxError::LowerLevelEmpty,
1249 SyntaxError::InvalidOccurrenceOutsideMessage,
1250 SyntaxError::TooManyRepetitions,
1251 SyntaxError::TooManyGroupRepetitions,
1252 SyntaxError::InvalidCharacterType,
1253 SyntaxError::DataElementTooLong,
1254 SyntaxError::DataElementTooShort,
1255 SyntaxError::TrailingSeparator,
1256 SyntaxError::CharacterSetNotSupported,
1257 SyntaxError::EnvelopeFunctionalityNotSupported,
1258 ];
1259
1260 #[test]
1261 fn an_acknowledgement_validates_against_the_shipped_layouts() {
1262 let raw = b"UNB+UNOC:3+SENDER:14+RECEIVER:14+260101:0900+IC4711'\
1263 UNH+MSG1+ORDERS:D:96A:UN'BGM+220+PO-1+9'UNT+3+MSG1'\
1264 UNZ+1+IC4711'";
1265 let owned = parse(raw);
1266 let validated = crate::validate_envelope(&owned).expect("valid subject");
1267
1268 let contrl = Contrl::acknowledgement(&validated).with_message_reference("ACK1");
1269 let wire = contrl.to_edifact_string().expect("render");
1270
1271 assert_eq!(
1272 wire,
1273 "UNH+ACK1+CONTRL:4:1:UN'UCI+IC4711+SENDER:14+RECEIVER:14+7'UNT+3+ACK1'"
1274 );
1275
1276 let reparsed = parse(wire.as_bytes());
1278 let validator = crate::DirectoryValidator::new(
1279 "iso-9735-4",
1280 crate::service::lookup,
1281 |_, _| true,
1282 |_, _| None,
1283 |_, _| None,
1284 None,
1285 );
1286 let report = crate::ValidationContext::builder()
1287 .with_validator(crate::ValidationLayer::Structure, validator)
1288 .build()
1289 .validate(&reparsed);
1290 assert!(!report.has_errors(), "{:#?}", report.errors());
1291 }
1292
1293 #[test]
1294 fn a_receipt_carries_action_8_and_nothing_else() {
1295 let raw =
1296 b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'UNZ+1+IC1'";
1297 let owned = parse(raw);
1298 let validated = crate::validate_envelope(&owned).expect("valid subject");
1299
1300 let wire = Contrl::receipt(&validated.interchange)
1301 .to_edifact_string()
1302 .expect("render");
1303 assert!(wire.contains("UCI+IC1+S+R+8'"), "{wire}");
1304 assert_eq!(
1305 Contrl::receipt(&validated.interchange).action(),
1306 Action::Received
1307 );
1308 }
1309
1310 #[test]
1311 fn the_message_reference_defaults_to_the_subject_control_reference() {
1312 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'";
1313 let owned = parse(raw);
1314 let validated = crate::validate_envelope(&owned).expect("valid subject");
1315 assert_eq!(
1316 Contrl::acknowledgement(&validated).message_reference(),
1317 "IC-42"
1318 );
1319 }
1320
1321 #[test]
1322 fn a_forbidden_interchange_level_code_is_not_recorded() {
1323 let raw =
1324 b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'UNZ+1+IC1'";
1325 let owned = parse(raw);
1326 let validated = crate::validate_envelope(&owned).expect("valid subject");
1327
1328 let wire = Contrl::acknowledgement(&validated)
1330 .with_interchange_error(SyntaxError::TooManyGroupRepetitions)
1331 .to_edifact_string()
1332 .expect("render");
1333 assert!(wire.contains("UCI+IC1+S+R+7'"), "{wire}");
1334 }
1335
1336 fn report_for(raw: &[u8]) -> Contrl {
1341 let owned = parse(raw);
1342 let validated = crate::validate_envelope_lenient(&owned)
1343 .interchange
1344 .expect("subject must be structurally interpretable");
1345 let report = crate::ValidationContext::builder()
1346 .with_envelope_validation()
1347 .with_syntax_validation()
1348 .build()
1349 .validate(&owned);
1350 Contrl::from_report(&validated, &owned, &report)
1351 }
1352
1353 #[test]
1354 fn a_data_element_fault_is_reported_at_the_ucd_level() {
1355 let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1359 UNH+M1+ORDERS:D:96A:UN'FTX+ 'UNT+3+M1'\
1360 UNZ+1+IC1'";
1361 let contrl = report_for(raw);
1362
1363 assert_eq!(contrl.action(), Action::Acknowledged);
1366 let wire = contrl.to_edifact_string().expect("render");
1367
1368 assert!(wire.contains("UCM+M1+ORDERS:D:96A:UN+7'"), "{wire}");
1371 assert!(wire.contains("UCS+2'"), "{wire}");
1372 assert!(wire.contains("UCD+12+2:1'"), "{wire}");
1373 }
1374
1375 #[test]
1376 fn a_rejected_message_is_named_by_its_ucm() {
1377 let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1380 UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
1381 UNH+M1+ORDERS:D:96A:UN'BGM+221'UNT+3+M1'\
1382 UNZ+2+IC1'";
1383 let contrl = report_for(raw);
1384
1385 assert_eq!(contrl.action(), Action::Acknowledged);
1389 let wire = contrl.to_edifact_string().expect("render");
1390
1391 assert_eq!(
1396 wire,
1397 "UNH+IC1+CONTRL:4:1:UN'\
1398 UCI+IC1+S+R+7'\
1399 UCM+M1+ORDERS:D:96A:UN+4+26'\
1400 UNT+4+IC1'"
1401 );
1402 }
1403
1404 #[test]
1405 fn an_envelope_fault_rejects_the_interchange_and_emits_no_ucm() {
1406 let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1410 UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
1411 UNZ+1+IC-OTHER'";
1412 let contrl = report_for(raw);
1413
1414 assert_eq!(contrl.action(), Action::Rejected);
1415 let wire = contrl.to_edifact_string().expect("render");
1416 assert_eq!(wire, "UNH+IC1+CONTRL:4:1:UN'UCI+IC1+S+R+4+28'UNT+3+IC1'");
1417 }
1418
1419 #[test]
1420 fn a_count_mismatch_is_reported_on_the_message_that_got_it_wrong() {
1421 let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1424 UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+9+M1'\
1425 UNZ+1+IC1'";
1426 let contrl = report_for(raw);
1427
1428 assert_eq!(contrl.action(), Action::Acknowledged);
1429 let wire = contrl.to_edifact_string().expect("render");
1430 assert_eq!(
1433 wire,
1434 "UNH+IC1+CONTRL:4:1:UN'\
1435 UCI+IC1+S+R+7'\
1436 UCM+M1+ORDERS:D:96A:UN+4+29'\
1437 UNT+4+IC1'"
1438 );
1439 }
1440
1441 #[test]
1442 fn a_finding_lands_at_the_lowest_level_annex_a_permits() {
1443 let duplicate = Finding {
1446 scope: Scope::Message {
1447 index: 0,
1448 segment_position: 1,
1449 },
1450 element_position: Some(2),
1451 component_position: None,
1452 rejects: true,
1453 error: SyntaxError::DuplicateDetected,
1454 };
1455 assert_eq!(duplicate.level(), ReportingLevel::Message);
1456
1457 let invalid = Finding {
1459 error: SyntaxError::InvalidValue,
1460 ..duplicate.clone()
1461 };
1462 assert_eq!(invalid.level(), ReportingLevel::DataElement);
1463
1464 let segment_only = Finding {
1466 element_position: None,
1467 ..invalid.clone()
1468 };
1469 assert_eq!(segment_only.level(), ReportingLevel::Segment);
1470
1471 let group_repetitions = Finding {
1474 error: SyntaxError::TooManyGroupRepetitions,
1475 ..duplicate
1476 };
1477 assert_eq!(group_repetitions.level(), ReportingLevel::Segment);
1478 }
1479
1480 #[test]
1481 fn a_generated_contrl_reparses_and_counts_its_own_segments() {
1482 let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1483 UNH+M1+ORDERS:D:96A:UN'FTX+ 'UNT+3+M1'\
1484 UNZ+1+IC1'";
1485 let contrl = report_for(raw);
1486
1487 let wire = contrl
1488 .to_interchange_string("UNOC", "3", "260101", "0930", "ACK-1")
1489 .expect("render");
1490
1491 let segments: Vec<_> = crate::from_bytes(wire.as_bytes())
1494 .collect::<Result<Vec<_>, _>>()
1495 .expect("generated CONTRL must reparse");
1496 let validated =
1497 crate::validate_envelope(&segments).expect("generated CONTRL must validate");
1498 assert_eq!(validated.messages.len(), 1);
1499 assert_eq!(validated.messages[0].message_type, "CONTRL");
1500 assert_eq!(validated.messages[0].version, "4");
1501 assert_eq!(validated.messages[0].release, "1");
1502 assert_eq!(
1503 validated.messages[0].declared_segment_count,
1504 validated.messages[0].actual_segment_count
1505 );
1506 }
1507
1508 #[test]
1509 fn a_grouped_interchange_is_reported_through_ucf() {
1510 let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1513 UNG+ORDERS+SND:14+RCV:14+260101:0900+GRP1+UN+D:96A'\
1514 UNH+M1+ORDERS:D:96A:UN'FTX+ 'UNT+3+M1'\
1515 UNE+1+GRP1'\
1516 UNZ+1+IC1'";
1517 let contrl = report_for(raw);
1518
1519 assert_eq!(contrl.action(), Action::Acknowledged);
1520 let wire = contrl.to_edifact_string().expect("render");
1521
1522 assert_eq!(
1525 wire,
1526 "UNH+IC1+CONTRL:4:1:UN'\
1527 UCI+IC1+S+R+7'\
1528 UCF+GRP1+SND:14+RCV:14+7'\
1529 UCM+M1+ORDERS:D:96A:UN+7'\
1530 UCS+2'\
1531 UCD+12+2:1'\
1532 UNT+7+IC1'"
1533 );
1534 }
1535
1536 #[test]
1537 fn a_group_envelope_fault_rejects_only_that_group() {
1538 let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1542 UNG+ORDERS+SND+RCV+260101:0900+GRP1+UN+D:96A'\
1543 UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
1544 UNE+1+GRP-OTHER'\
1545 UNZ+1+IC1'";
1546 let contrl = report_for(raw);
1547
1548 assert_eq!(contrl.action(), Action::Acknowledged);
1549 let wire = contrl.to_edifact_string().expect("render");
1550 assert_eq!(
1551 wire,
1552 "UNH+IC1+CONTRL:4:1:UN'\
1553 UCI+IC1+S+R+7'\
1554 UCF+GRP1+SND+RCV+4+28'\
1555 UNT+4+IC1'"
1556 );
1557 }
1558
1559 #[test]
1560 fn a_clean_grouped_interchange_needs_no_ucf() {
1561 let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1564 UNG+ORDERS+SND+RCV+260101:0900+GRP1+UN+D:96A'\
1565 UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
1566 UNE+1+GRP1'\
1567 UNZ+1+IC1'";
1568 let contrl = report_for(raw);
1569 assert_eq!(
1570 contrl.to_edifact_string().expect("render"),
1571 "UNH+IC1+CONTRL:4:1:UN'UCI+IC1+S+R+7'UNT+3+IC1'"
1572 );
1573 }
1574
1575 #[test]
1576 fn error_mapping_picks_the_narrowest_annex_a_code() {
1577 use EdifactError as E;
1578 let cases: [(EdifactError, SyntaxError); 6] = [
1579 (
1580 E::MessageCountMismatch {
1581 expected: 1,
1582 actual: 2,
1583 },
1584 SyntaxError::ControlCountMismatch,
1585 ),
1586 (
1587 E::DuplicateReference {
1588 tag: "UNH".to_owned(),
1589 reference: "1".to_owned(),
1590 span: crate::Span::new(0, 1),
1591 },
1592 SyntaxError::DuplicateDetected,
1593 ),
1594 (
1595 E::UnsupportedCharset {
1596 syntax_identifier: "UNOX".to_owned(),
1597 },
1598 SyntaxError::CharacterSetNotSupported,
1599 ),
1600 (E::InvalidUna, SyntaxError::InvalidServiceCharacters),
1601 (
1602 E::EmptyInterchange {
1603 control_ref: "IC1".to_owned(),
1604 },
1605 SyntaxError::LowerLevelEmpty,
1606 ),
1607 (
1608 E::PackageNotSupported {
1609 tag: "UNO".to_owned(),
1610 span: crate::Span::new(0, 1),
1611 },
1612 SyntaxError::EnvelopeFunctionalityNotSupported,
1613 ),
1614 ];
1615 for (error, expected) in cases {
1616 assert_eq!(SyntaxError::for_error(&error), expected, "{error:?}");
1617 }
1618 }
1619}