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