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(&[reference.as_str()]),
843                OwnedElement::of(&["CONTRL", "4", "1", "UN"]),
844            ],
845        ));
846
847        // UCI — interchange level.
848        let mut uci = vec![
849            OwnedElement::of(&[self.control_ref.as_str()]),
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(&[group.envelope.group_ref.as_str()]),
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![
889                OwnedElement::of(&[count.as_str()]),
890                OwnedElement::of(&[reference.as_str()]),
891            ],
892        ));
893        out
894    }
895
896    /// Emit one `UCM` and the `UCS`/`UCD` pairs beneath it.
897    fn message_report(
898        &self,
899        index: usize,
900        message: &MessageEnvelope,
901        action: Action,
902    ) -> Vec<OwnedSegment> {
903        let mut ucm = vec![
904            OwnedElement::of(&[message.message_ref.as_str()]),
905            OwnedElement::of(&[
906                message.message_type.as_str(),
907                message.version.as_str(),
908                message.release.as_str(),
909                message.controlling_agency.as_str(),
910            ]),
911            OwnedElement::of(&[action.code()]),
912        ];
913        // A fault that no lower level may carry is reported here, on DE 0085.
914        if let Some(finding) = self
915            .findings
916            .iter()
917            .find(|f| f.message_index() == Some(index) && f.level() == ReportingLevel::Message)
918        {
919            ucm.push(OwnedElement::of(&[finding.error.code()]));
920        }
921        let mut out = vec![OwnedSegment::new("UCM", ucm)];
922        out.extend(self.segment_reports(index));
923        out
924    }
925
926    /// Emit the `UCS`/`UCD` pairs (segment group 2 or 5) for one message.
927    ///
928    /// Findings that belong on the `UCM` are skipped here —
929    /// [`message_report`][Self::message_report] has already placed them.
930    fn segment_reports(&self, message_index: usize) -> Vec<OwnedSegment> {
931        let mut out = Vec::new();
932        let mut reported_positions: Vec<u32> = Vec::new();
933
934        for finding in self
935            .findings
936            .iter()
937            .filter(|f| f.message_index() == Some(message_index))
938        {
939            let level = finding.level();
940            let Scope::Message {
941                segment_position: position,
942                ..
943            } = finding.scope
944            else {
945                continue;
946            };
947            if level == ReportingLevel::Message {
948                continue;
949            }
950            // §5.3.3: no more than one reporting level per referenced level, so a
951            // second finding on the same segment extends it with a UCD rather
952            // than opening a second UCS for it.
953            if !reported_positions.contains(&position) {
954                reported_positions.push(position);
955                let position_text = position.to_string();
956                let mut ucs = vec![OwnedElement::of(&[position_text.as_str()])];
957                // The UCS states the code only when the segment itself is the
958                // deepest level that can carry it.
959                if level == ReportingLevel::Segment {
960                    ucs.push(OwnedElement::of(&[finding.error.code()]));
961                }
962                out.push(OwnedSegment::new("UCS", ucs));
963            }
964
965            if level == ReportingLevel::DataElement {
966                let element_text = finding
967                    .element_position
968                    .expect("DataElement level implies a known element position")
969                    .to_string();
970                let mut identification = vec![element_text];
971                if let Some(component) = finding.component_position {
972                    identification.push(component.to_string());
973                }
974                out.push(OwnedSegment::new(
975                    "UCD",
976                    vec![
977                        OwnedElement::of(&[finding.error.code()]),
978                        OwnedElement::of(&identification),
979                    ],
980                ));
981            }
982        }
983        out
984    }
985
986    /// Render the message to EDIFACT bytes, without an interchange envelope.
987    ///
988    /// # Errors
989    ///
990    /// Propagates any writer failure.
991    pub fn to_bytes(&self) -> Result<Vec<u8>, EdifactError> {
992        crate::segments_to_bytes_owned(&self.segments())
993    }
994
995    /// Render the message to an EDIFACT string, without an interchange envelope.
996    ///
997    /// # Errors
998    ///
999    /// As [`to_bytes`][Self::to_bytes], plus [`EdifactError::InvalidUtf8`].
1000    pub fn to_edifact_string(&self) -> Result<String, EdifactError> {
1001        String::from_utf8(self.to_bytes()?).map_err(|_| EdifactError::InvalidUtf8)
1002    }
1003
1004    /// Wrap the message in its own interchange and render it.
1005    ///
1006    /// ISO 9735-4 §5.3 requires a `CONTRL` to travel in an interchange of its
1007    /// own and never inside a group, so this is the form to actually send. The
1008    /// sender and recipient are swapped relative to the subject interchange,
1009    /// because the reply goes back the way it came.
1010    ///
1011    /// # Example
1012    ///
1013    /// ```
1014    /// # use edifact_rs::{Contrl, from_bytes, validate_envelope};
1015    /// # 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'";
1016    /// # let segments: Vec<_> = from_bytes(raw).collect::<Result<Vec<_>, _>>()?;
1017    /// # let validated = validate_envelope(&segments)?;
1018    /// let wire = Contrl::acknowledgement(&validated)
1019    ///     .to_interchange_string("UNOC", "3", "260101", "0930", "ACK-1")?;
1020    ///
1021    /// // The reply is addressed back to the original sender.
1022    /// assert!(wire.starts_with("UNB+UNOC:3+RECEIVER+SENDER+260101:0930+ACK-1'"));
1023    /// assert!(wire.ends_with("UNZ+1+ACK-1'"));
1024    /// # Ok::<(), edifact_rs::EdifactError>(())
1025    /// ```
1026    ///
1027    /// # Errors
1028    ///
1029    /// Propagates any writer failure.
1030    pub fn to_interchange_bytes(
1031        &self,
1032        syntax_identifier: &str,
1033        syntax_version: &str,
1034        date: &str,
1035        time: &str,
1036        control_reference: &str,
1037    ) -> Result<Vec<u8>, EdifactError> {
1038        let mut writer = Writer::new(Vec::new());
1039        writer.begin_interchange(
1040            syntax_identifier,
1041            syntax_version,
1042            // The reply goes back the way it came.
1043            &self.recipient,
1044            &self.sender,
1045            date,
1046            time,
1047            control_reference,
1048        )?;
1049        for segment in self.segments() {
1050            writer.write_segment(&segment.as_borrowed())?;
1051        }
1052        writer.end_interchange(1, control_reference)?;
1053        writer.finish()
1054    }
1055
1056    /// String form of [`to_interchange_bytes`][Self::to_interchange_bytes].
1057    ///
1058    /// # Errors
1059    ///
1060    /// As [`to_interchange_bytes`][Self::to_interchange_bytes], plus
1061    /// [`EdifactError::InvalidUtf8`].
1062    pub fn to_interchange_string(
1063        &self,
1064        syntax_identifier: &str,
1065        syntax_version: &str,
1066        date: &str,
1067        time: &str,
1068        control_reference: &str,
1069    ) -> Result<String, EdifactError> {
1070        let bytes = self.to_interchange_bytes(
1071            syntax_identifier,
1072            syntax_version,
1073            date,
1074            time,
1075            control_reference,
1076        )?;
1077        String::from_utf8(bytes).map_err(|_| EdifactError::InvalidUtf8)
1078    }
1079}
1080
1081/// Build an S002/S003-shaped element, omitting an absent qualifier.
1082fn party(id: &str, qualifier: &str) -> OwnedElement {
1083    if qualifier.is_empty() {
1084        OwnedElement::of(&[id])
1085    } else {
1086        OwnedElement::of(&[id, qualifier])
1087    }
1088}
1089
1090/// `(first segment index, last segment index)` per message, in document order.
1091///
1092/// The order matches [`ValidatedInterchange::messages`], which is what lets a
1093/// finding be keyed by message index rather than by a reference that may repeat.
1094fn message_boundaries(segments: &[Segment<'_>]) -> Vec<(usize, usize)> {
1095    spans_between(segments, "UNH", "UNT")
1096}
1097
1098/// `(first segment index, last segment index)` per group, in document order.
1099///
1100/// The order matches [`ValidatedInterchange::functional_groups`].
1101fn group_boundaries(segments: &[Segment<'_>]) -> Vec<(usize, usize)> {
1102    spans_between(segments, "UNG", "UNE")
1103}
1104
1105/// Index ranges of each `open`…`close` pair, inclusive of both ends.
1106fn spans_between(segments: &[Segment<'_>], open: &str, close: &str) -> Vec<(usize, usize)> {
1107    let mut out = Vec::new();
1108    let mut start: Option<usize> = None;
1109    for (index, segment) in segments.iter().enumerate() {
1110        if segment.tag == open {
1111            start = Some(index);
1112        } else if segment.tag == close {
1113            if let Some(from) = start.take() {
1114                out.push((from, index));
1115            }
1116        }
1117    }
1118    out
1119}
1120
1121/// Resolve a byte offset to the deepest structure that contains it.
1122///
1123/// Going through the byte span rather than through a dedicated field on
1124/// [`ValidationIssue`] is what makes this work for findings from *any* validator,
1125/// including third-party ones that know nothing about `CONTRL`.
1126///
1127/// A message wins over the group around it, because §5.3.3 wants the lowest
1128/// level that can express the fault. `None` means the offset fell in neither —
1129/// the `UNB`, the `UNZ`, or the gap between structures — which makes it the
1130/// interchange's own.
1131///
1132/// `UCS` DE 0096 counts from the `UNH` as position 1, so the position is the
1133/// segment's offset from its message header plus one.
1134fn locate(
1135    segments: &[Segment<'_>],
1136    messages: &[(usize, usize)],
1137    groups: &[(usize, usize)],
1138    offset: usize,
1139) -> Option<Scope> {
1140    // Segments are in source order, so the containing one is the last whose
1141    // span starts at or before the offset.
1142    let index = match segments.binary_search_by(|segment| segment.span.start.cmp(&offset)) {
1143        Ok(exact) => exact,
1144        Err(0) => return None,
1145        Err(next) => next - 1,
1146    };
1147    if let Some((message_index, (start, _))) = messages
1148        .iter()
1149        .enumerate()
1150        .find(|(_, (start, end))| (*start..=*end).contains(&index))
1151    {
1152        return u32::try_from(index - start + 1)
1153            .ok()
1154            .map(|segment_position| Scope::Message {
1155                index: message_index,
1156                segment_position,
1157            });
1158    }
1159    groups
1160        .iter()
1161        .position(|(start, end)| (*start..=*end).contains(&index))
1162        .map(Scope::Group)
1163}
1164
1165#[cfg(test)]
1166mod tests {
1167    use super::*;
1168
1169    fn parse(input: &[u8]) -> Vec<OwnedSegment> {
1170        crate::from_bytes_owned(input)
1171            .collect::<Result<Vec<_>, _>>()
1172            .expect("parse")
1173    }
1174
1175    #[test]
1176    fn annex_a_permits_a_code_only_where_the_table_says() {
1177        // Spot-checks straight out of the Annex A grid.
1178        assert!(SyntaxError::NotActualRecipient.permitted_at(ReportingLevel::Interchange));
1179        assert!(!SyntaxError::NotActualRecipient.permitted_at(ReportingLevel::Group));
1180        assert!(SyntaxError::TooManyGroupRepetitions.permitted_at(ReportingLevel::Segment));
1181        assert!(!SyntaxError::TooManyGroupRepetitions.permitted_at(ReportingLevel::DataElement));
1182        assert!(SyntaxError::LowerLevelEmpty.permitted_at(ReportingLevel::Group));
1183        assert!(!SyntaxError::LowerLevelEmpty.permitted_at(ReportingLevel::Message));
1184        assert!(SyntaxError::EnvelopeFunctionalityNotSupported.permitted_at(ReportingLevel::Group));
1185        assert!(
1186            !SyntaxError::EnvelopeFunctionalityNotSupported
1187                .permitted_at(ReportingLevel::Interchange)
1188        );
1189        // "Invalid value" is usable at every level.
1190        for level in [
1191            ReportingLevel::Interchange,
1192            ReportingLevel::Group,
1193            ReportingLevel::Message,
1194            ReportingLevel::Segment,
1195            ReportingLevel::DataElement,
1196        ] {
1197            assert!(SyntaxError::InvalidValue.permitted_at(level), "{level:?}");
1198        }
1199    }
1200
1201    #[test]
1202    fn every_code_is_permitted_somewhere() {
1203        // A code no level may carry would be unreachable, which would mean the
1204        // table above is wrong rather than merely strict.
1205        for error in ALL_ERRORS {
1206            assert!(
1207                [
1208                    ReportingLevel::Interchange,
1209                    ReportingLevel::Group,
1210                    ReportingLevel::Message,
1211                    ReportingLevel::Segment,
1212                    ReportingLevel::DataElement,
1213                ]
1214                .iter()
1215                .any(|level| error.permitted_at(*level)),
1216                "{error:?} is permitted nowhere"
1217            );
1218        }
1219    }
1220
1221    #[test]
1222    fn codes_are_unique() {
1223        let mut codes: Vec<&str> = ALL_ERRORS.iter().map(|e| e.code()).collect();
1224        codes.sort_unstable();
1225        let before = codes.len();
1226        codes.dedup();
1227        assert_eq!(before, codes.len(), "duplicate DE 0085 code");
1228    }
1229
1230    const ALL_ERRORS: &[SyntaxError] = &[
1231        SyntaxError::SyntaxVersionNotSupported,
1232        SyntaxError::NotActualRecipient,
1233        SyntaxError::InvalidValue,
1234        SyntaxError::Missing,
1235        SyntaxError::ValueNotSupportedHere,
1236        SyntaxError::NotSupportedHere,
1237        SyntaxError::TooManyConstituents,
1238        SyntaxError::NoAgreement,
1239        SyntaxError::Unspecified,
1240        SyntaxError::InvalidAsServiceCharacter,
1241        SyntaxError::InvalidCharacters,
1242        SyntaxError::InvalidServiceCharacters,
1243        SyntaxError::UnknownSender,
1244        SyntaxError::TooOld,
1245        SyntaxError::TestIndicatorNotSupported,
1246        SyntaxError::DuplicateDetected,
1247        SyntaxError::ReferencesDoNotMatch,
1248        SyntaxError::ControlCountMismatch,
1249        SyntaxError::GroupsAndMessagesMixed,
1250        SyntaxError::LowerLevelEmpty,
1251        SyntaxError::InvalidOccurrenceOutsideMessage,
1252        SyntaxError::TooManyRepetitions,
1253        SyntaxError::TooManyGroupRepetitions,
1254        SyntaxError::InvalidCharacterType,
1255        SyntaxError::DataElementTooLong,
1256        SyntaxError::DataElementTooShort,
1257        SyntaxError::TrailingSeparator,
1258        SyntaxError::CharacterSetNotSupported,
1259        SyntaxError::EnvelopeFunctionalityNotSupported,
1260    ];
1261
1262    #[test]
1263    fn an_acknowledgement_validates_against_the_shipped_layouts() {
1264        let raw = b"UNB+UNOC:3+SENDER:14+RECEIVER:14+260101:0900+IC4711'\
1265                    UNH+MSG1+ORDERS:D:96A:UN'BGM+220+PO-1+9'UNT+3+MSG1'\
1266                    UNZ+1+IC4711'";
1267        let owned = parse(raw);
1268        let segments: Vec<_> = owned.iter().map(OwnedSegment::as_borrowed).collect();
1269        let validated = crate::validate_envelope(&segments).expect("valid subject");
1270
1271        let contrl = Contrl::acknowledgement(&validated).with_message_reference("ACK1");
1272        let wire = contrl.to_edifact_string().expect("render");
1273
1274        assert_eq!(
1275            wire,
1276            "UNH+ACK1+CONTRL:4:1:UN'UCI+IC4711+SENDER:14+RECEIVER:14+7'UNT+3+ACK1'"
1277        );
1278
1279        // And it is itself a well-formed CONTRL by the crate's own tables.
1280        let reparsed = parse(wire.as_bytes());
1281        let borrowed: Vec<_> = reparsed.iter().map(OwnedSegment::as_borrowed).collect();
1282        let validator = crate::DirectoryValidator::new(
1283            "iso-9735-4",
1284            crate::service::lookup,
1285            |_, _| true,
1286            |_, _| None,
1287            |_, _| None,
1288            None,
1289        );
1290        let report = crate::ValidationContext::builder()
1291            .with_validator(crate::ValidationLayer::Structure, validator)
1292            .build()
1293            .validate_lenient(&borrowed);
1294        assert!(!report.has_errors(), "{:#?}", report.errors());
1295    }
1296
1297    #[test]
1298    fn a_receipt_carries_action_8_and_nothing_else() {
1299        let raw =
1300            b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'UNZ+1+IC1'";
1301        let owned = parse(raw);
1302        let segments: Vec<_> = owned.iter().map(OwnedSegment::as_borrowed).collect();
1303        let validated = crate::validate_envelope(&segments).expect("valid subject");
1304
1305        let wire = Contrl::receipt(&validated.interchange)
1306            .to_edifact_string()
1307            .expect("render");
1308        assert!(wire.contains("UCI+IC1+S+R+8'"), "{wire}");
1309        assert_eq!(
1310            Contrl::receipt(&validated.interchange).action(),
1311            Action::Received
1312        );
1313    }
1314
1315    #[test]
1316    fn the_message_reference_defaults_to_the_subject_control_reference() {
1317        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'";
1318        let owned = parse(raw);
1319        let segments: Vec<_> = owned.iter().map(OwnedSegment::as_borrowed).collect();
1320        let validated = crate::validate_envelope(&segments).expect("valid subject");
1321        assert_eq!(
1322            Contrl::acknowledgement(&validated).message_reference(),
1323            "IC-42"
1324        );
1325    }
1326
1327    #[test]
1328    fn a_forbidden_interchange_level_code_is_not_recorded() {
1329        let raw =
1330            b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'UNZ+1+IC1'";
1331        let owned = parse(raw);
1332        let segments: Vec<_> = owned.iter().map(OwnedSegment::as_borrowed).collect();
1333        let validated = crate::validate_envelope(&segments).expect("valid subject");
1334
1335        // Annex A: "too many segment group repetitions" is UCS-only.
1336        let wire = Contrl::acknowledgement(&validated)
1337            .with_interchange_error(SyntaxError::TooManyGroupRepetitions)
1338            .to_edifact_string()
1339            .expect("render");
1340        assert!(wire.contains("UCI+IC1+S+R+7'"), "{wire}");
1341    }
1342
1343    /// Parse, validate, and build a CONTRL from the result.
1344    ///
1345    /// The lenient path, because that is the only one that yields an interchange
1346    /// *and* its faults — which is exactly what a CONTRL reports.
1347    fn report_for(raw: &[u8]) -> Contrl {
1348        let owned = parse(raw);
1349        let segments: Vec<_> = owned.iter().map(OwnedSegment::as_borrowed).collect();
1350        let validated = crate::validate_envelope_lenient(&segments)
1351            .interchange
1352            .expect("subject must be structurally interpretable");
1353        let report = crate::ValidationContext::builder()
1354            .with_envelope_validation()
1355            .with_syntax_validation()
1356            .build()
1357            .validate_lenient(&segments);
1358        Contrl::from_report(&validated, &segments, &report)
1359    }
1360
1361    #[test]
1362    fn a_data_element_fault_is_reported_at_the_ucd_level() {
1363        // `FTX+   ` is a value of nothing but spaces — ISO 9735-1 §9.3, which
1364        // the syntax validator raises as a warning with a span and a component
1365        // index.  That is enough to place it precisely.
1366        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1367                    UNH+M1+ORDERS:D:96A:UN'FTX+   'UNT+3+M1'\
1368                    UNZ+1+IC1'";
1369        let contrl = report_for(raw);
1370
1371        // A warning does not reject: the interchange and the message stay
1372        // acknowledged, and the finding is still reported (§5.3.3).
1373        assert_eq!(contrl.action(), Action::Acknowledged);
1374        let wire = contrl.to_edifact_string().expect("render");
1375
1376        // FTX is the second segment of the message, counting UNH as 1.
1377        // Element 0 is DE position 2 (the tag is position 1); component 0 is 1.
1378        assert!(wire.contains("UCM+M1+ORDERS:D:96A:UN+7'"), "{wire}");
1379        assert!(wire.contains("UCS+2'"), "{wire}");
1380        assert!(wire.contains("UCD+12+2:1'"), "{wire}");
1381    }
1382
1383    #[test]
1384    fn a_rejected_message_is_named_by_its_ucm() {
1385        // Two messages share a UNH reference — a hard ISO 9735-1 violation with
1386        // a span on the offending UNH, so it lands on that message.
1387        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1388                    UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
1389                    UNH+M1+ORDERS:D:96A:UN'BGM+221'UNT+3+M1'\
1390                    UNZ+2+IC1'";
1391        let contrl = report_for(raw);
1392
1393        // The *interchange* envelope is fine — the fault is inside it. Code 4 on
1394        // the UCI would reject every other message too (§5.3.2), so the UCI
1395        // stays at 7 and only the offending message is explicitly rejected.
1396        assert_eq!(contrl.action(), Action::Acknowledged);
1397        let wire = contrl.to_edifact_string().expect("render");
1398
1399        // The duplicate belongs to the *second* message, and Annex A does not
1400        // allow code 26 below the message level — so it is reported on that
1401        // UCM's DE 0085 rather than pushed into a UCS that could not carry it.
1402        // The first message needs no UCM at all: the UCI's 7 covers it.
1403        assert_eq!(
1404            wire,
1405            "UNH+IC1+CONTRL:4:1:UN'\
1406             UCI+IC1+S+R+7'\
1407             UCM+M1+ORDERS:D:96A:UN+4+26'\
1408             UNT+4+IC1'"
1409        );
1410    }
1411
1412    #[test]
1413    fn an_envelope_fault_rejects_the_interchange_and_emits_no_ucm() {
1414        // A UNZ control reference that does not match the UNB is the envelope's
1415        // own fault, so code 4 on the UCI is right — and it implicitly rejects
1416        // every message, which is why no UCM follows.
1417        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1418                    UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
1419                    UNZ+1+IC-OTHER'";
1420        let contrl = report_for(raw);
1421
1422        assert_eq!(contrl.action(), Action::Rejected);
1423        let wire = contrl.to_edifact_string().expect("render");
1424        assert_eq!(wire, "UNH+IC1+CONTRL:4:1:UN'UCI+IC1+S+R+4+28'UNT+3+IC1'");
1425    }
1426
1427    #[test]
1428    fn a_count_mismatch_is_reported_on_the_message_that_got_it_wrong() {
1429        // UNT DE 0074 is the trailer's fault, so the finding belongs to that
1430        // message — not to the interchange, which is otherwise sound.
1431        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1432                    UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+9+M1'\
1433                    UNZ+1+IC1'";
1434        let contrl = report_for(raw);
1435
1436        assert_eq!(contrl.action(), Action::Acknowledged);
1437        let wire = contrl.to_edifact_string().expect("render");
1438        // Code 29 (control count mismatch) is not permitted below the message,
1439        // so it is reported on the UCM.
1440        assert_eq!(
1441            wire,
1442            "UNH+IC1+CONTRL:4:1:UN'\
1443             UCI+IC1+S+R+7'\
1444             UCM+M1+ORDERS:D:96A:UN+4+29'\
1445             UNT+4+IC1'"
1446        );
1447    }
1448
1449    #[test]
1450    fn a_finding_lands_at_the_lowest_level_annex_a_permits() {
1451        // "Duplicate detected" is legal at UCI/UCF/UCM and nowhere lower, so it
1452        // must stop at the message even though the fault has a segment position.
1453        let duplicate = Finding {
1454            scope: Scope::Message {
1455                index: 0,
1456                segment_position: 1,
1457            },
1458            element_position: Some(2),
1459            component_position: None,
1460            rejects: true,
1461            error: SyntaxError::DuplicateDetected,
1462        };
1463        assert_eq!(duplicate.level(), ReportingLevel::Message);
1464
1465        // "Invalid value" is legal everywhere, so it descends all the way.
1466        let invalid = Finding {
1467            error: SyntaxError::InvalidValue,
1468            ..duplicate.clone()
1469        };
1470        assert_eq!(invalid.level(), ReportingLevel::DataElement);
1471
1472        // …and stops at the segment when there is no data element to name.
1473        let segment_only = Finding {
1474            element_position: None,
1475            ..invalid.clone()
1476        };
1477        assert_eq!(segment_only.level(), ReportingLevel::Segment);
1478
1479        // "Too many segment group repetitions" is UCS-only: it cannot descend to
1480        // UCD even with an element position, and must not climb to UCM.
1481        let group_repetitions = Finding {
1482            error: SyntaxError::TooManyGroupRepetitions,
1483            ..duplicate
1484        };
1485        assert_eq!(group_repetitions.level(), ReportingLevel::Segment);
1486    }
1487
1488    #[test]
1489    fn a_generated_contrl_reparses_and_counts_its_own_segments() {
1490        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1491                    UNH+M1+ORDERS:D:96A:UN'FTX+   'UNT+3+M1'\
1492                    UNZ+1+IC1'";
1493        let contrl = report_for(raw);
1494
1495        let wire = contrl
1496            .to_interchange_string("UNOC", "3", "260101", "0930", "ACK-1")
1497            .expect("render");
1498
1499        // The generated interchange must satisfy the crate's own envelope rules,
1500        // including the UNT count this module computes for itself.
1501        let segments: Vec<_> = crate::from_bytes(wire.as_bytes())
1502            .collect::<Result<Vec<_>, _>>()
1503            .expect("generated CONTRL must reparse");
1504        let validated =
1505            crate::validate_envelope(&segments).expect("generated CONTRL must validate");
1506        assert_eq!(validated.messages.len(), 1);
1507        assert_eq!(validated.messages[0].message_type, "CONTRL");
1508        assert_eq!(validated.messages[0].version, "4");
1509        assert_eq!(validated.messages[0].release, "1");
1510        assert_eq!(
1511            validated.messages[0].declared_segment_count,
1512            validated.messages[0].actual_segment_count
1513        );
1514    }
1515
1516    #[test]
1517    fn a_grouped_interchange_is_reported_through_ucf() {
1518        // §5.3.1: segment groups 1 and 3 are mutually exclusive — a subject that
1519        // uses UNG/UNE is reported through UCF, never through a bare UCM.
1520        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1521                    UNG+ORDERS+SND:14+RCV:14+260101:0900+GRP1+UN+D:96A'\
1522                    UNH+M1+ORDERS:D:96A:UN'FTX+   'UNT+3+M1'\
1523                    UNE+1+GRP1'\
1524                    UNZ+1+IC1'";
1525        let contrl = report_for(raw);
1526
1527        assert_eq!(contrl.action(), Action::Acknowledged);
1528        let wire = contrl.to_edifact_string().expect("render");
1529
1530        // The UCF names the group by its own reference and application parties,
1531        // and the message report nests underneath it.
1532        assert_eq!(
1533            wire,
1534            "UNH+IC1+CONTRL:4:1:UN'\
1535             UCI+IC1+S+R+7'\
1536             UCF+GRP1+SND:14+RCV:14+7'\
1537             UCM+M1+ORDERS:D:96A:UN+7'\
1538             UCS+2'\
1539             UCD+12+2:1'\
1540             UNT+7+IC1'"
1541        );
1542    }
1543
1544    #[test]
1545    fn a_group_envelope_fault_rejects_only_that_group() {
1546        // The UNE reference does not match its UNG.  That is the *group's*
1547        // envelope, not the interchange's — so the UCI stays at 7 and the UCF
1548        // carries the rejection, which implicitly rejects the messages inside it.
1549        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1550                    UNG+ORDERS+SND+RCV+260101:0900+GRP1+UN+D:96A'\
1551                    UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
1552                    UNE+1+GRP-OTHER'\
1553                    UNZ+1+IC1'";
1554        let contrl = report_for(raw);
1555
1556        assert_eq!(contrl.action(), Action::Acknowledged);
1557        let wire = contrl.to_edifact_string().expect("render");
1558        assert_eq!(
1559            wire,
1560            "UNH+IC1+CONTRL:4:1:UN'\
1561             UCI+IC1+S+R+7'\
1562             UCF+GRP1+SND+RCV+4+28'\
1563             UNT+4+IC1'"
1564        );
1565    }
1566
1567    #[test]
1568    fn a_clean_grouped_interchange_needs_no_ucf() {
1569        // The UCI's code 7 already acknowledges every group and message under
1570        // it (§5.3.4); a UCF that only repeats that is noise.
1571        let raw = b"UNB+UNOC:3+S+R+260101:0900+IC1'\
1572                    UNG+ORDERS+SND+RCV+260101:0900+GRP1+UN+D:96A'\
1573                    UNH+M1+ORDERS:D:96A:UN'BGM+220'UNT+3+M1'\
1574                    UNE+1+GRP1'\
1575                    UNZ+1+IC1'";
1576        let contrl = report_for(raw);
1577        assert_eq!(
1578            contrl.to_edifact_string().expect("render"),
1579            "UNH+IC1+CONTRL:4:1:UN'UCI+IC1+S+R+7'UNT+3+IC1'"
1580        );
1581    }
1582
1583    #[test]
1584    fn error_mapping_picks_the_narrowest_annex_a_code() {
1585        use EdifactError as E;
1586        let cases: [(EdifactError, SyntaxError); 6] = [
1587            (
1588                E::MessageCountMismatch {
1589                    expected: 1,
1590                    actual: 2,
1591                },
1592                SyntaxError::ControlCountMismatch,
1593            ),
1594            (
1595                E::DuplicateReference {
1596                    tag: "UNH".to_owned(),
1597                    reference: "1".to_owned(),
1598                    span: crate::Span::new(0, 1),
1599                },
1600                SyntaxError::DuplicateDetected,
1601            ),
1602            (
1603                E::UnsupportedCharset {
1604                    syntax_identifier: "UNOX".to_owned(),
1605                },
1606                SyntaxError::CharacterSetNotSupported,
1607            ),
1608            (E::InvalidUna, SyntaxError::InvalidServiceCharacters),
1609            (
1610                E::EmptyInterchange {
1611                    control_ref: "IC1".to_owned(),
1612                },
1613                SyntaxError::LowerLevelEmpty,
1614            ),
1615            (
1616                E::PackageNotSupported {
1617                    tag: "UNO".to_owned(),
1618                    span: crate::Span::new(0, 1),
1619                },
1620                SyntaxError::EnvelopeFunctionalityNotSupported,
1621            ),
1622        ];
1623        for (error, expected) in cases {
1624            assert_eq!(SyntaxError::for_error(&error), expected, "{error:?}");
1625        }
1626    }
1627}