1use crate::{
49 error::EdifactError,
50 model::{Segment, Span},
51};
52use std::collections::HashSet;
53
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
78#[non_exhaustive]
79#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
80pub struct InterchangeEnvelope {
81 pub syntax_identifier: String,
83 pub syntax_version: String,
87 pub sender_id: String,
89 pub sender_qualifier: String,
94 pub sender_routing_address: Option<String>,
103 pub recipient_id: String,
105 pub recipient_qualifier: String,
109 pub recipient_routing_address: Option<String>,
114 pub date: String,
116 pub time: Option<String>,
120 pub control_ref: String,
122 pub recipient_password: Option<String>,
127 pub recipient_password_qualifier: Option<String>,
132 pub app_ref: Option<String>,
136 pub processing_priority: Option<String>,
141 pub acknowledgement_request: bool,
146 pub communications_agreement_id: Option<String>,
150 pub test_indicator: bool,
156 pub declared_unit_count: u32,
164 pub actual_unit_count: u32,
166}
167
168impl InterchangeEnvelope {
169 #[inline]
191 #[must_use]
192 pub fn is_test(&self) -> bool {
193 self.test_indicator
194 }
195
196 #[inline]
200 #[must_use]
201 pub fn ack_requested(&self) -> bool {
202 self.acknowledgement_request
203 }
204}
205
206impl std::fmt::Display for InterchangeEnvelope {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 write!(
209 f,
210 "{sender} -> {recipient} [{ctrl}] ({syntax}:{ver})",
211 sender = self.sender_id,
212 recipient = self.recipient_id,
213 ctrl = self.control_ref,
214 syntax = self.syntax_identifier,
215 ver = self.syntax_version,
216 )
217 }
218}
219
220#[derive(Debug, Clone, PartialEq, Eq, Hash)]
222#[non_exhaustive]
223#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
224pub struct MessageEnvelope {
225 pub message_ref: String,
227 pub message_type: String,
229 pub version: String,
231 pub release: String,
233 pub controlling_agency: String,
235 pub association_code: String,
237 pub common_access_ref: Option<String>,
244 pub sequence_of_transfers: Option<u32>,
250 pub transfer_position: Option<String>,
255 pub declared_segment_count: u32,
257 pub actual_segment_count: u32,
259 pub header_span: Span,
261 pub trailer_span: Span,
267}
268
269impl std::fmt::Display for MessageEnvelope {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 write!(
272 f,
273 "{msg_type}:{ver}:{rel} ref={msg_ref} seg={actual}/{declared}",
274 msg_type = self.message_type,
275 ver = self.version,
276 rel = self.release,
277 msg_ref = self.message_ref,
278 actual = self.actual_segment_count,
279 declared = self.declared_segment_count,
280 )
281 }
282}
283
284#[derive(Debug, Clone, PartialEq, Eq, Hash)]
290#[non_exhaustive]
291#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
292pub struct FunctionalGroupEnvelope {
293 pub group_id: String,
295 pub app_sender: String,
297 pub app_sender_qualifier: String,
301 pub app_recipient: String,
303 pub app_recipient_qualifier: String,
307 pub date: String,
309 pub time: Option<String>,
311 pub group_ref: String,
313 pub controlling_agency: String,
315 pub version: String,
317 pub release: String,
319 pub application_password: Option<String>,
325 pub declared_message_count: u32,
327 pub actual_message_count: u32,
329 pub messages: Vec<MessageEnvelope>,
331}
332
333#[derive(Debug, Clone, PartialEq, Eq, Hash)]
339#[non_exhaustive]
340#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
341pub struct ValidatedInterchange {
342 pub interchange: InterchangeEnvelope,
344 pub functional_groups: Vec<FunctionalGroupEnvelope>,
349 pub messages: Vec<MessageEnvelope>,
354}
355
356impl ValidatedInterchange {
357 #[inline]
359 #[must_use]
360 pub fn has_functional_groups(&self) -> bool {
361 !self.functional_groups.is_empty()
362 }
363
364 #[inline]
368 #[must_use]
369 pub fn message_count(&self) -> usize {
370 self.messages.len()
371 }
372
373 #[inline]
378 pub fn iter_messages(&self) -> impl Iterator<Item = &MessageEnvelope> {
379 self.messages.iter()
380 }
381
382 #[inline]
389 #[must_use]
390 pub fn find_message(&self, reference: &str) -> Option<&MessageEnvelope> {
391 self.messages.iter().find(|m| m.message_ref == reference)
392 }
393
394 #[must_use]
403 pub fn messages_by_type(&self, message_type: &str) -> Vec<&MessageEnvelope> {
404 self.messages
405 .iter()
406 .filter(|m| m.message_type == message_type)
407 .collect()
408 }
409
410 #[inline]
421 pub fn iter_messages_by_type<'s, 'q: 's>(
422 &'s self,
423 message_type: &'q str,
424 ) -> impl Iterator<Item = &'s MessageEnvelope> + 's {
425 self.messages
426 .iter()
427 .filter(move |m| m.message_type == message_type)
428 }
429}
430
431impl std::fmt::Display for ValidatedInterchange {
432 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
433 write!(
434 f,
435 "{ic} messages={n}",
436 ic = self.interchange,
437 n = self.messages.len(),
438 )
439 }
440}
441
442impl std::fmt::Display for FunctionalGroupEnvelope {
443 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444 write!(
445 f,
446 "{gid} sender={sender} recipient={recip} [{gref}] ({agency}) msgs={actual}/{declared}",
447 gid = self.group_id,
448 sender = self.app_sender,
449 recip = self.app_recipient,
450 gref = self.group_ref,
451 agency = self.controlling_agency,
452 actual = self.actual_message_count,
453 declared = self.declared_message_count,
454 )
455 }
456}
457
458#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
463#[non_exhaustive]
464#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
465pub struct MessageIdentifier<'a> {
466 pub message_ref: &'a str,
468 pub message_type: &'a str,
469 pub version: &'a str,
470 pub release: &'a str,
471 pub controlling_agency: &'a str,
472 pub association_code: &'a str,
476}
477
478pub fn parse_unh<'a>(unh: &'a Segment<'a>) -> Result<MessageIdentifier<'a>, EdifactError> {
482 let message_ref = unh
484 .get_element(0)
485 .and_then(|e| e.get_component(0))
486 .filter(|s| !s.is_empty())
487 .ok_or_else(|| EdifactError::MissingRequiredComponent {
488 tag: "UNH".to_owned(),
489 element_index: 0,
490 component_index: 0,
491 })?;
492 let elem = unh
494 .get_element(1)
495 .ok_or_else(|| EdifactError::MissingRequiredElement {
496 tag: "UNH".to_owned(),
497 element_index: 1,
498 })?;
499 let message_type =
500 elem.get_component(0)
501 .ok_or_else(|| EdifactError::MissingRequiredComponent {
502 tag: "UNH".to_owned(),
503 element_index: 1,
504 component_index: 0,
505 })?;
506 Ok(MessageIdentifier {
507 message_ref,
508 message_type,
509 version: elem.get_component(1).unwrap_or(""),
510 release: elem.get_component(2).unwrap_or(""),
511 controlling_agency: elem.get_component(3).unwrap_or(""),
512 association_code: elem.get_component(4).unwrap_or(""),
513 })
514}
515
516#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
535#[non_exhaustive]
536#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
537pub struct GroupIdentifier<'a> {
538 pub group_id: &'a str,
540 pub app_sender: &'a str,
542 pub app_sender_qualifier: &'a str,
544 pub app_recipient: &'a str,
546 pub app_recipient_qualifier: &'a str,
548 pub group_ref: &'a str,
550 pub controlling_agency: &'a str,
552 pub version: &'a str,
554 pub release: &'a str,
556 pub application_password: &'a str,
558}
559
560pub fn parse_ung<'a>(ung: &'a Segment<'a>) -> Result<GroupIdentifier<'a>, EdifactError> {
565 let group_id = ung
566 .get_element(0)
567 .and_then(|e| e.get_component(0))
568 .unwrap_or("");
569 let app_sender = ung
570 .get_element(1)
571 .and_then(|e| e.get_component(0))
572 .unwrap_or("");
573 let app_sender_qualifier = ung
574 .get_element(1)
575 .and_then(|e| e.get_component(1))
576 .unwrap_or("");
577 let app_recipient = ung
578 .get_element(2)
579 .and_then(|e| e.get_component(0))
580 .unwrap_or("");
581 let app_recipient_qualifier = ung
582 .get_element(2)
583 .and_then(|e| e.get_component(1))
584 .unwrap_or("");
585 let group_ref = ung
586 .get_element(4)
587 .and_then(|e| e.get_component(0))
588 .ok_or_else(|| EdifactError::MissingRequiredComponent {
589 tag: "UNG".to_owned(),
590 element_index: 4,
591 component_index: 0,
592 })?;
593 let controlling_agency = ung
594 .get_element(5)
595 .and_then(|e| e.get_component(0))
596 .unwrap_or("");
597 let s008 = ung.get_element(6);
598 let version = s008.as_ref().and_then(|e| e.get_component(0)).unwrap_or("");
599 let release = s008.as_ref().and_then(|e| e.get_component(1)).unwrap_or("");
600 let application_password = ung
601 .get_element(7)
602 .and_then(|e| e.get_component(0))
603 .unwrap_or("");
604 Ok(GroupIdentifier {
605 group_id,
606 app_sender,
607 app_sender_qualifier,
608 app_recipient,
609 app_recipient_qualifier,
610 group_ref,
611 controlling_agency,
612 version,
613 release,
614 application_password,
615 })
616}
617
618pub fn validate_envelope(segments: &[Segment<'_>]) -> Result<ValidatedInterchange, EdifactError> {
631 validate_envelope_impl(segments)
632}
633
634#[derive(Debug)]
647#[non_exhaustive]
648pub struct LenientResult {
649 pub interchange: Option<ValidatedInterchange>,
651 pub errors: Vec<EdifactError>,
653}
654
655impl LenientResult {
656 #[inline]
658 #[must_use]
659 pub fn is_valid(&self) -> bool {
660 self.errors.is_empty()
661 }
662
663 #[inline]
668 #[must_use]
669 pub fn has_errors(&self) -> bool {
670 !self.errors.is_empty()
671 }
672
673 pub fn into_strict(self) -> Result<ValidatedInterchange, Vec<EdifactError>> {
683 match self.interchange {
684 Some(interchange) if self.errors.is_empty() => Ok(interchange),
685 _ => Err(self.errors),
686 }
687 }
688}
689
690pub fn validate_envelope_lenient(segments: &[Segment<'_>]) -> LenientResult {
701 validate_envelope_lenient_impl(segments)
702}
703
704#[derive(Default)]
721struct ErrorSink {
722 errors: Vec<EdifactError>,
723}
724
725impl ErrorSink {
726 #[inline]
727 fn push(&mut self, error: EdifactError) {
728 self.errors.push(error);
729 }
730
731 #[inline]
733 fn recover<T>(&mut self, result: Result<T, EdifactError>, fallback: T) -> T {
734 match result {
735 Ok(value) => value,
736 Err(error) => {
737 self.errors.push(error);
738 fallback
739 }
740 }
741 }
742
743 #[inline]
745 fn required(&mut self, seg: &Segment<'_>, element: usize, component: usize) -> String {
746 self.recover(
747 seg.required_component(element, component)
748 .map(str::to_owned),
749 String::new(),
750 )
751 }
752}
753
754fn validate_envelope_collecting(
759 segments: &[Segment<'_>],
760) -> (Option<ValidatedInterchange>, Vec<EdifactError>) {
761 let mut sink = ErrorSink::default();
762
763 let mut interchange_env = match extract_interchange(segments, &mut sink) {
764 Ok(env) => env,
765 Err(fatal) => {
766 sink.push(fatal);
767 return (None, sink.errors);
768 }
769 };
770
771 let inner = if segments.len() >= 2 {
772 &segments[1..segments.len() - 1]
773 } else {
774 &[]
775 };
776
777 let (functional_groups, messages) = match extract_content(inner, &mut sink) {
778 Ok(pair) => pair,
779 Err(fatal) => {
780 sink.push(fatal);
781 return (None, sink.errors);
782 }
783 };
784
785 if functional_groups.is_empty() && messages.is_empty() {
789 sink.push(EdifactError::EmptyInterchange {
790 control_ref: interchange_env.control_ref.clone(),
791 });
792 }
793
794 let actual_unit_count = if functional_groups.is_empty() {
797 messages.len()
798 } else {
799 functional_groups.len()
800 };
801 interchange_env.actual_unit_count = sink.recover(
802 u32::try_from(actual_unit_count).map_err(|_| EdifactError::InterchangeTooLarge {
803 count: actual_unit_count as u64,
804 }),
805 u32::MAX,
806 );
807
808 if interchange_env.declared_unit_count != interchange_env.actual_unit_count {
809 sink.push(EdifactError::MessageCountMismatch {
810 expected: interchange_env.declared_unit_count,
811 actual: interchange_env.actual_unit_count,
812 });
813 }
814
815 for msg in &messages {
816 if msg.declared_segment_count != msg.actual_segment_count {
817 sink.push(EdifactError::SegmentCountMismatch {
818 expected: msg.declared_segment_count,
819 actual: msg.actual_segment_count,
820 message_ref: msg.message_ref.clone(),
821 span: msg.trailer_span,
822 });
823 }
824 }
825
826 (
827 Some(ValidatedInterchange {
828 interchange: interchange_env,
829 functional_groups,
830 messages,
831 }),
832 sink.errors,
833 )
834}
835
836fn validate_envelope_impl(segments: &[Segment<'_>]) -> Result<ValidatedInterchange, EdifactError> {
837 match validate_envelope_collecting(segments) {
838 (Some(result), errors) if errors.is_empty() => Ok(result),
839 (_, mut errors) => Err(errors
840 .drain(..)
841 .next()
842 .unwrap_or(EdifactError::MissingSegment {
843 tag: "UNB".to_owned(),
844 expected_position: "first segment of interchange".to_owned(),
845 })),
846 }
847}
848
849fn validate_envelope_lenient_impl(segments: &[Segment<'_>]) -> LenientResult {
850 let (interchange, errors) = validate_envelope_collecting(segments);
851 LenientResult {
852 interchange,
853 errors,
854 }
855}
856
857fn extract_interchange(
860 segments: &[Segment<'_>],
861 sink: &mut ErrorSink,
862) -> Result<InterchangeEnvelope, EdifactError> {
863 if segments.first().map(|s| s.tag()) != Some("UNB") {
864 return Err(EdifactError::MissingSegment {
865 tag: "UNB".to_owned(),
866 expected_position: "first segment of interchange".to_owned(),
867 });
868 }
869 if segments.last().map(|s| s.tag()) != Some("UNZ") {
870 return Err(EdifactError::MissingSegment {
871 tag: "UNZ".to_owned(),
872 expected_position: "last segment of interchange".to_owned(),
873 });
874 }
875
876 let unb = &segments[0];
877 let unz = &segments[segments.len() - 1];
878
879 let syntax_identifier = sink.required(unb, 0, 0);
880 let syntax_version = unb.component_str(0, 1).unwrap_or("").to_owned();
881
882 if !syntax_identifier.is_empty() {
887 if let Err(error) = crate::Charset::from_syntax_identifier(&syntax_identifier) {
888 sink.push(error);
889 }
890 }
891
892 let sender_id = sink.required(unb, 1, 0);
893 let sender_qualifier = unb.component_str(1, 1).unwrap_or("").to_owned();
894 let sender_routing_address = unb
896 .component_str(1, 2)
897 .filter(|s| !s.is_empty())
898 .map(str::to_owned);
899
900 let recipient_id = sink.required(unb, 2, 0);
901 let recipient_qualifier = unb.component_str(2, 1).unwrap_or("").to_owned();
902 let recipient_routing_address = unb
904 .component_str(2, 2)
905 .filter(|s| !s.is_empty())
906 .map(str::to_owned);
907
908 let date = sink.required(unb, 3, 0);
909 let time_raw = unb.component_str(3, 1).unwrap_or("");
910 let time = if time_raw.is_empty() {
911 None
912 } else {
913 Some(time_raw.to_owned())
914 };
915
916 let control_ref = sink.required(unb, 4, 0);
917
918 let recipient_password = unb
920 .component_str(5, 0)
921 .filter(|s| !s.is_empty())
922 .map(str::to_owned);
923 let recipient_password_qualifier = unb
924 .component_str(5, 1)
925 .filter(|s| !s.is_empty())
926 .map(str::to_owned);
927 let app_ref = unb
929 .component_str(6, 0)
930 .filter(|s| !s.is_empty())
931 .map(str::to_owned);
932 let processing_priority = unb
934 .component_str(7, 0)
935 .filter(|s| !s.is_empty())
936 .map(str::to_owned);
937 let acknowledgement_request = unb.component_str(8, 0) == Some("1");
939 let communications_agreement_id = unb
941 .component_str(9, 0)
942 .filter(|s| !s.is_empty())
943 .map(str::to_owned);
944 let test_indicator = unb.component_str(10, 0) == Some("1");
946
947 let unz_control_ref = sink.required(unz, 1, 0);
948 if unz_control_ref != control_ref {
949 sink.push(EdifactError::QualifierMismatch {
950 tag: "UNZ".to_owned(),
951 actual: unz_control_ref,
952 expected: control_ref.clone(),
953 span: unz.span,
954 });
955 }
956
957 let declared_unit_count_raw = sink.required(unz, 0, 0);
958 let declared_unit_count: u32 = sink.recover(
959 declared_unit_count_raw
960 .parse()
961 .map_err(|_| EdifactError::InvalidText {
962 offset: unz.span.start,
963 }),
964 0,
965 );
966
967 Ok(InterchangeEnvelope {
968 syntax_identifier,
969 syntax_version,
970 sender_id,
971 sender_qualifier,
972 sender_routing_address,
973 recipient_id,
974 recipient_qualifier,
975 recipient_routing_address,
976 date,
977 time,
978 control_ref,
979 recipient_password,
980 recipient_password_qualifier,
981 app_ref,
982 processing_priority,
983 acknowledgement_request,
984 communications_agreement_id,
985 test_indicator,
986 declared_unit_count,
987 actual_unit_count: 0,
988 })
989}
990
991fn extract_content(
994 inner: &[Segment<'_>],
995 sink: &mut ErrorSink,
996) -> Result<(Vec<FunctionalGroupEnvelope>, Vec<MessageEnvelope>), EdifactError> {
997 if inner.first().is_some_and(|s| s.tag == "UNG") {
1001 let groups = extract_with_groups(inner, sink)?;
1002 let messages = groups
1003 .iter()
1004 .flat_map(|g| g.messages.iter().cloned())
1005 .collect();
1006 Ok((groups, messages))
1007 } else {
1008 let mut seen_refs = HashSet::new();
1009 let messages = extract_messages_flat(inner, sink, &mut seen_refs)?;
1010 Ok((vec![], messages))
1011 }
1012}
1013
1014fn find_matching_une(segments: &[Segment<'_>], start: usize) -> Result<usize, EdifactError> {
1016 for (offset, seg) in segments[start..].iter().enumerate() {
1017 match seg.tag() {
1018 "UNE" => return Ok(start + offset),
1019 "UNG" => {
1020 return Err(EdifactError::InvalidSegmentForMessage {
1021 tag: "UNG".to_owned(),
1022 message_type: "ENVELOPE".to_owned(),
1023 span: seg.span,
1024 });
1025 }
1026 _ => {}
1027 }
1028 }
1029 Err(EdifactError::MissingSegment {
1030 tag: "UNE".to_owned(),
1031 expected_position: "end of functional group".to_owned(),
1032 })
1033}
1034
1035fn extract_with_groups(
1036 inner: &[Segment<'_>],
1037 sink: &mut ErrorSink,
1038) -> Result<Vec<FunctionalGroupEnvelope>, EdifactError> {
1039 let mut groups: Vec<FunctionalGroupEnvelope> = Vec::new();
1040 let mut seen_group_refs: HashSet<String> = HashSet::new();
1043 let mut seen_message_refs: HashSet<String> = HashSet::new();
1044 let mut i = 0;
1045
1046 while i < inner.len() {
1047 let seg = &inner[i];
1048 match seg.tag() {
1049 "UNG" => {
1050 let ung_idx = i;
1051 let une_idx = find_matching_une(inner, ung_idx + 1)?;
1052
1053 let ung = &inner[ung_idx];
1054 let group_id = ung.component_str(0, 0).unwrap_or("").to_owned();
1055 let app_sender = ung.component_str(1, 0).unwrap_or("").to_owned();
1056 let app_sender_qualifier = ung.component_str(1, 1).unwrap_or("").to_owned();
1057 let app_recipient = ung.component_str(2, 0).unwrap_or("").to_owned();
1058 let app_recipient_qualifier = ung.component_str(2, 1).unwrap_or("").to_owned();
1059 let date = ung.component_str(3, 0).unwrap_or("").to_owned();
1060 let time_raw = ung.component_str(3, 1).unwrap_or("");
1061 let time = if time_raw.is_empty() {
1062 None
1063 } else {
1064 Some(time_raw.to_owned())
1065 };
1066 let group_ref = sink.required(ung, 4, 0);
1068 if !seen_group_refs.insert(group_ref.clone()) {
1069 sink.push(EdifactError::DuplicateReference {
1070 tag: "UNG".to_owned(),
1071 reference: group_ref.clone(),
1072 span: ung.span,
1073 });
1074 }
1075 let controlling_agency = ung.component_str(5, 0).unwrap_or("").to_owned();
1076 let version = ung.component_str(6, 0).unwrap_or("").to_owned();
1078 let release = ung.component_str(6, 1).unwrap_or("").to_owned();
1079 let application_password = ung
1081 .component_str(7, 0)
1082 .filter(|s| !s.is_empty())
1083 .map(str::to_owned);
1084
1085 let une = &inner[une_idx];
1086 let declared_str = sink.required(une, 0, 0);
1087 let declared_message_count: u32 = sink.recover(
1088 declared_str.parse().map_err(|_| EdifactError::InvalidText {
1089 offset: une.span.start,
1090 }),
1091 0,
1092 );
1093 let une_ref = sink.required(une, 1, 0);
1094 if une_ref != group_ref {
1095 sink.push(EdifactError::QualifierMismatch {
1096 tag: "UNE".to_owned(),
1097 actual: une_ref,
1098 expected: group_ref.clone(),
1099 span: une.span,
1100 });
1101 }
1102
1103 let group_content = &inner[ung_idx + 1..une_idx];
1104 let messages = extract_messages_flat(group_content, sink, &mut seen_message_refs)?;
1105 let actual_message_count = sink.recover(
1106 u32::try_from(messages.len()).map_err(|_| EdifactError::InterchangeTooLarge {
1107 count: messages.len() as u64,
1108 }),
1109 u32::MAX,
1110 );
1111
1112 if declared_message_count != actual_message_count {
1113 sink.push(EdifactError::MessageCountMismatch {
1114 expected: declared_message_count,
1115 actual: actual_message_count,
1116 });
1117 }
1118
1119 groups.push(FunctionalGroupEnvelope {
1120 group_id,
1121 app_sender,
1122 app_sender_qualifier,
1123 app_recipient,
1124 app_recipient_qualifier,
1125 date,
1126 time,
1127 group_ref,
1128 controlling_agency,
1129 version,
1130 release,
1131 application_password,
1132 declared_message_count,
1133 actual_message_count,
1134 messages,
1135 });
1136 i = une_idx + 1;
1137 }
1138 "UNE" => {
1139 return Err(EdifactError::InvalidSegmentForMessage {
1140 tag: "UNE".to_owned(),
1141 message_type: "ENVELOPE".to_owned(),
1142 span: seg.span,
1143 });
1144 }
1145 "UNH" => {
1146 return Err(EdifactError::GroupsAndMessagesMixed { span: seg.span });
1151 }
1152 "UNO" | "UNP" => {
1153 return Err(EdifactError::PackageNotSupported {
1154 tag: seg.tag.clone().into_owned(),
1155 span: seg.span,
1156 });
1157 }
1158 _ => {
1159 return Err(EdifactError::InvalidSegmentForMessage {
1160 tag: seg.tag.clone().into_owned(),
1161 message_type: "ENVELOPE".to_owned(),
1162 span: seg.span,
1163 });
1164 }
1165 }
1166 }
1167
1168 Ok(groups)
1169}
1170
1171fn extract_messages_flat(
1173 segments: &[Segment<'_>],
1174 sink: &mut ErrorSink,
1175 seen_refs: &mut HashSet<String>,
1176) -> Result<Vec<MessageEnvelope>, EdifactError> {
1177 let mut messages: Vec<MessageEnvelope> = Vec::new();
1178 let mut unh_idx: Option<usize> = None;
1182
1183 for (i, seg) in segments.iter().enumerate() {
1184 match seg.tag() {
1185 "UNH" => {
1186 if unh_idx.is_some() {
1187 return Err(EdifactError::InvalidSegmentForMessage {
1188 tag: "UNH".to_owned(),
1189 message_type: "ENVELOPE".to_owned(),
1190 span: seg.span,
1191 });
1192 }
1193 unh_idx = Some(i);
1194 }
1195 "UNT" if unh_idx.is_some() => {
1196 let msg_start_idx = unh_idx.take().expect("guarded by the match arm");
1197 let unh = &segments[msg_start_idx];
1198
1199 let message_ref = sink.required(unh, 0, 0);
1200 if !seen_refs.insert(message_ref.clone()) {
1201 sink.push(EdifactError::DuplicateReference {
1202 tag: "UNH".to_owned(),
1203 reference: message_ref.clone(),
1204 span: unh.span,
1205 });
1206 }
1207 let message_type = sink.required(unh, 1, 0);
1208 let version = sink.required(unh, 1, 1);
1209 let release = sink.required(unh, 1, 2);
1210 let controlling_agency = sink.required(unh, 1, 3);
1211 let association_code = unh.component_str(1, 4).unwrap_or("").to_owned();
1212 let common_access_ref = unh
1214 .component_str(2, 0)
1215 .filter(|s| !s.is_empty())
1216 .map(str::to_owned);
1217 let sequence_of_transfers = unh
1220 .component_str(3, 0)
1221 .filter(|s| !s.is_empty())
1222 .and_then(|s| s.parse::<u32>().ok());
1223 let transfer_position = unh
1224 .component_str(3, 1)
1225 .filter(|s| !s.is_empty())
1226 .map(str::to_owned);
1227
1228 let declared_raw = sink.required(seg, 0, 0);
1229 let declared_segment_count: u32 = sink.recover(
1230 declared_raw.parse().map_err(|_| EdifactError::InvalidText {
1231 offset: seg.span.start,
1232 }),
1233 0,
1234 );
1235 let unt_ref = sink.required(seg, 1, 0);
1236 if unt_ref != message_ref {
1237 sink.push(EdifactError::QualifierMismatch {
1238 tag: "UNT".to_owned(),
1239 actual: unt_ref,
1240 expected: message_ref.clone(),
1241 span: seg.span,
1242 });
1243 }
1244
1245 let segment_span = i - msg_start_idx + 1;
1246 let actual_segment_count = sink.recover(
1247 u32::try_from(segment_span).map_err(|_| EdifactError::InterchangeTooLarge {
1248 count: segment_span as u64,
1249 }),
1250 u32::MAX,
1251 );
1252
1253 if segment_span < 3 {
1258 sink.push(EdifactError::EmptyMessage {
1259 message_ref: message_ref.clone(),
1260 span: unh.span,
1261 });
1262 }
1263
1264 messages.push(MessageEnvelope {
1265 message_ref,
1266 message_type,
1267 version,
1268 release,
1269 controlling_agency,
1270 association_code,
1271 common_access_ref,
1272 sequence_of_transfers,
1273 transfer_position,
1274 declared_segment_count,
1275 actual_segment_count,
1276 header_span: unh.span,
1277 trailer_span: seg.span,
1278 });
1279 }
1280 "UNT" => {
1281 return Err(EdifactError::InvalidSegmentForMessage {
1282 tag: "UNT".to_owned(),
1283 message_type: "ENVELOPE".to_owned(),
1284 span: seg.span,
1285 });
1286 }
1287 "UNO" | "UNP" => {
1293 return Err(EdifactError::PackageNotSupported {
1294 tag: seg.tag.clone().into_owned(),
1295 span: seg.span,
1296 });
1297 }
1298 "UNB" | "UNZ" | "UNG" | "UNE" if unh_idx.is_some() => {
1299 return Err(EdifactError::InvalidSegmentForMessage {
1300 tag: seg.tag.clone().into_owned(),
1301 message_type: "ENVELOPE".to_owned(),
1302 span: seg.span,
1303 });
1304 }
1305 _ if unh_idx.is_none() => {
1306 return Err(EdifactError::InvalidSegmentForMessage {
1307 tag: seg.tag.clone().into_owned(),
1308 message_type: "ENVELOPE".to_owned(),
1309 span: seg.span,
1310 });
1311 }
1312 _ => {}
1313 }
1314 }
1315
1316 if unh_idx.is_some() {
1317 return Err(EdifactError::MissingSegment {
1318 tag: "UNT".to_owned(),
1319 expected_position: "end of message group".to_owned(),
1320 });
1321 }
1322
1323 Ok(messages)
1324}
1325
1326#[cfg(test)]
1329mod tests {
1330 use super::*;
1331
1332 fn parse(input: &[u8]) -> Vec<crate::OwnedSegment> {
1333 crate::from_reader(std::io::Cursor::new(input))
1334 .collect::<Result<Vec<_>, _>>()
1335 .expect("parse failed")
1336 }
1337
1338 fn parse_and_validate(input: &[u8]) -> Result<ValidatedInterchange, EdifactError> {
1339 validate_envelope(&parse(input))
1340 }
1341
1342 fn parse_and_validate_lenient(input: &[u8]) -> LenientResult {
1343 validate_envelope_lenient(&parse(input))
1344 }
1345
1346 #[test]
1347 fn lenient_collects_every_violation_not_just_the_first() {
1348 let input = b"UNB+UNOA:1+S+R+200101:0900+CTRL1'\
1351 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+99+1'\
1352 UNZ+1+CTRL2'";
1353 let result = parse_and_validate_lenient(input);
1354 assert!(
1355 result.errors.len() >= 2,
1356 "expected both violations, got {:?}",
1357 result.errors
1358 );
1359 assert!(
1360 result
1361 .errors
1362 .iter()
1363 .any(|e| matches!(e, EdifactError::QualifierMismatch { tag, .. } if tag == "UNZ")),
1364 "missing UNZ control-ref mismatch in {:?}",
1365 result.errors
1366 );
1367 assert!(
1368 result
1369 .errors
1370 .iter()
1371 .any(|e| matches!(e, EdifactError::SegmentCountMismatch { .. })),
1372 "missing UNT segment-count mismatch in {:?}",
1373 result.errors
1374 );
1375 }
1376
1377 #[test]
1378 fn strict_reports_the_first_error_lenient_collects() {
1379 for input in [
1382 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+2+1'UNZ+9+C1'"[..],
1383 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+99+1'UNZ+1+C1'"[..],
1384 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+2+9'UNZ+1+C1'"[..],
1385 ] {
1386 let strict = parse_and_validate(input);
1387 let lenient = parse_and_validate_lenient(input);
1388 match strict {
1389 Err(e) => assert_eq!(
1390 Some(&e),
1391 lenient.errors.first(),
1392 "strict/lenient diverged for {:?}",
1393 std::str::from_utf8(input).unwrap()
1394 ),
1395 Ok(_) => assert!(lenient.errors.is_empty()),
1396 }
1397 }
1398 }
1399
1400 #[test]
1401 fn duplicate_message_references_are_rejected() {
1402 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'\
1403 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+3+1'\
1404 UNH+1+ORDERS:D:96A:UN'BGM+221'UNT+3+1'\
1405 UNZ+2+C1'";
1406 let err = parse_and_validate(input).expect_err("duplicate UNH refs must fail");
1407 assert!(
1408 matches!(&err, EdifactError::DuplicateReference { tag, reference, .. }
1409 if tag == "UNH" && reference == "1"),
1410 "got {err:?}"
1411 );
1412 }
1413
1414 #[test]
1415 fn distinct_message_references_are_accepted() {
1416 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'\
1417 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+3+1'\
1418 UNH+2+ORDERS:D:96A:UN'BGM+221'UNT+3+2'\
1419 UNZ+2+C1'";
1420 parse_and_validate(input).expect("distinct refs must validate");
1421 }
1422
1423 #[test]
1424 fn into_strict_is_total_after_the_caller_filters_errors() {
1425 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'UNZ+1+C2'";
1428 let mut lenient = parse_and_validate_lenient(input);
1429 lenient.errors.clear();
1430 let _ = lenient.into_strict();
1432 }
1433
1434 fn parse_and_validate_owned(input: &[u8]) -> Result<ValidatedInterchange, EdifactError> {
1435 validate_envelope(&parse(input))
1436 }
1437
1438 const VALID_INTERCHANGE: &[u8] =
1439 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'";
1440
1441 #[test]
1442 fn valid_envelope_parses_ok() {
1443 let result = parse_and_validate(VALID_INTERCHANGE).expect("envelope should be valid");
1444 assert_eq!(result.interchange.sender_id, "SENDER");
1445 assert_eq!(result.interchange.sender_qualifier, ""); assert_eq!(result.interchange.recipient_id, "RECEIVER");
1447 assert_eq!(result.interchange.recipient_qualifier, "");
1448 assert_eq!(result.interchange.syntax_identifier, "UNOA");
1449 assert_eq!(result.interchange.syntax_version, "3");
1450 assert_eq!(result.interchange.control_ref, "00001");
1451 assert_eq!(result.interchange.declared_unit_count, 1);
1452 assert_eq!(result.interchange.actual_unit_count, 1);
1453 assert!(!result.interchange.is_test());
1454 assert!(!result.has_functional_groups());
1455 assert_eq!(result.message_count(), 1);
1456 assert_eq!(result.messages[0].message_type, "ORDERS");
1457 assert_eq!(result.messages[0].association_code, "EAN010");
1458 assert_eq!(result.messages[0].declared_segment_count, 4);
1459 assert_eq!(result.messages[0].actual_segment_count, 4);
1460 }
1461
1462 #[test]
1463 fn valid_envelope_parses_ok_owned_path() {
1464 let result = parse_and_validate_owned(VALID_INTERCHANGE).expect("envelope should be valid");
1465 assert_eq!(result.interchange.sender_id, "SENDER");
1466 assert_eq!(result.interchange.actual_unit_count, 1);
1467 assert_eq!(result.messages[0].declared_segment_count, 4);
1468 }
1469
1470 #[test]
1471 fn unt_count_mismatch_returns_err() {
1472 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'";
1473 let result = parse_and_validate(input);
1474 assert!(
1475 matches!(
1476 result,
1477 Err(EdifactError::SegmentCountMismatch { expected: 99, .. })
1478 ),
1479 "expected SegmentCountMismatch, got {result:?}"
1480 );
1481 }
1482
1483 #[test]
1484 fn unz_count_mismatch_returns_err() {
1485 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'";
1486 let result = parse_and_validate(input);
1487 assert!(
1488 matches!(
1489 result,
1490 Err(EdifactError::MessageCountMismatch {
1491 expected: 2,
1492 actual: 1
1493 })
1494 ),
1495 "expected MessageCountMismatch(2,1), got {result:?}"
1496 );
1497 }
1498
1499 #[test]
1500 fn missing_unb_returns_err() {
1501 let input = b"UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
1502 assert!(parse_and_validate(input).is_err());
1503 }
1504
1505 #[test]
1506 fn extracts_una_interchange_correctly() {
1507 let result = parse_and_validate(VALID_INTERCHANGE).unwrap();
1508 assert_eq!(result.interchange.syntax_identifier, "UNOA");
1509 assert_eq!(result.interchange.syntax_version, "3");
1510 assert_eq!(result.interchange.date, "230401");
1511 assert_eq!(result.interchange.time.as_deref(), Some("0900"));
1512 }
1513
1514 #[test]
1515 fn sender_and_recipient_qualifiers_extracted() {
1516 let input = b"UNB+UNOA:3+1234567890123:14+9876543210987:14+200101:0900+1'\
1518 UNH+1+ORDERS:D:96A:UN'\
1519 BGM+220+PO-001+9'\
1520 UNT+3+1'\
1521 UNZ+1+1'";
1522 let r = parse_and_validate(input).expect("GLN-qualified UNB must parse ok");
1523 assert_eq!(r.interchange.sender_id, "1234567890123");
1524 assert_eq!(r.interchange.sender_qualifier, "14");
1525 assert_eq!(r.interchange.recipient_id, "9876543210987");
1526 assert_eq!(r.interchange.recipient_qualifier, "14");
1527 }
1528
1529 #[test]
1533 fn test_indicator_parsed_from_unb() {
1534 let input = b"UNB+UNOA:3+S+R+200101:0900+1++++++1'\
1538 UNH+1+ORDERS:D:96A:UN'\
1539 BGM+220+PO-001+9'\
1540 UNT+3+1'\
1541 UNZ+1+1'";
1542 let r = parse_and_validate(input).expect("test-flagged UNB must parse ok");
1543 assert!(
1544 r.interchange.test_indicator,
1545 "test_indicator should be true"
1546 );
1547 assert!(r.interchange.is_test(), "is_test() convenience must agree");
1548 }
1549
1550 #[test]
1551 fn no_test_indicator_defaults_false() {
1552 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1553 assert!(!r.interchange.test_indicator);
1554 assert!(!r.interchange.is_test());
1555 }
1556
1557 #[test]
1558 fn app_ref_extracted_when_present() {
1559 let input = b"UNB+UNOA:3+S+R+200101:0900+1++MYAPP'\
1561 UNH+1+ORDERS:D:96A:UN'\
1562 BGM+220+PO-001+9'\
1563 UNT+3+1'\
1564 UNZ+1+1'";
1565 let r = parse_and_validate(input).expect("UNB with app_ref must parse ok");
1566 assert_eq!(r.interchange.app_ref.as_deref(), Some("MYAPP"));
1567 }
1568
1569 #[test]
1570 fn app_ref_is_none_when_absent() {
1571 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1572 assert!(r.interchange.app_ref.is_none());
1573 }
1574
1575 #[test]
1576 fn recipient_password_extracted_when_present() {
1577 let input = b"UNB+UNOA:3+S+R+200101:0900+1+MYPASS'\
1579 UNH+1+ORDERS:D:96A:UN'\
1580 BGM+220+PO-001+9'\
1581 UNT+3+1'\
1582 UNZ+1+1'";
1583 let r = parse_and_validate(input).expect("UNB with password must parse ok");
1584 assert_eq!(r.interchange.recipient_password.as_deref(), Some("MYPASS"));
1585 }
1586
1587 #[test]
1588 fn recipient_password_is_none_when_absent() {
1589 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1590 assert!(r.interchange.recipient_password.is_none());
1591 }
1592
1593 #[test]
1594 fn acknowledgement_request_flag_parsed() {
1595 let input = b"UNB+UNOA:3+S+R+200101:0900+1++++1'\
1598 UNH+1+ORDERS:D:96A:UN'\
1599 BGM+220+PO-001+9'\
1600 UNT+3+1'\
1601 UNZ+1+1'";
1602 let r = parse_and_validate(input).expect("UNB with ack-request must parse ok");
1603 assert!(r.interchange.acknowledgement_request);
1604 assert!(r.interchange.ack_requested());
1605 }
1606
1607 #[test]
1608 fn acknowledgement_request_defaults_false() {
1609 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1610 assert!(!r.interchange.acknowledgement_request);
1611 assert!(!r.interchange.ack_requested());
1612 }
1613
1614 #[test]
1615 fn communications_agreement_id_extracted() {
1616 let input = b"UNB+UNOA:3+S+R+200101:0900+1+++++AGREEMENT-1'\
1618 UNH+1+ORDERS:D:96A:UN'\
1619 BGM+220+PO-001+9'\
1620 UNT+3+1'\
1621 UNZ+1+1'";
1622 let r = parse_and_validate(input).expect("UNB with comms-agreement must parse ok");
1623 assert_eq!(
1624 r.interchange.communications_agreement_id.as_deref(),
1625 Some("AGREEMENT-1")
1626 );
1627 }
1628
1629 #[test]
1630 fn dangling_unh_without_unt_returns_err() {
1631 let input =
1632 b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNZ+1+1'";
1633 let result = parse_and_validate(input);
1634 assert!(
1635 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNT")
1636 );
1637 }
1638
1639 #[test]
1640 fn stray_segment_outside_message_returns_err() {
1641 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'";
1642 assert!(matches!(
1643 parse_and_validate(input),
1644 Err(EdifactError::InvalidSegmentForMessage { .. })
1645 ));
1646 }
1647
1648 #[test]
1649 fn missing_unb_sender_component_returns_err() {
1650 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'";
1651 let result = parse_and_validate(input);
1652 assert!(
1653 matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 0 }) if tag == "UNB"),
1654 "expected MissingRequiredComponent for empty sender, got: {result:?}"
1655 );
1656 }
1657
1658 #[test]
1659 fn nested_unh_without_closing_previous_message_returns_err() {
1660 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'";
1661 let result = parse_and_validate(input);
1662 assert!(
1663 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNH"),
1664 "expected InvalidSegmentForMessage(UNH), got {result:?}"
1665 );
1666 }
1667
1668 #[test]
1669 fn unt_message_reference_must_match_unh() {
1670 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'";
1671 let result = parse_and_validate(input);
1672 assert!(
1673 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNT")
1674 );
1675 }
1676
1677 #[test]
1678 fn unz_control_reference_must_match_unb() {
1679 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'";
1680 let result = parse_and_validate(input);
1681 assert!(
1682 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNZ")
1683 );
1684 }
1685
1686 #[test]
1687 fn missing_unh_message_type_components_return_err() {
1688 let input =
1689 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'";
1690 let result = parse_and_validate(input);
1691 assert!(
1692 matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 3 }) if tag == "UNH"),
1693 "got: {result:?}"
1694 );
1695 }
1696
1697 #[test]
1698 fn nested_unz_inside_message_returns_err() {
1699 let input =
1700 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'";
1701 let result = parse_and_validate(input);
1702 assert!(
1703 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNZ")
1704 );
1705 }
1706
1707 #[test]
1708 fn lenient_returns_partial_result_on_count_mismatch() {
1709 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'";
1712 let owned = parse(input);
1713 let lenient = validate_envelope_lenient(&owned);
1714 let result = lenient.interchange;
1715 let errors = lenient.errors;
1716 assert!(
1717 result.is_some(),
1718 "lenient mode must return Some even on count mismatch"
1719 );
1720 assert_eq!(errors.len(), 1);
1721 assert!(
1722 matches!(
1723 &errors[0],
1724 EdifactError::MessageCountMismatch {
1725 expected: 2,
1726 actual: 1
1727 }
1728 ),
1729 "expected MessageCountMismatch(2,1), got {:?}",
1730 errors[0]
1731 );
1732 let partial = result.unwrap();
1733 assert_eq!(partial.messages.len(), 1);
1734 assert_eq!(partial.interchange.actual_unit_count, 1);
1735 assert_eq!(partial.interchange.declared_unit_count, 2);
1736 }
1737
1738 #[test]
1739 fn lenient_returns_none_on_structural_error() {
1740 let input = b"UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
1742 let owned = parse(input);
1743 let lenient = validate_envelope_lenient(&owned);
1744 let result = lenient.interchange;
1745 let errors = lenient.errors;
1746 assert!(result.is_none(), "missing UNB must yield None");
1747 assert!(!errors.is_empty());
1748 }
1749
1750 #[test]
1751 fn message_count_convenience_method() {
1752 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1753 assert_eq!(r.message_count(), r.messages.len());
1754 assert_eq!(r.message_count(), 1);
1755 }
1756
1757 #[test]
1758 fn interchange_with_single_functional_group_parses_ok() {
1759 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1760 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1761 UNH+1+ORDERS:D:96A:UN'\
1762 BGM+220+PO-001+9'\
1763 UNT+3+1'\
1764 UNE+1+1'\
1765 UNZ+1+1'";
1766 let result = parse_and_validate(input).expect("single-group interchange must parse ok");
1767 assert!(result.has_functional_groups());
1768 assert_eq!(result.functional_groups.len(), 1);
1769 let g = &result.functional_groups[0];
1770 assert_eq!(g.group_id, "ORDERS");
1771 assert_eq!(g.group_ref, "1");
1772 assert_eq!(g.controlling_agency, "UN");
1773 assert_eq!(g.declared_message_count, 1);
1774 assert_eq!(g.actual_message_count, 1);
1775 assert_eq!(result.messages.len(), 1);
1776 assert_eq!(result.messages[0].message_type, "ORDERS");
1777 assert_eq!(result.interchange.declared_unit_count, 1);
1778 assert_eq!(result.interchange.actual_unit_count, 1);
1779 }
1780
1781 #[test]
1782 fn interchange_with_multi_message_group_parses_ok() {
1783 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1786 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1787 UNH+1+ORDERS:D:96A:UN'\
1788 BGM+220+PO-001+9'\
1789 UNT+3+1'\
1790 UNH+2+ORDERS:D:96A:UN'\
1791 BGM+220+PO-002+9'\
1792 UNT+3+2'\
1793 UNE+2+1'\
1794 UNZ+1+1'";
1795 let result = parse_and_validate(input).expect("multi-message group must parse ok");
1796 assert!(result.has_functional_groups());
1797 assert_eq!(result.functional_groups.len(), 1);
1798 assert_eq!(result.functional_groups[0].actual_message_count, 2);
1799 assert_eq!(result.messages.len(), 2);
1800 assert_eq!(result.interchange.actual_unit_count, 1);
1802 assert_eq!(result.interchange.declared_unit_count, 1);
1803 }
1804
1805 #[test]
1806 fn interchange_with_multiple_groups_parses_ok() {
1807 let input = b"UNB+UNOA:3+S+R+200101:0900+2'\
1808 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1809 UNH+1+ORDERS:D:96A:UN'\
1810 BGM+220+PO-001+9'\
1811 UNT+3+1'\
1812 UNE+1+1'\
1813 UNG+INVOIC+S+R+200101:0900+2+UN+D:96A'\
1814 UNH+2+INVOIC:D:96A:UN'\
1815 BGM+380+INV-001+9'\
1816 UNT+3+2'\
1817 UNE+1+2'\
1818 UNZ+2+2'";
1819 let result = parse_and_validate(input).expect("multi-group interchange must parse ok");
1820 assert_eq!(result.functional_groups.len(), 2);
1821 assert_eq!(result.functional_groups[0].group_id, "ORDERS");
1822 assert_eq!(result.functional_groups[1].group_id, "INVOIC");
1823 assert_eq!(result.messages.len(), 2);
1824 assert_eq!(result.interchange.declared_unit_count, 2);
1825 assert_eq!(result.interchange.actual_unit_count, 2);
1826 }
1827
1828 #[test]
1829 fn ung_une_count_mismatch_returns_err() {
1830 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1831 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1832 UNH+1+ORDERS:D:96A:UN'\
1833 BGM+220+PO-001+9'\
1834 UNT+3+1'\
1835 UNE+2+1'\
1836 UNZ+1+1'";
1837 let result = parse_and_validate(input);
1838 assert!(
1839 matches!(
1840 result,
1841 Err(EdifactError::MessageCountMismatch {
1842 expected: 2,
1843 actual: 1
1844 })
1845 ),
1846 "expected MessageCountMismatch(2,1), got {result:?}"
1847 );
1848 }
1849
1850 #[test]
1851 fn une_without_ung_returns_err() {
1852 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1853 UNE+1+1'\
1854 UNH+1+ORDERS:D:96A:UN'\
1855 BGM+220+PO-001+9'\
1856 UNT+3+1'\
1857 UNZ+1+1'";
1858 let result = parse_and_validate(input);
1859 assert!(
1860 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNE"),
1861 "expected InvalidSegmentForMessage(UNE), got {result:?}"
1862 );
1863 }
1864
1865 #[test]
1866 fn ung_without_une_returns_err() {
1867 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1868 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1869 UNH+1+ORDERS:D:96A:UN'\
1870 BGM+220+PO-001+9'\
1871 UNT+3+1'\
1872 UNZ+1+1'";
1873 let result = parse_and_validate(input);
1874 assert!(
1875 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNE"),
1876 "expected MissingSegment(UNE), got {result:?}"
1877 );
1878 }
1879
1880 #[test]
1881 fn une_group_ref_must_match_ung() {
1882 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1883 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1884 UNH+1+ORDERS:D:96A:UN'\
1885 BGM+220+PO-001+9'\
1886 UNT+3+1'\
1887 UNE+1+999'\
1888 UNZ+1+1'";
1889 let result = parse_and_validate(input);
1890 assert!(
1891 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNE"),
1892 "expected QualifierMismatch(UNE), got {result:?}"
1893 );
1894 }
1895
1896 #[test]
1899 fn processing_priority_extracted_when_present() {
1900 let input = b"UNB+UNOA:3+S+R+200101:0900+1+++A'\
1902 UNH+1+ORDERS:D:96A:UN'\
1903 BGM+220+PO-001+9'\
1904 UNT+3+1'\
1905 UNZ+1+1'";
1906 let r = parse_and_validate(input).expect("UNB with processing_priority must parse ok");
1907 assert_eq!(r.interchange.processing_priority.as_deref(), Some("A"));
1908 }
1909
1910 #[test]
1911 fn processing_priority_is_none_when_absent() {
1912 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1913 assert!(r.interchange.processing_priority.is_none());
1914 }
1915
1916 #[test]
1917 fn sender_routing_address_extracted_when_present() {
1918 let input = b"UNB+UNOA:3+SENDER:14:ROUTEA+RECIP+200101:0900+1'\
1920 UNH+1+ORDERS:D:96A:UN'\
1921 BGM+220+PO-001+9'\
1922 UNT+3+1'\
1923 UNZ+1+1'";
1924 let r = parse_and_validate(input).expect("UNB with sender routing must parse ok");
1925 assert_eq!(
1926 r.interchange.sender_routing_address.as_deref(),
1927 Some("ROUTEA")
1928 );
1929 assert!(r.interchange.recipient_routing_address.is_none());
1930 }
1931
1932 #[test]
1933 fn recipient_routing_address_extracted_when_present() {
1934 let input = b"UNB+UNOA:3+SENDER+RECIP:14:ROUTEB+200101:0900+1'\
1936 UNH+1+ORDERS:D:96A:UN'\
1937 BGM+220+PO-001+9'\
1938 UNT+3+1'\
1939 UNZ+1+1'";
1940 let r = parse_and_validate(input).expect("UNB with recipient routing must parse ok");
1941 assert!(r.interchange.sender_routing_address.is_none());
1942 assert_eq!(
1943 r.interchange.recipient_routing_address.as_deref(),
1944 Some("ROUTEB")
1945 );
1946 }
1947
1948 #[test]
1949 fn routing_address_is_none_when_absent() {
1950 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1952 UNH+1+ORDERS:D:96A:UN'\
1953 BGM+220+PO-001+9'\
1954 UNT+3+1'\
1955 UNZ+1+1'";
1956 let r = parse_and_validate(input).unwrap();
1957 assert!(r.interchange.sender_routing_address.is_none());
1958 assert!(r.interchange.recipient_routing_address.is_none());
1959 }
1960
1961 #[test]
1962 fn common_access_ref_extracted_when_present() {
1963 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1965 UNH+1+ORDERS:D:96A:UN+COMREF1'\
1966 BGM+220+PO-001+9'\
1967 UNT+3+1'\
1968 UNZ+1+1'";
1969 let r = parse_and_validate(input).expect("UNH with common_access_ref must parse ok");
1970 assert_eq!(r.messages[0].common_access_ref.as_deref(), Some("COMREF1"));
1971 }
1972
1973 #[test]
1974 fn common_access_ref_is_none_when_absent() {
1975 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1976 assert!(r.messages[0].common_access_ref.is_none());
1977 }
1978
1979 #[test]
1980 fn parse_unh_includes_message_ref() {
1981 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1982 UNH+REF42+ORDERS:D:96A:UN'\
1983 BGM+220+PO-001+9'\
1984 UNT+3+REF42'\
1985 UNZ+1+1'";
1986 let r = parse_and_validate(input).expect("must parse ok");
1989 assert_eq!(r.messages[0].message_ref, "REF42");
1990 assert_eq!(r.messages[0].message_type, "ORDERS");
1991 }
1992
1993 #[test]
1994 fn iter_messages_by_type_returns_matching_messages() {
1995 let input = b"UNB+UNOA:3+S+R+200101:0900+CTRL2'\
1996 UNH+1+ORDERS:D:96A:UN'\
1997 BGM+220+PO-001+9'\
1998 UNT+3+1'\
1999 UNH+2+INVOIC:D:96A:UN'\
2000 BGM+380+INV-001+9'\
2001 UNT+3+2'\
2002 UNZ+2+CTRL2'";
2003 let r = parse_and_validate(input).expect("two-message interchange must parse ok");
2004 let orders: Vec<_> = r.iter_messages_by_type("ORDERS").collect();
2005 assert_eq!(orders.len(), 1);
2006 assert_eq!(orders[0].message_ref, "1");
2007 let invoices: Vec<_> = r.iter_messages_by_type("INVOIC").collect();
2008 assert_eq!(invoices.len(), 1);
2009 assert_eq!(invoices[0].message_ref, "2");
2010 let none: Vec<_> = r.iter_messages_by_type("DESADV").collect();
2011 assert!(none.is_empty());
2012 }
2013
2014 #[test]
2015 fn display_interchange_envelope() {
2016 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2017 let s = r.interchange.to_string();
2018 assert!(s.contains("SENDER"), "Display must include sender_id");
2019 assert!(s.contains("UNOA"), "Display must include syntax_identifier");
2020 }
2021
2022 #[test]
2023 fn display_message_envelope() {
2024 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2025 let s = r.messages[0].to_string();
2026 assert!(s.contains("ORDERS"), "Display must include message_type");
2027 assert!(s.contains("ref="), "Display must include message ref label");
2028 }
2029
2030 #[test]
2031 fn display_validated_interchange() {
2032 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2033 let s = r.to_string();
2034 assert!(
2035 s.contains("messages="),
2036 "Display must include message count label"
2037 );
2038 }
2039
2040 #[test]
2043 fn unrecognised_syntax_identifier_returns_err() {
2044 let input = b"UNB+XXXX:3+S+R+200101:0900+1'\
2046 UNH+1+ORDERS:D:96A:UN'\
2047 BGM+220+PO-001+9'\
2048 UNT+3+1'\
2049 UNZ+1+1'";
2050 let result = parse_and_validate(input);
2051 assert!(
2052 matches!(result, Err(EdifactError::UnrecognisedSyntaxIdentifier(ref id)) if id == "XXXX"),
2053 "expected UnrecognisedSyntaxIdentifier(\"XXXX\"), got {result:?}"
2054 );
2055 }
2056
2057 #[test]
2058 fn every_repertoire_the_crate_decodes_is_accepted_in_the_unb() {
2059 for id in [
2062 "UNOA", "UNOB", "UNOC", "UNOD", "UNOE", "UNOF", "UNOG", "UNOH", "UNOI", "UNOJ", "UNOK",
2063 "UNOY",
2064 ] {
2065 let input = format!(
2066 "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'"
2067 );
2068 let result = parse_and_validate(input.as_bytes());
2069 assert!(
2070 result.is_ok(),
2071 "syntax id '{id}' should be accepted, got {result:?}"
2072 );
2073 }
2074 }
2075
2076 #[test]
2077 fn a_defined_but_undecodable_repertoire_says_so() {
2078 for id in ["UNOX", "KECA"] {
2082 let input = format!(
2083 "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'"
2084 );
2085 let result = parse_and_validate(input.as_bytes());
2086 assert!(
2087 matches!(result, Err(EdifactError::UnsupportedCharset { ref syntax_identifier }) if syntax_identifier == id),
2088 "expected UnsupportedCharset for '{id}', got {result:?}"
2089 );
2090 }
2091 }
2092
2093 #[test]
2094 fn an_interchange_with_no_content_is_rejected() {
2095 let result = parse_and_validate(b"UNB+UNOC:3+S+R+260101:0900+IC1'UNZ+0+IC1'");
2098 assert!(
2099 matches!(result, Err(EdifactError::EmptyInterchange { ref control_ref }) if control_ref == "IC1"),
2100 "expected EmptyInterchange, got {result:?}"
2101 );
2102 }
2103
2104 #[test]
2105 fn a_message_with_no_body_is_rejected() {
2106 let result = parse_and_validate(
2109 b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+1+ORDERS:D:96A:UN'UNT+2+1'UNZ+1+IC1'",
2110 );
2111 assert!(
2112 matches!(result, Err(EdifactError::EmptyMessage { ref message_ref, .. }) if message_ref == "1"),
2113 "expected EmptyMessage, got {result:?}"
2114 );
2115 }
2116
2117 #[test]
2118 fn a_package_is_reported_as_a_package() {
2119 let result = parse_and_validate(
2123 b"UNB+UNOC:4+S+R+260101:0900+IC1'UNO+PKG1+AAF:1+ZZZ+1024'UNZ+1+IC1'",
2124 );
2125 assert!(
2126 matches!(result, Err(EdifactError::PackageNotSupported { ref tag, .. }) if tag == "UNO"),
2127 "expected PackageNotSupported, got {result:?}"
2128 );
2129 }
2130
2131 #[test]
2134 fn recipient_password_qualifier_extracted_when_present() {
2135 let input = b"UNB+UNOA:3+S+R+200101:0900+1+MYPASS:AA'\
2137 UNH+1+ORDERS:D:96A:UN'\
2138 BGM+220+PO-001+9'\
2139 UNT+3+1'\
2140 UNZ+1+1'";
2141 let r = parse_and_validate(input).expect("UNB with password+qualifier must parse ok");
2142 assert_eq!(r.interchange.recipient_password.as_deref(), Some("MYPASS"));
2143 assert_eq!(
2144 r.interchange.recipient_password_qualifier.as_deref(),
2145 Some("AA")
2146 );
2147 }
2148
2149 #[test]
2150 fn recipient_password_qualifier_is_none_when_absent() {
2151 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2152 assert!(r.interchange.recipient_password_qualifier.is_none());
2153 }
2154
2155 #[test]
2158 fn sequence_of_transfers_extracted_when_present() {
2159 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2161 UNH+1+ORDERS:D:96A:UN++2:C'\
2162 BGM+220+PO-001+9'\
2163 UNT+3+1'\
2164 UNZ+1+1'";
2165 let r = parse_and_validate(input).expect("UNH with S010 must parse ok");
2166 assert_eq!(r.messages[0].sequence_of_transfers, Some(2));
2167 assert_eq!(r.messages[0].transfer_position.as_deref(), Some("C"));
2168 }
2169
2170 #[test]
2171 fn sequence_of_transfers_none_when_absent() {
2172 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2173 assert!(r.messages[0].sequence_of_transfers.is_none());
2174 assert!(r.messages[0].transfer_position.is_none());
2175 }
2176
2177 #[test]
2180 fn ung_app_sender_and_recipient_qualifiers_extracted() {
2181 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2183 UNG+ORDERS+APPSEND:ZZZ+APPRECV:14+200101:0900+1+UN+D:96A'\
2184 UNH+1+ORDERS:D:96A:UN'\
2185 BGM+220+PO-001+9'\
2186 UNT+3+1'\
2187 UNE+1+1'\
2188 UNZ+1+1'";
2189 let r = parse_and_validate(input).expect("UNG with qualifiers must parse ok");
2190 let g = &r.functional_groups[0];
2191 assert_eq!(g.app_sender, "APPSEND");
2192 assert_eq!(g.app_sender_qualifier, "ZZZ");
2193 assert_eq!(g.app_recipient, "APPRECV");
2194 assert_eq!(g.app_recipient_qualifier, "14");
2195 }
2196
2197 #[test]
2198 fn ung_qualifiers_empty_when_absent() {
2199 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2200 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
2201 UNH+1+ORDERS:D:96A:UN'\
2202 BGM+220+PO-001+9'\
2203 UNT+3+1'\
2204 UNE+1+1'\
2205 UNZ+1+1'";
2206 let r = parse_and_validate(input).unwrap();
2207 let g = &r.functional_groups[0];
2208 assert_eq!(g.app_sender_qualifier, "");
2209 assert_eq!(g.app_recipient_qualifier, "");
2210 }
2211
2212 #[test]
2215 fn lenient_result_is_valid_true_on_clean_interchange() {
2216 let owned = parse(VALID_INTERCHANGE);
2217 let r = validate_envelope_lenient(&owned);
2218 assert!(r.is_valid());
2219 assert!(r.errors.is_empty());
2220 assert!(r.interchange.is_some());
2221 }
2222
2223 #[test]
2224 fn lenient_result_into_strict_ok_path() {
2225 let owned = parse(VALID_INTERCHANGE);
2226 let r = validate_envelope_lenient(&owned);
2227 let strict = r.into_strict();
2228 assert!(
2229 strict.is_ok(),
2230 "into_strict() should succeed for valid interchange"
2231 );
2232 assert_eq!(strict.unwrap().messages.len(), 1);
2233 }
2234
2235 #[test]
2236 fn lenient_result_into_strict_err_path() {
2237 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'";
2239 let owned = parse(input);
2240 let r = validate_envelope_lenient(&owned);
2241 assert!(!r.is_valid());
2242 let strict = r.into_strict();
2243 assert!(strict.is_err());
2244 let errors = strict.unwrap_err();
2245 assert_eq!(errors.len(), 1);
2246 assert!(matches!(
2247 &errors[0],
2248 EdifactError::MessageCountMismatch {
2249 expected: 2,
2250 actual: 1
2251 }
2252 ));
2253 }
2254
2255 #[test]
2258 fn parse_unh_direct_extracts_all_s009_fields() {
2259 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2264 UNH+REF99+ORDERS:D:96A:UN:EAN010'\
2265 BGM+220+PO-001+9'\
2266 UNT+3+REF99'\
2267 UNZ+1+1'";
2268 let r = parse_and_validate(input).expect("must parse ok");
2269 let msg = &r.messages[0];
2270 assert_eq!(msg.message_ref, "REF99");
2271 assert_eq!(msg.message_type, "ORDERS");
2272 assert_eq!(msg.version, "D");
2273 assert_eq!(msg.release, "96A");
2274 assert_eq!(msg.controlling_agency, "UN");
2275 assert_eq!(msg.association_code, "EAN010");
2276 }
2277
2278 #[test]
2279 fn parse_ung_direct_extracts_identifier_fields() {
2280 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2282 UNG+ORDERS+APPSEND:ZZZ+APPRECV:14+200101:0900+GRP01+UN+D:96A'\
2283 UNH+1+ORDERS:D:96A:UN'\
2284 BGM+220+PO-001+9'\
2285 UNT+3+1'\
2286 UNE+1+GRP01'\
2287 UNZ+1+1'";
2288 let r = parse_and_validate(input).expect("must parse ok");
2289 let g = &r.functional_groups[0];
2290 assert_eq!(g.group_id, "ORDERS");
2291 assert_eq!(g.app_sender, "APPSEND");
2292 assert_eq!(g.app_sender_qualifier, "ZZZ");
2293 assert_eq!(g.app_recipient, "APPRECV");
2294 assert_eq!(g.app_recipient_qualifier, "14");
2295 assert_eq!(g.group_ref, "GRP01");
2296 assert_eq!(g.controlling_agency, "UN");
2297 assert_eq!(g.version, "D");
2298 assert_eq!(g.release, "96A");
2299 }
2300
2301 #[test]
2304 fn find_message_returns_correct_message_by_ref() {
2305 let input = b"UNB+UNOA:3+S+R+200101:0900+CTRL2'\
2306 UNH+REF-A+ORDERS:D:96A:UN'\
2307 BGM+220+PO-001+9'\
2308 UNT+3+REF-A'\
2309 UNH+REF-B+INVOIC:D:96A:UN'\
2310 BGM+380+INV-001+9'\
2311 UNT+3+REF-B'\
2312 UNZ+2+CTRL2'";
2313 let r = parse_and_validate(input).expect("two-message interchange must parse ok");
2314 let msg_a = r.find_message("REF-A");
2315 assert!(msg_a.is_some());
2316 assert_eq!(msg_a.unwrap().message_type, "ORDERS");
2317 let msg_b = r.find_message("REF-B");
2318 assert!(msg_b.is_some());
2319 assert_eq!(msg_b.unwrap().message_type, "INVOIC");
2320 assert!(r.find_message("MISSING").is_none());
2321 }
2322
2323 #[test]
2326 fn ung_missing_group_ref_returns_err() {
2327 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2329 UNG+ORDERS+S+R+200101:0900++UN+D:96A'\
2330 UNH+1+ORDERS:D:96A:UN'\
2331 BGM+220+PO-001+9'\
2332 UNT+3+1'\
2333 UNE+1+'\
2334 UNZ+1+1'";
2335 let result = parse_and_validate(input);
2336 assert!(
2337 matches!(result,
2338 Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 4, component_index: 0 })
2339 if tag == "UNG"
2340 ),
2341 "expected MissingRequiredComponent for empty UNG group_ref, got {result:?}"
2342 );
2343 }
2344
2345 #[test]
2348 fn empty_segment_list_returns_missing_unb() {
2349 let result = validate_envelope(&[]);
2352 assert!(
2353 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNB"),
2354 "expected MissingSegment(UNB) for empty input, got {result:?}"
2355 );
2356 }
2357
2358 #[test]
2359 fn single_segment_only_unb_returns_missing_unz() {
2360 let input = b"UNB+UNOA:3+S+R+200101:0900+1'";
2362 let owned = parse(input);
2363 let result = validate_envelope(&owned);
2364 assert!(
2365 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNZ"),
2366 "expected MissingSegment(UNZ) for UNB-only input, got {result:?}"
2367 );
2368 }
2369
2370 #[test]
2371 fn display_validated_interchange_includes_count() {
2372 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2373 let s = r.to_string();
2374 assert!(
2376 s.contains("messages=1"),
2377 "Display must include count '1': {s}"
2378 );
2379 }
2380
2381 #[test]
2382 fn display_interchange_envelope_contains_arrow() {
2383 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2384 let s = r.interchange.to_string();
2385 assert!(s.contains("->"), "Display must use ASCII '->' arrow: {s}");
2387 assert!(
2388 !s.contains('\u{2192}'),
2389 "Display must not use Unicode → arrow: {s}"
2390 );
2391 }
2392
2393 #[test]
2394 fn display_functional_group_envelope() {
2395 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2396 UNG+ORDERS+APPSEND+APPRECV+200101:0900+GRP01+UN+D:96A'\
2397 UNH+1+ORDERS:D:96A:UN'\
2398 BGM+220+PO-001+9'\
2399 UNT+3+1'\
2400 UNE+1+GRP01'\
2401 UNZ+1+1'";
2402 let r = parse_and_validate(input).expect("must parse ok");
2403 let g = &r.functional_groups[0];
2404 let s = g.to_string();
2405 assert!(s.contains("ORDERS"), "Display must include group_id: {s}");
2406 assert!(
2407 s.contains("APPSEND"),
2408 "Display must include app_sender: {s}"
2409 );
2410 assert!(
2411 s.contains("APPRECV"),
2412 "Display must include app_recipient: {s}"
2413 );
2414 assert!(s.contains("GRP01"), "Display must include group_ref: {s}");
2415 assert!(
2416 s.contains("msgs="),
2417 "Display must include message count label: {s}"
2418 );
2419 assert!(
2420 s.contains("1/1"),
2421 "Display must include actual/declared counts: {s}"
2422 );
2423 }
2424
2425 #[test]
2426 fn lenient_has_errors_is_inverse_of_is_valid() {
2427 let owned = parse(VALID_INTERCHANGE);
2428 let valid = validate_envelope_lenient(&owned);
2429 assert!(valid.is_valid());
2430 assert!(!valid.has_errors());
2431
2432 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'";
2434 let owned2 = parse(input);
2435 let invalid = validate_envelope_lenient(&owned2);
2436 assert!(!invalid.is_valid());
2437 assert!(invalid.has_errors());
2438 }
2439}