Skip to main content

edifact_rs/
contrl.rs

1//! `CONTRL` — the syntax and service report message (ISO 9735-4).
2//!
3//! `CONTRL` is how one EDI partner tells another what happened to an
4//! interchange: that it arrived, that it was syntactically accepted, or that it
5//! was rejected and precisely where. It is not an application-level reply —
6//! ISO 9735-4 §5.2 is explicit that acknowledging an interchange with `CONTRL`
7//! says nothing about whether the business content was agreed to.
8//!
9//! # The two messages
10//!
11//! §5.3.1 defines a maximum of two `CONTRL` messages per subject interchange:
12//!
13//! 1. **Receipt** — optional, sent immediately, action code `8`. Built with
14//!    [`Contrl::receipt`].
15//! 2. **Acknowledgement or rejection** — sent after the syntax check, action
16//!    code `7` or `4`. Built with [`Contrl::acknowledgement`] or
17//!    [`Contrl::from_report`].
18//!
19//! If the subject interchange requested an acknowledgement (`UNB` DE 0031 —
20//! [`InterchangeEnvelope::ack_requested`]), the second message is mandatory.
21//!
22//! # Reporting levels
23//!
24//! The message nests five reporting levels, each naming a part of the subject
25//! interchange and each able to carry **one** error code (§5.3.3):
26//!
27//! ```text
28//! UCI   the interchange          → UNB / UNZ
29//!  UCF  a group                  → UNG / UNE
30//!   UCM a message or package     → UNH / UNT
31//!    UCS a segment               → by position, UNH = 1
32//!     UCD a data element         → by position within that segment
33//! ```
34//!
35//! §5.3.3 also requires the *lowest* level that can express an error to be the
36//! one that reports it, and forbids repeating the same code further up. This
37//! module follows both rules: an issue that names a segment becomes a `UCS`, one
38//! that names a data element becomes a `UCD`, and only an issue about the
39//! envelope itself reaches `UCI`.
40//!
41//! # Example
42//!
43//! ```
44//! use edifact_rs::{Contrl, from_bytes, validate_envelope};
45//!
46//! let raw = b"UNB+UNOC:3+SENDER+RECEIVER+260101:0900+IC4711'\
47//!             UNH+MSG1+ORDERS:D:96A:UN'BGM+220+PO-1+9'UNT+3+MSG1'\
48//!             UNZ+1+IC4711'";
49//! let segments: Vec<_> = from_bytes(raw).collect::<Result<Vec<_>, _>>()?;
50//! let validated = validate_envelope(&segments)?;
51//!
52//! let contrl = Contrl::acknowledgement(&validated).with_message_reference("ACK1");
53//! let wire = contrl.to_edifact_string()?;
54//!
55//! assert!(wire.starts_with("UNH+ACK1+CONTRL:4:1:UN'"));
56//! assert!(wire.contains("UCI+IC4711+SENDER+RECEIVER+7'"));
57//! # Ok::<(), edifact_rs::EdifactError>(())
58//! ```
59
60use crate::envelope::{
61    FunctionalGroupEnvelope, InterchangeEnvelope, MessageEnvelope, ValidatedInterchange,
62};
63use crate::model::{OwnedElement, OwnedSegment, Segment};
64use crate::report::{ValidationIssue, ValidationReport};
65use crate::{EdifactError, Writer};
66
67// ── action codes (DE 0083) ────────────────────────────────────────────────────
68
69/// `CONTRL` action code — DE 0083, the verdict on one reporting level.
70///
71/// ISO 9735-4 §5.3.2 restricts which codes may appear in which message: `4` and
72/// `7` only after a complete syntax check, `8` only in a receipt.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74#[non_exhaustive]
75pub enum Action {
76    /// `4` — this level and all lower levels rejected.
77    Rejected,
78    /// `7` — this level acknowledged; every lower level is acknowledged too
79    /// unless a reporting level explicitly rejects it.
80    Acknowledged,
81    /// `8` — interchange received. Only valid in a receipt message.
82    Received,
83}
84
85impl Action {
86    /// The DE 0083 code value.
87    #[must_use]
88    pub const fn code(self) -> &'static str {
89        match self {
90            Self::Rejected => "4",
91            Self::Acknowledged => "7",
92            Self::Received => "8",
93        }
94    }
95}
96
97impl std::fmt::Display for Action {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        f.write_str(self.code())
100    }
101}
102
103// ── reporting levels ──────────────────────────────────────────────────────────
104
105/// One of the five `CONTRL` reporting levels.
106///
107/// Which level may carry which error code is fixed by ISO 9735-4 Annex A; see
108/// [`SyntaxError::permitted_at`].
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110#[non_exhaustive]
111pub enum ReportingLevel {
112    /// `UCI` — the interchange.
113    Interchange,
114    /// `UCF` — a group.
115    Group,
116    /// `UCM` — a message or package.
117    Message,
118    /// `UCS` — a segment.
119    Segment,
120    /// `UCD` — a data element.
121    DataElement,
122}
123
124impl ReportingLevel {
125    /// The segment tag that carries this level.
126    #[must_use]
127    pub const fn tag(self) -> &'static str {
128        match self {
129            Self::Interchange => "UCI",
130            Self::Group => "UCF",
131            Self::Message => "UCM",
132            Self::Segment => "UCS",
133            Self::DataElement => "UCD",
134        }
135    }
136}
137
138// ── syntax error codes (DE 0085) ──────────────────────────────────────────────
139
140/// `CONTRL` syntax error code — DE 0085, the nature of one fault.
141///
142/// The variants are exactly the code set of ISO 9735-4 Annex A, and
143/// [`permitted_at`][Self::permitted_at] reproduces that annex's table of which
144/// code may be used at which reporting level. Emitting a code at a level the
145/// annex forbids produces a `CONTRL` the partner's translator will reject, so
146/// [`Contrl`] checks it rather than trusting the caller.
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
148#[non_exhaustive]
149pub enum SyntaxError {
150    /// `2` — syntax version or level not supported.
151    SyntaxVersionNotSupported,
152    /// `7` — interchange recipient not actual recipient.
153    NotActualRecipient,
154    /// `12` — invalid value.
155    InvalidValue,
156    /// `13` — missing.
157    Missing,
158    /// `14` — value not supported in this position.
159    ValueNotSupportedHere,
160    /// `15` — not supported in this position.
161    NotSupportedHere,
162    /// `16` — too many constituents.
163    TooManyConstituents,
164    /// `17` — no agreement.
165    NoAgreement,
166    /// `18` — unspecified error.
167    Unspecified,
168    /// `20` — character invalid as service character.
169    InvalidAsServiceCharacter,
170    /// `21` — invalid character(s).
171    InvalidCharacters,
172    /// `22` — invalid service character(s).
173    InvalidServiceCharacters,
174    /// `23` — unknown interchange sender.
175    UnknownSender,
176    /// `24` — too old.
177    TooOld,
178    /// `25` — test indicator not supported.
179    TestIndicatorNotSupported,
180    /// `26` — duplicate detected.
181    DuplicateDetected,
182    /// `28` — references do not match.
183    ReferencesDoNotMatch,
184    /// `29` — control or octet count does not match number of instances received.
185    ControlCountMismatch,
186    /// `30` — groups and messages/packages mixed.
187    GroupsAndMessagesMixed,
188    /// `32` — lower level empty.
189    LowerLevelEmpty,
190    /// `33` — invalid occurrence outside message, package or group.
191    InvalidOccurrenceOutsideMessage,
192    /// `35` — too many repetitions.
193    TooManyRepetitions,
194    /// `36` — too many segment group repetitions.
195    TooManyGroupRepetitions,
196    /// `37` — invalid type of character(s).
197    InvalidCharacterType,
198    /// `39` — data element too long.
199    DataElementTooLong,
200    /// `40` — data element too short.
201    DataElementTooShort,
202    /// `45` — trailing separator.
203    TrailingSeparator,
204    /// `46` — character set not supported.
205    CharacterSetNotSupported,
206    /// `47` — envelope functionality not supported.
207    EnvelopeFunctionalityNotSupported,
208}
209
210impl SyntaxError {
211    /// The DE 0085 code value.
212    #[must_use]
213    pub const fn code(self) -> &'static str {
214        match self {
215            Self::SyntaxVersionNotSupported => "2",
216            Self::NotActualRecipient => "7",
217            Self::InvalidValue => "12",
218            Self::Missing => "13",
219            Self::ValueNotSupportedHere => "14",
220            Self::NotSupportedHere => "15",
221            Self::TooManyConstituents => "16",
222            Self::NoAgreement => "17",
223            Self::Unspecified => "18",
224            Self::InvalidAsServiceCharacter => "20",
225            Self::InvalidCharacters => "21",
226            Self::InvalidServiceCharacters => "22",
227            Self::UnknownSender => "23",
228            Self::TooOld => "24",
229            Self::TestIndicatorNotSupported => "25",
230            Self::DuplicateDetected => "26",
231            Self::ReferencesDoNotMatch => "28",
232            Self::ControlCountMismatch => "29",
233            Self::GroupsAndMessagesMixed => "30",
234            Self::LowerLevelEmpty => "32",
235            Self::InvalidOccurrenceOutsideMessage => "33",
236            Self::TooManyRepetitions => "35",
237            Self::TooManyGroupRepetitions => "36",
238            Self::InvalidCharacterType => "37",
239            Self::DataElementTooLong => "39",
240            Self::DataElementTooShort => "40",
241            Self::TrailingSeparator => "45",
242            Self::CharacterSetNotSupported => "46",
243            Self::EnvelopeFunctionalityNotSupported => "47",
244        }
245    }
246
247    /// The Annex A code name.
248    #[must_use]
249    pub const fn description(self) -> &'static str {
250        match self {
251            Self::SyntaxVersionNotSupported => "syntax version or level not supported",
252            Self::NotActualRecipient => "interchange recipient not actual recipient",
253            Self::InvalidValue => "invalid value",
254            Self::Missing => "missing",
255            Self::ValueNotSupportedHere => "value not supported in this position",
256            Self::NotSupportedHere => "not supported in this position",
257            Self::TooManyConstituents => "too many constituents",
258            Self::NoAgreement => "no agreement",
259            Self::Unspecified => "unspecified error",
260            Self::InvalidAsServiceCharacter => "character invalid as service character",
261            Self::InvalidCharacters => "invalid character(s)",
262            Self::InvalidServiceCharacters => "invalid service character(s)",
263            Self::UnknownSender => "unknown interchange sender",
264            Self::TooOld => "too old",
265            Self::TestIndicatorNotSupported => "test indicator not supported",
266            Self::DuplicateDetected => "duplicate detected",
267            Self::ReferencesDoNotMatch => "references do not match",
268            Self::ControlCountMismatch => {
269                "control or octet count does not match number of instances received"
270            }
271            Self::GroupsAndMessagesMixed => "groups and messages/packages mixed",
272            Self::LowerLevelEmpty => "lower level empty",
273            Self::InvalidOccurrenceOutsideMessage => {
274                "invalid occurrence outside message, package or group"
275            }
276            Self::TooManyRepetitions => "too many repetitions",
277            Self::TooManyGroupRepetitions => "too many segment group repetitions",
278            Self::InvalidCharacterType => "invalid type of character(s)",
279            Self::DataElementTooLong => "data element too long",
280            Self::DataElementTooShort => "data element too short",
281            Self::TrailingSeparator => "trailing separator",
282            Self::CharacterSetNotSupported => "character set not supported",
283            Self::EnvelopeFunctionalityNotSupported => "envelope functionality not supported",
284        }
285    }
286
287    /// Whether ISO 9735-4 Annex A allows this code at `level`.
288    ///
289    /// # Example
290    ///
291    /// ```
292    /// use edifact_rs::contrl::{ReportingLevel, SyntaxError};
293    ///
294    /// // "Too many segment group repetitions" is a segment-level finding only.
295    /// assert!(SyntaxError::TooManyGroupRepetitions.permitted_at(ReportingLevel::Segment));
296    /// assert!(!SyntaxError::TooManyGroupRepetitions.permitted_at(ReportingLevel::Interchange));
297    ///
298    /// // "Unknown interchange sender" can only be said about the interchange.
299    /// assert!(SyntaxError::UnknownSender.permitted_at(ReportingLevel::Interchange));
300    /// assert!(!SyntaxError::UnknownSender.permitted_at(ReportingLevel::Message));
301    /// ```
302    #[must_use]
303    pub const fn permitted_at(self, level: ReportingLevel) -> bool {
304        use ReportingLevel as L;
305        match self {
306            // Interchange-only findings.
307            Self::SyntaxVersionNotSupported
308            | Self::NotActualRecipient
309            | Self::InvalidAsServiceCharacter
310            | Self::UnknownSender
311            | Self::CharacterSetNotSupported => matches!(l_of(level), L::Interchange),
312            // Everywhere.
313            Self::InvalidValue
314            | Self::Missing
315            | Self::ValueNotSupportedHere
316            | Self::NotSupportedHere
317            | Self::TooManyConstituents
318            | Self::Unspecified
319            | Self::InvalidCharacters => true,
320            // Not at the data-element level.
321            Self::NoAgreement | Self::TestIndicatorNotSupported => matches!(
322                l_of(level),
323                L::Interchange | L::Group | L::Message | L::Segment
324            ),
325            Self::InvalidServiceCharacters => true,
326            Self::TooOld => matches!(l_of(level), L::Interchange | L::Group),
327            Self::DuplicateDetected
328            | Self::ReferencesDoNotMatch
329            | Self::ControlCountMismatch
330            | Self::GroupsAndMessagesMixed => {
331                matches!(l_of(level), L::Interchange | L::Group | L::Message)
332            }
333            Self::LowerLevelEmpty | Self::InvalidOccurrenceOutsideMessage => {
334                matches!(l_of(level), L::Interchange | L::Group)
335            }
336            Self::TooManyRepetitions => {
337                matches!(l_of(level), L::Message | L::Segment | L::DataElement)
338            }
339            Self::TooManyGroupRepetitions => matches!(l_of(level), L::Segment),
340            Self::InvalidCharacterType | Self::DataElementTooLong | Self::DataElementTooShort => {
341                matches!(
342                    l_of(level),
343                    L::Interchange | L::Group | L::Message | L::DataElement
344                )
345            }
346            Self::TrailingSeparator => matches!(
347                l_of(level),
348                L::Interchange | L::Group | L::Message | L::Segment
349            ),
350            Self::EnvelopeFunctionalityNotSupported => matches!(l_of(level), L::Group | L::Message),
351        }
352    }
353
354    /// The `CONTRL` code that best names an [`EdifactError`].
355    ///
356    /// ISO 9735-4 §5.3.3 asks for the most precise code available and warns
357    /// against reaching for a general one when a specific one fits, so this maps
358    /// each variant to the narrowest Annex A code that is true of it, falling
359    /// back to `18` (unspecified) only where the annex genuinely offers nothing
360    /// better.
361    ///
362    /// # Example
363    ///
364    /// ```
365    /// use edifact_rs::contrl::SyntaxError;
366    /// use edifact_rs::EdifactError;
367    ///
368    /// let err = EdifactError::MessageCountMismatch { expected: 2, actual: 1 };
369    /// assert_eq!(SyntaxError::for_error(&err), SyntaxError::ControlCountMismatch);
370    /// assert_eq!(SyntaxError::for_error(&err).code(), "29");
371    /// ```
372    #[must_use]
373    pub fn for_error(error: &EdifactError) -> Self {
374        use EdifactError as E;
375        match error {
376            E::MessageCountMismatch { .. } | E::SegmentCountMismatch { .. } => {
377                Self::ControlCountMismatch
378            }
379            E::QualifierMismatch { .. } => Self::ReferencesDoNotMatch,
380            E::DuplicateReference { .. } => Self::DuplicateDetected,
381            E::MissingRequiredElement { .. }
382            | E::MissingRequiredComponent { .. }
383            | E::MissingSegment { .. } => Self::Missing,
384            E::InvalidCodeValue { .. } | E::InvalidFieldValue { .. } => Self::InvalidValue,
385            E::InvalidSegmentForMessage { .. } | E::ConditionalRequirementNotMet { .. } => {
386                Self::NotSupportedHere
387            }
388            E::InvalidElementCount { .. } | E::InvalidComponentCount { .. } => {
389                Self::TooManyConstituents
390            }
391            E::UnrecognisedSyntaxIdentifier(_) | E::UnsupportedCharset { .. } => {
392                Self::CharacterSetNotSupported
393            }
394            E::CharacterNotInRepertoire { .. } | E::InvalidText { .. } => Self::InvalidCharacters,
395            E::InvalidUna | E::InvalidDelimiter { .. } | E::InvalidReleaseSequence { .. } => {
396                Self::InvalidServiceCharacters
397            }
398            E::EmptyInterchange { .. } | E::EmptyMessage { .. } => Self::LowerLevelEmpty,
399            E::SegmentWithoutDataElements { .. } => Self::Missing,
400            E::BlankDataElementValue { .. } => Self::InvalidValue,
401            E::PackageNotSupported { .. } => Self::EnvelopeFunctionalityNotSupported,
402            E::SegmentTooLong { .. } | E::DataElementTooLong { .. } => Self::DataElementTooLong,
403            E::DataElementTooShort { .. } => Self::DataElementTooShort,
404            E::InvalidCharacterType { .. } => Self::InvalidCharacterType,
405            E::TooManyRepetitions { .. } => Self::TooManyRepetitions,
406            E::TrailingSeparator { .. } => Self::TrailingSeparator,
407            E::GroupsAndMessagesMixed { .. } => Self::GroupsAndMessagesMixed,
408            E::InsignificantCharacters { .. } => Self::InvalidValue,
409            E::UnexpectedDataToken { .. } | E::InvalidSegmentTag(_) => {
410                Self::InvalidOccurrenceOutsideMessage
411            }
412            _ => Self::Unspecified,
413        }
414    }
415
416    /// The best `CONTRL` code for a [`ValidationIssue`], via its stable code.
417    ///
418    /// Works for issues raised by *any* validator, including third-party ones,
419    /// because it reads [`ValidationIssue::error_code`] rather than needing the
420    /// original [`EdifactError`]. An issue with no stable code — a profile rule
421    /// finding, typically — is `18` (unspecified), which is what the annex
422    /// provides for a fault it does not name.
423    #[must_use]
424    pub fn for_issue(issue: &ValidationIssue) -> Self {
425        match issue.error_code() {
426            Some("E004" | "E005") => Self::ControlCountMismatch,
427            Some("E016") => Self::ReferencesDoNotMatch,
428            Some("E032") => Self::DuplicateDetected,
429            Some("E008" | "E015" | "E021" | "E046") => Self::Missing,
430            Some("E014" | "E027" | "E045") => Self::InvalidValue,
431            Some("E011" | "E017") => Self::NotSupportedHere,
432            Some("E012" | "E013") => Self::TooManyConstituents,
433            Some("E031" | "E039") => Self::CharacterSetNotSupported,
434            Some("E003" | "E038") => Self::InvalidCharacters,
435            Some("E002" | "E007" | "E019") => Self::InvalidServiceCharacters,
436            Some("E042" | "E043") => Self::LowerLevelEmpty,
437            Some("E044") => Self::EnvelopeFunctionalityNotSupported,
438            Some("E020" | "E049") => Self::DataElementTooLong,
439            Some("E050") => Self::DataElementTooShort,
440            Some("E048") => Self::InvalidCharacterType,
441            Some("E047") => Self::TooManyRepetitions,
442            Some("E051") => Self::TrailingSeparator,
443            Some("E052") => Self::GroupsAndMessagesMixed,
444            Some("E053") => Self::InvalidValue,
445            Some("E006" | "E028") => Self::InvalidOccurrenceOutsideMessage,
446            _ => Self::Unspecified,
447        }
448    }
449}
450
451/// Identity helper that keeps [`SyntaxError::permitted_at`] usable in `const`.
452///
453/// `match` on a `#[non_exhaustive]` enum from inside its own crate is fine; this
454/// exists only so the arms below read as `L::Interchange` rather than repeating
455/// the full path, without tripping the `const fn` restriction on `use` inside a
456/// match guard.
457#[inline]
458const fn l_of(level: ReportingLevel) -> ReportingLevel {
459    level
460}
461
462impl std::fmt::Display for SyntaxError {
463    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464        write!(f, "{} ({})", self.code(), self.description())
465    }
466}
467
468// ── the message ───────────────────────────────────────────────────────────────
469
470/// What part of the subject interchange a finding was traced to.
471///
472/// Messages and groups are keyed by **position** rather than by their control
473/// reference: the reference is ambiguous in exactly the case a `CONTRL` most
474/// needs to be precise about, namely when two of them share one and that
475/// duplication is the fault being reported.
476#[derive(Debug, Clone, Copy, PartialEq, Eq)]
477enum Scope {
478    /// The `UNG`/`UNE` envelope of the group at this index.
479    Group(usize),
480    /// A segment inside the message at this flat index across the interchange.
481    Message {
482        index: usize,
483        /// One-based segment position within that message, `UNH` being 1.
484        segment_position: u32,
485    },
486}
487
488/// One finding placed at a `CONTRL` reporting level.
489#[derive(Debug, Clone, PartialEq, Eq)]
490struct Finding {
491    scope: Scope,
492    /// One-based data element position within that segment, the tag being 1.
493    element_position: Option<u32>,
494    /// One-based component position within that data element.
495    component_position: Option<u32>,
496    /// Whether this finding rejects its level, or merely annotates it.
497    ///
498    /// §5.3.3 permits reporting an error on a level that is nonetheless
499    /// acknowledged, which is exactly what a warning is.
500    rejects: bool,
501    error: SyntaxError,
502}
503
504impl Finding {
505    /// The lowest reporting level that can both locate this fault and legally
506    /// carry its code.
507    ///
508    /// §5.3.3 asks for the lowest possible level, but Annex A decides which
509    /// levels may carry which code — "duplicate detected", for instance, is
510    /// meaningless below the message. Descending past the deepest *permitted*
511    /// level would emit a `UCS` with no code at all, which tells the partner
512    /// that a segment is wrong without saying how.
513    fn level(&self) -> ReportingLevel {
514        if let Scope::Group(_) = self.scope {
515            return ReportingLevel::Group;
516        }
517        if self.element_position.is_some() && self.error.permitted_at(ReportingLevel::DataElement) {
518            return ReportingLevel::DataElement;
519        }
520        if self.error.permitted_at(ReportingLevel::Segment) {
521            return ReportingLevel::Segment;
522        }
523        ReportingLevel::Message
524    }
525
526    /// The flat message index this finding belongs to, if it is inside a message.
527    fn message_index(&self) -> Option<usize> {
528        match self.scope {
529            Scope::Message { index, .. } => Some(index),
530            Scope::Group(_) => None,
531        }
532    }
533}
534
535/// A `CONTRL` message under construction.
536///
537/// Build one with [`receipt`][Self::receipt], [`acknowledgement`][Self::acknowledgement],
538/// or [`from_report`][Self::from_report], then render it with
539/// [`segments`][Self::segments] or [`to_edifact_string`][Self::to_edifact_string].
540///
541/// The message reference defaults to the subject interchange's control reference,
542/// which keeps a `CONTRL` traceable to what it answers without the caller having
543/// to invent one; override it with
544/// [`with_message_reference`][Self::with_message_reference].
545#[derive(Debug, Clone)]
546pub struct Contrl {
547    control_ref: String,
548    sender: String,
549    sender_qualifier: String,
550    recipient: String,
551    recipient_qualifier: String,
552    action: Action,
553    interchange_error: Option<SyntaxError>,
554    message_ref: Option<String>,
555    /// Reported messages: their flat index in the subject interchange, their
556    /// identity, and the verdict on them.
557    ///
558    /// Populated only when the subject interchange uses **no** groups —
559    /// ISO 9735-4 §5.3.1 makes segment groups 1 and 3 mutually exclusive.
560    messages: Vec<(usize, MessageEnvelope, Action)>,
561    /// Reported groups and, under each, the messages it contains.
562    ///
563    /// Populated only when the subject interchange **does** use groups.
564    groups: Vec<GroupReport>,
565    findings: Vec<Finding>,
566}
567
568/// One `UCF` and the `UCM`s beneath it.
569#[derive(Debug, Clone)]
570struct GroupReport {
571    envelope: FunctionalGroupEnvelope,
572    action: Action,
573    error: Option<SyntaxError>,
574    /// Flat message index, identity, and verdict for each reported message.
575    messages: Vec<(usize, MessageEnvelope, Action)>,
576}
577
578impl Contrl {
579    fn base(interchange: &InterchangeEnvelope, action: Action) -> Self {
580        Self {
581            control_ref: interchange.control_ref.clone(),
582            sender: interchange.sender_id.clone(),
583            sender_qualifier: interchange.sender_qualifier.clone(),
584            recipient: interchange.recipient_id.clone(),
585            recipient_qualifier: interchange.recipient_qualifier.clone(),
586            action,
587            interchange_error: None,
588            message_ref: None,
589            messages: Vec::new(),
590            groups: Vec::new(),
591            findings: Vec::new(),
592        }
593    }
594
595    /// The receipt message of ISO 9735-4 §5.3.1 — action code `8`.
596    ///
597    /// Says only that the interchange arrived. It carries no per-message
598    /// reporting, because nothing has been checked yet.
599    #[must_use]
600    pub fn receipt(interchange: &InterchangeEnvelope) -> Self {
601        Self::base(interchange, Action::Received)
602    }
603
604    /// Acknowledge a clean interchange — action code `7` throughout.
605    ///
606    /// Every message is acknowledged implicitly by the `UCI`, so no `UCM` is
607    /// emitted: §5.3.4 makes explicit acknowledgement of every message redundant
608    /// when the interchange level already says so.
609    #[must_use]
610    pub fn acknowledgement(subject: &ValidatedInterchange) -> Self {
611        Self::base(&subject.interchange, Action::Acknowledged)
612    }
613
614    /// Report the outcome of a validation run.
615    ///
616    /// `segments` is the subject interchange as parsed — it is what turns an
617    /// issue's byte span into the segment position `UCS` DE 0096 needs, so
618    /// findings land at the right place rather than all piling up on the `UCI`.
619    ///
620    /// # Where a rejection lands
621    ///
622    /// Action code `4` means "this level **and all lower levels** rejected", so
623    /// putting it on the `UCI` because one message was bad would reject every
624    /// other message in the interchange too. §5.3.2 pairs it with code `7` —
625    /// "this level acknowledged, next lower level acknowledged **if not
626    /// explicitly rejected**" — and that is the combination used here:
627    ///
628    /// - A fault in the interchange envelope itself rejects the `UCI`, and
629    ///   nothing follows, because everything below is implicitly rejected.
630    /// - A fault in a group's `UNG`/`UNE` rejects that group's `UCF`.
631    /// - A fault inside a message leaves both above it at `7` and rejects only
632    ///   that message's `UCM`.
633    ///
634    /// Warnings are reported but reject nothing, which matches the way
635    /// [`ValidationReport`] already separates the two.
636    ///
637    /// # Grouped interchanges
638    ///
639    /// §5.3.1 makes segment groups 1 and 3 mutually exclusive: a subject that
640    /// uses `UNG`/`UNE` is reported through `UCF`, and one that does not is
641    /// reported through `UCM` directly under the `UCI`. Which shape you get
642    /// follows the subject — there is nothing to configure.
643    #[must_use]
644    pub fn from_report(
645        subject: &ValidatedInterchange,
646        segments: &[Segment<'_>],
647        report: &ValidationReport,
648    ) -> Self {
649        let mut contrl = Self::base(&subject.interchange, Action::Acknowledged);
650
651        // Boundaries let a byte span be resolved to the deepest structure that
652        // contains it: a message first, then the group around it.
653        let messages = message_boundaries(segments);
654        let groups = group_boundaries(segments);
655
656        for (issue, rejects) in report
657            .errors()
658            .iter()
659            .map(|i| (i, true))
660            .chain(report.warnings().iter().map(|i| (i, false)))
661        {
662            let error = SyntaxError::for_issue(issue);
663            let located = issue
664                .span
665                .and_then(|span| locate(segments, &messages, &groups, span.start));
666
667            // §5.3.3: report at the lowest level that can express the fault, and
668            // never repeat the code higher up.  An issue that resolves to no
669            // structure at all is about the interchange envelope.
670            let Some(scope) = located else {
671                if rejects {
672                    contrl.action = Action::Rejected;
673                }
674                if contrl.interchange_error.is_none()
675                    && error.permitted_at(ReportingLevel::Interchange)
676                {
677                    contrl.interchange_error = Some(error);
678                }
679                continue;
680            };
681
682            contrl.findings.push(Finding {
683                scope,
684                // DE 0098 counts the segment tag as position 1, so a zero-based
685                // element index is two positions further along.
686                element_position: issue.element_index.map(|i| u32::from(i) + 2),
687                component_position: issue.component_index.map(|i| u32::from(i) + 1),
688                rejects,
689                error,
690            });
691        }
692
693        // A rejected UCI already rejects everything under it (code 4), so
694        // anything further would either repeat that or contradict it.
695        if contrl.action == Action::Rejected {
696            contrl.findings.clear();
697            return contrl;
698        }
699
700        if subject.functional_groups.is_empty() {
701            contrl.messages = contrl.reported_messages(subject.messages.iter().enumerate());
702        } else {
703            contrl.build_group_reports(subject);
704        }
705
706        contrl
707    }
708
709    /// Select the messages that need a `UCM`, with their verdicts.
710    ///
711    /// A clean message needs none: the level above already acknowledged it, and
712    /// §5.3.4 makes the redundant `UCM` pointless.
713    fn reported_messages<'m>(
714        &self,
715        candidates: impl Iterator<Item = (usize, &'m MessageEnvelope)>,
716    ) -> Vec<(usize, MessageEnvelope, Action)> {
717        let mut out = Vec::new();
718        for (index, message) in candidates {
719            let mut has_finding = false;
720            let mut rejected = false;
721            for finding in &self.findings {
722                if finding.message_index() == Some(index) {
723                    has_finding = true;
724                    rejected |= finding.rejects;
725                }
726            }
727            if !has_finding {
728                continue;
729            }
730            out.push((
731                index,
732                message.clone(),
733                if rejected {
734                    Action::Rejected
735                } else {
736                    Action::Acknowledged
737                },
738            ));
739        }
740        out
741    }
742
743    /// Build segment group 3: one `UCF` per group, each with its own messages.
744    fn build_group_reports(&mut self, subject: &ValidatedInterchange) {
745        // `ValidatedInterchange::messages` is the concatenation of the groups'
746        // message lists in document order, so a running counter converts a
747        // per-group position into the flat index findings are keyed by.
748        let mut flat = 0usize;
749        let mut reports = Vec::new();
750
751        for (group_index, group) in subject.functional_groups.iter().enumerate() {
752            let start = flat;
753            flat += group.messages.len();
754
755            let group_fault = self
756                .findings
757                .iter()
758                .find(|f| f.scope == Scope::Group(group_index));
759            let rejected = group_fault.is_some_and(|f| f.rejects);
760
761            // A rejected UCF rejects every message under it, so it carries no
762            // UCM — exactly as a rejected UCI carries no UCF.
763            let messages = if rejected {
764                Vec::new()
765            } else {
766                self.reported_messages(
767                    group
768                        .messages
769                        .iter()
770                        .enumerate()
771                        .map(|(offset, message)| (start + offset, message)),
772                )
773            };
774
775            if group_fault.is_none() && messages.is_empty() {
776                continue; // The UCI's acknowledgement already covers it.
777            }
778
779            reports.push(GroupReport {
780                envelope: group.clone(),
781                action: if rejected {
782                    Action::Rejected
783                } else {
784                    Action::Acknowledged
785                },
786                error: group_fault
787                    .map(|f| f.error)
788                    .filter(|e| e.permitted_at(ReportingLevel::Group)),
789                messages,
790            });
791        }
792        self.groups = reports;
793    }
794
795    /// Set the `UNH` message reference (DE 0062) of the `CONTRL` itself.
796    ///
797    /// Defaults to the subject interchange's control reference.
798    #[must_use]
799    pub fn with_message_reference(mut self, reference: impl Into<String>) -> Self {
800        self.message_ref = Some(reference.into());
801        self
802    }
803
804    /// Record an interchange-level error on the `UCI`.
805    ///
806    /// Ignored when `error` is not permitted at the interchange level by
807    /// ISO 9735-4 Annex A — a `CONTRL` carrying a code its level may not use is
808    /// one the partner's translator rejects, which helps nobody.
809    #[must_use]
810    pub fn with_interchange_error(mut self, error: SyntaxError) -> Self {
811        if error.permitted_at(ReportingLevel::Interchange) {
812            self.interchange_error = Some(error);
813        }
814        self
815    }
816
817    /// The verdict this message carries at the interchange level.
818    #[must_use]
819    pub const fn action(&self) -> Action {
820        self.action
821    }
822
823    /// The `UNH` message reference this `CONTRL` will carry.
824    #[must_use]
825    pub fn message_reference(&self) -> &str {
826        self.message_ref.as_deref().unwrap_or(&self.control_ref)
827    }
828
829    /// Render the message as segments, `UNH` through `UNT`.
830    ///
831    /// `UNT` DE 0074 is computed from what was actually emitted, so the count is
832    /// right by construction rather than by the caller remembering to update it.
833    #[must_use]
834    pub fn segments(&self) -> Vec<OwnedSegment> {
835        let mut out = Vec::new();
836        let reference = self.message_reference().to_owned();
837
838        // UNH — the message type is fixed by ISO 9735-4 §5.4.1.
839        out.push(OwnedSegment::new(
840            "UNH",
841            vec![
842                OwnedElement::of(std::slice::from_ref(&reference)),
843                OwnedElement::of(&["CONTRL", "4", "1", "UN"]),
844            ],
845        ));
846
847        // UCI — interchange level.
848        let mut uci = vec![
849            OwnedElement::of(std::slice::from_ref(&self.control_ref)),
850            party(&self.sender, &self.sender_qualifier),
851            party(&self.recipient, &self.recipient_qualifier),
852            OwnedElement::of(&[self.action.code()]),
853        ];
854        if let Some(error) = self.interchange_error {
855            uci.push(OwnedElement::of(&[error.code()]));
856        }
857        out.push(OwnedSegment::new("UCI", uci));
858
859        // SG1 (ungrouped) or SG3 (grouped) — never both, per §5.3.1.
860        for (index, message, action) in &self.messages {
861            out.extend(self.message_report(*index, message, *action));
862        }
863        for group in &self.groups {
864            let mut ucf = vec![
865                OwnedElement::of(std::slice::from_ref(&group.envelope.group_ref)),
866                party(
867                    &group.envelope.app_sender,
868                    &group.envelope.app_sender_qualifier,
869                ),
870                party(
871                    &group.envelope.app_recipient,
872                    &group.envelope.app_recipient_qualifier,
873                ),
874                OwnedElement::of(&[group.action.code()]),
875            ];
876            if let Some(error) = group.error {
877                ucf.push(OwnedElement::of(&[error.code()]));
878            }
879            out.push(OwnedSegment::new("UCF", ucf));
880            for (index, message, action) in &group.messages {
881                out.extend(self.message_report(*index, message, *action));
882            }
883        }
884
885        let count = (out.len() + 1).to_string();
886        out.push(OwnedSegment::new(
887            "UNT",
888            vec![OwnedElement::of(&[count]), OwnedElement::of(&[reference])],
889        ));
890        out
891    }
892
893    /// Emit one `UCM` and the `UCS`/`UCD` pairs beneath it.
894    fn message_report(
895        &self,
896        index: usize,
897        message: &MessageEnvelope,
898        action: Action,
899    ) -> Vec<OwnedSegment> {
900        let mut ucm = vec![
901            OwnedElement::of(std::slice::from_ref(&message.message_ref)),
902            OwnedElement::of(&[
903                message.message_type.clone(),
904                message.version.clone(),
905                message.release.clone(),
906                message.controlling_agency.clone(),
907            ]),
908            OwnedElement::of(&[action.code()]),
909        ];
910        // A fault that no lower level may carry is reported here, on DE 0085.
911        if let Some(finding) = self
912            .findings
913            .iter()
914            .find(|f| f.message_index() == Some(index) && f.level() == ReportingLevel::Message)
915        {
916            ucm.push(OwnedElement::of(&[finding.error.code()]));
917        }
918        let mut out = vec![OwnedSegment::new("UCM", ucm)];
919        out.extend(self.segment_reports(index));
920        out
921    }
922
923    /// Emit the `UCS`/`UCD` pairs (segment group 2 or 5) for one message.
924    ///
925    /// Findings that belong on the `UCM` are skipped here —
926    /// [`message_report`][Self::message_report] has already placed them.
927    fn segment_reports(&self, message_index: usize) -> Vec<OwnedSegment> {
928        let mut out = Vec::new();
929        let mut reported_positions: Vec<u32> = Vec::new();
930
931        for finding in self
932            .findings
933            .iter()
934            .filter(|f| f.message_index() == Some(message_index))
935        {
936            let level = finding.level();
937            let Scope::Message {
938                segment_position: position,
939                ..
940            } = finding.scope
941            else {
942                continue;
943            };
944            if level == ReportingLevel::Message {
945                continue;
946            }
947            // §5.3.3: no more than one reporting level per referenced level, so a
948            // second finding on the same segment extends it with a UCD rather
949            // than opening a second UCS for it.
950            if !reported_positions.contains(&position) {
951                reported_positions.push(position);
952                let position_text = position.to_string();
953                let mut ucs = vec![OwnedElement::of(&[position_text])];
954                // The UCS states the code only when the segment itself is the
955                // deepest level that can carry it.
956                if level == ReportingLevel::Segment {
957                    ucs.push(OwnedElement::of(&[finding.error.code()]));
958                }
959                out.push(OwnedSegment::new("UCS", ucs));
960            }
961
962            if level == ReportingLevel::DataElement {
963                let element_text = finding
964                    .element_position
965                    .expect("DataElement level implies a known element position")
966                    .to_string();
967                let mut identification = vec![element_text];
968                if let Some(component) = finding.component_position {
969                    identification.push(component.to_string());
970                }
971                out.push(OwnedSegment::new(
972                    "UCD",
973                    vec![
974                        OwnedElement::of(&[finding.error.code()]),
975                        OwnedElement::of(&identification),
976                    ],
977                ));
978            }
979        }
980        out
981    }
982
983    /// Render the message to EDIFACT bytes, without an interchange envelope.
984    ///
985    /// # Errors
986    ///
987    /// Propagates any writer failure.
988    pub fn to_bytes(&self) -> Result<Vec<u8>, EdifactError> {
989        crate::segments_to_bytes(&self.segments())
990    }
991
992    /// Render the message to an EDIFACT string, without an interchange envelope.
993    ///
994    /// # Errors
995    ///
996    /// As [`to_bytes`][Self::to_bytes], plus [`EdifactError::InvalidUtf8`].
997    pub fn to_edifact_string(&self) -> Result<String, EdifactError> {
998        String::from_utf8(self.to_bytes()?).map_err(|_| EdifactError::InvalidUtf8)
999    }
1000
1001    /// Wrap the message in its own interchange and render it.
1002    ///
1003    /// ISO 9735-4 §5.3 requires a `CONTRL` to travel in an interchange of its
1004    /// own and never inside a group, so this is the form to actually send. The
1005    /// sender and recipient are swapped relative to the subject interchange,
1006    /// because the reply goes back the way it came.
1007    ///
1008    /// # Example
1009    ///
1010    /// ```
1011    /// # use edifact_rs::{Contrl, from_bytes, validate_envelope};
1012    /// # let raw = b"UNB+UNOC:3+SENDER+RECEIVER+260101:0900+IC4711'UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'UNZ+1+IC4711'";
1013    /// # let segments: Vec<_> = from_bytes(raw).collect::<Result<Vec<_>, _>>()?;
1014    /// # let validated = validate_envelope(&segments)?;
1015    /// let wire = Contrl::acknowledgement(&validated)
1016    ///     .to_interchange_string("UNOC", "3", "260101", "0930", "ACK-1")?;
1017    ///
1018    /// // The reply is addressed back to the original sender.
1019    /// assert!(wire.starts_with("UNB+UNOC:3+RECEIVER+SENDER+260101:0930+ACK-1'"));
1020    /// assert!(wire.ends_with("UNZ+1+ACK-1'"));
1021    /// # Ok::<(), edifact_rs::EdifactError>(())
1022    /// ```
1023    ///
1024    /// # Errors
1025    ///
1026    /// Propagates any writer failure.
1027    pub fn to_interchange_bytes(
1028        &self,
1029        syntax_identifier: &str,
1030        syntax_version: &str,
1031        date: &str,
1032        time: &str,
1033        control_reference: &str,
1034    ) -> Result<Vec<u8>, EdifactError> {
1035        let mut writer = Writer::new(Vec::new());
1036        writer.begin_interchange(
1037            syntax_identifier,
1038            syntax_version,
1039            // The reply goes back the way it came.
1040            &self.recipient,
1041            &self.sender,
1042            date,
1043            time,
1044            control_reference,
1045        )?;
1046        for segment in self.segments() {
1047            writer.write_segment(&segment)?;
1048        }
1049        writer.end_interchange(1, control_reference)?;
1050        writer.finish()
1051    }
1052
1053    /// String form of [`to_interchange_bytes`][Self::to_interchange_bytes].
1054    ///
1055    /// # Errors
1056    ///
1057    /// As [`to_interchange_bytes`][Self::to_interchange_bytes], plus
1058    /// [`EdifactError::InvalidUtf8`].
1059    pub fn to_interchange_string(
1060        &self,
1061        syntax_identifier: &str,
1062        syntax_version: &str,
1063        date: &str,
1064        time: &str,
1065        control_reference: &str,
1066    ) -> Result<String, EdifactError> {
1067        let bytes = self.to_interchange_bytes(
1068            syntax_identifier,
1069            syntax_version,
1070            date,
1071            time,
1072            control_reference,
1073        )?;
1074        String::from_utf8(bytes).map_err(|_| EdifactError::InvalidUtf8)
1075    }
1076}
1077
1078/// Build an S002/S003-shaped element, omitting an absent qualifier.
1079fn party(id: &str, qualifier: &str) -> OwnedElement {
1080    if qualifier.is_empty() {
1081        OwnedElement::of(&[id.to_owned()])
1082    } else {
1083        OwnedElement::of(&[id.to_owned(), qualifier.to_owned()])
1084    }
1085}
1086
1087/// `(first segment index, last segment index)` per message, in document order.
1088///
1089/// The order matches [`ValidatedInterchange::messages`], which is what lets a
1090/// finding be keyed by message index rather than by a reference that may repeat.
1091fn message_boundaries(segments: &[Segment<'_>]) -> Vec<(usize, usize)> {
1092    spans_between(segments, "UNH", "UNT")
1093}
1094
1095/// `(first segment index, last segment index)` per group, in document order.
1096///
1097/// The order matches [`ValidatedInterchange::functional_groups`].
1098fn group_boundaries(segments: &[Segment<'_>]) -> Vec<(usize, usize)> {
1099    spans_between(segments, "UNG", "UNE")
1100}
1101
1102/// Index ranges of each `open`…`close` pair, inclusive of both ends.
1103fn spans_between(segments: &[Segment<'_>], open: &str, close: &str) -> Vec<(usize, usize)> {
1104    let mut out = Vec::new();
1105    let mut start: Option<usize> = None;
1106    for (index, segment) in segments.iter().enumerate() {
1107        if segment.tag == open {
1108            start = Some(index);
1109        } else if segment.tag == close {
1110            if let Some(from) = start.take() {
1111                out.push((from, index));
1112            }
1113        }
1114    }
1115    out
1116}
1117
1118/// Resolve a byte offset to the deepest structure that contains it.
1119///
1120/// Going through the byte span rather than through a dedicated field on
1121/// [`ValidationIssue`] is what makes this work for findings from *any* validator,
1122/// including third-party ones that know nothing about `CONTRL`.
1123///
1124/// A message wins over the group around it, because §5.3.3 wants the lowest
1125/// level that can express the fault. `None` means the offset fell in neither —
1126/// the `UNB`, the `UNZ`, or the gap between structures — which makes it the
1127/// interchange's own.
1128///
1129/// `UCS` DE 0096 counts from the `UNH` as position 1, so the position is the
1130/// segment's offset from its message header plus one.
1131fn locate(
1132    segments: &[Segment<'_>],
1133    messages: &[(usize, usize)],
1134    groups: &[(usize, usize)],
1135    offset: usize,
1136) -> Option<Scope> {
1137    // Segments are in source order, so the containing one is the last whose
1138    // span starts at or before the offset.
1139    let index = match segments.binary_search_by(|segment| segment.span.start.cmp(&offset)) {
1140        Ok(exact) => exact,
1141        Err(0) => return None,
1142        Err(next) => next - 1,
1143    };
1144    if let Some((message_index, (start, _))) = messages
1145        .iter()
1146        .enumerate()
1147        .find(|(_, (start, end))| (*start..=*end).contains(&index))
1148    {
1149        return u32::try_from(index - start + 1)
1150            .ok()
1151            .map(|segment_position| Scope::Message {
1152                index: message_index,
1153                segment_position,
1154            });
1155    }
1156    groups
1157        .iter()
1158        .position(|(start, end)| (*start..=*end).contains(&index))
1159        .map(Scope::Group)
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164    use super::*;
1165
1166    fn parse(input: &[u8]) -> Vec<OwnedSegment> {
1167        crate::from_bytes(input)
1168            .map(|r| r.map(|s| s.into_owned()))
1169            .collect::<Result<Vec<_>, _>>()
1170            .expect("parse")
1171    }
1172
1173    #[test]
1174    fn annex_a_permits_a_code_only_where_the_table_says() {
1175        // Spot-checks straight out of the Annex A grid.
1176        assert!(SyntaxError::NotActualRecipient.permitted_at(ReportingLevel::Interchange));
1177        assert!(!SyntaxError::NotActualRecipient.permitted_at(ReportingLevel::Group));
1178        assert!(SyntaxError::TooManyGroupRepetitions.permitted_at(ReportingLevel::Segment));
1179        assert!(!SyntaxError::TooManyGroupRepetitions.permitted_at(ReportingLevel::DataElement));
1180        assert!(SyntaxError::LowerLevelEmpty.permitted_at(ReportingLevel::Group));
1181        assert!(!SyntaxError::LowerLevelEmpty.permitted_at(ReportingLevel::Message));
1182        assert!(SyntaxError::EnvelopeFunctionalityNotSupported.permitted_at(ReportingLevel::Group));
1183        assert!(
1184            !SyntaxError::EnvelopeFunctionalityNotSupported
1185                .permitted_at(ReportingLevel::Interchange)
1186        );
1187        // "Invalid value" is usable at every level.
1188        for level in [
1189            ReportingLevel::Interchange,
1190            ReportingLevel::Group,
1191            ReportingLevel::Message,
1192            ReportingLevel::Segment,
1193            ReportingLevel::DataElement,
1194        ] {
1195            assert!(SyntaxError::InvalidValue.permitted_at(level), "{level:?}");
1196        }
1197    }
1198
1199    #[test]
1200    fn every_code_is_permitted_somewhere() {
1201        // A code no level may carry would be unreachable, which would mean the
1202        // table above is wrong rather than merely strict.
1203        for error in ALL_ERRORS {
1204            assert!(
1205                [
1206                    ReportingLevel::Interchange,
1207                    ReportingLevel::Group,
1208                    ReportingLevel::Message,
1209                    ReportingLevel::Segment,
1210                    ReportingLevel::DataElement,
1211                ]
1212                .iter()
1213                .any(|level| error.permitted_at(*level)),
1214                "{error:?} is permitted nowhere"
1215            );
1216        }
1217    }
1218
1219    #[test]
1220    fn codes_are_unique() {
1221        let mut codes: Vec<&str> = ALL_ERRORS.iter().map(|e| e.code()).collect();
1222        codes.sort_unstable();
1223        let before = codes.len();
1224        codes.dedup();
1225        assert_eq!(before, codes.len(), "duplicate DE 0085 code");
1226    }
1227
1228    const ALL_ERRORS: &[SyntaxError] = &[
1229        SyntaxError::SyntaxVersionNotSupported,
1230        SyntaxError::NotActualRecipient,
1231        SyntaxError::InvalidValue,
1232        SyntaxError::Missing,
1233        SyntaxError::ValueNotSupportedHere,
1234        SyntaxError::NotSupportedHere,
1235        SyntaxError::TooManyConstituents,
1236        SyntaxError::NoAgreement,
1237        SyntaxError::Unspecified,
1238        SyntaxError::InvalidAsServiceCharacter,
1239        SyntaxError::InvalidCharacters,
1240        SyntaxError::InvalidServiceCharacters,
1241        SyntaxError::UnknownSender,
1242        SyntaxError::TooOld,
1243        SyntaxError::TestIndicatorNotSupported,
1244        SyntaxError::DuplicateDetected,
1245        SyntaxError::ReferencesDoNotMatch,
1246        SyntaxError::ControlCountMismatch,
1247        SyntaxError::GroupsAndMessagesMixed,
1248        SyntaxError::LowerLevelEmpty,
1249        SyntaxError::InvalidOccurrenceOutsideMessage,
1250        SyntaxError::TooManyRepetitions,
1251        SyntaxError::TooManyGroupRepetitions,
1252        SyntaxError::InvalidCharacterType,
1253        SyntaxError::DataElementTooLong,
1254        SyntaxError::DataElementTooShort,
1255        SyntaxError::TrailingSeparator,
1256        SyntaxError::CharacterSetNotSupported,
1257        SyntaxError::EnvelopeFunctionalityNotSupported,
1258    ];
1259
1260    #[test]
1261    fn an_acknowledgement_validates_against_the_shipped_layouts() {
1262        let raw = b"UNB+UNOC:3+SENDER:14+RECEIVER:14+260101:0900+IC4711'\
1263                    UNH+MSG1+ORDERS:D:96A:UN'BGM+220+PO-1+9'UNT+3+MSG1'\
1264                    UNZ+1+IC4711'";
1265        let owned = parse(raw);
1266        let validated = crate::validate_envelope(&owned).expect("valid subject");
1267
1268        let contrl = Contrl::acknowledgement(&validated).with_message_reference("ACK1");
1269        let wire = contrl.to_edifact_string().expect("render");
1270
1271        assert_eq!(
1272            wire,
1273            "UNH+ACK1+CONTRL:4:1:UN'UCI+IC4711+SENDER:14+RECEIVER:14+7'UNT+3+ACK1'"
1274        );
1275
1276        // And it is itself a well-formed CONTRL by the crate's own tables.
1277        let reparsed = parse(wire.as_bytes());
1278        let validator = crate::DirectoryValidator::new(
1279            "iso-9735-4",
1280            crate::service::lookup,
1281            |_, _| true,
1282            |_, _| None,
1283            |_, _| None,
1284            None,
1285        );
1286        let report = crate::ValidationContext::builder()
1287            .with_validator(crate::ValidationLayer::Structure, validator)
1288            .build()
1289            .validate(&reparsed);
1290        assert!(!report.has_errors(), "{:#?}", report.errors());
1291    }
1292
1293    #[test]
1294    fn a_receipt_carries_action_8_and_nothing_else() {
1295        let raw =
1296            b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'UNZ+1+IC1'";
1297        let owned = parse(raw);
1298        let validated = crate::validate_envelope(&owned).expect("valid subject");
1299
1300        let wire = Contrl::receipt(&validated.interchange)
1301            .to_edifact_string()
1302            .expect("render");
1303        assert!(wire.contains("UCI+IC1+S+R+8'"), "{wire}");
1304        assert_eq!(
1305            Contrl::receipt(&validated.interchange).action(),
1306            Action::Received
1307        );
1308    }
1309
1310    #[test]
1311    fn the_message_reference_defaults_to_the_subject_control_reference() {
1312        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC-42'UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'UNZ+1+IC-42'";
1313        let owned = parse(raw);
1314        let validated = crate::validate_envelope(&owned).expect("valid subject");
1315        assert_eq!(
1316            Contrl::acknowledgement(&validated).message_reference(),
1317            "IC-42"
1318        );
1319    }
1320
1321    #[test]
1322    fn a_forbidden_interchange_level_code_is_not_recorded() {
1323        let raw =
1324            b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'UNZ+1+IC1'";
1325        let owned = parse(raw);
1326        let validated = crate::validate_envelope(&owned).expect("valid subject");
1327
1328        // Annex A: "too many segment group repetitions" is UCS-only.
1329        let wire = Contrl::acknowledgement(&validated)
1330            .with_interchange_error(SyntaxError::TooManyGroupRepetitions)
1331            .to_edifact_string()
1332            .expect("render");
1333        assert!(wire.contains("UCI+IC1+S+R+7'"), "{wire}");
1334    }
1335
1336    /// Parse, validate, and build a CONTRL from the result.
1337    ///
1338    /// The lenient path, because that is the only one that yields an interchange
1339    /// *and* its faults — which is exactly what a CONTRL reports.
1340    fn report_for(raw: &[u8]) -> Contrl {
1341        let owned = parse(raw);
1342        let validated = crate::validate_envelope_lenient(&owned)
1343            .interchange
1344            .expect("subject must be structurally interpretable");
1345        let report = crate::ValidationContext::builder()
1346            .with_envelope_validation()
1347            .with_syntax_validation()
1348            .build()
1349            .validate(&owned);
1350        Contrl::from_report(&validated, &owned, &report)
1351    }
1352
1353    #[test]
1354    fn a_data_element_fault_is_reported_at_the_ucd_level() {
1355        // `FTX+   ` is a value of nothing but spaces — ISO 9735-1 §9.3, which
1356        // the syntax validator raises as a warning with a span and a component
1357        // index.  That is enough to place it precisely.
1358        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1359                    UNH+M1+ORDERS:D:96A:UN'FTX+   'UNT+3+M1'\
1360                    UNZ+1+IC1'";
1361        let contrl = report_for(raw);
1362
1363        // A warning does not reject: the interchange and the message stay
1364        // acknowledged, and the finding is still reported (§5.3.3).
1365        assert_eq!(contrl.action(), Action::Acknowledged);
1366        let wire = contrl.to_edifact_string().expect("render");
1367
1368        // FTX is the second segment of the message, counting UNH as 1.
1369        // Element 0 is DE position 2 (the tag is position 1); component 0 is 1.
1370        assert!(wire.contains("UCM+M1+ORDERS:D:96A:UN+7'"), "{wire}");
1371        assert!(wire.contains("UCS+2'"), "{wire}");
1372        assert!(wire.contains("UCD+12+2:1'"), "{wire}");
1373    }
1374
1375    #[test]
1376    fn a_rejected_message_is_named_by_its_ucm() {
1377        // Two messages share a UNH reference — a hard ISO 9735-1 violation with
1378        // a span on the offending UNH, so it lands on that message.
1379        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1380                    UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
1381                    UNH+M1+ORDERS:D:96A:UN'BGM+221'UNT+3+M1'\
1382                    UNZ+2+IC1'";
1383        let contrl = report_for(raw);
1384
1385        // The *interchange* envelope is fine — the fault is inside it. Code 4 on
1386        // the UCI would reject every other message too (§5.3.2), so the UCI
1387        // stays at 7 and only the offending message is explicitly rejected.
1388        assert_eq!(contrl.action(), Action::Acknowledged);
1389        let wire = contrl.to_edifact_string().expect("render");
1390
1391        // The duplicate belongs to the *second* message, and Annex A does not
1392        // allow code 26 below the message level — so it is reported on that
1393        // UCM's DE 0085 rather than pushed into a UCS that could not carry it.
1394        // The first message needs no UCM at all: the UCI's 7 covers it.
1395        assert_eq!(
1396            wire,
1397            "UNH+IC1+CONTRL:4:1:UN'\
1398             UCI+IC1+S+R+7'\
1399             UCM+M1+ORDERS:D:96A:UN+4+26'\
1400             UNT+4+IC1'"
1401        );
1402    }
1403
1404    #[test]
1405    fn an_envelope_fault_rejects_the_interchange_and_emits_no_ucm() {
1406        // A UNZ control reference that does not match the UNB is the envelope's
1407        // own fault, so code 4 on the UCI is right — and it implicitly rejects
1408        // every message, which is why no UCM follows.
1409        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1410                    UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
1411                    UNZ+1+IC-OTHER'";
1412        let contrl = report_for(raw);
1413
1414        assert_eq!(contrl.action(), Action::Rejected);
1415        let wire = contrl.to_edifact_string().expect("render");
1416        assert_eq!(wire, "UNH+IC1+CONTRL:4:1:UN'UCI+IC1+S+R+4+28'UNT+3+IC1'");
1417    }
1418
1419    #[test]
1420    fn a_count_mismatch_is_reported_on_the_message_that_got_it_wrong() {
1421        // UNT DE 0074 is the trailer's fault, so the finding belongs to that
1422        // message — not to the interchange, which is otherwise sound.
1423        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1424                    UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+9+M1'\
1425                    UNZ+1+IC1'";
1426        let contrl = report_for(raw);
1427
1428        assert_eq!(contrl.action(), Action::Acknowledged);
1429        let wire = contrl.to_edifact_string().expect("render");
1430        // Code 29 (control count mismatch) is not permitted below the message,
1431        // so it is reported on the UCM.
1432        assert_eq!(
1433            wire,
1434            "UNH+IC1+CONTRL:4:1:UN'\
1435             UCI+IC1+S+R+7'\
1436             UCM+M1+ORDERS:D:96A:UN+4+29'\
1437             UNT+4+IC1'"
1438        );
1439    }
1440
1441    #[test]
1442    fn a_finding_lands_at_the_lowest_level_annex_a_permits() {
1443        // "Duplicate detected" is legal at UCI/UCF/UCM and nowhere lower, so it
1444        // must stop at the message even though the fault has a segment position.
1445        let duplicate = Finding {
1446            scope: Scope::Message {
1447                index: 0,
1448                segment_position: 1,
1449            },
1450            element_position: Some(2),
1451            component_position: None,
1452            rejects: true,
1453            error: SyntaxError::DuplicateDetected,
1454        };
1455        assert_eq!(duplicate.level(), ReportingLevel::Message);
1456
1457        // "Invalid value" is legal everywhere, so it descends all the way.
1458        let invalid = Finding {
1459            error: SyntaxError::InvalidValue,
1460            ..duplicate.clone()
1461        };
1462        assert_eq!(invalid.level(), ReportingLevel::DataElement);
1463
1464        // …and stops at the segment when there is no data element to name.
1465        let segment_only = Finding {
1466            element_position: None,
1467            ..invalid.clone()
1468        };
1469        assert_eq!(segment_only.level(), ReportingLevel::Segment);
1470
1471        // "Too many segment group repetitions" is UCS-only: it cannot descend to
1472        // UCD even with an element position, and must not climb to UCM.
1473        let group_repetitions = Finding {
1474            error: SyntaxError::TooManyGroupRepetitions,
1475            ..duplicate
1476        };
1477        assert_eq!(group_repetitions.level(), ReportingLevel::Segment);
1478    }
1479
1480    #[test]
1481    fn a_generated_contrl_reparses_and_counts_its_own_segments() {
1482        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1483                    UNH+M1+ORDERS:D:96A:UN'FTX+   'UNT+3+M1'\
1484                    UNZ+1+IC1'";
1485        let contrl = report_for(raw);
1486
1487        let wire = contrl
1488            .to_interchange_string("UNOC", "3", "260101", "0930", "ACK-1")
1489            .expect("render");
1490
1491        // The generated interchange must satisfy the crate's own envelope rules,
1492        // including the UNT count this module computes for itself.
1493        let segments: Vec<_> = crate::from_bytes(wire.as_bytes())
1494            .collect::<Result<Vec<_>, _>>()
1495            .expect("generated CONTRL must reparse");
1496        let validated =
1497            crate::validate_envelope(&segments).expect("generated CONTRL must validate");
1498        assert_eq!(validated.messages.len(), 1);
1499        assert_eq!(validated.messages[0].message_type, "CONTRL");
1500        assert_eq!(validated.messages[0].version, "4");
1501        assert_eq!(validated.messages[0].release, "1");
1502        assert_eq!(
1503            validated.messages[0].declared_segment_count,
1504            validated.messages[0].actual_segment_count
1505        );
1506    }
1507
1508    #[test]
1509    fn a_grouped_interchange_is_reported_through_ucf() {
1510        // §5.3.1: segment groups 1 and 3 are mutually exclusive — a subject that
1511        // uses UNG/UNE is reported through UCF, never through a bare UCM.
1512        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1513                    UNG+ORDERS+SND:14+RCV:14+260101:0900+GRP1+UN+D:96A'\
1514                    UNH+M1+ORDERS:D:96A:UN'FTX+   'UNT+3+M1'\
1515                    UNE+1+GRP1'\
1516                    UNZ+1+IC1'";
1517        let contrl = report_for(raw);
1518
1519        assert_eq!(contrl.action(), Action::Acknowledged);
1520        let wire = contrl.to_edifact_string().expect("render");
1521
1522        // The UCF names the group by its own reference and application parties,
1523        // and the message report nests underneath it.
1524        assert_eq!(
1525            wire,
1526            "UNH+IC1+CONTRL:4:1:UN'\
1527             UCI+IC1+S+R+7'\
1528             UCF+GRP1+SND:14+RCV:14+7'\
1529             UCM+M1+ORDERS:D:96A:UN+7'\
1530             UCS+2'\
1531             UCD+12+2:1'\
1532             UNT+7+IC1'"
1533        );
1534    }
1535
1536    #[test]
1537    fn a_group_envelope_fault_rejects_only_that_group() {
1538        // The UNE reference does not match its UNG.  That is the *group's*
1539        // envelope, not the interchange's — so the UCI stays at 7 and the UCF
1540        // carries the rejection, which implicitly rejects the messages inside it.
1541        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1542                    UNG+ORDERS+SND+RCV+260101:0900+GRP1+UN+D:96A'\
1543                    UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
1544                    UNE+1+GRP-OTHER'\
1545                    UNZ+1+IC1'";
1546        let contrl = report_for(raw);
1547
1548        assert_eq!(contrl.action(), Action::Acknowledged);
1549        let wire = contrl.to_edifact_string().expect("render");
1550        assert_eq!(
1551            wire,
1552            "UNH+IC1+CONTRL:4:1:UN'\
1553             UCI+IC1+S+R+7'\
1554             UCF+GRP1+SND+RCV+4+28'\
1555             UNT+4+IC1'"
1556        );
1557    }
1558
1559    #[test]
1560    fn a_clean_grouped_interchange_needs_no_ucf() {
1561        // The UCI's code 7 already acknowledges every group and message under
1562        // it (§5.3.4); a UCF that only repeats that is noise.
1563        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1564                    UNG+ORDERS+SND+RCV+260101:0900+GRP1+UN+D:96A'\
1565                    UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
1566                    UNE+1+GRP1'\
1567                    UNZ+1+IC1'";
1568        let contrl = report_for(raw);
1569        assert_eq!(
1570            contrl.to_edifact_string().expect("render"),
1571            "UNH+IC1+CONTRL:4:1:UN'UCI+IC1+S+R+7'UNT+3+IC1'"
1572        );
1573    }
1574
1575    #[test]
1576    fn error_mapping_picks_the_narrowest_annex_a_code() {
1577        use EdifactError as E;
1578        let cases: [(EdifactError, SyntaxError); 6] = [
1579            (
1580                E::MessageCountMismatch {
1581                    expected: 1,
1582                    actual: 2,
1583                },
1584                SyntaxError::ControlCountMismatch,
1585            ),
1586            (
1587                E::DuplicateReference {
1588                    tag: "UNH".to_owned(),
1589                    reference: "1".to_owned(),
1590                    span: crate::Span::new(0, 1),
1591                },
1592                SyntaxError::DuplicateDetected,
1593            ),
1594            (
1595                E::UnsupportedCharset {
1596                    syntax_identifier: "UNOX".to_owned(),
1597                },
1598                SyntaxError::CharacterSetNotSupported,
1599            ),
1600            (E::InvalidUna, SyntaxError::InvalidServiceCharacters),
1601            (
1602                E::EmptyInterchange {
1603                    control_ref: "IC1".to_owned(),
1604                },
1605                SyntaxError::LowerLevelEmpty,
1606            ),
1607            (
1608                E::PackageNotSupported {
1609                    tag: "UNO".to_owned(),
1610                    span: crate::Span::new(0, 1),
1611                },
1612                SyntaxError::EnvelopeFunctionalityNotSupported,
1613            ),
1614        ];
1615        for (error, expected) in cases {
1616            assert_eq!(SyntaxError::for_error(&error), expected, "{error:?}");
1617        }
1618    }
1619}