1use crate::{
21 OwnedSegment,
22 error::EdifactError,
23 model::{Segment, Span},
24};
25
26pub(crate) trait SegmentReader: sealed::Sealed {
29 fn tag(&self) -> &str;
30 fn span(&self) -> Span;
31 fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str>;
32
33 fn required_component_field(
34 &self,
35 elem_idx: usize,
36 comp_idx: usize,
37 ) -> Result<&str, EdifactError> {
38 self.component(elem_idx, comp_idx)
39 .filter(|s| !s.is_empty())
40 .ok_or_else(|| EdifactError::MissingRequiredComponent {
41 tag: self.tag().to_owned(),
42 element_index: elem_idx,
43 component_index: comp_idx,
44 })
45 }
46}
47
48mod sealed {
49 pub trait Sealed {}
50 impl Sealed for crate::model::Segment<'_> {}
51 impl Sealed for crate::OwnedSegment {}
52}
53
54impl SegmentReader for Segment<'_> {
55 #[inline]
56 fn tag(&self) -> &str {
57 self.tag
58 }
59 #[inline]
60 fn span(&self) -> Span {
61 self.span
62 }
63 #[inline]
64 fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str> {
65 self.get_element(elem_idx)?.get_component(comp_idx)
66 }
67}
68
69impl SegmentReader for OwnedSegment {
70 #[inline]
71 fn tag(&self) -> &str {
72 &self.tag
73 }
74 #[inline]
75 fn span(&self) -> Span {
76 self.span
77 }
78 #[inline]
79 fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str> {
80 self.component_str(elem_idx, comp_idx)
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Hash)]
108#[non_exhaustive]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub struct InterchangeEnvelope {
111 pub syntax_identifier: String,
113 pub syntax_version: String,
117 pub sender_id: String,
119 pub sender_qualifier: String,
124 pub sender_routing_address: Option<String>,
129 pub recipient_id: String,
131 pub recipient_qualifier: String,
135 pub recipient_routing_address: Option<String>,
139 pub date: String,
141 pub time: Option<String>,
145 pub control_ref: String,
147 pub recipient_password: Option<String>,
152 pub recipient_password_qualifier: Option<String>,
157 pub app_ref: Option<String>,
161 pub processing_priority: Option<String>,
166 pub acknowledgement_request: bool,
171 pub communications_agreement_id: Option<String>,
175 pub test_indicator: bool,
181 pub declared_unit_count: u32,
189 pub actual_unit_count: u32,
191}
192
193impl InterchangeEnvelope {
194 #[inline]
216 #[must_use]
217 pub fn is_test(&self) -> bool {
218 self.test_indicator
219 }
220
221 #[inline]
225 #[must_use]
226 pub fn ack_requested(&self) -> bool {
227 self.acknowledgement_request
228 }
229}
230
231impl std::fmt::Display for InterchangeEnvelope {
232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 write!(
234 f,
235 "{sender} -> {recipient} [{ctrl}] ({syntax}:{ver})",
236 sender = self.sender_id,
237 recipient = self.recipient_id,
238 ctrl = self.control_ref,
239 syntax = self.syntax_identifier,
240 ver = self.syntax_version,
241 )
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq, Hash)]
247#[non_exhaustive]
248#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
249pub struct MessageEnvelope {
250 pub message_ref: String,
252 pub message_type: String,
254 pub version: String,
256 pub release: String,
258 pub controlling_agency: String,
260 pub association_code: String,
262 pub common_access_ref: Option<String>,
269 pub sequence_of_transfers: Option<u32>,
275 pub transfer_position: Option<String>,
280 pub declared_segment_count: u32,
282 pub actual_segment_count: u32,
284}
285
286impl std::fmt::Display for MessageEnvelope {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 write!(
289 f,
290 "{msg_type}:{ver}:{rel} ref={msg_ref} seg={actual}/{declared}",
291 msg_type = self.message_type,
292 ver = self.version,
293 rel = self.release,
294 msg_ref = self.message_ref,
295 actual = self.actual_segment_count,
296 declared = self.declared_segment_count,
297 )
298 }
299}
300
301#[derive(Debug, Clone, PartialEq, Eq, Hash)]
307#[non_exhaustive]
308#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
309pub struct FunctionalGroupEnvelope {
310 pub group_id: String,
312 pub app_sender: String,
314 pub app_sender_qualifier: String,
318 pub app_recipient: String,
320 pub app_recipient_qualifier: String,
324 pub date: String,
326 pub time: Option<String>,
328 pub group_ref: String,
330 pub controlling_agency: String,
332 pub version: String,
334 pub release: String,
336 pub declared_message_count: u32,
338 pub actual_message_count: u32,
340 pub messages: Vec<MessageEnvelope>,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq, Hash)]
350#[non_exhaustive]
351#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
352pub struct ValidatedInterchange {
353 pub interchange: InterchangeEnvelope,
355 pub functional_groups: Vec<FunctionalGroupEnvelope>,
360 pub messages: Vec<MessageEnvelope>,
365}
366
367impl ValidatedInterchange {
368 #[inline]
370 #[must_use]
371 pub fn has_functional_groups(&self) -> bool {
372 !self.functional_groups.is_empty()
373 }
374
375 #[inline]
379 #[must_use]
380 pub fn message_count(&self) -> usize {
381 self.messages.len()
382 }
383
384 #[inline]
389 pub fn iter_messages(&self) -> impl Iterator<Item = &MessageEnvelope> {
390 self.messages.iter()
391 }
392
393 #[inline]
400 #[must_use]
401 pub fn find_message(&self, reference: &str) -> Option<&MessageEnvelope> {
402 self.messages.iter().find(|m| m.message_ref == reference)
403 }
404
405 #[must_use]
414 pub fn messages_by_type(&self, message_type: &str) -> Vec<&MessageEnvelope> {
415 self.messages
416 .iter()
417 .filter(|m| m.message_type == message_type)
418 .collect()
419 }
420
421 #[inline]
432 pub fn iter_messages_by_type<'s, 'q: 's>(
433 &'s self,
434 message_type: &'q str,
435 ) -> impl Iterator<Item = &'s MessageEnvelope> + 's {
436 self.messages
437 .iter()
438 .filter(move |m| m.message_type == message_type)
439 }
440}
441
442impl std::fmt::Display for ValidatedInterchange {
443 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444 write!(
445 f,
446 "{ic} messages={n}",
447 ic = self.interchange,
448 n = self.messages.len(),
449 )
450 }
451}
452
453impl std::fmt::Display for FunctionalGroupEnvelope {
454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455 write!(
456 f,
457 "{gid} sender={sender} recipient={recip} [{gref}] ({agency}) msgs={actual}/{declared}",
458 gid = self.group_id,
459 sender = self.app_sender,
460 recip = self.app_recipient,
461 gref = self.group_ref,
462 agency = self.controlling_agency,
463 actual = self.actual_message_count,
464 declared = self.declared_message_count,
465 )
466 }
467}
468
469#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
474#[non_exhaustive]
475#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
476pub struct MessageIdentifier<'a> {
477 pub message_ref: &'a str,
479 pub message_type: &'a str,
480 pub version: &'a str,
481 pub release: &'a str,
482 pub controlling_agency: &'a str,
483 pub association_code: &'a str,
487}
488
489pub fn parse_unh<'a>(unh: &'a Segment<'a>) -> Result<MessageIdentifier<'a>, EdifactError> {
493 let message_ref = unh
495 .get_element(0)
496 .and_then(|e| e.get_component(0))
497 .filter(|s| !s.is_empty())
498 .ok_or_else(|| EdifactError::MissingRequiredComponent {
499 tag: "UNH".to_owned(),
500 element_index: 0,
501 component_index: 0,
502 })?;
503 let elem = unh
505 .get_element(1)
506 .ok_or_else(|| EdifactError::MissingRequiredElement {
507 tag: "UNH".to_owned(),
508 element_index: 1,
509 })?;
510 let message_type =
511 elem.get_component(0)
512 .ok_or_else(|| EdifactError::MissingRequiredComponent {
513 tag: "UNH".to_owned(),
514 element_index: 1,
515 component_index: 0,
516 })?;
517 Ok(MessageIdentifier {
518 message_ref,
519 message_type,
520 version: elem.get_component(1).unwrap_or(""),
521 release: elem.get_component(2).unwrap_or(""),
522 controlling_agency: elem.get_component(3).unwrap_or(""),
523 association_code: elem.get_component(4).unwrap_or(""),
524 })
525}
526
527#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
545#[non_exhaustive]
546#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
547pub struct GroupIdentifier<'a> {
548 pub group_id: &'a str,
550 pub app_sender: &'a str,
552 pub app_sender_qualifier: &'a str,
554 pub app_recipient: &'a str,
556 pub app_recipient_qualifier: &'a str,
558 pub group_ref: &'a str,
560 pub controlling_agency: &'a str,
562 pub version: &'a str,
564 pub release: &'a str,
566}
567
568pub fn parse_ung<'a>(ung: &'a Segment<'a>) -> Result<GroupIdentifier<'a>, EdifactError> {
573 let group_id = ung
574 .get_element(0)
575 .and_then(|e| e.get_component(0))
576 .unwrap_or("");
577 let app_sender = ung
578 .get_element(1)
579 .and_then(|e| e.get_component(0))
580 .unwrap_or("");
581 let app_sender_qualifier = ung
582 .get_element(1)
583 .and_then(|e| e.get_component(1))
584 .unwrap_or("");
585 let app_recipient = ung
586 .get_element(2)
587 .and_then(|e| e.get_component(0))
588 .unwrap_or("");
589 let app_recipient_qualifier = ung
590 .get_element(2)
591 .and_then(|e| e.get_component(1))
592 .unwrap_or("");
593 let group_ref = ung
594 .get_element(4)
595 .and_then(|e| e.get_component(0))
596 .ok_or_else(|| EdifactError::MissingRequiredComponent {
597 tag: "UNG".to_owned(),
598 element_index: 4,
599 component_index: 0,
600 })?;
601 let controlling_agency = ung
602 .get_element(5)
603 .and_then(|e| e.get_component(0))
604 .unwrap_or("");
605 let s008 = ung.get_element(6);
606 let version = s008.as_ref().and_then(|e| e.get_component(0)).unwrap_or("");
607 let release = s008.as_ref().and_then(|e| e.get_component(1)).unwrap_or("");
608 Ok(GroupIdentifier {
609 group_id,
610 app_sender,
611 app_sender_qualifier,
612 app_recipient,
613 app_recipient_qualifier,
614 group_ref,
615 controlling_agency,
616 version,
617 release,
618 })
619}
620
621pub fn validate_envelope(segments: &[Segment<'_>]) -> Result<ValidatedInterchange, EdifactError> {
626 validate_envelope_impl(segments)
627}
628
629pub fn validate_envelope_from_owned(
631 segments: &[OwnedSegment],
632) -> Result<ValidatedInterchange, EdifactError> {
633 validate_envelope_impl(segments)
634}
635
636#[derive(Debug)]
649#[non_exhaustive]
650pub struct LenientResult {
651 pub interchange: Option<ValidatedInterchange>,
653 pub errors: Vec<EdifactError>,
655}
656
657impl LenientResult {
658 #[inline]
660 #[must_use]
661 pub fn is_valid(&self) -> bool {
662 self.errors.is_empty()
663 }
664
665 #[inline]
670 #[must_use]
671 pub fn has_errors(&self) -> bool {
672 !self.errors.is_empty()
673 }
674
675 pub fn into_strict(self) -> Result<ValidatedInterchange, Vec<EdifactError>> {
685 match self.interchange {
686 Some(interchange) if self.errors.is_empty() => Ok(interchange),
687 _ => Err(self.errors),
688 }
689 }
690}
691
692pub fn validate_envelope_lenient(segments: &[Segment<'_>]) -> LenientResult {
703 validate_envelope_lenient_impl(segments)
704}
705
706pub fn validate_envelope_lenient_from_owned(segments: &[OwnedSegment]) -> LenientResult {
710 validate_envelope_lenient_impl(segments)
711}
712
713#[derive(Default)]
730struct ErrorSink {
731 errors: Vec<EdifactError>,
732}
733
734impl ErrorSink {
735 #[inline]
736 fn push(&mut self, error: EdifactError) {
737 self.errors.push(error);
738 }
739
740 #[inline]
742 fn recover<T>(&mut self, result: Result<T, EdifactError>, fallback: T) -> T {
743 match result {
744 Ok(value) => value,
745 Err(error) => {
746 self.errors.push(error);
747 fallback
748 }
749 }
750 }
751
752 #[inline]
754 fn required<S: SegmentReader>(&mut self, seg: &S, element: usize, component: usize) -> String {
755 self.recover(
756 seg.required_component_field(element, component)
757 .map(str::to_owned),
758 String::new(),
759 )
760 }
761}
762
763fn validate_envelope_collecting<S: SegmentReader>(
768 segments: &[S],
769) -> (Option<ValidatedInterchange>, Vec<EdifactError>) {
770 let mut sink = ErrorSink::default();
771
772 let mut interchange_env = match extract_interchange(segments, &mut sink) {
773 Ok(env) => env,
774 Err(fatal) => {
775 sink.push(fatal);
776 return (None, sink.errors);
777 }
778 };
779
780 let inner = if segments.len() >= 2 {
781 &segments[1..segments.len() - 1]
782 } else {
783 &[]
784 };
785
786 let (functional_groups, messages) = match extract_content(inner, &mut sink) {
787 Ok(pair) => pair,
788 Err(fatal) => {
789 sink.push(fatal);
790 return (None, sink.errors);
791 }
792 };
793
794 let actual_unit_count = if functional_groups.is_empty() {
798 messages.len()
799 } else {
800 functional_groups.len()
801 };
802 interchange_env.actual_unit_count = sink.recover(
803 u32::try_from(actual_unit_count).map_err(|_| EdifactError::InterchangeTooLarge {
804 count: actual_unit_count as u64,
805 }),
806 u32::MAX,
807 );
808
809 if interchange_env.declared_unit_count != interchange_env.actual_unit_count {
810 sink.push(EdifactError::MessageCountMismatch {
811 expected: interchange_env.declared_unit_count,
812 actual: interchange_env.actual_unit_count,
813 });
814 }
815
816 for msg in &messages {
817 if msg.declared_segment_count != msg.actual_segment_count {
818 sink.push(EdifactError::SegmentCountMismatch {
819 expected: msg.declared_segment_count,
820 actual: msg.actual_segment_count,
821 message_ref: msg.message_ref.clone(),
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<S: SegmentReader>(
837 segments: &[S],
838) -> Result<ValidatedInterchange, EdifactError> {
839 match validate_envelope_collecting(segments) {
840 (Some(result), errors) if errors.is_empty() => Ok(result),
841 (_, mut errors) => Err(errors
842 .drain(..)
843 .next()
844 .unwrap_or(EdifactError::MissingSegment {
845 tag: "UNB".to_owned(),
846 expected_position: "first segment of interchange".to_owned(),
847 })),
848 }
849}
850
851fn validate_envelope_lenient_impl<S: SegmentReader>(segments: &[S]) -> LenientResult {
852 let (interchange, errors) = validate_envelope_collecting(segments);
853 LenientResult {
854 interchange,
855 errors,
856 }
857}
858
859fn extract_interchange<S: SegmentReader>(
862 segments: &[S],
863 sink: &mut ErrorSink,
864) -> Result<InterchangeEnvelope, EdifactError> {
865 if segments.first().map(|s| s.tag()) != Some("UNB") {
866 return Err(EdifactError::MissingSegment {
867 tag: "UNB".to_owned(),
868 expected_position: "first segment of interchange".to_owned(),
869 });
870 }
871 if segments.last().map(|s| s.tag()) != Some("UNZ") {
872 return Err(EdifactError::MissingSegment {
873 tag: "UNZ".to_owned(),
874 expected_position: "last segment of interchange".to_owned(),
875 });
876 }
877
878 let unb = &segments[0];
879 let unz = &segments[segments.len() - 1];
880
881 let syntax_identifier = sink.required(unb, 0, 0);
882 let syntax_version = unb.component(0, 1).unwrap_or("").to_owned();
883
884 const VALID_SYNTAX_IDS: &[&str] = &["UNOA", "UNOB", "UNOC", "UNOD", "UNOE", "UNOF", "KECA"];
886 if !VALID_SYNTAX_IDS.contains(&syntax_identifier.as_str()) {
887 sink.push(EdifactError::UnrecognisedSyntaxIdentifier(
888 syntax_identifier.clone(),
889 ));
890 }
891
892 let sender_id = sink.required(unb, 1, 0);
893 let sender_qualifier = unb.component(1, 1).unwrap_or("").to_owned();
894 let sender_routing_address = unb
896 .component(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(2, 1).unwrap_or("").to_owned();
902 let recipient_routing_address = unb
904 .component(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(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(5, 0)
921 .filter(|s| !s.is_empty())
922 .map(str::to_owned);
923 let recipient_password_qualifier = unb
924 .component(5, 1)
925 .filter(|s| !s.is_empty())
926 .map(str::to_owned);
927 let app_ref = unb
929 .component(6, 0)
930 .filter(|s| !s.is_empty())
931 .map(str::to_owned);
932 let processing_priority = unb
934 .component(7, 0)
935 .filter(|s| !s.is_empty())
936 .map(str::to_owned);
937 let acknowledgement_request = unb.component(8, 0) == Some("1");
939 let communications_agreement_id = unb
941 .component(9, 0)
942 .filter(|s| !s.is_empty())
943 .map(str::to_owned);
944 let test_indicator = unb.component(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<S: SegmentReader>(
994 inner: &[S],
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: Vec<String> = Vec::new();
1009 let messages = extract_messages_flat(inner, sink, &mut seen_refs)?;
1010 Ok((vec![], messages))
1011 }
1012}
1013
1014fn find_matching_une<S: SegmentReader>(
1016 segments: &[S],
1017 start: usize,
1018) -> Result<usize, EdifactError> {
1019 for (offset, seg) in segments[start..].iter().enumerate() {
1020 match seg.tag() {
1021 "UNE" => return Ok(start + offset),
1022 "UNG" => {
1023 return Err(EdifactError::InvalidSegmentForMessage {
1024 tag: "UNG".to_owned(),
1025 message_type: "ENVELOPE".to_owned(),
1026 span: seg.span(),
1027 });
1028 }
1029 _ => {}
1030 }
1031 }
1032 Err(EdifactError::MissingSegment {
1033 tag: "UNE".to_owned(),
1034 expected_position: "end of functional group".to_owned(),
1035 })
1036}
1037
1038fn extract_with_groups<S: SegmentReader>(
1039 inner: &[S],
1040 sink: &mut ErrorSink,
1041) -> Result<Vec<FunctionalGroupEnvelope>, EdifactError> {
1042 let mut groups: Vec<FunctionalGroupEnvelope> = Vec::new();
1043 let mut seen_group_refs: Vec<String> = Vec::new();
1046 let mut seen_message_refs: Vec<String> = Vec::new();
1047 let mut i = 0;
1048
1049 while i < inner.len() {
1050 let seg = &inner[i];
1051 match seg.tag() {
1052 "UNG" => {
1053 let ung_idx = i;
1054 let une_idx = find_matching_une(inner, ung_idx + 1)?;
1055
1056 let ung = &inner[ung_idx];
1057 let group_id = ung.component(0, 0).unwrap_or("").to_owned();
1058 let app_sender = ung.component(1, 0).unwrap_or("").to_owned();
1059 let app_sender_qualifier = ung.component(1, 1).unwrap_or("").to_owned();
1060 let app_recipient = ung.component(2, 0).unwrap_or("").to_owned();
1061 let app_recipient_qualifier = ung.component(2, 1).unwrap_or("").to_owned();
1062 let date = ung.component(3, 0).unwrap_or("").to_owned();
1063 let time_raw = ung.component(3, 1).unwrap_or("");
1064 let time = if time_raw.is_empty() {
1065 None
1066 } else {
1067 Some(time_raw.to_owned())
1068 };
1069 let group_ref = sink.required(ung, 4, 0);
1071 if seen_group_refs.contains(&group_ref) {
1072 sink.push(EdifactError::DuplicateReference {
1073 tag: "UNG".to_owned(),
1074 reference: group_ref.clone(),
1075 span: ung.span(),
1076 });
1077 } else {
1078 seen_group_refs.push(group_ref.clone());
1079 }
1080 let controlling_agency = ung.component(5, 0).unwrap_or("").to_owned();
1081 let version = ung.component(6, 0).unwrap_or("").to_owned();
1084 let release = ung.component(6, 1).unwrap_or("").to_owned();
1085
1086 let une = &inner[une_idx];
1087 let declared_str = sink.required(une, 0, 0);
1088 let declared_message_count: u32 = sink.recover(
1089 declared_str.parse().map_err(|_| EdifactError::InvalidText {
1090 offset: une.span().start,
1091 }),
1092 0,
1093 );
1094 let une_ref = sink.required(une, 1, 0);
1095 if une_ref != group_ref {
1096 sink.push(EdifactError::QualifierMismatch {
1097 tag: "UNE".to_owned(),
1098 actual: une_ref,
1099 expected: group_ref.clone(),
1100 span: une.span(),
1101 });
1102 }
1103
1104 let group_content = &inner[ung_idx + 1..une_idx];
1105 let messages = extract_messages_flat(group_content, sink, &mut seen_message_refs)?;
1106 let actual_message_count = sink.recover(
1107 u32::try_from(messages.len()).map_err(|_| EdifactError::InterchangeTooLarge {
1108 count: messages.len() as u64,
1109 }),
1110 u32::MAX,
1111 );
1112
1113 if declared_message_count != actual_message_count {
1114 sink.push(EdifactError::MessageCountMismatch {
1115 expected: declared_message_count,
1116 actual: actual_message_count,
1117 });
1118 }
1119
1120 groups.push(FunctionalGroupEnvelope {
1121 group_id,
1122 app_sender,
1123 app_sender_qualifier,
1124 app_recipient,
1125 app_recipient_qualifier,
1126 date,
1127 time,
1128 group_ref,
1129 controlling_agency,
1130 version,
1131 release,
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::InvalidSegmentForMessage {
1148 tag: "UNH".to_owned(),
1149 message_type: "ENVELOPE".to_owned(),
1150 span: seg.span(),
1151 });
1152 }
1153 _ => {
1154 return Err(EdifactError::InvalidSegmentForMessage {
1155 tag: seg.tag().to_owned(),
1156 message_type: "ENVELOPE".to_owned(),
1157 span: seg.span(),
1158 });
1159 }
1160 }
1161 }
1162
1163 Ok(groups)
1164}
1165
1166fn extract_messages_flat<S: SegmentReader>(
1168 segments: &[S],
1169 sink: &mut ErrorSink,
1170 seen_refs: &mut Vec<String>,
1171) -> Result<Vec<MessageEnvelope>, EdifactError> {
1172 let mut messages: Vec<MessageEnvelope> = Vec::new();
1173 let mut in_message = false;
1174 let mut msg_start_idx: usize = 0;
1175 let mut unh_idx: Option<usize> = None;
1176
1177 for (i, seg) in segments.iter().enumerate() {
1178 match seg.tag() {
1179 "UNH" => {
1180 if in_message {
1181 return Err(EdifactError::InvalidSegmentForMessage {
1182 tag: "UNH".to_owned(),
1183 message_type: "ENVELOPE".to_owned(),
1184 span: seg.span(),
1185 });
1186 }
1187 in_message = true;
1188 msg_start_idx = i;
1189 unh_idx = Some(i);
1190 }
1191 "UNT" if in_message => {
1192 let u_idx = unh_idx.take().unwrap();
1193 let unh = &segments[u_idx];
1194
1195 let message_ref = sink.required(unh, 0, 0);
1196 if seen_refs.contains(&message_ref) {
1197 sink.push(EdifactError::DuplicateReference {
1198 tag: "UNH".to_owned(),
1199 reference: message_ref.clone(),
1200 span: unh.span(),
1201 });
1202 } else {
1203 seen_refs.push(message_ref.clone());
1204 }
1205 let message_type = sink.required(unh, 1, 0);
1206 let version = sink.required(unh, 1, 1);
1207 let release = sink.required(unh, 1, 2);
1208 let controlling_agency = sink.required(unh, 1, 3);
1209 let association_code = unh.component(1, 4).unwrap_or("").to_owned();
1210 let common_access_ref = unh
1212 .component(2, 0)
1213 .filter(|s| !s.is_empty())
1214 .map(str::to_owned);
1215 let sequence_of_transfers = unh
1218 .component(3, 0)
1219 .filter(|s| !s.is_empty())
1220 .and_then(|s| s.parse::<u32>().ok());
1221 let transfer_position = unh
1222 .component(3, 1)
1223 .filter(|s| !s.is_empty())
1224 .map(str::to_owned);
1225
1226 let declared_raw = sink.required(seg, 0, 0);
1227 let declared_segment_count: u32 = sink.recover(
1228 declared_raw.parse().map_err(|_| EdifactError::InvalidText {
1229 offset: seg.span().start,
1230 }),
1231 0,
1232 );
1233 let unt_ref = sink.required(seg, 1, 0);
1234 if unt_ref != message_ref {
1235 sink.push(EdifactError::QualifierMismatch {
1236 tag: "UNT".to_owned(),
1237 actual: unt_ref,
1238 expected: message_ref.clone(),
1239 span: seg.span(),
1240 });
1241 }
1242
1243 let segment_span = i - msg_start_idx + 1;
1244 let actual_segment_count = sink.recover(
1245 u32::try_from(segment_span).map_err(|_| EdifactError::InterchangeTooLarge {
1246 count: segment_span as u64,
1247 }),
1248 u32::MAX,
1249 );
1250
1251 in_message = false;
1252 messages.push(MessageEnvelope {
1253 message_ref,
1254 message_type,
1255 version,
1256 release,
1257 controlling_agency,
1258 association_code,
1259 common_access_ref,
1260 sequence_of_transfers,
1261 transfer_position,
1262 declared_segment_count,
1263 actual_segment_count,
1264 });
1265 }
1266 "UNT" => {
1267 return Err(EdifactError::InvalidSegmentForMessage {
1268 tag: "UNT".to_owned(),
1269 message_type: "ENVELOPE".to_owned(),
1270 span: seg.span(),
1271 });
1272 }
1273 "UNB" | "UNZ" | "UNG" | "UNE" if in_message => {
1274 return Err(EdifactError::InvalidSegmentForMessage {
1275 tag: seg.tag().to_owned(),
1276 message_type: "ENVELOPE".to_owned(),
1277 span: seg.span(),
1278 });
1279 }
1280 _ if !in_message => {
1281 return Err(EdifactError::InvalidSegmentForMessage {
1282 tag: seg.tag().to_owned(),
1283 message_type: "ENVELOPE".to_owned(),
1284 span: seg.span(),
1285 });
1286 }
1287 _ => {}
1288 }
1289 }
1290
1291 if in_message {
1292 return Err(EdifactError::MissingSegment {
1293 tag: "UNT".to_owned(),
1294 expected_position: "end of message group".to_owned(),
1295 });
1296 }
1297
1298 Ok(messages)
1299}
1300
1301#[cfg(test)]
1304mod tests {
1305 use super::*;
1306
1307 fn parse(input: &[u8]) -> Vec<crate::OwnedSegment> {
1308 crate::from_reader_collect(std::io::Cursor::new(input)).expect("parse failed")
1309 }
1310
1311 fn parse_and_validate(input: &[u8]) -> Result<ValidatedInterchange, EdifactError> {
1312 let owned = parse(input);
1313 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1314 validate_envelope(&segs)
1315 }
1316
1317 fn parse_and_validate_lenient(input: &[u8]) -> LenientResult {
1318 let owned = parse(input);
1319 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1320 validate_envelope_lenient(&segs)
1321 }
1322
1323 #[test]
1324 fn lenient_collects_every_violation_not_just_the_first() {
1325 let input = b"UNB+UNOA:1+S+R+200101:0900+CTRL1'\
1329 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+99+1'\
1330 UNZ+1+CTRL2'";
1331 let result = parse_and_validate_lenient(input);
1332 assert!(
1333 result.errors.len() >= 2,
1334 "expected both violations, got {:?}",
1335 result.errors
1336 );
1337 assert!(
1338 result
1339 .errors
1340 .iter()
1341 .any(|e| matches!(e, EdifactError::QualifierMismatch { tag, .. } if tag == "UNZ")),
1342 "missing UNZ control-ref mismatch in {:?}",
1343 result.errors
1344 );
1345 assert!(
1346 result
1347 .errors
1348 .iter()
1349 .any(|e| matches!(e, EdifactError::SegmentCountMismatch { .. })),
1350 "missing UNT segment-count mismatch in {:?}",
1351 result.errors
1352 );
1353 }
1354
1355 #[test]
1356 fn strict_reports_the_first_error_lenient_collects() {
1357 for input in [
1360 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+2+1'UNZ+9+C1'"[..],
1361 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+99+1'UNZ+1+C1'"[..],
1362 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+2+9'UNZ+1+C1'"[..],
1363 ] {
1364 let strict = parse_and_validate(input);
1365 let lenient = parse_and_validate_lenient(input);
1366 match strict {
1367 Err(e) => assert_eq!(
1368 Some(&e),
1369 lenient.errors.first(),
1370 "strict/lenient diverged for {:?}",
1371 std::str::from_utf8(input).unwrap()
1372 ),
1373 Ok(_) => assert!(lenient.errors.is_empty()),
1374 }
1375 }
1376 }
1377
1378 #[test]
1379 fn duplicate_message_references_are_rejected() {
1380 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'\
1381 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+3+1'\
1382 UNH+1+ORDERS:D:96A:UN'BGM+221'UNT+3+1'\
1383 UNZ+2+C1'";
1384 let err = parse_and_validate(input).expect_err("duplicate UNH refs must fail");
1385 assert!(
1386 matches!(&err, EdifactError::DuplicateReference { tag, reference, .. }
1387 if tag == "UNH" && reference == "1"),
1388 "got {err:?}"
1389 );
1390 }
1391
1392 #[test]
1393 fn distinct_message_references_are_accepted() {
1394 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'\
1395 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+3+1'\
1396 UNH+2+ORDERS:D:96A:UN'BGM+221'UNT+3+2'\
1397 UNZ+2+C1'";
1398 parse_and_validate(input).expect("distinct refs must validate");
1399 }
1400
1401 #[test]
1402 fn into_strict_is_total_after_the_caller_filters_errors() {
1403 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'UNZ+1+C2'";
1406 let mut lenient = parse_and_validate_lenient(input);
1407 lenient.errors.clear();
1408 let _ = lenient.into_strict();
1410 }
1411
1412 fn parse_and_validate_owned(input: &[u8]) -> Result<ValidatedInterchange, EdifactError> {
1413 validate_envelope_from_owned(&parse(input))
1414 }
1415
1416 const VALID_INTERCHANGE: &[u8] =
1417 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'";
1418
1419 #[test]
1420 fn valid_envelope_parses_ok() {
1421 let result = parse_and_validate(VALID_INTERCHANGE).expect("envelope should be valid");
1422 assert_eq!(result.interchange.sender_id, "SENDER");
1423 assert_eq!(result.interchange.sender_qualifier, ""); assert_eq!(result.interchange.recipient_id, "RECEIVER");
1425 assert_eq!(result.interchange.recipient_qualifier, "");
1426 assert_eq!(result.interchange.syntax_identifier, "UNOA");
1427 assert_eq!(result.interchange.syntax_version, "3");
1428 assert_eq!(result.interchange.control_ref, "00001");
1429 assert_eq!(result.interchange.declared_unit_count, 1);
1430 assert_eq!(result.interchange.actual_unit_count, 1);
1431 assert!(!result.interchange.is_test());
1432 assert!(!result.has_functional_groups());
1433 assert_eq!(result.message_count(), 1);
1434 assert_eq!(result.messages[0].message_type, "ORDERS");
1435 assert_eq!(result.messages[0].association_code, "EAN010");
1436 assert_eq!(result.messages[0].declared_segment_count, 4);
1437 assert_eq!(result.messages[0].actual_segment_count, 4);
1438 }
1439
1440 #[test]
1441 fn valid_envelope_parses_ok_owned_path() {
1442 let result = parse_and_validate_owned(VALID_INTERCHANGE).expect("envelope should be valid");
1443 assert_eq!(result.interchange.sender_id, "SENDER");
1444 assert_eq!(result.interchange.actual_unit_count, 1);
1445 assert_eq!(result.messages[0].declared_segment_count, 4);
1446 }
1447
1448 #[test]
1449 fn unt_count_mismatch_returns_err() {
1450 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'";
1451 let result = parse_and_validate(input);
1452 assert!(
1453 matches!(
1454 result,
1455 Err(EdifactError::SegmentCountMismatch { expected: 99, .. })
1456 ),
1457 "expected SegmentCountMismatch, got {result:?}"
1458 );
1459 }
1460
1461 #[test]
1462 fn unz_count_mismatch_returns_err() {
1463 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'";
1464 let result = parse_and_validate(input);
1465 assert!(
1466 matches!(
1467 result,
1468 Err(EdifactError::MessageCountMismatch {
1469 expected: 2,
1470 actual: 1
1471 })
1472 ),
1473 "expected MessageCountMismatch(2,1), got {result:?}"
1474 );
1475 }
1476
1477 #[test]
1478 fn missing_unb_returns_err() {
1479 let input = b"UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
1480 assert!(parse_and_validate(input).is_err());
1481 }
1482
1483 #[test]
1484 fn extracts_una_interchange_correctly() {
1485 let result = parse_and_validate(VALID_INTERCHANGE).unwrap();
1486 assert_eq!(result.interchange.syntax_identifier, "UNOA");
1487 assert_eq!(result.interchange.syntax_version, "3");
1488 assert_eq!(result.interchange.date, "230401");
1489 assert_eq!(result.interchange.time.as_deref(), Some("0900"));
1490 }
1491
1492 #[test]
1493 fn sender_and_recipient_qualifiers_extracted() {
1494 let input = b"UNB+UNOA:3+1234567890123:14+9876543210987:14+200101:0900+1'\
1496 UNH+1+ORDERS:D:96A:UN'\
1497 BGM+220+PO-001+9'\
1498 UNT+3+1'\
1499 UNZ+1+1'";
1500 let r = parse_and_validate(input).expect("GLN-qualified UNB must parse ok");
1501 assert_eq!(r.interchange.sender_id, "1234567890123");
1502 assert_eq!(r.interchange.sender_qualifier, "14");
1503 assert_eq!(r.interchange.recipient_id, "9876543210987");
1504 assert_eq!(r.interchange.recipient_qualifier, "14");
1505 }
1506
1507 #[test]
1511 fn test_indicator_parsed_from_unb() {
1512 let input = b"UNB+UNOA:3+S+R+200101:0900+1++++++1'\
1516 UNH+1+ORDERS:D:96A:UN'\
1517 BGM+220+PO-001+9'\
1518 UNT+3+1'\
1519 UNZ+1+1'";
1520 let r = parse_and_validate(input).expect("test-flagged UNB must parse ok");
1521 assert!(
1522 r.interchange.test_indicator,
1523 "test_indicator should be true"
1524 );
1525 assert!(r.interchange.is_test(), "is_test() convenience must agree");
1526 }
1527
1528 #[test]
1529 fn no_test_indicator_defaults_false() {
1530 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1531 assert!(!r.interchange.test_indicator);
1532 assert!(!r.interchange.is_test());
1533 }
1534
1535 #[test]
1536 fn app_ref_extracted_when_present() {
1537 let input = b"UNB+UNOA:3+S+R+200101:0900+1++MYAPP'\
1539 UNH+1+ORDERS:D:96A:UN'\
1540 BGM+220+PO-001+9'\
1541 UNT+3+1'\
1542 UNZ+1+1'";
1543 let r = parse_and_validate(input).expect("UNB with app_ref must parse ok");
1544 assert_eq!(r.interchange.app_ref.as_deref(), Some("MYAPP"));
1545 }
1546
1547 #[test]
1548 fn app_ref_is_none_when_absent() {
1549 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1550 assert!(r.interchange.app_ref.is_none());
1551 }
1552
1553 #[test]
1554 fn recipient_password_extracted_when_present() {
1555 let input = b"UNB+UNOA:3+S+R+200101:0900+1+MYPASS'\
1557 UNH+1+ORDERS:D:96A:UN'\
1558 BGM+220+PO-001+9'\
1559 UNT+3+1'\
1560 UNZ+1+1'";
1561 let r = parse_and_validate(input).expect("UNB with password must parse ok");
1562 assert_eq!(r.interchange.recipient_password.as_deref(), Some("MYPASS"));
1563 }
1564
1565 #[test]
1566 fn recipient_password_is_none_when_absent() {
1567 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1568 assert!(r.interchange.recipient_password.is_none());
1569 }
1570
1571 #[test]
1572 fn acknowledgement_request_flag_parsed() {
1573 let input = b"UNB+UNOA:3+S+R+200101:0900+1++++1'\
1576 UNH+1+ORDERS:D:96A:UN'\
1577 BGM+220+PO-001+9'\
1578 UNT+3+1'\
1579 UNZ+1+1'";
1580 let r = parse_and_validate(input).expect("UNB with ack-request must parse ok");
1581 assert!(r.interchange.acknowledgement_request);
1582 assert!(r.interchange.ack_requested());
1583 }
1584
1585 #[test]
1586 fn acknowledgement_request_defaults_false() {
1587 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1588 assert!(!r.interchange.acknowledgement_request);
1589 assert!(!r.interchange.ack_requested());
1590 }
1591
1592 #[test]
1593 fn communications_agreement_id_extracted() {
1594 let input = b"UNB+UNOA:3+S+R+200101:0900+1+++++EANCOM'\
1596 UNH+1+ORDERS:D:96A:UN'\
1597 BGM+220+PO-001+9'\
1598 UNT+3+1'\
1599 UNZ+1+1'";
1600 let r = parse_and_validate(input).expect("UNB with comms-agreement must parse ok");
1601 assert_eq!(
1602 r.interchange.communications_agreement_id.as_deref(),
1603 Some("EANCOM")
1604 );
1605 }
1606
1607 #[test]
1608 fn dangling_unh_without_unt_returns_err() {
1609 let input =
1610 b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNZ+1+1'";
1611 let result = parse_and_validate(input);
1612 assert!(
1613 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNT")
1614 );
1615 }
1616
1617 #[test]
1618 fn stray_segment_outside_message_returns_err() {
1619 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'";
1620 assert!(matches!(
1621 parse_and_validate(input),
1622 Err(EdifactError::InvalidSegmentForMessage { .. })
1623 ));
1624 }
1625
1626 #[test]
1627 fn missing_unb_sender_component_returns_err() {
1628 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'";
1629 let result = parse_and_validate(input);
1630 assert!(
1631 matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 0 }) if tag == "UNB"),
1632 "expected MissingRequiredComponent for empty sender, got: {result:?}"
1633 );
1634 }
1635
1636 #[test]
1637 fn nested_unh_without_closing_previous_message_returns_err() {
1638 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'";
1639 let result = parse_and_validate(input);
1640 assert!(
1641 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNH"),
1642 "expected InvalidSegmentForMessage(UNH), got {result:?}"
1643 );
1644 }
1645
1646 #[test]
1647 fn unt_message_reference_must_match_unh() {
1648 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'";
1649 let result = parse_and_validate(input);
1650 assert!(
1651 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNT")
1652 );
1653 }
1654
1655 #[test]
1656 fn unz_control_reference_must_match_unb() {
1657 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'";
1658 let result = parse_and_validate(input);
1659 assert!(
1660 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNZ")
1661 );
1662 }
1663
1664 #[test]
1665 fn missing_unh_message_type_components_return_err() {
1666 let input =
1667 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'";
1668 let result = parse_and_validate(input);
1669 assert!(
1670 matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 3 }) if tag == "UNH"),
1671 "got: {result:?}"
1672 );
1673 }
1674
1675 #[test]
1676 fn nested_unz_inside_message_returns_err() {
1677 let input =
1678 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'";
1679 let result = parse_and_validate(input);
1680 assert!(
1681 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNZ")
1682 );
1683 }
1684
1685 #[test]
1686 fn lenient_returns_partial_result_on_count_mismatch() {
1687 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'";
1690 let owned = parse(input);
1691 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1692 let lenient = validate_envelope_lenient(&segs);
1693 let result = lenient.interchange;
1694 let errors = lenient.errors;
1695 assert!(
1696 result.is_some(),
1697 "lenient mode must return Some even on count mismatch"
1698 );
1699 assert_eq!(errors.len(), 1);
1700 assert!(
1701 matches!(
1702 &errors[0],
1703 EdifactError::MessageCountMismatch {
1704 expected: 2,
1705 actual: 1
1706 }
1707 ),
1708 "expected MessageCountMismatch(2,1), got {:?}",
1709 errors[0]
1710 );
1711 let partial = result.unwrap();
1712 assert_eq!(partial.messages.len(), 1);
1713 assert_eq!(partial.interchange.actual_unit_count, 1);
1714 assert_eq!(partial.interchange.declared_unit_count, 2);
1715 }
1716
1717 #[test]
1718 fn lenient_returns_none_on_structural_error() {
1719 let input = b"UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
1721 let owned = parse(input);
1722 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1723 let lenient = validate_envelope_lenient(&segs);
1724 let result = lenient.interchange;
1725 let errors = lenient.errors;
1726 assert!(result.is_none(), "missing UNB must yield None");
1727 assert!(!errors.is_empty());
1728 }
1729
1730 #[test]
1731 fn message_count_convenience_method() {
1732 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1733 assert_eq!(r.message_count(), r.messages.len());
1734 assert_eq!(r.message_count(), 1);
1735 }
1736
1737 #[test]
1738 fn interchange_with_single_functional_group_parses_ok() {
1739 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1740 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1741 UNH+1+ORDERS:D:96A:UN'\
1742 BGM+220+PO-001+9'\
1743 UNT+3+1'\
1744 UNE+1+1'\
1745 UNZ+1+1'";
1746 let result = parse_and_validate(input).expect("single-group interchange must parse ok");
1747 assert!(result.has_functional_groups());
1748 assert_eq!(result.functional_groups.len(), 1);
1749 let g = &result.functional_groups[0];
1750 assert_eq!(g.group_id, "ORDERS");
1751 assert_eq!(g.group_ref, "1");
1752 assert_eq!(g.controlling_agency, "UN");
1753 assert_eq!(g.declared_message_count, 1);
1754 assert_eq!(g.actual_message_count, 1);
1755 assert_eq!(result.messages.len(), 1);
1756 assert_eq!(result.messages[0].message_type, "ORDERS");
1757 assert_eq!(result.interchange.declared_unit_count, 1);
1758 assert_eq!(result.interchange.actual_unit_count, 1);
1759 }
1760
1761 #[test]
1762 fn interchange_with_multi_message_group_parses_ok() {
1763 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1766 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1767 UNH+1+ORDERS:D:96A:UN'\
1768 BGM+220+PO-001+9'\
1769 UNT+3+1'\
1770 UNH+2+ORDERS:D:96A:UN'\
1771 BGM+220+PO-002+9'\
1772 UNT+3+2'\
1773 UNE+2+1'\
1774 UNZ+1+1'";
1775 let result = parse_and_validate(input).expect("multi-message group must parse ok");
1776 assert!(result.has_functional_groups());
1777 assert_eq!(result.functional_groups.len(), 1);
1778 assert_eq!(result.functional_groups[0].actual_message_count, 2);
1779 assert_eq!(result.messages.len(), 2);
1780 assert_eq!(result.interchange.actual_unit_count, 1);
1782 assert_eq!(result.interchange.declared_unit_count, 1);
1783 }
1784
1785 #[test]
1786 fn interchange_with_multiple_groups_parses_ok() {
1787 let input = b"UNB+UNOA:3+S+R+200101:0900+2'\
1788 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1789 UNH+1+ORDERS:D:96A:UN'\
1790 BGM+220+PO-001+9'\
1791 UNT+3+1'\
1792 UNE+1+1'\
1793 UNG+INVOIC+S+R+200101:0900+2+UN+D:96A'\
1794 UNH+2+INVOIC:D:96A:UN'\
1795 BGM+380+INV-001+9'\
1796 UNT+3+2'\
1797 UNE+1+2'\
1798 UNZ+2+2'";
1799 let result = parse_and_validate(input).expect("multi-group interchange must parse ok");
1800 assert_eq!(result.functional_groups.len(), 2);
1801 assert_eq!(result.functional_groups[0].group_id, "ORDERS");
1802 assert_eq!(result.functional_groups[1].group_id, "INVOIC");
1803 assert_eq!(result.messages.len(), 2);
1804 assert_eq!(result.interchange.declared_unit_count, 2);
1805 assert_eq!(result.interchange.actual_unit_count, 2);
1806 }
1807
1808 #[test]
1809 fn ung_une_count_mismatch_returns_err() {
1810 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1811 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1812 UNH+1+ORDERS:D:96A:UN'\
1813 BGM+220+PO-001+9'\
1814 UNT+3+1'\
1815 UNE+2+1'\
1816 UNZ+1+1'";
1817 let result = parse_and_validate(input);
1818 assert!(
1819 matches!(
1820 result,
1821 Err(EdifactError::MessageCountMismatch {
1822 expected: 2,
1823 actual: 1
1824 })
1825 ),
1826 "expected MessageCountMismatch(2,1), got {result:?}"
1827 );
1828 }
1829
1830 #[test]
1831 fn une_without_ung_returns_err() {
1832 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1833 UNE+1+1'\
1834 UNH+1+ORDERS:D:96A:UN'\
1835 BGM+220+PO-001+9'\
1836 UNT+3+1'\
1837 UNZ+1+1'";
1838 let result = parse_and_validate(input);
1839 assert!(
1840 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNE"),
1841 "expected InvalidSegmentForMessage(UNE), got {result:?}"
1842 );
1843 }
1844
1845 #[test]
1846 fn ung_without_une_returns_err() {
1847 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1848 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1849 UNH+1+ORDERS:D:96A:UN'\
1850 BGM+220+PO-001+9'\
1851 UNT+3+1'\
1852 UNZ+1+1'";
1853 let result = parse_and_validate(input);
1854 assert!(
1855 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNE"),
1856 "expected MissingSegment(UNE), got {result:?}"
1857 );
1858 }
1859
1860 #[test]
1861 fn une_group_ref_must_match_ung() {
1862 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1863 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1864 UNH+1+ORDERS:D:96A:UN'\
1865 BGM+220+PO-001+9'\
1866 UNT+3+1'\
1867 UNE+1+999'\
1868 UNZ+1+1'";
1869 let result = parse_and_validate(input);
1870 assert!(
1871 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNE"),
1872 "expected QualifierMismatch(UNE), got {result:?}"
1873 );
1874 }
1875
1876 #[test]
1879 fn processing_priority_extracted_when_present() {
1880 let input = b"UNB+UNOA:3+S+R+200101:0900+1+++A'\
1882 UNH+1+ORDERS:D:96A:UN'\
1883 BGM+220+PO-001+9'\
1884 UNT+3+1'\
1885 UNZ+1+1'";
1886 let r = parse_and_validate(input).expect("UNB with processing_priority must parse ok");
1887 assert_eq!(r.interchange.processing_priority.as_deref(), Some("A"));
1888 }
1889
1890 #[test]
1891 fn processing_priority_is_none_when_absent() {
1892 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1893 assert!(r.interchange.processing_priority.is_none());
1894 }
1895
1896 #[test]
1897 fn sender_routing_address_extracted_when_present() {
1898 let input = b"UNB+UNOA:3+SENDER:14:ROUTEA+RECIP+200101:0900+1'\
1900 UNH+1+ORDERS:D:96A:UN'\
1901 BGM+220+PO-001+9'\
1902 UNT+3+1'\
1903 UNZ+1+1'";
1904 let r = parse_and_validate(input).expect("UNB with sender routing must parse ok");
1905 assert_eq!(
1906 r.interchange.sender_routing_address.as_deref(),
1907 Some("ROUTEA")
1908 );
1909 assert!(r.interchange.recipient_routing_address.is_none());
1910 }
1911
1912 #[test]
1913 fn recipient_routing_address_extracted_when_present() {
1914 let input = b"UNB+UNOA:3+SENDER+RECIP:14:ROUTEB+200101:0900+1'\
1916 UNH+1+ORDERS:D:96A:UN'\
1917 BGM+220+PO-001+9'\
1918 UNT+3+1'\
1919 UNZ+1+1'";
1920 let r = parse_and_validate(input).expect("UNB with recipient routing must parse ok");
1921 assert!(r.interchange.sender_routing_address.is_none());
1922 assert_eq!(
1923 r.interchange.recipient_routing_address.as_deref(),
1924 Some("ROUTEB")
1925 );
1926 }
1927
1928 #[test]
1929 fn routing_address_is_none_when_absent() {
1930 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1932 UNH+1+ORDERS:D:96A:UN'\
1933 BGM+220+PO-001+9'\
1934 UNT+3+1'\
1935 UNZ+1+1'";
1936 let r = parse_and_validate(input).unwrap();
1937 assert!(r.interchange.sender_routing_address.is_none());
1938 assert!(r.interchange.recipient_routing_address.is_none());
1939 }
1940
1941 #[test]
1942 fn common_access_ref_extracted_when_present() {
1943 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1945 UNH+1+ORDERS:D:96A:UN+COMREF1'\
1946 BGM+220+PO-001+9'\
1947 UNT+3+1'\
1948 UNZ+1+1'";
1949 let r = parse_and_validate(input).expect("UNH with common_access_ref must parse ok");
1950 assert_eq!(r.messages[0].common_access_ref.as_deref(), Some("COMREF1"));
1951 }
1952
1953 #[test]
1954 fn common_access_ref_is_none_when_absent() {
1955 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1956 assert!(r.messages[0].common_access_ref.is_none());
1957 }
1958
1959 #[test]
1960 fn parse_unh_includes_message_ref() {
1961 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1962 UNH+REF42+ORDERS:D:96A:UN'\
1963 BGM+220+PO-001+9'\
1964 UNT+3+REF42'\
1965 UNZ+1+1'";
1966 let r = parse_and_validate(input).expect("must parse ok");
1969 assert_eq!(r.messages[0].message_ref, "REF42");
1970 assert_eq!(r.messages[0].message_type, "ORDERS");
1971 }
1972
1973 #[test]
1974 fn iter_messages_by_type_returns_matching_messages() {
1975 let input = b"UNB+UNOA:3+S+R+200101:0900+CTRL2'\
1976 UNH+1+ORDERS:D:96A:UN'\
1977 BGM+220+PO-001+9'\
1978 UNT+3+1'\
1979 UNH+2+INVOIC:D:96A:UN'\
1980 BGM+380+INV-001+9'\
1981 UNT+3+2'\
1982 UNZ+2+CTRL2'";
1983 let r = parse_and_validate(input).expect("two-message interchange must parse ok");
1984 let orders: Vec<_> = r.iter_messages_by_type("ORDERS").collect();
1985 assert_eq!(orders.len(), 1);
1986 assert_eq!(orders[0].message_ref, "1");
1987 let invoices: Vec<_> = r.iter_messages_by_type("INVOIC").collect();
1988 assert_eq!(invoices.len(), 1);
1989 assert_eq!(invoices[0].message_ref, "2");
1990 let none: Vec<_> = r.iter_messages_by_type("DESADV").collect();
1991 assert!(none.is_empty());
1992 }
1993
1994 #[test]
1995 fn display_interchange_envelope() {
1996 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1997 let s = r.interchange.to_string();
1998 assert!(s.contains("SENDER"), "Display must include sender_id");
1999 assert!(s.contains("UNOA"), "Display must include syntax_identifier");
2000 }
2001
2002 #[test]
2003 fn display_message_envelope() {
2004 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2005 let s = r.messages[0].to_string();
2006 assert!(s.contains("ORDERS"), "Display must include message_type");
2007 assert!(s.contains("ref="), "Display must include message ref label");
2008 }
2009
2010 #[test]
2011 fn display_validated_interchange() {
2012 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2013 let s = r.to_string();
2014 assert!(
2015 s.contains("messages="),
2016 "Display must include message count label"
2017 );
2018 }
2019
2020 #[test]
2023 fn unrecognised_syntax_identifier_returns_err() {
2024 let input = b"UNB+XXXX:3+S+R+200101:0900+1'\
2026 UNH+1+ORDERS:D:96A:UN'\
2027 BGM+220+PO-001+9'\
2028 UNT+3+1'\
2029 UNZ+1+1'";
2030 let result = parse_and_validate(input);
2031 assert!(
2032 matches!(result, Err(EdifactError::UnrecognisedSyntaxIdentifier(ref id)) if id == "XXXX"),
2033 "expected UnrecognisedSyntaxIdentifier(\"XXXX\"), got {result:?}"
2034 );
2035 }
2036
2037 #[test]
2038 fn all_valid_syntax_identifiers_accepted() {
2039 for id in &["UNOA", "UNOB", "UNOC", "UNOD", "UNOE", "UNOF", "KECA"] {
2040 let input = format!(
2041 "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'"
2042 );
2043 let result = parse_and_validate(input.as_bytes());
2044 assert!(
2045 result.is_ok(),
2046 "syntax id '{id}' should be accepted, got {result:?}"
2047 );
2048 }
2049 }
2050
2051 #[test]
2054 fn recipient_password_qualifier_extracted_when_present() {
2055 let input = b"UNB+UNOA:3+S+R+200101:0900+1+MYPASS:AA'\
2057 UNH+1+ORDERS:D:96A:UN'\
2058 BGM+220+PO-001+9'\
2059 UNT+3+1'\
2060 UNZ+1+1'";
2061 let r = parse_and_validate(input).expect("UNB with password+qualifier must parse ok");
2062 assert_eq!(r.interchange.recipient_password.as_deref(), Some("MYPASS"));
2063 assert_eq!(
2064 r.interchange.recipient_password_qualifier.as_deref(),
2065 Some("AA")
2066 );
2067 }
2068
2069 #[test]
2070 fn recipient_password_qualifier_is_none_when_absent() {
2071 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2072 assert!(r.interchange.recipient_password_qualifier.is_none());
2073 }
2074
2075 #[test]
2078 fn sequence_of_transfers_extracted_when_present() {
2079 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2081 UNH+1+ORDERS:D:96A:UN++2:C'\
2082 BGM+220+PO-001+9'\
2083 UNT+3+1'\
2084 UNZ+1+1'";
2085 let r = parse_and_validate(input).expect("UNH with S010 must parse ok");
2086 assert_eq!(r.messages[0].sequence_of_transfers, Some(2));
2087 assert_eq!(r.messages[0].transfer_position.as_deref(), Some("C"));
2088 }
2089
2090 #[test]
2091 fn sequence_of_transfers_none_when_absent() {
2092 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2093 assert!(r.messages[0].sequence_of_transfers.is_none());
2094 assert!(r.messages[0].transfer_position.is_none());
2095 }
2096
2097 #[test]
2100 fn ung_app_sender_and_recipient_qualifiers_extracted() {
2101 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2103 UNG+ORDERS+APPSEND:ZZZ+APPRECV:14+200101:0900+1+UN+D:96A'\
2104 UNH+1+ORDERS:D:96A:UN'\
2105 BGM+220+PO-001+9'\
2106 UNT+3+1'\
2107 UNE+1+1'\
2108 UNZ+1+1'";
2109 let r = parse_and_validate(input).expect("UNG with qualifiers must parse ok");
2110 let g = &r.functional_groups[0];
2111 assert_eq!(g.app_sender, "APPSEND");
2112 assert_eq!(g.app_sender_qualifier, "ZZZ");
2113 assert_eq!(g.app_recipient, "APPRECV");
2114 assert_eq!(g.app_recipient_qualifier, "14");
2115 }
2116
2117 #[test]
2118 fn ung_qualifiers_empty_when_absent() {
2119 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2120 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
2121 UNH+1+ORDERS:D:96A:UN'\
2122 BGM+220+PO-001+9'\
2123 UNT+3+1'\
2124 UNE+1+1'\
2125 UNZ+1+1'";
2126 let r = parse_and_validate(input).unwrap();
2127 let g = &r.functional_groups[0];
2128 assert_eq!(g.app_sender_qualifier, "");
2129 assert_eq!(g.app_recipient_qualifier, "");
2130 }
2131
2132 #[test]
2135 fn lenient_result_is_valid_true_on_clean_interchange() {
2136 let owned = parse(VALID_INTERCHANGE);
2137 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2138 let r = validate_envelope_lenient(&segs);
2139 assert!(r.is_valid());
2140 assert!(r.errors.is_empty());
2141 assert!(r.interchange.is_some());
2142 }
2143
2144 #[test]
2145 fn lenient_result_into_strict_ok_path() {
2146 let owned = parse(VALID_INTERCHANGE);
2147 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2148 let r = validate_envelope_lenient(&segs);
2149 let strict = r.into_strict();
2150 assert!(
2151 strict.is_ok(),
2152 "into_strict() should succeed for valid interchange"
2153 );
2154 assert_eq!(strict.unwrap().messages.len(), 1);
2155 }
2156
2157 #[test]
2158 fn lenient_result_into_strict_err_path() {
2159 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'";
2161 let owned = parse(input);
2162 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2163 let r = validate_envelope_lenient(&segs);
2164 assert!(!r.is_valid());
2165 let strict = r.into_strict();
2166 assert!(strict.is_err());
2167 let errors = strict.unwrap_err();
2168 assert_eq!(errors.len(), 1);
2169 assert!(matches!(
2170 &errors[0],
2171 EdifactError::MessageCountMismatch {
2172 expected: 2,
2173 actual: 1
2174 }
2175 ));
2176 }
2177
2178 #[test]
2181 fn parse_unh_direct_extracts_all_s009_fields() {
2182 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2187 UNH+REF99+ORDERS:D:96A:UN:EAN010'\
2188 BGM+220+PO-001+9'\
2189 UNT+3+REF99'\
2190 UNZ+1+1'";
2191 let r = parse_and_validate(input).expect("must parse ok");
2192 let msg = &r.messages[0];
2193 assert_eq!(msg.message_ref, "REF99");
2194 assert_eq!(msg.message_type, "ORDERS");
2195 assert_eq!(msg.version, "D");
2196 assert_eq!(msg.release, "96A");
2197 assert_eq!(msg.controlling_agency, "UN");
2198 assert_eq!(msg.association_code, "EAN010");
2199 }
2200
2201 #[test]
2202 fn parse_ung_direct_extracts_identifier_fields() {
2203 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2205 UNG+ORDERS+APPSEND:ZZZ+APPRECV:14+200101:0900+GRP01+UN+D:96A'\
2206 UNH+1+ORDERS:D:96A:UN'\
2207 BGM+220+PO-001+9'\
2208 UNT+3+1'\
2209 UNE+1+GRP01'\
2210 UNZ+1+1'";
2211 let r = parse_and_validate(input).expect("must parse ok");
2212 let g = &r.functional_groups[0];
2213 assert_eq!(g.group_id, "ORDERS");
2214 assert_eq!(g.app_sender, "APPSEND");
2215 assert_eq!(g.app_sender_qualifier, "ZZZ");
2216 assert_eq!(g.app_recipient, "APPRECV");
2217 assert_eq!(g.app_recipient_qualifier, "14");
2218 assert_eq!(g.group_ref, "GRP01");
2219 assert_eq!(g.controlling_agency, "UN");
2220 assert_eq!(g.version, "D");
2221 assert_eq!(g.release, "96A");
2222 }
2223
2224 #[test]
2227 fn find_message_returns_correct_message_by_ref() {
2228 let input = b"UNB+UNOA:3+S+R+200101:0900+CTRL2'\
2229 UNH+REF-A+ORDERS:D:96A:UN'\
2230 BGM+220+PO-001+9'\
2231 UNT+3+REF-A'\
2232 UNH+REF-B+INVOIC:D:96A:UN'\
2233 BGM+380+INV-001+9'\
2234 UNT+3+REF-B'\
2235 UNZ+2+CTRL2'";
2236 let r = parse_and_validate(input).expect("two-message interchange must parse ok");
2237 let msg_a = r.find_message("REF-A");
2238 assert!(msg_a.is_some());
2239 assert_eq!(msg_a.unwrap().message_type, "ORDERS");
2240 let msg_b = r.find_message("REF-B");
2241 assert!(msg_b.is_some());
2242 assert_eq!(msg_b.unwrap().message_type, "INVOIC");
2243 assert!(r.find_message("MISSING").is_none());
2244 }
2245
2246 #[test]
2249 fn ung_missing_group_ref_returns_err() {
2250 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2252 UNG+ORDERS+S+R+200101:0900++UN+D:96A'\
2253 UNH+1+ORDERS:D:96A:UN'\
2254 BGM+220+PO-001+9'\
2255 UNT+3+1'\
2256 UNE+1+'\
2257 UNZ+1+1'";
2258 let result = parse_and_validate(input);
2259 assert!(
2260 matches!(result,
2261 Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 4, component_index: 0 })
2262 if tag == "UNG"
2263 ),
2264 "expected MissingRequiredComponent for empty UNG group_ref, got {result:?}"
2265 );
2266 }
2267
2268 #[test]
2271 fn empty_segment_list_returns_missing_unb() {
2272 let result = validate_envelope(&[]);
2275 assert!(
2276 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNB"),
2277 "expected MissingSegment(UNB) for empty input, got {result:?}"
2278 );
2279 }
2280
2281 #[test]
2282 fn single_segment_only_unb_returns_missing_unz() {
2283 let input = b"UNB+UNOA:3+S+R+200101:0900+1'";
2285 let owned = parse(input);
2286 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2287 let result = validate_envelope(&segs);
2288 assert!(
2289 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNZ"),
2290 "expected MissingSegment(UNZ) for UNB-only input, got {result:?}"
2291 );
2292 }
2293
2294 #[test]
2295 fn display_validated_interchange_includes_count() {
2296 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2297 let s = r.to_string();
2298 assert!(
2300 s.contains("messages=1"),
2301 "Display must include count '1': {s}"
2302 );
2303 }
2304
2305 #[test]
2306 fn display_interchange_envelope_contains_arrow() {
2307 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2308 let s = r.interchange.to_string();
2309 assert!(s.contains("->"), "Display must use ASCII '->' arrow: {s}");
2311 assert!(
2312 !s.contains('\u{2192}'),
2313 "Display must not use Unicode → arrow: {s}"
2314 );
2315 }
2316
2317 #[test]
2318 fn display_functional_group_envelope() {
2319 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2320 UNG+ORDERS+APPSEND+APPRECV+200101:0900+GRP01+UN+D:96A'\
2321 UNH+1+ORDERS:D:96A:UN'\
2322 BGM+220+PO-001+9'\
2323 UNT+3+1'\
2324 UNE+1+GRP01'\
2325 UNZ+1+1'";
2326 let r = parse_and_validate(input).expect("must parse ok");
2327 let g = &r.functional_groups[0];
2328 let s = g.to_string();
2329 assert!(s.contains("ORDERS"), "Display must include group_id: {s}");
2330 assert!(
2331 s.contains("APPSEND"),
2332 "Display must include app_sender: {s}"
2333 );
2334 assert!(
2335 s.contains("APPRECV"),
2336 "Display must include app_recipient: {s}"
2337 );
2338 assert!(s.contains("GRP01"), "Display must include group_ref: {s}");
2339 assert!(
2340 s.contains("msgs="),
2341 "Display must include message count label: {s}"
2342 );
2343 assert!(
2344 s.contains("1/1"),
2345 "Display must include actual/declared counts: {s}"
2346 );
2347 }
2348
2349 #[test]
2350 fn lenient_has_errors_is_inverse_of_is_valid() {
2351 let owned = parse(VALID_INTERCHANGE);
2352 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2353 let valid = validate_envelope_lenient(&segs);
2354 assert!(valid.is_valid());
2355 assert!(!valid.has_errors());
2356
2357 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'";
2359 let owned2 = parse(input);
2360 let segs2: Vec<Segment<'_>> = owned2
2361 .iter()
2362 .map(crate::OwnedSegment::as_borrowed)
2363 .collect();
2364 let invalid = validate_envelope_lenient(&segs2);
2365 assert!(!invalid.is_valid());
2366 assert!(invalid.has_errors());
2367 }
2368}