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    /// An interchange or message contains more segments or messages than can be
382    /// represented in a `u32` counter (> 4 294 967 295).
383    ///
384    /// This is effectively unreachable in practice — no real-world EDIFACT
385    /// interchange has billions of segments — but the parser returns this error
386    /// rather than silently saturating or wrapping the counter.
387    #[error("interchange too large: count {count} exceeds u32::MAX")]
388    InterchangeTooLarge {
389        /// The count that could not be represented as `u32`.
390        count: u64,
391    },
392
393    /// An [`crate::EventEmitter`] received events in an invalid sequence.
394    ///
395    /// This indicates a programming error in the caller's serialization code:
396    /// for example, emitting an [`crate::EdifactEvent::Element`] without a prior
397    /// [`crate::EdifactEvent::StartSegment`], or emitting
398    /// [`crate::EdifactEvent::ComponentElement`] without a preceding
399    /// [`crate::EdifactEvent::Element`].
400    #[error("invalid event sequence: {message}")]
401    InvalidEventSequence {
402        /// Description of the protocol violation.
403        message: &'static str,
404    },
405
406    /// An [`crate::OwnedElementRef`] has `position = 0`, which is never valid.
407    ///
408    /// Element positions are one-based: position 1 refers to the first element
409    /// slot.  Position 0 is reserved and invalid.  Use [`crate::OwnedElementRef::try_new`]
410    /// to get a `Result` instead of a panic.
411    #[error("element definition contains invalid position 0; positions must be >= 1 (one-based)")]
412    InvalidElementPosition,
413
414    /// Two [`crate::ProfileRulePack`] values with incompatible release scopes were composed.
415    ///
416    /// When composing packs via [`crate::ProfileRulePack::extend_from`] or
417    /// [`crate::ProfileRulePack::merge_with_override`], both packs must either
418    /// share the same release scope or at most one may carry a scope.
419    #[error("incompatible release scopes: cannot compose {current:?} with {incoming:?}")]
420    IncompatibleReleaseScopes {
421        /// Release scope of the pack being composed into.
422        current: String,
423        /// Release scope of the pack being composed in.
424        incoming: String,
425    },
426
427    /// A field value failed semantic validation (e.g. wrong format, out-of-range).
428    ///
429    /// Distinct from [`InvalidCodeValue`][Self::InvalidCodeValue] which is for
430    /// code-list membership checks.  Use this variant when a free-text or numeric
431    /// field contains a value that is structurally invalid for its purpose.
432    #[error("segment {tag} element {element_index}: invalid field value {value:?}")]
433    InvalidFieldValue {
434        /// Segment tag that contains the invalid field.
435        tag: String,
436        /// Zero-based element index of the invalid field.
437        element_index: usize,
438        /// The invalid value that was observed.
439        value: String,
440    },
441
442    /// A data or component element token appeared before the first segment tag.
443    ///
444    /// EDIFACT syntax requires that every data element follows a segment tag.
445    /// A data element token encountered before any tag (e.g. after a stray
446    /// separator at the start of the stream) is a protocol violation.
447    ///
448    /// Unlike stray segment terminators (which are tolerated as blank lines),
449    /// stray data tokens indicate encoding corruption or a partial write.
450    #[error("unexpected data token at byte offset {offset}: data element before segment tag")]
451    UnexpectedDataToken {
452        /// Byte offset of the stray token.
453        offset: usize,
454    },
455
456    /// The interchange syntax identifier (`UNB` S001 DE 0001) names no defined
457    /// character repertoire.
458    ///
459    /// DE 0001 is `UN` followed by a two-character repertoire code, so the
460    /// defined values are `UNOA` through `UNOK`, `UNOX`, `UNOY`, and `KECA`
461    /// (Korean EDI Centre A).  Anything else is a non-standard generator or a
462    /// corrupted `UNB`.
463    ///
464    /// A value that *is* defined but that this crate cannot decode is
465    /// [`UnsupportedCharset`][Self::UnsupportedCharset] instead — the two say
466    /// different things about whose problem it is.
467    #[error("unrecognised syntax identifier '{0}': expected UNOA-UNOK, UNOX, UNOY, or KECA")]
468    UnrecognisedSyntaxIdentifier(String),
469
470    /// A control reference was reused within the scope that requires it to be unique.
471    ///
472    /// ISO 9735-1 requires the message reference number (`UNH` DE 0062) to be
473    /// unique within an interchange, and the group reference number (`UNG`
474    /// DE 0048) to be unique within an interchange.  Duplicates make a message
475    /// unaddressable: a receiver keying on the reference silently processes one
476    /// occurrence and drops the rest.
477    #[error("duplicate {tag} reference '{reference}' at bytes {span}")]
478    DuplicateReference {
479        /// Segment tag that carries the duplicated reference (`UNH` or `UNG`).
480        tag: String,
481        /// The reference value that appeared more than once.
482        reference: String,
483        /// Byte range of the duplicate occurrence.
484        span: Span,
485    },
486
487    /// A UN/EDIFACT data element code was not found in the segment definition.
488    ///
489    /// Produced by the code-addressed accessors
490    /// ([`Segment::value_by_code`][crate::Segment::value_by_code] and friends)
491    /// when the requested data element identifier does not appear anywhere in
492    /// the supplied [`SegmentLayout`][crate::SegmentLayout].  This is the error
493    /// that turns a mistyped or stale DE reference into a loud failure instead
494    /// of a silent off-by-one read of the wrong element.
495    #[error("segment {tag} has no data element {data_element} in its definition")]
496    UnknownDataElement {
497        /// Segment tag whose definition was searched.
498        tag: String,
499        /// The data element identifier that was not found.
500        data_element: String,
501    },
502
503    /// A UN/EDIFACT data element code appears more than once in a segment definition.
504    ///
505    /// Code-addressed access requires an unambiguous target.  When a directory
506    /// genuinely repeats a code (e.g. the same DE used at two positions), address
507    /// it positionally with [`Segment::element_str`][crate::Segment::element_str]
508    /// or split the definition.
509    #[error("segment {tag} defines data element {data_element} at more than one position")]
510    AmbiguousDataElement {
511        /// Segment tag whose definition was searched.
512        tag: String,
513        /// The data element identifier that resolved to multiple positions.
514        data_element: String,
515    },
516
517    /// A configured [`ReaderConfig`][crate::ReaderConfig] resource limit was exceeded.
518    ///
519    /// Raised by the parsing iterators when the input carries more segments,
520    /// messages, or bytes than the caller allowed.  The limit is reported rather
521    /// than silently applied: a budget that ends the iterator without an error is
522    /// indistinguishable from a clean end of input, so the caller would accept a
523    /// **truncated** interchange as complete.
524    ///
525    /// [`SegmentTooLong`][Self::SegmentTooLong] covers the per-segment size
526    /// guard; this variant covers the whole-input budgets.
527    #[error("input exceeded the configured {limit} limit of {max}")]
528    LimitExceeded {
529        /// Name of the limit that tripped: `"max_segments"`, `"max_messages"`,
530        /// or `"max_input_bytes"`.
531        limit: &'static str,
532        /// The configured ceiling.
533        max: u64,
534    },
535
536    /// A repeating data element was written under a service string advice that
537    /// declares no repetition separator.
538    ///
539    /// ISO 9735-1 §8.6 repetitions can only be expressed when `UNA` position 7
540    /// carries a real separator.  With the space "not used" sentinel there is no
541    /// byte to write between occurrences, and joining them anyway would emit
542    /// output that reads back as a single occurrence — silent data corruption.
543    /// Construct the writer with [`Writer::with_una`][crate::Writer::with_una]
544    /// and a service string advice whose `repetition_sep` is set.
545    #[error(
546        "cannot write a repeating data element: the active service string advice declares no repetition separator"
547    )]
548    RepetitionSeparatorNotDeclared,
549
550    /// A non-finite float was handed to a numeric serializer.
551    ///
552    /// ISO 9735-1 §10 admits the ISO 6093 numeric representations — digits, an
553    /// optional minus sign, a decimal mark, an exponent — and nothing else.
554    /// There is no representation for `NaN` or infinity, and `Display` would
555    /// emit `NaN` / `inf`, text no receiver can parse and that the crate's own
556    /// reader would hand back as a string.
557    #[error("non-finite number {value} has no EDIFACT representation")]
558    NonFiniteNumber {
559        /// The offending value, formatted for the message.
560        value: String,
561    },
562
563    /// A character cannot be represented in the interchange's declared repertoire.
564    ///
565    /// The `UNB` S001 DE 0001 syntax identifier names the character repertoire the
566    /// payload is written in (`UNOA`, `UNOC`, …).  Writing a character outside it
567    /// produces bytes the receiver decodes as something else — or as nothing at
568    /// all — so the writer refuses instead.
569    ///
570    /// Also raised by [`Charset::encode`][crate::Charset::encode].
571    #[error(
572        "character {character:?} at offset {offset} is not in the {charset} character repertoire"
573    )]
574    CharacterNotInRepertoire {
575        /// The syntax identifier of the repertoire that rejected the character.
576        charset: &'static str,
577        /// The offending character.
578        character: char,
579        /// Byte offset of the character within the value it appeared in.
580        offset: usize,
581    },
582
583    /// A `UNB` was written declaring a repertoire other than the writer's own.
584    ///
585    /// A header that names `UNOA` while the body goes out as ISO 8859-1 is
586    /// unreadable at the far end in exactly the way that is hardest to diagnose,
587    /// so [`Writer::begin_interchange`][crate::Writer::begin_interchange] refuses
588    /// the combination rather than emitting it.
589    #[error("UNB declares repertoire {declared}, but the writer encodes {writer}")]
590    CharacterRepertoireMismatch {
591        /// Syntax identifier passed to `begin_interchange`.
592        declared: String,
593        /// Repertoire the writer was bound to with `Writer::with_charset`.
594        writer: &'static str,
595    },
596
597    /// The interchange declares a character repertoire this crate cannot decode.
598    ///
599    /// `UNOX` (ISO 2022 code extension) and `KECA` (Korean) are stateful or
600    /// multi-byte in ways that break the byte-level delimiter scanning every
601    /// other repertoire allows.  They are reported rather than silently
602    /// mis-decoded.
603    #[error("character repertoire '{syntax_identifier}' is not supported")]
604    UnsupportedCharset {
605        /// The `UNB` S001 DE 0001 value that could not be handled.
606        syntax_identifier: String,
607    },
608
609    /// An interchange carries neither a message nor a group.
610    ///
611    /// ISO 9735-1 §7.1: an interchange "shall contain at least one group, or one
612    /// message or one package".  A bare `UNB`/`UNZ` pair is a delivery that says
613    /// nothing, and because `UNZ+0` makes the control count agree with the
614    /// (absent) content, no count check can catch it.
615    #[error("interchange {control_ref} contains no message or group")]
616    EmptyInterchange {
617        /// Interchange control reference (`UNB` DE 0020) of the empty interchange.
618        control_ref: String,
619    },
620
621    /// A message carries no segment between its header and trailer.
622    ///
623    /// ISO 9735-1 §7.3: a message "shall be started and identified by a message
624    /// header, shall be terminated by a message trailer, and shall contain at
625    /// least one additional segment".
626    #[error("message {message_ref} has no segments between UNH and UNT")]
627    EmptyMessage {
628        /// Message reference number (`UNH` DE 0062) of the empty message.
629        message_ref: String,
630        /// Byte range of the offending `UNH`.
631        span: Span,
632    },
633
634    /// The interchange carries a package (`UNO`…`UNP`), which this crate does not
635    /// tokenize.
636    ///
637    /// A package wraps an arbitrary object — ISO 9735-1 §7.9, elaborated by
638    /// ISO 9735-8 — whose bytes are not EDIFACT-encoded and whose length is
639    /// declared in `UNO` S022 DE 0810.  Feeding those bytes to a tokenizer that
640    /// scans for delimiters produces nonsense, so the package is reported instead.
641    /// Split the object out of the byte stream using the declared length before
642    /// parsing the remainder.
643    #[error("segment {tag} opens or closes a package, which this crate does not parse")]
644    PackageNotSupported {
645        /// The package segment encountered: `UNO` or `UNP`.
646        tag: String,
647        /// Byte range of the offending segment.
648        span: Span,
649    },
650
651    /// A data element value consists only of spaces.
652    ///
653    /// ISO 9735-1 §9.3: "A data element value containing only space(s) shall not
654    /// be allowed."  Trailing spaces are insignificant and must be suppressed
655    /// (§9.1), so a value that is nothing but spaces is an element that should
656    /// have been omitted — and a receiver comparing it against a code list, a
657    /// reference, or a previous message will not treat it as absent.
658    #[error(
659        "segment {tag} element {element_index} component {component_index}: value is only spaces"
660    )]
661    BlankDataElementValue {
662        /// Segment tag containing the blank value.
663        tag: String,
664        /// Zero-based element index.
665        element_index: usize,
666        /// Zero-based component index.
667        component_index: usize,
668        /// Byte range of the offending value.
669        span: Span,
670    },
671
672    /// A segment carries nothing but its tag.
673    ///
674    /// ISO 9735-1 §7.5: "A segment shall contain at least one data element in
675    /// addition to the segment tag."  §8.5 adds that a conditional segment whose
676    /// only content is the tag "shall be omitted in its entirety" — so `ABC'` is
677    /// either a mandatory segment that lost its data or a conditional one that
678    /// should not have been sent.
679    #[error("segment {tag} contains no data element")]
680    SegmentWithoutDataElements {
681        /// The offending segment tag.
682        tag: String,
683        /// Byte range of the offending segment.
684        span: Span,
685    },
686
687    /// A data element occurred more times than its definition allows.
688    ///
689    /// ISO 9735-1 §7.5: "Each stand-alone or composite data element's position,
690    /// status and maximum number of occurrences within the segment structure
691    /// shall be stated in the segment specification." Exceeding the stated
692    /// maximum is a structural violation, reported by `CONTRL` as code 35.
693    #[error("segment {tag} element {element_index} occurs {actual} times, at most {max} allowed")]
694    TooManyRepetitions {
695        /// Segment tag carrying the over-repeated element.
696        tag: String,
697        /// Zero-based element index.
698        element_index: usize,
699        /// Maximum occurrences the definition allows.
700        max: u8,
701        /// Occurrences actually present.
702        actual: usize,
703        /// Byte range of the offending element.
704        span: Span,
705    },
706
707    /// A value's characters do not match its declared representation class.
708    ///
709    /// A directory types every data element `a` (alphabetic), `n` (numeric), or
710    /// `an` (alphanumeric). ISO 9735-1 §10 fixes what "numeric" admits: digits,
711    /// an optional minus, a decimal mark, and an exponent — and explicitly not
712    /// the space character or a plus sign. Reported by `CONTRL` as code 37.
713    #[error(
714        "segment {tag} element {element_index} component {component_index}: {value:?} is not {repr}"
715    )]
716    InvalidCharacterType {
717        /// Segment tag containing the value.
718        tag: String,
719        /// Zero-based element index.
720        element_index: usize,
721        /// Zero-based component index.
722        component_index: usize,
723        /// The declared representation, e.g. `n..6`.
724        repr: String,
725        /// The offending value.
726        value: String,
727        /// Byte range of the offending value.
728        span: Span,
729    },
730
731    /// A value is longer than its declared representation allows.
732    ///
733    /// Length is counted in **characters**, not bytes (ISO 9735-1 §6), and for a
734    /// numeric value excludes the sign, the decimal mark, and the exponent
735    /// (§10). Reported by `CONTRL` as code 39.
736    #[error(
737        "segment {tag} element {element_index} component {component_index}: {actual} characters exceeds {repr}"
738    )]
739    DataElementTooLong {
740        /// Segment tag containing the value.
741        tag: String,
742        /// Zero-based element index.
743        element_index: usize,
744        /// Zero-based component index.
745        component_index: usize,
746        /// The declared representation, e.g. `an..35`.
747        repr: String,
748        /// The measured character count.
749        actual: usize,
750        /// Byte range of the offending value.
751        span: Span,
752    },
753
754    /// A value is shorter than its declared fixed-length representation.
755    ///
756    /// Only a fixed-length representation (`n8`, `a1`) has a minimum above one;
757    /// a variable one (`an..35`) is satisfied by any non-empty value. Reported
758    /// by `CONTRL` as code 40.
759    #[error(
760        "segment {tag} element {element_index} component {component_index}: {actual} characters is short of {repr}"
761    )]
762    DataElementTooShort {
763        /// Segment tag containing the value.
764        tag: String,
765        /// Zero-based element index.
766        element_index: usize,
767        /// Zero-based component index.
768        component_index: usize,
769        /// The declared representation, e.g. `n8`.
770        repr: String,
771        /// The measured character count.
772        actual: usize,
773        /// Byte range of the offending value.
774        span: Span,
775    },
776
777    /// A segment or composite ends in separators that carry no value.
778    ///
779    /// ISO 9735-1 §8.7.1: "If one or more non-repeating composite data elements
780    /// or stand-alone data elements at the end of a segment are omitted, the
781    /// data element separators which would normally follow them shall also be
782    /// omitted." §8.7.2 says the same for components at the end of a composite.
783    ///
784    /// `BGM+220+'` and `DTM+137:20260101:'` are therefore both malformed, and a
785    /// receiver that trims them is being generous rather than correct. Reported
786    /// by `CONTRL` as code 45.
787    #[error("segment {tag} ends in a separator that carries no value")]
788    TrailingSeparator {
789        /// Segment tag carrying the trailing separator.
790        tag: String,
791        /// `Some(index)` when the trailing separators close that element's
792        /// composite; `None` when they close the segment itself.
793        element_index: Option<usize>,
794        /// Byte range of the offending segment or element.
795        span: Span,
796    },
797
798    /// An interchange mixes groups with ungrouped messages.
799    ///
800    /// ISO 9735-1 §7.1 lists what an interchange may contain, and every entry is
801    /// exclusive: messages, or packages, or groups containing them — never a
802    /// group alongside a bare message. A message outside every group has no
803    /// group to be counted in, so `UNZ` DE 0036 cannot describe the interchange
804    /// at all. Reported by `CONTRL` as code 30.
805    #[error("interchange mixes groups with ungrouped messages")]
806    GroupsAndMessagesMixed {
807        /// Byte range of the segment that revealed the mix.
808        span: Span,
809    },
810
811    /// A value carries characters ISO 9735-1 §9.1 requires to be suppressed.
812    ///
813    /// §9.1: "In variable length numeric data elements, leading zeroes shall be
814    /// suppressed... In variable length alphabetic and alphanumeric data
815    /// elements, trailing spaces shall be suppressed."
816    ///
817    /// Both are artefacts of a fixed-width source record copied into a
818    /// variable-length field. The value is still readable, so this is a warning
819    /// — but a receiver comparing `007` against the code `7`, or `"ACME "`
820    /// against `"ACME"`, will not match them.
821    #[error("segment {tag} element {element_index} component {component_index}: {kind}")]
822    InsignificantCharacters {
823        /// Segment tag containing the value.
824        tag: String,
825        /// Zero-based element index.
826        element_index: usize,
827        /// Zero-based component index.
828        component_index: usize,
829        /// Which rule of §9.1 was violated.
830        kind: Insignificant,
831        /// Byte range of the offending value.
832        span: Span,
833    },
834
835    /// A [`SegmentLayout`][crate::SegmentLayout] was applied to a segment with a different tag.
836    ///
837    /// Passing the `NAD` definition to a `DTM` segment would resolve codes
838    /// against the wrong table, which is exactly the class of mistake that
839    /// code-addressed access exists to prevent — so it is rejected up front.
840    #[error("segment layout is for {expected}, but the segment is {actual}")]
841    SegmentLayoutMismatch {
842        /// Tag the layout describes.
843        expected: String,
844        /// Tag of the segment the layout was applied to.
845        actual: String,
846    },
847}
848
849impl From<std::io::Error> for EdifactError {
850    fn from(e: std::io::Error) -> Self {
851        Self::Io(IoError(e))
852    }
853}
854
855impl EdifactError {
856    /// Stable diagnostic code for this error variant.
857    #[must_use]
858    pub const fn stable_code(&self) -> &'static str {
859        match self {
860            Self::UnexpectedEof { .. } => "E001",
861            Self::InvalidDelimiter { .. } => "E002",
862            Self::InvalidText { .. } => "E003",
863            Self::MessageCountMismatch { .. } => "E004",
864            Self::SegmentCountMismatch { .. } => "E005",
865            Self::InvalidSegmentTag(_) => "E006",
866            Self::InvalidUna => "E007",
867            Self::MissingRequiredElement { .. } => "E008",
868            Self::InvalidUtf8 => "E009",
869            Self::Io(_) => "E010",
870            Self::InvalidSegmentForMessage { .. } => "E011",
871            Self::InvalidElementCount { .. } => "E012",
872            Self::InvalidComponentCount { .. } => "E013",
873            Self::InvalidCodeValue { .. } => "E014",
874            Self::MissingSegment { .. } => "E015",
875            Self::QualifierMismatch { .. } => "E016",
876            Self::ConditionalRequirementNotMet { .. } => "E017",
877            // E018 is permanently retired (was ValidationFailed, removed in 0.8.0)
878            Self::InvalidReleaseSequence { .. } => "E019",
879            Self::SegmentTooLong { .. } => "E020",
880            Self::MissingRequiredComponent { .. } => "E021",
881            // E022 is permanently retired (was UnexpectedMessageType, removed
882            // in 0.17.0 along with the type-erased MessageDispatch it served).
883            Self::InterchangeTooLarge { .. } => "E023",
884            Self::InvalidEventSequence { .. } => "E024",
885            Self::InvalidElementPosition => "E025",
886            Self::IncompatibleReleaseScopes { .. } => "E026",
887            Self::InvalidFieldValue { .. } => "E027",
888            Self::UnexpectedDataToken { .. } => "E028",
889            // E029 is permanently retired (was FunctionalGroupNotSupported, removed when
890            // full UNG/UNE support was added — functional groups are now parsed natively)
891            Self::ValidationErrors { .. } => "E030",
892            Self::UnrecognisedSyntaxIdentifier(_) => "E031",
893            Self::DuplicateReference { .. } => "E032",
894            Self::UnknownDataElement { .. } => "E033",
895            Self::AmbiguousDataElement { .. } => "E034",
896            Self::SegmentLayoutMismatch { .. } => "E035",
897            Self::LimitExceeded { .. } => "E036",
898            Self::RepetitionSeparatorNotDeclared => "E037",
899            Self::CharacterNotInRepertoire { .. } => "E038",
900            Self::UnsupportedCharset { .. } => "E039",
901            Self::NonFiniteNumber { .. } => "E040",
902            Self::CharacterRepertoireMismatch { .. } => "E041",
903            Self::EmptyInterchange { .. } => "E042",
904            Self::EmptyMessage { .. } => "E043",
905            Self::PackageNotSupported { .. } => "E044",
906            Self::BlankDataElementValue { .. } => "E045",
907            Self::SegmentWithoutDataElements { .. } => "E046",
908            Self::TooManyRepetitions { .. } => "E047",
909            Self::InvalidCharacterType { .. } => "E048",
910            Self::DataElementTooLong { .. } => "E049",
911            Self::DataElementTooShort { .. } => "E050",
912            Self::TrailingSeparator { .. } => "E051",
913            Self::GroupsAndMessagesMixed { .. } => "E052",
914            Self::InsignificantCharacters { .. } => "E053",
915        }
916    }
917
918    /// Stable recovery hint for common malformed input and validation cases.
919    #[must_use]
920    pub fn recovery_hint(&self) -> Option<&'static str> {
921        match self {
922            Self::UnexpectedEof { .. } => {
923                Some("Ensure every segment ends with the configured segment terminator")
924            }
925            Self::InvalidDelimiter { .. } => {
926                Some("Check UNA service string advice and delimiter bytes in the payload")
927            }
928            Self::InvalidText { .. } => {
929                Some("Input must be valid UTF-8 text for segment and element values")
930            }
931            Self::InvalidReleaseSequence { .. } => {
932                Some("Release character must escape one following byte; trailing '?' is invalid")
933            }
934            Self::InvalidSegmentTag(_) => Some("Segment tags must be 3 ASCII uppercase letters"),
935            Self::InvalidUna => Some(
936                "UNA must be exactly 9 bytes: 'UNA' followed by 6 service characters, of which the active five must be distinct and non-whitespace",
937            ),
938            Self::MissingRequiredElement { .. } => {
939                Some("Provide all mandatory elements for the segment per directory rules")
940            }
941            Self::MissingRequiredComponent { .. } => Some(
942                "Provide all mandatory components for the composite element per directory rules",
943            ),
944            Self::InvalidSegmentForMessage { .. } => {
945                Some("Remove unsupported segment or switch to the correct message type")
946            }
947            Self::InvalidElementCount { .. } => {
948                Some("Adjust the segment element count to the allowed min/max range")
949            }
950            Self::InvalidComponentCount { .. } => {
951                Some("Fix composite element arity to match the expected component count")
952            }
953            Self::InvalidCodeValue { .. } => {
954                Some("Use a value from the referenced code list for this element")
955            }
956            Self::MissingSegment { .. } => {
957                Some("Insert the required segment at the expected position")
958            }
959            Self::QualifierMismatch { .. } => {
960                Some("Set the segment qualifier to the expected value")
961            }
962            Self::ConditionalRequirementNotMet { .. } => {
963                Some("When the condition is met, include the conditionally required element")
964            }
965            Self::SegmentTooLong { limit, .. } => {
966                let _ = limit; // used in the error message; hint is generic
967                Some("Increase max_segment_bytes in ReaderConfig or reject the input as malformed")
968            }
969            Self::InvalidEventSequence { .. } => {
970                Some("Emit StartSegment before Element, and Element before ComponentElement")
971            }
972            Self::InvalidElementPosition => Some(
973                "Set element position to a value >= 1; positions are one-based (1 = first element slot)",
974            ),
975            Self::IncompatibleReleaseScopes { .. } => Some(
976                "Only compose ProfileRulePack values that share the same release scope, or where at most one has a release scope set",
977            ),
978            Self::InvalidFieldValue { .. } => Some(
979                "Correct the field value to match the expected format or range for this element",
980            ),
981            Self::UnexpectedDataToken { .. } => Some(
982                "A data element appeared before any segment tag; check for partial writes or encoding corruption",
983            ),
984            Self::DuplicateReference { .. } => Some(
985                "Assign a unique control reference to every UNH (DE 0062) and UNG (DE 0048) within an interchange",
986            ),
987            Self::UnknownDataElement { .. } => Some(
988                "Check the data element identifier against the segment definition; the directory is the source of truth",
989            ),
990            Self::AmbiguousDataElement { .. } => Some(
991                "The code appears at more than one position; address the element positionally instead",
992            ),
993            Self::SegmentLayoutMismatch { .. } => {
994                Some("Resolve codes against the segment definition whose tag matches the segment")
995            }
996            Self::LimitExceeded { .. } => {
997                Some("Raise the corresponding ReaderConfig limit, or reject the input as oversized")
998            }
999            Self::RepetitionSeparatorNotDeclared => Some(
1000                "Build the writer with Writer::with_una and a ServiceStringAdvice whose repetition_sep is set",
1001            ),
1002            Self::CharacterNotInRepertoire { .. } => Some(
1003                "Transliterate the value into the declared repertoire, or declare a wider one in UNB S001 (UNOC for Latin-1, UNOY for UTF-8)",
1004            ),
1005            Self::UnsupportedCharset { .. } => Some(
1006                "UNOX and KECA are not supported; ask the partner for UNOC or UNOY, or transcode the interchange before parsing",
1007            ),
1008            Self::NonFiniteNumber { .. } => Some(
1009                "EDIFACT has no representation for NaN or infinity; check the calculation, or omit the element",
1010            ),
1011            Self::CharacterRepertoireMismatch { .. } => Some(
1012                "Pass the writer's own syntax identifier to begin_interchange, or bind the writer to the repertoire the header declares",
1013            ),
1014            Self::EmptyInterchange { .. } => Some(
1015                "An interchange must carry at least one message or group; send nothing rather than an empty envelope",
1016            ),
1017            Self::EmptyMessage { .. } => Some(
1018                "A message needs at least one segment between UNH and UNT; omit the message entirely if it has no content",
1019            ),
1020            Self::PackageNotSupported { .. } => Some(
1021                "Split the object out of the byte stream using the length in UNO S022 DE 0810, then parse the remaining segments",
1022            ),
1023            Self::BlankDataElementValue { .. } => Some(
1024                "Omit the data element instead of sending spaces; trailing spaces are insignificant and must be suppressed",
1025            ),
1026            Self::SegmentWithoutDataElements { .. } => {
1027                Some("Supply the segment's data, or omit the segment entirely if it is conditional")
1028            }
1029            Self::TooManyRepetitions { .. } => Some(
1030                "Reduce the occurrences to the definition's maximum, or correct the definition if the directory allows more",
1031            ),
1032            Self::InvalidCharacterType { .. } => Some(
1033                "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",
1034            ),
1035            Self::DataElementTooLong { .. } => Some(
1036                "Shorten the value to the declared maximum; length is counted in characters, and a numeric value excludes its sign, decimal mark and exponent",
1037            ),
1038            Self::DataElementTooShort { .. } => Some(
1039                "Pad the value to the declared fixed length, or correct the definition if the directory declares it variable",
1040            ),
1041            Self::TrailingSeparator { .. } => Some(
1042                "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",
1043            ),
1044            Self::GroupsAndMessagesMixed { .. } => Some(
1045                "Put every message inside a group, or none of them; ISO 9735-1 §7.1 does not allow both in one interchange",
1046            ),
1047            Self::InsignificantCharacters { .. } => Some(
1048                "Suppress the insignificant characters before sending: leading zeroes in a variable-length numeric value, trailing spaces in a variable-length text one",
1049            ),
1050            Self::UnrecognisedSyntaxIdentifier(_) => Some(
1051                "UNB S001 DE 0001 must name a defined repertoire: UNOA-UNOK, UNOX, UNOY, or KECA",
1052            ),
1053            Self::ValidationErrors { .. }
1054            | Self::MessageCountMismatch { .. }
1055            | Self::SegmentCountMismatch { .. }
1056            | Self::InterchangeTooLarge { .. }
1057            | Self::InvalidUtf8
1058            | Self::Io(_) => None,
1059        }
1060    }
1061}
1062
1063#[cfg(feature = "diagnostics")]
1064#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))]
1065impl miette::Diagnostic for EdifactError {
1066    fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
1067        Some(Box::new(self.stable_code()))
1068    }
1069
1070    /// Mirrors the severity the validation pipeline assigns.
1071    ///
1072    /// The two must agree: a `miette` render that calls a control-reference
1073    /// mismatch a warning while [`ValidationReport`] files it as an error tells
1074    /// the operator and the program two different things about the same
1075    /// interchange. [`crate::ValidationReport`] is the source of truth, and this
1076    /// delegates to it.
1077    fn severity(&self) -> Option<miette::Severity> {
1078        Some(match crate::report::severity_for_error(self) {
1079            crate::ValidationSeverity::Info => miette::Severity::Advice,
1080            crate::ValidationSeverity::Warning => miette::Severity::Warning,
1081            crate::ValidationSeverity::Error | crate::ValidationSeverity::Critical => {
1082                miette::Severity::Error
1083            }
1084        })
1085    }
1086
1087    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
1088        match self {
1089            // Static text — no allocation needed.
1090            Self::InvalidUna => Some(Box::new(
1091                "UNA segment must be exactly 9 bytes: 'UNA' + 6 service characters. See EDIFACT spec",
1092            )),
1093            Self::InvalidUtf8 => Some(Box::new(
1094                "Internal error: serialized output contains invalid UTF-8. Please report this as a bug",
1095            )),
1096            // Dynamic help text.
1097            Self::UnexpectedEof { offset } => Some(Box::new(format!(
1098                "Check that all segments are terminated with the segment terminator (usually '). \
1099                 Reached end at offset {offset}",
1100            ))),
1101            Self::InvalidDelimiter { byte, offset } => Some(Box::new(format!(
1102                "The byte 0x{byte:02X} at offset {offset} is not a valid delimiter. \
1103                 Check UNA configuration",
1104            ))),
1105            Self::InvalidText { offset } => Some(Box::new(format!(
1106                "The byte sequence at offset {offset} contains invalid UTF-8. \
1107                 Ensure input is valid UTF-8",
1108            ))),
1109            Self::InvalidReleaseSequence { offset } => Some(Box::new(format!(
1110                "Release character at offset {offset} is dangling. \
1111                 Ensure '?' is followed by an escaped byte",
1112            ))),
1113            Self::MessageCountMismatch { expected, actual } => Some(Box::new(format!(
1114                "UNZ declares {expected} message(s) but {actual} UNH/UNT pair(s) were found. \
1115                 Check the UNZ message count",
1116            ))),
1117            Self::SegmentCountMismatch {
1118                expected,
1119                actual,
1120                message_ref,
1121                ..
1122            } => Some(Box::new(format!(
1123                "UNT for message {message_ref} declares {expected} segment(s) but {actual} were found. \
1124                 Check the UNT segment count",
1125            ))),
1126            Self::InvalidSegmentTag(tag) => Some(Box::new(format!(
1127                "Segment tag '{tag}' must be exactly 3 ASCII uppercase letters",
1128            ))),
1129            Self::MissingRequiredElement { tag, element_index } => Some(Box::new(format!(
1130                "Segment {tag} requires element at index {element_index}",
1131            ))),
1132            Self::MissingRequiredComponent {
1133                tag,
1134                element_index,
1135                component_index,
1136            } => Some(Box::new(format!(
1137                "Segment {tag} element {element_index} requires component at index {component_index}",
1138            ))),
1139            Self::Io(e) => Some(Box::new(format!("I/O error: {e}"))),
1140            Self::InvalidSegmentForMessage {
1141                tag, message_type, ..
1142            } => Some(Box::new(format!(
1143                "Segment {tag} should not appear in a {message_type} message. \
1144                 Check the directory definition",
1145            ))),
1146            Self::InvalidElementCount {
1147                tag,
1148                min,
1149                max,
1150                actual,
1151                ..
1152            } => Some(Box::new(format!(
1153                "Segment {tag} should have between {min} and {max} elements, but has {actual}. \
1154                 Check segment structure",
1155            ))),
1156            Self::InvalidComponentCount {
1157                tag,
1158                element_index,
1159                expected,
1160                actual,
1161                ..
1162            } => Some(Box::new(format!(
1163                "In segment {tag}, element {element_index} should have {expected} components \
1164                     but has {actual}. Check element structure",
1165            ))),
1166            Self::InvalidCodeValue {
1167                tag,
1168                element_index,
1169                value,
1170                code_list,
1171                ..
1172            } => Some(Box::new(format!(
1173                "Value '{value}' in segment {tag} element {element_index} is not in the \
1174                     {code_list} code list. Check the directory for valid codes",
1175            ))),
1176            Self::MissingSegment {
1177                tag,
1178                expected_position,
1179            } => Some(Box::new(format!(
1180                "Segment {tag} is required at position {expected_position} but is missing. \
1181                 Add this segment to the message",
1182            ))),
1183            Self::QualifierMismatch {
1184                tag,
1185                actual,
1186                expected,
1187                ..
1188            } => Some(Box::new(format!(
1189                "Segment {tag} has qualifier '{actual}' but expected '{expected}'. \
1190                 Check the segment's first component",
1191            ))),
1192            Self::ConditionalRequirementNotMet {
1193                tag,
1194                element_index,
1195                condition,
1196                ..
1197            } => Some(Box::new(format!(
1198                "In segment {tag}, element {element_index} is conditionally required when: \
1199                     {condition}. Check if the condition is met",
1200            ))),
1201            Self::SegmentTooLong { offset, limit } => Some(Box::new(format!(
1202                "Segment starting at byte offset {offset} exceeds the {limit}-byte limit. \
1203                 Use ReaderConfig::max_segment_bytes to adjust the limit if needed, \
1204                 or verify the input for a missing segment terminator",
1205            ))),
1206            Self::InterchangeTooLarge { count } => Some(Box::new(format!(
1207                "Interchange contains {count} items which exceeds the u32::MAX limit. \
1208                 This is an extremely unusual input; verify the message is not corrupted.",
1209            ))),
1210            Self::InvalidEventSequence { message } => Some(Box::new(format!(
1211                "Event sequence violation: {message}. \
1212                 Check that StartSegment is emitted before Element, and Element before ComponentElement.",
1213            ))),
1214            Self::InvalidElementPosition => Some(Box::new(
1215                "Element positions must be >= 1 (one-based). \
1216                 Ensure no OwnedElementRef is constructed with position == 0",
1217            )),
1218            Self::IncompatibleReleaseScopes { current, incoming } => Some(Box::new(format!(
1219                "Release scope {current:?} and {incoming:?} are incompatible. \
1220                 Only compose ProfileRulePack values that share the same release scope, \
1221                 or where at most one carries a release scope",
1222            ))),
1223            Self::InvalidFieldValue {
1224                tag,
1225                element_index,
1226                value,
1227            } => Some(Box::new(format!(
1228                "Segment {tag} element {element_index} has invalid value '{value}'. \
1229                 Check the expected format or range for this field",
1230            ))),
1231            Self::UnexpectedDataToken { offset } => Some(Box::new(format!(
1232                "Data element at offset {offset} appeared before any segment tag. \
1233                 Check for partial writes or encoding corruption",
1234            ))),
1235            Self::ValidationErrors { error_count, .. } => Some(Box::new(format!(
1236                "Validation found {error_count} error(s). Inspect the ValidationReport for details",
1237            ))),
1238            Self::UnrecognisedSyntaxIdentifier(id) => Some(Box::new(format!(
1239                "Syntax identifier '{id}' is not defined in ISO 9735-1. \
1240                 Valid values are UNOA, UNOB, UNOC, UNOD, UNOE, UNOF (or KECA for KEC-A profile)",
1241            ))),
1242            Self::DuplicateReference { tag, reference, .. } => Some(Box::new(format!(
1243                "Reference '{reference}' is used by more than one {tag} in this interchange; \
1244                 each must be unique so receivers can address messages unambiguously",
1245            ))),
1246            Self::UnknownDataElement { tag, data_element } => Some(Box::new(format!(
1247                "Segment {tag} does not define data element {data_element}. \
1248                 Check the identifier against the directory definition for {tag}",
1249            ))),
1250            Self::AmbiguousDataElement { tag, data_element } => Some(Box::new(format!(
1251                "Segment {tag} defines data element {data_element} at more than one position, \
1252                 so code-addressed access cannot pick one; use a positional accessor",
1253            ))),
1254            Self::SegmentLayoutMismatch { expected, actual } => Some(Box::new(format!(
1255                "The supplied layout describes segment {expected} but was applied to {actual}. \
1256                 Look up the definition by the segment's own tag",
1257            ))),
1258            Self::RepetitionSeparatorNotDeclared => Some(Box::new(
1259                "UNA position 7 holds the space \"not used\" sentinel, so repeating data \
1260                 elements cannot be expressed. Use Writer::with_una with a repetition_sep",
1261            )),
1262            Self::CharacterNotInRepertoire {
1263                charset,
1264                character,
1265                offset,
1266            } => Some(Box::new(format!(
1267                "The character {character:?} at offset {offset} has no representation in {charset}. \
1268                 Transliterate it, or declare a wider repertoire in UNB S001 DE 0001",
1269            ))),
1270            Self::UnsupportedCharset { syntax_identifier } => Some(Box::new(format!(
1271                "'{syntax_identifier}' is stateful or multi-byte, so byte-level delimiter scanning \
1272                 would be unsound. Transcode the interchange to UNOC or UNOY before parsing",
1273            ))),
1274            Self::CharacterRepertoireMismatch { declared, writer } => Some(Box::new(format!(
1275                "The UNB declares {declared} but the writer encodes {writer}. \
1276                 The receiver would decode the body with the wrong table",
1277            ))),
1278            Self::NonFiniteNumber { value } => Some(Box::new(format!(
1279                "The value {value} is not finite. EDIFACT numeric data elements have no \
1280                 representation for NaN or infinity",
1281            ))),
1282            Self::LimitExceeded { limit, max } => Some(Box::new(format!(
1283                "The input exceeds the configured {limit} limit of {max}. \
1284                 Raise it via ReaderConfig if the input is legitimate, or reject the input",
1285            ))),
1286            Self::EmptyInterchange { control_ref } => Some(Box::new(format!(
1287                "Interchange {control_ref} carries no message and no group. \
1288                 ISO 9735-1 §7.1 requires at least one; send nothing rather than an empty envelope",
1289            ))),
1290            Self::EmptyMessage { message_ref, .. } => Some(Box::new(format!(
1291                "Message {message_ref} has nothing between UNH and UNT. \
1292                 ISO 9735-1 §7.3 requires at least one additional segment",
1293            ))),
1294            Self::PackageNotSupported { tag, .. } => Some(Box::new(format!(
1295                "{tag} opens or closes a package, whose object is arbitrary binary data \
1296                 rather than EDIFACT. Split it out using the length in UNO S022 DE 0810, \
1297                 then parse the remaining segments",
1298            ))),
1299            Self::BlankDataElementValue {
1300                tag,
1301                element_index,
1302                component_index,
1303                ..
1304            } => Some(Box::new(format!(
1305                "Segment {tag} element {element_index} component {component_index} holds only \
1306                 spaces. ISO 9735-1 §9.3 forbids that — omit the element instead",
1307            ))),
1308            Self::SegmentWithoutDataElements { tag, .. } => Some(Box::new(format!(
1309                "Segment {tag} carries only its tag. ISO 9735-1 §7.5 requires at least one \
1310                 data element; §8.5 says a conditional segment with no data is omitted entirely",
1311            ))),
1312            Self::TooManyRepetitions {
1313                tag,
1314                element_index,
1315                max,
1316                actual,
1317                ..
1318            } => Some(Box::new(format!(
1319                "Segment {tag} element {element_index} occurs {actual} times but its definition \
1320                 allows at most {max}",
1321            ))),
1322            Self::InvalidCharacterType {
1323                repr, value, tag, ..
1324            } => Some(Box::new(format!(
1325                "Segment {tag}: {value:?} is not a valid {repr} value. ISO 9735-1 §10 admits \
1326                 digits, an optional minus sign, a decimal mark and an exponent — the space \
1327                 character and the plus sign are not allowed",
1328            ))),
1329            Self::DataElementTooLong {
1330                repr, actual, tag, ..
1331            } => Some(Box::new(format!(
1332                "Segment {tag}: the value is {actual} characters, but the directory declares \
1333                 {repr}. Length is counted in characters rather than bytes",
1334            ))),
1335            Self::DataElementTooShort {
1336                repr, actual, tag, ..
1337            } => Some(Box::new(format!(
1338                "Segment {tag}: the value is {actual} characters, but the directory declares the \
1339                 fixed length {repr}",
1340            ))),
1341            Self::TrailingSeparator {
1342                tag, element_index, ..
1343            } => Some(Box::new(match element_index {
1344                Some(index) => format!(
1345                    "Segment {tag} element {index} ends in a component separator with no value \
1346                     after it. ISO 9735-1 §8.7.2 requires trailing component separators to be \
1347                     omitted",
1348                ),
1349                None => format!(
1350                    "Segment {tag} ends in a data element separator with no value after it. \
1351                     ISO 9735-1 §8.7.1 requires trailing data element separators to be omitted",
1352                ),
1353            })),
1354            Self::GroupsAndMessagesMixed { .. } => Some(Box::new(
1355                "ISO 9735-1 §7.1 lists what an interchange may contain, and the entries are \
1356                 exclusive: groups containing messages, or bare messages — never both, because a \
1357                 message outside every group cannot be counted in UNZ DE 0036",
1358            )),
1359            Self::InsignificantCharacters { tag, kind, .. } => Some(Box::new(format!(
1360                "Segment {tag}: {kind}. ISO 9735-1 §9.1 requires insignificant characters to be \
1361                 suppressed before transfer",
1362            ))),
1363        }
1364    }
1365}
1366
1367// ── validation report ─────────────────────────────────────────────────────────
1368
1369pub use crate::report::ValidationReport;
1370
1371#[cfg(test)]
1372mod tests {
1373    use super::*;
1374
1375    #[test]
1376    fn recovery_hint_exists_for_common_malformed_cases() {
1377        let err = EdifactError::InvalidReleaseSequence { offset: 10 };
1378        assert!(err.recovery_hint().is_some());
1379
1380        let err = EdifactError::InvalidCodeValue {
1381            tag: "BGM".to_owned(),
1382            element_index: 0,
1383            value: "X".to_owned(),
1384            code_list: "1001".to_owned(),
1385            span: Span::new(0, 9),
1386            suggestion: None,
1387        };
1388        assert!(err.recovery_hint().is_some());
1389    }
1390}