1use crate::{error::EdifactError, model::Segment};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct InterchangeEnvelope {
11 pub syntax_identifier: String,
13 pub sender_id: String,
15 pub recipient_id: String,
17 pub datetime: String,
19 pub control_ref: String,
21 pub declared_message_count: u32,
23 pub actual_message_count: u32,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct MessageEnvelope {
30 pub message_ref: String,
32 pub message_type: String,
34 pub version: String,
36 pub release: String,
38 pub controlling_agency: String,
40 pub association_code: String,
42 pub declared_segment_count: u32,
44 pub actual_segment_count: u32,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct MessageIdentifier<'a> {
54 pub message_type: &'a str,
56 pub version: &'a str,
58 pub release: &'a str,
60 pub controlling_agency: &'a str,
62 pub association_assigned: &'a str,
64}
65
66pub fn parse_unh<'a>(unh: &'a Segment<'a>) -> Result<MessageIdentifier<'a>, EdifactError> {
77 let elem = unh
78 .get_element(1)
79 .ok_or_else(|| EdifactError::MissingRequiredElement {
80 tag: "UNH".to_owned(),
81 element_index: 1,
82 })?;
83 let message_type =
84 elem.get_component(0)
85 .ok_or_else(|| EdifactError::MissingRequiredComponent {
86 tag: "UNH".to_owned(),
87 element_index: 1,
88 component_index: 0,
89 })?;
90 Ok(MessageIdentifier {
91 message_type,
92 version: elem.get_component(1).unwrap_or(""),
93 release: elem.get_component(2).unwrap_or(""),
94 controlling_agency: elem.get_component(3).unwrap_or(""),
95 association_assigned: elem.get_component(4).unwrap_or(""),
96 })
97}
98
99pub fn validate_envelope(
119 segments: &[Segment<'_>],
120) -> Result<(InterchangeEnvelope, Vec<MessageEnvelope>), EdifactError> {
121 if let Some(ung_or_une) = segments.iter().find(|s| s.tag == "UNG" || s.tag == "UNE") {
125 return Err(EdifactError::FunctionalGroupNotSupported {
126 offset: ung_or_une.span.start,
127 });
128 }
129
130 let mut interchange_env = extract_interchange(segments)?;
131 let message_envs = extract_messages(segments)?;
132 interchange_env.actual_message_count =
133 u32::try_from(message_envs.len()).map_err(|_| EdifactError::InterchangeTooLarge {
134 count: message_envs.len() as u64,
135 })?;
136
137 if interchange_env.declared_message_count != interchange_env.actual_message_count {
139 return Err(EdifactError::MessageCountMismatch {
140 expected: interchange_env.declared_message_count,
141 actual: interchange_env.actual_message_count,
142 });
143 }
144
145 for msg in &message_envs {
147 if msg.declared_segment_count != msg.actual_segment_count {
148 return Err(EdifactError::SegmentCountMismatch {
149 expected: msg.declared_segment_count,
150 actual: msg.actual_segment_count,
151 message_ref: msg.message_ref.clone(),
152 });
153 }
154 }
155
156 Ok((interchange_env, message_envs))
157}
158
159pub fn validate_envelope_lenient(segments: &[Segment<'_>]) -> Vec<EdifactError> {
180 let mut errors: Vec<EdifactError> = Vec::new();
181
182 if let Some(ung_or_une) = segments.iter().find(|s| s.tag == "UNG" || s.tag == "UNE") {
184 errors.push(EdifactError::FunctionalGroupNotSupported {
185 offset: ung_or_une.span.start,
186 });
187 return errors;
189 }
190
191 match validate_envelope(segments) {
193 Ok(_) => {}
194 Err(first) => {
195 errors.push(first);
196
197 if let Ok(mut ie) = extract_interchange(segments) {
200 match extract_messages(segments) {
202 Ok(msgs) => {
203 ie.actual_message_count = u32::try_from(msgs.len()).unwrap_or(u32::MAX);
204 if ie.declared_message_count != ie.actual_message_count {
205 let dup = EdifactError::MessageCountMismatch {
208 expected: ie.declared_message_count,
209 actual: ie.actual_message_count,
210 };
211 if !errors.iter().any(|e| e == &dup) {
212 errors.push(dup);
213 }
214 }
215 for msg in &msgs {
216 if msg.declared_segment_count != msg.actual_segment_count {
217 let dup = EdifactError::SegmentCountMismatch {
218 expected: msg.declared_segment_count,
219 actual: msg.actual_segment_count,
220 message_ref: msg.message_ref.clone(),
221 };
222 if !errors.iter().any(|e| e == &dup) {
223 errors.push(dup);
224 }
225 }
226 }
227 }
228 Err(e) => {
229 if !errors.iter().any(|err| err == &e) {
230 errors.push(e);
231 }
232 }
233 }
234 }
235 }
236 }
237
238 errors
239}
240
241fn extract_interchange(segments: &[Segment<'_>]) -> Result<InterchangeEnvelope, EdifactError> {
242 if segments.first().map(|segment| segment.tag) != Some("UNB") {
243 return Err(EdifactError::MissingSegment {
244 tag: "UNB".to_owned(),
245 expected_position: "first segment of interchange".to_owned(),
246 });
247 }
248
249 if segments.last().map(|segment| segment.tag) != Some("UNZ") {
250 return Err(EdifactError::MissingSegment {
251 tag: "UNZ".to_owned(),
252 expected_position: "last segment of interchange".to_owned(),
253 });
254 }
255
256 let unb = &segments[0];
257 let unz = &segments[segments.len() - 1];
258
259 let syntax_identifier = required_component(unb, 0, 0)?.to_owned();
260
261 let sender_id = required_component(unb, 1, 0)?.to_owned();
262
263 let recipient_id = required_component(unb, 2, 0)?.to_owned();
264
265 let date = required_component(unb, 3, 0)?;
267 let time = unb
268 .get_element(3)
269 .and_then(|e| e.get_component(1))
270 .unwrap_or("");
271 let datetime = if time.is_empty() {
272 date.to_owned()
273 } else {
274 format!("{date}:{time}")
275 };
276
277 let control_ref = required_component(unb, 4, 0)?.to_owned();
278 let unz_control_ref = required_component(unz, 1, 0)?;
279 if unz_control_ref != control_ref {
280 return Err(EdifactError::QualifierMismatch {
281 tag: "UNZ".to_owned(),
282 actual: unz_control_ref.to_owned(),
283 expected: control_ref,
284 offset: unz.span.start,
285 });
286 }
287
288 let declared_message_count: u32 =
289 required_component(unz, 0, 0)?
290 .parse()
291 .map_err(|_| EdifactError::InvalidText {
292 offset: unz.span.start,
293 })?;
294
295 Ok(InterchangeEnvelope {
296 syntax_identifier,
297 sender_id,
298 recipient_id,
299 datetime,
300 control_ref,
301 declared_message_count,
302 actual_message_count: 0,
303 })
304}
305
306#[inline]
308fn required_component<'a>(
309 segment: &'a Segment<'_>,
310 element_index: usize,
311 component_index: usize,
312) -> Result<&'a str, EdifactError> {
313 crate::de::required_component(segment, element_index, component_index)
314}
315
316fn extract_messages(segments: &[Segment<'_>]) -> Result<Vec<MessageEnvelope>, EdifactError> {
317 let mut messages: Vec<MessageEnvelope> = Vec::new();
318 let mut in_message = false;
319 let mut msg_start_idx: usize = 0;
320 let mut current_unh: Option<&Segment<'_>> = None;
321
322 for (i, seg) in segments[1..segments.len() - 1].iter().enumerate() {
323 match seg.tag {
324 "UNH" => {
325 if in_message {
326 return Err(EdifactError::InvalidSegmentForMessage {
327 tag: "UNH".to_owned(),
328 message_type: "ENVELOPE".to_owned(),
329 offset: seg.span.start,
330 });
331 }
332 in_message = true;
333 msg_start_idx = i;
334 current_unh = Some(seg);
335 }
336 "UNT" if in_message => {
337 let unh = current_unh
338 .take()
339 .ok_or(EdifactError::InvalidSegmentForMessage {
340 tag: "UNT".to_owned(),
341 message_type: "ENVELOPE".to_owned(),
342 offset: seg.span.start,
343 })?;
344
345 let message_ref = required_component(unh, 0, 0)?.to_owned();
346
347 let message_type = required_component(unh, 1, 0)?.to_owned();
348 let version = required_component(unh, 1, 1)?.to_owned();
349 let release = required_component(unh, 1, 2)?.to_owned();
350 let controlling_agency = required_component(unh, 1, 3)?.to_owned();
351 let association_code = unh
352 .get_element(1)
353 .and_then(|e| e.get_component(4))
354 .unwrap_or("")
355 .to_owned();
356
357 let declared_segment_count: u32 =
358 required_component(seg, 0, 0)?.parse().map_err(|_| {
359 EdifactError::InvalidText {
360 offset: seg.span.start,
361 }
362 })?;
363 let unt_ref = required_component(seg, 1, 0)?;
364 if unt_ref != message_ref {
365 return Err(EdifactError::QualifierMismatch {
366 tag: "UNT".to_owned(),
367 actual: unt_ref.to_owned(),
368 expected: message_ref.clone(),
369 offset: seg.span.start,
370 });
371 }
372
373 let actual_segment_count = u32::try_from(i - msg_start_idx + 1).map_err(|_| {
375 EdifactError::InterchangeTooLarge {
376 count: u64::try_from(i - msg_start_idx + 1).unwrap_or(u64::MAX),
379 }
380 })?;
381
382 in_message = false;
383 messages.push(MessageEnvelope {
384 message_ref,
385 message_type,
386 version,
387 release,
388 controlling_agency,
389 association_code,
390 declared_segment_count,
391 actual_segment_count,
392 });
393 }
394 "UNT" => {
395 return Err(EdifactError::InvalidSegmentForMessage {
396 tag: "UNT".to_owned(),
397 message_type: "ENVELOPE".to_owned(),
398 offset: seg.span.start,
399 });
400 }
401 "UNB" | "UNZ" if in_message => {
402 return Err(EdifactError::InvalidSegmentForMessage {
403 tag: seg.tag.to_owned(),
404 message_type: "ENVELOPE".to_owned(),
405 offset: seg.span.start,
406 });
407 }
408 _ if !in_message => {
409 return Err(EdifactError::InvalidSegmentForMessage {
410 tag: seg.tag.to_owned(),
411 message_type: "ENVELOPE".to_owned(),
412 offset: seg.span.start,
413 });
414 }
415 _ => {}
416 }
417 }
418
419 if in_message {
420 return Err(EdifactError::MissingSegment {
421 tag: "UNT".to_owned(),
422 expected_position: "end of message group".to_owned(),
423 });
424 }
425
426 Ok(messages)
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432
433 fn parse(input: &[u8]) -> Vec<crate::OwnedSegment> {
435 crate::from_reader_collect(std::io::Cursor::new(input)).expect("parse failed")
436 }
437
438 fn parse_and_validate(
440 input: &[u8],
441 ) -> Result<(InterchangeEnvelope, Vec<MessageEnvelope>), EdifactError> {
442 let owned = parse(input);
443 let segs: Vec<Segment<'_>> = owned.iter().map(crate::OwnedSegment::as_borrowed).collect();
444 validate_envelope(&segs)
445 }
446
447 const VALID_INTERCHANGE: &[u8] =
448 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'";
449
450 #[test]
451 fn valid_envelope_parses_ok() {
452 let (interchange, messages) =
453 parse_and_validate(VALID_INTERCHANGE).expect("envelope should be valid");
454 assert_eq!(interchange.sender_id, "SENDER");
455 assert_eq!(interchange.recipient_id, "RECEIVER");
456 assert_eq!(interchange.control_ref, "00001");
457 assert_eq!(interchange.declared_message_count, 1);
458 assert_eq!(interchange.actual_message_count, 1);
459 assert_eq!(messages.len(), 1);
460 assert_eq!(messages[0].message_type, "ORDERS");
461 assert_eq!(messages[0].association_code, "EAN010");
462 assert_eq!(messages[0].declared_segment_count, 4);
463 assert_eq!(messages[0].actual_segment_count, 4); }
465
466 #[test]
467 fn unt_count_mismatch_returns_err() {
468 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'";
470 let result = parse_and_validate(input);
471 assert!(
472 matches!(
473 result,
474 Err(EdifactError::SegmentCountMismatch { expected: 99, .. })
475 ),
476 "expected SegmentCountMismatch, got {result:?}"
477 );
478 }
479
480 #[test]
481 fn unz_count_mismatch_returns_err() {
482 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'";
484 let result = parse_and_validate(input);
485 assert!(
486 matches!(
487 result,
488 Err(EdifactError::MessageCountMismatch {
489 expected: 2,
490 actual: 1
491 })
492 ),
493 "expected MessageCountMismatch(2,1), got {result:?}"
494 );
495 }
496
497 #[test]
498 fn missing_unb_returns_err() {
499 let input = b"UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNT+3+1'UNZ+1+1'";
500 let result = parse_and_validate(input);
501 assert!(result.is_err());
502 }
503
504 #[test]
505 fn extracts_una_interchange_correctly() {
506 let (env, _) = parse_and_validate(VALID_INTERCHANGE).unwrap();
508 assert_eq!(env.syntax_identifier, "UNOA");
510 assert_eq!(env.datetime, "230401:0900");
511 }
512
513 #[test]
514 fn dangling_unh_without_unt_returns_err() {
515 let input =
516 b"UNB+UNOA:3+S+R+200101:0900+1'UNH+1+ORDERS:D:11A:UN:EAN010'BGM+220+PO-1+9'UNZ+1+1'";
517 let result = parse_and_validate(input);
518 assert!(
519 matches!(result, Err(EdifactError::MissingSegment { ref tag, .. }) if tag == "UNT")
520 );
521 }
522
523 #[test]
524 fn stray_segment_outside_message_returns_err() {
525 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'";
526 let result = parse_and_validate(input);
527 assert!(matches!(
528 result,
529 Err(EdifactError::InvalidSegmentForMessage { .. })
530 ));
531 }
532
533 #[test]
534 fn missing_unb_sender_component_returns_err() {
535 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'";
536 let result = parse_and_validate(input);
537 assert!(
539 matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 0 }) if tag == "UNB"),
540 "expected MissingRequiredComponent for empty sender, got: {result:?}"
541 );
542 }
543
544 #[test]
545 fn nested_unh_without_closing_previous_message_returns_err() {
546 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'";
547 let result = parse_and_validate(input);
548 assert!(
549 matches!(result, Err(EdifactError::InvalidSegmentForMessage { ref tag, .. }) if tag == "UNH"),
550 "expected InvalidSegmentForMessage(UNH), got {result:?}"
551 );
552 }
553
554 #[test]
555 fn unt_message_reference_must_match_unh() {
556 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'";
557 let result = parse_and_validate(input);
558 assert!(matches!(result, Err(EdifactError::QualifierMismatch { tag, .. }) if tag == "UNT"));
559 }
560
561 #[test]
562 fn unz_control_reference_must_match_unb() {
563 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'";
564 let result = parse_and_validate(input);
565 assert!(matches!(result, Err(EdifactError::QualifierMismatch { tag, .. }) if tag == "UNZ"));
566 }
567
568 #[test]
569 fn missing_unh_message_type_components_return_err() {
570 let input =
571 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'";
572 let result = parse_and_validate(input);
573 assert!(
575 matches!(result, Err(EdifactError::MissingRequiredComponent { ref tag, element_index: 1, component_index: 3 }) if tag == "UNH"),
576 "expected MissingRequiredComponent for truncated UNH message type, got: {result:?}"
577 );
578 }
579
580 #[test]
581 fn nested_unz_inside_message_returns_err() {
582 let input =
583 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'";
584 let result = parse_and_validate(input);
585 assert!(
586 matches!(result, Err(EdifactError::InvalidSegmentForMessage { tag, .. }) if tag == "UNZ")
587 );
588 }
589
590 #[test]
599 fn envelope_with_ung_returns_explicit_error() {
600 let input = b"UNB+UNOA:3+S+R+200101:0900+1'\
603 UNG+ORDERS+S+R+200101:0900+1+UN+D:96A'\
604 UNH+1+ORDERS:D:96A:UN'\
605 BGM+220+PO-001+9'\
606 UNT+3+1'\
607 UNE+1+1'\
608 UNZ+1+1'";
609 let result = parse_and_validate(input);
610 assert!(
611 result.is_err(),
612 "UNG/UNE is documented as unsupported; must return an error, not silently produce wrong counts"
613 );
614 assert!(
617 matches!(
618 result,
619 Err(EdifactError::FunctionalGroupNotSupported { .. })
620 ),
621 "expected FunctionalGroupNotSupported, got {result:?}"
622 );
623 }
624}