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>,
130 pub recipient_id: String,
132 pub recipient_qualifier: String,
136 pub recipient_routing_address: Option<String>,
140 pub date: String,
142 pub time: Option<String>,
146 pub control_ref: String,
148 pub recipient_password: Option<String>,
153 pub recipient_password_qualifier: Option<String>,
158 pub app_ref: Option<String>,
162 pub processing_priority: Option<String>,
167 pub acknowledgement_request: bool,
172 pub communications_agreement_id: Option<String>,
176 pub test_indicator: bool,
182 pub declared_unit_count: u32,
190 pub actual_unit_count: u32,
192}
193
194impl InterchangeEnvelope {
195 #[inline]
217 #[must_use]
218 pub fn is_test(&self) -> bool {
219 self.test_indicator
220 }
221
222 #[inline]
226 #[must_use]
227 pub fn ack_requested(&self) -> bool {
228 self.acknowledgement_request
229 }
230}
231
232impl std::fmt::Display for InterchangeEnvelope {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 write!(
235 f,
236 "{sender} -> {recipient} [{ctrl}] ({syntax}:{ver})",
237 sender = self.sender_id,
238 recipient = self.recipient_id,
239 ctrl = self.control_ref,
240 syntax = self.syntax_identifier,
241 ver = self.syntax_version,
242 )
243 }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Hash)]
248#[non_exhaustive]
249#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
250pub struct MessageEnvelope {
251 pub message_ref: String,
253 pub message_type: String,
255 pub version: String,
257 pub release: String,
259 pub controlling_agency: String,
261 pub association_code: String,
263 pub common_access_ref: Option<String>,
270 pub sequence_of_transfers: Option<u32>,
276 pub transfer_position: Option<String>,
281 pub declared_segment_count: u32,
283 pub actual_segment_count: u32,
285}
286
287impl std::fmt::Display for MessageEnvelope {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289 write!(
290 f,
291 "{msg_type}:{ver}:{rel} ref={msg_ref} seg={actual}/{declared}",
292 msg_type = self.message_type,
293 ver = self.version,
294 rel = self.release,
295 msg_ref = self.message_ref,
296 actual = self.actual_segment_count,
297 declared = self.declared_segment_count,
298 )
299 }
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Hash)]
308#[non_exhaustive]
309#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
310pub struct FunctionalGroupEnvelope {
311 pub group_id: String,
313 pub app_sender: String,
315 pub app_sender_qualifier: String,
319 pub app_recipient: String,
321 pub app_recipient_qualifier: String,
325 pub date: String,
327 pub time: Option<String>,
329 pub group_ref: String,
331 pub controlling_agency: String,
333 pub version: String,
335 pub release: String,
337 pub declared_message_count: u32,
339 pub actual_message_count: u32,
341 pub messages: Vec<MessageEnvelope>,
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Hash)]
351#[non_exhaustive]
352#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
353pub struct ValidatedInterchange {
354 pub interchange: InterchangeEnvelope,
356 pub functional_groups: Vec<FunctionalGroupEnvelope>,
361 pub messages: Vec<MessageEnvelope>,
366}
367
368impl ValidatedInterchange {
369 #[inline]
371 #[must_use]
372 pub fn has_functional_groups(&self) -> bool {
373 !self.functional_groups.is_empty()
374 }
375
376 #[inline]
380 #[must_use]
381 pub fn message_count(&self) -> usize {
382 self.messages.len()
383 }
384
385 #[inline]
390 pub fn iter_messages(&self) -> impl Iterator<Item = &MessageEnvelope> {
391 self.messages.iter()
392 }
393
394 #[inline]
401 #[must_use]
402 pub fn find_message(&self, reference: &str) -> Option<&MessageEnvelope> {
403 self.messages.iter().find(|m| m.message_ref == reference)
404 }
405
406 #[must_use]
415 pub fn messages_by_type(&self, message_type: &str) -> Vec<&MessageEnvelope> {
416 self.messages
417 .iter()
418 .filter(|m| m.message_type == message_type)
419 .collect()
420 }
421
422 #[inline]
433 pub fn iter_messages_by_type<'s, 'q: 's>(
434 &'s self,
435 message_type: &'q str,
436 ) -> impl Iterator<Item = &'s MessageEnvelope> + 's {
437 self.messages
438 .iter()
439 .filter(move |m| m.message_type == message_type)
440 }
441}
442
443impl std::fmt::Display for ValidatedInterchange {
444 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445 write!(
446 f,
447 "{ic} messages={n}",
448 ic = self.interchange,
449 n = self.messages.len(),
450 )
451 }
452}
453
454impl std::fmt::Display for FunctionalGroupEnvelope {
455 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456 write!(
457 f,
458 "{gid} sender={sender} recipient={recip} [{gref}] ({agency}) msgs={actual}/{declared}",
459 gid = self.group_id,
460 sender = self.app_sender,
461 recip = self.app_recipient,
462 gref = self.group_ref,
463 agency = self.controlling_agency,
464 actual = self.actual_message_count,
465 declared = self.declared_message_count,
466 )
467 }
468}
469
470#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
475#[non_exhaustive]
476#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
477pub struct MessageIdentifier<'a> {
478 pub message_ref: &'a str,
480 pub message_type: &'a str,
481 pub version: &'a str,
482 pub release: &'a str,
483 pub controlling_agency: &'a str,
484 pub association_code: &'a str,
488}
489
490pub fn parse_unh<'a>(unh: &'a Segment<'a>) -> Result<MessageIdentifier<'a>, EdifactError> {
494 let message_ref = unh
496 .get_element(0)
497 .and_then(|e| e.get_component(0))
498 .filter(|s| !s.is_empty())
499 .ok_or_else(|| EdifactError::MissingRequiredComponent {
500 tag: "UNH".to_owned(),
501 element_index: 0,
502 component_index: 0,
503 })?;
504 let elem = unh
506 .get_element(1)
507 .ok_or_else(|| EdifactError::MissingRequiredElement {
508 tag: "UNH".to_owned(),
509 element_index: 1,
510 })?;
511 let message_type =
512 elem.get_component(0)
513 .ok_or_else(|| EdifactError::MissingRequiredComponent {
514 tag: "UNH".to_owned(),
515 element_index: 1,
516 component_index: 0,
517 })?;
518 Ok(MessageIdentifier {
519 message_ref,
520 message_type,
521 version: elem.get_component(1).unwrap_or(""),
522 release: elem.get_component(2).unwrap_or(""),
523 controlling_agency: elem.get_component(3).unwrap_or(""),
524 association_code: elem.get_component(4).unwrap_or(""),
525 })
526}
527
528#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
546#[non_exhaustive]
547#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
548pub struct GroupIdentifier<'a> {
549 pub group_id: &'a str,
551 pub app_sender: &'a str,
553 pub app_sender_qualifier: &'a str,
555 pub app_recipient: &'a str,
557 pub app_recipient_qualifier: &'a str,
559 pub group_ref: &'a str,
561 pub controlling_agency: &'a str,
563 pub version: &'a str,
565 pub release: &'a str,
567}
568
569pub fn parse_ung<'a>(ung: &'a Segment<'a>) -> Result<GroupIdentifier<'a>, EdifactError> {
574 let group_id = ung
575 .get_element(0)
576 .and_then(|e| e.get_component(0))
577 .unwrap_or("");
578 let app_sender = ung
579 .get_element(1)
580 .and_then(|e| e.get_component(0))
581 .unwrap_or("");
582 let app_sender_qualifier = ung
583 .get_element(1)
584 .and_then(|e| e.get_component(1))
585 .unwrap_or("");
586 let app_recipient = ung
587 .get_element(2)
588 .and_then(|e| e.get_component(0))
589 .unwrap_or("");
590 let app_recipient_qualifier = ung
591 .get_element(2)
592 .and_then(|e| e.get_component(1))
593 .unwrap_or("");
594 let group_ref = ung
595 .get_element(4)
596 .and_then(|e| e.get_component(0))
597 .ok_or_else(|| EdifactError::MissingRequiredComponent {
598 tag: "UNG".to_owned(),
599 element_index: 4,
600 component_index: 0,
601 })?;
602 let controlling_agency = ung
603 .get_element(5)
604 .and_then(|e| e.get_component(0))
605 .unwrap_or("");
606 let s008 = ung.get_element(6);
607 let version = s008.as_ref().and_then(|e| e.get_component(0)).unwrap_or("");
608 let release = s008.as_ref().and_then(|e| e.get_component(1)).unwrap_or("");
609 Ok(GroupIdentifier {
610 group_id,
611 app_sender,
612 app_sender_qualifier,
613 app_recipient,
614 app_recipient_qualifier,
615 group_ref,
616 controlling_agency,
617 version,
618 release,
619 })
620}
621
622pub fn validate_envelope(segments: &[Segment<'_>]) -> Result<ValidatedInterchange, EdifactError> {
627 validate_envelope_impl(segments)
628}
629
630pub fn validate_envelope_from_owned(
632 segments: &[OwnedSegment],
633) -> Result<ValidatedInterchange, EdifactError> {
634 validate_envelope_impl(segments)
635}
636
637#[derive(Debug)]
650#[non_exhaustive]
651pub struct LenientResult {
652 pub interchange: Option<ValidatedInterchange>,
654 pub errors: Vec<EdifactError>,
656}
657
658impl LenientResult {
659 #[inline]
661 #[must_use]
662 pub fn is_valid(&self) -> bool {
663 self.errors.is_empty()
664 }
665
666 #[inline]
671 #[must_use]
672 pub fn has_errors(&self) -> bool {
673 !self.errors.is_empty()
674 }
675
676 pub fn into_strict(self) -> Result<ValidatedInterchange, Vec<EdifactError>> {
686 match self.interchange {
687 Some(interchange) if self.errors.is_empty() => Ok(interchange),
688 _ => Err(self.errors),
689 }
690 }
691}
692
693pub fn validate_envelope_lenient(segments: &[Segment<'_>]) -> LenientResult {
704 validate_envelope_lenient_impl(segments)
705}
706
707pub fn validate_envelope_lenient_from_owned(segments: &[OwnedSegment]) -> LenientResult {
711 validate_envelope_lenient_impl(segments)
712}
713
714#[derive(Default)]
731struct ErrorSink {
732 errors: Vec<EdifactError>,
733}
734
735impl ErrorSink {
736 #[inline]
737 fn push(&mut self, error: EdifactError) {
738 self.errors.push(error);
739 }
740
741 #[inline]
743 fn recover<T>(&mut self, result: Result<T, EdifactError>, fallback: T) -> T {
744 match result {
745 Ok(value) => value,
746 Err(error) => {
747 self.errors.push(error);
748 fallback
749 }
750 }
751 }
752
753 #[inline]
755 fn required<S: SegmentReader>(&mut self, seg: &S, element: usize, component: usize) -> String {
756 self.recover(
757 seg.required_component_field(element, component)
758 .map(str::to_owned),
759 String::new(),
760 )
761 }
762}
763
764fn validate_envelope_collecting<S: SegmentReader>(
769 segments: &[S],
770) -> (Option<ValidatedInterchange>, Vec<EdifactError>) {
771 let mut sink = ErrorSink::default();
772
773 let mut interchange_env = match extract_interchange(segments, &mut sink) {
774 Ok(env) => env,
775 Err(fatal) => {
776 sink.push(fatal);
777 return (None, sink.errors);
778 }
779 };
780
781 let inner = if segments.len() >= 2 {
782 &segments[1..segments.len() - 1]
783 } else {
784 &[]
785 };
786
787 let (functional_groups, messages) = match extract_content(inner, &mut sink) {
788 Ok(pair) => pair,
789 Err(fatal) => {
790 sink.push(fatal);
791 return (None, sink.errors);
792 }
793 };
794
795 let actual_unit_count = if functional_groups.is_empty() {
799 messages.len()
800 } else {
801 functional_groups.len()
802 };
803 interchange_env.actual_unit_count = sink.recover(
804 u32::try_from(actual_unit_count).map_err(|_| EdifactError::InterchangeTooLarge {
805 count: actual_unit_count as u64,
806 }),
807 u32::MAX,
808 );
809
810 if interchange_env.declared_unit_count != interchange_env.actual_unit_count {
811 sink.push(EdifactError::MessageCountMismatch {
812 expected: interchange_env.declared_unit_count,
813 actual: interchange_env.actual_unit_count,
814 });
815 }
816
817 for msg in &messages {
818 if msg.declared_segment_count != msg.actual_segment_count {
819 sink.push(EdifactError::SegmentCountMismatch {
820 expected: msg.declared_segment_count,
821 actual: msg.actual_segment_count,
822 message_ref: msg.message_ref.clone(),
823 });
824 }
825 }
826
827 (
828 Some(ValidatedInterchange {
829 interchange: interchange_env,
830 functional_groups,
831 messages,
832 }),
833 sink.errors,
834 )
835}
836
837fn validate_envelope_impl<S: SegmentReader>(
838 segments: &[S],
839) -> Result<ValidatedInterchange, EdifactError> {
840 match validate_envelope_collecting(segments) {
841 (Some(result), errors) if errors.is_empty() => Ok(result),
842 (_, mut errors) => Err(errors
843 .drain(..)
844 .next()
845 .unwrap_or(EdifactError::MissingSegment {
846 tag: "UNB".to_owned(),
847 expected_position: "first segment of interchange".to_owned(),
848 })),
849 }
850}
851
852fn validate_envelope_lenient_impl<S: SegmentReader>(segments: &[S]) -> LenientResult {
853 let (interchange, errors) = validate_envelope_collecting(segments);
854 LenientResult {
855 interchange,
856 errors,
857 }
858}
859
860fn extract_interchange<S: SegmentReader>(
863 segments: &[S],
864 sink: &mut ErrorSink,
865) -> Result<InterchangeEnvelope, EdifactError> {
866 if segments.first().map(|s| s.tag()) != Some("UNB") {
867 return Err(EdifactError::MissingSegment {
868 tag: "UNB".to_owned(),
869 expected_position: "first segment of interchange".to_owned(),
870 });
871 }
872 if segments.last().map(|s| s.tag()) != Some("UNZ") {
873 return Err(EdifactError::MissingSegment {
874 tag: "UNZ".to_owned(),
875 expected_position: "last segment of interchange".to_owned(),
876 });
877 }
878
879 let unb = &segments[0];
880 let unz = &segments[segments.len() - 1];
881
882 let syntax_identifier = sink.required(unb, 0, 0);
883 let syntax_version = unb.component(0, 1).unwrap_or("").to_owned();
884
885 const VALID_SYNTAX_IDS: &[&str] = &["UNOA", "UNOB", "UNOC", "UNOD", "UNOE", "UNOF", "KECA"];
887 if !VALID_SYNTAX_IDS.contains(&syntax_identifier.as_str()) {
888 sink.push(EdifactError::UnrecognisedSyntaxIdentifier(
889 syntax_identifier.clone(),
890 ));
891 }
892
893 let sender_id = sink.required(unb, 1, 0);
894 let sender_qualifier = unb.component(1, 1).unwrap_or("").to_owned();
895 let sender_routing_address = unb
897 .component(1, 2)
898 .filter(|s| !s.is_empty())
899 .map(str::to_owned);
900
901 let recipient_id = sink.required(unb, 2, 0);
902 let recipient_qualifier = unb.component(2, 1).unwrap_or("").to_owned();
903 let recipient_routing_address = unb
905 .component(2, 2)
906 .filter(|s| !s.is_empty())
907 .map(str::to_owned);
908
909 let date = sink.required(unb, 3, 0);
910 let time_raw = unb.component(3, 1).unwrap_or("");
911 let time = if time_raw.is_empty() {
912 None
913 } else {
914 Some(time_raw.to_owned())
915 };
916
917 let control_ref = sink.required(unb, 4, 0);
918
919 let recipient_password = unb
921 .component(5, 0)
922 .filter(|s| !s.is_empty())
923 .map(str::to_owned);
924 let recipient_password_qualifier = unb
925 .component(5, 1)
926 .filter(|s| !s.is_empty())
927 .map(str::to_owned);
928 let app_ref = unb
930 .component(6, 0)
931 .filter(|s| !s.is_empty())
932 .map(str::to_owned);
933 let processing_priority = unb
935 .component(7, 0)
936 .filter(|s| !s.is_empty())
937 .map(str::to_owned);
938 let acknowledgement_request = unb.component(8, 0) == Some("1");
940 let communications_agreement_id = unb
942 .component(9, 0)
943 .filter(|s| !s.is_empty())
944 .map(str::to_owned);
945 let test_indicator = unb.component(10, 0) == Some("1");
947
948 let unz_control_ref = sink.required(unz, 1, 0);
949 if unz_control_ref != control_ref {
950 sink.push(EdifactError::QualifierMismatch {
951 tag: "UNZ".to_owned(),
952 actual: unz_control_ref,
953 expected: control_ref.clone(),
954 span: unz.span(),
955 });
956 }
957
958 let declared_unit_count_raw = sink.required(unz, 0, 0);
959 let declared_unit_count: u32 = sink.recover(
960 declared_unit_count_raw
961 .parse()
962 .map_err(|_| EdifactError::InvalidText {
963 offset: unz.span().start,
964 }),
965 0,
966 );
967
968 Ok(InterchangeEnvelope {
969 syntax_identifier,
970 syntax_version,
971 sender_id,
972 sender_qualifier,
973 sender_routing_address,
974 recipient_id,
975 recipient_qualifier,
976 recipient_routing_address,
977 date,
978 time,
979 control_ref,
980 recipient_password,
981 recipient_password_qualifier,
982 app_ref,
983 processing_priority,
984 acknowledgement_request,
985 communications_agreement_id,
986 test_indicator,
987 declared_unit_count,
988 actual_unit_count: 0,
989 })
990}
991
992fn extract_content<S: SegmentReader>(
995 inner: &[S],
996 sink: &mut ErrorSink,
997) -> Result<(Vec<FunctionalGroupEnvelope>, Vec<MessageEnvelope>), EdifactError> {
998 if inner.first().is_some_and(|s| s.tag() == "UNG") {
1002 let groups = extract_with_groups(inner, sink)?;
1003 let messages = groups
1004 .iter()
1005 .flat_map(|g| g.messages.iter().cloned())
1006 .collect();
1007 Ok((groups, messages))
1008 } else {
1009 let mut seen_refs = HashSet::new();
1010 let messages = extract_messages_flat(inner, sink, &mut seen_refs)?;
1011 Ok((vec![], messages))
1012 }
1013}
1014
1015fn find_matching_une<S: SegmentReader>(
1017 segments: &[S],
1018 start: usize,
1019) -> Result<usize, EdifactError> {
1020 for (offset, seg) in segments[start..].iter().enumerate() {
1021 match seg.tag() {
1022 "UNE" => return Ok(start + offset),
1023 "UNG" => {
1024 return Err(EdifactError::InvalidSegmentForMessage {
1025 tag: "UNG".to_owned(),
1026 message_type: "ENVELOPE".to_owned(),
1027 span: seg.span(),
1028 });
1029 }
1030 _ => {}
1031 }
1032 }
1033 Err(EdifactError::MissingSegment {
1034 tag: "UNE".to_owned(),
1035 expected_position: "end of functional group".to_owned(),
1036 })
1037}
1038
1039fn extract_with_groups<S: SegmentReader>(
1040 inner: &[S],
1041 sink: &mut ErrorSink,
1042) -> Result<Vec<FunctionalGroupEnvelope>, EdifactError> {
1043 let mut groups: Vec<FunctionalGroupEnvelope> = Vec::new();
1044 let mut seen_group_refs: HashSet<String> = HashSet::new();
1047 let mut seen_message_refs: HashSet<String> = HashSet::new();
1048 let mut i = 0;
1049
1050 while i < inner.len() {
1051 let seg = &inner[i];
1052 match seg.tag() {
1053 "UNG" => {
1054 let ung_idx = i;
1055 let une_idx = find_matching_une(inner, ung_idx + 1)?;
1056
1057 let ung = &inner[ung_idx];
1058 let group_id = ung.component(0, 0).unwrap_or("").to_owned();
1059 let app_sender = ung.component(1, 0).unwrap_or("").to_owned();
1060 let app_sender_qualifier = ung.component(1, 1).unwrap_or("").to_owned();
1061 let app_recipient = ung.component(2, 0).unwrap_or("").to_owned();
1062 let app_recipient_qualifier = ung.component(2, 1).unwrap_or("").to_owned();
1063 let date = ung.component(3, 0).unwrap_or("").to_owned();
1064 let time_raw = ung.component(3, 1).unwrap_or("");
1065 let time = if time_raw.is_empty() {
1066 None
1067 } else {
1068 Some(time_raw.to_owned())
1069 };
1070 let group_ref = sink.required(ung, 4, 0);
1072 if !seen_group_refs.insert(group_ref.clone()) {
1073 sink.push(EdifactError::DuplicateReference {
1074 tag: "UNG".to_owned(),
1075 reference: group_ref.clone(),
1076 span: ung.span(),
1077 });
1078 }
1079 let controlling_agency = ung.component(5, 0).unwrap_or("").to_owned();
1080 let version = ung.component(6, 0).unwrap_or("").to_owned();
1083 let release = ung.component(6, 1).unwrap_or("").to_owned();
1084
1085 let une = &inner[une_idx];
1086 let declared_str = sink.required(une, 0, 0);
1087 let declared_message_count: u32 = sink.recover(
1088 declared_str.parse().map_err(|_| EdifactError::InvalidText {
1089 offset: une.span().start,
1090 }),
1091 0,
1092 );
1093 let une_ref = sink.required(une, 1, 0);
1094 if une_ref != group_ref {
1095 sink.push(EdifactError::QualifierMismatch {
1096 tag: "UNE".to_owned(),
1097 actual: une_ref,
1098 expected: group_ref.clone(),
1099 span: une.span(),
1100 });
1101 }
1102
1103 let group_content = &inner[ung_idx + 1..une_idx];
1104 let messages = extract_messages_flat(group_content, sink, &mut seen_message_refs)?;
1105 let actual_message_count = sink.recover(
1106 u32::try_from(messages.len()).map_err(|_| EdifactError::InterchangeTooLarge {
1107 count: messages.len() as u64,
1108 }),
1109 u32::MAX,
1110 );
1111
1112 if declared_message_count != actual_message_count {
1113 sink.push(EdifactError::MessageCountMismatch {
1114 expected: declared_message_count,
1115 actual: actual_message_count,
1116 });
1117 }
1118
1119 groups.push(FunctionalGroupEnvelope {
1120 group_id,
1121 app_sender,
1122 app_sender_qualifier,
1123 app_recipient,
1124 app_recipient_qualifier,
1125 date,
1126 time,
1127 group_ref,
1128 controlling_agency,
1129 version,
1130 release,
1131 declared_message_count,
1132 actual_message_count,
1133 messages,
1134 });
1135 i = une_idx + 1;
1136 }
1137 "UNE" => {
1138 return Err(EdifactError::InvalidSegmentForMessage {
1139 tag: "UNE".to_owned(),
1140 message_type: "ENVELOPE".to_owned(),
1141 span: seg.span(),
1142 });
1143 }
1144 "UNH" => {
1145 return Err(EdifactError::InvalidSegmentForMessage {
1147 tag: "UNH".to_owned(),
1148 message_type: "ENVELOPE".to_owned(),
1149 span: seg.span(),
1150 });
1151 }
1152 _ => {
1153 return Err(EdifactError::InvalidSegmentForMessage {
1154 tag: seg.tag().to_owned(),
1155 message_type: "ENVELOPE".to_owned(),
1156 span: seg.span(),
1157 });
1158 }
1159 }
1160 }
1161
1162 Ok(groups)
1163}
1164
1165fn extract_messages_flat<S: SegmentReader>(
1167 segments: &[S],
1168 sink: &mut ErrorSink,
1169 seen_refs: &mut HashSet<String>,
1170) -> Result<Vec<MessageEnvelope>, EdifactError> {
1171 let mut messages: Vec<MessageEnvelope> = Vec::new();
1172 let mut unh_idx: Option<usize> = None;
1176
1177 for (i, seg) in segments.iter().enumerate() {
1178 match seg.tag() {
1179 "UNH" => {
1180 if unh_idx.is_some() {
1181 return Err(EdifactError::InvalidSegmentForMessage {
1182 tag: "UNH".to_owned(),
1183 message_type: "ENVELOPE".to_owned(),
1184 span: seg.span(),
1185 });
1186 }
1187 unh_idx = Some(i);
1188 }
1189 "UNT" if unh_idx.is_some() => {
1190 let msg_start_idx = unh_idx.take().expect("guarded by the match arm");
1191 let unh = &segments[msg_start_idx];
1192
1193 let message_ref = sink.required(unh, 0, 0);
1194 if !seen_refs.insert(message_ref.clone()) {
1195 sink.push(EdifactError::DuplicateReference {
1196 tag: "UNH".to_owned(),
1197 reference: message_ref.clone(),
1198 span: unh.span(),
1199 });
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 span: seg.span(),
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 messages.push(MessageEnvelope {
1248 message_ref,
1249 message_type,
1250 version,
1251 release,
1252 controlling_agency,
1253 association_code,
1254 common_access_ref,
1255 sequence_of_transfers,
1256 transfer_position,
1257 declared_segment_count,
1258 actual_segment_count,
1259 });
1260 }
1261 "UNT" => {
1262 return Err(EdifactError::InvalidSegmentForMessage {
1263 tag: "UNT".to_owned(),
1264 message_type: "ENVELOPE".to_owned(),
1265 span: seg.span(),
1266 });
1267 }
1268 "UNB" | "UNZ" | "UNG" | "UNE" if unh_idx.is_some() => {
1269 return Err(EdifactError::InvalidSegmentForMessage {
1270 tag: seg.tag().to_owned(),
1271 message_type: "ENVELOPE".to_owned(),
1272 span: seg.span(),
1273 });
1274 }
1275 _ if unh_idx.is_none() => {
1276 return Err(EdifactError::InvalidSegmentForMessage {
1277 tag: seg.tag().to_owned(),
1278 message_type: "ENVELOPE".to_owned(),
1279 span: seg.span(),
1280 });
1281 }
1282 _ => {}
1283 }
1284 }
1285
1286 if unh_idx.is_some() {
1287 return Err(EdifactError::MissingSegment {
1288 tag: "UNT".to_owned(),
1289 expected_position: "end of message group".to_owned(),
1290 });
1291 }
1292
1293 Ok(messages)
1294}
1295
1296#[cfg(test)]
1299mod tests {
1300 use super::*;
1301
1302 fn parse(input: &[u8]) -> Vec<crate::OwnedSegment> {
1303 crate::from_reader_collect(std::io::Cursor::new(input)).expect("parse failed")
1304 }
1305
1306 fn parse_and_validate(input: &[u8]) -> Result<ValidatedInterchange, EdifactError> {
1307 let owned = parse(input);
1308 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1309 validate_envelope(&segs)
1310 }
1311
1312 fn parse_and_validate_lenient(input: &[u8]) -> LenientResult {
1313 let owned = parse(input);
1314 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1315 validate_envelope_lenient(&segs)
1316 }
1317
1318 #[test]
1319 fn lenient_collects_every_violation_not_just_the_first() {
1320 let input = b"UNB+UNOA:1+S+R+200101:0900+CTRL1'\
1324 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+99+1'\
1325 UNZ+1+CTRL2'";
1326 let result = parse_and_validate_lenient(input);
1327 assert!(
1328 result.errors.len() >= 2,
1329 "expected both violations, got {:?}",
1330 result.errors
1331 );
1332 assert!(
1333 result
1334 .errors
1335 .iter()
1336 .any(|e| matches!(e, EdifactError::QualifierMismatch { tag, .. } if tag == "UNZ")),
1337 "missing UNZ control-ref mismatch in {:?}",
1338 result.errors
1339 );
1340 assert!(
1341 result
1342 .errors
1343 .iter()
1344 .any(|e| matches!(e, EdifactError::SegmentCountMismatch { .. })),
1345 "missing UNT segment-count mismatch in {:?}",
1346 result.errors
1347 );
1348 }
1349
1350 #[test]
1351 fn strict_reports_the_first_error_lenient_collects() {
1352 for input in [
1355 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+2+1'UNZ+9+C1'"[..],
1356 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+99+1'UNZ+1+C1'"[..],
1357 &b"UNB+UNOA:1+S+R+200101:0900+C1'UNH+1+ORDERS:D:96A:UN'UNT+2+9'UNZ+1+C1'"[..],
1358 ] {
1359 let strict = parse_and_validate(input);
1360 let lenient = parse_and_validate_lenient(input);
1361 match strict {
1362 Err(e) => assert_eq!(
1363 Some(&e),
1364 lenient.errors.first(),
1365 "strict/lenient diverged for {:?}",
1366 std::str::from_utf8(input).unwrap()
1367 ),
1368 Ok(_) => assert!(lenient.errors.is_empty()),
1369 }
1370 }
1371 }
1372
1373 #[test]
1374 fn duplicate_message_references_are_rejected() {
1375 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'\
1376 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+3+1'\
1377 UNH+1+ORDERS:D:96A:UN'BGM+221'UNT+3+1'\
1378 UNZ+2+C1'";
1379 let err = parse_and_validate(input).expect_err("duplicate UNH refs must fail");
1380 assert!(
1381 matches!(&err, EdifactError::DuplicateReference { tag, reference, .. }
1382 if tag == "UNH" && reference == "1"),
1383 "got {err:?}"
1384 );
1385 }
1386
1387 #[test]
1388 fn distinct_message_references_are_accepted() {
1389 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'\
1390 UNH+1+ORDERS:D:96A:UN'BGM+220'UNT+3+1'\
1391 UNH+2+ORDERS:D:96A:UN'BGM+221'UNT+3+2'\
1392 UNZ+2+C1'";
1393 parse_and_validate(input).expect("distinct refs must validate");
1394 }
1395
1396 #[test]
1397 fn into_strict_is_total_after_the_caller_filters_errors() {
1398 let input = b"UNB+UNOA:1+S+R+200101:0900+C1'UNZ+1+C2'";
1401 let mut lenient = parse_and_validate_lenient(input);
1402 lenient.errors.clear();
1403 let _ = lenient.into_strict();
1405 }
1406
1407 fn parse_and_validate_owned(input: &[u8]) -> Result<ValidatedInterchange, EdifactError> {
1408 validate_envelope_from_owned(&parse(input))
1409 }
1410
1411 const VALID_INTERCHANGE: &[u8] =
1412 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'";
1413
1414 #[test]
1415 fn valid_envelope_parses_ok() {
1416 let result = parse_and_validate(VALID_INTERCHANGE).expect("envelope should be valid");
1417 assert_eq!(result.interchange.sender_id, "SENDER");
1418 assert_eq!(result.interchange.sender_qualifier, ""); assert_eq!(result.interchange.recipient_id, "RECEIVER");
1420 assert_eq!(result.interchange.recipient_qualifier, "");
1421 assert_eq!(result.interchange.syntax_identifier, "UNOA");
1422 assert_eq!(result.interchange.syntax_version, "3");
1423 assert_eq!(result.interchange.control_ref, "00001");
1424 assert_eq!(result.interchange.declared_unit_count, 1);
1425 assert_eq!(result.interchange.actual_unit_count, 1);
1426 assert!(!result.interchange.is_test());
1427 assert!(!result.has_functional_groups());
1428 assert_eq!(result.message_count(), 1);
1429 assert_eq!(result.messages[0].message_type, "ORDERS");
1430 assert_eq!(result.messages[0].association_code, "EAN010");
1431 assert_eq!(result.messages[0].declared_segment_count, 4);
1432 assert_eq!(result.messages[0].actual_segment_count, 4);
1433 }
1434
1435 #[test]
1436 fn valid_envelope_parses_ok_owned_path() {
1437 let result = parse_and_validate_owned(VALID_INTERCHANGE).expect("envelope should be valid");
1438 assert_eq!(result.interchange.sender_id, "SENDER");
1439 assert_eq!(result.interchange.actual_unit_count, 1);
1440 assert_eq!(result.messages[0].declared_segment_count, 4);
1441 }
1442
1443 #[test]
1444 fn unt_count_mismatch_returns_err() {
1445 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'";
1446 let result = parse_and_validate(input);
1447 assert!(
1448 matches!(
1449 result,
1450 Err(EdifactError::SegmentCountMismatch { expected: 99, .. })
1451 ),
1452 "expected SegmentCountMismatch, got {result:?}"
1453 );
1454 }
1455
1456 #[test]
1457 fn unz_count_mismatch_returns_err() {
1458 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'";
1459 let result = parse_and_validate(input);
1460 assert!(
1461 matches!(
1462 result,
1463 Err(EdifactError::MessageCountMismatch {
1464 expected: 2,
1465 actual: 1
1466 })
1467 ),
1468 "expected MessageCountMismatch(2,1), got {result:?}"
1469 );
1470 }
1471
1472 #[test]
1473 fn missing_unb_returns_err() {
1474 let input = b"UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
1475 assert!(parse_and_validate(input).is_err());
1476 }
1477
1478 #[test]
1479 fn extracts_una_interchange_correctly() {
1480 let result = parse_and_validate(VALID_INTERCHANGE).unwrap();
1481 assert_eq!(result.interchange.syntax_identifier, "UNOA");
1482 assert_eq!(result.interchange.syntax_version, "3");
1483 assert_eq!(result.interchange.date, "230401");
1484 assert_eq!(result.interchange.time.as_deref(), Some("0900"));
1485 }
1486
1487 #[test]
1488 fn sender_and_recipient_qualifiers_extracted() {
1489 let input = b"UNB+UNOA:3+1234567890123:14+9876543210987:14+200101:0900+1'\
1491 UNH+1+ORDERS:D:96A:UN'\
1492 BGM+220+PO-001+9'\
1493 UNT+3+1'\
1494 UNZ+1+1'";
1495 let r = parse_and_validate(input).expect("GLN-qualified UNB must parse ok");
1496 assert_eq!(r.interchange.sender_id, "1234567890123");
1497 assert_eq!(r.interchange.sender_qualifier, "14");
1498 assert_eq!(r.interchange.recipient_id, "9876543210987");
1499 assert_eq!(r.interchange.recipient_qualifier, "14");
1500 }
1501
1502 #[test]
1506 fn test_indicator_parsed_from_unb() {
1507 let input = b"UNB+UNOA:3+S+R+200101:0900+1++++++1'\
1511 UNH+1+ORDERS:D:96A:UN'\
1512 BGM+220+PO-001+9'\
1513 UNT+3+1'\
1514 UNZ+1+1'";
1515 let r = parse_and_validate(input).expect("test-flagged UNB must parse ok");
1516 assert!(
1517 r.interchange.test_indicator,
1518 "test_indicator should be true"
1519 );
1520 assert!(r.interchange.is_test(), "is_test() convenience must agree");
1521 }
1522
1523 #[test]
1524 fn no_test_indicator_defaults_false() {
1525 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1526 assert!(!r.interchange.test_indicator);
1527 assert!(!r.interchange.is_test());
1528 }
1529
1530 #[test]
1531 fn app_ref_extracted_when_present() {
1532 let input = b"UNB+UNOA:3+S+R+200101:0900+1++MYAPP'\
1534 UNH+1+ORDERS:D:96A:UN'\
1535 BGM+220+PO-001+9'\
1536 UNT+3+1'\
1537 UNZ+1+1'";
1538 let r = parse_and_validate(input).expect("UNB with app_ref must parse ok");
1539 assert_eq!(r.interchange.app_ref.as_deref(), Some("MYAPP"));
1540 }
1541
1542 #[test]
1543 fn app_ref_is_none_when_absent() {
1544 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1545 assert!(r.interchange.app_ref.is_none());
1546 }
1547
1548 #[test]
1549 fn recipient_password_extracted_when_present() {
1550 let input = b"UNB+UNOA:3+S+R+200101:0900+1+MYPASS'\
1552 UNH+1+ORDERS:D:96A:UN'\
1553 BGM+220+PO-001+9'\
1554 UNT+3+1'\
1555 UNZ+1+1'";
1556 let r = parse_and_validate(input).expect("UNB with password must parse ok");
1557 assert_eq!(r.interchange.recipient_password.as_deref(), Some("MYPASS"));
1558 }
1559
1560 #[test]
1561 fn recipient_password_is_none_when_absent() {
1562 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1563 assert!(r.interchange.recipient_password.is_none());
1564 }
1565
1566 #[test]
1567 fn acknowledgement_request_flag_parsed() {
1568 let input = b"UNB+UNOA:3+S+R+200101:0900+1++++1'\
1571 UNH+1+ORDERS:D:96A:UN'\
1572 BGM+220+PO-001+9'\
1573 UNT+3+1'\
1574 UNZ+1+1'";
1575 let r = parse_and_validate(input).expect("UNB with ack-request must parse ok");
1576 assert!(r.interchange.acknowledgement_request);
1577 assert!(r.interchange.ack_requested());
1578 }
1579
1580 #[test]
1581 fn acknowledgement_request_defaults_false() {
1582 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1583 assert!(!r.interchange.acknowledgement_request);
1584 assert!(!r.interchange.ack_requested());
1585 }
1586
1587 #[test]
1588 fn communications_agreement_id_extracted() {
1589 let input = b"UNB+UNOA:3+S+R+200101:0900+1+++++AGREEMENT-1'\
1591 UNH+1+ORDERS:D:96A:UN'\
1592 BGM+220+PO-001+9'\
1593 UNT+3+1'\
1594 UNZ+1+1'";
1595 let r = parse_and_validate(input).expect("UNB with comms-agreement must parse ok");
1596 assert_eq!(
1597 r.interchange.communications_agreement_id.as_deref(),
1598 Some("AGREEMENT-1")
1599 );
1600 }
1601
1602 #[test]
1603 fn dangling_unh_without_unt_returns_err() {
1604 let input =
1605 b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNZ+1+1'";
1606 let result = parse_and_validate(input);
1607 assert!(
1608 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNT")
1609 );
1610 }
1611
1612 #[test]
1613 fn stray_segment_outside_message_returns_err() {
1614 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'";
1615 assert!(matches!(
1616 parse_and_validate(input),
1617 Err(EdifactError::InvalidSegmentForMessage { .. })
1618 ));
1619 }
1620
1621 #[test]
1622 fn missing_unb_sender_component_returns_err() {
1623 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'";
1624 let result = parse_and_validate(input);
1625 assert!(
1626 matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 0 }) if tag == "UNB"),
1627 "expected MissingRequiredComponent for empty sender, got: {result:?}"
1628 );
1629 }
1630
1631 #[test]
1632 fn nested_unh_without_closing_previous_message_returns_err() {
1633 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'";
1634 let result = parse_and_validate(input);
1635 assert!(
1636 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNH"),
1637 "expected InvalidSegmentForMessage(UNH), got {result:?}"
1638 );
1639 }
1640
1641 #[test]
1642 fn unt_message_reference_must_match_unh() {
1643 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'";
1644 let result = parse_and_validate(input);
1645 assert!(
1646 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNT")
1647 );
1648 }
1649
1650 #[test]
1651 fn unz_control_reference_must_match_unb() {
1652 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'";
1653 let result = parse_and_validate(input);
1654 assert!(
1655 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNZ")
1656 );
1657 }
1658
1659 #[test]
1660 fn missing_unh_message_type_components_return_err() {
1661 let input =
1662 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'";
1663 let result = parse_and_validate(input);
1664 assert!(
1665 matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 3 }) if tag == "UNH"),
1666 "got: {result:?}"
1667 );
1668 }
1669
1670 #[test]
1671 fn nested_unz_inside_message_returns_err() {
1672 let input =
1673 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'";
1674 let result = parse_and_validate(input);
1675 assert!(
1676 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNZ")
1677 );
1678 }
1679
1680 #[test]
1681 fn lenient_returns_partial_result_on_count_mismatch() {
1682 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'";
1685 let owned = parse(input);
1686 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1687 let lenient = validate_envelope_lenient(&segs);
1688 let result = lenient.interchange;
1689 let errors = lenient.errors;
1690 assert!(
1691 result.is_some(),
1692 "lenient mode must return Some even on count mismatch"
1693 );
1694 assert_eq!(errors.len(), 1);
1695 assert!(
1696 matches!(
1697 &errors[0],
1698 EdifactError::MessageCountMismatch {
1699 expected: 2,
1700 actual: 1
1701 }
1702 ),
1703 "expected MessageCountMismatch(2,1), got {:?}",
1704 errors[0]
1705 );
1706 let partial = result.unwrap();
1707 assert_eq!(partial.messages.len(), 1);
1708 assert_eq!(partial.interchange.actual_unit_count, 1);
1709 assert_eq!(partial.interchange.declared_unit_count, 2);
1710 }
1711
1712 #[test]
1713 fn lenient_returns_none_on_structural_error() {
1714 let input = b"UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
1716 let owned = parse(input);
1717 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
1718 let lenient = validate_envelope_lenient(&segs);
1719 let result = lenient.interchange;
1720 let errors = lenient.errors;
1721 assert!(result.is_none(), "missing UNB must yield None");
1722 assert!(!errors.is_empty());
1723 }
1724
1725 #[test]
1726 fn message_count_convenience_method() {
1727 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1728 assert_eq!(r.message_count(), r.messages.len());
1729 assert_eq!(r.message_count(), 1);
1730 }
1731
1732 #[test]
1733 fn interchange_with_single_functional_group_parses_ok() {
1734 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1735 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1736 UNH+1+ORDERS:D:96A:UN'\
1737 BGM+220+PO-001+9'\
1738 UNT+3+1'\
1739 UNE+1+1'\
1740 UNZ+1+1'";
1741 let result = parse_and_validate(input).expect("single-group interchange must parse ok");
1742 assert!(result.has_functional_groups());
1743 assert_eq!(result.functional_groups.len(), 1);
1744 let g = &result.functional_groups[0];
1745 assert_eq!(g.group_id, "ORDERS");
1746 assert_eq!(g.group_ref, "1");
1747 assert_eq!(g.controlling_agency, "UN");
1748 assert_eq!(g.declared_message_count, 1);
1749 assert_eq!(g.actual_message_count, 1);
1750 assert_eq!(result.messages.len(), 1);
1751 assert_eq!(result.messages[0].message_type, "ORDERS");
1752 assert_eq!(result.interchange.declared_unit_count, 1);
1753 assert_eq!(result.interchange.actual_unit_count, 1);
1754 }
1755
1756 #[test]
1757 fn interchange_with_multi_message_group_parses_ok() {
1758 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1761 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1762 UNH+1+ORDERS:D:96A:UN'\
1763 BGM+220+PO-001+9'\
1764 UNT+3+1'\
1765 UNH+2+ORDERS:D:96A:UN'\
1766 BGM+220+PO-002+9'\
1767 UNT+3+2'\
1768 UNE+2+1'\
1769 UNZ+1+1'";
1770 let result = parse_and_validate(input).expect("multi-message group must parse ok");
1771 assert!(result.has_functional_groups());
1772 assert_eq!(result.functional_groups.len(), 1);
1773 assert_eq!(result.functional_groups[0].actual_message_count, 2);
1774 assert_eq!(result.messages.len(), 2);
1775 assert_eq!(result.interchange.actual_unit_count, 1);
1777 assert_eq!(result.interchange.declared_unit_count, 1);
1778 }
1779
1780 #[test]
1781 fn interchange_with_multiple_groups_parses_ok() {
1782 let input = b"UNB+UNOA:3+S+R+200101:0900+2'\
1783 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1784 UNH+1+ORDERS:D:96A:UN'\
1785 BGM+220+PO-001+9'\
1786 UNT+3+1'\
1787 UNE+1+1'\
1788 UNG+INVOIC+S+R+200101:0900+2+UN+D:96A'\
1789 UNH+2+INVOIC:D:96A:UN'\
1790 BGM+380+INV-001+9'\
1791 UNT+3+2'\
1792 UNE+1+2'\
1793 UNZ+2+2'";
1794 let result = parse_and_validate(input).expect("multi-group interchange must parse ok");
1795 assert_eq!(result.functional_groups.len(), 2);
1796 assert_eq!(result.functional_groups[0].group_id, "ORDERS");
1797 assert_eq!(result.functional_groups[1].group_id, "INVOIC");
1798 assert_eq!(result.messages.len(), 2);
1799 assert_eq!(result.interchange.declared_unit_count, 2);
1800 assert_eq!(result.interchange.actual_unit_count, 2);
1801 }
1802
1803 #[test]
1804 fn ung_une_count_mismatch_returns_err() {
1805 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1806 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1807 UNH+1+ORDERS:D:96A:UN'\
1808 BGM+220+PO-001+9'\
1809 UNT+3+1'\
1810 UNE+2+1'\
1811 UNZ+1+1'";
1812 let result = parse_and_validate(input);
1813 assert!(
1814 matches!(
1815 result,
1816 Err(EdifactError::MessageCountMismatch {
1817 expected: 2,
1818 actual: 1
1819 })
1820 ),
1821 "expected MessageCountMismatch(2,1), got {result:?}"
1822 );
1823 }
1824
1825 #[test]
1826 fn une_without_ung_returns_err() {
1827 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1828 UNE+1+1'\
1829 UNH+1+ORDERS:D:96A:UN'\
1830 BGM+220+PO-001+9'\
1831 UNT+3+1'\
1832 UNZ+1+1'";
1833 let result = parse_and_validate(input);
1834 assert!(
1835 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNE"),
1836 "expected InvalidSegmentForMessage(UNE), got {result:?}"
1837 );
1838 }
1839
1840 #[test]
1841 fn ung_without_une_returns_err() {
1842 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1843 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1844 UNH+1+ORDERS:D:96A:UN'\
1845 BGM+220+PO-001+9'\
1846 UNT+3+1'\
1847 UNZ+1+1'";
1848 let result = parse_and_validate(input);
1849 assert!(
1850 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNE"),
1851 "expected MissingSegment(UNE), got {result:?}"
1852 );
1853 }
1854
1855 #[test]
1856 fn une_group_ref_must_match_ung() {
1857 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1858 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
1859 UNH+1+ORDERS:D:96A:UN'\
1860 BGM+220+PO-001+9'\
1861 UNT+3+1'\
1862 UNE+1+999'\
1863 UNZ+1+1'";
1864 let result = parse_and_validate(input);
1865 assert!(
1866 matches!(result, Err(EdifactError::QualifierMismatch { ref tag, .. }) if tag == "UNE"),
1867 "expected QualifierMismatch(UNE), got {result:?}"
1868 );
1869 }
1870
1871 #[test]
1874 fn processing_priority_extracted_when_present() {
1875 let input = b"UNB+UNOA:3+S+R+200101:0900+1+++A'\
1877 UNH+1+ORDERS:D:96A:UN'\
1878 BGM+220+PO-001+9'\
1879 UNT+3+1'\
1880 UNZ+1+1'";
1881 let r = parse_and_validate(input).expect("UNB with processing_priority must parse ok");
1882 assert_eq!(r.interchange.processing_priority.as_deref(), Some("A"));
1883 }
1884
1885 #[test]
1886 fn processing_priority_is_none_when_absent() {
1887 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1888 assert!(r.interchange.processing_priority.is_none());
1889 }
1890
1891 #[test]
1892 fn sender_routing_address_extracted_when_present() {
1893 let input = b"UNB+UNOA:3+SENDER:14:ROUTEA+RECIP+200101:0900+1'\
1895 UNH+1+ORDERS:D:96A:UN'\
1896 BGM+220+PO-001+9'\
1897 UNT+3+1'\
1898 UNZ+1+1'";
1899 let r = parse_and_validate(input).expect("UNB with sender routing must parse ok");
1900 assert_eq!(
1901 r.interchange.sender_routing_address.as_deref(),
1902 Some("ROUTEA")
1903 );
1904 assert!(r.interchange.recipient_routing_address.is_none());
1905 }
1906
1907 #[test]
1908 fn recipient_routing_address_extracted_when_present() {
1909 let input = b"UNB+UNOA:3+SENDER+RECIP:14:ROUTEB+200101:0900+1'\
1911 UNH+1+ORDERS:D:96A:UN'\
1912 BGM+220+PO-001+9'\
1913 UNT+3+1'\
1914 UNZ+1+1'";
1915 let r = parse_and_validate(input).expect("UNB with recipient routing must parse ok");
1916 assert!(r.interchange.sender_routing_address.is_none());
1917 assert_eq!(
1918 r.interchange.recipient_routing_address.as_deref(),
1919 Some("ROUTEB")
1920 );
1921 }
1922
1923 #[test]
1924 fn routing_address_is_none_when_absent() {
1925 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1927 UNH+1+ORDERS:D:96A:UN'\
1928 BGM+220+PO-001+9'\
1929 UNT+3+1'\
1930 UNZ+1+1'";
1931 let r = parse_and_validate(input).unwrap();
1932 assert!(r.interchange.sender_routing_address.is_none());
1933 assert!(r.interchange.recipient_routing_address.is_none());
1934 }
1935
1936 #[test]
1937 fn common_access_ref_extracted_when_present() {
1938 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1940 UNH+1+ORDERS:D:96A:UN+COMREF1'\
1941 BGM+220+PO-001+9'\
1942 UNT+3+1'\
1943 UNZ+1+1'";
1944 let r = parse_and_validate(input).expect("UNH with common_access_ref must parse ok");
1945 assert_eq!(r.messages[0].common_access_ref.as_deref(), Some("COMREF1"));
1946 }
1947
1948 #[test]
1949 fn common_access_ref_is_none_when_absent() {
1950 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1951 assert!(r.messages[0].common_access_ref.is_none());
1952 }
1953
1954 #[test]
1955 fn parse_unh_includes_message_ref() {
1956 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
1957 UNH+REF42+ORDERS:D:96A:UN'\
1958 BGM+220+PO-001+9'\
1959 UNT+3+REF42'\
1960 UNZ+1+1'";
1961 let r = parse_and_validate(input).expect("must parse ok");
1964 assert_eq!(r.messages[0].message_ref, "REF42");
1965 assert_eq!(r.messages[0].message_type, "ORDERS");
1966 }
1967
1968 #[test]
1969 fn iter_messages_by_type_returns_matching_messages() {
1970 let input = b"UNB+UNOA:3+S+R+200101:0900+CTRL2'\
1971 UNH+1+ORDERS:D:96A:UN'\
1972 BGM+220+PO-001+9'\
1973 UNT+3+1'\
1974 UNH+2+INVOIC:D:96A:UN'\
1975 BGM+380+INV-001+9'\
1976 UNT+3+2'\
1977 UNZ+2+CTRL2'";
1978 let r = parse_and_validate(input).expect("two-message interchange must parse ok");
1979 let orders: Vec<_> = r.iter_messages_by_type("ORDERS").collect();
1980 assert_eq!(orders.len(), 1);
1981 assert_eq!(orders[0].message_ref, "1");
1982 let invoices: Vec<_> = r.iter_messages_by_type("INVOIC").collect();
1983 assert_eq!(invoices.len(), 1);
1984 assert_eq!(invoices[0].message_ref, "2");
1985 let none: Vec<_> = r.iter_messages_by_type("DESADV").collect();
1986 assert!(none.is_empty());
1987 }
1988
1989 #[test]
1990 fn display_interchange_envelope() {
1991 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
1992 let s = r.interchange.to_string();
1993 assert!(s.contains("SENDER"), "Display must include sender_id");
1994 assert!(s.contains("UNOA"), "Display must include syntax_identifier");
1995 }
1996
1997 #[test]
1998 fn display_message_envelope() {
1999 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2000 let s = r.messages[0].to_string();
2001 assert!(s.contains("ORDERS"), "Display must include message_type");
2002 assert!(s.contains("ref="), "Display must include message ref label");
2003 }
2004
2005 #[test]
2006 fn display_validated_interchange() {
2007 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2008 let s = r.to_string();
2009 assert!(
2010 s.contains("messages="),
2011 "Display must include message count label"
2012 );
2013 }
2014
2015 #[test]
2018 fn unrecognised_syntax_identifier_returns_err() {
2019 let input = b"UNB+XXXX:3+S+R+200101:0900+1'\
2021 UNH+1+ORDERS:D:96A:UN'\
2022 BGM+220+PO-001+9'\
2023 UNT+3+1'\
2024 UNZ+1+1'";
2025 let result = parse_and_validate(input);
2026 assert!(
2027 matches!(result, Err(EdifactError::UnrecognisedSyntaxIdentifier(ref id)) if id == "XXXX"),
2028 "expected UnrecognisedSyntaxIdentifier(\"XXXX\"), got {result:?}"
2029 );
2030 }
2031
2032 #[test]
2033 fn all_valid_syntax_identifiers_accepted() {
2034 for id in &["UNOA", "UNOB", "UNOC", "UNOD", "UNOE", "UNOF", "KECA"] {
2035 let input = format!(
2036 "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'"
2037 );
2038 let result = parse_and_validate(input.as_bytes());
2039 assert!(
2040 result.is_ok(),
2041 "syntax id '{id}' should be accepted, got {result:?}"
2042 );
2043 }
2044 }
2045
2046 #[test]
2049 fn recipient_password_qualifier_extracted_when_present() {
2050 let input = b"UNB+UNOA:3+S+R+200101:0900+1+MYPASS:AA'\
2052 UNH+1+ORDERS:D:96A:UN'\
2053 BGM+220+PO-001+9'\
2054 UNT+3+1'\
2055 UNZ+1+1'";
2056 let r = parse_and_validate(input).expect("UNB with password+qualifier must parse ok");
2057 assert_eq!(r.interchange.recipient_password.as_deref(), Some("MYPASS"));
2058 assert_eq!(
2059 r.interchange.recipient_password_qualifier.as_deref(),
2060 Some("AA")
2061 );
2062 }
2063
2064 #[test]
2065 fn recipient_password_qualifier_is_none_when_absent() {
2066 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2067 assert!(r.interchange.recipient_password_qualifier.is_none());
2068 }
2069
2070 #[test]
2073 fn sequence_of_transfers_extracted_when_present() {
2074 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2076 UNH+1+ORDERS:D:96A:UN++2:C'\
2077 BGM+220+PO-001+9'\
2078 UNT+3+1'\
2079 UNZ+1+1'";
2080 let r = parse_and_validate(input).expect("UNH with S010 must parse ok");
2081 assert_eq!(r.messages[0].sequence_of_transfers, Some(2));
2082 assert_eq!(r.messages[0].transfer_position.as_deref(), Some("C"));
2083 }
2084
2085 #[test]
2086 fn sequence_of_transfers_none_when_absent() {
2087 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2088 assert!(r.messages[0].sequence_of_transfers.is_none());
2089 assert!(r.messages[0].transfer_position.is_none());
2090 }
2091
2092 #[test]
2095 fn ung_app_sender_and_recipient_qualifiers_extracted() {
2096 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2098 UNG+ORDERS+APPSEND:ZZZ+APPRECV:14+200101:0900+1+UN+D:96A'\
2099 UNH+1+ORDERS:D:96A:UN'\
2100 BGM+220+PO-001+9'\
2101 UNT+3+1'\
2102 UNE+1+1'\
2103 UNZ+1+1'";
2104 let r = parse_and_validate(input).expect("UNG with qualifiers must parse ok");
2105 let g = &r.functional_groups[0];
2106 assert_eq!(g.app_sender, "APPSEND");
2107 assert_eq!(g.app_sender_qualifier, "ZZZ");
2108 assert_eq!(g.app_recipient, "APPRECV");
2109 assert_eq!(g.app_recipient_qualifier, "14");
2110 }
2111
2112 #[test]
2113 fn ung_qualifiers_empty_when_absent() {
2114 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2115 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
2116 UNH+1+ORDERS:D:96A:UN'\
2117 BGM+220+PO-001+9'\
2118 UNT+3+1'\
2119 UNE+1+1'\
2120 UNZ+1+1'";
2121 let r = parse_and_validate(input).unwrap();
2122 let g = &r.functional_groups[0];
2123 assert_eq!(g.app_sender_qualifier, "");
2124 assert_eq!(g.app_recipient_qualifier, "");
2125 }
2126
2127 #[test]
2130 fn lenient_result_is_valid_true_on_clean_interchange() {
2131 let owned = parse(VALID_INTERCHANGE);
2132 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2133 let r = validate_envelope_lenient(&segs);
2134 assert!(r.is_valid());
2135 assert!(r.errors.is_empty());
2136 assert!(r.interchange.is_some());
2137 }
2138
2139 #[test]
2140 fn lenient_result_into_strict_ok_path() {
2141 let owned = parse(VALID_INTERCHANGE);
2142 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2143 let r = validate_envelope_lenient(&segs);
2144 let strict = r.into_strict();
2145 assert!(
2146 strict.is_ok(),
2147 "into_strict() should succeed for valid interchange"
2148 );
2149 assert_eq!(strict.unwrap().messages.len(), 1);
2150 }
2151
2152 #[test]
2153 fn lenient_result_into_strict_err_path() {
2154 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'";
2156 let owned = parse(input);
2157 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2158 let r = validate_envelope_lenient(&segs);
2159 assert!(!r.is_valid());
2160 let strict = r.into_strict();
2161 assert!(strict.is_err());
2162 let errors = strict.unwrap_err();
2163 assert_eq!(errors.len(), 1);
2164 assert!(matches!(
2165 &errors[0],
2166 EdifactError::MessageCountMismatch {
2167 expected: 2,
2168 actual: 1
2169 }
2170 ));
2171 }
2172
2173 #[test]
2176 fn parse_unh_direct_extracts_all_s009_fields() {
2177 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2182 UNH+REF99+ORDERS:D:96A:UN:EAN010'\
2183 BGM+220+PO-001+9'\
2184 UNT+3+REF99'\
2185 UNZ+1+1'";
2186 let r = parse_and_validate(input).expect("must parse ok");
2187 let msg = &r.messages[0];
2188 assert_eq!(msg.message_ref, "REF99");
2189 assert_eq!(msg.message_type, "ORDERS");
2190 assert_eq!(msg.version, "D");
2191 assert_eq!(msg.release, "96A");
2192 assert_eq!(msg.controlling_agency, "UN");
2193 assert_eq!(msg.association_code, "EAN010");
2194 }
2195
2196 #[test]
2197 fn parse_ung_direct_extracts_identifier_fields() {
2198 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2200 UNG+ORDERS+APPSEND:ZZZ+APPRECV:14+200101:0900+GRP01+UN+D:96A'\
2201 UNH+1+ORDERS:D:96A:UN'\
2202 BGM+220+PO-001+9'\
2203 UNT+3+1'\
2204 UNE+1+GRP01'\
2205 UNZ+1+1'";
2206 let r = parse_and_validate(input).expect("must parse ok");
2207 let g = &r.functional_groups[0];
2208 assert_eq!(g.group_id, "ORDERS");
2209 assert_eq!(g.app_sender, "APPSEND");
2210 assert_eq!(g.app_sender_qualifier, "ZZZ");
2211 assert_eq!(g.app_recipient, "APPRECV");
2212 assert_eq!(g.app_recipient_qualifier, "14");
2213 assert_eq!(g.group_ref, "GRP01");
2214 assert_eq!(g.controlling_agency, "UN");
2215 assert_eq!(g.version, "D");
2216 assert_eq!(g.release, "96A");
2217 }
2218
2219 #[test]
2222 fn find_message_returns_correct_message_by_ref() {
2223 let input = b"UNB+UNOA:3+S+R+200101:0900+CTRL2'\
2224 UNH+REF-A+ORDERS:D:96A:UN'\
2225 BGM+220+PO-001+9'\
2226 UNT+3+REF-A'\
2227 UNH+REF-B+INVOIC:D:96A:UN'\
2228 BGM+380+INV-001+9'\
2229 UNT+3+REF-B'\
2230 UNZ+2+CTRL2'";
2231 let r = parse_and_validate(input).expect("two-message interchange must parse ok");
2232 let msg_a = r.find_message("REF-A");
2233 assert!(msg_a.is_some());
2234 assert_eq!(msg_a.unwrap().message_type, "ORDERS");
2235 let msg_b = r.find_message("REF-B");
2236 assert!(msg_b.is_some());
2237 assert_eq!(msg_b.unwrap().message_type, "INVOIC");
2238 assert!(r.find_message("MISSING").is_none());
2239 }
2240
2241 #[test]
2244 fn ung_missing_group_ref_returns_err() {
2245 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2247 UNG+ORDERS+S+R+200101:0900++UN+D:96A'\
2248 UNH+1+ORDERS:D:96A:UN'\
2249 BGM+220+PO-001+9'\
2250 UNT+3+1'\
2251 UNE+1+'\
2252 UNZ+1+1'";
2253 let result = parse_and_validate(input);
2254 assert!(
2255 matches!(result,
2256 Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 4, component_index: 0 })
2257 if tag == "UNG"
2258 ),
2259 "expected MissingRequiredComponent for empty UNG group_ref, got {result:?}"
2260 );
2261 }
2262
2263 #[test]
2266 fn empty_segment_list_returns_missing_unb() {
2267 let result = validate_envelope(&[]);
2270 assert!(
2271 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNB"),
2272 "expected MissingSegment(UNB) for empty input, got {result:?}"
2273 );
2274 }
2275
2276 #[test]
2277 fn single_segment_only_unb_returns_missing_unz() {
2278 let input = b"UNB+UNOA:3+S+R+200101:0900+1'";
2280 let owned = parse(input);
2281 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2282 let result = validate_envelope(&segs);
2283 assert!(
2284 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNZ"),
2285 "expected MissingSegment(UNZ) for UNB-only input, got {result:?}"
2286 );
2287 }
2288
2289 #[test]
2290 fn display_validated_interchange_includes_count() {
2291 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2292 let s = r.to_string();
2293 assert!(
2295 s.contains("messages=1"),
2296 "Display must include count '1': {s}"
2297 );
2298 }
2299
2300 #[test]
2301 fn display_interchange_envelope_contains_arrow() {
2302 let r = parse_and_validate(VALID_INTERCHANGE).unwrap();
2303 let s = r.interchange.to_string();
2304 assert!(s.contains("->"), "Display must use ASCII '->' arrow: {s}");
2306 assert!(
2307 !s.contains('\u{2192}'),
2308 "Display must not use Unicode → arrow: {s}"
2309 );
2310 }
2311
2312 #[test]
2313 fn display_functional_group_envelope() {
2314 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
2315 UNG+ORDERS+APPSEND+APPRECV+200101:0900+GRP01+UN+D:96A'\
2316 UNH+1+ORDERS:D:96A:UN'\
2317 BGM+220+PO-001+9'\
2318 UNT+3+1'\
2319 UNE+1+GRP01'\
2320 UNZ+1+1'";
2321 let r = parse_and_validate(input).expect("must parse ok");
2322 let g = &r.functional_groups[0];
2323 let s = g.to_string();
2324 assert!(s.contains("ORDERS"), "Display must include group_id: {s}");
2325 assert!(
2326 s.contains("APPSEND"),
2327 "Display must include app_sender: {s}"
2328 );
2329 assert!(
2330 s.contains("APPRECV"),
2331 "Display must include app_recipient: {s}"
2332 );
2333 assert!(s.contains("GRP01"), "Display must include group_ref: {s}");
2334 assert!(
2335 s.contains("msgs="),
2336 "Display must include message count label: {s}"
2337 );
2338 assert!(
2339 s.contains("1/1"),
2340 "Display must include actual/declared counts: {s}"
2341 );
2342 }
2343
2344 #[test]
2345 fn lenient_has_errors_is_inverse_of_is_valid() {
2346 let owned = parse(VALID_INTERCHANGE);
2347 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
2348 let valid = validate_envelope_lenient(&segs);
2349 assert!(valid.is_valid());
2350 assert!(!valid.has_errors());
2351
2352 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'";
2354 let owned2 = parse(input);
2355 let segs2: Vec<Segment<'_>> = owned2
2356 .iter()
2357 .map(crate::OwnedSegment::as_borrowed)
2358 .collect();
2359 let invalid = validate_envelope_lenient(&segs2);
2360 assert!(!invalid.is_valid());
2361 assert!(invalid.has_errors());
2362 }
2363}