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}