1use crate::{
49 OwnedSegment,
50 error::EdifactError,
51 model::{Segment, Span},
52};
53use std::collections::HashSet;
54
55pub(crate) trait SegmentReader: sealed::Sealed {
58 fn tag(&self) -> &str;
59 fn span(&self) -> Span;
60 fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str>;
61
62 fn required_component_field(
63 &self,
64 elem_idx: usize,
65 comp_idx: usize,
66 ) -> Result<&str, EdifactError> {
67 self.component(elem_idx, comp_idx)
68 .filter(|s| !s.is_empty())
69 .ok_or_else(|| EdifactError::MissingRequiredComponent {
70 tag: self.tag().to_owned(),
71 element_index: elem_idx,
72 component_index: comp_idx,
73 })
74 }
75}
76
77mod sealed {
78 pub trait Sealed {}
79 impl Sealed for crate::model::Segment<'_> {}
80 impl Sealed for crate::OwnedSegment {}
81}
82
83impl SegmentReader for Segment<'_> {
84 #[inline]
85 fn tag(&self) -> &str {
86 self.tag
87 }
88 #[inline]
89 fn span(&self) -> Span {
90 self.span
91 }
92 #[inline]
93 fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str> {
94 self.get_element(elem_idx)?.get_component(comp_idx)
95 }
96}
97
98impl SegmentReader for OwnedSegment {
99 #[inline]
100 fn tag(&self) -> &str {
101 &self.tag
102 }
103 #[inline]
104 fn span(&self) -> Span {
105 self.span
106 }
107 #[inline]
108 fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str> {
109 self.component_str(elem_idx, comp_idx)
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Hash)]
137#[non_exhaustive]
138#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
139pub struct InterchangeEnvelope {
140 pub syntax_identifier: String,
142 pub syntax_version: String,
146 pub sender_id: String,
148 pub sender_qualifier: String,
153 pub sender_routing_address: Option<String>,
162 pub recipient_id: String,
164 pub recipient_qualifier: String,
168 pub recipient_routing_address: Option<String>,
173 pub date: String,
175 pub time: Option<String>,
179 pub control_ref: String,
181 pub recipient_password: Option<String>,
186 pub recipient_password_qualifier: Option<String>,
191 pub app_ref: Option<String>,
195 pub processing_priority: Option<String>,
200 pub acknowledgement_request: bool,
205 pub communications_agreement_id: Option<String>,
209 pub test_indicator: bool,
215 pub declared_unit_count: u32,
223 pub actual_unit_count: u32,
225}
226
227impl InterchangeEnvelope {
228 #[inline]
250 #[must_use]
251 pub fn is_test(&self) -> bool {
252 self.test_indicator
253 }
254
255 #[inline]
259 #[must_use]
260 pub fn ack_requested(&self) -> bool {
261 self.acknowledgement_request
262 }
263}
264
265impl std::fmt::Display for InterchangeEnvelope {
266 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267 write!(
268 f,
269 "{sender} -> {recipient} [{ctrl}] ({syntax}:{ver})",
270 sender = self.sender_id,
271 recipient = self.recipient_id,
272 ctrl = self.control_ref,
273 syntax = self.syntax_identifier,
274 ver = self.syntax_version,
275 )
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq, Hash)]
281#[non_exhaustive]
282#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
283pub struct MessageEnvelope {
284 pub message_ref: String,
286 pub message_type: String,
288 pub version: String,
290 pub release: String,
292 pub controlling_agency: String,
294 pub association_code: String,
296 pub common_access_ref: Option<String>,
303 pub sequence_of_transfers: Option<u32>,
309 pub transfer_position: Option<String>,
314 pub declared_segment_count: u32,
316 pub actual_segment_count: u32,
318 pub header_span: Span,
320 pub trailer_span: Span,
326}
327
328impl std::fmt::Display for MessageEnvelope {
329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 write!(
331 f,
332 "{msg_type}:{ver}:{rel} ref={msg_ref} seg={actual}/{declared}",
333 msg_type = self.message_type,
334 ver = self.version,
335 rel = self.release,
336 msg_ref = self.message_ref,
337 actual = self.actual_segment_count,
338 declared = self.declared_segment_count,
339 )
340 }
341}
342
343#[derive(Debug, Clone, PartialEq, Eq, Hash)]
349#[non_exhaustive]
350#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
351pub struct FunctionalGroupEnvelope {
352 pub group_id: String,
354 pub app_sender: String,
356 pub app_sender_qualifier: String,
360 pub app_recipient: String,
362 pub app_recipient_qualifier: String,
366 pub date: String,
368 pub time: Option<String>,
370 pub group_ref: String,
372 pub controlling_agency: String,
374 pub version: String,
376 pub release: String,
378 pub application_password: Option<String>,
384 pub declared_message_count: u32,
386 pub actual_message_count: u32,
388 pub messages: Vec<MessageEnvelope>,
390}
391
392#[derive(Debug, Clone, PartialEq, Eq, Hash)]
398#[non_exhaustive]
399#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
400pub struct ValidatedInterchange {
401 pub interchange: InterchangeEnvelope,
403 pub functional_groups: Vec<FunctionalGroupEnvelope>,
408 pub messages: Vec<MessageEnvelope>,
413}
414
415impl ValidatedInterchange {
416 #[inline]
418 #[must_use]
419 pub fn has_functional_groups(&self) -> bool {
420 !self.functional_groups.is_empty()
421 }
422
423 #[inline]
427 #[must_use]
428 pub fn message_count(&self) -> usize {
429 self.messages.len()
430 }
431
432 #[inline]
437 pub fn iter_messages(&self) -> impl Iterator<Item = &MessageEnvelope> {
438 self.messages.iter()
439 }
440
441 #[inline]
448 #[must_use]
449 pub fn find_message(&self, reference: &str) -> Option<&MessageEnvelope> {
450 self.messages.iter().find(|m| m.message_ref == reference)
451 }
452
453 #[must_use]
462 pub fn messages_by_type(&self, message_type: &str) -> Vec<&MessageEnvelope> {
463 self.messages
464 .iter()
465 .filter(|m| m.message_type == message_type)
466 .collect()
467 }
468
469 #[inline]
480 pub fn iter_messages_by_type<'s, 'q: 's>(
481 &'s self,
482 message_type: &'q str,
483 ) -> impl Iterator<Item = &'s MessageEnvelope> + 's {
484 self.messages
485 .iter()
486 .filter(move |m| m.message_type == message_type)
487 }
488}
489
490impl std::fmt::Display for ValidatedInterchange {
491 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492 write!(
493 f,
494 "{ic} messages={n}",
495 ic = self.interchange,
496 n = self.messages.len(),
497 )
498 }
499}
500
501impl std::fmt::Display for FunctionalGroupEnvelope {
502 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
503 write!(
504 f,
505 "{gid} sender={sender} recipient={recip} [{gref}] ({agency}) msgs={actual}/{declared}",
506 gid = self.group_id,
507 sender = self.app_sender,
508 recip = self.app_recipient,
509 gref = self.group_ref,
510 agency = self.controlling_agency,
511 actual = self.actual_message_count,
512 declared = self.declared_message_count,
513 )
514 }
515}
516
517#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
522#[non_exhaustive]
523#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
524pub struct MessageIdentifier<'a> {
525 pub message_ref: &'a str,
527 pub message_type: &'a str,
528 pub version: &'a str,
529 pub release: &'a str,
530 pub controlling_agency: &'a str,
531 pub association_code: &'a str,
535}
536
537pub fn parse_unh<'a>(unh: &'a Segment<'a>) -> Result<MessageIdentifier<'a>, EdifactError> {
541 let message_ref = unh
543 .get_element(0)
544 .and_then(|e| e.get_component(0))
545 .filter(|s| !s.is_empty())
546 .ok_or_else(|| EdifactError::MissingRequiredComponent {
547 tag: "UNH".to_owned(),
548 element_index: 0,
549 component_index: 0,
550 })?;
551 let elem = unh
553 .get_element(1)
554 .ok_or_else(|| EdifactError::MissingRequiredElement {
555 tag: "UNH".to_owned(),
556 element_index: 1,
557 })?;
558 let message_type =
559 elem.get_component(0)
560 .ok_or_else(|| EdifactError::MissingRequiredComponent {
561 tag: "UNH".to_owned(),
562 element_index: 1,
563 component_index: 0,
564 })?;
565 Ok(MessageIdentifier {
566 message_ref,
567 message_type,
568 version: elem.get_component(1).unwrap_or(""),
569 release: elem.get_component(2).unwrap_or(""),
570 controlling_agency: elem.get_component(3).unwrap_or(""),
571 association_code: elem.get_component(4).unwrap_or(""),
572 })
573}
574
575#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
594#[non_exhaustive]
595#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
596pub struct GroupIdentifier<'a> {
597 pub group_id: &'a str,
599 pub app_sender: &'a str,
601 pub app_sender_qualifier: &'a str,
603 pub app_recipient: &'a str,
605 pub app_recipient_qualifier: &'a str,
607 pub group_ref: &'a str,
609 pub controlling_agency: &'a str,
611 pub version: &'a str,
613 pub release: &'a str,
615 pub application_password: &'a str,
617}
618
619pub fn parse_ung<'a>(ung: &'a Segment<'a>) -> Result<GroupIdentifier<'a>, EdifactError> {
624 let group_id = ung
625 .get_element(0)
626 .and_then(|e| e.get_component(0))
627 .unwrap_or("");
628 let app_sender = ung
629 .get_element(1)
630 .and_then(|e| e.get_component(0))
631 .unwrap_or("");
632 let app_sender_qualifier = ung
633 .get_element(1)
634 .and_then(|e| e.get_component(1))
635 .unwrap_or("");
636 let app_recipient = ung
637 .get_element(2)
638 .and_then(|e| e.get_component(0))
639 .unwrap_or("");
640 let app_recipient_qualifier = ung
641 .get_element(2)
642 .and_then(|e| e.get_component(1))
643 .unwrap_or("");
644 let group_ref = ung
645 .get_element(4)
646 .and_then(|e| e.get_component(0))
647 .ok_or_else(|| EdifactError::MissingRequiredComponent {
648 tag: "UNG".to_owned(),
649 element_index: 4,
650 component_index: 0,
651 })?;
652 let controlling_agency = ung
653 .get_element(5)
654 .and_then(|e| e.get_component(0))
655 .unwrap_or("");
656 let s008 = ung.get_element(6);
657 let version = s008.as_ref().and_then(|e| e.get_component(0)).unwrap_or("");
658 let release = s008.as_ref().and_then(|e| e.get_component(1)).unwrap_or("");
659 let application_password = ung
660 .get_element(7)
661 .and_then(|e| e.get_component(0))
662 .unwrap_or("");
663 Ok(GroupIdentifier {
664 group_id,
665 app_sender,
666 app_sender_qualifier,
667 app_recipient,
668 app_recipient_qualifier,
669 group_ref,
670 controlling_agency,
671 version,
672 release,
673 application_password,
674 })
675}
676
677pub fn validate_envelope(segments: &[Segment<'_>]) -> Result<ValidatedInterchange, EdifactError> {
682 validate_envelope_impl(segments)
683}
684
685pub fn validate_envelope_from_owned(
687 segments: &[OwnedSegment],
688) -> Result<ValidatedInterchange, EdifactError> {
689 validate_envelope_impl(segments)
690}
691
692#[derive(Debug)]
705#[non_exhaustive]
706pub struct LenientResult {
707 pub interchange: Option<ValidatedInterchange>,
709 pub errors: Vec<EdifactError>,
711}
712
713impl LenientResult {
714 #[inline]
716 #[must_use]
717 pub fn is_valid(&self) -> bool {
718 self.errors.is_empty()
719 }
720
721 #[inline]
726 #[must_use]
727 pub fn has_errors(&self) -> bool {
728 !self.errors.is_empty()
729 }
730
731 pub fn into_strict(self) -> Result<ValidatedInterchange, Vec<EdifactError>> {
741 match self.interchange {
742 Some(interchange) if self.errors.is_empty() => Ok(interchange),
743 _ => Err(self.errors),
744 }
745 }
746}
747
748pub fn validate_envelope_lenient(segments: &[Segment<'_>]) -> LenientResult {
759 validate_envelope_lenient_impl(segments)
760}
761
762pub fn validate_envelope_lenient_from_owned(segments: &[OwnedSegment]) -> LenientResult {
766 validate_envelope_lenient_impl(segments)
767}
768
769#[derive(Default)]
786struct ErrorSink {
787 errors: Vec<EdifactError>,
788}
789
790impl ErrorSink {
791 #[inline]
792 fn push(&mut self, error: EdifactError) {
793 self.errors.push(error);
794 }
795
796 #[inline]
798 fn recover<T>(&mut self, result: Result<T, EdifactError>, fallback: T) -> T {
799 match result {
800 Ok(value) => value,
801 Err(error) => {
802 self.errors.push(error);
803 fallback
804 }
805 }
806 }
807
808 #[inline]
810 fn required<S: SegmentReader>(&mut self, seg: &S, element: usize, component: usize) -> String {
811 self.recover(
812 seg.required_component_field(element, component)
813 .map(str::to_owned),
814 String::new(),
815 )
816 }
817}
818
819fn validate_envelope_collecting<S: SegmentReader>(
824 segments: &[S],
825) -> (Option<ValidatedInterchange>, Vec<EdifactError>) {
826 let mut sink = ErrorSink::default();
827
828 let mut interchange_env = match extract_interchange(segments, &mut sink) {
829 Ok(env) => env,
830 Err(fatal) => {
831 sink.push(fatal);
832 return (None, sink.errors);
833 }
834 };
835
836 let inner = if segments.len() >= 2 {
837 &segments[1..segments.len() - 1]
838 } else {
839 &[]
840 };
841
842 let (functional_groups, messages) = match extract_content(inner, &mut sink) {
843 Ok(pair) => pair,
844 Err(fatal) => {
845 sink.push(fatal);
846 return (None, sink.errors);
847 }
848 };
849
850 if functional_groups.is_empty() && messages.is_empty() {
854 sink.push(EdifactError::EmptyInterchange {
855 control_ref: interchange_env.control_ref.clone(),
856 });
857 }
858
859 let actual_unit_count = if functional_groups.is_empty() {
862 messages.len()
863 } else {
864 functional_groups.len()
865 };
866 interchange_env.actual_unit_count = sink.recover(
867 u32::try_from(actual_unit_count).map_err(|_| EdifactError::InterchangeTooLarge {
868 count: actual_unit_count as u64,
869 }),
870 u32::MAX,
871 );
872
873 if interchange_env.declared_unit_count != interchange_env.actual_unit_count {
874 sink.push(EdifactError::MessageCountMismatch {
875 expected: interchange_env.declared_unit_count,
876 actual: interchange_env.actual_unit_count,
877 });
878 }
879
880 for msg in &messages {
881 if msg.declared_segment_count != msg.actual_segment_count {
882 sink.push(EdifactError::SegmentCountMismatch {
883 expected: msg.declared_segment_count,
884 actual: msg.actual_segment_count,
885 message_ref: msg.message_ref.clone(),
886 span: msg.trailer_span,
887 });
888 }
889 }
890
891 (
892 Some(ValidatedInterchange {
893 interchange: interchange_env,
894 functional_groups,
895 messages,
896 }),
897 sink.errors,
898 )
899}
900
901fn validate_envelope_impl<S: SegmentReader>(
902 segments: &[S],
903) -> Result<ValidatedInterchange, EdifactError> {
904 match validate_envelope_collecting(segments) {
905 (Some(result), errors) if errors.is_empty() => Ok(result),
906 (_, mut errors) => Err(errors
907 .drain(..)
908 .next()
909 .unwrap_or(EdifactError::MissingSegment {
910 tag: "UNB".to_owned(),
911 expected_position: "first segment of interchange".to_owned(),
912 })),
913 }
914}
915
916fn validate_envelope_lenient_impl<S: SegmentReader>(segments: &[S]) -> LenientResult {
917 let (interchange, errors) = validate_envelope_collecting(segments);
918 LenientResult {
919 interchange,
920 errors,
921 }
922}
923
924fn extract_interchange<S: SegmentReader>(
927 segments: &[S],
928 sink: &mut ErrorSink,
929) -> Result<InterchangeEnvelope, EdifactError> {
930 if segments.first().map(|s| s.tag()) != Some("UNB") {
931 return Err(EdifactError::MissingSegment {
932 tag: "UNB".to_owned(),
933 expected_position: "first segment of interchange".to_owned(),
934 });
935 }
936 if segments.last().map(|s| s.tag()) != Some("UNZ") {
937 return Err(EdifactError::MissingSegment {
938 tag: "UNZ".to_owned(),
939 expected_position: "last segment of interchange".to_owned(),
940 });
941 }
942
943 let unb = &segments[0];
944 let unz = &segments[segments.len() - 1];
945
946 let syntax_identifier = sink.required(unb, 0, 0);
947 let syntax_version = unb.component(0, 1).unwrap_or("").to_owned();
948
949 if !syntax_identifier.is_empty() {
955 if let Err(error) = crate::Charset::from_syntax_identifier(&syntax_identifier) {
956 sink.push(error);
957 }
958 }
959
960 let sender_id = sink.required(unb, 1, 0);
961 let sender_qualifier = unb.component(1, 1).unwrap_or("").to_owned();
962 let sender_routing_address = unb
964 .component(1, 2)
965 .filter(|s| !s.is_empty())
966 .map(str::to_owned);
967
968 let recipient_id = sink.required(unb, 2, 0);
969 let recipient_qualifier = unb.component(2, 1).unwrap_or("").to_owned();
970 let recipient_routing_address = unb
972 .component(2, 2)
973 .filter(|s| !s.is_empty())
974 .map(str::to_owned);
975
976 let date = sink.required(unb, 3, 0);
977 let time_raw = unb.component(3, 1).unwrap_or("");
978 let time = if time_raw.is_empty() {
979 None
980 } else {
981 Some(time_raw.to_owned())
982 };
983
984 let control_ref = sink.required(unb, 4, 0);
985
986 let recipient_password = unb
988 .component(5, 0)
989 .filter(|s| !s.is_empty())
990 .map(str::to_owned);
991 let recipient_password_qualifier = unb
992 .component(5, 1)
993 .filter(|s| !s.is_empty())
994 .map(str::to_owned);
995 let app_ref = unb
997 .component(6, 0)
998 .filter(|s| !s.is_empty())
999 .map(str::to_owned);
1000 let processing_priority = unb
1002 .component(7, 0)
1003 .filter(|s| !s.is_empty())
1004 .map(str::to_owned);
1005 let acknowledgement_request = unb.component(8, 0) == Some("1");
1007 let communications_agreement_id = unb
1009 .component(9, 0)
1010 .filter(|s| !s.is_empty())
1011 .map(str::to_owned);
1012 let test_indicator = unb.component(10, 0) == Some("1");
1014
1015 let unz_control_ref = sink.required(unz, 1, 0);
1016 if unz_control_ref != control_ref {
1017 sink.push(EdifactError::QualifierMismatch {
1018 tag: "UNZ".to_owned(),
1019 actual: unz_control_ref,
1020 expected: control_ref.clone(),
1021 span: unz.span(),
1022 });
1023 }
1024
1025 let declared_unit_count_raw = sink.required(unz, 0, 0);
1026 let declared_unit_count: u32 = sink.recover(
1027 declared_unit_count_raw
1028 .parse()
1029 .map_err(|_| EdifactError::InvalidText {
1030 offset: unz.span().start,
1031 }),
1032 0,
1033 );
1034
1035 Ok(InterchangeEnvelope {
1036 syntax_identifier,
1037 syntax_version,
1038 sender_id,
1039 sender_qualifier,
1040 sender_routing_address,
1041 recipient_id,
1042 recipient_qualifier,
1043 recipient_routing_address,
1044 date,
1045 time,
1046 control_ref,
1047 recipient_password,
1048 recipient_password_qualifier,
1049 app_ref,
1050 processing_priority,
1051 acknowledgement_request,
1052 communications_agreement_id,
1053 test_indicator,
1054 declared_unit_count,
1055 actual_unit_count: 0,
1056 })
1057}
1058
1059fn extract_content<S: SegmentReader>(
1062 inner: &[S],
1063 sink: &mut ErrorSink,
1064) -> Result<(Vec<FunctionalGroupEnvelope>, Vec<MessageEnvelope>), EdifactError> {
1065 if inner.first().is_some_and(|s| s.tag() == "UNG") {
1069 let groups = extract_with_groups(inner, sink)?;
1070 let messages = groups
1071 .iter()
1072 .flat_map(|g| g.messages.iter().cloned())
1073 .collect();
1074 Ok((groups, messages))
1075 } else {
1076 let mut seen_refs = HashSet::new();
1077 let messages = extract_messages_flat(inner, sink, &mut seen_refs)?;
1078 Ok((vec![], messages))
1079 }
1080}
1081
1082fn find_matching_une<S: SegmentReader>(
1084 segments: &[S],
1085 start: usize,
1086) -> Result<usize, EdifactError> {
1087 for (offset, seg) in segments[start..].iter().enumerate() {
1088 match seg.tag() {
1089 "UNE" => return Ok(start + offset),
1090 "UNG" => {
1091 return Err(EdifactError::InvalidSegmentForMessage {
1092 tag: "UNG".to_owned(),
1093 message_type: "ENVELOPE".to_owned(),
1094 span: seg.span(),
1095 });
1096 }
1097 _ => {}
1098 }
1099 }
1100 Err(EdifactError::MissingSegment {
1101 tag: "UNE".to_owned(),
1102 expected_position: "end of functional group".to_owned(),
1103 })
1104}
1105
1106fn extract_with_groups<S: SegmentReader>(
1107 inner: &[S],
1108 sink: &mut ErrorSink,
1109) -> Result<Vec<FunctionalGroupEnvelope>, EdifactError> {
1110 let mut groups: Vec<FunctionalGroupEnvelope> = Vec::new();
1111 let mut seen_group_refs: HashSet<String> = HashSet::new();
1114 let mut seen_message_refs: HashSet<String> = HashSet::new();
1115 let mut i = 0;
1116
1117 while i < inner.len() {
1118 let seg = &inner[i];
1119 match seg.tag() {
1120 "UNG" => {
1121 let ung_idx = i;
1122 let une_idx = find_matching_une(inner, ung_idx + 1)?;
1123
1124 let ung = &inner[ung_idx];
1125 let group_id = ung.component(0, 0).unwrap_or("").to_owned();
1126 let app_sender = ung.component(1, 0).unwrap_or("").to_owned();
1127 let app_sender_qualifier = ung.component(1, 1).unwrap_or("").to_owned();
1128 let app_recipient = ung.component(2, 0).unwrap_or("").to_owned();
1129 let app_recipient_qualifier = ung.component(2, 1).unwrap_or("").to_owned();
1130 let date = ung.component(3, 0).unwrap_or("").to_owned();
1131 let time_raw = ung.component(3, 1).unwrap_or("");
1132 let time = if time_raw.is_empty() {
1133 None
1134 } else {
1135 Some(time_raw.to_owned())
1136 };
1137 let group_ref = sink.required(ung, 4, 0);
1139 if !seen_group_refs.insert(group_ref.clone()) {
1140 sink.push(EdifactError::DuplicateReference {
1141 tag: "UNG".to_owned(),
1142 reference: group_ref.clone(),
1143 span: ung.span(),
1144 });
1145 }
1146 let controlling_agency = ung.component(5, 0).unwrap_or("").to_owned();
1147 let version = ung.component(6, 0).unwrap_or("").to_owned();
1149 let release = ung.component(6, 1).unwrap_or("").to_owned();
1150 let application_password = ung
1152 .component(7, 0)
1153 .filter(|s| !s.is_empty())
1154 .map(str::to_owned);
1155
1156 let une = &inner[une_idx];
1157 let declared_str = sink.required(une, 0, 0);
1158 let declared_message_count: u32 = sink.recover(
1159 declared_str.parse().map_err(|_| EdifactError::InvalidText {
1160 offset: une.span().start,
1161 }),
1162 0,
1163 );
1164 let une_ref = sink.required(une, 1, 0);
1165 if une_ref != group_ref {
1166 sink.push(EdifactError::QualifierMismatch {
1167 tag: "UNE".to_owned(),
1168 actual: une_ref,
1169 expected: group_ref.clone(),
1170 span: une.span(),
1171 });
1172 }
1173
1174 let group_content = &inner[ung_idx + 1..une_idx];
1175 let messages = extract_messages_flat(group_content, sink, &mut seen_message_refs)?;
1176 let actual_message_count = sink.recover(
1177 u32::try_from(messages.len()).map_err(|_| EdifactError::InterchangeTooLarge {
1178 count: messages.len() as u64,
1179 }),
1180 u32::MAX,
1181 );
1182
1183 if declared_message_count != actual_message_count {
1184 sink.push(EdifactError::MessageCountMismatch {
1185 expected: declared_message_count,
1186 actual: actual_message_count,
1187 });
1188 }
1189
1190 groups.push(FunctionalGroupEnvelope {
1191 group_id,
1192 app_sender,
1193 app_sender_qualifier,
1194 app_recipient,
1195 app_recipient_qualifier,
1196 date,
1197 time,
1198 group_ref,
1199 controlling_agency,
1200 version,
1201 release,
1202 application_password,
1203 declared_message_count,
1204 actual_message_count,
1205 messages,
1206 });
1207 i = une_idx + 1;
1208 }
1209 "UNE" => {
1210 return Err(EdifactError::InvalidSegmentForMessage {
1211 tag: "UNE".to_owned(),
1212 message_type: "ENVELOPE".to_owned(),
1213 span: seg.span(),
1214 });
1215 }
1216 "UNH" => {
1217 return Err(EdifactError::GroupsAndMessagesMixed { span: seg.span() });
1222 }
1223 "UNO" | "UNP" => {
1224 return Err(EdifactError::PackageNotSupported {
1225 tag: seg.tag().to_owned(),
1226 span: seg.span(),
1227 });
1228 }
1229 _ => {
1230 return Err(EdifactError::InvalidSegmentForMessage {
1231 tag: seg.tag().to_owned(),
1232 message_type: "ENVELOPE".to_owned(),
1233 span: seg.span(),
1234 });
1235 }
1236 }
1237 }
1238
1239 Ok(groups)
1240}
1241
1242fn extract_messages_flat<S: SegmentReader>(
1244 segments: &[S],
1245 sink: &mut ErrorSink,
1246 seen_refs: &mut HashSet<String>,
1247) -> Result<Vec<MessageEnvelope>, EdifactError> {
1248 let mut messages: Vec<MessageEnvelope> = Vec::new();
1249 let mut unh_idx: Option<usize> = None;
1253
1254 for (i, seg) in segments.iter().enumerate() {
1255 match seg.tag() {
1256 "UNH" => {
1257 if unh_idx.is_some() {
1258 return Err(EdifactError::InvalidSegmentForMessage {
1259 tag: "UNH".to_owned(),
1260 message_type: "ENVELOPE".to_owned(),
1261 span: seg.span(),
1262 });
1263 }
1264 unh_idx = Some(i);
1265 }
1266 "UNT" if unh_idx.is_some() => {
1267 let msg_start_idx = unh_idx.take().expect("guarded by the match arm");
1268 let unh = &segments[msg_start_idx];
1269
1270 let message_ref = sink.required(unh, 0, 0);
1271 if !seen_refs.insert(message_ref.clone()) {
1272 sink.push(EdifactError::DuplicateReference {
1273 tag: "UNH".to_owned(),
1274 reference: message_ref.clone(),
1275 span: unh.span(),
1276 });
1277 }
1278 let message_type = sink.required(unh, 1, 0);
1279 let version = sink.required(unh, 1, 1);
1280 let release = sink.required(unh, 1, 2);
1281 let controlling_agency = sink.required(unh, 1, 3);
1282 let association_code = unh.component(1, 4).unwrap_or("").to_owned();
1283 let common_access_ref = unh
1285 .component(2, 0)
1286 .filter(|s| !s.is_empty())
1287 .map(str::to_owned);
1288 let sequence_of_transfers = unh
1291 .component(3, 0)
1292 .filter(|s| !s.is_empty())
1293 .and_then(|s| s.parse::<u32>().ok());
1294 let transfer_position = unh
1295 .component(3, 1)
1296 .filter(|s| !s.is_empty())
1297 .map(str::to_owned);
1298
1299 let declared_raw = sink.required(seg, 0, 0);
1300 let declared_segment_count: u32 = sink.recover(
1301 declared_raw.parse().map_err(|_| EdifactError::InvalidText {
1302 offset: seg.span().start,
1303 }),
1304 0,
1305 );
1306 let unt_ref = sink.required(seg, 1, 0);
1307 if unt_ref != message_ref {
1308 sink.push(EdifactError::QualifierMismatch {
1309 tag: "UNT".to_owned(),
1310 actual: unt_ref,
1311 expected: message_ref.clone(),
1312 span: seg.span(),
1313 });
1314 }
1315
1316 let segment_span = i - msg_start_idx + 1;
1317 let actual_segment_count = sink.recover(
1318 u32::try_from(segment_span).map_err(|_| EdifactError::InterchangeTooLarge {
1319 count: segment_span as u64,
1320 }),
1321 u32::MAX,
1322 );
1323
1324 if segment_span < 3 {
1329 sink.push(EdifactError::EmptyMessage {
1330 message_ref: message_ref.clone(),
1331 span: unh.span(),
1332 });
1333 }
1334
1335 messages.push(MessageEnvelope {
1336 message_ref,
1337 message_type,
1338 version,
1339 release,
1340 controlling_agency,
1341 association_code,
1342 common_access_ref,
1343 sequence_of_transfers,
1344 transfer_position,
1345 declared_segment_count,
1346 actual_segment_count,
1347 header_span: unh.span(),
1348 trailer_span: seg.span(),
1349 });
1350 }
1351 "UNT" => {
1352 return Err(EdifactError::InvalidSegmentForMessage {
1353 tag: "UNT".to_owned(),
1354 message_type: "ENVELOPE".to_owned(),
1355 span: seg.span(),
1356 });
1357 }
1358 "UNO" | "UNP" => {
1364 return Err(EdifactError::PackageNotSupported {
1365 tag: seg.tag().to_owned(),
1366 span: seg.span(),
1367 });
1368 }
1369 "UNB" | "UNZ" | "UNG" | "UNE" if unh_idx.is_some() => {
1370 return Err(EdifactError::InvalidSegmentForMessage {
1371 tag: seg.tag().to_owned(),
1372 message_type: "ENVELOPE".to_owned(),
1373 span: seg.span(),
1374 });
1375 }
1376 _ if unh_idx.is_none() => {
1377 return Err(EdifactError::InvalidSegmentForMessage {
1378 tag: seg.tag().to_owned(),
1379 message_type: "ENVELOPE".to_owned(),
1380 span: seg.span(),
1381 });
1382 }
1383 _ => {}
1384 }
1385 }
1386
1387 if unh_idx.is_some() {
1388 return Err(EdifactError::MissingSegment {
1389 tag: "UNT".to_owned(),
1390 expected_position: "end of message group".to_owned(),
1391 });
1392 }
1393
1394 Ok(messages)
1395}
1396
1397#[cfg(test)]
1400mod tests {
1401 use super::*;
1402
1403 fn parse(input: &[u8]) -> Vec<crate::OwnedSegment> {
1404 crate::from_reader_collect(std::io::Cursor::new(input)).expect("parse failed")
1405 }
1406
1407 fn parse_and_validate(input: &[u8]) -> Result<ValidatedInterchange, EdifactError> {
1408 let owned = parse(input);
1409 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1410 validate_envelope(&segs)
1411 }
1412
1413 fn parse_and_validate_lenient(input: &[u8]) -> LenientResult {
1414 let owned = parse(input);
1415 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1416 validate_envelope_lenient(&segs)
1417 }
1418
1419 #[test]
1420 fn lenient_collects_every_violation_not_just_the_first() {
1421 let input = b"UNB+UNOA:1+S+R+200101:0900+CTRL1'\
1425 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+99+1'\
1426 UNZ+1+CTRL2'";
1427 let result = parse_and_validate_lenient(input);
1428 assert!(
1429 result.errors.len() >= 2,
1430 "expected both violations, got {:?}",
1431 result.errors
1432 );
1433 assert!(
1434 result
1435 .errors
1436 .iter()
1437 .any(|e| matches!(e, EdifactError::QualifierMismatch { tag, .. } if tag == "UNZ")),
1438 "missing UNZ control-ref mismatch in {:?}",
1439 result.errors
1440 );
1441 assert!(
1442 result
1443 .errors
1444 .iter()
1445 .any(|e| matches!(e, EdifactError::SegmentCountMismatch { .. })),
1446 "missing UNT segment-count mismatch in {:?}",
1447 result.errors
1448 );
1449 }
1450
1451 #[test]
1452 fn strict_reports_the_first_error_lenient_collects() {
1453 for input in [
1456 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+2+1'UNZ+9+C1'"[..],
1457 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+99+1'UNZ+1+C1'"[..],
1458 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+2+9'UNZ+1+C1'"[..],
1459 ] {
1460 let strict = parse_and_validate(input);
1461 let lenient = parse_and_validate_lenient(input);
1462 match strict {
1463 Err(e) => assert_eq!(
1464 Some(&e),
1465 lenient.errors.first(),
1466 "strict/lenient diverged for {:?}",
1467 std::str::from_utf8(input).unwrap()
1468 ),
1469 Ok(_) => assert!(lenient.errors.is_empty()),
1470 }
1471 }
1472 }
1473
1474 #[test]
1475 fn duplicate_message_references_are_rejected() {
1476 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'\
1477 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+3+1'\
1478 UNH+1+ORDERS:D:96A:UN'BGM+221'UNT+3+1'\
1479 UNZ+2+C1'";
1480 let err = parse_and_validate(input).expect_err("duplicate UNH refs must fail");
1481 assert!(
1482 matches!(&err, EdifactError::DuplicateReference { tag, reference, .. }
1483 if tag == "UNH" && reference == "1"),
1484 "got {err:?}"
1485 );
1486 }
1487
1488 #[test]
1489 fn distinct_message_references_are_accepted() {
1490 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'\
1491 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+3+1'\
1492 UNH+2+ORDERS:D:96A:UN'BGM+221'UNT+3+2'\
1493 UNZ+2+C1'";
1494 parse_and_validate(input).expect("distinct refs must validate");
1495 }
1496
1497 #[test]
1498 fn into_strict_is_total_after_the_caller_filters_errors() {
1499 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'UNZ+1+C2'";
1502 let mut lenient = parse_and_validate_lenient(input);
1503 lenient.errors.clear();
1504 let _ = lenient.into_strict();
1506 }
1507
1508 fn parse_and_validate_owned(input: &[u8]) -> Result<ValidatedInterchange, EdifactError> {
1509 validate_envelope_from_owned(&parse(input))
1510 }
1511
1512 const VALID_INTERCHANGE: &[u8] =
1513 b"UNA:+.? 'UNB+UNOA:3+SENDER::293+RECEIVER::293+230401:0900+00001'UNH+00001+ORDERS:D:11A:UN:EAN010'BGM+220+PO-4711+9'DTM+137:20230401:102'UNT+4+00001'UNZ+1+00001'";
1514
1515 #[test]
1516 fn valid_envelope_parses_ok() {
1517 let result = parse_and_validate(VALID_INTERCHANGE).expect("envelope should be valid");
1518 assert_eq!(result.interchange.sender_id, "SENDER");
1519 assert_eq!(result.interchange.sender_qualifier, ""); assert_eq!(result.interchange.recipient_id, "RECEIVER");
1521 assert_eq!(result.interchange.recipient_qualifier, "");
1522 assert_eq!(result.interchange.syntax_identifier, "UNOA");
1523 assert_eq!(result.interchange.syntax_version, "3");
1524 assert_eq!(result.interchange.control_ref, "00001");
1525 assert_eq!(result.interchange.declared_unit_count, 1);
1526 assert_eq!(result.interchange.actual_unit_count, 1);
1527 assert!(!result.interchange.is_test());
1528 assert!(!result.has_functional_groups());
1529 assert_eq!(result.message_count(), 1);
1530 assert_eq!(result.messages[0].message_type, "ORDERS");
1531 assert_eq!(result.messages[0].association_code, "EAN010");
1532 assert_eq!(result.messages[0].declared_segment_count, 4);
1533 assert_eq!(result.messages[0].actual_segment_count, 4);
1534 }
1535
1536 #[test]
1537 fn valid_envelope_parses_ok_owned_path() {
1538 let result = parse_and_validate_owned(VALID_INTERCHANGE).expect("envelope should be valid");
1539 assert_eq!(result.interchange.sender_id, "SENDER");
1540 assert_eq!(result.interchange.actual_unit_count, 1);
1541 assert_eq!(result.messages[0].declared_segment_count, 4);
1542 }
1543
1544 #[test]
1545 fn unt_count_mismatch_returns_err() {
1546 let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'DTM+137:20200101:102'UNT+99+1'UNZ+1+1'";
1547 let result = parse_and_validate(input);
1548 assert!(
1549 matches!(
1550 result,
1551 Err(EdifactError::SegmentCountMismatch { expected: 99, .. })
1552 ),
1553 "expected SegmentCountMismatch, got {result:?}"
1554 );
1555 }
1556
1557 #[test]
1558 fn unz_count_mismatch_returns_err() {
1559 let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+2+1'";
1560 let result = parse_and_validate(input);
1561 assert!(
1562 matches!(
1563 result,
1564 Err(EdifactError::MessageCountMismatch {
1565 expected: 2,
1566 actual: 1
1567 })
1568 ),
1569 "expected MessageCountMismatch(2,1), got {result:?}"
1570 );
1571 }
1572
1573 #[test]
1574 fn missing_unb_returns_err() {
1575 let input = b"UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
1576 assert!(parse_and_validate(input).is_err());
1577 }
1578
1579 #[test]
1580 fn extracts_una_interchange_correctly() {
1581 let result = parse_and_validate(VALID_INTERCHANGE).unwrap();
1582 assert_eq!(result.interchange.syntax_identifier, "UNOA");
1583 assert_eq!(result.interchange.syntax_version, "3");
1584 assert_eq!(result.interchange.date, "230401");
1585 assert_eq!(result.interchange.time.as_deref(), Some("0900"));
1586 }
1587
1588 #[test]
1589 fn sender_and_recipient_qualifiers_extracted() {
1590 let input = b"UNB+UNOA:3+1234567890123:14+9876543210987:14+200101:0900+1'\
1592 UNH+1+ORDERS:D:96A:UN'\
1593 BGM+220+PO-001+9'\
1594 UNT+3+1'\
1595 UNZ+1+1'";
1596 let r = parse_and_validate(input).expect("GLN-qualified UNB must parse ok");
1597 assert_eq!(r.interchange.sender_id, "1234567890123");
1598 assert_eq!(r.interchange.sender_qualifier, "14");
1599 assert_eq!(r.interchange.recipient_id, "9876543210987");
1600 assert_eq!(r.interchange.recipient_qualifier, "14");
1601 }
1602
1603 #[test]
1607 fn test_indicator_parsed_from_unb() {
1608 let input = b"UNB+UNOA:3+S+R+200101:0900+1++++++1'\
1612 UNH+1+ORDERS:D:96A:UN'\
1613 BGM+220+PO-001+9'\
1614 UNT+3+1'\
1615 UNZ+1+1'";
1616 let r = parse_and_validate(input).expect("test-flagged UNB must parse ok");
1617 assert!(
1618 r.interchange.test_indicator,
1619 "test_indicator should be true"
1620 );
1621 assert!(r.interchange.is_test(), "is_test() convenience must agree");
1622 }
1623
1624 #[test]
1625 fn no_test_indicator_defaults_false() {
1626 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1627 assert!(!r.interchange.test_indicator);
1628 assert!(!r.interchange.is_test());
1629 }
1630
1631 #[test]
1632 fn app_ref_extracted_when_present() {
1633 let input = b"UNB+UNOA:3+S+R+200101:0900+1++MYAPP'\
1635 UNH+1+ORDERS:D:96A:UN'\
1636 BGM+220+PO-001+9'\
1637 UNT+3+1'\
1638 UNZ+1+1'";
1639 let r = parse_and_validate(input).expect("UNB with app_ref must parse ok");
1640 assert_eq!(r.interchange.app_ref.as_deref(), Some("MYAPP"));
1641 }
1642
1643 #[test]
1644 fn app_ref_is_none_when_absent() {
1645 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1646 assert!(r.interchange.app_ref.is_none());
1647 }
1648
1649 #[test]
1650 fn recipient_password_extracted_when_present() {
1651 let input = b"UNB+UNOA:3+S+R+200101:0900+1+MYPASS'\
1653 UNH+1+ORDERS:D:96A:UN'\
1654 BGM+220+PO-001+9'\
1655 UNT+3+1'\
1656 UNZ+1+1'";
1657 let r = parse_and_validate(input).expect("UNB with password must parse ok");
1658 assert_eq!(r.interchange.recipient_password.as_deref(), Some("MYPASS"));
1659 }
1660
1661 #[test]
1662 fn recipient_password_is_none_when_absent() {
1663 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1664 assert!(r.interchange.recipient_password.is_none());
1665 }
1666
1667 #[test]
1668 fn acknowledgement_request_flag_parsed() {
1669 let input = b"UNB+UNOA:3+S+R+200101:0900+1++++1'\
1672 UNH+1+ORDERS:D:96A:UN'\
1673 BGM+220+PO-001+9'\
1674 UNT+3+1'\
1675 UNZ+1+1'";
1676 let r = parse_and_validate(input).expect("UNB with ack-request must parse ok");
1677 assert!(r.interchange.acknowledgement_request);
1678 assert!(r.interchange.ack_requested());
1679 }
1680
1681 #[test]
1682 fn acknowledgement_request_defaults_false() {
1683 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1684 assert!(!r.interchange.acknowledgement_request);
1685 assert!(!r.interchange.ack_requested());
1686 }
1687
1688 #[test]
1689 fn communications_agreement_id_extracted() {
1690 let input = b"UNB+UNOA:3+S+R+200101:0900+1+++++AGREEMENT-1'\
1692 UNH+1+ORDERS:D:96A:UN'\
1693 BGM+220+PO-001+9'\
1694 UNT+3+1'\
1695 UNZ+1+1'";
1696 let r = parse_and_validate(input).expect("UNB with comms-agreement must parse ok");
1697 assert_eq!(
1698 r.interchange.communications_agreement_id.as_deref(),
1699 Some("AGREEMENT-1")
1700 );
1701 }
1702
1703 #[test]
1704 fn dangling_unh_without_unt_returns_err() {
1705 let input =
1706 b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNZ+1+1'";
1707 let result = parse_and_validate(input);
1708 assert!(
1709 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNT")
1710 );
1711 }
1712
1713 #[test]
1714 fn stray_segment_outside_message_returns_err() {
1715 let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'BGM+999+PO-2+9'UNZ+1+1'";
1716 assert!(matches!(
1717 parse_and_validate(input),
1718 Err(EdifactError::InvalidSegmentForMessage { .. })
1719 ));
1720 }
1721
1722 #[test]
1723 fn missing_unb_sender_component_returns_err() {
1724 let input = b"UNB+UNOA:3++R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
1725 let result = parse_and_validate(input);
1726 assert!(
1727 matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 0 }) if tag == "UNB"),
1728 "expected MissingRequiredComponent for empty sender, got: {result:?}"
1729 );
1730 }
1731
1732 #[test]
1733 fn nested_unh_without_closing_previous_message_returns_err() {
1734 let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNH+2+ORDERS:D:11A:UN:EAN010'UNT+3+2'UNZ+1+1'";
1735 let result = parse_and_validate(input);
1736 assert!(
1737 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNH"),
1738 "expected InvalidSegmentForMessage(UNH), got {result:?}"
1739 );
1740 }
1741
1742 #[test]
1743 fn unt_message_reference_must_match_unh() {
1744 let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+999'UNZ+1+1'";
1745 let result = parse_and_validate(input);
1746 assert!(
1747 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNT")
1748 );
1749 }
1750
1751 #[test]
1752 fn unz_control_reference_must_match_unb() {
1753 let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+999'";
1754 let result = parse_and_validate(input);
1755 assert!(
1756 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNZ")
1757 );
1758 }
1759
1760 #[test]
1761 fn missing_unh_message_type_components_return_err() {
1762 let input =
1763 b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
1764 let result = parse_and_validate(input);
1765 assert!(
1766 matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 3 }) if tag == "UNH"),
1767 "got: {result:?}"
1768 );
1769 }
1770
1771 #[test]
1772 fn nested_unz_inside_message_returns_err() {
1773 let input =
1774 b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'UNZ+1+1'UNT+2+1'UNZ+1+1'";
1775 let result = parse_and_validate(input);
1776 assert!(
1777 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNZ")
1778 );
1779 }
1780
1781 #[test]
1782 fn lenient_returns_partial_result_on_count_mismatch() {
1783 let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+2+1'";
1786 let owned = parse(input);
1787 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1788 let lenient = validate_envelope_lenient(&segs);
1789 let result = lenient.interchange;
1790 let errors = lenient.errors;
1791 assert!(
1792 result.is_some(),
1793 "lenient mode must return Some even on count mismatch"
1794 );
1795 assert_eq!(errors.len(), 1);
1796 assert!(
1797 matches!(
1798 &errors[0],
1799 EdifactError::MessageCountMismatch {
1800 expected: 2,
1801 actual: 1
1802 }
1803 ),
1804 "expected MessageCountMismatch(2,1), got {:?}",
1805 errors[0]
1806 );
1807 let partial = result.unwrap();
1808 assert_eq!(partial.messages.len(), 1);
1809 assert_eq!(partial.interchange.actual_unit_count, 1);
1810 assert_eq!(partial.interchange.declared_unit_count, 2);
1811 }
1812
1813 #[test]
1814 fn lenient_returns_none_on_structural_error() {
1815 let input = b"UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
1817 let owned = parse(input);
1818 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1819 let lenient = validate_envelope_lenient(&segs);
1820 let result = lenient.interchange;
1821 let errors = lenient.errors;
1822 assert!(result.is_none(), "missing UNB must yield None");
1823 assert!(!errors.is_empty());
1824 }
1825
1826 #[test]
1827 fn message_count_convenience_method() {
1828 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1829 assert_eq!(r.message_count(), r.messages.len());
1830 assert_eq!(r.message_count(), 1);
1831 }
1832
1833 #[test]
1834 fn interchange_with_single_functional_group_parses_ok() {
1835 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1836 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1837 UNH+1+ORDERS:D:96A:UN'\
1838 BGM+220+PO-001+9'\
1839 UNT+3+1'\
1840 UNE+1+1'\
1841 UNZ+1+1'";
1842 let result = parse_and_validate(input).expect("single-group interchange must parse ok");
1843 assert!(result.has_functional_groups());
1844 assert_eq!(result.functional_groups.len(), 1);
1845 let g = &result.functional_groups[0];
1846 assert_eq!(g.group_id, "ORDERS");
1847 assert_eq!(g.group_ref, "1");
1848 assert_eq!(g.controlling_agency, "UN");
1849 assert_eq!(g.declared_message_count, 1);
1850 assert_eq!(g.actual_message_count, 1);
1851 assert_eq!(result.messages.len(), 1);
1852 assert_eq!(result.messages[0].message_type, "ORDERS");
1853 assert_eq!(result.interchange.declared_unit_count, 1);
1854 assert_eq!(result.interchange.actual_unit_count, 1);
1855 }
1856
1857 #[test]
1858 fn interchange_with_multi_message_group_parses_ok() {
1859 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1862 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1863 UNH+1+ORDERS:D:96A:UN'\
1864 BGM+220+PO-001+9'\
1865 UNT+3+1'\
1866 UNH+2+ORDERS:D:96A:UN'\
1867 BGM+220+PO-002+9'\
1868 UNT+3+2'\
1869 UNE+2+1'\
1870 UNZ+1+1'";
1871 let result = parse_and_validate(input).expect("multi-message group must parse ok");
1872 assert!(result.has_functional_groups());
1873 assert_eq!(result.functional_groups.len(), 1);
1874 assert_eq!(result.functional_groups[0].actual_message_count, 2);
1875 assert_eq!(result.messages.len(), 2);
1876 assert_eq!(result.interchange.actual_unit_count, 1);
1878 assert_eq!(result.interchange.declared_unit_count, 1);
1879 }
1880
1881 #[test]
1882 fn interchange_with_multiple_groups_parses_ok() {
1883 let input = b"UNB+UNOA:3+S+R+200101:0900+2'\
1884 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1885 UNH+1+ORDERS:D:96A:UN'\
1886 BGM+220+PO-001+9'\
1887 UNT+3+1'\
1888 UNE+1+1'\
1889 UNG+INVOIC+S+R+200101:0900+2+UN+D:96A'\
1890 UNH+2+INVOIC:D:96A:UN'\
1891 BGM+380+INV-001+9'\
1892 UNT+3+2'\
1893 UNE+1+2'\
1894 UNZ+2+2'";
1895 let result = parse_and_validate(input).expect("multi-group interchange must parse ok");
1896 assert_eq!(result.functional_groups.len(), 2);
1897 assert_eq!(result.functional_groups[0].group_id, "ORDERS");
1898 assert_eq!(result.functional_groups[1].group_id, "INVOIC");
1899 assert_eq!(result.messages.len(), 2);
1900 assert_eq!(result.interchange.declared_unit_count, 2);
1901 assert_eq!(result.interchange.actual_unit_count, 2);
1902 }
1903
1904 #[test]
1905 fn ung_une_count_mismatch_returns_err() {
1906 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1907 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1908 UNH+1+ORDERS:D:96A:UN'\
1909 BGM+220+PO-001+9'\
1910 UNT+3+1'\
1911 UNE+2+1'\
1912 UNZ+1+1'";
1913 let result = parse_and_validate(input);
1914 assert!(
1915 matches!(
1916 result,
1917 Err(EdifactError::MessageCountMismatch {
1918 expected: 2,
1919 actual: 1
1920 })
1921 ),
1922 "expected MessageCountMismatch(2,1), got {result:?}"
1923 );
1924 }
1925
1926 #[test]
1927 fn une_without_ung_returns_err() {
1928 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1929 UNE+1+1'\
1930 UNH+1+ORDERS:D:96A:UN'\
1931 BGM+220+PO-001+9'\
1932 UNT+3+1'\
1933 UNZ+1+1'";
1934 let result = parse_and_validate(input);
1935 assert!(
1936 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNE"),
1937 "expected InvalidSegmentForMessage(UNE), got {result:?}"
1938 );
1939 }
1940
1941 #[test]
1942 fn ung_without_une_returns_err() {
1943 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1944 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1945 UNH+1+ORDERS:D:96A:UN'\
1946 BGM+220+PO-001+9'\
1947 UNT+3+1'\
1948 UNZ+1+1'";
1949 let result = parse_and_validate(input);
1950 assert!(
1951 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNE"),
1952 "expected MissingSegment(UNE), got {result:?}"
1953 );
1954 }
1955
1956 #[test]
1957 fn une_group_ref_must_match_ung() {
1958 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1959 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1960 UNH+1+ORDERS:D:96A:UN'\
1961 BGM+220+PO-001+9'\
1962 UNT+3+1'\
1963 UNE+1+999'\
1964 UNZ+1+1'";
1965 let result = parse_and_validate(input);
1966 assert!(
1967 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNE"),
1968 "expected QualifierMismatch(UNE), got {result:?}"
1969 );
1970 }
1971
1972 #[test]
1975 fn processing_priority_extracted_when_present() {
1976 let input = b"UNB+UNOA:3+S+R+200101:0900+1+++A'\
1978 UNH+1+ORDERS:D:96A:UN'\
1979 BGM+220+PO-001+9'\
1980 UNT+3+1'\
1981 UNZ+1+1'";
1982 let r = parse_and_validate(input).expect("UNB with processing_priority must parse ok");
1983 assert_eq!(r.interchange.processing_priority.as_deref(), Some("A"));
1984 }
1985
1986 #[test]
1987 fn processing_priority_is_none_when_absent() {
1988 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1989 assert!(r.interchange.processing_priority.is_none());
1990 }
1991
1992 #[test]
1993 fn sender_routing_address_extracted_when_present() {
1994 let input = b"UNB+UNOA:3+SENDER:14:ROUTEA+RECIP+200101:0900+1'\
1996 UNH+1+ORDERS:D:96A:UN'\
1997 BGM+220+PO-001+9'\
1998 UNT+3+1'\
1999 UNZ+1+1'";
2000 let r = parse_and_validate(input).expect("UNB with sender routing must parse ok");
2001 assert_eq!(
2002 r.interchange.sender_routing_address.as_deref(),
2003 Some("ROUTEA")
2004 );
2005 assert!(r.interchange.recipient_routing_address.is_none());
2006 }
2007
2008 #[test]
2009 fn recipient_routing_address_extracted_when_present() {
2010 let input = b"UNB+UNOA:3+SENDER+RECIP:14:ROUTEB+200101:0900+1'\
2012 UNH+1+ORDERS:D:96A:UN'\
2013 BGM+220+PO-001+9'\
2014 UNT+3+1'\
2015 UNZ+1+1'";
2016 let r = parse_and_validate(input).expect("UNB with recipient routing must parse ok");
2017 assert!(r.interchange.sender_routing_address.is_none());
2018 assert_eq!(
2019 r.interchange.recipient_routing_address.as_deref(),
2020 Some("ROUTEB")
2021 );
2022 }
2023
2024 #[test]
2025 fn routing_address_is_none_when_absent() {
2026 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2028 UNH+1+ORDERS:D:96A:UN'\
2029 BGM+220+PO-001+9'\
2030 UNT+3+1'\
2031 UNZ+1+1'";
2032 let r = parse_and_validate(input).unwrap();
2033 assert!(r.interchange.sender_routing_address.is_none());
2034 assert!(r.interchange.recipient_routing_address.is_none());
2035 }
2036
2037 #[test]
2038 fn common_access_ref_extracted_when_present() {
2039 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2041 UNH+1+ORDERS:D:96A:UN+COMREF1'\
2042 BGM+220+PO-001+9'\
2043 UNT+3+1'\
2044 UNZ+1+1'";
2045 let r = parse_and_validate(input).expect("UNH with common_access_ref must parse ok");
2046 assert_eq!(r.messages[0].common_access_ref.as_deref(), Some("COMREF1"));
2047 }
2048
2049 #[test]
2050 fn common_access_ref_is_none_when_absent() {
2051 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2052 assert!(r.messages[0].common_access_ref.is_none());
2053 }
2054
2055 #[test]
2056 fn parse_unh_includes_message_ref() {
2057 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2058 UNH+REF42+ORDERS:D:96A:UN'\
2059 BGM+220+PO-001+9'\
2060 UNT+3+REF42'\
2061 UNZ+1+1'";
2062 let r = parse_and_validate(input).expect("must parse ok");
2065 assert_eq!(r.messages[0].message_ref, "REF42");
2066 assert_eq!(r.messages[0].message_type, "ORDERS");
2067 }
2068
2069 #[test]
2070 fn iter_messages_by_type_returns_matching_messages() {
2071 let input = b"UNB+UNOA:3+S+R+200101:0900+CTRL2'\
2072 UNH+1+ORDERS:D:96A:UN'\
2073 BGM+220+PO-001+9'\
2074 UNT+3+1'\
2075 UNH+2+INVOIC:D:96A:UN'\
2076 BGM+380+INV-001+9'\
2077 UNT+3+2'\
2078 UNZ+2+CTRL2'";
2079 let r = parse_and_validate(input).expect("two-message interchange must parse ok");
2080 let orders: Vec<_> = r.iter_messages_by_type("ORDERS").collect();
2081 assert_eq!(orders.len(), 1);
2082 assert_eq!(orders[0].message_ref, "1");
2083 let invoices: Vec<_> = r.iter_messages_by_type("INVOIC").collect();
2084 assert_eq!(invoices.len(), 1);
2085 assert_eq!(invoices[0].message_ref, "2");
2086 let none: Vec<_> = r.iter_messages_by_type("DESADV").collect();
2087 assert!(none.is_empty());
2088 }
2089
2090 #[test]
2091 fn display_interchange_envelope() {
2092 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2093 let s = r.interchange.to_string();
2094 assert!(s.contains("SENDER"), "Display must include sender_id");
2095 assert!(s.contains("UNOA"), "Display must include syntax_identifier");
2096 }
2097
2098 #[test]
2099 fn display_message_envelope() {
2100 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2101 let s = r.messages[0].to_string();
2102 assert!(s.contains("ORDERS"), "Display must include message_type");
2103 assert!(s.contains("ref="), "Display must include message ref label");
2104 }
2105
2106 #[test]
2107 fn display_validated_interchange() {
2108 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2109 let s = r.to_string();
2110 assert!(
2111 s.contains("messages="),
2112 "Display must include message count label"
2113 );
2114 }
2115
2116 #[test]
2119 fn unrecognised_syntax_identifier_returns_err() {
2120 let input = b"UNB+XXXX:3+S+R+200101:0900+1'\
2122 UNH+1+ORDERS:D:96A:UN'\
2123 BGM+220+PO-001+9'\
2124 UNT+3+1'\
2125 UNZ+1+1'";
2126 let result = parse_and_validate(input);
2127 assert!(
2128 matches!(result, Err(EdifactError::UnrecognisedSyntaxIdentifier(ref id)) if id == "XXXX"),
2129 "expected UnrecognisedSyntaxIdentifier(\"XXXX\"), got {result:?}"
2130 );
2131 }
2132
2133 #[test]
2134 fn every_repertoire_the_crate_decodes_is_accepted_in_the_unb() {
2135 for id in [
2140 "UNOA", "UNOB", "UNOC", "UNOD", "UNOE", "UNOF", "UNOG", "UNOH", "UNOI", "UNOJ", "UNOK",
2141 "UNOY",
2142 ] {
2143 let input = format!(
2144 "UNB+{id}:3+S+R+200101:0900+1'UNH+1+ORDERS:D:96A:UN'BGM+220+PO-001+9'UNT+3+1'UNZ+1+1'"
2145 );
2146 let result = parse_and_validate(input.as_bytes());
2147 assert!(
2148 result.is_ok(),
2149 "syntax id '{id}' should be accepted, got {result:?}"
2150 );
2151 }
2152 }
2153
2154 #[test]
2155 fn a_defined_but_undecodable_repertoire_says_so() {
2156 for id in ["UNOX", "KECA"] {
2160 let input = format!(
2161 "UNB+{id}:3+S+R+200101:0900+1'UNH+1+ORDERS:D:96A:UN'BGM+220+PO-001+9'UNT+3+1'UNZ+1+1'"
2162 );
2163 let result = parse_and_validate(input.as_bytes());
2164 assert!(
2165 matches!(result, Err(EdifactError::UnsupportedCharset { ref syntax_identifier }) if syntax_identifier == id),
2166 "expected UnsupportedCharset for '{id}', got {result:?}"
2167 );
2168 }
2169 }
2170
2171 #[test]
2172 fn an_interchange_with_no_content_is_rejected() {
2173 let result = parse_and_validate(b"UNB+UNOC:3+S+R+260101:0900+IC1'UNZ+0+IC1'");
2176 assert!(
2177 matches!(result, Err(EdifactError::EmptyInterchange { ref control_ref }) if control_ref == "IC1"),
2178 "expected EmptyInterchange, got {result:?}"
2179 );
2180 }
2181
2182 #[test]
2183 fn a_message_with_no_body_is_rejected() {
2184 let result = parse_and_validate(
2187 b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+1+ORDERS:D:96A:UN'UNT+2+1'UNZ+1+IC1'",
2188 );
2189 assert!(
2190 matches!(result, Err(EdifactError::EmptyMessage { ref message_ref, .. }) if message_ref == "1"),
2191 "expected EmptyMessage, got {result:?}"
2192 );
2193 }
2194
2195 #[test]
2196 fn a_package_is_reported_as_a_package() {
2197 let result = parse_and_validate(
2201 b"UNB+UNOC:4+S+R+260101:0900+IC1'UNO+PKG1+AAF:1+ZZZ+1024'UNZ+1+IC1'",
2202 );
2203 assert!(
2204 matches!(result, Err(EdifactError::PackageNotSupported { ref tag, .. }) if tag == "UNO"),
2205 "expected PackageNotSupported, got {result:?}"
2206 );
2207 }
2208
2209 #[test]
2212 fn recipient_password_qualifier_extracted_when_present() {
2213 let input = b"UNB+UNOA:3+S+R+200101:0900+1+MYPASS:AA'\
2215 UNH+1+ORDERS:D:96A:UN'\
2216 BGM+220+PO-001+9'\
2217 UNT+3+1'\
2218 UNZ+1+1'";
2219 let r = parse_and_validate(input).expect("UNB with password+qualifier must parse ok");
2220 assert_eq!(r.interchange.recipient_password.as_deref(), Some("MYPASS"));
2221 assert_eq!(
2222 r.interchange.recipient_password_qualifier.as_deref(),
2223 Some("AA")
2224 );
2225 }
2226
2227 #[test]
2228 fn recipient_password_qualifier_is_none_when_absent() {
2229 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2230 assert!(r.interchange.recipient_password_qualifier.is_none());
2231 }
2232
2233 #[test]
2236 fn sequence_of_transfers_extracted_when_present() {
2237 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2239 UNH+1+ORDERS:D:96A:UN++2:C'\
2240 BGM+220+PO-001+9'\
2241 UNT+3+1'\
2242 UNZ+1+1'";
2243 let r = parse_and_validate(input).expect("UNH with S010 must parse ok");
2244 assert_eq!(r.messages[0].sequence_of_transfers, Some(2));
2245 assert_eq!(r.messages[0].transfer_position.as_deref(), Some("C"));
2246 }
2247
2248 #[test]
2249 fn sequence_of_transfers_none_when_absent() {
2250 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2251 assert!(r.messages[0].sequence_of_transfers.is_none());
2252 assert!(r.messages[0].transfer_position.is_none());
2253 }
2254
2255 #[test]
2258 fn ung_app_sender_and_recipient_qualifiers_extracted() {
2259 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2261 UNG+ORDERS+APPSEND:ZZZ+APPRECV:14+200101:0900+1+UN+D:96A'\
2262 UNH+1+ORDERS:D:96A:UN'\
2263 BGM+220+PO-001+9'\
2264 UNT+3+1'\
2265 UNE+1+1'\
2266 UNZ+1+1'";
2267 let r = parse_and_validate(input).expect("UNG with qualifiers must parse ok");
2268 let g = &r.functional_groups[0];
2269 assert_eq!(g.app_sender, "APPSEND");
2270 assert_eq!(g.app_sender_qualifier, "ZZZ");
2271 assert_eq!(g.app_recipient, "APPRECV");
2272 assert_eq!(g.app_recipient_qualifier, "14");
2273 }
2274
2275 #[test]
2276 fn ung_qualifiers_empty_when_absent() {
2277 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2278 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
2279 UNH+1+ORDERS:D:96A:UN'\
2280 BGM+220+PO-001+9'\
2281 UNT+3+1'\
2282 UNE+1+1'\
2283 UNZ+1+1'";
2284 let r = parse_and_validate(input).unwrap();
2285 let g = &r.functional_groups[0];
2286 assert_eq!(g.app_sender_qualifier, "");
2287 assert_eq!(g.app_recipient_qualifier, "");
2288 }
2289
2290 #[test]
2293 fn lenient_result_is_valid_true_on_clean_interchange() {
2294 let owned = parse(VALID_INTERCHANGE);
2295 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2296 let r = validate_envelope_lenient(&segs);
2297 assert!(r.is_valid());
2298 assert!(r.errors.is_empty());
2299 assert!(r.interchange.is_some());
2300 }
2301
2302 #[test]
2303 fn lenient_result_into_strict_ok_path() {
2304 let owned = parse(VALID_INTERCHANGE);
2305 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2306 let r = validate_envelope_lenient(&segs);
2307 let strict = r.into_strict();
2308 assert!(
2309 strict.is_ok(),
2310 "into_strict() should succeed for valid interchange"
2311 );
2312 assert_eq!(strict.unwrap().messages.len(), 1);
2313 }
2314
2315 #[test]
2316 fn lenient_result_into_strict_err_path() {
2317 let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+2+1'";
2319 let owned = parse(input);
2320 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2321 let r = validate_envelope_lenient(&segs);
2322 assert!(!r.is_valid());
2323 let strict = r.into_strict();
2324 assert!(strict.is_err());
2325 let errors = strict.unwrap_err();
2326 assert_eq!(errors.len(), 1);
2327 assert!(matches!(
2328 &errors[0],
2329 EdifactError::MessageCountMismatch {
2330 expected: 2,
2331 actual: 1
2332 }
2333 ));
2334 }
2335
2336 #[test]
2339 fn parse_unh_direct_extracts_all_s009_fields() {
2340 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2345 UNH+REF99+ORDERS:D:96A:UN:EAN010'\
2346 BGM+220+PO-001+9'\
2347 UNT+3+REF99'\
2348 UNZ+1+1'";
2349 let r = parse_and_validate(input).expect("must parse ok");
2350 let msg = &r.messages[0];
2351 assert_eq!(msg.message_ref, "REF99");
2352 assert_eq!(msg.message_type, "ORDERS");
2353 assert_eq!(msg.version, "D");
2354 assert_eq!(msg.release, "96A");
2355 assert_eq!(msg.controlling_agency, "UN");
2356 assert_eq!(msg.association_code, "EAN010");
2357 }
2358
2359 #[test]
2360 fn parse_ung_direct_extracts_identifier_fields() {
2361 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2363 UNG+ORDERS+APPSEND:ZZZ+APPRECV:14+200101:0900+GRP01+UN+D:96A'\
2364 UNH+1+ORDERS:D:96A:UN'\
2365 BGM+220+PO-001+9'\
2366 UNT+3+1'\
2367 UNE+1+GRP01'\
2368 UNZ+1+1'";
2369 let r = parse_and_validate(input).expect("must parse ok");
2370 let g = &r.functional_groups[0];
2371 assert_eq!(g.group_id, "ORDERS");
2372 assert_eq!(g.app_sender, "APPSEND");
2373 assert_eq!(g.app_sender_qualifier, "ZZZ");
2374 assert_eq!(g.app_recipient, "APPRECV");
2375 assert_eq!(g.app_recipient_qualifier, "14");
2376 assert_eq!(g.group_ref, "GRP01");
2377 assert_eq!(g.controlling_agency, "UN");
2378 assert_eq!(g.version, "D");
2379 assert_eq!(g.release, "96A");
2380 }
2381
2382 #[test]
2385 fn find_message_returns_correct_message_by_ref() {
2386 let input = b"UNB+UNOA:3+S+R+200101:0900+CTRL2'\
2387 UNH+REF-A+ORDERS:D:96A:UN'\
2388 BGM+220+PO-001+9'\
2389 UNT+3+REF-A'\
2390 UNH+REF-B+INVOIC:D:96A:UN'\
2391 BGM+380+INV-001+9'\
2392 UNT+3+REF-B'\
2393 UNZ+2+CTRL2'";
2394 let r = parse_and_validate(input).expect("two-message interchange must parse ok");
2395 let msg_a = r.find_message("REF-A");
2396 assert!(msg_a.is_some());
2397 assert_eq!(msg_a.unwrap().message_type, "ORDERS");
2398 let msg_b = r.find_message("REF-B");
2399 assert!(msg_b.is_some());
2400 assert_eq!(msg_b.unwrap().message_type, "INVOIC");
2401 assert!(r.find_message("MISSING").is_none());
2402 }
2403
2404 #[test]
2407 fn ung_missing_group_ref_returns_err() {
2408 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2410 UNG+ORDERS+S+R+200101:0900++UN+D:96A'\
2411 UNH+1+ORDERS:D:96A:UN'\
2412 BGM+220+PO-001+9'\
2413 UNT+3+1'\
2414 UNE+1+'\
2415 UNZ+1+1'";
2416 let result = parse_and_validate(input);
2417 assert!(
2418 matches!(result,
2419 Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 4, component_index: 0 })
2420 if tag == "UNG"
2421 ),
2422 "expected MissingRequiredComponent for empty UNG group_ref, got {result:?}"
2423 );
2424 }
2425
2426 #[test]
2429 fn empty_segment_list_returns_missing_unb() {
2430 let result = validate_envelope(&[]);
2433 assert!(
2434 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNB"),
2435 "expected MissingSegment(UNB) for empty input, got {result:?}"
2436 );
2437 }
2438
2439 #[test]
2440 fn single_segment_only_unb_returns_missing_unz() {
2441 let input = b"UNB+UNOA:3+S+R+200101:0900+1'";
2443 let owned = parse(input);
2444 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2445 let result = validate_envelope(&segs);
2446 assert!(
2447 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNZ"),
2448 "expected MissingSegment(UNZ) for UNB-only input, got {result:?}"
2449 );
2450 }
2451
2452 #[test]
2453 fn display_validated_interchange_includes_count() {
2454 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2455 let s = r.to_string();
2456 assert!(
2458 s.contains("messages=1"),
2459 "Display must include count '1': {s}"
2460 );
2461 }
2462
2463 #[test]
2464 fn display_interchange_envelope_contains_arrow() {
2465 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2466 let s = r.interchange.to_string();
2467 assert!(s.contains("->"), "Display must use ASCII '->' arrow: {s}");
2469 assert!(
2470 !s.contains('\u{2192}'),
2471 "Display must not use Unicode → arrow: {s}"
2472 );
2473 }
2474
2475 #[test]
2476 fn display_functional_group_envelope() {
2477 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2478 UNG+ORDERS+APPSEND+APPRECV+200101:0900+GRP01+UN+D:96A'\
2479 UNH+1+ORDERS:D:96A:UN'\
2480 BGM+220+PO-001+9'\
2481 UNT+3+1'\
2482 UNE+1+GRP01'\
2483 UNZ+1+1'";
2484 let r = parse_and_validate(input).expect("must parse ok");
2485 let g = &r.functional_groups[0];
2486 let s = g.to_string();
2487 assert!(s.contains("ORDERS"), "Display must include group_id: {s}");
2488 assert!(
2489 s.contains("APPSEND"),
2490 "Display must include app_sender: {s}"
2491 );
2492 assert!(
2493 s.contains("APPRECV"),
2494 "Display must include app_recipient: {s}"
2495 );
2496 assert!(s.contains("GRP01"), "Display must include group_ref: {s}");
2497 assert!(
2498 s.contains("msgs="),
2499 "Display must include message count label: {s}"
2500 );
2501 assert!(
2502 s.contains("1/1"),
2503 "Display must include actual/declared counts: {s}"
2504 );
2505 }
2506
2507 #[test]
2508 fn lenient_has_errors_is_inverse_of_is_valid() {
2509 let owned = parse(VALID_INTERCHANGE);
2510 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2511 let valid = validate_envelope_lenient(&segs);
2512 assert!(valid.is_valid());
2513 assert!(!valid.has_errors());
2514
2515 let input = b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+2+1'";
2517 let owned2 = parse(input);
2518 let segs2: Vec<Segment<'_>> = owned2
2519 .iter()
2520 .map(crate::OwnedSegment::as_borrowed)
2521 .collect();
2522 let invalid = validate_envelope_lenient(&segs2);
2523 assert!(!invalid.is_valid());
2524 assert!(invalid.has_errors());
2525 }
2526}