1use crate::{
21 OwnedSegment,
22 error::EdifactError,
23 model::{Segment, Span},
24};
25use std::collections::HashSet;
26
27pub(crate) trait SegmentReader: sealed::Sealed {
30 fn tag(&self) -> &str;
31 fn span(&self) -> Span;
32 fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str>;
33
34 fn required_component_field(
35 &self,
36 elem_idx: usize,
37 comp_idx: usize,
38 ) -> Result<&str, EdifactError> {
39 self.component(elem_idx, comp_idx)
40 .filter(|s| !s.is_empty())
41 .ok_or_else(|| EdifactError::MissingRequiredComponent {
42 tag: self.tag().to_owned(),
43 element_index: elem_idx,
44 component_index: comp_idx,
45 })
46 }
47}
48
49mod sealed {
50 pub trait Sealed {}
51 impl Sealed for crate::model::Segment<'_> {}
52 impl Sealed for crate::OwnedSegment {}
53}
54
55impl SegmentReader for Segment<'_> {
56 #[inline]
57 fn tag(&self) -> &str {
58 self.tag
59 }
60 #[inline]
61 fn span(&self) -> Span {
62 self.span
63 }
64 #[inline]
65 fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str> {
66 self.get_element(elem_idx)?.get_component(comp_idx)
67 }
68}
69
70impl SegmentReader for OwnedSegment {
71 #[inline]
72 fn tag(&self) -> &str {
73 &self.tag
74 }
75 #[inline]
76 fn span(&self) -> Span {
77 self.span
78 }
79 #[inline]
80 fn component(&self, elem_idx: usize, comp_idx: usize) -> Option<&str> {
81 self.component_str(elem_idx, comp_idx)
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Hash)]
109#[non_exhaustive]
110#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
111pub struct InterchangeEnvelope {
112 pub syntax_identifier: String,
114 pub syntax_version: String,
118 pub sender_id: String,
120 pub sender_qualifier: String,
125 pub sender_routing_address: Option<String>,
134 pub recipient_id: String,
136 pub recipient_qualifier: String,
140 pub recipient_routing_address: Option<String>,
145 pub date: String,
147 pub time: Option<String>,
151 pub control_ref: String,
153 pub recipient_password: Option<String>,
158 pub recipient_password_qualifier: Option<String>,
163 pub app_ref: Option<String>,
167 pub processing_priority: Option<String>,
172 pub acknowledgement_request: bool,
177 pub communications_agreement_id: Option<String>,
181 pub test_indicator: bool,
187 pub declared_unit_count: u32,
195 pub actual_unit_count: u32,
197}
198
199impl InterchangeEnvelope {
200 #[inline]
222 #[must_use]
223 pub fn is_test(&self) -> bool {
224 self.test_indicator
225 }
226
227 #[inline]
231 #[must_use]
232 pub fn ack_requested(&self) -> bool {
233 self.acknowledgement_request
234 }
235}
236
237impl std::fmt::Display for InterchangeEnvelope {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 write!(
240 f,
241 "{sender} -> {recipient} [{ctrl}] ({syntax}:{ver})",
242 sender = self.sender_id,
243 recipient = self.recipient_id,
244 ctrl = self.control_ref,
245 syntax = self.syntax_identifier,
246 ver = self.syntax_version,
247 )
248 }
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Hash)]
253#[non_exhaustive]
254#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
255pub struct MessageEnvelope {
256 pub message_ref: String,
258 pub message_type: String,
260 pub version: String,
262 pub release: String,
264 pub controlling_agency: String,
266 pub association_code: String,
268 pub common_access_ref: Option<String>,
275 pub sequence_of_transfers: Option<u32>,
281 pub transfer_position: Option<String>,
286 pub declared_segment_count: u32,
288 pub actual_segment_count: u32,
290}
291
292impl std::fmt::Display for MessageEnvelope {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 write!(
295 f,
296 "{msg_type}:{ver}:{rel} ref={msg_ref} seg={actual}/{declared}",
297 msg_type = self.message_type,
298 ver = self.version,
299 rel = self.release,
300 msg_ref = self.message_ref,
301 actual = self.actual_segment_count,
302 declared = self.declared_segment_count,
303 )
304 }
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Hash)]
313#[non_exhaustive]
314#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
315pub struct FunctionalGroupEnvelope {
316 pub group_id: String,
318 pub app_sender: String,
320 pub app_sender_qualifier: String,
324 pub app_recipient: String,
326 pub app_recipient_qualifier: String,
330 pub date: String,
332 pub time: Option<String>,
334 pub group_ref: String,
336 pub controlling_agency: String,
338 pub version: String,
340 pub release: String,
342 pub declared_message_count: u32,
344 pub actual_message_count: u32,
346 pub messages: Vec<MessageEnvelope>,
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, Hash)]
356#[non_exhaustive]
357#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
358pub struct ValidatedInterchange {
359 pub interchange: InterchangeEnvelope,
361 pub functional_groups: Vec<FunctionalGroupEnvelope>,
366 pub messages: Vec<MessageEnvelope>,
371}
372
373impl ValidatedInterchange {
374 #[inline]
376 #[must_use]
377 pub fn has_functional_groups(&self) -> bool {
378 !self.functional_groups.is_empty()
379 }
380
381 #[inline]
385 #[must_use]
386 pub fn message_count(&self) -> usize {
387 self.messages.len()
388 }
389
390 #[inline]
395 pub fn iter_messages(&self) -> impl Iterator<Item = &MessageEnvelope> {
396 self.messages.iter()
397 }
398
399 #[inline]
406 #[must_use]
407 pub fn find_message(&self, reference: &str) -> Option<&MessageEnvelope> {
408 self.messages.iter().find(|m| m.message_ref == reference)
409 }
410
411 #[must_use]
420 pub fn messages_by_type(&self, message_type: &str) -> Vec<&MessageEnvelope> {
421 self.messages
422 .iter()
423 .filter(|m| m.message_type == message_type)
424 .collect()
425 }
426
427 #[inline]
438 pub fn iter_messages_by_type<'s, 'q: 's>(
439 &'s self,
440 message_type: &'q str,
441 ) -> impl Iterator<Item = &'s MessageEnvelope> + 's {
442 self.messages
443 .iter()
444 .filter(move |m| m.message_type == message_type)
445 }
446}
447
448impl std::fmt::Display for ValidatedInterchange {
449 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
450 write!(
451 f,
452 "{ic} messages={n}",
453 ic = self.interchange,
454 n = self.messages.len(),
455 )
456 }
457}
458
459impl std::fmt::Display for FunctionalGroupEnvelope {
460 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461 write!(
462 f,
463 "{gid} sender={sender} recipient={recip} [{gref}] ({agency}) msgs={actual}/{declared}",
464 gid = self.group_id,
465 sender = self.app_sender,
466 recip = self.app_recipient,
467 gref = self.group_ref,
468 agency = self.controlling_agency,
469 actual = self.actual_message_count,
470 declared = self.declared_message_count,
471 )
472 }
473}
474
475#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
480#[non_exhaustive]
481#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
482pub struct MessageIdentifier<'a> {
483 pub message_ref: &'a str,
485 pub message_type: &'a str,
486 pub version: &'a str,
487 pub release: &'a str,
488 pub controlling_agency: &'a str,
489 pub association_code: &'a str,
493}
494
495pub fn parse_unh<'a>(unh: &'a Segment<'a>) -> Result<MessageIdentifier<'a>, EdifactError> {
499 let message_ref = unh
501 .get_element(0)
502 .and_then(|e| e.get_component(0))
503 .filter(|s| !s.is_empty())
504 .ok_or_else(|| EdifactError::MissingRequiredComponent {
505 tag: "UNH".to_owned(),
506 element_index: 0,
507 component_index: 0,
508 })?;
509 let elem = unh
511 .get_element(1)
512 .ok_or_else(|| EdifactError::MissingRequiredElement {
513 tag: "UNH".to_owned(),
514 element_index: 1,
515 })?;
516 let message_type =
517 elem.get_component(0)
518 .ok_or_else(|| EdifactError::MissingRequiredComponent {
519 tag: "UNH".to_owned(),
520 element_index: 1,
521 component_index: 0,
522 })?;
523 Ok(MessageIdentifier {
524 message_ref,
525 message_type,
526 version: elem.get_component(1).unwrap_or(""),
527 release: elem.get_component(2).unwrap_or(""),
528 controlling_agency: elem.get_component(3).unwrap_or(""),
529 association_code: elem.get_component(4).unwrap_or(""),
530 })
531}
532
533#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
551#[non_exhaustive]
552#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
553pub struct GroupIdentifier<'a> {
554 pub group_id: &'a str,
556 pub app_sender: &'a str,
558 pub app_sender_qualifier: &'a str,
560 pub app_recipient: &'a str,
562 pub app_recipient_qualifier: &'a str,
564 pub group_ref: &'a str,
566 pub controlling_agency: &'a str,
568 pub version: &'a str,
570 pub release: &'a str,
572}
573
574pub fn parse_ung<'a>(ung: &'a Segment<'a>) -> Result<GroupIdentifier<'a>, EdifactError> {
579 let group_id = ung
580 .get_element(0)
581 .and_then(|e| e.get_component(0))
582 .unwrap_or("");
583 let app_sender = ung
584 .get_element(1)
585 .and_then(|e| e.get_component(0))
586 .unwrap_or("");
587 let app_sender_qualifier = ung
588 .get_element(1)
589 .and_then(|e| e.get_component(1))
590 .unwrap_or("");
591 let app_recipient = ung
592 .get_element(2)
593 .and_then(|e| e.get_component(0))
594 .unwrap_or("");
595 let app_recipient_qualifier = ung
596 .get_element(2)
597 .and_then(|e| e.get_component(1))
598 .unwrap_or("");
599 let group_ref = ung
600 .get_element(4)
601 .and_then(|e| e.get_component(0))
602 .ok_or_else(|| EdifactError::MissingRequiredComponent {
603 tag: "UNG".to_owned(),
604 element_index: 4,
605 component_index: 0,
606 })?;
607 let controlling_agency = ung
608 .get_element(5)
609 .and_then(|e| e.get_component(0))
610 .unwrap_or("");
611 let s008 = ung.get_element(6);
612 let version = s008.as_ref().and_then(|e| e.get_component(0)).unwrap_or("");
613 let release = s008.as_ref().and_then(|e| e.get_component(1)).unwrap_or("");
614 Ok(GroupIdentifier {
615 group_id,
616 app_sender,
617 app_sender_qualifier,
618 app_recipient,
619 app_recipient_qualifier,
620 group_ref,
621 controlling_agency,
622 version,
623 release,
624 })
625}
626
627pub fn validate_envelope(segments: &[Segment<'_>]) -> Result<ValidatedInterchange, EdifactError> {
632 validate_envelope_impl(segments)
633}
634
635pub fn validate_envelope_from_owned(
637 segments: &[OwnedSegment],
638) -> Result<ValidatedInterchange, EdifactError> {
639 validate_envelope_impl(segments)
640}
641
642#[derive(Debug)]
655#[non_exhaustive]
656pub struct LenientResult {
657 pub interchange: Option<ValidatedInterchange>,
659 pub errors: Vec<EdifactError>,
661}
662
663impl LenientResult {
664 #[inline]
666 #[must_use]
667 pub fn is_valid(&self) -> bool {
668 self.errors.is_empty()
669 }
670
671 #[inline]
676 #[must_use]
677 pub fn has_errors(&self) -> bool {
678 !self.errors.is_empty()
679 }
680
681 pub fn into_strict(self) -> Result<ValidatedInterchange, Vec<EdifactError>> {
691 match self.interchange {
692 Some(interchange) if self.errors.is_empty() => Ok(interchange),
693 _ => Err(self.errors),
694 }
695 }
696}
697
698pub fn validate_envelope_lenient(segments: &[Segment<'_>]) -> LenientResult {
709 validate_envelope_lenient_impl(segments)
710}
711
712pub fn validate_envelope_lenient_from_owned(segments: &[OwnedSegment]) -> LenientResult {
716 validate_envelope_lenient_impl(segments)
717}
718
719#[derive(Default)]
736struct ErrorSink {
737 errors: Vec<EdifactError>,
738}
739
740impl ErrorSink {
741 #[inline]
742 fn push(&mut self, error: EdifactError) {
743 self.errors.push(error);
744 }
745
746 #[inline]
748 fn recover<T>(&mut self, result: Result<T, EdifactError>, fallback: T) -> T {
749 match result {
750 Ok(value) => value,
751 Err(error) => {
752 self.errors.push(error);
753 fallback
754 }
755 }
756 }
757
758 #[inline]
760 fn required<S: SegmentReader>(&mut self, seg: &S, element: usize, component: usize) -> String {
761 self.recover(
762 seg.required_component_field(element, component)
763 .map(str::to_owned),
764 String::new(),
765 )
766 }
767}
768
769fn validate_envelope_collecting<S: SegmentReader>(
774 segments: &[S],
775) -> (Option<ValidatedInterchange>, Vec<EdifactError>) {
776 let mut sink = ErrorSink::default();
777
778 let mut interchange_env = match extract_interchange(segments, &mut sink) {
779 Ok(env) => env,
780 Err(fatal) => {
781 sink.push(fatal);
782 return (None, sink.errors);
783 }
784 };
785
786 let inner = if segments.len() >= 2 {
787 &segments[1..segments.len() - 1]
788 } else {
789 &[]
790 };
791
792 let (functional_groups, messages) = match extract_content(inner, &mut sink) {
793 Ok(pair) => pair,
794 Err(fatal) => {
795 sink.push(fatal);
796 return (None, sink.errors);
797 }
798 };
799
800 let actual_unit_count = if functional_groups.is_empty() {
804 messages.len()
805 } else {
806 functional_groups.len()
807 };
808 interchange_env.actual_unit_count = sink.recover(
809 u32::try_from(actual_unit_count).map_err(|_| EdifactError::InterchangeTooLarge {
810 count: actual_unit_count as u64,
811 }),
812 u32::MAX,
813 );
814
815 if interchange_env.declared_unit_count != interchange_env.actual_unit_count {
816 sink.push(EdifactError::MessageCountMismatch {
817 expected: interchange_env.declared_unit_count,
818 actual: interchange_env.actual_unit_count,
819 });
820 }
821
822 for msg in &messages {
823 if msg.declared_segment_count != msg.actual_segment_count {
824 sink.push(EdifactError::SegmentCountMismatch {
825 expected: msg.declared_segment_count,
826 actual: msg.actual_segment_count,
827 message_ref: msg.message_ref.clone(),
828 });
829 }
830 }
831
832 (
833 Some(ValidatedInterchange {
834 interchange: interchange_env,
835 functional_groups,
836 messages,
837 }),
838 sink.errors,
839 )
840}
841
842fn validate_envelope_impl<S: SegmentReader>(
843 segments: &[S],
844) -> Result<ValidatedInterchange, EdifactError> {
845 match validate_envelope_collecting(segments) {
846 (Some(result), errors) if errors.is_empty() => Ok(result),
847 (_, mut errors) => Err(errors
848 .drain(..)
849 .next()
850 .unwrap_or(EdifactError::MissingSegment {
851 tag: "UNB".to_owned(),
852 expected_position: "first segment of interchange".to_owned(),
853 })),
854 }
855}
856
857fn validate_envelope_lenient_impl<S: SegmentReader>(segments: &[S]) -> LenientResult {
858 let (interchange, errors) = validate_envelope_collecting(segments);
859 LenientResult {
860 interchange,
861 errors,
862 }
863}
864
865fn extract_interchange<S: SegmentReader>(
868 segments: &[S],
869 sink: &mut ErrorSink,
870) -> Result<InterchangeEnvelope, EdifactError> {
871 if segments.first().map(|s| s.tag()) != Some("UNB") {
872 return Err(EdifactError::MissingSegment {
873 tag: "UNB".to_owned(),
874 expected_position: "first segment of interchange".to_owned(),
875 });
876 }
877 if segments.last().map(|s| s.tag()) != Some("UNZ") {
878 return Err(EdifactError::MissingSegment {
879 tag: "UNZ".to_owned(),
880 expected_position: "last segment of interchange".to_owned(),
881 });
882 }
883
884 let unb = &segments[0];
885 let unz = &segments[segments.len() - 1];
886
887 let syntax_identifier = sink.required(unb, 0, 0);
888 let syntax_version = unb.component(0, 1).unwrap_or("").to_owned();
889
890 const VALID_SYNTAX_IDS: &[&str] = &["UNOA", "UNOB", "UNOC", "UNOD", "UNOE", "UNOF", "KECA"];
892 if !VALID_SYNTAX_IDS.contains(&syntax_identifier.as_str()) {
893 sink.push(EdifactError::UnrecognisedSyntaxIdentifier(
894 syntax_identifier.clone(),
895 ));
896 }
897
898 let sender_id = sink.required(unb, 1, 0);
899 let sender_qualifier = unb.component(1, 1).unwrap_or("").to_owned();
900 let sender_routing_address = unb
902 .component(1, 2)
903 .filter(|s| !s.is_empty())
904 .map(str::to_owned);
905
906 let recipient_id = sink.required(unb, 2, 0);
907 let recipient_qualifier = unb.component(2, 1).unwrap_or("").to_owned();
908 let recipient_routing_address = unb
910 .component(2, 2)
911 .filter(|s| !s.is_empty())
912 .map(str::to_owned);
913
914 let date = sink.required(unb, 3, 0);
915 let time_raw = unb.component(3, 1).unwrap_or("");
916 let time = if time_raw.is_empty() {
917 None
918 } else {
919 Some(time_raw.to_owned())
920 };
921
922 let control_ref = sink.required(unb, 4, 0);
923
924 let recipient_password = unb
926 .component(5, 0)
927 .filter(|s| !s.is_empty())
928 .map(str::to_owned);
929 let recipient_password_qualifier = unb
930 .component(5, 1)
931 .filter(|s| !s.is_empty())
932 .map(str::to_owned);
933 let app_ref = unb
935 .component(6, 0)
936 .filter(|s| !s.is_empty())
937 .map(str::to_owned);
938 let processing_priority = unb
940 .component(7, 0)
941 .filter(|s| !s.is_empty())
942 .map(str::to_owned);
943 let acknowledgement_request = unb.component(8, 0) == Some("1");
945 let communications_agreement_id = unb
947 .component(9, 0)
948 .filter(|s| !s.is_empty())
949 .map(str::to_owned);
950 let test_indicator = unb.component(10, 0) == Some("1");
952
953 let unz_control_ref = sink.required(unz, 1, 0);
954 if unz_control_ref != control_ref {
955 sink.push(EdifactError::QualifierMismatch {
956 tag: "UNZ".to_owned(),
957 actual: unz_control_ref,
958 expected: control_ref.clone(),
959 span: unz.span(),
960 });
961 }
962
963 let declared_unit_count_raw = sink.required(unz, 0, 0);
964 let declared_unit_count: u32 = sink.recover(
965 declared_unit_count_raw
966 .parse()
967 .map_err(|_| EdifactError::InvalidText {
968 offset: unz.span().start,
969 }),
970 0,
971 );
972
973 Ok(InterchangeEnvelope {
974 syntax_identifier,
975 syntax_version,
976 sender_id,
977 sender_qualifier,
978 sender_routing_address,
979 recipient_id,
980 recipient_qualifier,
981 recipient_routing_address,
982 date,
983 time,
984 control_ref,
985 recipient_password,
986 recipient_password_qualifier,
987 app_ref,
988 processing_priority,
989 acknowledgement_request,
990 communications_agreement_id,
991 test_indicator,
992 declared_unit_count,
993 actual_unit_count: 0,
994 })
995}
996
997fn extract_content<S: SegmentReader>(
1000 inner: &[S],
1001 sink: &mut ErrorSink,
1002) -> Result<(Vec<FunctionalGroupEnvelope>, Vec<MessageEnvelope>), EdifactError> {
1003 if inner.first().is_some_and(|s| s.tag() == "UNG") {
1007 let groups = extract_with_groups(inner, sink)?;
1008 let messages = groups
1009 .iter()
1010 .flat_map(|g| g.messages.iter().cloned())
1011 .collect();
1012 Ok((groups, messages))
1013 } else {
1014 let mut seen_refs = HashSet::new();
1015 let messages = extract_messages_flat(inner, sink, &mut seen_refs)?;
1016 Ok((vec![], messages))
1017 }
1018}
1019
1020fn find_matching_une<S: SegmentReader>(
1022 segments: &[S],
1023 start: usize,
1024) -> Result<usize, EdifactError> {
1025 for (offset, seg) in segments[start..].iter().enumerate() {
1026 match seg.tag() {
1027 "UNE" => return Ok(start + offset),
1028 "UNG" => {
1029 return Err(EdifactError::InvalidSegmentForMessage {
1030 tag: "UNG".to_owned(),
1031 message_type: "ENVELOPE".to_owned(),
1032 span: seg.span(),
1033 });
1034 }
1035 _ => {}
1036 }
1037 }
1038 Err(EdifactError::MissingSegment {
1039 tag: "UNE".to_owned(),
1040 expected_position: "end of functional group".to_owned(),
1041 })
1042}
1043
1044fn extract_with_groups<S: SegmentReader>(
1045 inner: &[S],
1046 sink: &mut ErrorSink,
1047) -> Result<Vec<FunctionalGroupEnvelope>, EdifactError> {
1048 let mut groups: Vec<FunctionalGroupEnvelope> = Vec::new();
1049 let mut seen_group_refs: HashSet<String> = HashSet::new();
1052 let mut seen_message_refs: HashSet<String> = HashSet::new();
1053 let mut i = 0;
1054
1055 while i < inner.len() {
1056 let seg = &inner[i];
1057 match seg.tag() {
1058 "UNG" => {
1059 let ung_idx = i;
1060 let une_idx = find_matching_une(inner, ung_idx + 1)?;
1061
1062 let ung = &inner[ung_idx];
1063 let group_id = ung.component(0, 0).unwrap_or("").to_owned();
1064 let app_sender = ung.component(1, 0).unwrap_or("").to_owned();
1065 let app_sender_qualifier = ung.component(1, 1).unwrap_or("").to_owned();
1066 let app_recipient = ung.component(2, 0).unwrap_or("").to_owned();
1067 let app_recipient_qualifier = ung.component(2, 1).unwrap_or("").to_owned();
1068 let date = ung.component(3, 0).unwrap_or("").to_owned();
1069 let time_raw = ung.component(3, 1).unwrap_or("");
1070 let time = if time_raw.is_empty() {
1071 None
1072 } else {
1073 Some(time_raw.to_owned())
1074 };
1075 let group_ref = sink.required(ung, 4, 0);
1077 if !seen_group_refs.insert(group_ref.clone()) {
1078 sink.push(EdifactError::DuplicateReference {
1079 tag: "UNG".to_owned(),
1080 reference: group_ref.clone(),
1081 span: ung.span(),
1082 });
1083 }
1084 let controlling_agency = ung.component(5, 0).unwrap_or("").to_owned();
1085 let version = ung.component(6, 0).unwrap_or("").to_owned();
1088 let release = ung.component(6, 1).unwrap_or("").to_owned();
1089
1090 let une = &inner[une_idx];
1091 let declared_str = sink.required(une, 0, 0);
1092 let declared_message_count: u32 = sink.recover(
1093 declared_str.parse().map_err(|_| EdifactError::InvalidText {
1094 offset: une.span().start,
1095 }),
1096 0,
1097 );
1098 let une_ref = sink.required(une, 1, 0);
1099 if une_ref != group_ref {
1100 sink.push(EdifactError::QualifierMismatch {
1101 tag: "UNE".to_owned(),
1102 actual: une_ref,
1103 expected: group_ref.clone(),
1104 span: une.span(),
1105 });
1106 }
1107
1108 let group_content = &inner[ung_idx + 1..une_idx];
1109 let messages = extract_messages_flat(group_content, sink, &mut seen_message_refs)?;
1110 let actual_message_count = sink.recover(
1111 u32::try_from(messages.len()).map_err(|_| EdifactError::InterchangeTooLarge {
1112 count: messages.len() as u64,
1113 }),
1114 u32::MAX,
1115 );
1116
1117 if declared_message_count != actual_message_count {
1118 sink.push(EdifactError::MessageCountMismatch {
1119 expected: declared_message_count,
1120 actual: actual_message_count,
1121 });
1122 }
1123
1124 groups.push(FunctionalGroupEnvelope {
1125 group_id,
1126 app_sender,
1127 app_sender_qualifier,
1128 app_recipient,
1129 app_recipient_qualifier,
1130 date,
1131 time,
1132 group_ref,
1133 controlling_agency,
1134 version,
1135 release,
1136 declared_message_count,
1137 actual_message_count,
1138 messages,
1139 });
1140 i = une_idx + 1;
1141 }
1142 "UNE" => {
1143 return Err(EdifactError::InvalidSegmentForMessage {
1144 tag: "UNE".to_owned(),
1145 message_type: "ENVELOPE".to_owned(),
1146 span: seg.span(),
1147 });
1148 }
1149 "UNH" => {
1150 return Err(EdifactError::InvalidSegmentForMessage {
1152 tag: "UNH".to_owned(),
1153 message_type: "ENVELOPE".to_owned(),
1154 span: seg.span(),
1155 });
1156 }
1157 _ => {
1158 return Err(EdifactError::InvalidSegmentForMessage {
1159 tag: seg.tag().to_owned(),
1160 message_type: "ENVELOPE".to_owned(),
1161 span: seg.span(),
1162 });
1163 }
1164 }
1165 }
1166
1167 Ok(groups)
1168}
1169
1170fn extract_messages_flat<S: SegmentReader>(
1172 segments: &[S],
1173 sink: &mut ErrorSink,
1174 seen_refs: &mut HashSet<String>,
1175) -> Result<Vec<MessageEnvelope>, EdifactError> {
1176 let mut messages: Vec<MessageEnvelope> = Vec::new();
1177 let mut unh_idx: Option<usize> = None;
1181
1182 for (i, seg) in segments.iter().enumerate() {
1183 match seg.tag() {
1184 "UNH" => {
1185 if unh_idx.is_some() {
1186 return Err(EdifactError::InvalidSegmentForMessage {
1187 tag: "UNH".to_owned(),
1188 message_type: "ENVELOPE".to_owned(),
1189 span: seg.span(),
1190 });
1191 }
1192 unh_idx = Some(i);
1193 }
1194 "UNT" if unh_idx.is_some() => {
1195 let msg_start_idx = unh_idx.take().expect("guarded by the match arm");
1196 let unh = &segments[msg_start_idx];
1197
1198 let message_ref = sink.required(unh, 0, 0);
1199 if !seen_refs.insert(message_ref.clone()) {
1200 sink.push(EdifactError::DuplicateReference {
1201 tag: "UNH".to_owned(),
1202 reference: message_ref.clone(),
1203 span: unh.span(),
1204 });
1205 }
1206 let message_type = sink.required(unh, 1, 0);
1207 let version = sink.required(unh, 1, 1);
1208 let release = sink.required(unh, 1, 2);
1209 let controlling_agency = sink.required(unh, 1, 3);
1210 let association_code = unh.component(1, 4).unwrap_or("").to_owned();
1211 let common_access_ref = unh
1213 .component(2, 0)
1214 .filter(|s| !s.is_empty())
1215 .map(str::to_owned);
1216 let sequence_of_transfers = unh
1219 .component(3, 0)
1220 .filter(|s| !s.is_empty())
1221 .and_then(|s| s.parse::<u32>().ok());
1222 let transfer_position = unh
1223 .component(3, 1)
1224 .filter(|s| !s.is_empty())
1225 .map(str::to_owned);
1226
1227 let declared_raw = sink.required(seg, 0, 0);
1228 let declared_segment_count: u32 = sink.recover(
1229 declared_raw.parse().map_err(|_| EdifactError::InvalidText {
1230 offset: seg.span().start,
1231 }),
1232 0,
1233 );
1234 let unt_ref = sink.required(seg, 1, 0);
1235 if unt_ref != message_ref {
1236 sink.push(EdifactError::QualifierMismatch {
1237 tag: "UNT".to_owned(),
1238 actual: unt_ref,
1239 expected: message_ref.clone(),
1240 span: seg.span(),
1241 });
1242 }
1243
1244 let segment_span = i - msg_start_idx + 1;
1245 let actual_segment_count = sink.recover(
1246 u32::try_from(segment_span).map_err(|_| EdifactError::InterchangeTooLarge {
1247 count: segment_span as u64,
1248 }),
1249 u32::MAX,
1250 );
1251
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 unh_idx.is_some() => {
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 unh_idx.is_none() => {
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 unh_idx.is_some() {
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+++++AGREEMENT-1'\
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("AGREEMENT-1")
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}