Skip to main content

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}