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