1use crate::{OwnedSegment, error::EdifactError, model::Segment};
21
22pub(crate) trait SegmentReader: sealed::Sealed {
25 fn tag(&self) -> &str;
26 fn span_start(&self) -> usize;
27 fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str>;
28
29 fn required_component_field(
30 &self,
31 elem_idx: usize,
32 comp_idx: usize,
33 ) -> Result<&str, EdifactError> {
34 self.component(elem_idx, comp_idx)
35 .filter(|s| !s.is_empty())
36 .ok_or_else(|| EdifactError::MissingRequiredComponent {
37 tag: self.tag().to_owned(),
38 element_index: elem_idx,
39 component_index: comp_idx,
40 })
41 }
42}
43
44mod sealed {
45 pub trait Sealed {}
46 impl Sealed for crate::model::Segment<'_> {}
47 impl Sealed for crate::OwnedSegment {}
48}
49
50impl SegmentReader for Segment<'_> {
51 #[inline]
52 fn tag(&self) -> &str {
53 self.tag
54 }
55 #[inline]
56 fn span_start(&self) -> usize {
57 self.span.start
58 }
59 #[inline]
60 fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str> {
61 self.get_element(elem_idx)?.get_component(comp_idx)
62 }
63}
64
65impl SegmentReader for OwnedSegment {
66 #[inline]
67 fn tag(&self) -> &str {
68 &self.tag
69 }
70 #[inline]
71 fn span_start(&self) -> usize {
72 self.span.start
73 }
74 #[inline]
75 fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str> {
76 self.component_str(elem_idx, comp_idx)
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
104#[non_exhaustive]
105#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
106pub struct InterchangeEnvelope {
107 pub syntax_identifier: String,
109 pub syntax_version: String,
113 pub sender_id: String,
115 pub sender_qualifier: String,
120 pub sender_routing_address: Option<String>,
125 pub recipient_id: String,
127 pub recipient_qualifier: String,
131 pub recipient_routing_address: Option<String>,
135 pub date: String,
137 pub time: Option<String>,
141 pub control_ref: String,
143 pub recipient_password: Option<String>,
148 pub recipient_password_qualifier: Option<String>,
153 pub app_ref: Option<String>,
157 pub processing_priority: Option<String>,
162 pub acknowledgement_request: bool,
167 pub communications_agreement_id: Option<String>,
171 pub test_indicator: bool,
177 pub declared_unit_count: u32,
185 pub actual_unit_count: u32,
187}
188
189impl InterchangeEnvelope {
190 #[inline]
212 #[must_use]
213 pub fn is_test(&self) -> bool {
214 self.test_indicator
215 }
216
217 #[inline]
221 #[must_use]
222 pub fn ack_requested(&self) -> bool {
223 self.acknowledgement_request
224 }
225}
226
227impl std::fmt::Display for InterchangeEnvelope {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 write!(
230 f,
231 "{sender} -> {recipient} [{ctrl}] ({syntax}:{ver})",
232 sender = self.sender_id,
233 recipient = self.recipient_id,
234 ctrl = self.control_ref,
235 syntax = self.syntax_identifier,
236 ver = self.syntax_version,
237 )
238 }
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Hash)]
243#[non_exhaustive]
244#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
245pub struct MessageEnvelope {
246 pub message_ref: String,
248 pub message_type: String,
250 pub version: String,
252 pub release: String,
254 pub controlling_agency: String,
256 pub association_code: String,
258 pub common_access_ref: Option<String>,
265 pub sequence_of_transfers: Option<u32>,
271 pub transfer_position: Option<String>,
276 pub declared_segment_count: u32,
278 pub actual_segment_count: u32,
280}
281
282impl std::fmt::Display for MessageEnvelope {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 write!(
285 f,
286 "{msg_type}:{ver}:{rel} ref={msg_ref} seg={actual}/{declared}",
287 msg_type = self.message_type,
288 ver = self.version,
289 rel = self.release,
290 msg_ref = self.message_ref,
291 actual = self.actual_segment_count,
292 declared = self.declared_segment_count,
293 )
294 }
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, Hash)]
303#[non_exhaustive]
304#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
305pub struct FunctionalGroupEnvelope {
306 pub group_id: String,
308 pub app_sender: String,
310 pub app_sender_qualifier: String,
314 pub app_recipient: String,
316 pub app_recipient_qualifier: String,
320 pub date: String,
322 pub time: Option<String>,
324 pub group_ref: String,
326 pub controlling_agency: String,
328 pub version: String,
330 pub release: String,
332 pub declared_message_count: u32,
334 pub actual_message_count: u32,
336 pub messages: Vec<MessageEnvelope>,
338}
339
340#[derive(Debug, Clone, PartialEq, Eq, Hash)]
346#[non_exhaustive]
347#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
348pub struct ValidatedInterchange {
349 pub interchange: InterchangeEnvelope,
351 pub functional_groups: Vec<FunctionalGroupEnvelope>,
356 pub messages: Vec<MessageEnvelope>,
361}
362
363impl ValidatedInterchange {
364 #[inline]
366 #[must_use]
367 pub fn has_functional_groups(&self) -> bool {
368 !self.functional_groups.is_empty()
369 }
370
371 #[inline]
375 #[must_use]
376 pub fn message_count(&self) -> usize {
377 self.messages.len()
378 }
379
380 #[inline]
385 pub fn iter_messages(&self) -> impl Iterator<Item = &MessageEnvelope> {
386 self.messages.iter()
387 }
388
389 #[inline]
396 #[must_use]
397 pub fn find_message(&self, reference: &str) -> Option<&MessageEnvelope> {
398 self.messages.iter().find(|m| m.message_ref == reference)
399 }
400
401 #[must_use]
410 pub fn messages_by_type(&self, message_type: &str) -> Vec<&MessageEnvelope> {
411 self.messages
412 .iter()
413 .filter(|m| m.message_type == message_type)
414 .collect()
415 }
416
417 #[inline]
428 pub fn iter_messages_by_type<'s, 'q: 's>(
429 &'s self,
430 message_type: &'q str,
431 ) -> impl Iterator<Item = &'s MessageEnvelope> + 's {
432 self.messages
433 .iter()
434 .filter(move |m| m.message_type == message_type)
435 }
436}
437
438impl std::fmt::Display for ValidatedInterchange {
439 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
440 write!(
441 f,
442 "{ic} messages={n}",
443 ic = self.interchange,
444 n = self.messages.len(),
445 )
446 }
447}
448
449impl std::fmt::Display for FunctionalGroupEnvelope {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 write!(
452 f,
453 "{gid} sender={sender} recipient={recip} [{gref}] ({agency}) msgs={actual}/{declared}",
454 gid = self.group_id,
455 sender = self.app_sender,
456 recip = self.app_recipient,
457 gref = self.group_ref,
458 agency = self.controlling_agency,
459 actual = self.actual_message_count,
460 declared = self.declared_message_count,
461 )
462 }
463}
464
465#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
470#[non_exhaustive]
471#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
472pub struct MessageIdentifier<'a> {
473 pub message_ref: &'a str,
475 pub message_type: &'a str,
476 pub version: &'a str,
477 pub release: &'a str,
478 pub controlling_agency: &'a str,
479 pub association_code: &'a str,
483}
484
485pub fn parse_unh<'a>(unh: &'a Segment<'a>) -> Result<MessageIdentifier<'a>, EdifactError> {
489 let message_ref = unh
491 .get_element(0)
492 .and_then(|e| e.get_component(0))
493 .filter(|s| !s.is_empty())
494 .ok_or_else(|| EdifactError::MissingRequiredComponent {
495 tag: "UNH".to_owned(),
496 element_index: 0,
497 component_index: 0,
498 })?;
499 let elem = unh
501 .get_element(1)
502 .ok_or_else(|| EdifactError::MissingRequiredElement {
503 tag: "UNH".to_owned(),
504 element_index: 1,
505 })?;
506 let message_type =
507 elem.get_component(0)
508 .ok_or_else(|| EdifactError::MissingRequiredComponent {
509 tag: "UNH".to_owned(),
510 element_index: 1,
511 component_index: 0,
512 })?;
513 Ok(MessageIdentifier {
514 message_ref,
515 message_type,
516 version: elem.get_component(1).unwrap_or(""),
517 release: elem.get_component(2).unwrap_or(""),
518 controlling_agency: elem.get_component(3).unwrap_or(""),
519 association_code: elem.get_component(4).unwrap_or(""),
520 })
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
541#[non_exhaustive]
542#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
543pub struct GroupIdentifier<'a> {
544 pub group_id: &'a str,
546 pub app_sender: &'a str,
548 pub app_sender_qualifier: &'a str,
550 pub app_recipient: &'a str,
552 pub app_recipient_qualifier: &'a str,
554 pub group_ref: &'a str,
556 pub controlling_agency: &'a str,
558 pub version: &'a str,
560 pub release: &'a str,
562}
563
564pub fn parse_ung<'a>(ung: &'a Segment<'a>) -> Result<GroupIdentifier<'a>, EdifactError> {
569 let group_id = ung
570 .get_element(0)
571 .and_then(|e| e.get_component(0))
572 .unwrap_or("");
573 let app_sender = ung
574 .get_element(1)
575 .and_then(|e| e.get_component(0))
576 .unwrap_or("");
577 let app_sender_qualifier = ung
578 .get_element(1)
579 .and_then(|e| e.get_component(1))
580 .unwrap_or("");
581 let app_recipient = ung
582 .get_element(2)
583 .and_then(|e| e.get_component(0))
584 .unwrap_or("");
585 let app_recipient_qualifier = ung
586 .get_element(2)
587 .and_then(|e| e.get_component(1))
588 .unwrap_or("");
589 let group_ref = ung
590 .get_element(4)
591 .and_then(|e| e.get_component(0))
592 .ok_or_else(|| EdifactError::MissingRequiredComponent {
593 tag: "UNG".to_owned(),
594 element_index: 4,
595 component_index: 0,
596 })?;
597 let controlling_agency = ung
598 .get_element(5)
599 .and_then(|e| e.get_component(0))
600 .unwrap_or("");
601 let s008 = ung.get_element(6);
602 let version = s008.as_ref().and_then(|e| e.get_component(0)).unwrap_or("");
603 let release = s008.as_ref().and_then(|e| e.get_component(1)).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 })
615}
616
617pub fn validate_envelope(segments: &[Segment<'_>]) -> Result<ValidatedInterchange, EdifactError> {
622 validate_envelope_impl(segments)
623}
624
625pub fn validate_envelope_from_owned(
627 segments: &[OwnedSegment],
628) -> Result<ValidatedInterchange, EdifactError> {
629 validate_envelope_impl(segments)
630}
631
632#[derive(Debug)]
645#[non_exhaustive]
646pub struct LenientResult {
647 pub interchange: Option<ValidatedInterchange>,
649 pub errors: Vec<EdifactError>,
651}
652
653impl LenientResult {
654 #[inline]
656 #[must_use]
657 pub fn is_valid(&self) -> bool {
658 self.errors.is_empty()
659 }
660
661 #[inline]
666 #[must_use]
667 pub fn has_errors(&self) -> bool {
668 !self.errors.is_empty()
669 }
670
671 pub fn into_strict(self) -> Result<ValidatedInterchange, Vec<EdifactError>> {
681 match self.interchange {
682 Some(interchange) if self.errors.is_empty() => Ok(interchange),
683 _ => Err(self.errors),
684 }
685 }
686}
687
688pub fn validate_envelope_lenient(segments: &[Segment<'_>]) -> LenientResult {
699 validate_envelope_lenient_impl(segments)
700}
701
702pub fn validate_envelope_lenient_from_owned(segments: &[OwnedSegment]) -> LenientResult {
706 validate_envelope_lenient_impl(segments)
707}
708
709#[derive(Default)]
726struct ErrorSink {
727 errors: Vec<EdifactError>,
728}
729
730impl ErrorSink {
731 #[inline]
732 fn push(&mut self, error: EdifactError) {
733 self.errors.push(error);
734 }
735
736 #[inline]
738 fn recover<T>(&mut self, result: Result<T, EdifactError>, fallback: T) -> T {
739 match result {
740 Ok(value) => value,
741 Err(error) => {
742 self.errors.push(error);
743 fallback
744 }
745 }
746 }
747
748 #[inline]
750 fn required<S: SegmentReader>(&mut self, seg: &S, element: usize, component: usize) -> String {
751 self.recover(
752 seg.required_component_field(element, component)
753 .map(str::to_owned),
754 String::new(),
755 )
756 }
757}
758
759fn validate_envelope_collecting<S: SegmentReader>(
764 segments: &[S],
765) -> (Option<ValidatedInterchange>, Vec<EdifactError>) {
766 let mut sink = ErrorSink::default();
767
768 let mut interchange_env = match extract_interchange(segments, &mut sink) {
769 Ok(env) => env,
770 Err(fatal) => {
771 sink.push(fatal);
772 return (None, sink.errors);
773 }
774 };
775
776 let inner = if segments.len() >= 2 {
777 &segments[1..segments.len() - 1]
778 } else {
779 &[]
780 };
781
782 let (functional_groups, messages) = match extract_content(inner, &mut sink) {
783 Ok(pair) => pair,
784 Err(fatal) => {
785 sink.push(fatal);
786 return (None, sink.errors);
787 }
788 };
789
790 let actual_unit_count = if functional_groups.is_empty() {
794 messages.len()
795 } else {
796 functional_groups.len()
797 };
798 interchange_env.actual_unit_count = sink.recover(
799 u32::try_from(actual_unit_count).map_err(|_| EdifactError::InterchangeTooLarge {
800 count: actual_unit_count as u64,
801 }),
802 u32::MAX,
803 );
804
805 if interchange_env.declared_unit_count != interchange_env.actual_unit_count {
806 sink.push(EdifactError::MessageCountMismatch {
807 expected: interchange_env.declared_unit_count,
808 actual: interchange_env.actual_unit_count,
809 });
810 }
811
812 for msg in &messages {
813 if msg.declared_segment_count != msg.actual_segment_count {
814 sink.push(EdifactError::SegmentCountMismatch {
815 expected: msg.declared_segment_count,
816 actual: msg.actual_segment_count,
817 message_ref: msg.message_ref.clone(),
818 });
819 }
820 }
821
822 (
823 Some(ValidatedInterchange {
824 interchange: interchange_env,
825 functional_groups,
826 messages,
827 }),
828 sink.errors,
829 )
830}
831
832fn validate_envelope_impl<S: SegmentReader>(
833 segments: &[S],
834) -> Result<ValidatedInterchange, EdifactError> {
835 match validate_envelope_collecting(segments) {
836 (Some(result), errors) if errors.is_empty() => Ok(result),
837 (_, mut errors) => Err(errors
838 .drain(..)
839 .next()
840 .unwrap_or(EdifactError::MissingSegment {
841 tag: "UNB".to_owned(),
842 expected_position: "first segment of interchange".to_owned(),
843 })),
844 }
845}
846
847fn validate_envelope_lenient_impl<S: SegmentReader>(segments: &[S]) -> LenientResult {
848 let (interchange, errors) = validate_envelope_collecting(segments);
849 LenientResult {
850 interchange,
851 errors,
852 }
853}
854
855fn extract_interchange<S: SegmentReader>(
858 segments: &[S],
859 sink: &mut ErrorSink,
860) -> Result<InterchangeEnvelope, EdifactError> {
861 if segments.first().map(|s| s.tag()) != Some("UNB") {
862 return Err(EdifactError::MissingSegment {
863 tag: "UNB".to_owned(),
864 expected_position: "first segment of interchange".to_owned(),
865 });
866 }
867 if segments.last().map(|s| s.tag()) != Some("UNZ") {
868 return Err(EdifactError::MissingSegment {
869 tag: "UNZ".to_owned(),
870 expected_position: "last segment of interchange".to_owned(),
871 });
872 }
873
874 let unb = &segments[0];
875 let unz = &segments[segments.len() - 1];
876
877 let syntax_identifier = sink.required(unb, 0, 0);
878 let syntax_version = unb.component(0, 1).unwrap_or("").to_owned();
879
880 const VALID_SYNTAX_IDS: &[&str] = &["UNOA", "UNOB", "UNOC", "UNOD", "UNOE", "UNOF", "KECA"];
882 if !VALID_SYNTAX_IDS.contains(&syntax_identifier.as_str()) {
883 sink.push(EdifactError::UnrecognisedSyntaxIdentifier(
884 syntax_identifier.clone(),
885 ));
886 }
887
888 let sender_id = sink.required(unb, 1, 0);
889 let sender_qualifier = unb.component(1, 1).unwrap_or("").to_owned();
890 let sender_routing_address = unb
892 .component(1, 2)
893 .filter(|s| !s.is_empty())
894 .map(str::to_owned);
895
896 let recipient_id = sink.required(unb, 2, 0);
897 let recipient_qualifier = unb.component(2, 1).unwrap_or("").to_owned();
898 let recipient_routing_address = unb
900 .component(2, 2)
901 .filter(|s| !s.is_empty())
902 .map(str::to_owned);
903
904 let date = sink.required(unb, 3, 0);
905 let time_raw = unb.component(3, 1).unwrap_or("");
906 let time = if time_raw.is_empty() {
907 None
908 } else {
909 Some(time_raw.to_owned())
910 };
911
912 let control_ref = sink.required(unb, 4, 0);
913
914 let recipient_password = unb
916 .component(5, 0)
917 .filter(|s| !s.is_empty())
918 .map(str::to_owned);
919 let recipient_password_qualifier = unb
920 .component(5, 1)
921 .filter(|s| !s.is_empty())
922 .map(str::to_owned);
923 let app_ref = unb
925 .component(6, 0)
926 .filter(|s| !s.is_empty())
927 .map(str::to_owned);
928 let processing_priority = unb
930 .component(7, 0)
931 .filter(|s| !s.is_empty())
932 .map(str::to_owned);
933 let acknowledgement_request = unb.component(8, 0) == Some("1");
935 let communications_agreement_id = unb
937 .component(9, 0)
938 .filter(|s| !s.is_empty())
939 .map(str::to_owned);
940 let test_indicator = unb.component(10, 0) == Some("1");
942
943 let unz_control_ref = sink.required(unz, 1, 0);
944 if unz_control_ref != control_ref {
945 sink.push(EdifactError::QualifierMismatch {
946 tag: "UNZ".to_owned(),
947 actual: unz_control_ref,
948 expected: control_ref.clone(),
949 offset: unz.span_start(),
950 });
951 }
952
953 let declared_unit_count_raw = sink.required(unz, 0, 0);
954 let declared_unit_count: u32 = sink.recover(
955 declared_unit_count_raw
956 .parse()
957 .map_err(|_| EdifactError::InvalidText {
958 offset: unz.span_start(),
959 }),
960 0,
961 );
962
963 Ok(InterchangeEnvelope {
964 syntax_identifier,
965 syntax_version,
966 sender_id,
967 sender_qualifier,
968 sender_routing_address,
969 recipient_id,
970 recipient_qualifier,
971 recipient_routing_address,
972 date,
973 time,
974 control_ref,
975 recipient_password,
976 recipient_password_qualifier,
977 app_ref,
978 processing_priority,
979 acknowledgement_request,
980 communications_agreement_id,
981 test_indicator,
982 declared_unit_count,
983 actual_unit_count: 0,
984 })
985}
986
987fn extract_content<S: SegmentReader>(
990 inner: &[S],
991 sink: &mut ErrorSink,
992) -> Result<(Vec<FunctionalGroupEnvelope>, Vec<MessageEnvelope>), EdifactError> {
993 if inner.first().is_some_and(|s| s.tag() == "UNG") {
997 let groups = extract_with_groups(inner, sink)?;
998 let messages = groups
999 .iter()
1000 .flat_map(|g| g.messages.iter().cloned())
1001 .collect();
1002 Ok((groups, messages))
1003 } else {
1004 let mut seen_refs: Vec<String> = Vec::new();
1005 let messages = extract_messages_flat(inner, sink, &mut seen_refs)?;
1006 Ok((vec![], messages))
1007 }
1008}
1009
1010fn find_matching_une<S: SegmentReader>(
1012 segments: &[S],
1013 start: usize,
1014) -> Result<usize, EdifactError> {
1015 for (offset, seg) in segments[start..].iter().enumerate() {
1016 match seg.tag() {
1017 "UNE" => return Ok(start + offset),
1018 "UNG" => {
1019 return Err(EdifactError::InvalidSegmentForMessage {
1020 tag: "UNG".to_owned(),
1021 message_type: "ENVELOPE".to_owned(),
1022 offset: seg.span_start(),
1023 });
1024 }
1025 _ => {}
1026 }
1027 }
1028 Err(EdifactError::MissingSegment {
1029 tag: "UNE".to_owned(),
1030 expected_position: "end of functional group".to_owned(),
1031 })
1032}
1033
1034fn extract_with_groups<S: SegmentReader>(
1035 inner: &[S],
1036 sink: &mut ErrorSink,
1037) -> Result<Vec<FunctionalGroupEnvelope>, EdifactError> {
1038 let mut groups: Vec<FunctionalGroupEnvelope> = Vec::new();
1039 let mut seen_group_refs: Vec<String> = Vec::new();
1042 let mut seen_message_refs: Vec<String> = Vec::new();
1043 let mut i = 0;
1044
1045 while i < inner.len() {
1046 let seg = &inner[i];
1047 match seg.tag() {
1048 "UNG" => {
1049 let ung_idx = i;
1050 let une_idx = find_matching_une(inner, ung_idx + 1)?;
1051
1052 let ung = &inner[ung_idx];
1053 let group_id = ung.component(0, 0).unwrap_or("").to_owned();
1054 let app_sender = ung.component(1, 0).unwrap_or("").to_owned();
1055 let app_sender_qualifier = ung.component(1, 1).unwrap_or("").to_owned();
1056 let app_recipient = ung.component(2, 0).unwrap_or("").to_owned();
1057 let app_recipient_qualifier = ung.component(2, 1).unwrap_or("").to_owned();
1058 let date = ung.component(3, 0).unwrap_or("").to_owned();
1059 let time_raw = ung.component(3, 1).unwrap_or("");
1060 let time = if time_raw.is_empty() {
1061 None
1062 } else {
1063 Some(time_raw.to_owned())
1064 };
1065 let group_ref = sink.required(ung, 4, 0);
1067 if seen_group_refs.contains(&group_ref) {
1068 sink.push(EdifactError::DuplicateReference {
1069 tag: "UNG".to_owned(),
1070 reference: group_ref.clone(),
1071 offset: ung.span_start(),
1072 });
1073 } else {
1074 seen_group_refs.push(group_ref.clone());
1075 }
1076 let controlling_agency = ung.component(5, 0).unwrap_or("").to_owned();
1077 let version = ung.component(6, 0).unwrap_or("").to_owned();
1080 let release = ung.component(6, 1).unwrap_or("").to_owned();
1081
1082 let une = &inner[une_idx];
1083 let declared_str = sink.required(une, 0, 0);
1084 let declared_message_count: u32 = sink.recover(
1085 declared_str.parse().map_err(|_| EdifactError::InvalidText {
1086 offset: une.span_start(),
1087 }),
1088 0,
1089 );
1090 let une_ref = sink.required(une, 1, 0);
1091 if une_ref != group_ref {
1092 sink.push(EdifactError::QualifierMismatch {
1093 tag: "UNE".to_owned(),
1094 actual: une_ref,
1095 expected: group_ref.clone(),
1096 offset: une.span_start(),
1097 });
1098 }
1099
1100 let group_content = &inner[ung_idx + 1..une_idx];
1101 let messages = extract_messages_flat(group_content, sink, &mut seen_message_refs)?;
1102 let actual_message_count = sink.recover(
1103 u32::try_from(messages.len()).map_err(|_| EdifactError::InterchangeTooLarge {
1104 count: messages.len() as u64,
1105 }),
1106 u32::MAX,
1107 );
1108
1109 if declared_message_count != actual_message_count {
1110 sink.push(EdifactError::MessageCountMismatch {
1111 expected: declared_message_count,
1112 actual: actual_message_count,
1113 });
1114 }
1115
1116 groups.push(FunctionalGroupEnvelope {
1117 group_id,
1118 app_sender,
1119 app_sender_qualifier,
1120 app_recipient,
1121 app_recipient_qualifier,
1122 date,
1123 time,
1124 group_ref,
1125 controlling_agency,
1126 version,
1127 release,
1128 declared_message_count,
1129 actual_message_count,
1130 messages,
1131 });
1132 i = une_idx + 1;
1133 }
1134 "UNE" => {
1135 return Err(EdifactError::InvalidSegmentForMessage {
1136 tag: "UNE".to_owned(),
1137 message_type: "ENVELOPE".to_owned(),
1138 offset: seg.span_start(),
1139 });
1140 }
1141 "UNH" => {
1142 return Err(EdifactError::InvalidSegmentForMessage {
1144 tag: "UNH".to_owned(),
1145 message_type: "ENVELOPE".to_owned(),
1146 offset: seg.span_start(),
1147 });
1148 }
1149 _ => {
1150 return Err(EdifactError::InvalidSegmentForMessage {
1151 tag: seg.tag().to_owned(),
1152 message_type: "ENVELOPE".to_owned(),
1153 offset: seg.span_start(),
1154 });
1155 }
1156 }
1157 }
1158
1159 Ok(groups)
1160}
1161
1162fn extract_messages_flat<S: SegmentReader>(
1164 segments: &[S],
1165 sink: &mut ErrorSink,
1166 seen_refs: &mut Vec<String>,
1167) -> Result<Vec<MessageEnvelope>, EdifactError> {
1168 let mut messages: Vec<MessageEnvelope> = Vec::new();
1169 let mut in_message = false;
1170 let mut msg_start_idx: usize = 0;
1171 let mut unh_idx: Option<usize> = None;
1172
1173 for (i, seg) in segments.iter().enumerate() {
1174 match seg.tag() {
1175 "UNH" => {
1176 if in_message {
1177 return Err(EdifactError::InvalidSegmentForMessage {
1178 tag: "UNH".to_owned(),
1179 message_type: "ENVELOPE".to_owned(),
1180 offset: seg.span_start(),
1181 });
1182 }
1183 in_message = true;
1184 msg_start_idx = i;
1185 unh_idx = Some(i);
1186 }
1187 "UNT" if in_message => {
1188 let u_idx = unh_idx.take().unwrap();
1189 let unh = &segments[u_idx];
1190
1191 let message_ref = sink.required(unh, 0, 0);
1192 if seen_refs.contains(&message_ref) {
1193 sink.push(EdifactError::DuplicateReference {
1194 tag: "UNH".to_owned(),
1195 reference: message_ref.clone(),
1196 offset: unh.span_start(),
1197 });
1198 } else {
1199 seen_refs.push(message_ref.clone());
1200 }
1201 let message_type = sink.required(unh, 1, 0);
1202 let version = sink.required(unh, 1, 1);
1203 let release = sink.required(unh, 1, 2);
1204 let controlling_agency = sink.required(unh, 1, 3);
1205 let association_code = unh.component(1, 4).unwrap_or("").to_owned();
1206 let common_access_ref = unh
1208 .component(2, 0)
1209 .filter(|s| !s.is_empty())
1210 .map(str::to_owned);
1211 let sequence_of_transfers = unh
1214 .component(3, 0)
1215 .filter(|s| !s.is_empty())
1216 .and_then(|s| s.parse::<u32>().ok());
1217 let transfer_position = unh
1218 .component(3, 1)
1219 .filter(|s| !s.is_empty())
1220 .map(str::to_owned);
1221
1222 let declared_raw = sink.required(seg, 0, 0);
1223 let declared_segment_count: u32 = sink.recover(
1224 declared_raw.parse().map_err(|_| EdifactError::InvalidText {
1225 offset: seg.span_start(),
1226 }),
1227 0,
1228 );
1229 let unt_ref = sink.required(seg, 1, 0);
1230 if unt_ref != message_ref {
1231 sink.push(EdifactError::QualifierMismatch {
1232 tag: "UNT".to_owned(),
1233 actual: unt_ref,
1234 expected: message_ref.clone(),
1235 offset: seg.span_start(),
1236 });
1237 }
1238
1239 let segment_span = i - msg_start_idx + 1;
1240 let actual_segment_count = sink.recover(
1241 u32::try_from(segment_span).map_err(|_| EdifactError::InterchangeTooLarge {
1242 count: segment_span as u64,
1243 }),
1244 u32::MAX,
1245 );
1246
1247 in_message = false;
1248 messages.push(MessageEnvelope {
1249 message_ref,
1250 message_type,
1251 version,
1252 release,
1253 controlling_agency,
1254 association_code,
1255 common_access_ref,
1256 sequence_of_transfers,
1257 transfer_position,
1258 declared_segment_count,
1259 actual_segment_count,
1260 });
1261 }
1262 "UNT" => {
1263 return Err(EdifactError::InvalidSegmentForMessage {
1264 tag: "UNT".to_owned(),
1265 message_type: "ENVELOPE".to_owned(),
1266 offset: seg.span_start(),
1267 });
1268 }
1269 "UNB" | "UNZ" | "UNG" | "UNE" if in_message => {
1270 return Err(EdifactError::InvalidSegmentForMessage {
1271 tag: seg.tag().to_owned(),
1272 message_type: "ENVELOPE".to_owned(),
1273 offset: seg.span_start(),
1274 });
1275 }
1276 _ if !in_message => {
1277 return Err(EdifactError::InvalidSegmentForMessage {
1278 tag: seg.tag().to_owned(),
1279 message_type: "ENVELOPE".to_owned(),
1280 offset: seg.span_start(),
1281 });
1282 }
1283 _ => {}
1284 }
1285 }
1286
1287 if in_message {
1288 return Err(EdifactError::MissingSegment {
1289 tag: "UNT".to_owned(),
1290 expected_position: "end of message group".to_owned(),
1291 });
1292 }
1293
1294 Ok(messages)
1295}
1296
1297#[cfg(test)]
1300mod tests {
1301 use super::*;
1302
1303 fn parse(input: &[u8]) -> Vec<crate::OwnedSegment> {
1304 crate::from_reader_collect(std::io::Cursor::new(input)).expect("parse failed")
1305 }
1306
1307 fn parse_and_validate(input: &[u8]) -> Result<ValidatedInterchange, EdifactError> {
1308 let owned = parse(input);
1309 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1310 validate_envelope(&segs)
1311 }
1312
1313 fn parse_and_validate_lenient(input: &[u8]) -> LenientResult {
1314 let owned = parse(input);
1315 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1316 validate_envelope_lenient(&segs)
1317 }
1318
1319 #[test]
1320 fn lenient_collects_every_violation_not_just_the_first() {
1321 let input = b"UNB+UNOA:1+S+R+200101:0900+CTRL1'\
1325 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+99+1'\
1326 UNZ+1+CTRL2'";
1327 let result = parse_and_validate_lenient(input);
1328 assert!(
1329 result.errors.len() >= 2,
1330 "expected both violations, got {:?}",
1331 result.errors
1332 );
1333 assert!(
1334 result
1335 .errors
1336 .iter()
1337 .any(|e| matches!(e, EdifactError::QualifierMismatch { tag, .. } if tag == "UNZ")),
1338 "missing UNZ control-ref mismatch in {:?}",
1339 result.errors
1340 );
1341 assert!(
1342 result
1343 .errors
1344 .iter()
1345 .any(|e| matches!(e, EdifactError::SegmentCountMismatch { .. })),
1346 "missing UNT segment-count mismatch in {:?}",
1347 result.errors
1348 );
1349 }
1350
1351 #[test]
1352 fn strict_reports_the_first_error_lenient_collects() {
1353 for input in [
1356 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+2+1'UNZ+9+C1'"[..],
1357 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+99+1'UNZ+1+C1'"[..],
1358 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+2+9'UNZ+1+C1'"[..],
1359 ] {
1360 let strict = parse_and_validate(input);
1361 let lenient = parse_and_validate_lenient(input);
1362 match strict {
1363 Err(e) => assert_eq!(
1364 Some(&e),
1365 lenient.errors.first(),
1366 "strict/lenient diverged for {:?}",
1367 std::str::from_utf8(input).unwrap()
1368 ),
1369 Ok(_) => assert!(lenient.errors.is_empty()),
1370 }
1371 }
1372 }
1373
1374 #[test]
1375 fn duplicate_message_references_are_rejected() {
1376 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'\
1377 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+3+1'\
1378 UNH+1+ORDERS:D:96A:UN'BGM+221'UNT+3+1'\
1379 UNZ+2+C1'";
1380 let err = parse_and_validate(input).expect_err("duplicate UNH refs must fail");
1381 assert!(
1382 matches!(&err, EdifactError::DuplicateReference { tag, reference, .. }
1383 if tag == "UNH" && reference == "1"),
1384 "got {err:?}"
1385 );
1386 }
1387
1388 #[test]
1389 fn distinct_message_references_are_accepted() {
1390 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'\
1391 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+3+1'\
1392 UNH+2+ORDERS:D:96A:UN'BGM+221'UNT+3+2'\
1393 UNZ+2+C1'";
1394 parse_and_validate(input).expect("distinct refs must validate");
1395 }
1396
1397 #[test]
1398 fn into_strict_is_total_after_the_caller_filters_errors() {
1399 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'UNZ+1+C2'";
1402 let mut lenient = parse_and_validate_lenient(input);
1403 lenient.errors.clear();
1404 let _ = lenient.into_strict();
1406 }
1407
1408 fn parse_and_validate_owned(input: &[u8]) -> Result<ValidatedInterchange, EdifactError> {
1409 validate_envelope_from_owned(&parse(input))
1410 }
1411
1412 const VALID_INTERCHANGE: &[u8] =
1413 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'";
1414
1415 #[test]
1416 fn valid_envelope_parses_ok() {
1417 let result = parse_and_validate(VALID_INTERCHANGE).expect("envelope should be valid");
1418 assert_eq!(result.interchange.sender_id, "SENDER");
1419 assert_eq!(result.interchange.sender_qualifier, ""); assert_eq!(result.interchange.recipient_id, "RECEIVER");
1421 assert_eq!(result.interchange.recipient_qualifier, "");
1422 assert_eq!(result.interchange.syntax_identifier, "UNOA");
1423 assert_eq!(result.interchange.syntax_version, "3");
1424 assert_eq!(result.interchange.control_ref, "00001");
1425 assert_eq!(result.interchange.declared_unit_count, 1);
1426 assert_eq!(result.interchange.actual_unit_count, 1);
1427 assert!(!result.interchange.is_test());
1428 assert!(!result.has_functional_groups());
1429 assert_eq!(result.message_count(), 1);
1430 assert_eq!(result.messages[0].message_type, "ORDERS");
1431 assert_eq!(result.messages[0].association_code, "EAN010");
1432 assert_eq!(result.messages[0].declared_segment_count, 4);
1433 assert_eq!(result.messages[0].actual_segment_count, 4);
1434 }
1435
1436 #[test]
1437 fn valid_envelope_parses_ok_owned_path() {
1438 let result = parse_and_validate_owned(VALID_INTERCHANGE).expect("envelope should be valid");
1439 assert_eq!(result.interchange.sender_id, "SENDER");
1440 assert_eq!(result.interchange.actual_unit_count, 1);
1441 assert_eq!(result.messages[0].declared_segment_count, 4);
1442 }
1443
1444 #[test]
1445 fn unt_count_mismatch_returns_err() {
1446 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'";
1447 let result = parse_and_validate(input);
1448 assert!(
1449 matches!(
1450 result,
1451 Err(EdifactError::SegmentCountMismatch { expected: 99, .. })
1452 ),
1453 "expected SegmentCountMismatch, got {result:?}"
1454 );
1455 }
1456
1457 #[test]
1458 fn unz_count_mismatch_returns_err() {
1459 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'";
1460 let result = parse_and_validate(input);
1461 assert!(
1462 matches!(
1463 result,
1464 Err(EdifactError::MessageCountMismatch {
1465 expected: 2,
1466 actual: 1
1467 })
1468 ),
1469 "expected MessageCountMismatch(2,1), got {result:?}"
1470 );
1471 }
1472
1473 #[test]
1474 fn missing_unb_returns_err() {
1475 let input = b"UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
1476 assert!(parse_and_validate(input).is_err());
1477 }
1478
1479 #[test]
1480 fn extracts_una_interchange_correctly() {
1481 let result = parse_and_validate(VALID_INTERCHANGE).unwrap();
1482 assert_eq!(result.interchange.syntax_identifier, "UNOA");
1483 assert_eq!(result.interchange.syntax_version, "3");
1484 assert_eq!(result.interchange.date, "230401");
1485 assert_eq!(result.interchange.time.as_deref(), Some("0900"));
1486 }
1487
1488 #[test]
1489 fn sender_and_recipient_qualifiers_extracted() {
1490 let input = b"UNB+UNOA:3+1234567890123:14+9876543210987:14+200101:0900+1'\
1492 UNH+1+ORDERS:D:96A:UN'\
1493 BGM+220+PO-001+9'\
1494 UNT+3+1'\
1495 UNZ+1+1'";
1496 let r = parse_and_validate(input).expect("GLN-qualified UNB must parse ok");
1497 assert_eq!(r.interchange.sender_id, "1234567890123");
1498 assert_eq!(r.interchange.sender_qualifier, "14");
1499 assert_eq!(r.interchange.recipient_id, "9876543210987");
1500 assert_eq!(r.interchange.recipient_qualifier, "14");
1501 }
1502
1503 #[test]
1507 fn test_indicator_parsed_from_unb() {
1508 let input = b"UNB+UNOA:3+S+R+200101:0900+1++++++1'\
1512 UNH+1+ORDERS:D:96A:UN'\
1513 BGM+220+PO-001+9'\
1514 UNT+3+1'\
1515 UNZ+1+1'";
1516 let r = parse_and_validate(input).expect("test-flagged UNB must parse ok");
1517 assert!(
1518 r.interchange.test_indicator,
1519 "test_indicator should be true"
1520 );
1521 assert!(r.interchange.is_test(), "is_test() convenience must agree");
1522 }
1523
1524 #[test]
1525 fn no_test_indicator_defaults_false() {
1526 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1527 assert!(!r.interchange.test_indicator);
1528 assert!(!r.interchange.is_test());
1529 }
1530
1531 #[test]
1532 fn app_ref_extracted_when_present() {
1533 let input = b"UNB+UNOA:3+S+R+200101:0900+1++MYAPP'\
1535 UNH+1+ORDERS:D:96A:UN'\
1536 BGM+220+PO-001+9'\
1537 UNT+3+1'\
1538 UNZ+1+1'";
1539 let r = parse_and_validate(input).expect("UNB with app_ref must parse ok");
1540 assert_eq!(r.interchange.app_ref.as_deref(), Some("MYAPP"));
1541 }
1542
1543 #[test]
1544 fn app_ref_is_none_when_absent() {
1545 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1546 assert!(r.interchange.app_ref.is_none());
1547 }
1548
1549 #[test]
1550 fn recipient_password_extracted_when_present() {
1551 let input = b"UNB+UNOA:3+S+R+200101:0900+1+MYPASS'\
1553 UNH+1+ORDERS:D:96A:UN'\
1554 BGM+220+PO-001+9'\
1555 UNT+3+1'\
1556 UNZ+1+1'";
1557 let r = parse_and_validate(input).expect("UNB with password must parse ok");
1558 assert_eq!(r.interchange.recipient_password.as_deref(), Some("MYPASS"));
1559 }
1560
1561 #[test]
1562 fn recipient_password_is_none_when_absent() {
1563 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1564 assert!(r.interchange.recipient_password.is_none());
1565 }
1566
1567 #[test]
1568 fn acknowledgement_request_flag_parsed() {
1569 let input = b"UNB+UNOA:3+S+R+200101:0900+1++++1'\
1572 UNH+1+ORDERS:D:96A:UN'\
1573 BGM+220+PO-001+9'\
1574 UNT+3+1'\
1575 UNZ+1+1'";
1576 let r = parse_and_validate(input).expect("UNB with ack-request must parse ok");
1577 assert!(r.interchange.acknowledgement_request);
1578 assert!(r.interchange.ack_requested());
1579 }
1580
1581 #[test]
1582 fn acknowledgement_request_defaults_false() {
1583 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1584 assert!(!r.interchange.acknowledgement_request);
1585 assert!(!r.interchange.ack_requested());
1586 }
1587
1588 #[test]
1589 fn communications_agreement_id_extracted() {
1590 let input = b"UNB+UNOA:3+S+R+200101:0900+1+++++EANCOM'\
1592 UNH+1+ORDERS:D:96A:UN'\
1593 BGM+220+PO-001+9'\
1594 UNT+3+1'\
1595 UNZ+1+1'";
1596 let r = parse_and_validate(input).expect("UNB with comms-agreement must parse ok");
1597 assert_eq!(
1598 r.interchange.communications_agreement_id.as_deref(),
1599 Some("EANCOM")
1600 );
1601 }
1602
1603 #[test]
1604 fn dangling_unh_without_unt_returns_err() {
1605 let input =
1606 b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNZ+1+1'";
1607 let result = parse_and_validate(input);
1608 assert!(
1609 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNT")
1610 );
1611 }
1612
1613 #[test]
1614 fn stray_segment_outside_message_returns_err() {
1615 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'";
1616 assert!(matches!(
1617 parse_and_validate(input),
1618 Err(EdifactError::InvalidSegmentForMessage { .. })
1619 ));
1620 }
1621
1622 #[test]
1623 fn missing_unb_sender_component_returns_err() {
1624 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'";
1625 let result = parse_and_validate(input);
1626 assert!(
1627 matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 0 }) if tag == "UNB"),
1628 "expected MissingRequiredComponent for empty sender, got: {result:?}"
1629 );
1630 }
1631
1632 #[test]
1633 fn nested_unh_without_closing_previous_message_returns_err() {
1634 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'";
1635 let result = parse_and_validate(input);
1636 assert!(
1637 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNH"),
1638 "expected InvalidSegmentForMessage(UNH), got {result:?}"
1639 );
1640 }
1641
1642 #[test]
1643 fn unt_message_reference_must_match_unh() {
1644 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'";
1645 let result = parse_and_validate(input);
1646 assert!(
1647 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNT")
1648 );
1649 }
1650
1651 #[test]
1652 fn unz_control_reference_must_match_unb() {
1653 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'";
1654 let result = parse_and_validate(input);
1655 assert!(
1656 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNZ")
1657 );
1658 }
1659
1660 #[test]
1661 fn missing_unh_message_type_components_return_err() {
1662 let input =
1663 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'";
1664 let result = parse_and_validate(input);
1665 assert!(
1666 matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 3 }) if tag == "UNH"),
1667 "got: {result:?}"
1668 );
1669 }
1670
1671 #[test]
1672 fn nested_unz_inside_message_returns_err() {
1673 let input =
1674 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'";
1675 let result = parse_and_validate(input);
1676 assert!(
1677 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNZ")
1678 );
1679 }
1680
1681 #[test]
1682 fn lenient_returns_partial_result_on_count_mismatch() {
1683 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'";
1686 let owned = parse(input);
1687 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1688 let lenient = validate_envelope_lenient(&segs);
1689 let result = lenient.interchange;
1690 let errors = lenient.errors;
1691 assert!(
1692 result.is_some(),
1693 "lenient mode must return Some even on count mismatch"
1694 );
1695 assert_eq!(errors.len(), 1);
1696 assert!(
1697 matches!(
1698 &errors[0],
1699 EdifactError::MessageCountMismatch {
1700 expected: 2,
1701 actual: 1
1702 }
1703 ),
1704 "expected MessageCountMismatch(2,1), got {:?}",
1705 errors[0]
1706 );
1707 let partial = result.unwrap();
1708 assert_eq!(partial.messages.len(), 1);
1709 assert_eq!(partial.interchange.actual_unit_count, 1);
1710 assert_eq!(partial.interchange.declared_unit_count, 2);
1711 }
1712
1713 #[test]
1714 fn lenient_returns_none_on_structural_error() {
1715 let input = b"UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
1717 let owned = parse(input);
1718 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1719 let lenient = validate_envelope_lenient(&segs);
1720 let result = lenient.interchange;
1721 let errors = lenient.errors;
1722 assert!(result.is_none(), "missing UNB must yield None");
1723 assert!(!errors.is_empty());
1724 }
1725
1726 #[test]
1727 fn message_count_convenience_method() {
1728 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1729 assert_eq!(r.message_count(), r.messages.len());
1730 assert_eq!(r.message_count(), 1);
1731 }
1732
1733 #[test]
1734 fn interchange_with_single_functional_group_parses_ok() {
1735 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1736 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1737 UNH+1+ORDERS:D:96A:UN'\
1738 BGM+220+PO-001+9'\
1739 UNT+3+1'\
1740 UNE+1+1'\
1741 UNZ+1+1'";
1742 let result = parse_and_validate(input).expect("single-group interchange must parse ok");
1743 assert!(result.has_functional_groups());
1744 assert_eq!(result.functional_groups.len(), 1);
1745 let g = &result.functional_groups[0];
1746 assert_eq!(g.group_id, "ORDERS");
1747 assert_eq!(g.group_ref, "1");
1748 assert_eq!(g.controlling_agency, "UN");
1749 assert_eq!(g.declared_message_count, 1);
1750 assert_eq!(g.actual_message_count, 1);
1751 assert_eq!(result.messages.len(), 1);
1752 assert_eq!(result.messages[0].message_type, "ORDERS");
1753 assert_eq!(result.interchange.declared_unit_count, 1);
1754 assert_eq!(result.interchange.actual_unit_count, 1);
1755 }
1756
1757 #[test]
1758 fn interchange_with_multi_message_group_parses_ok() {
1759 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1762 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1763 UNH+1+ORDERS:D:96A:UN'\
1764 BGM+220+PO-001+9'\
1765 UNT+3+1'\
1766 UNH+2+ORDERS:D:96A:UN'\
1767 BGM+220+PO-002+9'\
1768 UNT+3+2'\
1769 UNE+2+1'\
1770 UNZ+1+1'";
1771 let result = parse_and_validate(input).expect("multi-message group must parse ok");
1772 assert!(result.has_functional_groups());
1773 assert_eq!(result.functional_groups.len(), 1);
1774 assert_eq!(result.functional_groups[0].actual_message_count, 2);
1775 assert_eq!(result.messages.len(), 2);
1776 assert_eq!(result.interchange.actual_unit_count, 1);
1778 assert_eq!(result.interchange.declared_unit_count, 1);
1779 }
1780
1781 #[test]
1782 fn interchange_with_multiple_groups_parses_ok() {
1783 let input = b"UNB+UNOA:3+S+R+200101:0900+2'\
1784 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1785 UNH+1+ORDERS:D:96A:UN'\
1786 BGM+220+PO-001+9'\
1787 UNT+3+1'\
1788 UNE+1+1'\
1789 UNG+INVOIC+S+R+200101:0900+2+UN+D:96A'\
1790 UNH+2+INVOIC:D:96A:UN'\
1791 BGM+380+INV-001+9'\
1792 UNT+3+2'\
1793 UNE+1+2'\
1794 UNZ+2+2'";
1795 let result = parse_and_validate(input).expect("multi-group interchange must parse ok");
1796 assert_eq!(result.functional_groups.len(), 2);
1797 assert_eq!(result.functional_groups[0].group_id, "ORDERS");
1798 assert_eq!(result.functional_groups[1].group_id, "INVOIC");
1799 assert_eq!(result.messages.len(), 2);
1800 assert_eq!(result.interchange.declared_unit_count, 2);
1801 assert_eq!(result.interchange.actual_unit_count, 2);
1802 }
1803
1804 #[test]
1805 fn ung_une_count_mismatch_returns_err() {
1806 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1807 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1808 UNH+1+ORDERS:D:96A:UN'\
1809 BGM+220+PO-001+9'\
1810 UNT+3+1'\
1811 UNE+2+1'\
1812 UNZ+1+1'";
1813 let result = parse_and_validate(input);
1814 assert!(
1815 matches!(
1816 result,
1817 Err(EdifactError::MessageCountMismatch {
1818 expected: 2,
1819 actual: 1
1820 })
1821 ),
1822 "expected MessageCountMismatch(2,1), got {result:?}"
1823 );
1824 }
1825
1826 #[test]
1827 fn une_without_ung_returns_err() {
1828 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1829 UNE+1+1'\
1830 UNH+1+ORDERS:D:96A:UN'\
1831 BGM+220+PO-001+9'\
1832 UNT+3+1'\
1833 UNZ+1+1'";
1834 let result = parse_and_validate(input);
1835 assert!(
1836 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNE"),
1837 "expected InvalidSegmentForMessage(UNE), got {result:?}"
1838 );
1839 }
1840
1841 #[test]
1842 fn ung_without_une_returns_err() {
1843 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1844 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1845 UNH+1+ORDERS:D:96A:UN'\
1846 BGM+220+PO-001+9'\
1847 UNT+3+1'\
1848 UNZ+1+1'";
1849 let result = parse_and_validate(input);
1850 assert!(
1851 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNE"),
1852 "expected MissingSegment(UNE), got {result:?}"
1853 );
1854 }
1855
1856 #[test]
1857 fn une_group_ref_must_match_ung() {
1858 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1859 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1860 UNH+1+ORDERS:D:96A:UN'\
1861 BGM+220+PO-001+9'\
1862 UNT+3+1'\
1863 UNE+1+999'\
1864 UNZ+1+1'";
1865 let result = parse_and_validate(input);
1866 assert!(
1867 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNE"),
1868 "expected QualifierMismatch(UNE), got {result:?}"
1869 );
1870 }
1871
1872 #[test]
1875 fn processing_priority_extracted_when_present() {
1876 let input = b"UNB+UNOA:3+S+R+200101:0900+1+++A'\
1878 UNH+1+ORDERS:D:96A:UN'\
1879 BGM+220+PO-001+9'\
1880 UNT+3+1'\
1881 UNZ+1+1'";
1882 let r = parse_and_validate(input).expect("UNB with processing_priority must parse ok");
1883 assert_eq!(r.interchange.processing_priority.as_deref(), Some("A"));
1884 }
1885
1886 #[test]
1887 fn processing_priority_is_none_when_absent() {
1888 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1889 assert!(r.interchange.processing_priority.is_none());
1890 }
1891
1892 #[test]
1893 fn sender_routing_address_extracted_when_present() {
1894 let input = b"UNB+UNOA:3+SENDER:14:ROUTEA+RECIP+200101:0900+1'\
1896 UNH+1+ORDERS:D:96A:UN'\
1897 BGM+220+PO-001+9'\
1898 UNT+3+1'\
1899 UNZ+1+1'";
1900 let r = parse_and_validate(input).expect("UNB with sender routing must parse ok");
1901 assert_eq!(
1902 r.interchange.sender_routing_address.as_deref(),
1903 Some("ROUTEA")
1904 );
1905 assert!(r.interchange.recipient_routing_address.is_none());
1906 }
1907
1908 #[test]
1909 fn recipient_routing_address_extracted_when_present() {
1910 let input = b"UNB+UNOA:3+SENDER+RECIP:14:ROUTEB+200101:0900+1'\
1912 UNH+1+ORDERS:D:96A:UN'\
1913 BGM+220+PO-001+9'\
1914 UNT+3+1'\
1915 UNZ+1+1'";
1916 let r = parse_and_validate(input).expect("UNB with recipient routing must parse ok");
1917 assert!(r.interchange.sender_routing_address.is_none());
1918 assert_eq!(
1919 r.interchange.recipient_routing_address.as_deref(),
1920 Some("ROUTEB")
1921 );
1922 }
1923
1924 #[test]
1925 fn routing_address_is_none_when_absent() {
1926 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1928 UNH+1+ORDERS:D:96A:UN'\
1929 BGM+220+PO-001+9'\
1930 UNT+3+1'\
1931 UNZ+1+1'";
1932 let r = parse_and_validate(input).unwrap();
1933 assert!(r.interchange.sender_routing_address.is_none());
1934 assert!(r.interchange.recipient_routing_address.is_none());
1935 }
1936
1937 #[test]
1938 fn common_access_ref_extracted_when_present() {
1939 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1941 UNH+1+ORDERS:D:96A:UN+COMREF1'\
1942 BGM+220+PO-001+9'\
1943 UNT+3+1'\
1944 UNZ+1+1'";
1945 let r = parse_and_validate(input).expect("UNH with common_access_ref must parse ok");
1946 assert_eq!(r.messages[0].common_access_ref.as_deref(), Some("COMREF1"));
1947 }
1948
1949 #[test]
1950 fn common_access_ref_is_none_when_absent() {
1951 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1952 assert!(r.messages[0].common_access_ref.is_none());
1953 }
1954
1955 #[test]
1956 fn parse_unh_includes_message_ref() {
1957 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1958 UNH+REF42+ORDERS:D:96A:UN'\
1959 BGM+220+PO-001+9'\
1960 UNT+3+REF42'\
1961 UNZ+1+1'";
1962 let r = parse_and_validate(input).expect("must parse ok");
1965 assert_eq!(r.messages[0].message_ref, "REF42");
1966 assert_eq!(r.messages[0].message_type, "ORDERS");
1967 }
1968
1969 #[test]
1970 fn iter_messages_by_type_returns_matching_messages() {
1971 let input = b"UNB+UNOA:3+S+R+200101:0900+CTRL2'\
1972 UNH+1+ORDERS:D:96A:UN'\
1973 BGM+220+PO-001+9'\
1974 UNT+3+1'\
1975 UNH+2+INVOIC:D:96A:UN'\
1976 BGM+380+INV-001+9'\
1977 UNT+3+2'\
1978 UNZ+2+CTRL2'";
1979 let r = parse_and_validate(input).expect("two-message interchange must parse ok");
1980 let orders: Vec<_> = r.iter_messages_by_type("ORDERS").collect();
1981 assert_eq!(orders.len(), 1);
1982 assert_eq!(orders[0].message_ref, "1");
1983 let invoices: Vec<_> = r.iter_messages_by_type("INVOIC").collect();
1984 assert_eq!(invoices.len(), 1);
1985 assert_eq!(invoices[0].message_ref, "2");
1986 let none: Vec<_> = r.iter_messages_by_type("DESADV").collect();
1987 assert!(none.is_empty());
1988 }
1989
1990 #[test]
1991 fn display_interchange_envelope() {
1992 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1993 let s = r.interchange.to_string();
1994 assert!(s.contains("SENDER"), "Display must include sender_id");
1995 assert!(s.contains("UNOA"), "Display must include syntax_identifier");
1996 }
1997
1998 #[test]
1999 fn display_message_envelope() {
2000 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2001 let s = r.messages[0].to_string();
2002 assert!(s.contains("ORDERS"), "Display must include message_type");
2003 assert!(s.contains("ref="), "Display must include message ref label");
2004 }
2005
2006 #[test]
2007 fn display_validated_interchange() {
2008 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2009 let s = r.to_string();
2010 assert!(
2011 s.contains("messages="),
2012 "Display must include message count label"
2013 );
2014 }
2015
2016 #[test]
2019 fn unrecognised_syntax_identifier_returns_err() {
2020 let input = b"UNB+XXXX:3+S+R+200101:0900+1'\
2022 UNH+1+ORDERS:D:96A:UN'\
2023 BGM+220+PO-001+9'\
2024 UNT+3+1'\
2025 UNZ+1+1'";
2026 let result = parse_and_validate(input);
2027 assert!(
2028 matches!(result, Err(EdifactError::UnrecognisedSyntaxIdentifier(ref id)) if id == "XXXX"),
2029 "expected UnrecognisedSyntaxIdentifier(\"XXXX\"), got {result:?}"
2030 );
2031 }
2032
2033 #[test]
2034 fn all_valid_syntax_identifiers_accepted() {
2035 for id in &["UNOA", "UNOB", "UNOC", "UNOD", "UNOE", "UNOF", "KECA"] {
2036 let input = format!(
2037 "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'"
2038 );
2039 let result = parse_and_validate(input.as_bytes());
2040 assert!(
2041 result.is_ok(),
2042 "syntax id '{id}' should be accepted, got {result:?}"
2043 );
2044 }
2045 }
2046
2047 #[test]
2050 fn recipient_password_qualifier_extracted_when_present() {
2051 let input = b"UNB+UNOA:3+S+R+200101:0900+1+MYPASS:AA'\
2053 UNH+1+ORDERS:D:96A:UN'\
2054 BGM+220+PO-001+9'\
2055 UNT+3+1'\
2056 UNZ+1+1'";
2057 let r = parse_and_validate(input).expect("UNB with password+qualifier must parse ok");
2058 assert_eq!(r.interchange.recipient_password.as_deref(), Some("MYPASS"));
2059 assert_eq!(
2060 r.interchange.recipient_password_qualifier.as_deref(),
2061 Some("AA")
2062 );
2063 }
2064
2065 #[test]
2066 fn recipient_password_qualifier_is_none_when_absent() {
2067 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2068 assert!(r.interchange.recipient_password_qualifier.is_none());
2069 }
2070
2071 #[test]
2074 fn sequence_of_transfers_extracted_when_present() {
2075 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2077 UNH+1+ORDERS:D:96A:UN++2:C'\
2078 BGM+220+PO-001+9'\
2079 UNT+3+1'\
2080 UNZ+1+1'";
2081 let r = parse_and_validate(input).expect("UNH with S010 must parse ok");
2082 assert_eq!(r.messages[0].sequence_of_transfers, Some(2));
2083 assert_eq!(r.messages[0].transfer_position.as_deref(), Some("C"));
2084 }
2085
2086 #[test]
2087 fn sequence_of_transfers_none_when_absent() {
2088 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2089 assert!(r.messages[0].sequence_of_transfers.is_none());
2090 assert!(r.messages[0].transfer_position.is_none());
2091 }
2092
2093 #[test]
2096 fn ung_app_sender_and_recipient_qualifiers_extracted() {
2097 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2099 UNG+ORDERS+APPSEND:ZZZ+APPRECV:14+200101:0900+1+UN+D:96A'\
2100 UNH+1+ORDERS:D:96A:UN'\
2101 BGM+220+PO-001+9'\
2102 UNT+3+1'\
2103 UNE+1+1'\
2104 UNZ+1+1'";
2105 let r = parse_and_validate(input).expect("UNG with qualifiers must parse ok");
2106 let g = &r.functional_groups[0];
2107 assert_eq!(g.app_sender, "APPSEND");
2108 assert_eq!(g.app_sender_qualifier, "ZZZ");
2109 assert_eq!(g.app_recipient, "APPRECV");
2110 assert_eq!(g.app_recipient_qualifier, "14");
2111 }
2112
2113 #[test]
2114 fn ung_qualifiers_empty_when_absent() {
2115 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2116 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
2117 UNH+1+ORDERS:D:96A:UN'\
2118 BGM+220+PO-001+9'\
2119 UNT+3+1'\
2120 UNE+1+1'\
2121 UNZ+1+1'";
2122 let r = parse_and_validate(input).unwrap();
2123 let g = &r.functional_groups[0];
2124 assert_eq!(g.app_sender_qualifier, "");
2125 assert_eq!(g.app_recipient_qualifier, "");
2126 }
2127
2128 #[test]
2131 fn lenient_result_is_valid_true_on_clean_interchange() {
2132 let owned = parse(VALID_INTERCHANGE);
2133 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2134 let r = validate_envelope_lenient(&segs);
2135 assert!(r.is_valid());
2136 assert!(r.errors.is_empty());
2137 assert!(r.interchange.is_some());
2138 }
2139
2140 #[test]
2141 fn lenient_result_into_strict_ok_path() {
2142 let owned = parse(VALID_INTERCHANGE);
2143 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2144 let r = validate_envelope_lenient(&segs);
2145 let strict = r.into_strict();
2146 assert!(
2147 strict.is_ok(),
2148 "into_strict() should succeed for valid interchange"
2149 );
2150 assert_eq!(strict.unwrap().messages.len(), 1);
2151 }
2152
2153 #[test]
2154 fn lenient_result_into_strict_err_path() {
2155 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'";
2157 let owned = parse(input);
2158 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2159 let r = validate_envelope_lenient(&segs);
2160 assert!(!r.is_valid());
2161 let strict = r.into_strict();
2162 assert!(strict.is_err());
2163 let errors = strict.unwrap_err();
2164 assert_eq!(errors.len(), 1);
2165 assert!(matches!(
2166 &errors[0],
2167 EdifactError::MessageCountMismatch {
2168 expected: 2,
2169 actual: 1
2170 }
2171 ));
2172 }
2173
2174 #[test]
2177 fn parse_unh_direct_extracts_all_s009_fields() {
2178 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2183 UNH+REF99+ORDERS:D:96A:UN:EAN010'\
2184 BGM+220+PO-001+9'\
2185 UNT+3+REF99'\
2186 UNZ+1+1'";
2187 let r = parse_and_validate(input).expect("must parse ok");
2188 let msg = &r.messages[0];
2189 assert_eq!(msg.message_ref, "REF99");
2190 assert_eq!(msg.message_type, "ORDERS");
2191 assert_eq!(msg.version, "D");
2192 assert_eq!(msg.release, "96A");
2193 assert_eq!(msg.controlling_agency, "UN");
2194 assert_eq!(msg.association_code, "EAN010");
2195 }
2196
2197 #[test]
2198 fn parse_ung_direct_extracts_identifier_fields() {
2199 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2201 UNG+ORDERS+APPSEND:ZZZ+APPRECV:14+200101:0900+GRP01+UN+D:96A'\
2202 UNH+1+ORDERS:D:96A:UN'\
2203 BGM+220+PO-001+9'\
2204 UNT+3+1'\
2205 UNE+1+GRP01'\
2206 UNZ+1+1'";
2207 let r = parse_and_validate(input).expect("must parse ok");
2208 let g = &r.functional_groups[0];
2209 assert_eq!(g.group_id, "ORDERS");
2210 assert_eq!(g.app_sender, "APPSEND");
2211 assert_eq!(g.app_sender_qualifier, "ZZZ");
2212 assert_eq!(g.app_recipient, "APPRECV");
2213 assert_eq!(g.app_recipient_qualifier, "14");
2214 assert_eq!(g.group_ref, "GRP01");
2215 assert_eq!(g.controlling_agency, "UN");
2216 assert_eq!(g.version, "D");
2217 assert_eq!(g.release, "96A");
2218 }
2219
2220 #[test]
2223 fn find_message_returns_correct_message_by_ref() {
2224 let input = b"UNB+UNOA:3+S+R+200101:0900+CTRL2'\
2225 UNH+REF-A+ORDERS:D:96A:UN'\
2226 BGM+220+PO-001+9'\
2227 UNT+3+REF-A'\
2228 UNH+REF-B+INVOIC:D:96A:UN'\
2229 BGM+380+INV-001+9'\
2230 UNT+3+REF-B'\
2231 UNZ+2+CTRL2'";
2232 let r = parse_and_validate(input).expect("two-message interchange must parse ok");
2233 let msg_a = r.find_message("REF-A");
2234 assert!(msg_a.is_some());
2235 assert_eq!(msg_a.unwrap().message_type, "ORDERS");
2236 let msg_b = r.find_message("REF-B");
2237 assert!(msg_b.is_some());
2238 assert_eq!(msg_b.unwrap().message_type, "INVOIC");
2239 assert!(r.find_message("MISSING").is_none());
2240 }
2241
2242 #[test]
2245 fn ung_missing_group_ref_returns_err() {
2246 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2248 UNG+ORDERS+S+R+200101:0900++UN+D:96A'\
2249 UNH+1+ORDERS:D:96A:UN'\
2250 BGM+220+PO-001+9'\
2251 UNT+3+1'\
2252 UNE+1+'\
2253 UNZ+1+1'";
2254 let result = parse_and_validate(input);
2255 assert!(
2256 matches!(result,
2257 Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 4, component_index: 0 })
2258 if tag == "UNG"
2259 ),
2260 "expected MissingRequiredComponent for empty UNG group_ref, got {result:?}"
2261 );
2262 }
2263
2264 #[test]
2267 fn empty_segment_list_returns_missing_unb() {
2268 let result = validate_envelope(&[]);
2271 assert!(
2272 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNB"),
2273 "expected MissingSegment(UNB) for empty input, got {result:?}"
2274 );
2275 }
2276
2277 #[test]
2278 fn single_segment_only_unb_returns_missing_unz() {
2279 let input = b"UNB+UNOA:3+S+R+200101:0900+1'";
2281 let owned = parse(input);
2282 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2283 let result = validate_envelope(&segs);
2284 assert!(
2285 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNZ"),
2286 "expected MissingSegment(UNZ) for UNB-only input, got {result:?}"
2287 );
2288 }
2289
2290 #[test]
2291 fn display_validated_interchange_includes_count() {
2292 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2293 let s = r.to_string();
2294 assert!(
2296 s.contains("messages=1"),
2297 "Display must include count '1': {s}"
2298 );
2299 }
2300
2301 #[test]
2302 fn display_interchange_envelope_contains_arrow() {
2303 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2304 let s = r.interchange.to_string();
2305 assert!(s.contains("->"), "Display must use ASCII '->' arrow: {s}");
2307 assert!(
2308 !s.contains('\u{2192}'),
2309 "Display must not use Unicode → arrow: {s}"
2310 );
2311 }
2312
2313 #[test]
2314 fn display_functional_group_envelope() {
2315 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2316 UNG+ORDERS+APPSEND+APPRECV+200101:0900+GRP01+UN+D:96A'\
2317 UNH+1+ORDERS:D:96A:UN'\
2318 BGM+220+PO-001+9'\
2319 UNT+3+1'\
2320 UNE+1+GRP01'\
2321 UNZ+1+1'";
2322 let r = parse_and_validate(input).expect("must parse ok");
2323 let g = &r.functional_groups[0];
2324 let s = g.to_string();
2325 assert!(s.contains("ORDERS"), "Display must include group_id: {s}");
2326 assert!(
2327 s.contains("APPSEND"),
2328 "Display must include app_sender: {s}"
2329 );
2330 assert!(
2331 s.contains("APPRECV"),
2332 "Display must include app_recipient: {s}"
2333 );
2334 assert!(s.contains("GRP01"), "Display must include group_ref: {s}");
2335 assert!(
2336 s.contains("msgs="),
2337 "Display must include message count label: {s}"
2338 );
2339 assert!(
2340 s.contains("1/1"),
2341 "Display must include actual/declared counts: {s}"
2342 );
2343 }
2344
2345 #[test]
2346 fn lenient_has_errors_is_inverse_of_is_valid() {
2347 let owned = parse(VALID_INTERCHANGE);
2348 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2349 let valid = validate_envelope_lenient(&segs);
2350 assert!(valid.is_valid());
2351 assert!(!valid.has_errors());
2352
2353 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'";
2355 let owned2 = parse(input);
2356 let segs2: Vec<Segment<'_>> = owned2
2357 .iter()
2358 .map(crate::OwnedSegment::as_borrowed)
2359 .collect();
2360 let invalid = validate_envelope_lenient(&segs2);
2361 assert!(!invalid.is_valid());
2362 assert!(invalid.has_errors());
2363 }
2364}