edifact_rs/directory_validator.rs
1//! Shared UN/EDIFACT directory validation engine used by D.11A, D.01B and D.96A.
2
3use crate::error::Insignificant;
4use crate::validator::{ValidationRuleContext, Validator, report_error};
5use crate::{EdifactError, Segment, ValidationIssue, ValidationReport, ValidationSeverity};
6use std::sync::Arc;
7
8/// Mandatory/Conditional status of a data element within a segment.
9///
10/// Marked `#[non_exhaustive]` because UN/EDIFACT also defines Required, Advised,
11/// Dependent, and Not-used statuses; adding one must not break downstream `match`
12/// arms.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum Status {
16 /// Element must be present.
17 Mandatory,
18 /// Element is optional unless additional rules require it.
19 Conditional,
20}
21
22/// The character class of a data element value — the `a` / `n` / `an` of a
23/// directory's representation column.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25#[non_exhaustive]
26pub enum ReprKind {
27 /// `a` — alphabetic. Digits are not permitted.
28 Alphabetic,
29 /// `n` — numeric, in the ISO 6093 forms ISO 9735-1 §10 admits: digits, an
30 /// optional leading minus, a decimal mark (`.` or `,`), and an exponent.
31 /// The space character and the plus sign are explicitly not allowed.
32 Numeric,
33 /// `an` — alphanumeric. Any character the interchange's repertoire permits.
34 Alphanumeric,
35}
36
37impl ReprKind {
38 /// The directory's abbreviation for this class.
39 #[must_use]
40 pub const fn as_str(self) -> &'static str {
41 match self {
42 Self::Alphabetic => "a",
43 Self::Numeric => "n",
44 Self::Alphanumeric => "an",
45 }
46 }
47}
48
49/// A data element's representation — `an..35`, `n8`, `a1` and friends.
50///
51/// This is the column every UN/EDIFACT directory prints beside a data element,
52/// and the thing partners actually reject on: a sender identification of 40
53/// characters where the standard says `an..35` is refused at the far end, long
54/// after it was sent.
55///
56/// # Length is counted in characters
57///
58/// ISO 9735-1 §6: "one graphic character shall be counted as one character,
59/// irrespective of the number of bytes/octets required to encode it" — so `ü`
60/// is one, not two. §5 excludes the release character from the count, which is
61/// automatic here because release sequences are already resolved by the time a
62/// value reaches validation.
63///
64/// For a numeric value §10 excludes more: "the length ... shall not include the
65/// minus sign (-), the decimal mark (. or ,), or the exponent mark (E or e) and
66/// its exponent". `-123.45` is therefore five characters long, not seven.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub struct Repr {
69 kind: ReprKind,
70 max: u16,
71 fixed: bool,
72}
73
74impl Repr {
75 /// `an{max}` — exactly `max` alphanumeric characters.
76 #[must_use]
77 pub const fn an(max: u16) -> Self {
78 Self {
79 kind: ReprKind::Alphanumeric,
80 max,
81 fixed: true,
82 }
83 }
84
85 /// `an..{max}` — up to `max` alphanumeric characters.
86 #[must_use]
87 pub const fn an_up_to(max: u16) -> Self {
88 Self {
89 kind: ReprKind::Alphanumeric,
90 max,
91 fixed: false,
92 }
93 }
94
95 /// `a{max}` — exactly `max` alphabetic characters.
96 #[must_use]
97 pub const fn a(max: u16) -> Self {
98 Self {
99 kind: ReprKind::Alphabetic,
100 max,
101 fixed: true,
102 }
103 }
104
105 /// `a..{max}` — up to `max` alphabetic characters.
106 #[must_use]
107 pub const fn a_up_to(max: u16) -> Self {
108 Self {
109 kind: ReprKind::Alphabetic,
110 max,
111 fixed: false,
112 }
113 }
114
115 /// `n{max}` — exactly `max` numeric characters.
116 #[must_use]
117 pub const fn n(max: u16) -> Self {
118 Self {
119 kind: ReprKind::Numeric,
120 max,
121 fixed: true,
122 }
123 }
124
125 /// `n..{max}` — up to `max` numeric characters.
126 #[must_use]
127 pub const fn n_up_to(max: u16) -> Self {
128 Self {
129 kind: ReprKind::Numeric,
130 max,
131 fixed: false,
132 }
133 }
134
135 /// The character class.
136 #[must_use]
137 pub const fn kind(self) -> ReprKind {
138 self.kind
139 }
140
141 /// The maximum number of characters.
142 #[must_use]
143 pub const fn max_length(self) -> u16 {
144 self.max
145 }
146
147 /// The minimum number of characters — `max_length` when fixed, else 1.
148 ///
149 /// A data element is "present" only when it carries at least one character
150 /// (§8.1), so a variable-length minimum is never zero.
151 #[must_use]
152 pub const fn min_length(self) -> u16 {
153 if self.fixed { self.max } else { 1 }
154 }
155
156 /// Whether the length is fixed (`an3`) rather than variable (`an..3`).
157 #[must_use]
158 pub const fn is_fixed(self) -> bool {
159 self.fixed
160 }
161
162 /// The number of characters `value` contributes toward its length limit.
163 ///
164 /// For a numeric value this is §10's count, which excludes the sign, the
165 /// decimal mark, and the exponent.
166 #[must_use]
167 pub fn measure(self, value: &str) -> usize {
168 match self.kind {
169 ReprKind::Numeric => {
170 let mantissa = value
171 .split_once(['E', 'e'])
172 .map_or(value, |(mantissa, _exponent)| mantissa);
173 mantissa
174 .chars()
175 .filter(|c| !matches!(c, '-' | '.' | ','))
176 .count()
177 }
178 _ => value.chars().count(),
179 }
180 }
181
182 /// Whether every character of `value` belongs to this representation's class.
183 #[must_use]
184 pub fn permits_characters(self, value: &str) -> bool {
185 match self.kind {
186 ReprKind::Alphanumeric => true,
187 ReprKind::Alphabetic => !value.chars().any(|c| c.is_ascii_digit()),
188 ReprKind::Numeric => is_iso6093_numeric(value),
189 }
190 }
191}
192
193impl std::fmt::Display for Repr {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 if self.fixed {
196 write!(f, "{}{}", self.kind.as_str(), self.max)
197 } else {
198 write!(f, "{}..{}", self.kind.as_str(), self.max)
199 }
200 }
201}
202
203/// Whether `value` is one of the numeric forms ISO 9735-1 §10 admits.
204///
205/// §10 takes ISO 6093's representations and subtracts: "The space character and
206/// plus sign shall not be allowed", and "when a decimal mark is transferred,
207/// there shall be at least one digit after the decimal mark" — which is why
208/// `1.` and `.` are rejected while `.5` and `2.00` are not.
209fn is_iso6093_numeric(value: &str) -> bool {
210 let (mantissa, exponent) = match value.split_once(['E', 'e']) {
211 Some((mantissa, exponent)) => (mantissa, Some(exponent)),
212 None => (value, None),
213 };
214 if let Some(exponent) = exponent {
215 let digits = exponent.strip_prefix('-').unwrap_or(exponent);
216 if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
217 return false;
218 }
219 }
220 let digits = mantissa.strip_prefix('-').unwrap_or(mantissa);
221 if digits.is_empty() {
222 return false;
223 }
224 match digits.split_once(['.', ',']) {
225 Some((integer, fraction)) => {
226 // At least one digit after the mark; the integer part may be empty.
227 !fraction.is_empty()
228 && fraction.chars().all(|c| c.is_ascii_digit())
229 && integer.chars().all(|c| c.is_ascii_digit())
230 }
231 None => digits.chars().all(|c| c.is_ascii_digit()),
232 }
233}
234
235/// What a value at one position must satisfy.
236///
237/// Usually a single [`Repr`]. Two only where the syntax versions disagree and
238/// the interchange has not said which it is — see
239/// [`ComponentRef::with_repr_by_syntax_version`].
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241struct ReprRequirement {
242 primary: Repr,
243 /// Accepted as well, when the syntax version could not be determined.
244 alternative: Option<Repr>,
245}
246
247impl ReprRequirement {
248 const fn single(repr: Repr) -> Self {
249 Self {
250 primary: repr,
251 alternative: None,
252 }
253 }
254
255 /// Whether `value` satisfies the class of any accepted representation.
256 fn permits_characters(&self, value: &str) -> bool {
257 self.primary.permits_characters(value)
258 || self
259 .alternative
260 .is_some_and(|repr| repr.permits_characters(value))
261 }
262
263 /// Whether `value`'s length satisfies any accepted representation.
264 fn permits_length(&self, value: &str) -> bool {
265 let fits = |repr: Repr| {
266 let length = repr.measure(value);
267 length >= usize::from(repr.min_length()) && length <= usize::from(repr.max_length())
268 };
269 fits(self.primary) || self.alternative.is_some_and(fits)
270 }
271
272 /// `true` when every accepted representation is longer than `value`.
273 fn is_too_short(&self, value: &str) -> bool {
274 let short = |repr: Repr| repr.measure(value) < usize::from(repr.min_length());
275 short(self.primary) && self.alternative.is_none_or(short)
276 }
277
278 /// The measured length under the primary representation, for reporting.
279 fn measure(&self, value: &str) -> usize {
280 self.primary.measure(value)
281 }
282}
283
284impl std::fmt::Display for ReprRequirement {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 match self.alternative {
287 Some(alternative) => write!(f, "{} or {alternative}", self.primary),
288 None => write!(f, "{}", self.primary),
289 }
290 }
291}
292
293/// Reference to a component data element within a composite data element.
294///
295/// Composites such as `C507` (DTM date/time/period) are addressed by the
296/// identifier of their *own* components (`2005`, `2380`, `2379`), which is what
297/// makes code-addressed access — [`Segment::value_by_code`][crate::Segment::value_by_code]
298/// and `#[edifact(element = "2005")]` — resolve to the right slot instead of a
299/// hand-counted index.
300///
301/// Fields are private to enforce the one-based position invariant.
302#[derive(Debug, Clone, Copy)]
303pub struct ComponentRef {
304 /// One-based position of the **first** slot this component occupies.
305 position: u8,
306 /// UN/EDIFACT component data element identifier.
307 data_element: &'static str,
308 /// Requirement status of the component.
309 status: Status,
310 /// How many consecutive slots this component occupies; `1` unless the
311 /// composite repeats it by design.
312 repeat_count: u8,
313 /// The directory's representation, when the definition states one.
314 repr: Option<Repr>,
315 /// The representation from syntax version 4 onward, where it differs.
316 repr_from_v4: Option<Repr>,
317}
318
319impl ComponentRef {
320 /// Construct a `ComponentRef` with compile-time position validation.
321 ///
322 /// `position` must be ≥ 1 (one-based). In a `const` context a zero
323 /// `position` is a **compile-time error**; at runtime it panics.
324 ///
325 /// # Panics
326 ///
327 /// Panics if `position == 0`.
328 ///
329 /// # Example
330 ///
331 /// ```rust
332 /// use edifact_rs::{ComponentRef, Status};
333 ///
334 /// const DTM_2005: ComponentRef = ComponentRef::new(1, "2005", Status::Mandatory);
335 /// ```
336 #[must_use]
337 pub const fn new(position: u8, data_element: &'static str, status: Status) -> Self {
338 assert!(
339 position != 0,
340 "ComponentRef position must be >= 1 (one-based)"
341 );
342 Self {
343 position,
344 data_element,
345 status,
346 repeat_count: 1,
347 repr: None,
348 repr_from_v4: None,
349 }
350 }
351
352 /// Declare a component the composite repeats by design.
353 ///
354 /// Several standard composites carry the same data element several times
355 /// over: `C080 PARTY NAME` is `3036` five times followed by `3045`, `C059
356 /// STREET` is `3042` four times. Spelling that out as five separate
357 /// [`new`][Self::new] entries made the code count as five positions, so
358 /// [`resolve_code`][SegmentLayout::resolve_code] reported it
359 /// [ambiguous][EdifactError::AmbiguousDataElement] and the component could
360 /// not be code-addressed at all — a faithful declaration was punished, and
361 /// the only way to use named access was to declare the composite
362 /// incompletely and disagree with the directory it claims to model.
363 ///
364 /// One `repeated` entry is one position, so `3036` resolves to occurrence 1
365 /// — what "the party name" means in every real message — while the
366 /// definition still records that five slots belong to it.
367 ///
368 /// `position` is the **first** slot; the next component follows at
369 /// `position + repeat_count`.
370 ///
371 /// # Panics
372 ///
373 /// Panics if `position == 0` or `repeat_count == 0`.
374 ///
375 /// # Example
376 ///
377 /// ```rust
378 /// use edifact_rs::{ComponentRef, ElementRef, SegmentDefinition, SegmentLayout, Status};
379 ///
380 /// // C080 PARTY NAME: 3036 ×5, then 3045 at position 6.
381 /// static C080: &[ComponentRef] = &[
382 /// ComponentRef::repeated(1, "3036", Status::Mandatory, 5),
383 /// ComponentRef::new(6, "3045", Status::Conditional),
384 /// ];
385 /// static NAD_ELEMENTS: &[ElementRef] = &[
386 /// ElementRef::new(1, "3035", Status::Mandatory, 1),
387 /// ElementRef::composite(4, "C080", Status::Conditional, 1, C080),
388 /// ];
389 /// static NAD: SegmentDefinition = SegmentDefinition::new("NAD", "Name and address", NAD_ELEMENTS);
390 ///
391 /// // Addressable, and it points at the first occurrence.
392 /// let path = NAD.resolve_code("3036")?;
393 /// assert_eq!((path.element, path.component), (3, Some(0)));
394 /// # Ok::<(), edifact_rs::EdifactError>(())
395 /// ```
396 #[must_use]
397 pub const fn repeated(
398 position: u8,
399 data_element: &'static str,
400 status: Status,
401 repeat_count: u8,
402 ) -> Self {
403 assert!(
404 position != 0,
405 "ComponentRef position must be >= 1 (one-based)"
406 );
407 assert!(
408 repeat_count != 0,
409 "ComponentRef repeat_count must be >= 1; use `new` for a component that does not repeat"
410 );
411 Self {
412 position,
413 data_element,
414 status,
415 repeat_count,
416 repr: None,
417 repr_from_v4: None,
418 }
419 }
420
421 /// How many consecutive slots this component occupies.
422 ///
423 /// `1` for a component declared with [`new`][Self::new].
424 #[must_use]
425 #[inline]
426 pub const fn repeat_count(&self) -> u8 {
427 self.repeat_count
428 }
429
430 /// One-based component position within the composite.
431 #[must_use]
432 #[inline]
433 pub const fn position(&self) -> u8 {
434 self.position
435 }
436
437 /// UN/EDIFACT component data element identifier.
438 #[must_use]
439 #[inline]
440 pub const fn data_element(&self) -> &'static str {
441 self.data_element
442 }
443
444 /// Requirement status of the component.
445 #[must_use]
446 #[inline]
447 pub const fn status(&self) -> Status {
448 self.status
449 }
450
451 /// Attach the directory's representation, e.g. `an..35`.
452 ///
453 /// Without one, a definition still resolves identifiers and checks presence;
454 /// with one, [`DirectoryValidator`] also checks length and character class —
455 /// which is what a partner's translator does before it rejects the file.
456 ///
457 /// # Example
458 ///
459 /// ```
460 /// use edifact_rs::{ComponentRef, Repr, Status};
461 ///
462 /// const SENDER: ComponentRef =
463 /// ComponentRef::new(1, "0004", Status::Mandatory).with_repr(Repr::an_up_to(35));
464 /// ```
465 #[must_use]
466 pub const fn with_repr(mut self, repr: Repr) -> Self {
467 self.repr = Some(repr);
468 self
469 }
470
471 /// The declared representation, if the definition states one.
472 #[must_use]
473 #[inline]
474 pub const fn repr(&self) -> Option<Repr> {
475 self.repr
476 }
477 /// Declare a representation that changed between syntax versions.
478 ///
479 /// The service directory has exactly one such position: `S004` DE 0017, the
480 /// date of preparation. Version 3 transfers `YYMMDD` (`n6`); version 4
481 /// widened it to `CCYYMMDD` (`n8`) to be year-2000 correct. It is the only
482 /// place where version 4 is *not* a superset of version 3.
483 ///
484 /// Declaring both keeps each version checked exactly. Collapsing them into
485 /// `n..8` would have accepted a six-digit date in a version 4 interchange
486 /// and a seven-digit one in either — validating neither version correctly.
487 ///
488 /// The syntax version comes from `UNB` S001 DE 0002. When it cannot be
489 /// determined — validating a bare message window, say — **both** are
490 /// accepted, because guessing would reject conformant data.
491 #[must_use]
492 pub const fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
493 self.repr = Some(up_to_v3);
494 self.repr_from_v4 = Some(from_v4);
495 self
496 }
497
498 /// The representation used from syntax version 4 onward, when it differs.
499 #[must_use]
500 #[inline]
501 pub const fn repr_from_v4(&self) -> Option<Repr> {
502 self.repr_from_v4
503 }
504}
505
506/// Reference to a data element within a segment definition.
507///
508/// Fields are private to enforce the one-based position invariant through the
509/// [`ElementRef::new`] constructor. Use [`ElementRef::new`] for a simple data
510/// element and [`ElementRef::composite`] for a composite whose components are
511/// themselves named (panics at compile time when `position == 0`).
512///
513/// Use [`OwnedElementRef`] for runtime-constructed element refs.
514#[derive(Debug, Clone, Copy)]
515pub struct ElementRef {
516 /// One-based element position in the segment definition.
517 position: u8,
518 /// UN/EDIFACT data element identifier.
519 data_element: &'static str,
520 /// Requirement status of the element.
521 status: Status,
522 /// Maximum repetition count for this element.
523 max_repeat: u8,
524 /// Component definitions when this element is a composite; empty for a
525 /// simple data element.
526 components: &'static [ComponentRef],
527 /// The directory's representation for a *simple* element; composites carry
528 /// theirs on each component.
529 repr: Option<Repr>,
530 /// The representation from syntax version 4 onward, where it differs.
531 repr_from_v4: Option<Repr>,
532}
533
534impl ElementRef {
535 /// Construct an `ElementRef` for a simple data element.
536 ///
537 /// `position` must be ≥ 1 (one-based). When called in a `const` context
538 /// (e.g. inside a `static` array initialiser), a zero `position` causes a
539 /// **compile-time error**. At runtime it panics.
540 ///
541 /// Use [`composite`][Self::composite] when the element is a composite whose
542 /// components carry their own UN/EDIFACT identifiers.
543 ///
544 /// # Panics
545 ///
546 /// Panics if `position == 0`.
547 ///
548 /// # Example
549 ///
550 /// ```rust
551 /// use edifact_rs::{ElementRef, Status};
552 ///
553 /// const BGM_1001: ElementRef = ElementRef::new(1, "1001", Status::Mandatory, 1);
554 /// ```
555 #[must_use]
556 pub const fn new(
557 position: u8,
558 data_element: &'static str,
559 status: Status,
560 max_repeat: u8,
561 ) -> Self {
562 assert!(
563 position != 0,
564 "ElementRef position must be >= 1 (one-based)"
565 );
566 Self {
567 position,
568 data_element,
569 status,
570 max_repeat,
571 components: &[],
572 repr: None,
573 repr_from_v4: None,
574 }
575 }
576
577 /// Construct an `ElementRef` for a composite data element with named components.
578 ///
579 /// Declaring components is what lets code-addressed access reach *inside* a
580 /// composite: `value_by_code(&DTM, "2380")` resolves to element 1,
581 /// component 2 without the caller counting positions. Declared components
582 /// also make the mandatory-component check in [`DirectoryValidator`] active
583 /// for this element.
584 ///
585 /// # Panics
586 ///
587 /// Panics if `position == 0`.
588 ///
589 /// # Example
590 ///
591 /// ```rust
592 /// use edifact_rs::{ComponentRef, ElementRef, Status};
593 ///
594 /// static C507: &[ComponentRef] = &[
595 /// ComponentRef::new(1, "2005", Status::Mandatory),
596 /// ComponentRef::new(2, "2380", Status::Conditional),
597 /// ComponentRef::new(3, "2379", Status::Conditional),
598 /// ];
599 /// const DTM_C507: ElementRef =
600 /// ElementRef::composite(1, "C507", Status::Mandatory, 1, C507);
601 /// ```
602 #[must_use]
603 pub const fn composite(
604 position: u8,
605 data_element: &'static str,
606 status: Status,
607 max_repeat: u8,
608 components: &'static [ComponentRef],
609 ) -> Self {
610 assert!(
611 position != 0,
612 "ElementRef position must be >= 1 (one-based)"
613 );
614 Self {
615 position,
616 data_element,
617 status,
618 max_repeat,
619 components,
620 repr: None,
621 repr_from_v4: None,
622 }
623 }
624
625 /// One-based element position in the segment definition.
626 #[must_use]
627 #[inline]
628 pub const fn position(&self) -> u8 {
629 self.position
630 }
631
632 /// UN/EDIFACT data element identifier.
633 #[must_use]
634 #[inline]
635 pub const fn data_element(&self) -> &'static str {
636 self.data_element
637 }
638
639 /// Requirement status of the element.
640 #[must_use]
641 #[inline]
642 pub const fn status(&self) -> Status {
643 self.status
644 }
645
646 /// Maximum repetition count for this element.
647 #[must_use]
648 #[inline]
649 pub const fn max_repeat(&self) -> u8 {
650 self.max_repeat
651 }
652
653 /// Component definitions; empty when this is a simple data element.
654 #[must_use]
655 #[inline]
656 pub const fn components(&self) -> &'static [ComponentRef] {
657 self.components
658 }
659
660 /// Attach the directory's representation for a simple data element.
661 ///
662 /// A composite carries its representations on the components instead, so
663 /// this is ignored when `components` is non-empty.
664 #[must_use]
665 pub const fn with_repr(mut self, repr: Repr) -> Self {
666 self.repr = Some(repr);
667 self
668 }
669
670 /// The declared representation, if the definition states one.
671 #[must_use]
672 #[inline]
673 pub const fn repr(&self) -> Option<Repr> {
674 self.repr
675 }
676 /// Declare a representation that changed between syntax versions.
677 ///
678 /// The service directory has exactly one such position: `S004` DE 0017, the
679 /// date of preparation. Version 3 transfers `YYMMDD` (`n6`); version 4
680 /// widened it to `CCYYMMDD` (`n8`) to be year-2000 correct. It is the only
681 /// place where version 4 is *not* a superset of version 3.
682 ///
683 /// Declaring both keeps each version checked exactly. Collapsing them into
684 /// `n..8` would have accepted a six-digit date in a version 4 interchange
685 /// and a seven-digit one in either — validating neither version correctly.
686 ///
687 /// The syntax version comes from `UNB` S001 DE 0002. When it cannot be
688 /// determined — validating a bare message window, say — **both** are
689 /// accepted, because guessing would reject conformant data.
690 #[must_use]
691 pub const fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
692 self.repr = Some(up_to_v3);
693 self.repr_from_v4 = Some(from_v4);
694 self
695 }
696
697 /// The representation used from syntax version 4 onward, when it differs.
698 #[must_use]
699 #[inline]
700 pub const fn repr_from_v4(&self) -> Option<Repr> {
701 self.repr_from_v4
702 }
703}
704
705/// Definition of an EDIFACT segment (tag + element structure).
706///
707/// Construct with [`SegmentDefinition::new`] rather than a struct literal, so
708/// that future fields (max repeat, description, …) are not a breaking change.
709#[derive(Debug)]
710#[non_exhaustive]
711pub struct SegmentDefinition {
712 /// Segment tag.
713 pub tag: &'static str,
714 /// Human-readable segment name.
715 pub name: &'static str,
716 /// Ordered element definitions.
717 pub elements: &'static [ElementRef],
718}
719
720/// Byte-wise string equality usable in a `const` context.
721///
722/// `str::eq` is not `const`, and code resolution has to run at compile time so
723/// that a mistyped data element identifier in `#[edifact(element = "3055")]`
724/// fails the build rather than reading the wrong slot at runtime.
725const fn const_str_eq(a: &str, b: &str) -> bool {
726 let (a, b) = (a.as_bytes(), b.as_bytes());
727 if a.len() != b.len() {
728 return false;
729 }
730 let mut i = 0;
731 while i < a.len() {
732 if a[i] != b[i] {
733 return false;
734 }
735 i += 1;
736 }
737 true
738}
739
740/// The resolved position of a UN/EDIFACT data element within a segment.
741///
742/// Produced by [`SegmentLayout::resolve_code`] and consumed by the `*_at`
743/// accessors on [`crate::Segment`], [`crate::BorrowedSegment`] and
744/// [`crate::OwnedSegment`].
745///
746/// Both indices are **zero-based**, matching the positional accessors — the
747/// one-based positions used in directory definitions are converted during
748/// resolution.
749#[derive(Debug, Clone, Copy, PartialEq, Eq)]
750pub struct ElementPath {
751 /// Zero-based index of the data element within the segment.
752 pub element: usize,
753 /// Zero-based index of the component within a composite.
754 ///
755 /// `None` when the code names the data element itself (a simple element, or
756 /// a composite addressed as a whole). Value lookups treat `None` as
757 /// component 0, which is the first — and for a simple element, only —
758 /// component.
759 pub component: Option<usize>,
760}
761
762impl ElementPath {
763 /// Path to a whole data element.
764 #[must_use]
765 #[inline]
766 pub const fn element(element: usize) -> Self {
767 Self {
768 element,
769 component: None,
770 }
771 }
772
773 /// Path to a component within a composite data element.
774 #[must_use]
775 #[inline]
776 pub const fn component(element: usize, component: usize) -> Self {
777 Self {
778 element,
779 component: Some(component),
780 }
781 }
782
783 /// Zero-based component index, treating "whole element" as component 0.
784 #[must_use]
785 #[inline]
786 pub const fn component_index(&self) -> usize {
787 match self.component {
788 Some(c) => c,
789 None => 0,
790 }
791 }
792}
793
794/// Directory metadata that maps UN/EDIFACT data element identifiers to positions.
795///
796/// Implemented by [`SegmentDefinition`] (compile-time tables) and
797/// [`OwnedSegmentDef`] (runtime-loaded definitions), so the same code-addressed
798/// accessors work against either source.
799///
800/// # Example
801///
802/// ```rust
803/// use edifact_rs::{ElementRef, SegmentDefinition, SegmentLayout, Status};
804///
805/// static BGM_ELEMENTS: &[ElementRef] = &[
806/// ElementRef::new(1, "C002", Status::Conditional, 1),
807/// ElementRef::new(2, "C106", Status::Conditional, 1),
808/// ElementRef::new(3, "1225", Status::Conditional, 1),
809/// ];
810/// static BGM: SegmentDefinition =
811/// SegmentDefinition::new("BGM", "Beginning of message", BGM_ELEMENTS);
812///
813/// let path = BGM.resolve_code("1225")?;
814/// assert_eq!(path.element, 2);
815/// assert!(BGM.resolve_code("9999").is_err());
816/// # Ok::<(), edifact_rs::EdifactError>(())
817/// ```
818pub trait SegmentLayout {
819 /// The segment tag this layout describes (e.g. `"NAD"`).
820 fn layout_tag(&self) -> &str;
821
822 /// Resolve a UN/EDIFACT data element identifier to a position.
823 ///
824 /// # Errors
825 ///
826 /// Returns [`EdifactError::UnknownDataElement`] when the identifier does not
827 /// appear in this definition, and [`EdifactError::AmbiguousDataElement`]
828 /// when it appears at more than one position.
829 fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError>;
830
831 /// Every position this layout declares, flattened and in order.
832 ///
833 /// Implemented by both the compile-time and runtime definitions, so tooling
834 /// can walk a layout without knowing which one it holds.
835 fn slots(&self) -> Vec<LayoutSlot>;
836
837 /// Check this layout against real messages and report what does not line up.
838 ///
839 /// Hand-authoring a segment definition has a silent failure mode: a layout
840 /// that disagrees with the wire resolves `value_by_code` to the *wrong
841 /// component*, returns a plausible value, and every test still passes. There
842 /// is no way to notice from inside the program — the definition is the only
843 /// thing that says what the positions mean.
844 ///
845 /// Pointing the definition at a corpus is what breaks that circle. Three
846 /// kinds of finding come back, and the third is the one that matters most:
847 ///
848 /// | Finding | Means |
849 /// |---|---|
850 /// | [`UndeclaredElement`][LayoutFinding::UndeclaredElement] / [`UndeclaredComponent`][LayoutFinding::UndeclaredComponent] | The wire carries a value the layout has no slot for — the layout is **wrong**. |
851 /// | [`MandatoryNeverPopulated`][LayoutFinding::MandatoryNeverPopulated] | A slot the layout calls mandatory is empty everywhere — the status or the position is **wrong**. |
852 /// | [`NeverObserved`][LayoutFinding::NeverObserved] | Nothing in the corpus reaches this slot, so **the corpus cannot confirm it**. |
853 ///
854 /// `NeverObserved` is not a defect. It is the honest answer to "does my
855 /// definition match the directory?" when the fixtures are too thin to tell,
856 /// and it names exactly which positions to go and check by hand.
857 ///
858 /// Only segments whose tag matches [`layout_tag`][Self::layout_tag] are
859 /// examined; the rest of the slice is ignored, so a whole interchange can be
860 /// passed in as-is.
861 ///
862 /// # Example
863 ///
864 /// ```
865 /// use edifact_rs::{ComponentRef, ElementRef, SegmentDefinition, SegmentLayout, Status, from_bytes};
866 ///
867 /// // A hand-authored C507 that stops one component short of the directory.
868 /// static C507: &[ComponentRef] = &[
869 /// ComponentRef::new(1, "2005", Status::Mandatory),
870 /// ComponentRef::new(2, "2380", Status::Conditional),
871 /// ];
872 /// static DTM_ELEMENTS: &[ElementRef] =
873 /// &[ElementRef::composite(1, "C507", Status::Mandatory, 1, C507)];
874 /// static DTM: SegmentDefinition =
875 /// SegmentDefinition::new("DTM", "Date/time/period", DTM_ELEMENTS);
876 ///
877 /// let corpus: Vec<_> = from_bytes(b"DTM+137:20260101:102'").collect::<Result<Vec<_>, _>>()?;
878 /// let audit = DTM.audit(&corpus);
879 ///
880 /// // The format qualifier `102` has nowhere to go — the layout is short.
881 /// assert!(audit.has_contradictions());
882 /// assert_eq!(audit.segments_examined(), 1);
883 /// # Ok::<(), edifact_rs::EdifactError>(())
884 /// ```
885 fn audit(&self, segments: &[crate::Segment<'_>]) -> LayoutAudit {
886 audit_layout(self.layout_tag(), &self.slots(), segments)
887 }
888}
889
890/// One declared position in a [`SegmentLayout`], flattened.
891///
892/// Produced by [`SegmentLayout::slots`].
893#[derive(Debug, Clone, PartialEq, Eq)]
894pub struct LayoutSlot {
895 /// Zero-based data element index within the segment.
896 pub element: usize,
897 /// Zero-based component index, or `None` for a simple data element.
898 pub component: Option<usize>,
899 /// The UN/EDIFACT identifier declared at this position.
900 pub data_element: String,
901 /// Whether the layout calls this position mandatory.
902 pub status: Status,
903 /// The status of the **enclosing data element**.
904 ///
905 /// Equal to `status` for a simple data element. For a component it is the
906 /// composite's own status, which is what decides whether a mandatory
907 /// component is actually required: ISO 9735-1 §8.6 makes it mandatory "if
908 /// the composite data element is present", not unconditionally.
909 pub element_status: Status,
910}
911
912impl LayoutSlot {
913 /// Component index treating a simple data element as component 0.
914 #[must_use]
915 pub fn component_index(&self) -> usize {
916 self.component.unwrap_or(0)
917 }
918}
919
920/// One way a layout and a corpus disagree — or fail to inform each other.
921#[derive(Debug, Clone, PartialEq, Eq)]
922#[non_exhaustive]
923pub enum LayoutFinding {
924 /// A segment carried a populated data element beyond the last one declared.
925 UndeclaredElement {
926 /// Zero-based index of the undeclared element.
927 element: usize,
928 /// Byte span of the segment that carried it.
929 span: crate::Span,
930 },
931 /// An element carried a populated component beyond the last one declared.
932 UndeclaredComponent {
933 /// Zero-based element index.
934 element: usize,
935 /// Zero-based index of the undeclared component.
936 component: usize,
937 /// Byte span of the segment that carried it.
938 span: crate::Span,
939 },
940 /// A position the layout calls mandatory was empty in every segment.
941 MandatoryNeverPopulated {
942 /// The declared position.
943 slot: LayoutSlot,
944 },
945 /// No segment in the corpus populated this position.
946 ///
947 /// Evidence of nothing rather than evidence of a fault: the corpus is too
948 /// thin to confirm or refute the slot.
949 NeverObserved {
950 /// The declared position.
951 slot: LayoutSlot,
952 },
953}
954
955/// The result of checking a [`SegmentLayout`] against a corpus.
956///
957/// See [`SegmentLayout::audit`].
958#[derive(Debug, Clone, Default)]
959pub struct LayoutAudit {
960 tag: String,
961 segments_examined: usize,
962 findings: Vec<LayoutFinding>,
963}
964
965impl LayoutAudit {
966 /// The segment tag that was audited.
967 #[must_use]
968 pub fn tag(&self) -> &str {
969 &self.tag
970 }
971
972 /// How many segments in the corpus carried that tag.
973 ///
974 /// Zero means the audit proved nothing at all — worth asserting on.
975 #[must_use]
976 pub fn segments_examined(&self) -> usize {
977 self.segments_examined
978 }
979
980 /// Every finding, in declaration order.
981 #[must_use]
982 pub fn findings(&self) -> &[LayoutFinding] {
983 &self.findings
984 }
985
986 /// Findings that mean the layout is **wrong**, as opposed to unconfirmed.
987 ///
988 /// [`NeverObserved`][LayoutFinding::NeverObserved] is excluded: a corpus
989 /// that never reaches a slot says nothing about whether the slot is right.
990 pub fn contradictions(&self) -> impl Iterator<Item = &LayoutFinding> {
991 self.findings
992 .iter()
993 .filter(|f| !matches!(f, LayoutFinding::NeverObserved { .. }))
994 }
995
996 /// `true` when the corpus contradicts the layout.
997 ///
998 /// This is the assertion to put in a test: it fails on a layout the wire
999 /// disproves, and stays quiet about slots the fixtures simply never exercise.
1000 #[must_use]
1001 pub fn has_contradictions(&self) -> bool {
1002 self.contradictions().next().is_some()
1003 }
1004
1005 /// Positions the corpus never reached, in declaration order.
1006 ///
1007 /// Each one is a slot to verify against the directory by hand — or a gap to
1008 /// fill with a fixture.
1009 pub fn unconfirmed(&self) -> impl Iterator<Item = &LayoutSlot> {
1010 self.findings.iter().filter_map(|f| match f {
1011 LayoutFinding::NeverObserved { slot } => Some(slot),
1012 _ => None,
1013 })
1014 }
1015}
1016
1017impl std::fmt::Display for LayoutAudit {
1018 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1019 writeln!(
1020 f,
1021 "{}: {} segment(s) examined, {} contradiction(s), {} unconfirmed slot(s)",
1022 self.tag,
1023 self.segments_examined,
1024 self.contradictions().count(),
1025 self.unconfirmed().count(),
1026 )?;
1027 for finding in &self.findings {
1028 match finding {
1029 LayoutFinding::UndeclaredElement { element, span } => writeln!(
1030 f,
1031 " element {element} is populated at bytes {span} but the layout declares no such element",
1032 )?,
1033 LayoutFinding::UndeclaredComponent {
1034 element,
1035 component,
1036 span,
1037 } => writeln!(
1038 f,
1039 " element {element} component {component} is populated at bytes {span} but the layout declares no such component",
1040 )?,
1041 LayoutFinding::MandatoryNeverPopulated { slot } => writeln!(
1042 f,
1043 " {} is declared mandatory but is empty in every segment",
1044 describe_slot(slot),
1045 )?,
1046 LayoutFinding::NeverObserved { slot } => writeln!(
1047 f,
1048 " {} was never populated — this corpus cannot confirm it",
1049 describe_slot(slot),
1050 )?,
1051 }
1052 }
1053 Ok(())
1054 }
1055}
1056
1057fn describe_slot(slot: &LayoutSlot) -> String {
1058 match slot.component {
1059 Some(component) => format!(
1060 "DE {} (element {}, component {component})",
1061 slot.data_element, slot.element
1062 ),
1063 None => format!("DE {} (element {})", slot.data_element, slot.element),
1064 }
1065}
1066
1067/// Audit every layout a corpus actually exercises, in one call.
1068///
1069/// [`SegmentLayout::audit`] answers for one segment. A hand-authored directory
1070/// has dozens, and the question worth asking is about all of them at once:
1071/// *which of my definitions does this corpus disprove, and which can it not
1072/// speak to?*
1073///
1074/// Only tags present in the corpus are audited — a definition the fixtures never
1075/// exercise would produce nothing but `NeverObserved` noise and drown the
1076/// findings that matter. Ask [`SegmentLayout::audit`] directly for those.
1077///
1078/// Results come back in the order the tags first appear, so the report reads in
1079/// message order.
1080///
1081/// # Example
1082///
1083/// ```
1084/// use edifact_rs::{audit_directory, from_bytes, service};
1085///
1086/// let corpus: Vec<_> = from_bytes(
1087/// b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+M1+ORDERS:D:96A:UN'UNT+2+M1'UNZ+1+IC1'",
1088/// )
1089/// .collect::<Result<Vec<_>, _>>()?;
1090///
1091/// let audits = audit_directory(service::lookup, &corpus);
1092///
1093/// // One audit per distinct tag the corpus contains.
1094/// assert_eq!(audits.len(), 4);
1095/// // The shipped service tables are not disproved by conformant input.
1096/// assert!(audits.iter().all(|a| !a.has_contradictions()));
1097/// # Ok::<(), edifact_rs::EdifactError>(())
1098/// ```
1099pub fn audit_directory<'a, L, F>(lookup: F, segments: &[crate::Segment<'_>]) -> Vec<LayoutAudit>
1100where
1101 L: SegmentLayout + ?Sized + 'a,
1102 F: Fn(&str) -> Option<&'a L>,
1103{
1104 let mut seen: Vec<&str> = Vec::new();
1105 for segment in segments {
1106 if !seen.contains(&segment.tag) {
1107 seen.push(segment.tag);
1108 }
1109 }
1110 seen.into_iter()
1111 .filter_map(|tag| lookup(tag).map(|layout| layout.audit(segments)))
1112 .collect()
1113}
1114
1115/// Shared implementation behind [`SegmentLayout::audit`].
1116fn audit_layout(tag: &str, slots: &[LayoutSlot], segments: &[crate::Segment<'_>]) -> LayoutAudit {
1117 let mut audit = LayoutAudit {
1118 tag: tag.to_owned(),
1119 segments_examined: 0,
1120 findings: Vec::new(),
1121 };
1122
1123 // Highest declared index per element, so "beyond the layout" is decidable.
1124 let declared_elements = slots.iter().map(|s| s.element + 1).max().unwrap_or(0);
1125 let mut declared_components: Vec<usize> = vec![0; declared_elements];
1126 for slot in slots {
1127 let width = slot.component_index() + 1;
1128 if width > declared_components[slot.element] {
1129 declared_components[slot.element] = width;
1130 }
1131 }
1132
1133 let mut populated: Vec<Vec<bool>> = declared_components
1134 .iter()
1135 .map(|width| vec![false; *width])
1136 .collect();
1137
1138 for segment in segments.iter().filter(|s| s.tag == tag) {
1139 audit.segments_examined += 1;
1140 for (element_index, element) in segment.elements.iter().enumerate() {
1141 // Every occurrence counts: a repeating element populates the same
1142 // declared positions each time (ISO 9735-1 §8.6).
1143 for occurrence in element.repetitions() {
1144 for (component_index, (value, _)) in occurrence.iter().enumerate() {
1145 // A trailing empty component is how EDIFACT spells "absent"
1146 // (§8.7.2), so only a populated one is evidence of anything.
1147 if value.is_empty() {
1148 continue;
1149 }
1150 if element_index >= declared_elements {
1151 push_once(
1152 &mut audit.findings,
1153 LayoutFinding::UndeclaredElement {
1154 element: element_index,
1155 span: segment.span,
1156 },
1157 );
1158 continue;
1159 }
1160 if component_index >= declared_components[element_index] {
1161 push_once(
1162 &mut audit.findings,
1163 LayoutFinding::UndeclaredComponent {
1164 element: element_index,
1165 component: component_index,
1166 span: segment.span,
1167 },
1168 );
1169 continue;
1170 }
1171 populated[element_index][component_index] = true;
1172 }
1173 }
1174 }
1175 }
1176
1177 // Whether any position of each element was populated — which is exactly
1178 // ISO 9735-1 §8.1's definition of a composite being "present".
1179 let element_populated: Vec<bool> = populated
1180 .iter()
1181 .map(|components| components.iter().any(|seen| *seen))
1182 .collect();
1183
1184 for slot in slots {
1185 if populated[slot.element][slot.component_index()] {
1186 continue;
1187 }
1188 // §8.6: "A mandatory component data element in a composite data element
1189 // shall be present **if the composite data element is present**." A
1190 // conditional composite that the corpus never carries therefore says
1191 // nothing about its mandatory components — reporting them as violations
1192 // would condemn every optional composite in the definition.
1193 let required_here = slot.status == Status::Mandatory
1194 && (slot.component.is_none()
1195 || slot.element_status == Status::Mandatory
1196 || element_populated[slot.element]);
1197 audit.findings.push(if required_here {
1198 LayoutFinding::MandatoryNeverPopulated { slot: slot.clone() }
1199 } else {
1200 LayoutFinding::NeverObserved { slot: slot.clone() }
1201 });
1202 }
1203
1204 audit
1205}
1206
1207/// Record a finding unless an equivalent one is already present.
1208///
1209/// A corpus of 360 fixtures would otherwise report the same undeclared
1210/// component 360 times, burying every other finding.
1211fn push_once(findings: &mut Vec<LayoutFinding>, finding: LayoutFinding) {
1212 let duplicate = findings.iter().any(|existing| match (existing, &finding) {
1213 (
1214 LayoutFinding::UndeclaredElement { element: a, .. },
1215 LayoutFinding::UndeclaredElement { element: b, .. },
1216 ) => a == b,
1217 (
1218 LayoutFinding::UndeclaredComponent {
1219 element: a,
1220 component: c,
1221 ..
1222 },
1223 LayoutFinding::UndeclaredComponent {
1224 element: b,
1225 component: d,
1226 ..
1227 },
1228 ) => a == b && c == d,
1229 _ => false,
1230 });
1231 if !duplicate {
1232 findings.push(finding);
1233 }
1234}
1235
1236impl SegmentDefinition {
1237 /// Create a segment definition.
1238 ///
1239 /// `const` so directory tables can still be built at compile time despite
1240 /// the `#[non_exhaustive]` attribute blocking external struct literals.
1241 #[must_use]
1242 pub const fn new(
1243 tag: &'static str,
1244 name: &'static str,
1245 elements: &'static [ElementRef],
1246 ) -> Self {
1247 Self {
1248 tag,
1249 name,
1250 elements,
1251 }
1252 }
1253
1254 /// Number of positions in this definition that carry `data_element`.
1255 ///
1256 /// `0` means unknown, `1` means unambiguously addressable, and anything
1257 /// larger means the identifier is repeated and cannot be code-addressed.
1258 /// `const`, so a derive macro can assert on it at compile time.
1259 ///
1260 /// # Example
1261 ///
1262 /// ```rust
1263 /// # use edifact_rs::{ElementRef, SegmentDefinition, Status};
1264 /// # static E: &[ElementRef] = &[ElementRef::new(1, "3035", Status::Mandatory, 1)];
1265 /// static NAD: SegmentDefinition = SegmentDefinition::new("NAD", "Name and address", E);
1266 /// const _: () = assert!(NAD.code_positions("3035") == 1);
1267 /// ```
1268 #[must_use]
1269 pub const fn code_positions(&self, data_element: &str) -> usize {
1270 let mut hits = 0;
1271 let mut i = 0;
1272 while i < self.elements.len() {
1273 let el = &self.elements[i];
1274 if const_str_eq(el.data_element, data_element) {
1275 hits += 1;
1276 }
1277 let mut c = 0;
1278 while c < el.components.len() {
1279 if const_str_eq(el.components[c].data_element, data_element) {
1280 hits += 1;
1281 }
1282 c += 1;
1283 }
1284 i += 1;
1285 }
1286 hits
1287 }
1288
1289 /// Zero-based element index for `data_element`, resolved at compile time.
1290 ///
1291 /// # Panics
1292 ///
1293 /// Panics when the identifier is unknown or appears at more than one
1294 /// position. In a `const` context — which is how the derive macro uses it —
1295 /// that panic is a **compile error**, so a mistyped identifier can never
1296 /// reach runtime. Guard with [`code_positions`][Self::code_positions] for a
1297 /// message that names the offending field.
1298 #[must_use]
1299 pub const fn element_slot(&self, data_element: &str) -> usize {
1300 // Two asserts rather than one: a const panic message cannot be
1301 // formatted, so naming the identifier is impossible — but saying which
1302 // of the two problems occurred is not, and it is the part that decides
1303 // what the author has to change.
1304 assert!(
1305 self.code_positions(data_element) != 0,
1306 "this segment definition declares no such data element identifier — check it against the directory"
1307 );
1308 assert!(
1309 self.code_positions(data_element) == 1,
1310 "this data element identifier is declared at more than one position; address it positionally, or declare the repeat with ComponentRef::repeated"
1311 );
1312 let mut i = 0;
1313 while i < self.elements.len() {
1314 let el = &self.elements[i];
1315 if const_str_eq(el.data_element, data_element) {
1316 return el.position as usize - 1;
1317 }
1318 let mut c = 0;
1319 while c < el.components.len() {
1320 if const_str_eq(el.components[c].data_element, data_element) {
1321 return el.position as usize - 1;
1322 }
1323 c += 1;
1324 }
1325 i += 1;
1326 }
1327 unreachable!()
1328 }
1329
1330 /// Zero-based component index for `data_element`, resolved at compile time.
1331 ///
1332 /// Returns `0` when the identifier names a data element rather than a
1333 /// component inside a composite — component 0 is the first (and for a simple
1334 /// element, only) component, so the same accessor works for both shapes.
1335 ///
1336 /// # Panics
1337 ///
1338 /// Panics when the identifier is unknown or appears at more than one
1339 /// position; see [`element_slot`][Self::element_slot].
1340 #[must_use]
1341 pub const fn component_slot(&self, data_element: &str) -> usize {
1342 assert!(
1343 self.code_positions(data_element) != 0,
1344 "this segment definition declares no such data element identifier — check it against the directory"
1345 );
1346 assert!(
1347 self.code_positions(data_element) == 1,
1348 "this data element identifier is declared at more than one position; address it positionally, or declare the repeat with ComponentRef::repeated"
1349 );
1350 let mut i = 0;
1351 while i < self.elements.len() {
1352 let el = &self.elements[i];
1353 if const_str_eq(el.data_element, data_element) {
1354 return 0;
1355 }
1356 let mut c = 0;
1357 while c < el.components.len() {
1358 if const_str_eq(el.components[c].data_element, data_element) {
1359 return el.components[c].position as usize - 1;
1360 }
1361 c += 1;
1362 }
1363 i += 1;
1364 }
1365 unreachable!()
1366 }
1367
1368 /// `true` when `data_element` names a component *inside* a composite rather
1369 /// than a data element of the segment.
1370 ///
1371 /// Lets a caller — the derive macro, in practice — pick the right
1372 /// "missing required" error variant without a second lookup:
1373 /// [`EdifactError::MissingRequiredComponent`] rather than
1374 /// [`EdifactError::MissingRequiredElement`]. `component_slot` alone cannot
1375 /// answer this, because a code naming the *first* component of a composite
1376 /// also resolves to component index 0.
1377 ///
1378 /// Returns `false` for an unknown identifier; pair with
1379 /// [`code_positions`][Self::code_positions] when that case matters.
1380 #[must_use]
1381 pub const fn code_is_component(&self, data_element: &str) -> bool {
1382 let mut i = 0;
1383 while i < self.elements.len() {
1384 let el = &self.elements[i];
1385 let mut c = 0;
1386 while c < el.components.len() {
1387 if const_str_eq(el.components[c].data_element, data_element) {
1388 return true;
1389 }
1390 c += 1;
1391 }
1392 i += 1;
1393 }
1394 false
1395 }
1396}
1397
1398impl SegmentLayout for SegmentDefinition {
1399 #[inline]
1400 fn layout_tag(&self) -> &str {
1401 self.tag
1402 }
1403
1404 fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError> {
1405 // One pass, not four: this runs per lookup on hot validation paths, and
1406 // composing the `const` helpers would rescan the table for each of the
1407 // count, the element index, and the component index.
1408 let mut hits = 0usize;
1409 let mut found = None;
1410 for el in self.elements {
1411 if el.data_element == data_element {
1412 hits += 1;
1413 found.get_or_insert(ElementPath::element(el.position as usize - 1));
1414 }
1415 for comp in el.components {
1416 if comp.data_element == data_element {
1417 hits += 1;
1418 found.get_or_insert(ElementPath::component(
1419 el.position as usize - 1,
1420 comp.position as usize - 1,
1421 ));
1422 }
1423 }
1424 }
1425 resolve_outcome(self.tag, data_element, hits, found)
1426 }
1427
1428 fn slots(&self) -> Vec<LayoutSlot> {
1429 let mut out = Vec::new();
1430 for element in self.elements {
1431 if element.components.is_empty() {
1432 out.push(LayoutSlot {
1433 element: element.position as usize - 1,
1434 component: None,
1435 data_element: element.data_element.to_owned(),
1436 status: element.status,
1437 element_status: element.status,
1438 });
1439 continue;
1440 }
1441 for component in element.components {
1442 out.push(LayoutSlot {
1443 element: element.position as usize - 1,
1444 component: Some(component.position as usize - 1),
1445 data_element: component.data_element.to_owned(),
1446 status: component.status,
1447 element_status: element.status,
1448 });
1449 }
1450 }
1451 out
1452 }
1453}
1454
1455/// Turn a resolution scan's `(hit count, first match)` into a `Result`.
1456///
1457/// Shared by both [`SegmentLayout`] impls so the static and runtime tables
1458/// cannot drift on which condition maps to which error.
1459fn resolve_outcome(
1460 tag: &str,
1461 data_element: &str,
1462 hits: usize,
1463 found: Option<ElementPath>,
1464) -> Result<ElementPath, EdifactError> {
1465 match (hits, found) {
1466 (1, Some(path)) => Ok(path),
1467 (0, _) => Err(EdifactError::UnknownDataElement {
1468 tag: tag.to_owned(),
1469 data_element: data_element.to_owned(),
1470 }),
1471 _ => Err(EdifactError::AmbiguousDataElement {
1472 tag: tag.to_owned(),
1473 data_element: data_element.to_owned(),
1474 }),
1475 }
1476}
1477
1478/// Owned runtime equivalent of [`ElementRef`].
1479///
1480/// Used by [`DirectoryValidatorBuilder`] and [`DirectoryValidator::from_owned_definitions`]
1481/// to construct validators from data that is not available at compile time (e.g. loaded
1482/// from JSON or a database at startup).
1483///
1484/// Use [`OwnedElementRef::new_unchecked`] for compile-time-known positions (panics on invalid
1485/// input, no error handling noise) or [`OwnedElementRef::try_new`] when the position
1486/// comes from an external source and you need a `Result`. Fields are private to prevent
1487/// bypassing the position invariant through struct-literal syntax.
1488#[derive(Debug, Clone)]
1489pub struct OwnedElementRef {
1490 /// One-based element position.
1491 position: u8,
1492 /// UN/EDIFACT data element identifier.
1493 data_element: String,
1494 /// Requirement status.
1495 status: Status,
1496 /// Maximum repetition count.
1497 max_repeat: u8,
1498 /// The directory's representation for a *simple* element.
1499 repr: Option<Repr>,
1500 /// The representation from syntax version 4 onward, where it differs.
1501 repr_from_v4: Option<Repr>,
1502 /// Component definitions when this element is a composite; empty for a
1503 /// simple data element.
1504 components: Vec<OwnedComponentRef>,
1505}
1506
1507/// Owned runtime equivalent of [`ComponentRef`].
1508///
1509/// Attach these to an [`OwnedElementRef`] with
1510/// [`OwnedElementRef::with_components`] so that runtime-loaded definitions
1511/// support code-addressed access into composites, exactly like compile-time
1512/// [`SegmentDefinition`] tables do.
1513#[derive(Debug, Clone)]
1514pub struct OwnedComponentRef {
1515 /// One-based position of the first slot this component occupies.
1516 position: u8,
1517 /// UN/EDIFACT component data element identifier.
1518 data_element: String,
1519 /// Requirement status.
1520 status: Status,
1521 /// The directory's representation, when the definition states one.
1522 repr: Option<Repr>,
1523 /// The representation from syntax version 4 onward, where it differs.
1524 repr_from_v4: Option<Repr>,
1525 /// How many consecutive slots this component occupies.
1526 repeat_count: u8,
1527}
1528
1529impl OwnedComponentRef {
1530 /// Construct an owned component reference.
1531 ///
1532 /// # Panics
1533 ///
1534 /// Panics if `position` is `0` (positions are one-based).
1535 pub fn new_unchecked(position: u8, data_element: String, status: Status) -> Self {
1536 assert!(
1537 position != 0,
1538 "OwnedComponentRef::new_unchecked: position must be >= 1 (one-based), got 0"
1539 );
1540 Self {
1541 position,
1542 data_element,
1543 status,
1544 repeat_count: 1,
1545 repr: None,
1546 repr_from_v4: None,
1547 }
1548 }
1549
1550 /// Runtime counterpart of [`ComponentRef::repeated`].
1551 ///
1552 /// # Panics
1553 ///
1554 /// Panics if `position == 0` or `repeat_count == 0`.
1555 #[must_use]
1556 pub fn repeated(position: u8, data_element: String, status: Status, repeat_count: u8) -> Self {
1557 assert!(
1558 position != 0,
1559 "OwnedComponentRef::repeated: position must be >= 1 (one-based), got 0"
1560 );
1561 assert!(
1562 repeat_count != 0,
1563 "OwnedComponentRef::repeated: repeat_count must be >= 1"
1564 );
1565 Self {
1566 position,
1567 data_element,
1568 status,
1569 repeat_count,
1570 repr: None,
1571 repr_from_v4: None,
1572 }
1573 }
1574
1575 /// How many consecutive slots this component occupies.
1576 #[inline]
1577 #[must_use]
1578 pub fn repeat_count(&self) -> u8 {
1579 self.repeat_count
1580 }
1581
1582 /// Construct an owned component reference, returning an error for position `0`.
1583 ///
1584 /// # Errors
1585 ///
1586 /// Returns [`EdifactError::InvalidElementPosition`] if `position` is `0`.
1587 pub fn try_new(
1588 position: u8,
1589 data_element: String,
1590 status: Status,
1591 ) -> Result<Self, EdifactError> {
1592 if position == 0 {
1593 return Err(EdifactError::InvalidElementPosition);
1594 }
1595 Ok(Self {
1596 position,
1597 data_element,
1598 status,
1599 repeat_count: 1,
1600 repr: None,
1601 repr_from_v4: None,
1602 })
1603 }
1604
1605 /// One-based component position (always >= 1).
1606 #[inline]
1607 pub fn position(&self) -> u8 {
1608 self.position
1609 }
1610
1611 /// UN/EDIFACT component data element identifier.
1612 #[inline]
1613 pub fn data_element(&self) -> &str {
1614 &self.data_element
1615 }
1616
1617 /// Requirement status of this component.
1618 #[inline]
1619 pub fn status(&self) -> Status {
1620 self.status
1621 }
1622
1623 /// Attach the directory's representation, e.g. `an..35`.
1624 #[must_use]
1625 pub fn with_repr(mut self, repr: Repr) -> Self {
1626 self.repr = Some(repr);
1627 self
1628 }
1629
1630 /// The declared representation, if the definition states one.
1631 #[inline]
1632 #[must_use]
1633 pub fn repr(&self) -> Option<Repr> {
1634 self.repr
1635 }
1636
1637 /// Declare a representation that changed between syntax versions.
1638 ///
1639 /// See [`ComponentRef::with_repr_by_syntax_version`].
1640 #[must_use]
1641 pub fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
1642 self.repr = Some(up_to_v3);
1643 self.repr_from_v4 = Some(from_v4);
1644 self
1645 }
1646
1647 /// The representation used from syntax version 4 onward, when it differs.
1648 #[inline]
1649 #[must_use]
1650 pub fn repr_from_v4(&self) -> Option<Repr> {
1651 self.repr_from_v4
1652 }
1653}
1654
1655/// Owned runtime equivalent of [`SegmentDefinition`].
1656///
1657/// Used by [`DirectoryValidatorBuilder`] and [`DirectoryValidator::from_owned_definitions`].
1658///
1659/// Use [`OwnedSegmentDef::new_unchecked`] for compile-time-known tags (panics on invalid input,
1660/// no error handling noise) or [`OwnedSegmentDef::try_new`] when the tag comes from
1661/// an external source and you need a `Result`. Fields are private to prevent bypassing
1662/// the tag invariant through struct-literal syntax.
1663#[derive(Debug, Clone)]
1664pub struct OwnedSegmentDef {
1665 /// Segment tag (e.g. `"BGM"`).
1666 tag: String,
1667 /// Human-readable segment name.
1668 name: String,
1669 /// Ordered element definitions.
1670 elements: Vec<OwnedElementRef>,
1671}
1672
1673impl OwnedSegmentDef {
1674 /// Construct an owned segment definition.
1675 ///
1676 /// This is the ergonomic constructor for compile-time-known tags (e.g.
1677 /// `"BGM"`, `"UNH"`). It panics immediately on invalid input so that
1678 /// call sites with literal tag strings require no `.unwrap()` / `.expect()`
1679 /// boilerplate.
1680 ///
1681 /// Use [`try_new`][Self::try_new] instead when the tag originates from an
1682 /// external source (user input, config file, database) and you need a
1683 /// `Result` to propagate errors gracefully.
1684 ///
1685 /// # Panics
1686 ///
1687 /// Panics if `tag` is not exactly three ASCII uppercase letters.
1688 pub fn new_unchecked(tag: String, name: String, elements: Vec<OwnedElementRef>) -> Self {
1689 assert!(
1690 tag.len() == 3 && tag.bytes().all(|b| b.is_ascii_uppercase()),
1691 "OwnedSegmentDef::new_unchecked: tag must be exactly three ASCII uppercase letters, got {tag:?}"
1692 );
1693 Self {
1694 tag,
1695 name,
1696 elements,
1697 }
1698 }
1699
1700 /// Construct an owned segment definition, returning an error for invalid tags.
1701 ///
1702 /// Prefer this over [`new_unchecked`][Self::new_unchecked] when the tag comes from an external
1703 /// source (user input, config file, database) and you want to handle the
1704 /// error without panicking.
1705 ///
1706 /// # Errors
1707 ///
1708 /// Returns [`EdifactError::InvalidSegmentTag`] if `tag` is not exactly three
1709 /// ASCII uppercase letters.
1710 pub fn try_new(
1711 tag: String,
1712 name: String,
1713 elements: Vec<OwnedElementRef>,
1714 ) -> Result<Self, EdifactError> {
1715 if tag.len() != 3 || !tag.bytes().all(|b| b.is_ascii_uppercase()) {
1716 return Err(EdifactError::InvalidSegmentTag(tag));
1717 }
1718 Ok(Self {
1719 tag,
1720 name,
1721 elements,
1722 })
1723 }
1724
1725 /// Segment tag (e.g. `"BGM"`).
1726 #[inline]
1727 pub fn tag(&self) -> &str {
1728 &self.tag
1729 }
1730
1731 /// Human-readable segment name.
1732 #[inline]
1733 pub fn name(&self) -> &str {
1734 &self.name
1735 }
1736
1737 /// Element definitions for this segment.
1738 #[inline]
1739 pub fn elements(&self) -> &[OwnedElementRef] {
1740 &self.elements
1741 }
1742
1743 /// Number of positions in this definition that carry `data_element`.
1744 ///
1745 /// Runtime counterpart of [`SegmentDefinition::code_positions`].
1746 #[must_use]
1747 pub fn code_positions(&self, data_element: &str) -> usize {
1748 self.elements
1749 .iter()
1750 .map(|el| {
1751 usize::from(el.data_element == data_element)
1752 + el.components
1753 .iter()
1754 .filter(|c| c.data_element == data_element)
1755 .count()
1756 })
1757 .sum()
1758 }
1759}
1760
1761impl SegmentLayout for OwnedSegmentDef {
1762 #[inline]
1763 fn layout_tag(&self) -> &str {
1764 &self.tag
1765 }
1766
1767 fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError> {
1768 let mut hits = 0usize;
1769 let mut found = None;
1770 for el in &self.elements {
1771 if el.data_element == data_element {
1772 hits += 1;
1773 found.get_or_insert(ElementPath::element(el.position as usize - 1));
1774 }
1775 for comp in &el.components {
1776 if comp.data_element == data_element {
1777 hits += 1;
1778 found.get_or_insert(ElementPath::component(
1779 el.position as usize - 1,
1780 comp.position as usize - 1,
1781 ));
1782 }
1783 }
1784 }
1785 resolve_outcome(&self.tag, data_element, hits, found)
1786 }
1787
1788 fn slots(&self) -> Vec<LayoutSlot> {
1789 let mut out = Vec::new();
1790 for element in &self.elements {
1791 if element.components.is_empty() {
1792 out.push(LayoutSlot {
1793 element: element.position as usize - 1,
1794 component: None,
1795 data_element: element.data_element.clone(),
1796 status: element.status,
1797 element_status: element.status,
1798 });
1799 continue;
1800 }
1801 for component in &element.components {
1802 out.push(LayoutSlot {
1803 element: element.position as usize - 1,
1804 component: Some(component.position as usize - 1),
1805 data_element: component.data_element.clone(),
1806 status: component.status,
1807 element_status: element.status,
1808 });
1809 }
1810 }
1811 out
1812 }
1813}
1814
1815impl OwnedElementRef {
1816 /// Construct an owned element reference.
1817 ///
1818 /// This is the ergonomic constructor for compile-time-known positions.
1819 /// It panics immediately on invalid input so that call sites with literal
1820 /// position numbers require no `.unwrap()` / `.expect()` boilerplate.
1821 ///
1822 /// Use [`try_new`][Self::try_new] instead when the position originates from
1823 /// an external source (user input, config file, database) and you need a
1824 /// `Result` to propagate errors gracefully.
1825 ///
1826 /// # Panics
1827 ///
1828 /// Panics if `position` is `0` (positions are one-based).
1829 pub fn new_unchecked(
1830 position: u8,
1831 data_element: String,
1832 status: Status,
1833 max_repeat: u8,
1834 ) -> Self {
1835 assert!(
1836 position != 0,
1837 "OwnedElementRef::new_unchecked: position must be >= 1 (one-based), got 0"
1838 );
1839 Self {
1840 position,
1841 data_element,
1842 status,
1843 max_repeat,
1844 repr: None,
1845 repr_from_v4: None,
1846 components: Vec::new(),
1847 }
1848 }
1849
1850 /// Construct an owned element reference, returning an error for position `0`.
1851 ///
1852 /// Prefer this over [`new_unchecked`][Self::new_unchecked] when the position comes from an
1853 /// external source (user input, config file, database) and you want to
1854 /// handle the error without panicking.
1855 ///
1856 /// # Errors
1857 ///
1858 /// Returns [`EdifactError::InvalidElementPosition`] if `position` is `0`.
1859 pub fn try_new(
1860 position: u8,
1861 data_element: String,
1862 status: Status,
1863 max_repeat: u8,
1864 ) -> Result<Self, EdifactError> {
1865 if position == 0 {
1866 return Err(EdifactError::InvalidElementPosition);
1867 }
1868 Ok(Self {
1869 position,
1870 data_element,
1871 status,
1872 max_repeat,
1873 repr: None,
1874 repr_from_v4: None,
1875 components: Vec::new(),
1876 })
1877 }
1878
1879 /// Attach component definitions, marking this element as a composite.
1880 ///
1881 /// Declared components make code-addressed access resolve *into* the
1882 /// composite and activate the mandatory-component check in
1883 /// [`DirectoryValidator`].
1884 ///
1885 /// # Example
1886 ///
1887 /// ```rust
1888 /// use edifact_rs::{OwnedComponentRef, OwnedElementRef, Status};
1889 ///
1890 /// let dtm = OwnedElementRef::new_unchecked(1, "C507".to_owned(), Status::Mandatory, 1)
1891 /// .with_components(vec![
1892 /// OwnedComponentRef::new_unchecked(1, "2005".to_owned(), Status::Mandatory),
1893 /// OwnedComponentRef::new_unchecked(2, "2380".to_owned(), Status::Conditional),
1894 /// ]);
1895 /// assert_eq!(dtm.components().len(), 2);
1896 /// ```
1897 #[must_use]
1898 pub fn with_components(mut self, components: Vec<OwnedComponentRef>) -> Self {
1899 self.components = components;
1900 self
1901 }
1902
1903 /// Component definitions; empty when this is a simple data element.
1904 #[inline]
1905 pub fn components(&self) -> &[OwnedComponentRef] {
1906 &self.components
1907 }
1908
1909 /// One-based element position (always >= 1).
1910 #[inline]
1911 pub fn position(&self) -> u8 {
1912 self.position
1913 }
1914
1915 /// UN/EDIFACT data element identifier.
1916 #[inline]
1917 pub fn data_element(&self) -> &str {
1918 &self.data_element
1919 }
1920
1921 /// Requirement status of this element.
1922 #[inline]
1923 pub fn status(&self) -> Status {
1924 self.status
1925 }
1926
1927 /// Maximum repetition count for this element.
1928 #[inline]
1929 pub fn max_repeat(&self) -> u8 {
1930 self.max_repeat
1931 }
1932
1933 /// Attach the directory's representation for a simple data element.
1934 #[must_use]
1935 pub fn with_repr(mut self, repr: Repr) -> Self {
1936 self.repr = Some(repr);
1937 self
1938 }
1939
1940 /// The declared representation, if the definition states one.
1941 #[inline]
1942 #[must_use]
1943 pub fn repr(&self) -> Option<Repr> {
1944 self.repr
1945 }
1946
1947 /// Declare a representation that changed between syntax versions.
1948 ///
1949 /// See [`ComponentRef::with_repr_by_syntax_version`].
1950 #[must_use]
1951 pub fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
1952 self.repr = Some(up_to_v3);
1953 self.repr_from_v4 = Some(from_v4);
1954 self
1955 }
1956
1957 /// The representation used from syntax version 4 onward, when it differs.
1958 #[inline]
1959 #[must_use]
1960 pub fn repr_from_v4(&self) -> Option<Repr> {
1961 self.repr_from_v4
1962 }
1963}
1964
1965type SegmentLookupFn = Arc<dyn Fn(&str) -> Option<&'static SegmentDefinition> + Send + Sync>;
1966type IsCodeValidFn = Arc<dyn Fn(&str, &str) -> bool + Send + Sync>;
1967type SuggestCodeFn = Arc<dyn Fn(&str, &str) -> Option<&'static str> + Send + Sync>;
1968type ExpectedComponentsFn = Arc<dyn Fn(&str, usize) -> Option<u8> + Send + Sync>;
1969type AdditionalStructureRuleRefFn = fn(&Segment<'_>) -> Result<(), EdifactError>;
1970type AdditionalStructureRuleFn =
1971 Arc<dyn Fn(&Segment<'_>) -> Result<(), EdifactError> + Send + Sync>;
1972/// Returns the `(element_index, component_index, data_element_id)` tuples to
1973/// validate against a code list for the given segment tag.
1974type CodeListRulesFn = Arc<dyn Fn(&str) -> &'static [(usize, usize, &'static str)] + Send + Sync>;
1975/// Returns the mandatory segment tags for a given EDIFACT message type.
1976///
1977/// The slice should contain every tag that must appear at least once in a
1978/// conformant message of the given type. The tags are also used to check
1979/// canonical ordering — their relative order in the returned slice is taken
1980/// as the expected order in the message.
1981type RequiredSegmentsFn = Arc<dyn Fn(&str) -> &'static [&'static str] + Send + Sync>;
1982
1983/// Internal enum that unifies lookup results from static and owned segment definitions.
1984///
1985/// Allows `validate_segment` to handle both code-generated (`&'static`) and
1986/// runtime-constructed ([`OwnedSegmentDef`]) definitions without duplication.
1987enum SegmentDefRef<'a> {
1988 Static(&'static SegmentDefinition),
1989 Owned(&'a OwnedSegmentDef),
1990}
1991
1992impl SegmentDefRef<'_> {
1993 /// Returns the highest defined element position (one-based → used directly as
1994 /// the maximum zero-based slot count for element-count validation).
1995 ///
1996 /// For owned definitions the highest `position` value may exceed the number
1997 /// of entries in the `elements` vec when positions are non-consecutive.
1998 fn max_element_position(&self) -> usize {
1999 match self {
2000 Self::Static(d) => d
2001 .elements
2002 .iter()
2003 .map(|e| e.position as usize)
2004 .max()
2005 .unwrap_or(0),
2006 Self::Owned(d) => d
2007 .elements
2008 .iter()
2009 .map(|e| e.position as usize)
2010 .max()
2011 .unwrap_or(0),
2012 }
2013 }
2014
2015 /// Returns the highest position number among mandatory elements (one-based).
2016 ///
2017 /// This equals the minimum number of elements that must be present in a
2018 /// segment: if the highest-positioned mandatory element is at position 5,
2019 /// the segment must supply at least 5 elements.
2020 fn last_mandatory_position(&self) -> usize {
2021 match self {
2022 Self::Static(d) => d
2023 .elements
2024 .iter()
2025 .filter(|e| e.status == Status::Mandatory)
2026 .map(|e| e.position as usize)
2027 .max()
2028 .unwrap_or(0),
2029 Self::Owned(d) => d
2030 .elements
2031 .iter()
2032 .filter(|e| e.status == Status::Mandatory)
2033 .map(|e| e.position as usize)
2034 .max()
2035 .unwrap_or(0),
2036 }
2037 }
2038
2039 /// Iterate over mandatory element positions without heap allocation.
2040 ///
2041 /// Calls `f(zero_based_index, data_element_id)` for each element whose
2042 /// status is [`Status::Mandatory`]. Returns `Err` immediately if `f`
2043 /// returns `Err`, short-circuiting the remaining elements.
2044 fn for_each_mandatory_position<E, F>(&self, mut f: F) -> Result<(), E>
2045 where
2046 F: FnMut(usize, &str) -> Result<(), E>,
2047 {
2048 match self {
2049 Self::Static(d) => {
2050 for e in d.elements.iter().filter(|e| e.status == Status::Mandatory) {
2051 f((e.position as usize).saturating_sub(1), e.data_element)?;
2052 }
2053 }
2054 Self::Owned(d) => {
2055 for e in d.elements.iter().filter(|e| e.status == Status::Mandatory) {
2056 f(
2057 (e.position as usize).saturating_sub(1),
2058 e.data_element.as_str(),
2059 )?;
2060 }
2061 }
2062 }
2063 Ok(())
2064 }
2065
2066 /// Iterate over mandatory *component* positions without heap allocation.
2067 ///
2068 /// Calls `f(element_index, component_index, data_element_id)` — both indices
2069 /// zero-based — for every declared component whose status is
2070 /// [`Status::Mandatory`]. Definitions that declare no components (the shape
2071 /// every pre-0.13 directory table had) yield nothing, so this check is
2072 /// inert until a directory opts in by declaring composites with
2073 /// [`ElementRef::composite`].
2074 fn for_each_mandatory_component<E, F>(&self, mut f: F) -> Result<(), E>
2075 where
2076 F: FnMut(usize, usize, &str) -> Result<(), E>,
2077 {
2078 match self {
2079 Self::Static(d) => {
2080 for e in d.elements {
2081 for c in e
2082 .components
2083 .iter()
2084 .filter(|c| c.status == Status::Mandatory)
2085 {
2086 f(
2087 (e.position as usize).saturating_sub(1),
2088 (c.position as usize).saturating_sub(1),
2089 c.data_element,
2090 )?;
2091 }
2092 }
2093 }
2094 Self::Owned(d) => {
2095 for e in &d.elements {
2096 for c in e
2097 .components
2098 .iter()
2099 .filter(|c| c.status == Status::Mandatory)
2100 {
2101 f(
2102 (e.position as usize).saturating_sub(1),
2103 (c.position as usize).saturating_sub(1),
2104 c.data_element.as_str(),
2105 )?;
2106 }
2107 }
2108 }
2109 }
2110 Ok(())
2111 }
2112
2113 /// Number of declared component **slots** for the element at zero-based `index`.
2114 ///
2115 /// A component declared with [`ComponentRef::repeated`] occupies several
2116 /// slots, so this sums repeat counts rather than counting entries: counting
2117 /// entries would cap `C080` at two components and reject the four extra
2118 /// `3036` occurrences the composite is defined to carry.
2119 ///
2120 /// `None` when the element is not defined, or is defined without
2121 /// components — in which case its arity is not constrained by the layout.
2122 /// The declared maximum occurrence count for the element at `index`.
2123 fn max_repeat_at(&self, index: usize) -> Option<u8> {
2124 let position = u8::try_from(index.checked_add(1)?).ok()?;
2125 match self {
2126 Self::Static(d) => d
2127 .elements
2128 .iter()
2129 .find(|e| e.position == position)
2130 .map(ElementRef::max_repeat),
2131 Self::Owned(d) => d
2132 .elements
2133 .iter()
2134 .find(|e| e.position == position)
2135 .map(OwnedElementRef::max_repeat),
2136 }
2137 }
2138
2139 /// The representation required at `(element, component)`, if any.
2140 ///
2141 /// A composite states its representations on the components; a simple data
2142 /// element states one on the element itself and only at component 0.
2143 ///
2144 /// `syntax_version` selects between the two forms of a position that changed
2145 /// between versions; `None` accepts either, because guessing would reject
2146 /// conformant data.
2147 fn repr_at(
2148 &self,
2149 element: usize,
2150 component: usize,
2151 syntax_version: Option<u8>,
2152 ) -> Option<ReprRequirement> {
2153 let position = u8::try_from(element.checked_add(1)?).ok()?;
2154 let component_position = u8::try_from(component.checked_add(1)?).ok()?;
2155 match self {
2156 Self::Static(d) => {
2157 let element = d.elements.iter().find(|e| e.position == position)?;
2158 if element.components.is_empty() {
2159 return if component == 0 {
2160 select_repr(element.repr(), element.repr_from_v4(), syntax_version)
2161 } else {
2162 None
2163 };
2164 }
2165 let component_ref = element.components.iter().find(|c| {
2166 // A component declared with `repeated` spans several
2167 // consecutive slots, all of the same representation.
2168 let first = c.position();
2169 let last = first.saturating_add(c.repeat_count().saturating_sub(1));
2170 (first..=last).contains(&component_position)
2171 })?;
2172 select_repr(
2173 component_ref.repr(),
2174 component_ref.repr_from_v4(),
2175 syntax_version,
2176 )
2177 }
2178 Self::Owned(d) => {
2179 let element = d.elements.iter().find(|e| e.position == position)?;
2180 if element.components.is_empty() {
2181 return if component == 0 {
2182 select_repr(
2183 OwnedElementRef::repr(element),
2184 OwnedElementRef::repr_from_v4(element),
2185 syntax_version,
2186 )
2187 } else {
2188 None
2189 };
2190 }
2191 let component_ref = element.components.iter().find(|c| {
2192 let first = c.position();
2193 let last = first.saturating_add(c.repeat_count().saturating_sub(1));
2194 (first..=last).contains(&component_position)
2195 })?;
2196 select_repr(
2197 component_ref.repr(),
2198 component_ref.repr_from_v4(),
2199 syntax_version,
2200 )
2201 }
2202 }
2203 }
2204
2205 fn declared_component_count(&self, index: usize) -> Option<u8> {
2206 let position = u8::try_from(index.checked_add(1)?).ok()?;
2207 let count: u32 = match self {
2208 Self::Static(d) => d
2209 .elements
2210 .iter()
2211 .find(|e| e.position == position)
2212 .map(|e| e.components.iter().map(|c| u32::from(c.repeat_count)).sum())?,
2213 Self::Owned(d) => d
2214 .elements
2215 .iter()
2216 .find(|e| e.position == position)
2217 .map(|e| e.components.iter().map(|c| u32::from(c.repeat_count)).sum())?,
2218 };
2219 if count == 0 {
2220 return None;
2221 }
2222 u8::try_from(count).ok()
2223 }
2224}
2225
2226/// Read the syntax version number from `UNB` S001 DE 0002.
2227///
2228/// `None` when the slice carries no readable `UNB` — a message window, say.
2229fn detect_syntax_version(segments: &[Segment<'_>]) -> Option<u8> {
2230 segments
2231 .iter()
2232 .find(|s| s.tag == "UNB")
2233 .and_then(|unb| unb.component_str(0, 1))
2234 .and_then(|version| version.parse().ok())
2235}
2236
2237/// Pick the representation that applies to `syntax_version`.
2238///
2239/// Version 4 onward uses `from_v4` when the definition states one. An unknown
2240/// version accepts either, because a definition that distinguishes them is
2241/// distinguishing a real incompatibility — guessing would reject conformant data
2242/// from whichever version we guessed against.
2243fn select_repr(
2244 base: Option<Repr>,
2245 from_v4: Option<Repr>,
2246 syntax_version: Option<u8>,
2247) -> Option<ReprRequirement> {
2248 match (base, from_v4) {
2249 (_, None) => base.map(ReprRequirement::single),
2250 (None, Some(v4)) => Some(ReprRequirement::single(v4)),
2251 (Some(base), Some(v4)) => Some(match syntax_version {
2252 Some(version) if version >= 4 => ReprRequirement::single(v4),
2253 Some(_) => ReprRequirement::single(base),
2254 None => ReprRequirement {
2255 primary: base,
2256 alternative: Some(v4),
2257 },
2258 }),
2259 }
2260}
2261
2262/// Default required-segments mapping used when no custom function is provided.
2263///
2264/// Returns the universal minimum: every EDIFACT message must begin with `UNH`
2265/// and end with `UNT`. Message-type-specific mandatory segments (such as
2266/// `BGM` for ORDERS/INVOIC) must be enforced by a
2267/// [`ProfileRulePack`][crate::ProfileRulePack] or a custom
2268/// [`DirectoryValidatorBuilder::with_required_segments`] function to avoid
2269/// false positives for message types that do not require `BGM`.
2270fn default_required_segments(_message_type: &str) -> &'static [&'static str] {
2271 &["UNH", "UNT"]
2272}
2273
2274/// Code-list validation rules common to all UN/EDIFACT directory releases.
2275///
2276/// Each entry is `(element_index, component_index, data_element_id)`.
2277/// `element_index` and `component_index` are zero-based.
2278///
2279/// Covers the most frequently validated qualifier/code elements across ORDERS,
2280/// INVOIC, and similar message types.
2281pub(crate) fn base_code_list_rules(tag: &str) -> &'static [(usize, usize, &'static str)] {
2282 match tag {
2283 "BGM" => &[(0, 0, "1001")],
2284 "DTM" => &[(0, 0, "2005")],
2285 "NAD" => &[(0, 0, "3035")],
2286 "QTY" => &[(0, 0, "6063")],
2287 "RFF" => &[(0, 0, "1153")],
2288 "MOA" => &[(0, 0, "5025")],
2289 "PRI" => &[(0, 0, "5125")],
2290 "LOC" => &[(0, 0, "3227")],
2291 _ => &[],
2292 }
2293}
2294
2295/// Shared validator implementation that is configured per UN/EDIFACT directory release.
2296///
2297/// # Scope and limitations
2298///
2299/// `DirectoryValidator` validates individual segment *content* (element counts,
2300/// component counts, code-list values, and conditional rules) and checks that
2301/// every *mandatory* segment type is present at least once. It does **not**
2302/// validate segment *sequence* or *repetition cardinality* — i.e., it cannot
2303/// tell you that a `BGM` segment appears more than once, or that a `RFF` group
2304/// appears in the wrong position. Full sequence validation requires a
2305/// state-machine per message type (UN/EDIFACT Segment Tables) which is outside
2306/// the scope of this implementation.
2307#[derive(Clone)]
2308pub struct DirectoryValidator {
2309 directory_id: String,
2310 segment_lookup: SegmentLookupFn,
2311 /// Runtime-owned segment definitions (from builder / JSON / DB).
2312 ///
2313 /// When `Some`, takes precedence over `segment_lookup` for tag resolution.
2314 owned_defs: Option<Arc<Vec<OwnedSegmentDef>>>,
2315 /// Tag -> index into `owned_defs`. Without this, `resolve_def` was a linear
2316 /// scan per segment, making validation O(n_segments x n_definitions).
2317 owned_index: Option<Arc<std::collections::HashMap<String, usize>>>,
2318 is_code_valid: IsCodeValidFn,
2319 suggest_code: SuggestCodeFn,
2320 expected_components: ExpectedComponentsFn,
2321 code_list_rules: CodeListRulesFn,
2322 additional_structure_rule: Option<AdditionalStructureRuleFn>,
2323 /// Configurable mapping from message type to required segment tags.
2324 required_segments: RequiredSegmentsFn,
2325 message_type: Option<String>,
2326 enforce_known_tags: bool,
2327 structure_checks: bool,
2328 code_list_checks: bool,
2329}
2330
2331impl std::fmt::Debug for DirectoryValidator {
2332 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2333 f.debug_struct("DirectoryValidator")
2334 .field("directory_id", &self.directory_id)
2335 .field("message_type", &self.message_type)
2336 .field("enforce_known_tags", &self.enforce_known_tags)
2337 .field("structure_checks", &self.structure_checks)
2338 .field("code_list_checks", &self.code_list_checks)
2339 .finish_non_exhaustive()
2340 }
2341}
2342
2343impl DirectoryValidator {
2344 /// Create a validator for a specific directory release with injected lookup/check hooks.
2345 pub fn new(
2346 directory_id: &'static str,
2347 segment_lookup: fn(&str) -> Option<&'static SegmentDefinition>,
2348 is_code_valid: fn(&str, &str) -> bool,
2349 suggest_code: fn(&str, &str) -> Option<&'static str>,
2350 expected_components: fn(&str, usize) -> Option<u8>,
2351 additional_structure_rule: Option<AdditionalStructureRuleRefFn>,
2352 ) -> Self {
2353 Self {
2354 directory_id: directory_id.to_owned(),
2355 segment_lookup: Arc::new(segment_lookup),
2356 owned_defs: None,
2357 owned_index: None,
2358 is_code_valid: Arc::new(is_code_valid),
2359 suggest_code: Arc::new(suggest_code),
2360 expected_components: Arc::new(expected_components),
2361 code_list_rules: Arc::new(base_code_list_rules),
2362 additional_structure_rule: additional_structure_rule
2363 .map(|f| Arc::new(f) as AdditionalStructureRuleFn),
2364 required_segments: Arc::new(default_required_segments),
2365 message_type: None,
2366 enforce_known_tags: true,
2367 structure_checks: true,
2368 code_list_checks: true,
2369 }
2370 }
2371
2372 /// Create a validator from a static slice of [`SegmentDefinition`]s.
2373 ///
2374 /// This is the preferred constructor when code-generating directory data as
2375 /// a `static` array: no manual fn-pointer boilerplate is required.
2376 ///
2377 /// Code-list checks are **disabled** by default (the built-in `is_code_valid`
2378 /// always returns `true`). Call [`with_code_list_rules`][Self::with_code_list_rules]
2379 /// to register directory-specific rules that actually validate code values.
2380 ///
2381 /// # Example
2382 ///
2383 /// ```rust,ignore
2384 /// static MY_SEGMENTS: &[SegmentDefinition] = &[ /* … */ ];
2385 ///
2386 /// let validator = DirectoryValidator::from_definitions(MY_SEGMENTS)
2387 /// .with_code_list_rules(my_code_list_rules);
2388 /// ```
2389 pub fn from_definitions(definitions: &'static [SegmentDefinition]) -> Self {
2390 let lookup_map: std::collections::HashMap<&'static str, &'static SegmentDefinition> =
2391 definitions.iter().map(|d| (d.tag, d)).collect();
2392 let lookup_map = Arc::new(lookup_map);
2393 Self {
2394 directory_id: "custom".to_owned(),
2395 segment_lookup: Arc::new(move |tag: &str| lookup_map.get(tag).copied()),
2396 owned_defs: None,
2397 owned_index: None,
2398 is_code_valid: Arc::new(|_de: &str, _code: &str| true),
2399 suggest_code: Arc::new(|_de: &str, _code: &str| None),
2400 expected_components: Arc::new(|_tag: &str, _idx: usize| None),
2401 code_list_rules: Arc::new(base_code_list_rules),
2402 additional_structure_rule: None,
2403 required_segments: Arc::new(default_required_segments),
2404 message_type: None,
2405 enforce_known_tags: true,
2406 structure_checks: true,
2407 code_list_checks: false,
2408 }
2409 }
2410
2411 /// Create a validator from a runtime-owned collection of segment definitions.
2412 ///
2413 /// Use this (or [`DirectoryValidatorBuilder`]) when segment definitions are
2414 /// loaded from an external source at startup (JSON, database, YAML, …) rather
2415 /// than being known at compile time.
2416 ///
2417 /// Code-list checks are **disabled** by default; enable them by chaining
2418 /// [`with_code_list_rules`][Self::with_code_list_rules] and setting
2419 /// `is_code_valid` via a custom [`new`][Self::new] call or by subclassing
2420 /// the builder.
2421 ///
2422 /// # Example
2423 ///
2424 /// ```rust,ignore
2425 /// let defs = vec![
2426 /// OwnedSegmentDef::new_unchecked(
2427 /// "BGM".to_owned(),
2428 /// "Beginning of message".to_owned(),
2429 /// vec![OwnedElementRef::new_unchecked(1, "C002".to_owned(), Status::Mandatory, 1)],
2430 /// ),
2431 /// ];
2432 /// let validator = DirectoryValidator::from_owned_definitions(defs)
2433 /// .with_directory_id("runtime-profile");
2434 /// ```
2435 pub fn from_owned_definitions(definitions: Vec<OwnedSegmentDef>) -> Self {
2436 Self {
2437 directory_id: "custom".to_owned(),
2438 // The static lookup is never consulted when `owned_defs` is `Some`.
2439 segment_lookup: Arc::new(|_| None),
2440 owned_index: Some(Arc::new(
2441 definitions
2442 .iter()
2443 .enumerate()
2444 .map(|(i, d)| (d.tag.clone(), i))
2445 .collect(),
2446 )),
2447 owned_defs: Some(Arc::new(definitions)),
2448 is_code_valid: Arc::new(|_de: &str, _code: &str| true),
2449 suggest_code: Arc::new(|_de: &str, _code: &str| None),
2450 expected_components: Arc::new(|_tag: &str, _idx: usize| None),
2451 code_list_rules: Arc::new(base_code_list_rules),
2452 additional_structure_rule: None,
2453 required_segments: Arc::new(default_required_segments),
2454 message_type: None,
2455 enforce_known_tags: true,
2456 structure_checks: true,
2457 code_list_checks: false,
2458 }
2459 }
2460
2461 /// Set the directory identifier string (used in error messages).
2462 pub fn with_directory_id(mut self, id: impl Into<String>) -> Self {
2463 self.directory_id = id.into();
2464 self
2465 }
2466
2467 /// Override the code-list rules function.
2468 ///
2469 /// Directories can supply a directory-specific implementation that extends or
2470 /// replaces the base rules from `base_code_list_rules`.
2471 pub fn with_code_list_rules(
2472 mut self,
2473 f: impl Fn(&str) -> &'static [(usize, usize, &'static str)] + Send + Sync + 'static,
2474 ) -> Self {
2475 self.code_list_rules = Arc::new(f);
2476 self
2477 }
2478
2479 /// Enable only structure checks and disable code-list checks.
2480 pub fn structure_only(mut self) -> Self {
2481 self.structure_checks = true;
2482 self.code_list_checks = false;
2483 self
2484 }
2485
2486 /// Enable only code-list checks and disable structure checks.
2487 pub fn code_list_only(mut self) -> Self {
2488 self.structure_checks = false;
2489 self.code_list_checks = true;
2490 self
2491 }
2492
2493 /// Configure whether unknown segment tags should be rejected.
2494 pub fn enforce_known_tags(mut self, enforce: bool) -> Self {
2495 self.enforce_known_tags = enforce;
2496 self
2497 }
2498
2499 /// Override the required-segments mapping used for structural validation.
2500 ///
2501 /// The supplied function receives an EDIFACT message type string (e.g. `"ORDERS"`)
2502 /// and must return a `'static` slice of segment tags that are mandatory for that
2503 /// type. The tags are checked both for *presence* and for *canonical ordering*
2504 /// within the message.
2505 ///
2506 /// # Example
2507 ///
2508 /// ```rust,ignore
2509 /// fn my_required_segments(msg_type: &str) -> &'static [&'static str] {
2510 /// match msg_type {
2511 /// "DESADV" => &["UNH", "BGM", "SHP", "UNT"],
2512 /// "INVOIC" => &["UNH", "BGM", "MOA", "UNT"],
2513 /// _ => &["UNH", "UNT"],
2514 /// }
2515 /// }
2516 ///
2517 /// let validator = DirectoryValidator::from_definitions(DEFS)
2518 /// .with_required_segments(my_required_segments);
2519 /// ```
2520 pub fn with_required_segments(
2521 mut self,
2522 f: impl Fn(&str) -> &'static [&'static str] + Send + Sync + 'static,
2523 ) -> Self {
2524 self.required_segments = Arc::new(f);
2525 self
2526 }
2527
2528 fn detect_message_type(&self, segments: &[Segment<'_>]) -> Option<String> {
2529 if let Some(explicit) = self.message_type.as_deref() {
2530 return Some(explicit.to_owned());
2531 }
2532
2533 segments
2534 .iter()
2535 .find(|s| s.tag == "UNH")
2536 .and_then(|s| s.get_element(1))
2537 .and_then(|e| e.get_component(0))
2538 .map(str::to_owned)
2539 }
2540
2541 /// Count the non-trailing-empty components in element `element_idx` of `seg`.
2542 ///
2543 /// Per ISO 9735-1 §8.7.2 ("Trailing empty component data elements may be omitted"),
2544 /// a sender is not required to transmit trailing empty components; this function
2545 /// therefore strips them before checking against the expected count so that
2546 /// conformant messages with omitted trailing components are still accepted.
2547 ///
2548 /// # Examples
2549 ///
2550 /// - `DTM+137:20200101:` has three declared components but only 2 non-empty → effective=2
2551 /// - `NAD+MS++::293` has a composite with 3 components, last two empty → effective=1
2552 fn effective_component_count(seg: &Segment<'_>, element_idx: usize) -> Option<u8> {
2553 let elem = seg.elements.get(element_idx)?;
2554 let mut count = elem.components.len();
2555 while count > 0 && elem.components[count - 1].0.as_ref().is_empty() {
2556 count -= 1;
2557 }
2558 u8::try_from(count).ok()
2559 }
2560
2561 fn collect_component_count_issues(
2562 &self,
2563 seg: &Segment<'_>,
2564 def: &SegmentDefRef<'_>,
2565 out: &mut Vec<EdifactError>,
2566 ) {
2567 for idx in 0..seg.elements.len() {
2568 let actual = Self::effective_component_count(seg, idx).unwrap_or(0);
2569 // The `expected_components` hook is an exact count and wins when set.
2570 if let Some(expected) = (self.expected_components)(seg.tag, idx) {
2571 if actual != expected {
2572 out.push(EdifactError::InvalidComponentCount {
2573 tag: seg.tag.to_owned(),
2574 element_index: idx,
2575 expected,
2576 actual,
2577 span: seg.element_span(idx).unwrap_or(seg.span),
2578 });
2579 }
2580 continue;
2581 }
2582 // Otherwise a composite that declares its components caps them:
2583 // more components than the directory defines is a structural error,
2584 // while fewer is normal (conditional components may be omitted).
2585 if let Some(declared) = def.declared_component_count(idx) {
2586 if actual > declared {
2587 out.push(EdifactError::InvalidComponentCount {
2588 tag: seg.tag.to_owned(),
2589 element_index: idx,
2590 expected: declared,
2591 actual,
2592 span: seg.element_span(idx).unwrap_or(seg.span),
2593 });
2594 }
2595 }
2596 }
2597 }
2598
2599 /// Enforce each element's declared maximum number of occurrences.
2600 ///
2601 /// `max_repeat` had been carried on every `ElementRef` since the type
2602 /// existed, exposed by a getter, and read by nothing — so a definition that
2603 /// said "this element occurs once" constrained nothing at all, and a caller
2604 /// who wrote it believed otherwise.
2605 fn collect_repetition_issues(
2606 &self,
2607 seg: &Segment<'_>,
2608 def: &SegmentDefRef<'_>,
2609 out: &mut Vec<EdifactError>,
2610 ) {
2611 for (index, element) in seg.elements.iter().enumerate() {
2612 let Some(max) = def.max_repeat_at(index) else {
2613 continue;
2614 };
2615 // A declared maximum of zero would forbid the element outright,
2616 // which is what `Status` is for; treat it as "unconstrained".
2617 if max == 0 {
2618 continue;
2619 }
2620 let actual = element.repeat_count();
2621 if actual > usize::from(max) {
2622 out.push(EdifactError::TooManyRepetitions {
2623 tag: seg.tag.to_owned(),
2624 element_index: index,
2625 max,
2626 actual,
2627 span: element.span,
2628 });
2629 }
2630 }
2631 }
2632
2633 /// Check every populated value against its declared representation.
2634 ///
2635 /// Only positions the definition actually states a representation for are
2636 /// checked, so a partial table stays useful rather than becoming a source of
2637 /// false findings.
2638 fn collect_representation_issues(
2639 &self,
2640 seg: &Segment<'_>,
2641 def: &SegmentDefRef<'_>,
2642 syntax_version: Option<u8>,
2643 out: &mut Vec<EdifactError>,
2644 ) {
2645 for (element_index, element) in seg.elements.iter().enumerate() {
2646 for occurrence in element.repetitions() {
2647 for (component_index, (value, span)) in occurrence.iter().enumerate() {
2648 // An empty value is an absent one (§8.1); its presence is
2649 // the mandatory check's business, not the representation's.
2650 if value.is_empty() {
2651 continue;
2652 }
2653 let Some(repr) = def.repr_at(element_index, component_index, syntax_version)
2654 else {
2655 continue;
2656 };
2657 if !repr.permits_characters(value) {
2658 out.push(EdifactError::InvalidCharacterType {
2659 tag: seg.tag.to_owned(),
2660 element_index,
2661 component_index,
2662 repr: repr.to_string(),
2663 value: value.to_string(),
2664 span: *span,
2665 });
2666 // The length of a value that is not of the declared
2667 // class is not meaningful — §10's numeric count in
2668 // particular assumes a numeric value.
2669 continue;
2670 }
2671 self.collect_insignificant_characters(
2672 seg,
2673 element_index,
2674 component_index,
2675 value,
2676 *span,
2677 repr.primary,
2678 out,
2679 );
2680 if repr.permits_length(value) {
2681 continue;
2682 }
2683 let actual = repr.measure(value);
2684 out.push(if repr.is_too_short(value) {
2685 EdifactError::DataElementTooShort {
2686 tag: seg.tag.to_owned(),
2687 element_index,
2688 component_index,
2689 repr: repr.to_string(),
2690 actual,
2691 span: *span,
2692 }
2693 } else {
2694 EdifactError::DataElementTooLong {
2695 tag: seg.tag.to_owned(),
2696 element_index,
2697 component_index,
2698 repr: repr.to_string(),
2699 actual,
2700 span: *span,
2701 }
2702 });
2703 }
2704 }
2705 }
2706 }
2707
2708 /// Report characters ISO 9735-1 §9.1 requires the sender to suppress.
2709 ///
2710 /// Only **variable length** elements are covered, which is what §9.1 says:
2711 /// a fixed-length numeric field is padded with leading zeroes by design, and
2712 /// a fixed-length text one with trailing spaces.
2713 #[allow(clippy::too_many_arguments)]
2714 fn collect_insignificant_characters(
2715 &self,
2716 seg: &Segment<'_>,
2717 element_index: usize,
2718 component_index: usize,
2719 value: &str,
2720 span: crate::Span,
2721 repr: Repr,
2722 out: &mut Vec<EdifactError>,
2723 ) {
2724 if repr.is_fixed() {
2725 return;
2726 }
2727 let kind = match repr.kind() {
2728 ReprKind::Numeric => {
2729 let digits = value.strip_prefix('-').unwrap_or(value);
2730 // "Nevertheless, a single zero before a decimal mark is
2731 // allowed", so `0.5` is correct and only `00…` is not.
2732 let leading_zeroes = digits.starts_with('0')
2733 && digits.len() > 1
2734 && !digits.starts_with("0.")
2735 && !digits.starts_with("0,");
2736 if !leading_zeroes {
2737 return;
2738 }
2739 Insignificant::LeadingZeroes
2740 }
2741 ReprKind::Alphabetic | ReprKind::Alphanumeric => {
2742 if !value.ends_with(' ') {
2743 return;
2744 }
2745 Insignificant::TrailingSpaces
2746 }
2747 };
2748 out.push(EdifactError::InsignificantCharacters {
2749 tag: seg.tag.to_owned(),
2750 element_index,
2751 component_index,
2752 kind,
2753 span,
2754 });
2755 }
2756
2757 fn collect_code_list_issues(&self, seg: &Segment<'_>, out: &mut Vec<EdifactError>) {
2758 for (elem_idx, comp_idx, de) in (self.code_list_rules)(seg.tag) {
2759 let value = seg
2760 .get_element(*elem_idx)
2761 .and_then(|e| e.get_component(*comp_idx))
2762 .unwrap_or("");
2763 if !value.is_empty() && !(self.is_code_valid)(de, value) {
2764 let suggestion = (self.suggest_code)(de, value);
2765 // Point at the offending *value*, not the whole segment, so
2766 // rendered diagnostics underline the code that failed.
2767 let span = seg
2768 .get_element(*elem_idx)
2769 .and_then(|e| e.component_span(*comp_idx))
2770 .unwrap_or(seg.span);
2771 out.push(EdifactError::InvalidCodeValue {
2772 tag: seg.tag.to_owned(),
2773 element_index: *elem_idx,
2774 value: value.to_owned(),
2775 code_list: (*de).to_owned(),
2776 span,
2777 suggestion,
2778 });
2779 }
2780 }
2781 }
2782}
2783
2784impl DirectoryValidator {
2785 fn resolve_def<'a>(&'a self, tag: &str) -> Option<SegmentDefRef<'a>> {
2786 if let Some(owned) = &self.owned_defs {
2787 let index = self.owned_index.as_ref()?;
2788 owned.get(*index.get(tag)?).map(SegmentDefRef::Owned)
2789 } else {
2790 (self.segment_lookup)(tag).map(SegmentDefRef::Static)
2791 }
2792 }
2793
2794 /// Check one segment, appending **every** violation found to `out`.
2795 ///
2796 /// Reporting continues past the first fault: a segment missing two mandatory
2797 /// elements and carrying an invalid code is three findings, and a validator
2798 /// whose whole purpose is an exhaustive report has no business hiding two of
2799 /// them. Only the checks that cannot proceed without a resolved definition
2800 /// short-circuit.
2801 fn collect_segment_issues(
2802 &self,
2803 seg: &Segment<'_>,
2804 syntax_version: Option<u8>,
2805 out: &mut Vec<EdifactError>,
2806 ) {
2807 if !self.structure_checks && !self.code_list_checks {
2808 return;
2809 }
2810
2811 let Some(def) = self.resolve_def(seg.tag) else {
2812 if self.structure_checks && self.enforce_known_tags {
2813 out.push(EdifactError::InvalidSegmentForMessage {
2814 tag: seg.tag.to_owned(),
2815 message_type: self
2816 .message_type
2817 .clone()
2818 .unwrap_or_else(|| self.directory_id.clone()),
2819 span: seg.tag_span,
2820 });
2821 }
2822 // Without a definition there is nothing further to check against.
2823 return;
2824 };
2825
2826 if self.structure_checks {
2827 let max_elements = def.max_element_position();
2828 let min_elements = def.last_mandatory_position();
2829 let actual = seg.elements.len();
2830 if actual < min_elements || actual > max_elements {
2831 out.push(EdifactError::InvalidElementCount {
2832 tag: seg.tag.to_owned(),
2833 min: min_elements,
2834 max: max_elements,
2835 actual,
2836 span: seg.span,
2837 });
2838 }
2839
2840 def.for_each_mandatory_position::<std::convert::Infallible, _>(|idx, _de| {
2841 let is_present = seg.elements.get(idx).is_some_and(|elem| {
2842 elem.components.iter().any(|(c, _)| !c.as_ref().is_empty())
2843 });
2844 if !is_present {
2845 out.push(EdifactError::MissingRequiredElement {
2846 tag: seg.tag.to_owned(),
2847 element_index: idx,
2848 });
2849 }
2850 Ok(())
2851 })
2852 .unwrap_or_else(|never| match never {});
2853
2854 // Mandatory *components* inside declared composites. Only fires for
2855 // definitions built with `ElementRef::composite` / `with_components`;
2856 // an element that is absent entirely is already reported above as a
2857 // missing element, so only present elements are checked here.
2858 def.for_each_mandatory_component::<std::convert::Infallible, _>(
2859 |elem_idx, comp_idx, _de| {
2860 let Some(elem) = seg.elements.get(elem_idx) else {
2861 return Ok(());
2862 };
2863 let present = elem
2864 .get_component(comp_idx)
2865 .is_some_and(|value| !value.is_empty());
2866 if !present {
2867 out.push(EdifactError::MissingRequiredComponent {
2868 tag: seg.tag.to_owned(),
2869 element_index: elem_idx,
2870 component_index: comp_idx,
2871 });
2872 }
2873 Ok(())
2874 },
2875 )
2876 .unwrap_or_else(|never| match never {});
2877
2878 self.collect_component_count_issues(seg, &def, out);
2879 self.collect_repetition_issues(seg, &def, out);
2880 self.collect_representation_issues(seg, &def, syntax_version, out);
2881
2882 if let Some(rule) = &self.additional_structure_rule {
2883 if let Err(error) = rule(seg) {
2884 out.push(error);
2885 }
2886 }
2887 }
2888
2889 if self.code_list_checks {
2890 self.collect_code_list_issues(seg, out);
2891 }
2892 }
2893}
2894
2895impl Validator for DirectoryValidator {
2896 fn set_message_type(&mut self, message_type: Option<&str>) {
2897 self.message_type = message_type.map(str::to_owned);
2898 }
2899
2900 fn validate_batch(
2901 &self,
2902 segments: &[Segment<'_>],
2903 report: &mut ValidationReport,
2904 _context: &ValidationRuleContext<'_>,
2905 ) {
2906 // The syntax version decides which form of a version-dependent
2907 // representation applies; `UNB` S001 DE 0002 is where it is stated.
2908 let syntax_version = detect_syntax_version(segments);
2909 let mut issues = Vec::new();
2910 for seg in segments {
2911 self.collect_segment_issues(seg, syntax_version, &mut issues);
2912 for err in issues.drain(..) {
2913 report_error(report, err);
2914 }
2915 }
2916
2917 if self.structure_checks {
2918 if let Some(message_type) = self.detect_message_type(segments) {
2919 // One pass recording each tag's first index answers both the
2920 // presence and the ordering question. The previous shape ran two
2921 // full scans *per required tag* and invoked `required_segments`
2922 // twice, which is O(|required| x n) on every batch.
2923 let mut first_index: std::collections::HashMap<&str, usize> =
2924 std::collections::HashMap::with_capacity(segments.len());
2925 for (i, seg) in segments.iter().enumerate() {
2926 first_index.entry(seg.tag).or_insert(i);
2927 }
2928
2929 let required = (self.required_segments)(&message_type);
2930 for required_tag in required {
2931 if !first_index.contains_key(*required_tag) {
2932 report.add_error(
2933 ValidationIssue::new(
2934 ValidationSeverity::Error,
2935 format!(
2936 "required segment {} missing for message type {}",
2937 required_tag, message_type
2938 ),
2939 )
2940 .with_segment(*required_tag)
2941 .with_suggestion("Add the mandatory segment at the correct position"),
2942 );
2943 }
2944 }
2945
2946 let mut last_idx = None;
2947 for tag in required {
2948 if let Some(&idx) = first_index.get(*tag) {
2949 if let Some(prev) = last_idx {
2950 if idx < prev {
2951 report.add_error(
2952 ValidationIssue::new(
2953 ValidationSeverity::Error,
2954 format!(
2955 "segment sequence violation for message type {}: '{}' appears out of order",
2956 message_type, tag
2957 ),
2958 )
2959 .with_segment(*tag)
2960 .with_suggestion(
2961 "Ensure required segments follow UN/EDIFACT canonical order",
2962 ),
2963 );
2964 }
2965 }
2966 last_idx = Some(idx);
2967 }
2968 }
2969 }
2970 }
2971 }
2972}
2973
2974// ── DirectoryValidatorBuilder ─────────────────────────────────────────────────
2975
2976/// Builder for [`DirectoryValidator`] using runtime-owned segment definitions.
2977///
2978/// Use this when segment definitions are loaded from an external source at
2979/// startup (JSON, database, YAML, …) rather than being available as `static`
2980/// arrays at compile time.
2981///
2982/// # Example
2983///
2984/// ```rust,ignore
2985/// let validator = DirectoryValidatorBuilder::new("my-profile")
2986/// .add_segment(
2987/// OwnedSegmentDef::new_unchecked(
2988/// "BGM".to_owned(),
2989/// "Beginning of message".to_owned(),
2990/// vec![OwnedElementRef::new_unchecked(1, "C002".to_owned(), Status::Mandatory, 1)],
2991/// ),
2992/// )
2993/// .build();
2994/// ```
2995#[derive(Debug, Default)]
2996pub struct DirectoryValidatorBuilder {
2997 directory_id: Option<String>,
2998 segments: Vec<OwnedSegmentDef>,
2999}
3000
3001impl DirectoryValidatorBuilder {
3002 /// Create a new builder with the given directory identifier.
3003 ///
3004 /// The identifier is used in error messages; set a human-readable value
3005 /// such as `"ORDERS-MIG-5.5"` or `"custom-profile"`.
3006 pub fn new(directory_id: impl Into<String>) -> Self {
3007 Self {
3008 directory_id: Some(directory_id.into()),
3009 segments: Vec::new(),
3010 }
3011 }
3012
3013 /// Add a segment definition to the builder.
3014 ///
3015 /// Definitions can be added in any order; the resulting validator looks
3016 /// them up by tag at validation time.
3017 pub fn add_segment(mut self, def: OwnedSegmentDef) -> Self {
3018 self.segments.push(def);
3019 self
3020 }
3021
3022 /// Extend the builder with multiple segment definitions at once.
3023 pub fn add_segments(mut self, defs: impl IntoIterator<Item = OwnedSegmentDef>) -> Self {
3024 self.segments.extend(defs);
3025 self
3026 }
3027
3028 /// Build the [`DirectoryValidator`].
3029 ///
3030 /// Returns a validator backed by the accumulated [`OwnedSegmentDef`]s.
3031 /// Code-list checks are disabled by default; chain
3032 /// [`DirectoryValidator::with_code_list_rules`] on the returned value to
3033 /// enable them.
3034 pub fn build(self) -> DirectoryValidator {
3035 let mut validator = DirectoryValidator::from_owned_definitions(self.segments);
3036 if let Some(id) = self.directory_id {
3037 validator.directory_id = id;
3038 }
3039 validator
3040 }
3041}
3042
3043#[cfg(test)]
3044mod tests {
3045 use super::*;
3046
3047 static TEST_ELEMENTS: &[ElementRef] = &[ElementRef::new(1, "C507", Status::Mandatory, 1)];
3048
3049 static TEST_SEGMENT: SegmentDefinition =
3050 SegmentDefinition::new("TST", "Test segment", TEST_ELEMENTS);
3051
3052 fn segment_lookup(tag: &str) -> Option<&'static SegmentDefinition> {
3053 match tag {
3054 "TST" => Some(&TEST_SEGMENT),
3055 _ => None,
3056 }
3057 }
3058
3059 fn code_valid(_de: &str, _code: &str) -> bool {
3060 true
3061 }
3062
3063 fn suggest_code(_de: &str, _code: &str) -> Option<&'static str> {
3064 None
3065 }
3066
3067 fn expected_components(_tag: &str, _idx: usize) -> Option<u8> {
3068 None
3069 }
3070
3071 #[test]
3072 fn mandatory_composite_present_when_any_component_non_empty() {
3073 let input = b"TST+:ABC'";
3074 let segments: Vec<_> = crate::from_bytes(input)
3075 .collect::<Result<Vec<_>, _>>()
3076 .expect("parse should succeed");
3077
3078 let validator = DirectoryValidator::new(
3079 "TEST",
3080 segment_lookup,
3081 code_valid,
3082 suggest_code,
3083 expected_components,
3084 None,
3085 );
3086
3087 let mut report = ValidationReport::default();
3088 validator.validate_batch(
3089 &segments,
3090 &mut report,
3091 &crate::validator::ValidationRuleContext::empty(),
3092 );
3093 assert!(!report.has_errors());
3094 }
3095
3096 // ── effective_component_count (ISO 9735-1 §8.7.2 trailing-empty-component trim) ──
3097
3098 fn parse_single(input: &[u8]) -> crate::OwnedSegment {
3099 crate::from_reader_collect(std::io::Cursor::new(input))
3100 .expect("parse should succeed")
3101 .into_iter()
3102 .next()
3103 .expect("at least one segment")
3104 }
3105
3106 #[test]
3107 fn trailing_empty_component_stripped_from_dtm() {
3108 // DTM+137:20200101: has three components in element 0; the third is empty.
3109 // ISO 9735-1 §8.7.2 says trailing empty components may be omitted,
3110 // so effective count should be 2.
3111 let owned = parse_single(b"DTM+137:20200101:'");
3112 let seg = owned.as_borrowed();
3113 let count = DirectoryValidator::effective_component_count(&seg, 0);
3114 assert_eq!(
3115 count,
3116 Some(2),
3117 "trailing empty component should be stripped"
3118 );
3119 }
3120
3121 #[test]
3122 fn all_empty_components_result_in_zero() {
3123 // NAD+MS++: → element 2 is ":" with two empty components → effective=0
3124 let owned = parse_single(b"NAD+MS++:'");
3125 let seg = owned.as_borrowed();
3126 let count = DirectoryValidator::effective_component_count(&seg, 2);
3127 assert_eq!(
3128 count,
3129 Some(0),
3130 "all-empty composite should have effective count 0"
3131 );
3132 }
3133
3134 #[test]
3135 fn non_empty_component_not_stripped() {
3136 // DTM+137:20200101:102 — all three components are non-empty
3137 let owned = parse_single(b"DTM+137:20200101:102'");
3138 let seg = owned.as_borrowed();
3139 let count = DirectoryValidator::effective_component_count(&seg, 0);
3140 assert_eq!(
3141 count,
3142 Some(3),
3143 "no components should be stripped when all non-empty"
3144 );
3145 }
3146
3147 #[test]
3148 fn with_code_list_rules_overrides_base() {
3149 // Override code-list rules to require element 0 of TST to be a specific code.
3150 fn custom_rules(tag: &str) -> &'static [(usize, usize, &'static str)] {
3151 match tag {
3152 "TST" => &[(0, 0, "CUSTOM_DE")],
3153 _ => &[],
3154 }
3155 }
3156 fn custom_code_valid(_de: &str, code: &str) -> bool {
3157 code == "VALID"
3158 }
3159 fn no_suggestion(_de: &str, _code: &str) -> Option<&'static str> {
3160 None
3161 }
3162
3163 let input = b"TST+INVALID'";
3164 let segments: Vec<_> = crate::from_bytes(input)
3165 .collect::<Result<Vec<_>, _>>()
3166 .expect("parse should succeed");
3167
3168 let validator = DirectoryValidator::new(
3169 "TEST",
3170 segment_lookup,
3171 custom_code_valid,
3172 no_suggestion,
3173 expected_components,
3174 None,
3175 )
3176 .with_code_list_rules(custom_rules);
3177
3178 let mut report = ValidationReport::default();
3179 validator.validate_batch(
3180 &segments,
3181 &mut report,
3182 &crate::validator::ValidationRuleContext::empty(),
3183 );
3184 assert!(
3185 report.has_warnings(),
3186 "INVALID is not in the custom code list so validation must warn"
3187 );
3188 }
3189}