Skip to main content

edifact_rs/
error.rs

1use crate::model::Span;
2use thiserror::Error;
3
4/// Wrapper around [`std::io::Error`] that implements [`PartialEq`] by comparing [`std::io::ErrorKind`].
5///
6/// This allows `EdifactError` to derive `PartialEq` without requiring `std::io::Error: PartialEq`.
7#[derive(Debug)]
8pub struct IoError(pub(crate) std::io::Error);
9
10impl IoError {
11    /// Returns a reference to the underlying [`std::io::Error`].
12    pub fn inner(&self) -> &std::io::Error {
13        &self.0
14    }
15}
16
17impl PartialEq for IoError {
18    /// Equality is determined by [`std::io::ErrorKind`] only.
19    ///
20    /// Two `IoError` values with the same kind but different OS-level error codes
21    /// (or different messages) will compare as equal.  This is a deliberate
22    /// limitation: `std::io::Error` is not `PartialEq`, so kind-based comparison
23    /// is the only practical option that lets `EdifactError` derive `PartialEq`.
24    fn eq(&self, other: &Self) -> bool {
25        self.0.kind() == other.0.kind()
26    }
27}
28
29impl std::fmt::Display for IoError {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        self.0.fmt(f)
32    }
33}
34
35impl std::error::Error for IoError {
36    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
37        self.0.source()
38    }
39}
40
41impl From<std::io::Error> for IoError {
42    fn from(e: std::io::Error) -> Self {
43        Self(e)
44    }
45}
46
47/// Which rule of ISO 9735-1 §9.1 a value violates.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49#[non_exhaustive]
50pub enum Insignificant {
51    /// A variable-length numeric value carries leading zeroes.
52    ///
53    /// "Nevertheless, a single zero before a decimal mark is allowed", so `0.5`
54    /// is correct and `00.5` is not.
55    LeadingZeroes,
56    /// A variable-length alphabetic or alphanumeric value carries trailing
57    /// spaces.
58    TrailingSpaces,
59}
60
61impl std::fmt::Display for Insignificant {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.write_str(match self {
64            Self::LeadingZeroes => "leading zeroes are not suppressed",
65            Self::TrailingSpaces => "trailing spaces are not suppressed",
66        })
67    }
68}
69
70/// All errors produced by `edifact-rs`.
71///
72/// # Positional data
73///
74/// Two kinds of position information appear in this enum, and the distinction is
75/// deliberate:
76///
77/// * **`offset: usize`** — a single byte position in the input stream.  Used by
78///   the lexical variants (`UnexpectedEof`, `InvalidDelimiter`, `InvalidText`,
79///   `InvalidReleaseSequence`, `SegmentTooLong`, `UnexpectedDataToken`), where
80///   the fault is a *point* in the byte stream and no meaningful end position
81///   exists.
82/// * **`span: Span`** — a half-open byte range.  Used by every variant produced
83///   while validating an already-parsed [`Segment`][crate::Segment], where the
84///   exact source range is known.  A [`ValidationIssue`][crate::ValidationIssue]
85///   built from such an error carries the full range, so `miette` and LSP
86///   tooling can underline the offending segment rather than place a
87///   zero-width caret.
88///
89/// Use `span.start` when only the start position is needed.
90#[derive(Debug, Error, PartialEq)]
91#[non_exhaustive]
92pub enum EdifactError {
93    /// Unexpected end of input while parsing.
94    ///
95    /// This typically occurs when a segment terminator or expected delimiter
96    /// is not found before the end of the input stream.
97    #[error("unexpected end of input at byte offset {offset}")]
98    UnexpectedEof {
99        /// Byte offset where the parser exhausted input.
100        offset: usize,
101    },
102
103    /// Invalid byte encountered in a delimiter context.
104    ///
105    /// Delimiters must be precisely ASCII characters from the UNA service string advice.
106    /// Any other byte is invalid in delimiter position.
107    #[error("invalid delimiter byte 0x{byte:02X} at offset {offset}")]
108    InvalidDelimiter {
109        /// Unexpected delimiter byte.
110        byte: u8,
111        /// Byte offset where the delimiter was observed.
112        offset: usize,
113    },
114
115    /// Invalid UTF-8 sequence in parsed text.
116    ///
117    /// While EDIFACT operates on bytes, segments and elements are expected to contain
118    /// valid UTF-8 text. Non-UTF-8 sequences are rejected at parse time.
119    #[error("invalid EDIFACT text at byte offset {offset}")]
120    InvalidText {
121        /// Byte offset where invalid UTF-8 text starts.
122        offset: usize,
123    },
124
125    /// Invalid release-character escape sequence in parsed text.
126    ///
127    /// The release character (`?` by default) must be followed by one escaped byte.
128    /// A trailing release character without a following byte is malformed.
129    #[error("invalid release sequence at byte offset {offset}: dangling release character")]
130    InvalidReleaseSequence {
131        /// Byte offset of the dangling release character.
132        offset: usize,
133    },
134
135    /// UNZ interchange message count does not match the number of UNH/UNT pairs found.
136    ///
137    /// The `UNZ` segment declares the number of messages in the interchange,
138    /// but the actual number of `UNH`/`UNT` pairs observed differs.
139    #[error("interchange message count mismatch: UNZ declared {expected}, found {actual}")]
140    MessageCountMismatch {
141        /// Message count declared in the UNZ segment.
142        expected: u32,
143        /// Actual number of UNH/UNT pairs observed.
144        actual: u32,
145    },
146
147    /// UNT segment count does not match the actual number of segments in the message.
148    ///
149    /// The `UNT` segment declares the number of segments in the message (including `UNH`/`UNT`),
150    /// but the actual count differs.
151    #[error(
152        "segment count mismatch in message {message_ref}: UNT declared {expected}, found {actual}"
153    )]
154    SegmentCountMismatch {
155        /// Segment count declared in the UNT segment.
156        expected: u32,
157        /// Actual number of segments observed.
158        actual: u32,
159        /// Message reference from the UNH segment.
160        message_ref: String,
161        /// Byte range of the offending `UNT`.
162        ///
163        /// This is what places the finding on the *message* rather than on the
164        /// interchange — a `CONTRL` built from the report reports it on that
165        /// message's `UCM`, and a rendered diagnostic underlines the trailer
166        /// that got the count wrong.
167        span: Span,
168    },
169
170    /// Invalid or malformed segment tag.
171    ///
172    /// Segment tags must be exactly 3 ASCII uppercase letters.
173    #[error("invalid segment tag {0:?}")]
174    InvalidSegmentTag(String),
175
176    /// Invalid UNA service string advice.
177    ///
178    /// If present, the UNA segment must be exactly 9 bytes: `"UNA"` followed by
179    /// 6 service characters.  The **active** ones — component separator, element
180    /// separator, release character, segment terminator, and the repetition
181    /// separator when it is not the "not used" space — must all be mutually
182    /// distinct and printable, non-alphanumeric ASCII.
183    ///
184    /// The **decimal mark is not checked**: ISO 9735-1 Annex B says the character
185    /// in that position "shall be ignored by the recipient", and it is the one
186    /// position where the standard permits a space.
187    #[error("invalid UNA service string advice")]
188    InvalidUna,
189
190    /// Missing required element in a segment.
191    ///
192    /// Certain segments require specific elements to be present. This error indicates
193    /// a mandatory element was not found.
194    #[error("missing required element {element_index} in segment {tag}")]
195    MissingRequiredElement {
196        /// Segment tag containing the missing element.
197        tag: String,
198        /// Zero-based required element index.
199        element_index: usize,
200    },
201
202    /// Missing required component in a composite element.
203    ///
204    /// The element is present, but the required component at the given index is absent or empty.
205    #[error(
206        "missing required component {component_index} in element {element_index} of segment {tag}"
207    )]
208    MissingRequiredComponent {
209        /// Segment tag containing the composite element.
210        tag: String,
211        /// Zero-based element index of the composite.
212        element_index: usize,
213        /// Zero-based component index that was absent.
214        component_index: usize,
215    },
216
217    /// Output serialization produced invalid UTF-8.
218    ///
219    /// This is an internal consistency error; the writer should never produce non-UTF-8 output.
220    /// If this occurs, it indicates a bug in the serialization logic.
221    #[error("serialized output contains invalid UTF-8")]
222    InvalidUtf8,
223
224    /// I/O error from reading or writing.
225    #[error(transparent)]
226    Io(#[from] IoError),
227
228    // ── validation variants (E010–E020) ────────────────────────────────────
229    /// Segment is not valid for the current message type.
230    ///
231    /// Structural validation found a segment that should not appear in this message.
232    #[error("segment {tag} is not valid for message type {message_type}")]
233    InvalidSegmentForMessage {
234        /// Segment tag that is not allowed for the message type.
235        tag: String,
236        /// Message type used for structural validation.
237        message_type: String,
238        /// Byte range of the offending segment tag.
239        span: Span,
240    },
241
242    /// Element count in segment exceeds or falls short of directory definition.
243    ///
244    /// Validation against directory metadata found an element count mismatch.
245    #[error("segment {tag} has {actual} elements, expected between {min} and {max}")]
246    InvalidElementCount {
247        /// Segment tag with wrong arity.
248        tag: String,
249        /// Minimum allowed element count.
250        min: usize,
251        /// Maximum allowed element count.
252        max: usize,
253        /// Actual element count found.
254        actual: usize,
255        /// Byte range of the offending segment.
256        span: Span,
257    },
258
259    /// Component count in a composite element is invalid.
260    ///
261    /// A composite data element does not have the expected number of components.
262    #[error("segment {tag} element {element_index} has {actual} components, expected {expected}")]
263    InvalidComponentCount {
264        /// Segment tag containing the composite.
265        tag: String,
266        /// Zero-based element index of the composite.
267        element_index: usize,
268        /// Expected component count.
269        expected: u8,
270        /// Actual component count found.
271        actual: u8,
272        /// Byte range of the offending composite element.
273        span: Span,
274    },
275
276    /// Code-list value is not valid.
277    ///
278    /// The value appears in a field that should contain a code from a specific code list,
279    /// but the value is not in that code list.
280    #[error(
281        "segment {tag} element {element_index}: '{value}' is not a valid code (code list {code_list})"
282    )]
283    InvalidCodeValue {
284        /// Segment tag containing the invalid value.
285        tag: String,
286        /// Zero-based element index containing the invalid code.
287        element_index: usize,
288        /// Invalid code value observed.
289        value: String,
290        /// Data element code list identifier.
291        code_list: String,
292        /// Byte range of the offending value.
293        span: Span,
294        /// Optional remediation suggestion from the code-list lookup function.
295        suggestion: Option<&'static str>,
296    },
297
298    /// A required segment is missing from the message.
299    ///
300    /// Structural validation found that a mandatory segment is absent.
301    #[error("required segment {tag} is missing from message (position {expected_position})")]
302    MissingSegment {
303        /// Missing segment tag.
304        tag: String,
305        /// Human-readable position hint.
306        expected_position: String,
307    },
308
309    /// Qualifier does not match expected value for segment.
310    ///
311    /// A qualified segment (e.g., NAD+MS) has a qualifier that does not match expected.
312    #[error("segment {tag} has qualifier '{actual}', expected '{expected}'")]
313    QualifierMismatch {
314        /// Segment tag whose qualifier mismatched.
315        tag: String,
316        /// Actual qualifier found.
317        actual: String,
318        /// Expected qualifier value.
319        expected: String,
320        /// Byte range of the offending segment.
321        span: Span,
322    },
323
324    /// Conditional requirement not met.
325    ///
326    /// A segment or element is conditionally required based on another element's value,
327    /// but the condition was not satisfied.
328    #[error("segment {tag} element {element_index}: conditional requirement not met ({condition})")]
329    ConditionalRequirementNotMet {
330        /// Segment tag that violated a conditional rule.
331        tag: String,
332        /// Zero-based element index governed by the condition.
333        element_index: usize,
334        /// Condition text describing the rule.
335        condition: String,
336        /// Byte range of the offending segment.
337        span: Span,
338    },
339
340    /// Validation failed and the full [`ValidationReport`] is preserved.
341    ///
342    /// Returned by validation helpers when errors are found.  Provides programmatic
343    /// access to all issues, warnings, and infos.
344    ///
345    /// # Example
346    ///
347    /// ```rust,ignore
348    /// match my_fn() {
349    ///     Err(EdifactError::ValidationErrors { report, .. }) => {
350    ///         for issue in report.errors() {
351    ///             eprintln!("{}", issue);
352    ///         }
353    ///     }
354    ///     other => { /* ... */ }
355    /// }
356    /// ```
357    #[error("validation failed with {error_count} error(s)")]
358    ValidationErrors {
359        /// Number of error-severity issues in the report.
360        error_count: usize,
361        /// Full report with all errors, warnings, and infos.
362        report: Box<ValidationReport>,
363    },
364
365    /// Segment exceeded the configured maximum byte length.
366    ///
367    /// Returned by reader-based parsers when an unterminated segment accumulates more
368    /// bytes than the configured `max_segment_bytes` limit in [`ReaderConfig`].  This
369    /// prevents resource exhaustion on adversarially crafted or truncated input that
370    /// never emits a segment terminator.
371    ///
372    /// [`ReaderConfig`]: crate::ReaderConfig
373    #[error("segment starting at byte offset {offset} exceeded maximum length of {limit} bytes")]
374    SegmentTooLong {
375        /// Byte offset where the overlong segment started.
376        offset: usize,
377        /// Configured maximum segment byte length.
378        limit: usize,
379    },
380
381    /// No handler was registered in [`crate::MessageDispatch`] for this message type.
382    ///
383    /// Returned by [`crate::MessageDispatch::dispatch`] when the message-type
384    /// extracted from the `UNH` segment does not match any registered handler
385    /// and no fallback was configured.
386    #[error("no handler registered for message type {message_type}")]
387    UnexpectedMessageType {
388        /// The unhandled message type string from the `UNH` segment.
389        message_type: String,
390    },
391
392    /// An interchange or message contains more segments or messages than can be
393    /// represented in a `u32` counter (> 4 294 967 295).
394    ///
395    /// This is effectively unreachable in practice — no real-world EDIFACT
396    /// interchange has billions of segments — but the parser returns this error
397    /// rather than silently saturating or wrapping the counter.
398    #[error("interchange too large: count {count} exceeds u32::MAX")]
399    InterchangeTooLarge {
400        /// The count that could not be represented as `u32`.
401        count: u64,
402    },
403
404    /// An [`crate::EventEmitter`] received events in an invalid sequence.
405    ///
406    /// This indicates a programming error in the caller's serialization code:
407    /// for example, emitting an [`crate::EdifactEvent::Element`] without a prior
408    /// [`crate::EdifactEvent::StartSegment`], or emitting
409    /// [`crate::EdifactEvent::ComponentElement`] without a preceding
410    /// [`crate::EdifactEvent::Element`].
411    #[error("invalid event sequence: {message}")]
412    InvalidEventSequence {
413        /// Description of the protocol violation.
414        message: &'static str,
415    },
416
417    /// An [`crate::OwnedElementRef`] has `position = 0`, which is never valid.
418    ///
419    /// Element positions are one-based: position 1 refers to the first element
420    /// slot.  Position 0 is reserved and invalid.  Use [`crate::OwnedElementRef::try_new`]
421    /// to get a `Result` instead of a panic.
422    #[error("element definition contains invalid position 0; positions must be >= 1 (one-based)")]
423    InvalidElementPosition,
424
425    /// Two [`crate::ProfileRulePack`] values with incompatible release scopes were composed.
426    ///
427    /// When composing packs via [`crate::ProfileRulePack::extend_from`] or
428    /// [`crate::ProfileRulePack::merge_with_override`], both packs must either
429    /// share the same release scope or at most one may carry a scope.
430    #[error("incompatible release scopes: cannot compose {current:?} with {incoming:?}")]
431    IncompatibleReleaseScopes {
432        /// Release scope of the pack being composed into.
433        current: String,
434        /// Release scope of the pack being composed in.
435        incoming: String,
436    },
437
438    /// A field value failed semantic validation (e.g. wrong format, out-of-range).
439    ///
440    /// Distinct from [`InvalidCodeValue`][Self::InvalidCodeValue] which is for
441    /// code-list membership checks.  Use this variant when a free-text or numeric
442    /// field contains a value that is structurally invalid for its purpose.
443    #[error("segment {tag} element {element_index}: invalid field value {value:?}")]
444    InvalidFieldValue {
445        /// Segment tag that contains the invalid field.
446        tag: String,
447        /// Zero-based element index of the invalid field.
448        element_index: usize,
449        /// The invalid value that was observed.
450        value: String,
451    },
452
453    /// A data or component element token appeared before the first segment tag.
454    ///
455    /// EDIFACT syntax requires that every data element follows a segment tag.
456    /// A data element token encountered before any tag (e.g. after a stray
457    /// separator at the start of the stream) is a protocol violation.
458    ///
459    /// Unlike stray segment terminators (which are tolerated as blank lines),
460    /// stray data tokens indicate encoding corruption or a partial write.
461    #[error("unexpected data token at byte offset {offset}: data element before segment tag")]
462    UnexpectedDataToken {
463        /// Byte offset of the stray token.
464        offset: usize,
465    },
466
467    /// The interchange syntax identifier (`UNB` S001 DE 0001) names no defined
468    /// character repertoire.
469    ///
470    /// DE 0001 is `UN` followed by a two-character repertoire code, so the
471    /// defined values are `UNOA` through `UNOK`, `UNOX`, `UNOY`, and `KECA`
472    /// (Korean EDI Centre A).  Anything else is a non-standard generator or a
473    /// corrupted `UNB`.
474    ///
475    /// A value that *is* defined but that this crate cannot decode is
476    /// [`UnsupportedCharset`][Self::UnsupportedCharset] instead — the two say
477    /// different things about whose problem it is.
478    #[error("unrecognised syntax identifier '{0}': expected UNOA-UNOK, UNOX, UNOY, or KECA")]
479    UnrecognisedSyntaxIdentifier(String),
480
481    /// A control reference was reused within the scope that requires it to be unique.
482    ///
483    /// ISO 9735-1 requires the message reference number (`UNH` DE 0062) to be
484    /// unique within an interchange, and the group reference number (`UNG`
485    /// DE 0048) to be unique within an interchange.  Duplicates make a message
486    /// unaddressable: a receiver keying on the reference silently processes one
487    /// occurrence and drops the rest.
488    #[error("duplicate {tag} reference '{reference}' at bytes {span}")]
489    DuplicateReference {
490        /// Segment tag that carries the duplicated reference (`UNH` or `UNG`).
491        tag: String,
492        /// The reference value that appeared more than once.
493        reference: String,
494        /// Byte range of the duplicate occurrence.
495        span: Span,
496    },
497
498    /// A UN/EDIFACT data element code was not found in the segment definition.
499    ///
500    /// Produced by the code-addressed accessors
501    /// ([`Segment::value_by_code`][crate::Segment::value_by_code] and friends)
502    /// when the requested data element identifier does not appear anywhere in
503    /// the supplied [`SegmentLayout`][crate::SegmentLayout].  This is the error
504    /// that turns a mistyped or stale DE reference into a loud failure instead
505    /// of a silent off-by-one read of the wrong element.
506    #[error("segment {tag} has no data element {data_element} in its definition")]
507    UnknownDataElement {
508        /// Segment tag whose definition was searched.
509        tag: String,
510        /// The data element identifier that was not found.
511        data_element: String,
512    },
513
514    /// A UN/EDIFACT data element code appears more than once in a segment definition.
515    ///
516    /// Code-addressed access requires an unambiguous target.  When a directory
517    /// genuinely repeats a code (e.g. the same DE used at two positions), address
518    /// it positionally with [`Segment::element_str`][crate::Segment::element_str]
519    /// or split the definition.
520    #[error("segment {tag} defines data element {data_element} at more than one position")]
521    AmbiguousDataElement {
522        /// Segment tag whose definition was searched.
523        tag: String,
524        /// The data element identifier that resolved to multiple positions.
525        data_element: String,
526    },
527
528    /// A configured [`ReaderConfig`][crate::ReaderConfig] resource limit was exceeded.
529    ///
530    /// Raised by the parsing iterators when the input carries more segments,
531    /// messages, or bytes than the caller allowed.  The limit is reported rather
532    /// than silently applied: a budget that ends the iterator without an error is
533    /// indistinguishable from a clean end of input, so the caller would accept a
534    /// **truncated** interchange as complete.
535    ///
536    /// [`SegmentTooLong`][Self::SegmentTooLong] covers the per-segment size
537    /// guard; this variant covers the whole-input budgets.
538    #[error("input exceeded the configured {limit} limit of {max}")]
539    LimitExceeded {
540        /// Name of the limit that tripped: `"max_segments"`, `"max_messages"`,
541        /// or `"max_input_bytes"`.
542        limit: &'static str,
543        /// The configured ceiling.
544        max: u64,
545    },
546
547    /// A repeating data element was written under a service string advice that
548    /// declares no repetition separator.
549    ///
550    /// ISO 9735-1 §8.6 repetitions can only be expressed when `UNA` position 7
551    /// carries a real separator.  With the space "not used" sentinel there is no
552    /// byte to write between occurrences, and joining them anyway would emit
553    /// output that reads back as a single occurrence — silent data corruption.
554    /// Construct the writer with [`Writer::with_una`][crate::Writer::with_una]
555    /// and a service string advice whose `repetition_sep` is set.
556    #[error(
557        "cannot write a repeating data element: the active service string advice declares no repetition separator"
558    )]
559    RepetitionSeparatorNotDeclared,
560
561    /// A non-finite float was handed to a numeric serializer.
562    ///
563    /// ISO 9735-1 §10 admits the ISO 6093 numeric representations — digits, an
564    /// optional minus sign, a decimal mark, an exponent — and nothing else.
565    /// There is no representation for `NaN` or infinity, and `Display` would
566    /// emit `NaN` / `inf`, text no receiver can parse and that the crate's own
567    /// reader would hand back as a string.
568    #[error("non-finite number {value} has no EDIFACT representation")]
569    NonFiniteNumber {
570        /// The offending value, formatted for the message.
571        value: String,
572    },
573
574    /// A character cannot be represented in the interchange's declared repertoire.
575    ///
576    /// The `UNB` S001 DE 0001 syntax identifier names the character repertoire the
577    /// payload is written in (`UNOA`, `UNOC`, …).  Writing a character outside it
578    /// produces bytes the receiver decodes as something else — or as nothing at
579    /// all — so the writer refuses instead.
580    ///
581    /// Also raised by [`Charset::encode`][crate::Charset::encode].
582    #[error(
583        "character {character:?} at offset {offset} is not in the {charset} character repertoire"
584    )]
585    CharacterNotInRepertoire {
586        /// The syntax identifier of the repertoire that rejected the character.
587        charset: &'static str,
588        /// The offending character.
589        character: char,
590        /// Byte offset of the character within the value it appeared in.
591        offset: usize,
592    },
593
594    /// A `UNB` was written declaring a repertoire other than the writer's own.
595    ///
596    /// A header that names `UNOA` while the body goes out as ISO 8859-1 is
597    /// unreadable at the far end in exactly the way that is hardest to diagnose,
598    /// so [`Writer::begin_interchange`][crate::Writer::begin_interchange] refuses
599    /// the combination rather than emitting it.
600    #[error("UNB declares repertoire {declared}, but the writer encodes {writer}")]
601    CharacterRepertoireMismatch {
602        /// Syntax identifier passed to `begin_interchange`.
603        declared: String,
604        /// Repertoire the writer was bound to with `Writer::with_charset`.
605        writer: &'static str,
606    },
607
608    /// The interchange declares a character repertoire this crate cannot decode.
609    ///
610    /// `UNOX` (ISO 2022 code extension) and `KECA` (Korean) are stateful or
611    /// multi-byte in ways that break the byte-level delimiter scanning every
612    /// other repertoire allows.  They are reported rather than silently
613    /// mis-decoded.
614    #[error("character repertoire '{syntax_identifier}' is not supported")]
615    UnsupportedCharset {
616        /// The `UNB` S001 DE 0001 value that could not be handled.
617        syntax_identifier: String,
618    },
619
620    /// An interchange carries neither a message nor a group.
621    ///
622    /// ISO 9735-1 §7.1: an interchange "shall contain at least one group, or one
623    /// message or one package".  A bare `UNB`/`UNZ` pair is a delivery that says
624    /// nothing, and because `UNZ+0` makes the control count agree with the
625    /// (absent) content, no count check can catch it.
626    #[error("interchange {control_ref} contains no message or group")]
627    EmptyInterchange {
628        /// Interchange control reference (`UNB` DE 0020) of the empty interchange.
629        control_ref: String,
630    },
631
632    /// A message carries no segment between its header and trailer.
633    ///
634    /// ISO 9735-1 §7.3: a message "shall be started and identified by a message
635    /// header, shall be terminated by a message trailer, and shall contain at
636    /// least one additional segment".
637    #[error("message {message_ref} has no segments between UNH and UNT")]
638    EmptyMessage {
639        /// Message reference number (`UNH` DE 0062) of the empty message.
640        message_ref: String,
641        /// Byte range of the offending `UNH`.
642        span: Span,
643    },
644
645    /// The interchange carries a package (`UNO`…`UNP`), which this crate does not
646    /// tokenize.
647    ///
648    /// A package wraps an arbitrary object — ISO 9735-1 §7.9, elaborated by
649    /// ISO 9735-8 — whose bytes are not EDIFACT-encoded and whose length is
650    /// declared in `UNO` S022 DE 0810.  Feeding those bytes to a tokenizer that
651    /// scans for delimiters produces nonsense, so the package is reported instead.
652    /// Split the object out of the byte stream using the declared length before
653    /// parsing the remainder.
654    #[error("segment {tag} opens or closes a package, which this crate does not parse")]
655    PackageNotSupported {
656        /// The package segment encountered: `UNO` or `UNP`.
657        tag: String,
658        /// Byte range of the offending segment.
659        span: Span,
660    },
661
662    /// A data element value consists only of spaces.
663    ///
664    /// ISO 9735-1 §9.3: "A data element value containing only space(s) shall not
665    /// be allowed."  Trailing spaces are insignificant and must be suppressed
666    /// (§9.1), so a value that is nothing but spaces is an element that should
667    /// have been omitted — and a receiver comparing it against a code list, a
668    /// reference, or a previous message will not treat it as absent.
669    #[error(
670        "segment {tag} element {element_index} component {component_index}: value is only spaces"
671    )]
672    BlankDataElementValue {
673        /// Segment tag containing the blank value.
674        tag: String,
675        /// Zero-based element index.
676        element_index: usize,
677        /// Zero-based component index.
678        component_index: usize,
679        /// Byte range of the offending value.
680        span: Span,
681    },
682
683    /// A segment carries nothing but its tag.
684    ///
685    /// ISO 9735-1 §7.5: "A segment shall contain at least one data element in
686    /// addition to the segment tag."  §8.5 adds that a conditional segment whose
687    /// only content is the tag "shall be omitted in its entirety" — so `ABC'` is
688    /// either a mandatory segment that lost its data or a conditional one that
689    /// should not have been sent.
690    #[error("segment {tag} contains no data element")]
691    SegmentWithoutDataElements {
692        /// The offending segment tag.
693        tag: String,
694        /// Byte range of the offending segment.
695        span: Span,
696    },
697
698    /// A data element occurred more times than its definition allows.
699    ///
700    /// ISO 9735-1 §7.5: "Each stand-alone or composite data element's position,
701    /// status and maximum number of occurrences within the segment structure
702    /// shall be stated in the segment specification." Exceeding the stated
703    /// maximum is a structural violation, reported by `CONTRL` as code 35.
704    #[error("segment {tag} element {element_index} occurs {actual} times, at most {max} allowed")]
705    TooManyRepetitions {
706        /// Segment tag carrying the over-repeated element.
707        tag: String,
708        /// Zero-based element index.
709        element_index: usize,
710        /// Maximum occurrences the definition allows.
711        max: u8,
712        /// Occurrences actually present.
713        actual: usize,
714        /// Byte range of the offending element.
715        span: Span,
716    },
717
718    /// A value's characters do not match its declared representation class.
719    ///
720    /// A directory types every data element `a` (alphabetic), `n` (numeric), or
721    /// `an` (alphanumeric). ISO 9735-1 §10 fixes what "numeric" admits: digits,
722    /// an optional minus, a decimal mark, and an exponent — and explicitly not
723    /// the space character or a plus sign. Reported by `CONTRL` as code 37.
724    #[error(
725        "segment {tag} element {element_index} component {component_index}: {value:?} is not {repr}"
726    )]
727    InvalidCharacterType {
728        /// Segment tag containing the value.
729        tag: String,
730        /// Zero-based element index.
731        element_index: usize,
732        /// Zero-based component index.
733        component_index: usize,
734        /// The declared representation, e.g. `n..6`.
735        repr: String,
736        /// The offending value.
737        value: String,
738        /// Byte range of the offending value.
739        span: Span,
740    },
741
742    /// A value is longer than its declared representation allows.
743    ///
744    /// Length is counted in **characters**, not bytes (ISO 9735-1 §6), and for a
745    /// numeric value excludes the sign, the decimal mark, and the exponent
746    /// (§10). Reported by `CONTRL` as code 39.
747    #[error(
748        "segment {tag} element {element_index} component {component_index}: {actual} characters exceeds {repr}"
749    )]
750    DataElementTooLong {
751        /// Segment tag containing the value.
752        tag: String,
753        /// Zero-based element index.
754        element_index: usize,
755        /// Zero-based component index.
756        component_index: usize,
757        /// The declared representation, e.g. `an..35`.
758        repr: String,
759        /// The measured character count.
760        actual: usize,
761        /// Byte range of the offending value.
762        span: Span,
763    },
764
765    /// A value is shorter than its declared fixed-length representation.
766    ///
767    /// Only a fixed-length representation (`n8`, `a1`) has a minimum above one;
768    /// a variable one (`an..35`) is satisfied by any non-empty value. Reported
769    /// by `CONTRL` as code 40.
770    #[error(
771        "segment {tag} element {element_index} component {component_index}: {actual} characters is short of {repr}"
772    )]
773    DataElementTooShort {
774        /// Segment tag containing the value.
775        tag: String,
776        /// Zero-based element index.
777        element_index: usize,
778        /// Zero-based component index.
779        component_index: usize,
780        /// The declared representation, e.g. `n8`.
781        repr: String,
782        /// The measured character count.
783        actual: usize,
784        /// Byte range of the offending value.
785        span: Span,
786    },
787
788    /// A segment or composite ends in separators that carry no value.
789    ///
790    /// ISO 9735-1 §8.7.1: "If one or more non-repeating composite data elements
791    /// or stand-alone data elements at the end of a segment are omitted, the
792    /// data element separators which would normally follow them shall also be
793    /// omitted." §8.7.2 says the same for components at the end of a composite.
794    ///
795    /// `BGM+220+'` and `DTM+137:20260101:'` are therefore both malformed, and a
796    /// receiver that trims them is being generous rather than correct. Reported
797    /// by `CONTRL` as code 45.
798    #[error("segment {tag} ends in a separator that carries no value")]
799    TrailingSeparator {
800        /// Segment tag carrying the trailing separator.
801        tag: String,
802        /// `Some(index)` when the trailing separators close that element's
803        /// composite; `None` when they close the segment itself.
804        element_index: Option<usize>,
805        /// Byte range of the offending segment or element.
806        span: Span,
807    },
808
809    /// An interchange mixes groups with ungrouped messages.
810    ///
811    /// ISO 9735-1 §7.1 lists what an interchange may contain, and every entry is
812    /// exclusive: messages, or packages, or groups containing them — never a
813    /// group alongside a bare message. A message outside every group has no
814    /// group to be counted in, so `UNZ` DE 0036 cannot describe the interchange
815    /// at all. Reported by `CONTRL` as code 30.
816    #[error("interchange mixes groups with ungrouped messages")]
817    GroupsAndMessagesMixed {
818        /// Byte range of the segment that revealed the mix.
819        span: Span,
820    },
821
822    /// A value carries characters ISO 9735-1 §9.1 requires to be suppressed.
823    ///
824    /// §9.1: "In variable length numeric data elements, leading zeroes shall be
825    /// suppressed... In variable length alphabetic and alphanumeric data
826    /// elements, trailing spaces shall be suppressed."
827    ///
828    /// Both are artefacts of a fixed-width source record copied into a
829    /// variable-length field. The value is still readable, so this is a warning
830    /// — but a receiver comparing `007` against the code `7`, or `"ACME "`
831    /// against `"ACME"`, will not match them.
832    #[error("segment {tag} element {element_index} component {component_index}: {kind}")]
833    InsignificantCharacters {
834        /// Segment tag containing the value.
835        tag: String,
836        /// Zero-based element index.
837        element_index: usize,
838        /// Zero-based component index.
839        component_index: usize,
840        /// Which rule of §9.1 was violated.
841        kind: Insignificant,
842        /// Byte range of the offending value.
843        span: Span,
844    },
845
846    /// A [`SegmentLayout`][crate::SegmentLayout] was applied to a segment with a different tag.
847    ///
848    /// Passing the `NAD` definition to a `DTM` segment would resolve codes
849    /// against the wrong table, which is exactly the class of mistake that
850    /// code-addressed access exists to prevent — so it is rejected up front.
851    #[error("segment layout is for {expected}, but the segment is {actual}")]
852    SegmentLayoutMismatch {
853        /// Tag the layout describes.
854        expected: String,
855        /// Tag of the segment the layout was applied to.
856        actual: String,
857    },
858}
859
860impl From<std::io::Error> for EdifactError {
861    fn from(e: std::io::Error) -> Self {
862        Self::Io(IoError(e))
863    }
864}
865
866impl EdifactError {
867    /// Stable diagnostic code for this error variant.
868    #[must_use]
869    pub const fn stable_code(&self) -> &'static str {
870        match self {
871            Self::UnexpectedEof { .. } => "E001",
872            Self::InvalidDelimiter { .. } => "E002",
873            Self::InvalidText { .. } => "E003",
874            Self::MessageCountMismatch { .. } => "E004",
875            Self::SegmentCountMismatch { .. } => "E005",
876            Self::InvalidSegmentTag(_) => "E006",
877            Self::InvalidUna => "E007",
878            Self::MissingRequiredElement { .. } => "E008",
879            Self::InvalidUtf8 => "E009",
880            Self::Io(_) => "E010",
881            Self::InvalidSegmentForMessage { .. } => "E011",
882            Self::InvalidElementCount { .. } => "E012",
883            Self::InvalidComponentCount { .. } => "E013",
884            Self::InvalidCodeValue { .. } => "E014",
885            Self::MissingSegment { .. } => "E015",
886            Self::QualifierMismatch { .. } => "E016",
887            Self::ConditionalRequirementNotMet { .. } => "E017",
888            // E018 is permanently retired (was ValidationFailed, removed in 0.8.0)
889            Self::InvalidReleaseSequence { .. } => "E019",
890            Self::SegmentTooLong { .. } => "E020",
891            Self::MissingRequiredComponent { .. } => "E021",
892            Self::UnexpectedMessageType { .. } => "E022",
893            Self::InterchangeTooLarge { .. } => "E023",
894            Self::InvalidEventSequence { .. } => "E024",
895            Self::InvalidElementPosition => "E025",
896            Self::IncompatibleReleaseScopes { .. } => "E026",
897            Self::InvalidFieldValue { .. } => "E027",
898            Self::UnexpectedDataToken { .. } => "E028",
899            // E029 is permanently retired (was FunctionalGroupNotSupported, removed when
900            // full UNG/UNE support was added — functional groups are now parsed natively)
901            Self::ValidationErrors { .. } => "E030",
902            Self::UnrecognisedSyntaxIdentifier(_) => "E031",
903            Self::DuplicateReference { .. } => "E032",
904            Self::UnknownDataElement { .. } => "E033",
905            Self::AmbiguousDataElement { .. } => "E034",
906            Self::SegmentLayoutMismatch { .. } => "E035",
907            Self::LimitExceeded { .. } => "E036",
908            Self::RepetitionSeparatorNotDeclared => "E037",
909            Self::CharacterNotInRepertoire { .. } => "E038",
910            Self::UnsupportedCharset { .. } => "E039",
911            Self::NonFiniteNumber { .. } => "E040",
912            Self::CharacterRepertoireMismatch { .. } => "E041",
913            Self::EmptyInterchange { .. } => "E042",
914            Self::EmptyMessage { .. } => "E043",
915            Self::PackageNotSupported { .. } => "E044",
916            Self::BlankDataElementValue { .. } => "E045",
917            Self::SegmentWithoutDataElements { .. } => "E046",
918            Self::TooManyRepetitions { .. } => "E047",
919            Self::InvalidCharacterType { .. } => "E048",
920            Self::DataElementTooLong { .. } => "E049",
921            Self::DataElementTooShort { .. } => "E050",
922            Self::TrailingSeparator { .. } => "E051",
923            Self::GroupsAndMessagesMixed { .. } => "E052",
924            Self::InsignificantCharacters { .. } => "E053",
925        }
926    }
927
928    /// Stable recovery hint for common malformed input and validation cases.
929    #[must_use]
930    pub fn recovery_hint(&self) -> Option<&'static str> {
931        match self {
932            Self::UnexpectedEof { .. } => {
933                Some("Ensure every segment ends with the configured segment terminator")
934            }
935            Self::InvalidDelimiter { .. } => {
936                Some("Check UNA service string advice and delimiter bytes in the payload")
937            }
938            Self::InvalidText { .. } => {
939                Some("Input must be valid UTF-8 text for segment and element values")
940            }
941            Self::InvalidReleaseSequence { .. } => {
942                Some("Release character must escape one following byte; trailing '?' is invalid")
943            }
944            Self::InvalidSegmentTag(_) => Some("Segment tags must be 3 ASCII uppercase letters"),
945            Self::InvalidUna => Some(
946                "UNA must be exactly 9 bytes: 'UNA' followed by 6 service characters, of which the active five must be distinct and non-whitespace",
947            ),
948            Self::MissingRequiredElement { .. } => {
949                Some("Provide all mandatory elements for the segment per directory rules")
950            }
951            Self::MissingRequiredComponent { .. } => Some(
952                "Provide all mandatory components for the composite element per directory rules",
953            ),
954            Self::InvalidSegmentForMessage { .. } => {
955                Some("Remove unsupported segment or switch to the correct message type")
956            }
957            Self::InvalidElementCount { .. } => {
958                Some("Adjust the segment element count to the allowed min/max range")
959            }
960            Self::InvalidComponentCount { .. } => {
961                Some("Fix composite element arity to match the expected component count")
962            }
963            Self::InvalidCodeValue { .. } => {
964                Some("Use a value from the referenced code list for this element")
965            }
966            Self::MissingSegment { .. } => {
967                Some("Insert the required segment at the expected position")
968            }
969            Self::QualifierMismatch { .. } => {
970                Some("Set the segment qualifier to the expected value")
971            }
972            Self::ConditionalRequirementNotMet { .. } => {
973                Some("When the condition is met, include the conditionally required element")
974            }
975            Self::SegmentTooLong { limit, .. } => {
976                let _ = limit; // used in the error message; hint is generic
977                Some("Increase max_segment_bytes in ReaderConfig or reject the input as malformed")
978            }
979            Self::InvalidEventSequence { .. } => {
980                Some("Emit StartSegment before Element, and Element before ComponentElement")
981            }
982            Self::InvalidElementPosition => Some(
983                "Set element position to a value >= 1; positions are one-based (1 = first element slot)",
984            ),
985            Self::IncompatibleReleaseScopes { .. } => Some(
986                "Only compose ProfileRulePack values that share the same release scope, or where at most one has a release scope set",
987            ),
988            Self::InvalidFieldValue { .. } => Some(
989                "Correct the field value to match the expected format or range for this element",
990            ),
991            Self::UnexpectedDataToken { .. } => Some(
992                "A data element appeared before any segment tag; check for partial writes or encoding corruption",
993            ),
994            Self::DuplicateReference { .. } => Some(
995                "Assign a unique control reference to every UNH (DE 0062) and UNG (DE 0048) within an interchange",
996            ),
997            Self::UnknownDataElement { .. } => Some(
998                "Check the data element identifier against the segment definition; the directory is the source of truth",
999            ),
1000            Self::AmbiguousDataElement { .. } => Some(
1001                "The code appears at more than one position; address the element positionally instead",
1002            ),
1003            Self::SegmentLayoutMismatch { .. } => {
1004                Some("Resolve codes against the segment definition whose tag matches the segment")
1005            }
1006            Self::LimitExceeded { .. } => {
1007                Some("Raise the corresponding ReaderConfig limit, or reject the input as oversized")
1008            }
1009            Self::RepetitionSeparatorNotDeclared => Some(
1010                "Build the writer with Writer::with_una and a ServiceStringAdvice whose repetition_sep is set",
1011            ),
1012            Self::CharacterNotInRepertoire { .. } => Some(
1013                "Transliterate the value into the declared repertoire, or declare a wider one in UNB S001 (UNOC for Latin-1, UNOY for UTF-8)",
1014            ),
1015            Self::UnsupportedCharset { .. } => Some(
1016                "UNOX and KECA are not supported; ask the partner for UNOC or UNOY, or transcode the interchange before parsing",
1017            ),
1018            Self::NonFiniteNumber { .. } => Some(
1019                "EDIFACT has no representation for NaN or infinity; check the calculation, or omit the element",
1020            ),
1021            Self::CharacterRepertoireMismatch { .. } => Some(
1022                "Pass the writer's own syntax identifier to begin_interchange, or bind the writer to the repertoire the header declares",
1023            ),
1024            Self::EmptyInterchange { .. } => Some(
1025                "An interchange must carry at least one message or group; send nothing rather than an empty envelope",
1026            ),
1027            Self::EmptyMessage { .. } => Some(
1028                "A message needs at least one segment between UNH and UNT; omit the message entirely if it has no content",
1029            ),
1030            Self::PackageNotSupported { .. } => Some(
1031                "Split the object out of the byte stream using the length in UNO S022 DE 0810, then parse the remaining segments",
1032            ),
1033            Self::BlankDataElementValue { .. } => Some(
1034                "Omit the data element instead of sending spaces; trailing spaces are insignificant and must be suppressed",
1035            ),
1036            Self::SegmentWithoutDataElements { .. } => {
1037                Some("Supply the segment's data, or omit the segment entirely if it is conditional")
1038            }
1039            Self::TooManyRepetitions { .. } => Some(
1040                "Reduce the occurrences to the definition's maximum, or correct the definition if the directory allows more",
1041            ),
1042            Self::InvalidCharacterType { .. } => Some(
1043                "Send a value of the declared class: `n` admits digits, an optional minus, a decimal mark and an exponent — never a space or a plus sign",
1044            ),
1045            Self::DataElementTooLong { .. } => Some(
1046                "Shorten the value to the declared maximum; length is counted in characters, and a numeric value excludes its sign, decimal mark and exponent",
1047            ),
1048            Self::DataElementTooShort { .. } => Some(
1049                "Pad the value to the declared fixed length, or correct the definition if the directory declares it variable",
1050            ),
1051            Self::TrailingSeparator { .. } => Some(
1052                "Stop emitting separators once the last value has been written; ISO 9735-1 §8.7.1 and §8.7.2 require trailing ones to be omitted",
1053            ),
1054            Self::GroupsAndMessagesMixed { .. } => Some(
1055                "Put every message inside a group, or none of them; ISO 9735-1 §7.1 does not allow both in one interchange",
1056            ),
1057            Self::InsignificantCharacters { .. } => Some(
1058                "Suppress the insignificant characters before sending: leading zeroes in a variable-length numeric value, trailing spaces in a variable-length text one",
1059            ),
1060            Self::UnrecognisedSyntaxIdentifier(_) => Some(
1061                "UNB S001 DE 0001 must name a defined repertoire: UNOA-UNOK, UNOX, UNOY, or KECA",
1062            ),
1063            Self::ValidationErrors { .. }
1064            | Self::MessageCountMismatch { .. }
1065            | Self::SegmentCountMismatch { .. }
1066            | Self::UnexpectedMessageType { .. }
1067            | Self::InterchangeTooLarge { .. }
1068            | Self::InvalidUtf8
1069            | Self::Io(_) => None,
1070        }
1071    }
1072}
1073
1074#[cfg(feature = "diagnostics")]
1075#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))]
1076impl miette::Diagnostic for EdifactError {
1077    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
1078        Some(Box::new(self.stable_code()))
1079    }
1080
1081    /// Mirrors the severity the validation pipeline assigns.
1082    ///
1083    /// The two must agree: a `miette` render that calls a control-reference
1084    /// mismatch a warning while [`ValidationReport`] files it as an error tells
1085    /// the operator and the program two different things about the same
1086    /// interchange. [`crate::ValidationReport`] is the source of truth, and this
1087    /// delegates to it.
1088    fn severity(&self) -> Option<miette::Severity> {
1089        Some(match crate::report::severity_for_error(self) {
1090            crate::ValidationSeverity::Info => miette::Severity::Advice,
1091            crate::ValidationSeverity::Warning => miette::Severity::Warning,
1092            crate::ValidationSeverity::Error | crate::ValidationSeverity::Critical => {
1093                miette::Severity::Error
1094            }
1095        })
1096    }
1097
1098    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
1099        match self {
1100            // Static text — no allocation needed.
1101            Self::InvalidUna => Some(Box::new(
1102                "UNA segment must be exactly 9 bytes: 'UNA' + 6 service characters. See EDIFACT spec",
1103            )),
1104            Self::InvalidUtf8 => Some(Box::new(
1105                "Internal error: serialized output contains invalid UTF-8. Please report this as a bug",
1106            )),
1107            // Dynamic help text.
1108            Self::UnexpectedEof { offset } => Some(Box::new(format!(
1109                "Check that all segments are terminated with the segment terminator (usually '). \
1110                 Reached end at offset {offset}",
1111            ))),
1112            Self::InvalidDelimiter { byte, offset } => Some(Box::new(format!(
1113                "The byte 0x{byte:02X} at offset {offset} is not a valid delimiter. \
1114                 Check UNA configuration",
1115            ))),
1116            Self::InvalidText { offset } => Some(Box::new(format!(
1117                "The byte sequence at offset {offset} contains invalid UTF-8. \
1118                 Ensure input is valid UTF-8",
1119            ))),
1120            Self::InvalidReleaseSequence { offset } => Some(Box::new(format!(
1121                "Release character at offset {offset} is dangling. \
1122                 Ensure '?' is followed by an escaped byte",
1123            ))),
1124            Self::MessageCountMismatch { expected, actual } => Some(Box::new(format!(
1125                "UNZ declares {expected} message(s) but {actual} UNH/UNT pair(s) were found. \
1126                 Check the UNZ message count",
1127            ))),
1128            Self::SegmentCountMismatch {
1129                expected,
1130                actual,
1131                message_ref,
1132                ..
1133            } => Some(Box::new(format!(
1134                "UNT for message {message_ref} declares {expected} segment(s) but {actual} were found. \
1135                 Check the UNT segment count",
1136            ))),
1137            Self::InvalidSegmentTag(tag) => Some(Box::new(format!(
1138                "Segment tag '{tag}' must be exactly 3 ASCII uppercase letters",
1139            ))),
1140            Self::MissingRequiredElement { tag, element_index } => Some(Box::new(format!(
1141                "Segment {tag} requires element at index {element_index}",
1142            ))),
1143            Self::MissingRequiredComponent {
1144                tag,
1145                element_index,
1146                component_index,
1147            } => Some(Box::new(format!(
1148                "Segment {tag} element {element_index} requires component at index {component_index}",
1149            ))),
1150            Self::Io(e) => Some(Box::new(format!("I/O error: {e}"))),
1151            Self::InvalidSegmentForMessage {
1152                tag, message_type, ..
1153            } => Some(Box::new(format!(
1154                "Segment {tag} should not appear in a {message_type} message. \
1155                 Check the directory definition",
1156            ))),
1157            Self::InvalidElementCount {
1158                tag,
1159                min,
1160                max,
1161                actual,
1162                ..
1163            } => Some(Box::new(format!(
1164                "Segment {tag} should have between {min} and {max} elements, but has {actual}. \
1165                 Check segment structure",
1166            ))),
1167            Self::InvalidComponentCount {
1168                tag,
1169                element_index,
1170                expected,
1171                actual,
1172                ..
1173            } => Some(Box::new(format!(
1174                "In segment {tag}, element {element_index} should have {expected} components \
1175                     but has {actual}. Check element structure",
1176            ))),
1177            Self::InvalidCodeValue {
1178                tag,
1179                element_index,
1180                value,
1181                code_list,
1182                ..
1183            } => Some(Box::new(format!(
1184                "Value '{value}' in segment {tag} element {element_index} is not in the \
1185                     {code_list} code list. Check the directory for valid codes",
1186            ))),
1187            Self::MissingSegment {
1188                tag,
1189                expected_position,
1190            } => Some(Box::new(format!(
1191                "Segment {tag} is required at position {expected_position} but is missing. \
1192                 Add this segment to the message",
1193            ))),
1194            Self::QualifierMismatch {
1195                tag,
1196                actual,
1197                expected,
1198                ..
1199            } => Some(Box::new(format!(
1200                "Segment {tag} has qualifier '{actual}' but expected '{expected}'. \
1201                 Check the segment's first component",
1202            ))),
1203            Self::ConditionalRequirementNotMet {
1204                tag,
1205                element_index,
1206                condition,
1207                ..
1208            } => Some(Box::new(format!(
1209                "In segment {tag}, element {element_index} is conditionally required when: \
1210                     {condition}. Check if the condition is met",
1211            ))),
1212            Self::SegmentTooLong { offset, limit } => Some(Box::new(format!(
1213                "Segment starting at byte offset {offset} exceeds the {limit}-byte limit. \
1214                 Use ReaderConfig::max_segment_bytes to adjust the limit if needed, \
1215                 or verify the input for a missing segment terminator",
1216            ))),
1217            Self::UnexpectedMessageType { message_type } => Some(Box::new(format!(
1218                "No handler was registered for message type '{message_type}'. \
1219                 Register a handler with MessageDispatch::on(\"{message_type}\", ...)",
1220            ))),
1221            Self::InterchangeTooLarge { count } => Some(Box::new(format!(
1222                "Interchange contains {count} items which exceeds the u32::MAX limit. \
1223                 This is an extremely unusual input; verify the message is not corrupted.",
1224            ))),
1225            Self::InvalidEventSequence { message } => Some(Box::new(format!(
1226                "Event sequence violation: {message}. \
1227                 Check that StartSegment is emitted before Element, and Element before ComponentElement.",
1228            ))),
1229            Self::InvalidElementPosition => Some(Box::new(
1230                "Element positions must be >= 1 (one-based). \
1231                 Ensure no OwnedElementRef is constructed with position == 0",
1232            )),
1233            Self::IncompatibleReleaseScopes { current, incoming } => Some(Box::new(format!(
1234                "Release scope {current:?} and {incoming:?} are incompatible. \
1235                 Only compose ProfileRulePack values that share the same release scope, \
1236                 or where at most one carries a release scope",
1237            ))),
1238            Self::InvalidFieldValue {
1239                tag,
1240                element_index,
1241                value,
1242            } => Some(Box::new(format!(
1243                "Segment {tag} element {element_index} has invalid value '{value}'. \
1244                 Check the expected format or range for this field",
1245            ))),
1246            Self::UnexpectedDataToken { offset } => Some(Box::new(format!(
1247                "Data element at offset {offset} appeared before any segment tag. \
1248                 Check for partial writes or encoding corruption",
1249            ))),
1250            Self::ValidationErrors { error_count, .. } => Some(Box::new(format!(
1251                "Validation found {error_count} error(s). Inspect the ValidationReport for details",
1252            ))),
1253            Self::UnrecognisedSyntaxIdentifier(id) => Some(Box::new(format!(
1254                "Syntax identifier '{id}' is not defined in ISO 9735-1. \
1255                 Valid values are UNOA, UNOB, UNOC, UNOD, UNOE, UNOF (or KECA for KEC-A profile)",
1256            ))),
1257            Self::DuplicateReference { tag, reference, .. } => Some(Box::new(format!(
1258                "Reference '{reference}' is used by more than one {tag} in this interchange; \
1259                 each must be unique so receivers can address messages unambiguously",
1260            ))),
1261            Self::UnknownDataElement { tag, data_element } => Some(Box::new(format!(
1262                "Segment {tag} does not define data element {data_element}. \
1263                 Check the identifier against the directory definition for {tag}",
1264            ))),
1265            Self::AmbiguousDataElement { tag, data_element } => Some(Box::new(format!(
1266                "Segment {tag} defines data element {data_element} at more than one position, \
1267                 so code-addressed access cannot pick one; use a positional accessor",
1268            ))),
1269            Self::SegmentLayoutMismatch { expected, actual } => Some(Box::new(format!(
1270                "The supplied layout describes segment {expected} but was applied to {actual}. \
1271                 Look up the definition by the segment's own tag",
1272            ))),
1273            Self::RepetitionSeparatorNotDeclared => Some(Box::new(
1274                "UNA position 7 holds the space \"not used\" sentinel, so repeating data \
1275                 elements cannot be expressed. Use Writer::with_una with a repetition_sep",
1276            )),
1277            Self::CharacterNotInRepertoire {
1278                charset,
1279                character,
1280                offset,
1281            } => Some(Box::new(format!(
1282                "The character {character:?} at offset {offset} has no representation in {charset}. \
1283                 Transliterate it, or declare a wider repertoire in UNB S001 DE 0001",
1284            ))),
1285            Self::UnsupportedCharset { syntax_identifier } => Some(Box::new(format!(
1286                "'{syntax_identifier}' is stateful or multi-byte, so byte-level delimiter scanning \
1287                 would be unsound. Transcode the interchange to UNOC or UNOY before parsing",
1288            ))),
1289            Self::CharacterRepertoireMismatch { declared, writer } => Some(Box::new(format!(
1290                "The UNB declares {declared} but the writer encodes {writer}. \
1291                 The receiver would decode the body with the wrong table",
1292            ))),
1293            Self::NonFiniteNumber { value } => Some(Box::new(format!(
1294                "The value {value} is not finite. EDIFACT numeric data elements have no \
1295                 representation for NaN or infinity",
1296            ))),
1297            Self::LimitExceeded { limit, max } => Some(Box::new(format!(
1298                "The input exceeds the configured {limit} limit of {max}. \
1299                 Raise it via ReaderConfig if the input is legitimate, or reject the input",
1300            ))),
1301            Self::EmptyInterchange { control_ref } => Some(Box::new(format!(
1302                "Interchange {control_ref} carries no message and no group. \
1303                 ISO 9735-1 §7.1 requires at least one; send nothing rather than an empty envelope",
1304            ))),
1305            Self::EmptyMessage { message_ref, .. } => Some(Box::new(format!(
1306                "Message {message_ref} has nothing between UNH and UNT. \
1307                 ISO 9735-1 §7.3 requires at least one additional segment",
1308            ))),
1309            Self::PackageNotSupported { tag, .. } => Some(Box::new(format!(
1310                "{tag} opens or closes a package, whose object is arbitrary binary data \
1311                 rather than EDIFACT. Split it out using the length in UNO S022 DE 0810, \
1312                 then parse the remaining segments",
1313            ))),
1314            Self::BlankDataElementValue {
1315                tag,
1316                element_index,
1317                component_index,
1318                ..
1319            } => Some(Box::new(format!(
1320                "Segment {tag} element {element_index} component {component_index} holds only \
1321                 spaces. ISO 9735-1 §9.3 forbids that — omit the element instead",
1322            ))),
1323            Self::SegmentWithoutDataElements { tag, .. } => Some(Box::new(format!(
1324                "Segment {tag} carries only its tag. ISO 9735-1 §7.5 requires at least one \
1325                 data element; §8.5 says a conditional segment with no data is omitted entirely",
1326            ))),
1327            Self::TooManyRepetitions {
1328                tag,
1329                element_index,
1330                max,
1331                actual,
1332                ..
1333            } => Some(Box::new(format!(
1334                "Segment {tag} element {element_index} occurs {actual} times but its definition \
1335                 allows at most {max}",
1336            ))),
1337            Self::InvalidCharacterType {
1338                repr, value, tag, ..
1339            } => Some(Box::new(format!(
1340                "Segment {tag}: {value:?} is not a valid {repr} value. ISO 9735-1 §10 admits \
1341                 digits, an optional minus sign, a decimal mark and an exponent — the space \
1342                 character and the plus sign are not allowed",
1343            ))),
1344            Self::DataElementTooLong {
1345                repr, actual, tag, ..
1346            } => Some(Box::new(format!(
1347                "Segment {tag}: the value is {actual} characters, but the directory declares \
1348                 {repr}. Length is counted in characters rather than bytes",
1349            ))),
1350            Self::DataElementTooShort {
1351                repr, actual, tag, ..
1352            } => Some(Box::new(format!(
1353                "Segment {tag}: the value is {actual} characters, but the directory declares the \
1354                 fixed length {repr}",
1355            ))),
1356            Self::TrailingSeparator {
1357                tag, element_index, ..
1358            } => Some(Box::new(match element_index {
1359                Some(index) => format!(
1360                    "Segment {tag} element {index} ends in a component separator with no value \
1361                     after it. ISO 9735-1 §8.7.2 requires trailing component separators to be \
1362                     omitted",
1363                ),
1364                None => format!(
1365                    "Segment {tag} ends in a data element separator with no value after it. \
1366                     ISO 9735-1 §8.7.1 requires trailing data element separators to be omitted",
1367                ),
1368            })),
1369            Self::GroupsAndMessagesMixed { .. } => Some(Box::new(
1370                "ISO 9735-1 §7.1 lists what an interchange may contain, and the entries are \
1371                 exclusive: groups containing messages, or bare messages — never both, because a \
1372                 message outside every group cannot be counted in UNZ DE 0036",
1373            )),
1374            Self::InsignificantCharacters { tag, kind, .. } => Some(Box::new(format!(
1375                "Segment {tag}: {kind}. ISO 9735-1 §9.1 requires insignificant characters to be \
1376                 suppressed before transfer",
1377            ))),
1378        }
1379    }
1380}
1381
1382// ── validation report ─────────────────────────────────────────────────────────
1383
1384pub use crate::report::ValidationReport;
1385
1386#[cfg(test)]
1387mod tests {
1388    use super::*;
1389
1390    #[test]
1391    fn recovery_hint_exists_for_common_malformed_cases() {
1392        let err = EdifactError::InvalidReleaseSequence { offset: 10 };
1393        assert!(err.recovery_hint().is_some());
1394
1395        let err = EdifactError::InvalidCodeValue {
1396            tag: "BGM".to_owned(),
1397            element_index: 0,
1398            value: "X".to_owned(),
1399            code_list: "1001".to_owned(),
1400            span: Span::new(0, 9),
1401            suggestion: None,
1402        };
1403        assert!(err.recovery_hint().is_some());
1404    }
1405}