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/// All errors produced by `edifact-rs`.
48///
49/// # Positional data
50///
51/// Two kinds of position information appear in this enum, and the distinction is
52/// deliberate:
53///
54/// * **`offset: usize`** — a single byte position in the input stream. Used by
55/// the lexical variants (`UnexpectedEof`, `InvalidDelimiter`, `InvalidText`,
56/// `InvalidReleaseSequence`, `SegmentTooLong`, `UnexpectedDataToken`), where
57/// the fault is a *point* in the byte stream and no meaningful end position
58/// exists.
59/// * **`span: Span`** — a half-open byte range. Used by every variant produced
60/// while validating an already-parsed [`Segment`][crate::Segment], where the
61/// exact source range is known. A [`ValidationIssue`][crate::ValidationIssue]
62/// built from such an error carries the full range, so `miette` and LSP
63/// tooling can underline the offending segment rather than place a
64/// zero-width caret.
65///
66/// Use `span.start` when only the start position is needed.
67#[derive(Debug, Error, PartialEq)]
68#[non_exhaustive]
69pub enum EdifactError {
70 /// Unexpected end of input while parsing.
71 ///
72 /// This typically occurs when a segment terminator or expected delimiter
73 /// is not found before the end of the input stream.
74 #[error("unexpected end of input at byte offset {offset}")]
75 UnexpectedEof {
76 /// Byte offset where the parser exhausted input.
77 offset: usize,
78 },
79
80 /// Invalid byte encountered in a delimiter context.
81 ///
82 /// Delimiters must be precisely ASCII characters from the UNA service string advice.
83 /// Any other byte is invalid in delimiter position.
84 #[error("invalid delimiter byte 0x{byte:02X} at offset {offset}")]
85 InvalidDelimiter {
86 /// Unexpected delimiter byte.
87 byte: u8,
88 /// Byte offset where the delimiter was observed.
89 offset: usize,
90 },
91
92 /// Invalid UTF-8 sequence in parsed text.
93 ///
94 /// While EDIFACT operates on bytes, segments and elements are expected to contain
95 /// valid UTF-8 text. Non-UTF-8 sequences are rejected at parse time.
96 #[error("invalid EDIFACT text at byte offset {offset}")]
97 InvalidText {
98 /// Byte offset where invalid UTF-8 text starts.
99 offset: usize,
100 },
101
102 /// Invalid release-character escape sequence in parsed text.
103 ///
104 /// The release character (`?` by default) must be followed by one escaped byte.
105 /// A trailing release character without a following byte is malformed.
106 #[error("invalid release sequence at byte offset {offset}: dangling release character")]
107 InvalidReleaseSequence {
108 /// Byte offset of the dangling release character.
109 offset: usize,
110 },
111
112 /// UNZ interchange message count does not match the number of UNH/UNT pairs found.
113 ///
114 /// The `UNZ` segment declares the number of messages in the interchange,
115 /// but the actual number of `UNH`/`UNT` pairs observed differs.
116 #[error("interchange message count mismatch: UNZ declared {expected}, found {actual}")]
117 MessageCountMismatch {
118 /// Message count declared in the UNZ segment.
119 expected: u32,
120 /// Actual number of UNH/UNT pairs observed.
121 actual: u32,
122 },
123
124 /// UNT segment count does not match the actual number of segments in the message.
125 ///
126 /// The `UNT` segment declares the number of segments in the message (including `UNH`/`UNT`),
127 /// but the actual count differs.
128 #[error(
129 "segment count mismatch in message {message_ref}: UNT declared {expected}, found {actual}"
130 )]
131 SegmentCountMismatch {
132 /// Segment count declared in the UNT segment.
133 expected: u32,
134 /// Actual number of segments observed.
135 actual: u32,
136 /// Message reference from the UNH segment.
137 message_ref: String,
138 },
139
140 /// Invalid or malformed segment tag.
141 ///
142 /// Segment tags must be exactly 3 ASCII uppercase letters.
143 #[error("invalid segment tag {0:?}")]
144 InvalidSegmentTag(String),
145
146 /// Invalid UNA service string advice.
147 ///
148 /// If present, the UNA segment must be exactly 9 bytes: `"UNA"` followed by
149 /// 6 service characters. The five active characters (`element_sep`,
150 /// `component_sep`, `decimal_mark`, `release_char`, and `segment_term`) must
151 /// all be mutually distinct and printable, non-alphanumeric ASCII. The
152 /// **repetition separator** (UNA byte 7) is validated on the same terms
153 /// unless it is a space, the conventional "not used" sentinel.
154 #[error("invalid UNA service string advice")]
155 InvalidUna,
156
157 /// Missing required element in a segment.
158 ///
159 /// Certain segments require specific elements to be present. This error indicates
160 /// a mandatory element was not found.
161 #[error("missing required element {element_index} in segment {tag}")]
162 MissingRequiredElement {
163 /// Segment tag containing the missing element.
164 tag: String,
165 /// Zero-based required element index.
166 element_index: usize,
167 },
168
169 /// Missing required component in a composite element.
170 ///
171 /// The element is present, but the required component at the given index is absent or empty.
172 #[error(
173 "missing required component {component_index} in element {element_index} of segment {tag}"
174 )]
175 MissingRequiredComponent {
176 /// Segment tag containing the composite element.
177 tag: String,
178 /// Zero-based element index of the composite.
179 element_index: usize,
180 /// Zero-based component index that was absent.
181 component_index: usize,
182 },
183
184 /// Output serialization produced invalid UTF-8.
185 ///
186 /// This is an internal consistency error; the writer should never produce non-UTF-8 output.
187 /// If this occurs, it indicates a bug in the serialization logic.
188 #[error("serialized output contains invalid UTF-8")]
189 InvalidUtf8,
190
191 /// I/O error from reading or writing.
192 #[error(transparent)]
193 Io(#[from] IoError),
194
195 // ── validation variants (E010–E020) ────────────────────────────────────
196 /// Segment is not valid for the current message type.
197 ///
198 /// Structural validation found a segment that should not appear in this message.
199 #[error("segment {tag} is not valid for message type {message_type}")]
200 InvalidSegmentForMessage {
201 /// Segment tag that is not allowed for the message type.
202 tag: String,
203 /// Message type used for structural validation.
204 message_type: String,
205 /// Byte range of the offending segment tag.
206 span: Span,
207 },
208
209 /// Element count in segment exceeds or falls short of directory definition.
210 ///
211 /// Validation against directory metadata found an element count mismatch.
212 #[error("segment {tag} has {actual} elements, expected between {min} and {max}")]
213 InvalidElementCount {
214 /// Segment tag with wrong arity.
215 tag: String,
216 /// Minimum allowed element count.
217 min: usize,
218 /// Maximum allowed element count.
219 max: usize,
220 /// Actual element count found.
221 actual: usize,
222 /// Byte range of the offending segment.
223 span: Span,
224 },
225
226 /// Component count in a composite element is invalid.
227 ///
228 /// A composite data element does not have the expected number of components.
229 #[error("segment {tag} element {element_index} has {actual} components, expected {expected}")]
230 InvalidComponentCount {
231 /// Segment tag containing the composite.
232 tag: String,
233 /// Zero-based element index of the composite.
234 element_index: usize,
235 /// Expected component count.
236 expected: u8,
237 /// Actual component count found.
238 actual: u8,
239 /// Byte range of the offending composite element.
240 span: Span,
241 },
242
243 /// Code-list value is not valid.
244 ///
245 /// The value appears in a field that should contain a code from a specific code list,
246 /// but the value is not in that code list.
247 #[error(
248 "segment {tag} element {element_index}: '{value}' is not a valid code (code list {code_list})"
249 )]
250 InvalidCodeValue {
251 /// Segment tag containing the invalid value.
252 tag: String,
253 /// Zero-based element index containing the invalid code.
254 element_index: usize,
255 /// Invalid code value observed.
256 value: String,
257 /// Data element code list identifier.
258 code_list: String,
259 /// Byte range of the offending value.
260 span: Span,
261 /// Optional remediation suggestion from the code-list lookup function.
262 suggestion: Option<&'static str>,
263 },
264
265 /// A required segment is missing from the message.
266 ///
267 /// Structural validation found that a mandatory segment is absent.
268 #[error("required segment {tag} is missing from message (position {expected_position})")]
269 MissingSegment {
270 /// Missing segment tag.
271 tag: String,
272 /// Human-readable position hint.
273 expected_position: String,
274 },
275
276 /// Qualifier does not match expected value for segment.
277 ///
278 /// A qualified segment (e.g., NAD+MS) has a qualifier that does not match expected.
279 #[error("segment {tag} has qualifier '{actual}', expected '{expected}'")]
280 QualifierMismatch {
281 /// Segment tag whose qualifier mismatched.
282 tag: String,
283 /// Actual qualifier found.
284 actual: String,
285 /// Expected qualifier value.
286 expected: String,
287 /// Byte range of the offending segment.
288 span: Span,
289 },
290
291 /// Conditional requirement not met.
292 ///
293 /// A segment or element is conditionally required based on another element's value,
294 /// but the condition was not satisfied.
295 #[error("segment {tag} element {element_index}: conditional requirement not met ({condition})")]
296 ConditionalRequirementNotMet {
297 /// Segment tag that violated a conditional rule.
298 tag: String,
299 /// Zero-based element index governed by the condition.
300 element_index: usize,
301 /// Condition text describing the rule.
302 condition: String,
303 /// Byte range of the offending segment.
304 span: Span,
305 },
306
307 /// Validation failed and the full [`ValidationReport`] is preserved.
308 ///
309 /// Returned by validation helpers when errors are found. Provides programmatic
310 /// access to all issues, warnings, and infos.
311 ///
312 /// # Example
313 ///
314 /// ```rust,ignore
315 /// match my_fn() {
316 /// Err(EdifactError::ValidationErrors { report, .. }) => {
317 /// for issue in report.errors() {
318 /// eprintln!("{}", issue);
319 /// }
320 /// }
321 /// other => { /* ... */ }
322 /// }
323 /// ```
324 #[error("validation failed with {error_count} error(s)")]
325 ValidationErrors {
326 /// Number of error-severity issues in the report.
327 error_count: usize,
328 /// Full report with all errors, warnings, and infos.
329 report: Box<ValidationReport>,
330 },
331
332 /// Segment exceeded the configured maximum byte length.
333 ///
334 /// Returned by reader-based parsers when an unterminated segment accumulates more
335 /// bytes than the configured `max_segment_bytes` limit in [`ReaderConfig`]. This
336 /// prevents resource exhaustion on adversarially crafted or truncated input that
337 /// never emits a segment terminator.
338 ///
339 /// [`ReaderConfig`]: crate::ReaderConfig
340 #[error("segment starting at byte offset {offset} exceeded maximum length of {limit} bytes")]
341 SegmentTooLong {
342 /// Byte offset where the overlong segment started.
343 offset: usize,
344 /// Configured maximum segment byte length.
345 limit: usize,
346 },
347
348 /// No handler was registered in [`crate::MessageDispatch`] for this message type.
349 ///
350 /// Returned by [`crate::MessageDispatch::dispatch`] when the message-type
351 /// extracted from the `UNH` segment does not match any registered handler
352 /// and no fallback was configured.
353 #[error("no handler registered for message type {message_type}")]
354 UnexpectedMessageType {
355 /// The unhandled message type string from the `UNH` segment.
356 message_type: String,
357 },
358
359 /// An interchange or message contains more segments or messages than can be
360 /// represented in a `u32` counter (> 4 294 967 295).
361 ///
362 /// This is effectively unreachable in practice — no real-world EDIFACT
363 /// interchange has billions of segments — but the parser returns this error
364 /// rather than silently saturating or wrapping the counter.
365 #[error("interchange too large: count {count} exceeds u32::MAX")]
366 InterchangeTooLarge {
367 /// The count that could not be represented as `u32`.
368 count: u64,
369 },
370
371 /// An [`crate::EventEmitter`] received events in an invalid sequence.
372 ///
373 /// This indicates a programming error in the caller's serialization code:
374 /// for example, emitting an [`crate::EdifactEvent::Element`] without a prior
375 /// [`crate::EdifactEvent::StartSegment`], or emitting
376 /// [`crate::EdifactEvent::ComponentElement`] without a preceding
377 /// [`crate::EdifactEvent::Element`].
378 #[error("invalid event sequence: {message}")]
379 InvalidEventSequence {
380 /// Description of the protocol violation.
381 message: &'static str,
382 },
383
384 /// An [`crate::OwnedElementRef`] has `position = 0`, which is never valid.
385 ///
386 /// Element positions are one-based: position 1 refers to the first element
387 /// slot. Position 0 is reserved and invalid. Use [`crate::OwnedElementRef::try_new`]
388 /// to get a `Result` instead of a panic.
389 #[error("element definition contains invalid position 0; positions must be >= 1 (one-based)")]
390 InvalidElementPosition,
391
392 /// Two [`crate::ProfileRulePack`] values with incompatible release scopes were composed.
393 ///
394 /// When composing packs via [`crate::ProfileRulePack::extend_from`] or
395 /// [`crate::ProfileRulePack::merge_with_override`], both packs must either
396 /// share the same release scope or at most one may carry a scope.
397 #[error("incompatible release scopes: cannot compose {current:?} with {incoming:?}")]
398 IncompatibleReleaseScopes {
399 /// Release scope of the pack being composed into.
400 current: String,
401 /// Release scope of the pack being composed in.
402 incoming: String,
403 },
404
405 /// A field value failed semantic validation (e.g. wrong format, out-of-range).
406 ///
407 /// Distinct from [`InvalidCodeValue`][Self::InvalidCodeValue] which is for
408 /// code-list membership checks. Use this variant when a free-text or numeric
409 /// field contains a value that is structurally invalid for its purpose.
410 #[error("segment {tag} element {element_index}: invalid field value {value:?}")]
411 InvalidFieldValue {
412 /// Segment tag that contains the invalid field.
413 tag: String,
414 /// Zero-based element index of the invalid field.
415 element_index: usize,
416 /// The invalid value that was observed.
417 value: String,
418 },
419
420 /// A data or component element token appeared before the first segment tag.
421 ///
422 /// EDIFACT syntax requires that every data element follows a segment tag.
423 /// A data element token encountered before any tag (e.g. after a stray
424 /// separator at the start of the stream) is a protocol violation.
425 ///
426 /// Unlike stray segment terminators (which are tolerated as blank lines),
427 /// stray data tokens indicate encoding corruption or a partial write.
428 #[error("unexpected data token at byte offset {offset}: data element before segment tag")]
429 UnexpectedDataToken {
430 /// Byte offset of the stray token.
431 offset: usize,
432 },
433
434 /// The interchange syntax identifier (UNB DE 0001) is not a recognised ISO 9735-1 value.
435 ///
436 /// Valid syntax identifiers are: `UNOA`, `UNOB`, `UNOC`, `UNOD`, `UNOE`, `UNOF`, and
437 /// `KECA` (Korean EDI Centre A). Any other value indicates a non-standard generator
438 /// or a corrupted UNB header.
439 #[error(
440 "unrecognised syntax identifier '{0}': expected UNOA/UNOB/UNOC/UNOD/UNOE/UNOF (or KECA)"
441 )]
442 UnrecognisedSyntaxIdentifier(String),
443
444 /// A control reference was reused within the scope that requires it to be unique.
445 ///
446 /// ISO 9735-1 requires the message reference number (`UNH` DE 0062) to be
447 /// unique within an interchange, and the group reference number (`UNG`
448 /// DE 0048) to be unique within an interchange. Duplicates make a message
449 /// unaddressable: a receiver keying on the reference silently processes one
450 /// occurrence and drops the rest.
451 #[error("duplicate {tag} reference '{reference}' at bytes {span}")]
452 DuplicateReference {
453 /// Segment tag that carries the duplicated reference (`UNH` or `UNG`).
454 tag: String,
455 /// The reference value that appeared more than once.
456 reference: String,
457 /// Byte range of the duplicate occurrence.
458 span: Span,
459 },
460
461 /// A UN/EDIFACT data element code was not found in the segment definition.
462 ///
463 /// Produced by the code-addressed accessors
464 /// ([`Segment::value_by_code`][crate::Segment::value_by_code] and friends)
465 /// when the requested data element identifier does not appear anywhere in
466 /// the supplied [`SegmentLayout`][crate::SegmentLayout]. This is the error
467 /// that turns a mistyped or stale DE reference into a loud failure instead
468 /// of a silent off-by-one read of the wrong element.
469 #[error("segment {tag} has no data element {data_element} in its definition")]
470 UnknownDataElement {
471 /// Segment tag whose definition was searched.
472 tag: String,
473 /// The data element identifier that was not found.
474 data_element: String,
475 },
476
477 /// A UN/EDIFACT data element code appears more than once in a segment definition.
478 ///
479 /// Code-addressed access requires an unambiguous target. When a directory
480 /// genuinely repeats a code (e.g. the same DE used at two positions), address
481 /// it positionally with [`Segment::element_str`][crate::Segment::element_str]
482 /// or split the definition.
483 #[error("segment {tag} defines data element {data_element} at more than one position")]
484 AmbiguousDataElement {
485 /// Segment tag whose definition was searched.
486 tag: String,
487 /// The data element identifier that resolved to multiple positions.
488 data_element: String,
489 },
490
491 /// A configured [`ReaderConfig`][crate::ReaderConfig] resource limit was exceeded.
492 ///
493 /// Raised by the parsing iterators when the input carries more segments,
494 /// messages, or bytes than the caller allowed. The limit is reported rather
495 /// than silently applied: a budget that ends the iterator without an error is
496 /// indistinguishable from a clean end of input, so the caller would accept a
497 /// **truncated** interchange as complete.
498 ///
499 /// [`SegmentTooLong`][Self::SegmentTooLong] covers the per-segment size
500 /// guard; this variant covers the whole-input budgets.
501 #[error("input exceeded the configured {limit} limit of {max}")]
502 LimitExceeded {
503 /// Name of the limit that tripped: `"max_segments"`, `"max_messages"`,
504 /// or `"max_input_bytes"`.
505 limit: &'static str,
506 /// The configured ceiling.
507 max: u64,
508 },
509
510 /// A repeating data element was written under a service string advice that
511 /// declares no repetition separator.
512 ///
513 /// ISO 9735-4 §3.1 repetitions can only be expressed when `UNA` position 7
514 /// carries a real separator. With the space "not used" sentinel there is no
515 /// byte to write between occurrences, and joining them anyway would emit
516 /// output that reads back as a single occurrence — silent data corruption.
517 /// Construct the writer with [`Writer::with_una`][crate::Writer::with_una]
518 /// and a service string advice whose `repetition_sep` is set.
519 #[error(
520 "cannot write a repeating data element: the active service string advice declares no repetition separator"
521 )]
522 RepetitionSeparatorNotDeclared,
523
524 /// A non-finite float was handed to a numeric serializer.
525 ///
526 /// EDIFACT numeric data elements are decimal digit strings with an optional
527 /// sign and decimal mark (ISO 9735-1 §7). There is no representation for
528 /// `NaN` or infinity, and `Display` would emit `NaN` / `inf` — text that no
529 /// receiver can parse and that the crate's own reader would reject.
530 #[error("non-finite number {value} has no EDIFACT representation")]
531 NonFiniteNumber {
532 /// The offending value, formatted for the message.
533 value: String,
534 },
535
536 /// A character cannot be represented in the interchange's declared repertoire.
537 ///
538 /// The `UNB` S001 DE 0001 syntax identifier names the character repertoire the
539 /// payload is written in (`UNOA`, `UNOC`, …). Writing a character outside it
540 /// produces bytes the receiver decodes as something else — or as nothing at
541 /// all — so the writer refuses instead.
542 ///
543 /// Also raised by [`Charset::encode`][crate::Charset::encode].
544 #[error(
545 "character {character:?} at offset {offset} is not in the {charset} character repertoire"
546 )]
547 CharacterNotInRepertoire {
548 /// The syntax identifier of the repertoire that rejected the character.
549 charset: &'static str,
550 /// The offending character.
551 character: char,
552 /// Byte offset of the character within the value it appeared in.
553 offset: usize,
554 },
555
556 /// A `UNB` was written declaring a repertoire other than the writer's own.
557 ///
558 /// A header that names `UNOA` while the body goes out as ISO 8859-1 is
559 /// unreadable at the far end in exactly the way that is hardest to diagnose,
560 /// so [`Writer::begin_interchange`][crate::Writer::begin_interchange] refuses
561 /// the combination rather than emitting it.
562 #[error("UNB declares repertoire {declared}, but the writer encodes {writer}")]
563 CharacterRepertoireMismatch {
564 /// Syntax identifier passed to `begin_interchange`.
565 declared: String,
566 /// Repertoire the writer was bound to with `Writer::with_charset`.
567 writer: &'static str,
568 },
569
570 /// The interchange declares a character repertoire this crate cannot decode.
571 ///
572 /// `UNOX` (ISO 2022 code extension) and `KECA` (Korean) are stateful or
573 /// multi-byte in ways that break the byte-level delimiter scanning every
574 /// other repertoire allows. They are reported rather than silently
575 /// mis-decoded.
576 #[error("character repertoire '{syntax_identifier}' is not supported")]
577 UnsupportedCharset {
578 /// The `UNB` S001 DE 0001 value that could not be handled.
579 syntax_identifier: String,
580 },
581
582 /// A [`SegmentLayout`][crate::SegmentLayout] was applied to a segment with a different tag.
583 ///
584 /// Passing the `NAD` definition to a `DTM` segment would resolve codes
585 /// against the wrong table, which is exactly the class of mistake that
586 /// code-addressed access exists to prevent — so it is rejected up front.
587 #[error("segment layout is for {expected}, but the segment is {actual}")]
588 SegmentLayoutMismatch {
589 /// Tag the layout describes.
590 expected: String,
591 /// Tag of the segment the layout was applied to.
592 actual: String,
593 },
594}
595
596impl From<std::io::Error> for EdifactError {
597 fn from(e: std::io::Error) -> Self {
598 Self::Io(IoError(e))
599 }
600}
601
602impl EdifactError {
603 /// Stable diagnostic code for this error variant.
604 #[must_use]
605 pub const fn stable_code(&self) -> &'static str {
606 match self {
607 Self::UnexpectedEof { .. } => "E001",
608 Self::InvalidDelimiter { .. } => "E002",
609 Self::InvalidText { .. } => "E003",
610 Self::MessageCountMismatch { .. } => "E004",
611 Self::SegmentCountMismatch { .. } => "E005",
612 Self::InvalidSegmentTag(_) => "E006",
613 Self::InvalidUna => "E007",
614 Self::MissingRequiredElement { .. } => "E008",
615 Self::InvalidUtf8 => "E009",
616 Self::Io(_) => "E010",
617 Self::InvalidSegmentForMessage { .. } => "E011",
618 Self::InvalidElementCount { .. } => "E012",
619 Self::InvalidComponentCount { .. } => "E013",
620 Self::InvalidCodeValue { .. } => "E014",
621 Self::MissingSegment { .. } => "E015",
622 Self::QualifierMismatch { .. } => "E016",
623 Self::ConditionalRequirementNotMet { .. } => "E017",
624 // E018 is permanently retired (was ValidationFailed, removed in 0.8.0)
625 Self::InvalidReleaseSequence { .. } => "E019",
626 Self::SegmentTooLong { .. } => "E020",
627 Self::MissingRequiredComponent { .. } => "E021",
628 Self::UnexpectedMessageType { .. } => "E022",
629 Self::InterchangeTooLarge { .. } => "E023",
630 Self::InvalidEventSequence { .. } => "E024",
631 Self::InvalidElementPosition => "E025",
632 Self::IncompatibleReleaseScopes { .. } => "E026",
633 Self::InvalidFieldValue { .. } => "E027",
634 Self::UnexpectedDataToken { .. } => "E028",
635 // E029 is permanently retired (was FunctionalGroupNotSupported, removed when
636 // full UNG/UNE support was added — functional groups are now parsed natively)
637 Self::ValidationErrors { .. } => "E030",
638 Self::UnrecognisedSyntaxIdentifier(_) => "E031",
639 Self::DuplicateReference { .. } => "E032",
640 Self::UnknownDataElement { .. } => "E033",
641 Self::AmbiguousDataElement { .. } => "E034",
642 Self::SegmentLayoutMismatch { .. } => "E035",
643 Self::LimitExceeded { .. } => "E036",
644 Self::RepetitionSeparatorNotDeclared => "E037",
645 Self::CharacterNotInRepertoire { .. } => "E038",
646 Self::UnsupportedCharset { .. } => "E039",
647 Self::NonFiniteNumber { .. } => "E040",
648 Self::CharacterRepertoireMismatch { .. } => "E041",
649 }
650 }
651
652 /// Stable recovery hint for common malformed input and validation cases.
653 #[must_use]
654 pub fn recovery_hint(&self) -> Option<&'static str> {
655 match self {
656 Self::UnexpectedEof { .. } => {
657 Some("Ensure every segment ends with the configured segment terminator")
658 }
659 Self::InvalidDelimiter { .. } => {
660 Some("Check UNA service string advice and delimiter bytes in the payload")
661 }
662 Self::InvalidText { .. } => {
663 Some("Input must be valid UTF-8 text for segment and element values")
664 }
665 Self::InvalidReleaseSequence { .. } => {
666 Some("Release character must escape one following byte; trailing '?' is invalid")
667 }
668 Self::InvalidSegmentTag(_) => Some("Segment tags must be 3 ASCII uppercase letters"),
669 Self::InvalidUna => Some(
670 "UNA must be exactly 9 bytes: 'UNA' followed by 6 distinct, non-whitespace service characters",
671 ),
672 Self::MissingRequiredElement { .. } => {
673 Some("Provide all mandatory elements for the segment per directory rules")
674 }
675 Self::MissingRequiredComponent { .. } => Some(
676 "Provide all mandatory components for the composite element per directory rules",
677 ),
678 Self::InvalidSegmentForMessage { .. } => {
679 Some("Remove unsupported segment or switch to the correct message type")
680 }
681 Self::InvalidElementCount { .. } => {
682 Some("Adjust the segment element count to the allowed min/max range")
683 }
684 Self::InvalidComponentCount { .. } => {
685 Some("Fix composite element arity to match the expected component count")
686 }
687 Self::InvalidCodeValue { .. } => {
688 Some("Use a value from the referenced code list for this element")
689 }
690 Self::MissingSegment { .. } => {
691 Some("Insert the required segment at the expected position")
692 }
693 Self::QualifierMismatch { .. } => {
694 Some("Set the segment qualifier to the expected value")
695 }
696 Self::ConditionalRequirementNotMet { .. } => {
697 Some("When the condition is met, include the conditionally required element")
698 }
699 Self::SegmentTooLong { limit, .. } => {
700 let _ = limit; // used in the error message; hint is generic
701 Some("Increase max_segment_bytes in ReaderConfig or reject the input as malformed")
702 }
703 Self::InvalidEventSequence { .. } => {
704 Some("Emit StartSegment before Element, and Element before ComponentElement")
705 }
706 Self::InvalidElementPosition => Some(
707 "Set element position to a value >= 1; positions are one-based (1 = first element slot)",
708 ),
709 Self::IncompatibleReleaseScopes { .. } => Some(
710 "Only compose ProfileRulePack values that share the same release scope, or where at most one has a release scope set",
711 ),
712 Self::InvalidFieldValue { .. } => Some(
713 "Correct the field value to match the expected format or range for this element",
714 ),
715 Self::UnexpectedDataToken { .. } => Some(
716 "A data element appeared before any segment tag; check for partial writes or encoding corruption",
717 ),
718 Self::DuplicateReference { .. } => Some(
719 "Assign a unique control reference to every UNH (DE 0062) and UNG (DE 0048) within an interchange",
720 ),
721 Self::UnknownDataElement { .. } => Some(
722 "Check the data element identifier against the segment definition; the directory is the source of truth",
723 ),
724 Self::AmbiguousDataElement { .. } => Some(
725 "The code appears at more than one position; address the element positionally instead",
726 ),
727 Self::SegmentLayoutMismatch { .. } => {
728 Some("Resolve codes against the segment definition whose tag matches the segment")
729 }
730 Self::LimitExceeded { .. } => {
731 Some("Raise the corresponding ReaderConfig limit, or reject the input as oversized")
732 }
733 Self::RepetitionSeparatorNotDeclared => Some(
734 "Build the writer with Writer::with_una and a ServiceStringAdvice whose repetition_sep is set",
735 ),
736 Self::CharacterNotInRepertoire { .. } => Some(
737 "Transliterate the value into the declared repertoire, or declare a wider one in UNB S001 (UNOC for Latin-1, UNOY for UTF-8)",
738 ),
739 Self::UnsupportedCharset { .. } => Some(
740 "UNOX and KECA are not supported; ask the partner for UNOC or UNOY, or transcode the interchange before parsing",
741 ),
742 Self::NonFiniteNumber { .. } => Some(
743 "EDIFACT has no representation for NaN or infinity; check the calculation, or omit the element",
744 ),
745 Self::CharacterRepertoireMismatch { .. } => Some(
746 "Pass the writer's own syntax identifier to begin_interchange, or bind the writer to the repertoire the header declares",
747 ),
748 Self::ValidationErrors { .. }
749 | Self::MessageCountMismatch { .. }
750 | Self::SegmentCountMismatch { .. }
751 | Self::UnexpectedMessageType { .. }
752 | Self::InterchangeTooLarge { .. }
753 | Self::UnrecognisedSyntaxIdentifier(_)
754 | Self::InvalidUtf8
755 | Self::Io(_) => None,
756 }
757 }
758}
759
760#[cfg(feature = "diagnostics")]
761#[cfg_attr(docsrs, doc(cfg(feature = "diagnostics")))]
762impl miette::Diagnostic for EdifactError {
763 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
764 Some(Box::new(self.stable_code()))
765 }
766
767 fn severity(&self) -> Option<miette::Severity> {
768 match self {
769 Self::InvalidCodeValue { .. }
770 | Self::InvalidComponentCount { .. }
771 | Self::QualifierMismatch { .. } => Some(miette::Severity::Warning),
772 _ => Some(miette::Severity::Error),
773 }
774 }
775
776 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
777 match self {
778 // Static text — no allocation needed.
779 Self::InvalidUna => Some(Box::new(
780 "UNA segment must be exactly 9 bytes: 'UNA' + 6 service characters. See EDIFACT spec",
781 )),
782 Self::InvalidUtf8 => Some(Box::new(
783 "Internal error: serialized output contains invalid UTF-8. Please report this as a bug",
784 )),
785 // Dynamic help text.
786 Self::UnexpectedEof { offset } => Some(Box::new(format!(
787 "Check that all segments are terminated with the segment terminator (usually '). \
788 Reached end at offset {offset}",
789 ))),
790 Self::InvalidDelimiter { byte, offset } => Some(Box::new(format!(
791 "The byte 0x{byte:02X} at offset {offset} is not a valid delimiter. \
792 Check UNA configuration",
793 ))),
794 Self::InvalidText { offset } => Some(Box::new(format!(
795 "The byte sequence at offset {offset} contains invalid UTF-8. \
796 Ensure input is valid UTF-8",
797 ))),
798 Self::InvalidReleaseSequence { offset } => Some(Box::new(format!(
799 "Release character at offset {offset} is dangling. \
800 Ensure '?' is followed by an escaped byte",
801 ))),
802 Self::MessageCountMismatch { expected, actual } => Some(Box::new(format!(
803 "UNZ declares {expected} message(s) but {actual} UNH/UNT pair(s) were found. \
804 Check the UNZ message count",
805 ))),
806 Self::SegmentCountMismatch {
807 expected,
808 actual,
809 message_ref,
810 } => Some(Box::new(format!(
811 "UNT for message {message_ref} declares {expected} segment(s) but {actual} were found. \
812 Check the UNT segment count",
813 ))),
814 Self::InvalidSegmentTag(tag) => Some(Box::new(format!(
815 "Segment tag '{tag}' must be exactly 3 ASCII uppercase letters",
816 ))),
817 Self::MissingRequiredElement { tag, element_index } => Some(Box::new(format!(
818 "Segment {tag} requires element at index {element_index}",
819 ))),
820 Self::MissingRequiredComponent {
821 tag,
822 element_index,
823 component_index,
824 } => Some(Box::new(format!(
825 "Segment {tag} element {element_index} requires component at index {component_index}",
826 ))),
827 Self::Io(e) => Some(Box::new(format!("I/O error: {e}"))),
828 Self::InvalidSegmentForMessage {
829 tag, message_type, ..
830 } => Some(Box::new(format!(
831 "Segment {tag} should not appear in a {message_type} message. \
832 Check the directory definition",
833 ))),
834 Self::InvalidElementCount {
835 tag,
836 min,
837 max,
838 actual,
839 ..
840 } => Some(Box::new(format!(
841 "Segment {tag} should have between {min} and {max} elements, but has {actual}. \
842 Check segment structure",
843 ))),
844 Self::InvalidComponentCount {
845 tag,
846 element_index,
847 expected,
848 actual,
849 ..
850 } => Some(Box::new(format!(
851 "In segment {tag}, element {element_index} should have {expected} components \
852 but has {actual}. Check element structure",
853 ))),
854 Self::InvalidCodeValue {
855 tag,
856 element_index,
857 value,
858 code_list,
859 ..
860 } => Some(Box::new(format!(
861 "Value '{value}' in segment {tag} element {element_index} is not in the \
862 {code_list} code list. Check the directory for valid codes",
863 ))),
864 Self::MissingSegment {
865 tag,
866 expected_position,
867 } => Some(Box::new(format!(
868 "Segment {tag} is required at position {expected_position} but is missing. \
869 Add this segment to the message",
870 ))),
871 Self::QualifierMismatch {
872 tag,
873 actual,
874 expected,
875 ..
876 } => Some(Box::new(format!(
877 "Segment {tag} has qualifier '{actual}' but expected '{expected}'. \
878 Check the segment's first component",
879 ))),
880 Self::ConditionalRequirementNotMet {
881 tag,
882 element_index,
883 condition,
884 ..
885 } => Some(Box::new(format!(
886 "In segment {tag}, element {element_index} is conditionally required when: \
887 {condition}. Check if the condition is met",
888 ))),
889 Self::SegmentTooLong { offset, limit } => Some(Box::new(format!(
890 "Segment starting at byte offset {offset} exceeds the {limit}-byte limit. \
891 Use ReaderConfig::max_segment_bytes to adjust the limit if needed, \
892 or verify the input for a missing segment terminator",
893 ))),
894 Self::UnexpectedMessageType { message_type } => Some(Box::new(format!(
895 "No handler was registered for message type '{message_type}'. \
896 Register a handler with MessageDispatch::on(\"{message_type}\", ...)",
897 ))),
898 Self::InterchangeTooLarge { count } => Some(Box::new(format!(
899 "Interchange contains {count} items which exceeds the u32::MAX limit. \
900 This is an extremely unusual input; verify the message is not corrupted.",
901 ))),
902 Self::InvalidEventSequence { message } => Some(Box::new(format!(
903 "Event sequence violation: {message}. \
904 Check that StartSegment is emitted before Element, and Element before ComponentElement.",
905 ))),
906 Self::InvalidElementPosition => Some(Box::new(
907 "Element positions must be >= 1 (one-based). \
908 Ensure no OwnedElementRef is constructed with position == 0",
909 )),
910 Self::IncompatibleReleaseScopes { current, incoming } => Some(Box::new(format!(
911 "Release scope {current:?} and {incoming:?} are incompatible. \
912 Only compose ProfileRulePack values that share the same release scope, \
913 or where at most one carries a release scope",
914 ))),
915 Self::InvalidFieldValue {
916 tag,
917 element_index,
918 value,
919 } => Some(Box::new(format!(
920 "Segment {tag} element {element_index} has invalid value '{value}'. \
921 Check the expected format or range for this field",
922 ))),
923 Self::UnexpectedDataToken { offset } => Some(Box::new(format!(
924 "Data element at offset {offset} appeared before any segment tag. \
925 Check for partial writes or encoding corruption",
926 ))),
927 Self::ValidationErrors { error_count, .. } => Some(Box::new(format!(
928 "Validation found {error_count} error(s). Inspect the ValidationReport for details",
929 ))),
930 Self::UnrecognisedSyntaxIdentifier(id) => Some(Box::new(format!(
931 "Syntax identifier '{id}' is not defined in ISO 9735-1. \
932 Valid values are UNOA, UNOB, UNOC, UNOD, UNOE, UNOF (or KECA for KEC-A profile)",
933 ))),
934 Self::DuplicateReference { tag, reference, .. } => Some(Box::new(format!(
935 "Reference '{reference}' is used by more than one {tag} in this interchange; \
936 each must be unique so receivers can address messages unambiguously",
937 ))),
938 Self::UnknownDataElement { tag, data_element } => Some(Box::new(format!(
939 "Segment {tag} does not define data element {data_element}. \
940 Check the identifier against the directory definition for {tag}",
941 ))),
942 Self::AmbiguousDataElement { tag, data_element } => Some(Box::new(format!(
943 "Segment {tag} defines data element {data_element} at more than one position, \
944 so code-addressed access cannot pick one; use a positional accessor",
945 ))),
946 Self::SegmentLayoutMismatch { expected, actual } => Some(Box::new(format!(
947 "The supplied layout describes segment {expected} but was applied to {actual}. \
948 Look up the definition by the segment's own tag",
949 ))),
950 Self::RepetitionSeparatorNotDeclared => Some(Box::new(
951 "UNA position 7 holds the space \"not used\" sentinel, so repeating data \
952 elements cannot be expressed. Use Writer::with_una with a repetition_sep",
953 )),
954 Self::CharacterNotInRepertoire {
955 charset,
956 character,
957 offset,
958 } => Some(Box::new(format!(
959 "The character {character:?} at offset {offset} has no representation in {charset}. \
960 Transliterate it, or declare a wider repertoire in UNB S001 DE 0001",
961 ))),
962 Self::UnsupportedCharset { syntax_identifier } => Some(Box::new(format!(
963 "'{syntax_identifier}' is stateful or multi-byte, so byte-level delimiter scanning \
964 would be unsound. Transcode the interchange to UNOC or UNOY before parsing",
965 ))),
966 Self::CharacterRepertoireMismatch { declared, writer } => Some(Box::new(format!(
967 "The UNB declares {declared} but the writer encodes {writer}. \
968 The receiver would decode the body with the wrong table",
969 ))),
970 Self::NonFiniteNumber { value } => Some(Box::new(format!(
971 "The value {value} is not finite. EDIFACT numeric data elements have no \
972 representation for NaN or infinity",
973 ))),
974 Self::LimitExceeded { limit, max } => Some(Box::new(format!(
975 "The input exceeds the configured {limit} limit of {max}. \
976 Raise it via ReaderConfig if the input is legitimate, or reject the input",
977 ))),
978 }
979 }
980}
981
982// ── validation report ─────────────────────────────────────────────────────────
983
984pub use crate::report::ValidationReport;
985
986#[cfg(test)]
987mod tests {
988 use super::*;
989
990 #[test]
991 fn recovery_hint_exists_for_common_malformed_cases() {
992 let err = EdifactError::InvalidReleaseSequence { offset: 10 };
993 assert!(err.recovery_hint().is_some());
994
995 let err = EdifactError::InvalidCodeValue {
996 tag: "BGM".to_owned(),
997 element_index: 0,
998 value: "X".to_owned(),
999 code_list: "1001".to_owned(),
1000 span: Span::new(0, 9),
1001 suggestion: None,
1002 };
1003 assert!(err.recovery_hint().is_some());
1004 }
1005}