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 [`Segment`] — and therefore on `OwnedSegment` too.
744///
745/// Both indices are **zero-based**, matching the positional accessors — the
746/// one-based positions used in directory definitions are converted during
747/// resolution.
748#[derive(Debug, Clone, Copy, PartialEq, Eq)]
749pub struct ElementPath {
750    /// Zero-based index of the data element within the segment.
751    pub element: usize,
752    /// Zero-based index of the component within a composite.
753    ///
754    /// `None` when the code names the data element itself (a simple element, or
755    /// a composite addressed as a whole).  Value lookups treat `None` as
756    /// component 0, which is the first — and for a simple element, only —
757    /// component.
758    pub component: Option<usize>,
759}
760
761impl ElementPath {
762    /// Path to a whole data element.
763    #[must_use]
764    #[inline]
765    pub const fn element(element: usize) -> Self {
766        Self {
767            element,
768            component: None,
769        }
770    }
771
772    /// Path to a component within a composite data element.
773    #[must_use]
774    #[inline]
775    pub const fn component(element: usize, component: usize) -> Self {
776        Self {
777            element,
778            component: Some(component),
779        }
780    }
781
782    /// Zero-based component index, treating "whole element" as component 0.
783    #[must_use]
784    #[inline]
785    pub const fn component_index(&self) -> usize {
786        match self.component {
787            Some(c) => c,
788            None => 0,
789        }
790    }
791}
792
793/// Directory metadata that maps UN/EDIFACT data element identifiers to positions.
794///
795/// Implemented by [`SegmentDefinition`] (compile-time tables) and
796/// [`OwnedSegmentDef`] (runtime-loaded definitions), so the same code-addressed
797/// accessors work against either source.
798///
799/// # Example
800///
801/// ```rust
802/// use edifact_rs::{ElementRef, SegmentDefinition, SegmentLayout, Status};
803///
804/// static BGM_ELEMENTS: &[ElementRef] = &[
805///     ElementRef::new(1, "C002", Status::Conditional, 1),
806///     ElementRef::new(2, "C106", Status::Conditional, 1),
807///     ElementRef::new(3, "1225", Status::Conditional, 1),
808/// ];
809/// static BGM: SegmentDefinition =
810///     SegmentDefinition::new("BGM", "Beginning of message", BGM_ELEMENTS);
811///
812/// let path = BGM.resolve_code("1225")?;
813/// assert_eq!(path.element, 2);
814/// assert!(BGM.resolve_code("9999").is_err());
815/// # Ok::<(), edifact_rs::EdifactError>(())
816/// ```
817pub trait SegmentLayout {
818    /// The segment tag this layout describes (e.g. `"NAD"`).
819    fn layout_tag(&self) -> &str;
820
821    /// Resolve a UN/EDIFACT data element identifier to a position.
822    ///
823    /// # Errors
824    ///
825    /// Returns [`EdifactError::UnknownDataElement`] when the identifier does not
826    /// appear in this definition, and [`EdifactError::AmbiguousDataElement`]
827    /// when it appears at more than one position.
828    fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError>;
829
830    /// Every position this layout declares, flattened and in order.
831    ///
832    /// Implemented by both the compile-time and runtime definitions, so tooling
833    /// can walk a layout without knowing which one it holds.
834    fn slots(&self) -> Vec<LayoutSlot>;
835
836    /// Check this layout against real messages and report what does not line up.
837    ///
838    /// Hand-authoring a segment definition has a silent failure mode: a layout
839    /// that disagrees with the wire resolves `value_by_code` to the *wrong
840    /// component*, returns a plausible value, and every test still passes. There
841    /// is no way to notice from inside the program — the definition is the only
842    /// thing that says what the positions mean.
843    ///
844    /// Pointing the definition at a corpus is what breaks that circle. Three
845    /// kinds of finding come back, and the third is the one that matters most:
846    ///
847    /// | Finding | Means |
848    /// |---|---|
849    /// | [`UndeclaredElement`][LayoutFinding::UndeclaredElement] / [`UndeclaredComponent`][LayoutFinding::UndeclaredComponent] | The wire carries a value the layout has no slot for — the layout is **wrong**. |
850    /// | [`MandatoryNeverPopulated`][LayoutFinding::MandatoryNeverPopulated] | A slot the layout calls mandatory is empty everywhere — the status or the position is **wrong**. |
851    /// | [`NeverObserved`][LayoutFinding::NeverObserved] | Nothing in the corpus reaches this slot, so **the corpus cannot confirm it**. |
852    ///
853    /// `NeverObserved` is not a defect. It is the honest answer to "does my
854    /// definition match the directory?" when the fixtures are too thin to tell,
855    /// and it names exactly which positions to go and check by hand.
856    ///
857    /// Only segments whose tag matches [`layout_tag`][Self::layout_tag] are
858    /// examined; the rest of the slice is ignored, so a whole interchange can be
859    /// passed in as-is.
860    ///
861    /// # Example
862    ///
863    /// ```
864    /// use edifact_rs::{ComponentRef, ElementRef, SegmentDefinition, SegmentLayout, Status, from_bytes};
865    ///
866    /// // A hand-authored C507 that stops one component short of the directory.
867    /// static C507: &[ComponentRef] = &[
868    ///     ComponentRef::new(1, "2005", Status::Mandatory),
869    ///     ComponentRef::new(2, "2380", Status::Conditional),
870    /// ];
871    /// static DTM_ELEMENTS: &[ElementRef] =
872    ///     &[ElementRef::composite(1, "C507", Status::Mandatory, 1, C507)];
873    /// static DTM: SegmentDefinition =
874    ///     SegmentDefinition::new("DTM", "Date/time/period", DTM_ELEMENTS);
875    ///
876    /// let corpus: Vec<_> = from_bytes(b"DTM+137:20260101:102'").collect::<Result<Vec<_>, _>>()?;
877    /// let audit = DTM.audit(&corpus);
878    ///
879    /// // The format qualifier `102` has nowhere to go — the layout is short.
880    /// assert!(audit.has_contradictions());
881    /// assert_eq!(audit.segments_examined(), 1);
882    /// # Ok::<(), edifact_rs::EdifactError>(())
883    /// ```
884    fn audit(&self, segments: &[crate::Segment<'_>]) -> LayoutAudit {
885        audit_layout(self.layout_tag(), &self.slots(), segments)
886    }
887}
888
889/// One declared position in a [`SegmentLayout`], flattened.
890///
891/// Produced by [`SegmentLayout::slots`].
892#[derive(Debug, Clone, PartialEq, Eq)]
893pub struct LayoutSlot {
894    /// Zero-based data element index within the segment.
895    pub element: usize,
896    /// Zero-based component index, or `None` for a simple data element.
897    pub component: Option<usize>,
898    /// The UN/EDIFACT identifier declared at this position.
899    pub data_element: String,
900    /// Whether the layout calls this position mandatory.
901    pub status: Status,
902    /// The status of the **enclosing data element**.
903    ///
904    /// Equal to `status` for a simple data element. For a component it is the
905    /// composite's own status, which is what decides whether a mandatory
906    /// component is actually required: ISO 9735-1 §8.6 makes it mandatory "if
907    /// the composite data element is present", not unconditionally.
908    pub element_status: Status,
909}
910
911impl LayoutSlot {
912    /// Component index treating a simple data element as component 0.
913    #[must_use]
914    pub fn component_index(&self) -> usize {
915        self.component.unwrap_or(0)
916    }
917}
918
919/// One way a layout and a corpus disagree — or fail to inform each other.
920#[derive(Debug, Clone, PartialEq, Eq)]
921#[non_exhaustive]
922pub enum LayoutFinding {
923    /// A segment carried a populated data element beyond the last one declared.
924    UndeclaredElement {
925        /// Zero-based index of the undeclared element.
926        element: usize,
927        /// Byte span of the segment that carried it.
928        span: crate::Span,
929    },
930    /// An element carried a populated component beyond the last one declared.
931    UndeclaredComponent {
932        /// Zero-based element index.
933        element: usize,
934        /// Zero-based index of the undeclared component.
935        component: usize,
936        /// Byte span of the segment that carried it.
937        span: crate::Span,
938    },
939    /// A position the layout calls mandatory was empty in every segment.
940    MandatoryNeverPopulated {
941        /// The declared position.
942        slot: LayoutSlot,
943    },
944    /// No segment in the corpus populated this position.
945    ///
946    /// Evidence of nothing rather than evidence of a fault: the corpus is too
947    /// thin to confirm or refute the slot.
948    NeverObserved {
949        /// The declared position.
950        slot: LayoutSlot,
951    },
952}
953
954/// The result of checking a [`SegmentLayout`] against a corpus.
955///
956/// See [`SegmentLayout::audit`].
957#[derive(Debug, Clone, Default)]
958pub struct LayoutAudit {
959    tag: String,
960    segments_examined: usize,
961    findings: Vec<LayoutFinding>,
962}
963
964impl LayoutAudit {
965    /// The segment tag that was audited.
966    #[must_use]
967    pub fn tag(&self) -> &str {
968        &self.tag
969    }
970
971    /// How many segments in the corpus carried that tag.
972    ///
973    /// Zero means the audit proved nothing at all — worth asserting on.
974    #[must_use]
975    pub fn segments_examined(&self) -> usize {
976        self.segments_examined
977    }
978
979    /// Every finding, in declaration order.
980    #[must_use]
981    pub fn findings(&self) -> &[LayoutFinding] {
982        &self.findings
983    }
984
985    /// Findings that mean the layout is **wrong**, as opposed to unconfirmed.
986    ///
987    /// [`NeverObserved`][LayoutFinding::NeverObserved] is excluded: a corpus
988    /// that never reaches a slot says nothing about whether the slot is right.
989    pub fn contradictions(&self) -> impl Iterator<Item = &LayoutFinding> {
990        self.findings
991            .iter()
992            .filter(|f| !matches!(f, LayoutFinding::NeverObserved { .. }))
993    }
994
995    /// `true` when the corpus contradicts the layout.
996    ///
997    /// This is the assertion to put in a test: it fails on a layout the wire
998    /// disproves, and stays quiet about slots the fixtures simply never exercise.
999    #[must_use]
1000    pub fn has_contradictions(&self) -> bool {
1001        self.contradictions().next().is_some()
1002    }
1003
1004    /// Positions the corpus never reached, in declaration order.
1005    ///
1006    /// Each one is a slot to verify against the directory by hand — or a gap to
1007    /// fill with a fixture.
1008    pub fn unconfirmed(&self) -> impl Iterator<Item = &LayoutSlot> {
1009        self.findings.iter().filter_map(|f| match f {
1010            LayoutFinding::NeverObserved { slot } => Some(slot),
1011            _ => None,
1012        })
1013    }
1014}
1015
1016impl std::fmt::Display for LayoutAudit {
1017    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1018        writeln!(
1019            f,
1020            "{}: {} segment(s) examined, {} contradiction(s), {} unconfirmed slot(s)",
1021            self.tag,
1022            self.segments_examined,
1023            self.contradictions().count(),
1024            self.unconfirmed().count(),
1025        )?;
1026        for finding in &self.findings {
1027            match finding {
1028                LayoutFinding::UndeclaredElement { element, span } => writeln!(
1029                    f,
1030                    "  element {element} is populated at bytes {span} but the layout declares no such element",
1031                )?,
1032                LayoutFinding::UndeclaredComponent {
1033                    element,
1034                    component,
1035                    span,
1036                } => writeln!(
1037                    f,
1038                    "  element {element} component {component} is populated at bytes {span} but the layout declares no such component",
1039                )?,
1040                LayoutFinding::MandatoryNeverPopulated { slot } => writeln!(
1041                    f,
1042                    "  {} is declared mandatory but is empty in every segment",
1043                    describe_slot(slot),
1044                )?,
1045                LayoutFinding::NeverObserved { slot } => writeln!(
1046                    f,
1047                    "  {} was never populated — this corpus cannot confirm it",
1048                    describe_slot(slot),
1049                )?,
1050            }
1051        }
1052        Ok(())
1053    }
1054}
1055
1056fn describe_slot(slot: &LayoutSlot) -> String {
1057    match slot.component {
1058        Some(component) => format!(
1059            "DE {} (element {}, component {component})",
1060            slot.data_element, slot.element
1061        ),
1062        None => format!("DE {} (element {})", slot.data_element, slot.element),
1063    }
1064}
1065
1066/// Audit every layout a corpus actually exercises, in one call.
1067///
1068/// [`SegmentLayout::audit`] answers for one segment. A hand-authored directory
1069/// has dozens, and the question worth asking is about all of them at once:
1070/// *which of my definitions does this corpus disprove, and which can it not
1071/// speak to?*
1072///
1073/// Only tags present in the corpus are audited — a definition the fixtures never
1074/// exercise would produce nothing but `NeverObserved` noise and drown the
1075/// findings that matter. Ask [`SegmentLayout::audit`] directly for those.
1076///
1077/// Results come back in the order the tags first appear, so the report reads in
1078/// message order.
1079///
1080/// # Example
1081///
1082/// ```
1083/// use edifact_rs::{audit_directory, from_bytes, service};
1084///
1085/// let corpus: Vec<_> = from_bytes(
1086///     b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+M1+ORDERS:D:96A:UN'UNT+2+M1'UNZ+1+IC1'",
1087/// )
1088/// .collect::<Result<Vec<_>, _>>()?;
1089///
1090/// let audits = audit_directory(service::lookup, &corpus);
1091///
1092/// // One audit per distinct tag the corpus contains.
1093/// assert_eq!(audits.len(), 4);
1094/// // The shipped service tables are not disproved by conformant input.
1095/// assert!(audits.iter().all(|a| !a.has_contradictions()));
1096/// # Ok::<(), edifact_rs::EdifactError>(())
1097/// ```
1098pub fn audit_directory<'a, L, F>(lookup: F, segments: &[crate::Segment<'_>]) -> Vec<LayoutAudit>
1099where
1100    L: SegmentLayout + ?Sized + 'a,
1101    F: Fn(&str) -> Option<&'a L>,
1102{
1103    let mut seen: Vec<&str> = Vec::new();
1104    for segment in segments {
1105        if !seen.contains(&segment.tag()) {
1106            seen.push(segment.tag());
1107        }
1108    }
1109    seen.into_iter()
1110        .filter_map(|tag| lookup(tag).map(|layout| layout.audit(segments)))
1111        .collect()
1112}
1113
1114/// Shared implementation behind [`SegmentLayout::audit`].
1115fn audit_layout(tag: &str, slots: &[LayoutSlot], segments: &[crate::Segment<'_>]) -> LayoutAudit {
1116    let mut audit = LayoutAudit {
1117        tag: tag.to_owned(),
1118        segments_examined: 0,
1119        findings: Vec::new(),
1120    };
1121
1122    // Highest declared index per element, so "beyond the layout" is decidable.
1123    let declared_elements = slots.iter().map(|s| s.element + 1).max().unwrap_or(0);
1124    let mut declared_components: Vec<usize> = vec![0; declared_elements];
1125    for slot in slots {
1126        let width = slot.component_index() + 1;
1127        if width > declared_components[slot.element] {
1128            declared_components[slot.element] = width;
1129        }
1130    }
1131
1132    let mut populated: Vec<Vec<bool>> = declared_components
1133        .iter()
1134        .map(|width| vec![false; *width])
1135        .collect();
1136
1137    for segment in segments.iter().filter(|s| s.tag == tag) {
1138        audit.segments_examined += 1;
1139        for (element_index, element) in segment.elements.iter().enumerate() {
1140            // Every occurrence counts: a repeating element populates the same
1141            // declared positions each time (ISO 9735-1 §8.6).
1142            for occurrence in element.repetitions() {
1143                for (component_index, (value, _)) in occurrence.iter().enumerate() {
1144                    // A trailing empty component is how EDIFACT spells "absent"
1145                    // (§8.7.2), so only a populated one is evidence of anything.
1146                    if value.is_empty() {
1147                        continue;
1148                    }
1149                    if element_index >= declared_elements {
1150                        push_once(
1151                            &mut audit.findings,
1152                            LayoutFinding::UndeclaredElement {
1153                                element: element_index,
1154                                span: segment.span,
1155                            },
1156                        );
1157                        continue;
1158                    }
1159                    if component_index >= declared_components[element_index] {
1160                        push_once(
1161                            &mut audit.findings,
1162                            LayoutFinding::UndeclaredComponent {
1163                                element: element_index,
1164                                component: component_index,
1165                                span: segment.span,
1166                            },
1167                        );
1168                        continue;
1169                    }
1170                    populated[element_index][component_index] = true;
1171                }
1172            }
1173        }
1174    }
1175
1176    // Whether any position of each element was populated — which is exactly
1177    // ISO 9735-1 §8.1's definition of a composite being "present".
1178    let element_populated: Vec<bool> = populated
1179        .iter()
1180        .map(|components| components.iter().any(|seen| *seen))
1181        .collect();
1182
1183    for slot in slots {
1184        if populated[slot.element][slot.component_index()] {
1185            continue;
1186        }
1187        // §8.6: "A mandatory component data element in a composite data element
1188        // shall be present **if the composite data element is present**."  A
1189        // conditional composite that the corpus never carries therefore says
1190        // nothing about its mandatory components — reporting them as violations
1191        // would condemn every optional composite in the definition.
1192        let required_here = slot.status == Status::Mandatory
1193            && (slot.component.is_none()
1194                || slot.element_status == Status::Mandatory
1195                || element_populated[slot.element]);
1196        audit.findings.push(if required_here {
1197            LayoutFinding::MandatoryNeverPopulated { slot: slot.clone() }
1198        } else {
1199            LayoutFinding::NeverObserved { slot: slot.clone() }
1200        });
1201    }
1202
1203    audit
1204}
1205
1206/// Record a finding unless an equivalent one is already present.
1207///
1208/// A corpus of 360 fixtures would otherwise report the same undeclared
1209/// component 360 times, burying every other finding.
1210fn push_once(findings: &mut Vec<LayoutFinding>, finding: LayoutFinding) {
1211    let duplicate = findings.iter().any(|existing| match (existing, &finding) {
1212        (
1213            LayoutFinding::UndeclaredElement { element: a, .. },
1214            LayoutFinding::UndeclaredElement { element: b, .. },
1215        ) => a == b,
1216        (
1217            LayoutFinding::UndeclaredComponent {
1218                element: a,
1219                component: c,
1220                ..
1221            },
1222            LayoutFinding::UndeclaredComponent {
1223                element: b,
1224                component: d,
1225                ..
1226            },
1227        ) => a == b && c == d,
1228        _ => false,
1229    });
1230    if !duplicate {
1231        findings.push(finding);
1232    }
1233}
1234
1235impl SegmentDefinition {
1236    /// Create a segment definition.
1237    ///
1238    /// `const` so directory tables can still be built at compile time despite
1239    /// the `#[non_exhaustive]` attribute blocking external struct literals.
1240    #[must_use]
1241    pub const fn new(
1242        tag: &'static str,
1243        name: &'static str,
1244        elements: &'static [ElementRef],
1245    ) -> Self {
1246        Self {
1247            tag,
1248            name,
1249            elements,
1250        }
1251    }
1252
1253    /// Number of positions in this definition that carry `data_element`.
1254    ///
1255    /// `0` means unknown, `1` means unambiguously addressable, and anything
1256    /// larger means the identifier is repeated and cannot be code-addressed.
1257    /// `const`, so a derive macro can assert on it at compile time.
1258    ///
1259    /// # Example
1260    ///
1261    /// ```rust
1262    /// # use edifact_rs::{ElementRef, SegmentDefinition, Status};
1263    /// # static E: &[ElementRef] = &[ElementRef::new(1, "3035", Status::Mandatory, 1)];
1264    /// static NAD: SegmentDefinition = SegmentDefinition::new("NAD", "Name and address", E);
1265    /// const _: () = assert!(NAD.code_positions("3035") == 1);
1266    /// ```
1267    #[must_use]
1268    pub const fn code_positions(&self, data_element: &str) -> usize {
1269        let mut hits = 0;
1270        let mut i = 0;
1271        while i < self.elements.len() {
1272            let el = &self.elements[i];
1273            if const_str_eq(el.data_element, data_element) {
1274                hits += 1;
1275            }
1276            let mut c = 0;
1277            while c < el.components.len() {
1278                if const_str_eq(el.components[c].data_element, data_element) {
1279                    hits += 1;
1280                }
1281                c += 1;
1282            }
1283            i += 1;
1284        }
1285        hits
1286    }
1287
1288    /// Zero-based element index for `data_element`, resolved at compile time.
1289    ///
1290    /// # Panics
1291    ///
1292    /// Panics when the identifier is unknown or appears at more than one
1293    /// position.  In a `const` context — which is how the derive macro uses it —
1294    /// that panic is a **compile error**, so a mistyped identifier can never
1295    /// reach runtime.  Guard with [`code_positions`][Self::code_positions] for a
1296    /// message that names the offending field.
1297    #[must_use]
1298    pub const fn element_slot(&self, data_element: &str) -> usize {
1299        // Two asserts rather than one: a const panic message cannot be
1300        // formatted, so naming the identifier is impossible — but saying which
1301        // of the two problems occurred is not, and it is the part that decides
1302        // what the author has to change.
1303        assert!(
1304            self.code_positions(data_element) != 0,
1305            "this segment definition declares no such data element identifier — check it against the directory"
1306        );
1307        assert!(
1308            self.code_positions(data_element) == 1,
1309            "this data element identifier is declared at more than one position; address it positionally, or declare the repeat with ComponentRef::repeated"
1310        );
1311        let mut i = 0;
1312        while i < self.elements.len() {
1313            let el = &self.elements[i];
1314            if const_str_eq(el.data_element, data_element) {
1315                return el.position as usize - 1;
1316            }
1317            let mut c = 0;
1318            while c < el.components.len() {
1319                if const_str_eq(el.components[c].data_element, data_element) {
1320                    return el.position as usize - 1;
1321                }
1322                c += 1;
1323            }
1324            i += 1;
1325        }
1326        unreachable!()
1327    }
1328
1329    /// Zero-based component index for `data_element`, resolved at compile time.
1330    ///
1331    /// Returns `0` when the identifier names a data element rather than a
1332    /// component inside a composite — component 0 is the first (and for a simple
1333    /// element, only) component, so the same accessor works for both shapes.
1334    ///
1335    /// # Panics
1336    ///
1337    /// Panics when the identifier is unknown or appears at more than one
1338    /// position; see [`element_slot`][Self::element_slot].
1339    #[must_use]
1340    pub const fn component_slot(&self, data_element: &str) -> usize {
1341        assert!(
1342            self.code_positions(data_element) != 0,
1343            "this segment definition declares no such data element identifier — check it against the directory"
1344        );
1345        assert!(
1346            self.code_positions(data_element) == 1,
1347            "this data element identifier is declared at more than one position; address it positionally, or declare the repeat with ComponentRef::repeated"
1348        );
1349        let mut i = 0;
1350        while i < self.elements.len() {
1351            let el = &self.elements[i];
1352            if const_str_eq(el.data_element, data_element) {
1353                return 0;
1354            }
1355            let mut c = 0;
1356            while c < el.components.len() {
1357                if const_str_eq(el.components[c].data_element, data_element) {
1358                    return el.components[c].position as usize - 1;
1359                }
1360                c += 1;
1361            }
1362            i += 1;
1363        }
1364        unreachable!()
1365    }
1366
1367    /// `true` when `data_element` names a component *inside* a composite rather
1368    /// than a data element of the segment.
1369    ///
1370    /// Lets a caller — the derive macro, in practice — pick the right
1371    /// "missing required" error variant without a second lookup:
1372    /// [`EdifactError::MissingRequiredComponent`] rather than
1373    /// [`EdifactError::MissingRequiredElement`]. `component_slot` alone cannot
1374    /// answer this, because a code naming the *first* component of a composite
1375    /// also resolves to component index 0.
1376    ///
1377    /// Returns `false` for an unknown identifier; pair with
1378    /// [`code_positions`][Self::code_positions] when that case matters.
1379    #[must_use]
1380    pub const fn code_is_component(&self, data_element: &str) -> bool {
1381        let mut i = 0;
1382        while i < self.elements.len() {
1383            let el = &self.elements[i];
1384            let mut c = 0;
1385            while c < el.components.len() {
1386                if const_str_eq(el.components[c].data_element, data_element) {
1387                    return true;
1388                }
1389                c += 1;
1390            }
1391            i += 1;
1392        }
1393        false
1394    }
1395}
1396
1397impl SegmentLayout for SegmentDefinition {
1398    #[inline]
1399    fn layout_tag(&self) -> &str {
1400        self.tag
1401    }
1402
1403    fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError> {
1404        // One pass, not four: this runs per lookup on hot validation paths, and
1405        // composing the `const` helpers would rescan the table for each of the
1406        // count, the element index, and the component index.
1407        let mut hits = 0usize;
1408        let mut found = None;
1409        for el in self.elements {
1410            if el.data_element == data_element {
1411                hits += 1;
1412                found.get_or_insert(ElementPath::element(el.position as usize - 1));
1413            }
1414            for comp in el.components {
1415                if comp.data_element == data_element {
1416                    hits += 1;
1417                    found.get_or_insert(ElementPath::component(
1418                        el.position as usize - 1,
1419                        comp.position as usize - 1,
1420                    ));
1421                }
1422            }
1423        }
1424        resolve_outcome(self.tag, data_element, hits, found)
1425    }
1426
1427    fn slots(&self) -> Vec<LayoutSlot> {
1428        let mut out = Vec::new();
1429        for element in self.elements {
1430            if element.components.is_empty() {
1431                out.push(LayoutSlot {
1432                    element: element.position as usize - 1,
1433                    component: None,
1434                    data_element: element.data_element.to_owned(),
1435                    status: element.status,
1436                    element_status: element.status,
1437                });
1438                continue;
1439            }
1440            for component in element.components {
1441                out.push(LayoutSlot {
1442                    element: element.position as usize - 1,
1443                    component: Some(component.position as usize - 1),
1444                    data_element: component.data_element.to_owned(),
1445                    status: component.status,
1446                    element_status: element.status,
1447                });
1448            }
1449        }
1450        out
1451    }
1452}
1453
1454/// Turn a resolution scan's `(hit count, first match)` into a `Result`.
1455///
1456/// Shared by both [`SegmentLayout`] impls so the static and runtime tables
1457/// cannot drift on which condition maps to which error.
1458fn resolve_outcome(
1459    tag: &str,
1460    data_element: &str,
1461    hits: usize,
1462    found: Option<ElementPath>,
1463) -> Result<ElementPath, EdifactError> {
1464    match (hits, found) {
1465        (1, Some(path)) => Ok(path),
1466        (0, _) => Err(EdifactError::UnknownDataElement {
1467            tag: tag.to_owned(),
1468            data_element: data_element.to_owned(),
1469        }),
1470        _ => Err(EdifactError::AmbiguousDataElement {
1471            tag: tag.to_owned(),
1472            data_element: data_element.to_owned(),
1473        }),
1474    }
1475}
1476
1477/// Owned runtime equivalent of [`ElementRef`].
1478///
1479/// Used by [`DirectoryValidatorBuilder`] and [`DirectoryValidator::from_owned_definitions`]
1480/// to construct validators from data that is not available at compile time (e.g. loaded
1481/// from JSON or a database at startup).
1482///
1483/// Use [`OwnedElementRef::new_unchecked`] for compile-time-known positions (panics on invalid
1484/// input, no error handling noise) or [`OwnedElementRef::try_new`] when the position
1485/// comes from an external source and you need a `Result`. Fields are private to prevent
1486/// bypassing the position invariant through struct-literal syntax.
1487#[derive(Debug, Clone)]
1488pub struct OwnedElementRef {
1489    /// One-based element position.
1490    position: u8,
1491    /// UN/EDIFACT data element identifier.
1492    data_element: String,
1493    /// Requirement status.
1494    status: Status,
1495    /// Maximum repetition count.
1496    max_repeat: u8,
1497    /// The directory's representation for a *simple* element.
1498    repr: Option<Repr>,
1499    /// The representation from syntax version 4 onward, where it differs.
1500    repr_from_v4: Option<Repr>,
1501    /// Component definitions when this element is a composite; empty for a
1502    /// simple data element.
1503    components: Vec<OwnedComponentRef>,
1504}
1505
1506/// Owned runtime equivalent of [`ComponentRef`].
1507///
1508/// Attach these to an [`OwnedElementRef`] with
1509/// [`OwnedElementRef::with_components`] so that runtime-loaded definitions
1510/// support code-addressed access into composites, exactly like compile-time
1511/// [`SegmentDefinition`] tables do.
1512#[derive(Debug, Clone)]
1513pub struct OwnedComponentRef {
1514    /// One-based position of the first slot this component occupies.
1515    position: u8,
1516    /// UN/EDIFACT component data element identifier.
1517    data_element: String,
1518    /// Requirement status.
1519    status: Status,
1520    /// The directory's representation, when the definition states one.
1521    repr: Option<Repr>,
1522    /// The representation from syntax version 4 onward, where it differs.
1523    repr_from_v4: Option<Repr>,
1524    /// How many consecutive slots this component occupies.
1525    repeat_count: u8,
1526}
1527
1528impl OwnedComponentRef {
1529    /// Construct an owned component reference.
1530    ///
1531    /// # Panics
1532    ///
1533    /// Panics if `position` is `0` (positions are one-based).
1534    pub fn new_unchecked(position: u8, data_element: String, status: Status) -> Self {
1535        assert!(
1536            position != 0,
1537            "OwnedComponentRef::new_unchecked: position must be >= 1 (one-based), got 0"
1538        );
1539        Self {
1540            position,
1541            data_element,
1542            status,
1543            repeat_count: 1,
1544            repr: None,
1545            repr_from_v4: None,
1546        }
1547    }
1548
1549    /// Runtime counterpart of [`ComponentRef::repeated`].
1550    ///
1551    /// # Panics
1552    ///
1553    /// Panics if `position == 0` or `repeat_count == 0`.
1554    #[must_use]
1555    pub fn repeated(position: u8, data_element: String, status: Status, repeat_count: u8) -> Self {
1556        assert!(
1557            position != 0,
1558            "OwnedComponentRef::repeated: position must be >= 1 (one-based), got 0"
1559        );
1560        assert!(
1561            repeat_count != 0,
1562            "OwnedComponentRef::repeated: repeat_count must be >= 1"
1563        );
1564        Self {
1565            position,
1566            data_element,
1567            status,
1568            repeat_count,
1569            repr: None,
1570            repr_from_v4: None,
1571        }
1572    }
1573
1574    /// How many consecutive slots this component occupies.
1575    #[inline]
1576    #[must_use]
1577    pub fn repeat_count(&self) -> u8 {
1578        self.repeat_count
1579    }
1580
1581    /// Construct an owned component reference, returning an error for position `0`.
1582    ///
1583    /// # Errors
1584    ///
1585    /// Returns [`EdifactError::InvalidElementPosition`] if `position` is `0`.
1586    pub fn try_new(
1587        position: u8,
1588        data_element: String,
1589        status: Status,
1590    ) -> Result<Self, EdifactError> {
1591        if position == 0 {
1592            return Err(EdifactError::InvalidElementPosition);
1593        }
1594        Ok(Self {
1595            position,
1596            data_element,
1597            status,
1598            repeat_count: 1,
1599            repr: None,
1600            repr_from_v4: None,
1601        })
1602    }
1603
1604    /// One-based component position (always >= 1).
1605    #[inline]
1606    pub fn position(&self) -> u8 {
1607        self.position
1608    }
1609
1610    /// UN/EDIFACT component data element identifier.
1611    #[inline]
1612    pub fn data_element(&self) -> &str {
1613        &self.data_element
1614    }
1615
1616    /// Requirement status of this component.
1617    #[inline]
1618    pub fn status(&self) -> Status {
1619        self.status
1620    }
1621
1622    /// Attach the directory's representation, e.g. `an..35`.
1623    #[must_use]
1624    pub fn with_repr(mut self, repr: Repr) -> Self {
1625        self.repr = Some(repr);
1626        self
1627    }
1628
1629    /// The declared representation, if the definition states one.
1630    #[inline]
1631    #[must_use]
1632    pub fn repr(&self) -> Option<Repr> {
1633        self.repr
1634    }
1635
1636    /// Declare a representation that changed between syntax versions.
1637    ///
1638    /// See [`ComponentRef::with_repr_by_syntax_version`].
1639    #[must_use]
1640    pub fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
1641        self.repr = Some(up_to_v3);
1642        self.repr_from_v4 = Some(from_v4);
1643        self
1644    }
1645
1646    /// The representation used from syntax version 4 onward, when it differs.
1647    #[inline]
1648    #[must_use]
1649    pub fn repr_from_v4(&self) -> Option<Repr> {
1650        self.repr_from_v4
1651    }
1652}
1653
1654/// Owned runtime equivalent of [`SegmentDefinition`].
1655///
1656/// Used by [`DirectoryValidatorBuilder`] and [`DirectoryValidator::from_owned_definitions`].
1657///
1658/// Use [`OwnedSegmentDef::new_unchecked`] for compile-time-known tags (panics on invalid input,
1659/// no error handling noise) or [`OwnedSegmentDef::try_new`] when the tag comes from
1660/// an external source and you need a `Result`. Fields are private to prevent bypassing
1661/// the tag invariant through struct-literal syntax.
1662#[derive(Debug, Clone)]
1663pub struct OwnedSegmentDef {
1664    /// Segment tag (e.g. `"BGM"`).
1665    tag: String,
1666    /// Human-readable segment name.
1667    name: String,
1668    /// Ordered element definitions.
1669    elements: Vec<OwnedElementRef>,
1670}
1671
1672impl OwnedSegmentDef {
1673    /// Construct an owned segment definition.
1674    ///
1675    /// This is the ergonomic constructor for compile-time-known tags (e.g.
1676    /// `"BGM"`, `"UNH"`).  It panics immediately on invalid input so that
1677    /// call sites with literal tag strings require no `.unwrap()` / `.expect()`
1678    /// boilerplate.
1679    ///
1680    /// Use [`try_new`][Self::try_new] instead when the tag originates from an
1681    /// external source (user input, config file, database) and you need a
1682    /// `Result` to propagate errors gracefully.
1683    ///
1684    /// # Panics
1685    ///
1686    /// Panics if `tag` is not exactly three ASCII uppercase letters.
1687    pub fn new_unchecked(tag: String, name: String, elements: Vec<OwnedElementRef>) -> Self {
1688        assert!(
1689            tag.len() == 3 && tag.bytes().all(|b| b.is_ascii_uppercase()),
1690            "OwnedSegmentDef::new_unchecked: tag must be exactly three ASCII uppercase letters, got {tag:?}"
1691        );
1692        Self {
1693            tag,
1694            name,
1695            elements,
1696        }
1697    }
1698
1699    /// Construct an owned segment definition, returning an error for invalid tags.
1700    ///
1701    /// Prefer this over [`new_unchecked`][Self::new_unchecked] when the tag comes from an external
1702    /// source (user input, config file, database) and you want to handle the
1703    /// error without panicking.
1704    ///
1705    /// # Errors
1706    ///
1707    /// Returns [`EdifactError::InvalidSegmentTag`] if `tag` is not exactly three
1708    /// ASCII uppercase letters.
1709    pub fn try_new(
1710        tag: String,
1711        name: String,
1712        elements: Vec<OwnedElementRef>,
1713    ) -> Result<Self, EdifactError> {
1714        if tag.len() != 3 || !tag.bytes().all(|b| b.is_ascii_uppercase()) {
1715            return Err(EdifactError::InvalidSegmentTag(tag));
1716        }
1717        Ok(Self {
1718            tag,
1719            name,
1720            elements,
1721        })
1722    }
1723
1724    /// Segment tag (e.g. `"BGM"`).
1725    #[inline]
1726    pub fn tag(&self) -> &str {
1727        &self.tag
1728    }
1729
1730    /// Human-readable segment name.
1731    #[inline]
1732    pub fn name(&self) -> &str {
1733        &self.name
1734    }
1735
1736    /// Element definitions for this segment.
1737    #[inline]
1738    pub fn elements(&self) -> &[OwnedElementRef] {
1739        &self.elements
1740    }
1741
1742    /// Number of positions in this definition that carry `data_element`.
1743    ///
1744    /// Runtime counterpart of [`SegmentDefinition::code_positions`].
1745    #[must_use]
1746    pub fn code_positions(&self, data_element: &str) -> usize {
1747        self.elements
1748            .iter()
1749            .map(|el| {
1750                usize::from(el.data_element == data_element)
1751                    + el.components
1752                        .iter()
1753                        .filter(|c| c.data_element == data_element)
1754                        .count()
1755            })
1756            .sum()
1757    }
1758}
1759
1760impl SegmentLayout for OwnedSegmentDef {
1761    #[inline]
1762    fn layout_tag(&self) -> &str {
1763        &self.tag
1764    }
1765
1766    fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError> {
1767        let mut hits = 0usize;
1768        let mut found = None;
1769        for el in &self.elements {
1770            if el.data_element == data_element {
1771                hits += 1;
1772                found.get_or_insert(ElementPath::element(el.position as usize - 1));
1773            }
1774            for comp in &el.components {
1775                if comp.data_element == data_element {
1776                    hits += 1;
1777                    found.get_or_insert(ElementPath::component(
1778                        el.position as usize - 1,
1779                        comp.position as usize - 1,
1780                    ));
1781                }
1782            }
1783        }
1784        resolve_outcome(&self.tag, data_element, hits, found)
1785    }
1786
1787    fn slots(&self) -> Vec<LayoutSlot> {
1788        let mut out = Vec::new();
1789        for element in &self.elements {
1790            if element.components.is_empty() {
1791                out.push(LayoutSlot {
1792                    element: element.position as usize - 1,
1793                    component: None,
1794                    data_element: element.data_element.clone(),
1795                    status: element.status,
1796                    element_status: element.status,
1797                });
1798                continue;
1799            }
1800            for component in &element.components {
1801                out.push(LayoutSlot {
1802                    element: element.position as usize - 1,
1803                    component: Some(component.position as usize - 1),
1804                    data_element: component.data_element.clone(),
1805                    status: component.status,
1806                    element_status: element.status,
1807                });
1808            }
1809        }
1810        out
1811    }
1812}
1813
1814impl OwnedElementRef {
1815    /// Construct an owned element reference.
1816    ///
1817    /// This is the ergonomic constructor for compile-time-known positions.
1818    /// It panics immediately on invalid input so that call sites with literal
1819    /// position numbers require no `.unwrap()` / `.expect()` boilerplate.
1820    ///
1821    /// Use [`try_new`][Self::try_new] instead when the position originates from
1822    /// an external source (user input, config file, database) and you need a
1823    /// `Result` to propagate errors gracefully.
1824    ///
1825    /// # Panics
1826    ///
1827    /// Panics if `position` is `0` (positions are one-based).
1828    pub fn new_unchecked(
1829        position: u8,
1830        data_element: String,
1831        status: Status,
1832        max_repeat: u8,
1833    ) -> Self {
1834        assert!(
1835            position != 0,
1836            "OwnedElementRef::new_unchecked: position must be >= 1 (one-based), got 0"
1837        );
1838        Self {
1839            position,
1840            data_element,
1841            status,
1842            max_repeat,
1843            repr: None,
1844            repr_from_v4: None,
1845            components: Vec::new(),
1846        }
1847    }
1848
1849    /// Construct an owned element reference, returning an error for position `0`.
1850    ///
1851    /// Prefer this over [`new_unchecked`][Self::new_unchecked] when the position comes from an
1852    /// external source (user input, config file, database) and you want to
1853    /// handle the error without panicking.
1854    ///
1855    /// # Errors
1856    ///
1857    /// Returns [`EdifactError::InvalidElementPosition`] if `position` is `0`.
1858    pub fn try_new(
1859        position: u8,
1860        data_element: String,
1861        status: Status,
1862        max_repeat: u8,
1863    ) -> Result<Self, EdifactError> {
1864        if position == 0 {
1865            return Err(EdifactError::InvalidElementPosition);
1866        }
1867        Ok(Self {
1868            position,
1869            data_element,
1870            status,
1871            max_repeat,
1872            repr: None,
1873            repr_from_v4: None,
1874            components: Vec::new(),
1875        })
1876    }
1877
1878    /// Attach component definitions, marking this element as a composite.
1879    ///
1880    /// Declared components make code-addressed access resolve *into* the
1881    /// composite and activate the mandatory-component check in
1882    /// [`DirectoryValidator`].
1883    ///
1884    /// # Example
1885    ///
1886    /// ```rust
1887    /// use edifact_rs::{OwnedComponentRef, OwnedElementRef, Status};
1888    ///
1889    /// let dtm = OwnedElementRef::new_unchecked(1, "C507".to_owned(), Status::Mandatory, 1)
1890    ///     .with_components(vec![
1891    ///         OwnedComponentRef::new_unchecked(1, "2005".to_owned(), Status::Mandatory),
1892    ///         OwnedComponentRef::new_unchecked(2, "2380".to_owned(), Status::Conditional),
1893    ///     ]);
1894    /// assert_eq!(dtm.components().len(), 2);
1895    /// ```
1896    #[must_use]
1897    pub fn with_components(mut self, components: Vec<OwnedComponentRef>) -> Self {
1898        self.components = components;
1899        self
1900    }
1901
1902    /// Component definitions; empty when this is a simple data element.
1903    #[inline]
1904    pub fn components(&self) -> &[OwnedComponentRef] {
1905        &self.components
1906    }
1907
1908    /// One-based element position (always >= 1).
1909    #[inline]
1910    pub fn position(&self) -> u8 {
1911        self.position
1912    }
1913
1914    /// UN/EDIFACT data element identifier.
1915    #[inline]
1916    pub fn data_element(&self) -> &str {
1917        &self.data_element
1918    }
1919
1920    /// Requirement status of this element.
1921    #[inline]
1922    pub fn status(&self) -> Status {
1923        self.status
1924    }
1925
1926    /// Maximum repetition count for this element.
1927    #[inline]
1928    pub fn max_repeat(&self) -> u8 {
1929        self.max_repeat
1930    }
1931
1932    /// Attach the directory's representation for a simple data element.
1933    #[must_use]
1934    pub fn with_repr(mut self, repr: Repr) -> Self {
1935        self.repr = Some(repr);
1936        self
1937    }
1938
1939    /// The declared representation, if the definition states one.
1940    #[inline]
1941    #[must_use]
1942    pub fn repr(&self) -> Option<Repr> {
1943        self.repr
1944    }
1945
1946    /// Declare a representation that changed between syntax versions.
1947    ///
1948    /// See [`ComponentRef::with_repr_by_syntax_version`].
1949    #[must_use]
1950    pub fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
1951        self.repr = Some(up_to_v3);
1952        self.repr_from_v4 = Some(from_v4);
1953        self
1954    }
1955
1956    /// The representation used from syntax version 4 onward, when it differs.
1957    #[inline]
1958    #[must_use]
1959    pub fn repr_from_v4(&self) -> Option<Repr> {
1960        self.repr_from_v4
1961    }
1962}
1963
1964type SegmentLookupFn = Arc<dyn Fn(&str) -> Option<&'static SegmentDefinition> + Send + Sync>;
1965type IsCodeValidFn = Arc<dyn Fn(&str, &str) -> bool + Send + Sync>;
1966type SuggestCodeFn = Arc<dyn Fn(&str, &str) -> Option<&'static str> + Send + Sync>;
1967type ExpectedComponentsFn = Arc<dyn Fn(&str, usize) -> Option<u8> + Send + Sync>;
1968type AdditionalStructureRuleRefFn = fn(&Segment<'_>) -> Result<(), EdifactError>;
1969type AdditionalStructureRuleFn =
1970    Arc<dyn Fn(&Segment<'_>) -> Result<(), EdifactError> + Send + Sync>;
1971/// Returns the `(element_index, component_index, data_element_id)` tuples to
1972/// validate against a code list for the given segment tag.
1973type CodeListRulesFn = Arc<dyn Fn(&str) -> &'static [(usize, usize, &'static str)] + Send + Sync>;
1974/// Returns the mandatory segment tags for a given EDIFACT message type.
1975///
1976/// The slice should contain every tag that must appear at least once in a
1977/// conformant message of the given type.  The tags are also used to check
1978/// canonical ordering — their relative order in the returned slice is taken
1979/// as the expected order in the message.
1980type RequiredSegmentsFn = Arc<dyn Fn(&str) -> &'static [&'static str] + Send + Sync>;
1981
1982/// Internal enum that unifies lookup results from static and owned segment definitions.
1983///
1984/// Allows `validate_segment` to handle both code-generated (`&'static`) and
1985/// runtime-constructed ([`OwnedSegmentDef`]) definitions without duplication.
1986enum SegmentDefRef<'a> {
1987    Static(&'static SegmentDefinition),
1988    Owned(&'a OwnedSegmentDef),
1989}
1990
1991impl SegmentDefRef<'_> {
1992    /// Returns the highest defined element position (one-based → used directly as
1993    /// the maximum zero-based slot count for element-count validation).
1994    ///
1995    /// For owned definitions the highest `position` value may exceed the number
1996    /// of entries in the `elements` vec when positions are non-consecutive.
1997    fn max_element_position(&self) -> usize {
1998        match self {
1999            Self::Static(d) => d
2000                .elements
2001                .iter()
2002                .map(|e| e.position as usize)
2003                .max()
2004                .unwrap_or(0),
2005            Self::Owned(d) => d
2006                .elements
2007                .iter()
2008                .map(|e| e.position as usize)
2009                .max()
2010                .unwrap_or(0),
2011        }
2012    }
2013
2014    /// Returns the highest position number among mandatory elements (one-based).
2015    ///
2016    /// This equals the minimum number of elements that must be present in a
2017    /// segment: if the highest-positioned mandatory element is at position 5,
2018    /// the segment must supply at least 5 elements.
2019    fn last_mandatory_position(&self) -> usize {
2020        match self {
2021            Self::Static(d) => d
2022                .elements
2023                .iter()
2024                .filter(|e| e.status == Status::Mandatory)
2025                .map(|e| e.position as usize)
2026                .max()
2027                .unwrap_or(0),
2028            Self::Owned(d) => d
2029                .elements
2030                .iter()
2031                .filter(|e| e.status == Status::Mandatory)
2032                .map(|e| e.position as usize)
2033                .max()
2034                .unwrap_or(0),
2035        }
2036    }
2037
2038    /// Iterate over mandatory element positions without heap allocation.
2039    ///
2040    /// Calls `f(zero_based_index, data_element_id)` for each element whose
2041    /// status is [`Status::Mandatory`].  Returns `Err` immediately if `f`
2042    /// returns `Err`, short-circuiting the remaining elements.
2043    fn for_each_mandatory_position<E, F>(&self, mut f: F) -> Result<(), E>
2044    where
2045        F: FnMut(usize, &str) -> Result<(), E>,
2046    {
2047        match self {
2048            Self::Static(d) => {
2049                for e in d.elements.iter().filter(|e| e.status == Status::Mandatory) {
2050                    f((e.position as usize).saturating_sub(1), e.data_element)?;
2051                }
2052            }
2053            Self::Owned(d) => {
2054                for e in d.elements.iter().filter(|e| e.status == Status::Mandatory) {
2055                    f(
2056                        (e.position as usize).saturating_sub(1),
2057                        e.data_element.as_str(),
2058                    )?;
2059                }
2060            }
2061        }
2062        Ok(())
2063    }
2064
2065    /// Iterate over mandatory *component* positions without heap allocation.
2066    ///
2067    /// Calls `f(element_index, component_index, data_element_id)` — both indices
2068    /// zero-based — for every declared component whose status is
2069    /// [`Status::Mandatory`].  Definitions that declare no components (the shape
2070    /// every pre-0.13 directory table had) yield nothing, so this check is
2071    /// inert until a directory opts in by declaring composites with
2072    /// [`ElementRef::composite`].
2073    fn for_each_mandatory_component<E, F>(&self, mut f: F) -> Result<(), E>
2074    where
2075        F: FnMut(usize, usize, &str) -> Result<(), E>,
2076    {
2077        match self {
2078            Self::Static(d) => {
2079                for e in d.elements {
2080                    for c in e
2081                        .components
2082                        .iter()
2083                        .filter(|c| c.status == Status::Mandatory)
2084                    {
2085                        f(
2086                            (e.position as usize).saturating_sub(1),
2087                            (c.position as usize).saturating_sub(1),
2088                            c.data_element,
2089                        )?;
2090                    }
2091                }
2092            }
2093            Self::Owned(d) => {
2094                for e in &d.elements {
2095                    for c in e
2096                        .components
2097                        .iter()
2098                        .filter(|c| c.status == Status::Mandatory)
2099                    {
2100                        f(
2101                            (e.position as usize).saturating_sub(1),
2102                            (c.position as usize).saturating_sub(1),
2103                            c.data_element.as_str(),
2104                        )?;
2105                    }
2106                }
2107            }
2108        }
2109        Ok(())
2110    }
2111
2112    /// Number of declared component **slots** for the element at zero-based `index`.
2113    ///
2114    /// A component declared with [`ComponentRef::repeated`] occupies several
2115    /// slots, so this sums repeat counts rather than counting entries: counting
2116    /// entries would cap `C080` at two components and reject the four extra
2117    /// `3036` occurrences the composite is defined to carry.
2118    ///
2119    /// `None` when the element is not defined, or is defined without
2120    /// components — in which case its arity is not constrained by the layout.
2121    /// The declared maximum occurrence count for the element at `index`.
2122    fn max_repeat_at(&self, index: usize) -> Option<u8> {
2123        let position = u8::try_from(index.checked_add(1)?).ok()?;
2124        match self {
2125            Self::Static(d) => d
2126                .elements
2127                .iter()
2128                .find(|e| e.position == position)
2129                .map(ElementRef::max_repeat),
2130            Self::Owned(d) => d
2131                .elements
2132                .iter()
2133                .find(|e| e.position == position)
2134                .map(OwnedElementRef::max_repeat),
2135        }
2136    }
2137
2138    /// The representation required at `(element, component)`, if any.
2139    ///
2140    /// A composite states its representations on the components; a simple data
2141    /// element states one on the element itself and only at component 0.
2142    ///
2143    /// `syntax_version` selects between the two forms of a position that changed
2144    /// between versions; `None` accepts either, because guessing would reject
2145    /// conformant data.
2146    fn repr_at(
2147        &self,
2148        element: usize,
2149        component: usize,
2150        syntax_version: Option<u8>,
2151    ) -> Option<ReprRequirement> {
2152        let position = u8::try_from(element.checked_add(1)?).ok()?;
2153        let component_position = u8::try_from(component.checked_add(1)?).ok()?;
2154        match self {
2155            Self::Static(d) => {
2156                let element = d.elements.iter().find(|e| e.position == position)?;
2157                if element.components.is_empty() {
2158                    return if component == 0 {
2159                        select_repr(element.repr(), element.repr_from_v4(), syntax_version)
2160                    } else {
2161                        None
2162                    };
2163                }
2164                let component_ref = element.components.iter().find(|c| {
2165                    // A component declared with `repeated` spans several
2166                    // consecutive slots, all of the same representation.
2167                    let first = c.position();
2168                    let last = first.saturating_add(c.repeat_count().saturating_sub(1));
2169                    (first..=last).contains(&component_position)
2170                })?;
2171                select_repr(
2172                    component_ref.repr(),
2173                    component_ref.repr_from_v4(),
2174                    syntax_version,
2175                )
2176            }
2177            Self::Owned(d) => {
2178                let element = d.elements.iter().find(|e| e.position == position)?;
2179                if element.components.is_empty() {
2180                    return if component == 0 {
2181                        select_repr(
2182                            OwnedElementRef::repr(element),
2183                            OwnedElementRef::repr_from_v4(element),
2184                            syntax_version,
2185                        )
2186                    } else {
2187                        None
2188                    };
2189                }
2190                let component_ref = element.components.iter().find(|c| {
2191                    let first = c.position();
2192                    let last = first.saturating_add(c.repeat_count().saturating_sub(1));
2193                    (first..=last).contains(&component_position)
2194                })?;
2195                select_repr(
2196                    component_ref.repr(),
2197                    component_ref.repr_from_v4(),
2198                    syntax_version,
2199                )
2200            }
2201        }
2202    }
2203
2204    fn declared_component_count(&self, index: usize) -> Option<u8> {
2205        let position = u8::try_from(index.checked_add(1)?).ok()?;
2206        let count: u32 = match self {
2207            Self::Static(d) => d
2208                .elements
2209                .iter()
2210                .find(|e| e.position == position)
2211                .map(|e| e.components.iter().map(|c| u32::from(c.repeat_count)).sum())?,
2212            Self::Owned(d) => d
2213                .elements
2214                .iter()
2215                .find(|e| e.position == position)
2216                .map(|e| e.components.iter().map(|c| u32::from(c.repeat_count)).sum())?,
2217        };
2218        if count == 0 {
2219            return None;
2220        }
2221        u8::try_from(count).ok()
2222    }
2223}
2224
2225/// Read the syntax version number from `UNB` S001 DE 0002.
2226///
2227/// `None` when the slice carries no readable `UNB` — a message window, say.
2228fn detect_syntax_version(segments: &[Segment<'_>]) -> Option<u8> {
2229    segments
2230        .iter()
2231        .find(|s| s.tag == "UNB")
2232        .and_then(|unb| unb.component_str(0, 1))
2233        .and_then(|version| version.parse().ok())
2234}
2235
2236/// Pick the representation that applies to `syntax_version`.
2237///
2238/// Version 4 onward uses `from_v4` when the definition states one. An unknown
2239/// version accepts either, because a definition that distinguishes them is
2240/// distinguishing a real incompatibility — guessing would reject conformant data
2241/// from whichever version we guessed against.
2242fn select_repr(
2243    base: Option<Repr>,
2244    from_v4: Option<Repr>,
2245    syntax_version: Option<u8>,
2246) -> Option<ReprRequirement> {
2247    match (base, from_v4) {
2248        (_, None) => base.map(ReprRequirement::single),
2249        (None, Some(v4)) => Some(ReprRequirement::single(v4)),
2250        (Some(base), Some(v4)) => Some(match syntax_version {
2251            Some(version) if version >= 4 => ReprRequirement::single(v4),
2252            Some(_) => ReprRequirement::single(base),
2253            None => ReprRequirement {
2254                primary: base,
2255                alternative: Some(v4),
2256            },
2257        }),
2258    }
2259}
2260
2261/// Default required-segments mapping used when no custom function is provided.
2262///
2263/// Returns the universal minimum: every EDIFACT message must begin with `UNH`
2264/// and end with `UNT`.  Message-type-specific mandatory segments (such as
2265/// `BGM` for ORDERS/INVOIC) must be enforced by a
2266/// [`ProfileRulePack`][crate::ProfileRulePack] or a custom
2267/// [`DirectoryValidatorBuilder::with_required_segments`] function to avoid
2268/// false positives for message types that do not require `BGM`.
2269fn default_required_segments(_message_type: &str) -> &'static [&'static str] {
2270    &["UNH", "UNT"]
2271}
2272
2273/// Code-list validation rules common to all UN/EDIFACT directory releases.
2274///
2275/// Each entry is `(element_index, component_index, data_element_id)`.
2276/// `element_index` and `component_index` are zero-based.
2277///
2278/// Covers the most frequently validated qualifier/code elements across ORDERS,
2279/// INVOIC, and similar message types.
2280pub(crate) fn base_code_list_rules(tag: &str) -> &'static [(usize, usize, &'static str)] {
2281    match tag {
2282        "BGM" => &[(0, 0, "1001")],
2283        "DTM" => &[(0, 0, "2005")],
2284        "NAD" => &[(0, 0, "3035")],
2285        "QTY" => &[(0, 0, "6063")],
2286        "RFF" => &[(0, 0, "1153")],
2287        "MOA" => &[(0, 0, "5025")],
2288        "PRI" => &[(0, 0, "5125")],
2289        "LOC" => &[(0, 0, "3227")],
2290        _ => &[],
2291    }
2292}
2293
2294/// Shared validator implementation that is configured per UN/EDIFACT directory release.
2295///
2296/// # Scope and limitations
2297///
2298/// `DirectoryValidator` validates individual segment *content* (element counts,
2299/// component counts, code-list values, and conditional rules) and checks that
2300/// every *mandatory* segment type is present at least once.  It does **not**
2301/// validate segment *sequence* or *repetition cardinality* — i.e., it cannot
2302/// tell you that a `BGM` segment appears more than once, or that a `RFF` group
2303/// appears in the wrong position.  Full sequence validation requires a
2304/// state-machine per message type (UN/EDIFACT Segment Tables) which is outside
2305/// the scope of this implementation.
2306#[derive(Clone)]
2307pub struct DirectoryValidator {
2308    directory_id: String,
2309    segment_lookup: SegmentLookupFn,
2310    /// Runtime-owned segment definitions (from builder / JSON / DB).
2311    ///
2312    /// When `Some`, takes precedence over `segment_lookup` for tag resolution.
2313    owned_defs: Option<Arc<Vec<OwnedSegmentDef>>>,
2314    /// Tag -> index into `owned_defs`.  Without this, `resolve_def` was a linear
2315    /// scan per segment, making validation O(n_segments x n_definitions).
2316    owned_index: Option<Arc<std::collections::HashMap<String, usize>>>,
2317    is_code_valid: IsCodeValidFn,
2318    suggest_code: SuggestCodeFn,
2319    expected_components: ExpectedComponentsFn,
2320    code_list_rules: CodeListRulesFn,
2321    additional_structure_rule: Option<AdditionalStructureRuleFn>,
2322    /// Configurable mapping from message type to required segment tags.
2323    required_segments: RequiredSegmentsFn,
2324    message_type: Option<String>,
2325    enforce_known_tags: bool,
2326    structure_checks: bool,
2327    code_list_checks: bool,
2328}
2329
2330impl std::fmt::Debug for DirectoryValidator {
2331    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2332        f.debug_struct("DirectoryValidator")
2333            .field("directory_id", &self.directory_id)
2334            .field("message_type", &self.message_type)
2335            .field("enforce_known_tags", &self.enforce_known_tags)
2336            .field("structure_checks", &self.structure_checks)
2337            .field("code_list_checks", &self.code_list_checks)
2338            .finish_non_exhaustive()
2339    }
2340}
2341
2342impl DirectoryValidator {
2343    /// Create a validator for a specific directory release with injected lookup/check hooks.
2344    pub fn new(
2345        directory_id: &'static str,
2346        segment_lookup: fn(&str) -> Option<&'static SegmentDefinition>,
2347        is_code_valid: fn(&str, &str) -> bool,
2348        suggest_code: fn(&str, &str) -> Option<&'static str>,
2349        expected_components: fn(&str, usize) -> Option<u8>,
2350        additional_structure_rule: Option<AdditionalStructureRuleRefFn>,
2351    ) -> Self {
2352        Self {
2353            directory_id: directory_id.to_owned(),
2354            segment_lookup: Arc::new(segment_lookup),
2355            owned_defs: None,
2356            owned_index: None,
2357            is_code_valid: Arc::new(is_code_valid),
2358            suggest_code: Arc::new(suggest_code),
2359            expected_components: Arc::new(expected_components),
2360            code_list_rules: Arc::new(base_code_list_rules),
2361            additional_structure_rule: additional_structure_rule
2362                .map(|f| Arc::new(f) as AdditionalStructureRuleFn),
2363            required_segments: Arc::new(default_required_segments),
2364            message_type: None,
2365            enforce_known_tags: true,
2366            structure_checks: true,
2367            code_list_checks: true,
2368        }
2369    }
2370
2371    /// Create a validator from a static slice of [`SegmentDefinition`]s.
2372    ///
2373    /// This is the preferred constructor when code-generating directory data as
2374    /// a `static` array: no manual fn-pointer boilerplate is required.
2375    ///
2376    /// Code-list checks are **disabled** by default (the built-in `is_code_valid`
2377    /// always returns `true`).  Call [`with_code_list_rules`][Self::with_code_list_rules]
2378    /// to register directory-specific rules that actually validate code values.
2379    ///
2380    /// # Example
2381    ///
2382    /// ```rust,ignore
2383    /// static MY_SEGMENTS: &[SegmentDefinition] = &[ /* … */ ];
2384    ///
2385    /// let validator = DirectoryValidator::from_definitions(MY_SEGMENTS)
2386    ///     .with_code_list_rules(my_code_list_rules);
2387    /// ```
2388    pub fn from_definitions(definitions: &'static [SegmentDefinition]) -> Self {
2389        let lookup_map: std::collections::HashMap<&'static str, &'static SegmentDefinition> =
2390            definitions.iter().map(|d| (d.tag, d)).collect();
2391        let lookup_map = Arc::new(lookup_map);
2392        Self {
2393            directory_id: "custom".to_owned(),
2394            segment_lookup: Arc::new(move |tag: &str| lookup_map.get(tag).copied()),
2395            owned_defs: None,
2396            owned_index: None,
2397            is_code_valid: Arc::new(|_de: &str, _code: &str| true),
2398            suggest_code: Arc::new(|_de: &str, _code: &str| None),
2399            expected_components: Arc::new(|_tag: &str, _idx: usize| None),
2400            code_list_rules: Arc::new(base_code_list_rules),
2401            additional_structure_rule: None,
2402            required_segments: Arc::new(default_required_segments),
2403            message_type: None,
2404            enforce_known_tags: true,
2405            structure_checks: true,
2406            code_list_checks: false,
2407        }
2408    }
2409
2410    /// Create a validator from a runtime-owned collection of segment definitions.
2411    ///
2412    /// Use this (or [`DirectoryValidatorBuilder`]) when segment definitions are
2413    /// loaded from an external source at startup (JSON, database, YAML, …) rather
2414    /// than being known at compile time.
2415    ///
2416    /// Code-list checks are **disabled** by default; enable them by chaining
2417    /// [`with_code_list_rules`][Self::with_code_list_rules] and setting
2418    /// `is_code_valid` via a custom [`new`][Self::new] call or by subclassing
2419    /// the builder.
2420    ///
2421    /// # Example
2422    ///
2423    /// ```rust,ignore
2424    /// let defs = vec![
2425    ///     OwnedSegmentDef::new_unchecked(
2426    ///         "BGM".to_owned(),
2427    ///         "Beginning of message".to_owned(),
2428    ///         vec![OwnedElementRef::new_unchecked(1, "C002".to_owned(), Status::Mandatory, 1)],
2429    ///     ),
2430    /// ];
2431    /// let validator = DirectoryValidator::from_owned_definitions(defs)
2432    ///     .with_directory_id("runtime-profile");
2433    /// ```
2434    pub fn from_owned_definitions(definitions: Vec<OwnedSegmentDef>) -> Self {
2435        Self {
2436            directory_id: "custom".to_owned(),
2437            // The static lookup is never consulted when `owned_defs` is `Some`.
2438            segment_lookup: Arc::new(|_| None),
2439            owned_index: Some(Arc::new(
2440                definitions
2441                    .iter()
2442                    .enumerate()
2443                    .map(|(i, d)| (d.tag.clone(), i))
2444                    .collect(),
2445            )),
2446            owned_defs: Some(Arc::new(definitions)),
2447            is_code_valid: Arc::new(|_de: &str, _code: &str| true),
2448            suggest_code: Arc::new(|_de: &str, _code: &str| None),
2449            expected_components: Arc::new(|_tag: &str, _idx: usize| None),
2450            code_list_rules: Arc::new(base_code_list_rules),
2451            additional_structure_rule: None,
2452            required_segments: Arc::new(default_required_segments),
2453            message_type: None,
2454            enforce_known_tags: true,
2455            structure_checks: true,
2456            code_list_checks: false,
2457        }
2458    }
2459
2460    /// Set the directory identifier string (used in error messages).
2461    pub fn with_directory_id(mut self, id: impl Into<String>) -> Self {
2462        self.directory_id = id.into();
2463        self
2464    }
2465
2466    /// Override the code-list rules function.
2467    ///
2468    /// Directories can supply a directory-specific implementation that extends or
2469    /// replaces the base rules from `base_code_list_rules`.
2470    pub fn with_code_list_rules(
2471        mut self,
2472        f: impl Fn(&str) -> &'static [(usize, usize, &'static str)] + Send + Sync + 'static,
2473    ) -> Self {
2474        self.code_list_rules = Arc::new(f);
2475        self
2476    }
2477
2478    /// Enable only structure checks and disable code-list checks.
2479    pub fn structure_only(mut self) -> Self {
2480        self.structure_checks = true;
2481        self.code_list_checks = false;
2482        self
2483    }
2484
2485    /// Enable only code-list checks and disable structure checks.
2486    pub fn code_list_only(mut self) -> Self {
2487        self.structure_checks = false;
2488        self.code_list_checks = true;
2489        self
2490    }
2491
2492    /// Configure whether unknown segment tags should be rejected.
2493    pub fn enforce_known_tags(mut self, enforce: bool) -> Self {
2494        self.enforce_known_tags = enforce;
2495        self
2496    }
2497
2498    /// Override the required-segments mapping used for structural validation.
2499    ///
2500    /// The supplied function receives an EDIFACT message type string (e.g. `"ORDERS"`)
2501    /// and must return a `'static` slice of segment tags that are mandatory for that
2502    /// type.  The tags are checked both for *presence* and for *canonical ordering*
2503    /// within the message.
2504    ///
2505    /// # Example
2506    ///
2507    /// ```rust,ignore
2508    /// fn my_required_segments(msg_type: &str) -> &'static [&'static str] {
2509    ///     match msg_type {
2510    ///         "DESADV" => &["UNH", "BGM", "SHP", "UNT"],
2511    ///         "INVOIC" => &["UNH", "BGM", "MOA", "UNT"],
2512    ///         _ => &["UNH", "UNT"],
2513    ///     }
2514    /// }
2515    ///
2516    /// let validator = DirectoryValidator::from_definitions(DEFS)
2517    ///     .with_required_segments(my_required_segments);
2518    /// ```
2519    pub fn with_required_segments(
2520        mut self,
2521        f: impl Fn(&str) -> &'static [&'static str] + Send + Sync + 'static,
2522    ) -> Self {
2523        self.required_segments = Arc::new(f);
2524        self
2525    }
2526
2527    fn detect_message_type(&self, segments: &[Segment<'_>]) -> Option<String> {
2528        if let Some(explicit) = self.message_type.as_deref() {
2529            return Some(explicit.to_owned());
2530        }
2531
2532        segments
2533            .iter()
2534            .find(|s| s.tag == "UNH")
2535            .and_then(|s| s.get_element(1))
2536            .and_then(|e| e.get_component(0))
2537            .map(str::to_owned)
2538    }
2539
2540    /// Count the non-trailing-empty components in element `element_idx` of `seg`.
2541    ///
2542    /// Per ISO 9735-1 §8.7.2 ("Trailing empty component data elements may be omitted"),
2543    /// a sender is not required to transmit trailing empty components; this function
2544    /// therefore strips them before checking against the expected count so that
2545    /// conformant messages with omitted trailing components are still accepted.
2546    ///
2547    /// # Examples
2548    ///
2549    /// - `DTM+137:20200101:` has three declared components but only 2 non-empty → effective=2
2550    /// - `NAD+MS++::293` has a composite with 3 components, last two empty → effective=1
2551    fn effective_component_count(seg: &Segment<'_>, element_idx: usize) -> Option<u8> {
2552        let elem = seg.elements.get(element_idx)?;
2553        let mut count = elem.components.len();
2554        while count > 0 && elem.components[count - 1].0.as_ref().is_empty() {
2555            count -= 1;
2556        }
2557        u8::try_from(count).ok()
2558    }
2559
2560    fn collect_component_count_issues(
2561        &self,
2562        seg: &Segment<'_>,
2563        def: &SegmentDefRef<'_>,
2564        out: &mut Vec<EdifactError>,
2565    ) {
2566        for idx in 0..seg.elements.len() {
2567            let actual = Self::effective_component_count(seg, idx).unwrap_or(0);
2568            // The `expected_components` hook is an exact count and wins when set.
2569            if let Some(expected) = (self.expected_components)(seg.tag(), idx) {
2570                if actual != expected {
2571                    out.push(EdifactError::InvalidComponentCount {
2572                        tag: seg.tag().to_owned(),
2573                        element_index: idx,
2574                        expected,
2575                        actual,
2576                        span: seg.element_span(idx).unwrap_or(seg.span),
2577                    });
2578                }
2579                continue;
2580            }
2581            // Otherwise a composite that declares its components caps them:
2582            // more components than the directory defines is a structural error,
2583            // while fewer is normal (conditional components may be omitted).
2584            if let Some(declared) = def.declared_component_count(idx) {
2585                if actual > declared {
2586                    out.push(EdifactError::InvalidComponentCount {
2587                        tag: seg.tag().to_owned(),
2588                        element_index: idx,
2589                        expected: declared,
2590                        actual,
2591                        span: seg.element_span(idx).unwrap_or(seg.span),
2592                    });
2593                }
2594            }
2595        }
2596    }
2597
2598    /// Enforce each element's declared maximum number of occurrences.
2599    ///
2600    /// `max_repeat` had been carried on every `ElementRef` since the type
2601    /// existed, exposed by a getter, and read by nothing — so a definition that
2602    /// said "this element occurs once" constrained nothing at all, and a caller
2603    /// who wrote it believed otherwise.
2604    fn collect_repetition_issues(
2605        &self,
2606        seg: &Segment<'_>,
2607        def: &SegmentDefRef<'_>,
2608        out: &mut Vec<EdifactError>,
2609    ) {
2610        for (index, element) in seg.elements.iter().enumerate() {
2611            let Some(max) = def.max_repeat_at(index) else {
2612                continue;
2613            };
2614            // A declared maximum of zero would forbid the element outright,
2615            // which is what `Status` is for; treat it as "unconstrained".
2616            if max == 0 {
2617                continue;
2618            }
2619            let actual = element.repeat_count();
2620            if actual > usize::from(max) {
2621                out.push(EdifactError::TooManyRepetitions {
2622                    tag: seg.tag().to_owned(),
2623                    element_index: index,
2624                    max,
2625                    actual,
2626                    span: element.span,
2627                });
2628            }
2629        }
2630    }
2631
2632    /// Check every populated value against its declared representation.
2633    ///
2634    /// Only positions the definition actually states a representation for are
2635    /// checked, so a partial table stays useful rather than becoming a source of
2636    /// false findings.
2637    fn collect_representation_issues(
2638        &self,
2639        seg: &Segment<'_>,
2640        def: &SegmentDefRef<'_>,
2641        syntax_version: Option<u8>,
2642        out: &mut Vec<EdifactError>,
2643    ) {
2644        for (element_index, element) in seg.elements.iter().enumerate() {
2645            for occurrence in element.repetitions() {
2646                for (component_index, (value, span)) in occurrence.iter().enumerate() {
2647                    // An empty value is an absent one (§8.1); its presence is
2648                    // the mandatory check's business, not the representation's.
2649                    if value.is_empty() {
2650                        continue;
2651                    }
2652                    let Some(repr) = def.repr_at(element_index, component_index, syntax_version)
2653                    else {
2654                        continue;
2655                    };
2656                    if !repr.permits_characters(value) {
2657                        out.push(EdifactError::InvalidCharacterType {
2658                            tag: seg.tag().to_owned(),
2659                            element_index,
2660                            component_index,
2661                            repr: repr.to_string(),
2662                            value: value.to_string(),
2663                            span: *span,
2664                        });
2665                        // The length of a value that is not of the declared
2666                        // class is not meaningful — §10's numeric count in
2667                        // particular assumes a numeric value.
2668                        continue;
2669                    }
2670                    self.collect_insignificant_characters(
2671                        seg,
2672                        element_index,
2673                        component_index,
2674                        value,
2675                        *span,
2676                        repr.primary,
2677                        out,
2678                    );
2679                    if repr.permits_length(value) {
2680                        continue;
2681                    }
2682                    let actual = repr.measure(value);
2683                    out.push(if repr.is_too_short(value) {
2684                        EdifactError::DataElementTooShort {
2685                            tag: seg.tag().to_owned(),
2686                            element_index,
2687                            component_index,
2688                            repr: repr.to_string(),
2689                            actual,
2690                            span: *span,
2691                        }
2692                    } else {
2693                        EdifactError::DataElementTooLong {
2694                            tag: seg.tag().to_owned(),
2695                            element_index,
2696                            component_index,
2697                            repr: repr.to_string(),
2698                            actual,
2699                            span: *span,
2700                        }
2701                    });
2702                }
2703            }
2704        }
2705    }
2706
2707    /// Report characters ISO 9735-1 §9.1 requires the sender to suppress.
2708    ///
2709    /// Only **variable length** elements are covered, which is what §9.1 says:
2710    /// a fixed-length numeric field is padded with leading zeroes by design, and
2711    /// a fixed-length text one with trailing spaces.
2712    #[allow(clippy::too_many_arguments)]
2713    fn collect_insignificant_characters(
2714        &self,
2715        seg: &Segment<'_>,
2716        element_index: usize,
2717        component_index: usize,
2718        value: &str,
2719        span: crate::Span,
2720        repr: Repr,
2721        out: &mut Vec<EdifactError>,
2722    ) {
2723        if repr.is_fixed() {
2724            return;
2725        }
2726        let kind = match repr.kind() {
2727            ReprKind::Numeric => {
2728                let digits = value.strip_prefix('-').unwrap_or(value);
2729                // "Nevertheless, a single zero before a decimal mark is
2730                // allowed", so `0.5` is correct and only `00…` is not.
2731                let leading_zeroes = digits.starts_with('0')
2732                    && digits.len() > 1
2733                    && !digits.starts_with("0.")
2734                    && !digits.starts_with("0,");
2735                if !leading_zeroes {
2736                    return;
2737                }
2738                Insignificant::LeadingZeroes
2739            }
2740            ReprKind::Alphabetic | ReprKind::Alphanumeric => {
2741                if !value.ends_with(' ') {
2742                    return;
2743                }
2744                Insignificant::TrailingSpaces
2745            }
2746        };
2747        out.push(EdifactError::InsignificantCharacters {
2748            tag: seg.tag().to_owned(),
2749            element_index,
2750            component_index,
2751            kind,
2752            span,
2753        });
2754    }
2755
2756    fn collect_code_list_issues(&self, seg: &Segment<'_>, out: &mut Vec<EdifactError>) {
2757        for (elem_idx, comp_idx, de) in (self.code_list_rules)(seg.tag()) {
2758            let value = seg
2759                .get_element(*elem_idx)
2760                .and_then(|e| e.get_component(*comp_idx))
2761                .unwrap_or("");
2762            if !value.is_empty() && !(self.is_code_valid)(de, value) {
2763                let suggestion = (self.suggest_code)(de, value);
2764                // Point at the offending *value*, not the whole segment, so
2765                // rendered diagnostics underline the code that failed.
2766                let span = seg
2767                    .get_element(*elem_idx)
2768                    .and_then(|e| e.component_span(*comp_idx))
2769                    .unwrap_or(seg.span);
2770                out.push(EdifactError::InvalidCodeValue {
2771                    tag: seg.tag().to_owned(),
2772                    element_index: *elem_idx,
2773                    value: value.to_owned(),
2774                    code_list: (*de).to_owned(),
2775                    span,
2776                    suggestion,
2777                });
2778            }
2779        }
2780    }
2781}
2782
2783impl DirectoryValidator {
2784    fn resolve_def<'a>(&'a self, tag: &str) -> Option<SegmentDefRef<'a>> {
2785        if let Some(owned) = &self.owned_defs {
2786            let index = self.owned_index.as_ref()?;
2787            owned.get(*index.get(tag)?).map(SegmentDefRef::Owned)
2788        } else {
2789            (self.segment_lookup)(tag).map(SegmentDefRef::Static)
2790        }
2791    }
2792
2793    /// Check one segment, appending **every** violation found to `out`.
2794    ///
2795    /// Reporting continues past the first fault: a segment missing two mandatory
2796    /// elements and carrying an invalid code is three findings, and a validator
2797    /// whose whole purpose is an exhaustive report has no business hiding two of
2798    /// them.  Only the checks that cannot proceed without a resolved definition
2799    /// short-circuit.
2800    fn collect_segment_issues(
2801        &self,
2802        seg: &Segment<'_>,
2803        syntax_version: Option<u8>,
2804        out: &mut Vec<EdifactError>,
2805    ) {
2806        if !self.structure_checks && !self.code_list_checks {
2807            return;
2808        }
2809
2810        let Some(def) = self.resolve_def(seg.tag()) else {
2811            if self.structure_checks && self.enforce_known_tags {
2812                out.push(EdifactError::InvalidSegmentForMessage {
2813                    tag: seg.tag().to_owned(),
2814                    message_type: self
2815                        .message_type
2816                        .clone()
2817                        .unwrap_or_else(|| self.directory_id.clone()),
2818                    span: seg.tag_span,
2819                });
2820            }
2821            // Without a definition there is nothing further to check against.
2822            return;
2823        };
2824
2825        if self.structure_checks {
2826            let max_elements = def.max_element_position();
2827            let min_elements = def.last_mandatory_position();
2828            let actual = seg.elements.len();
2829            if actual < min_elements || actual > max_elements {
2830                out.push(EdifactError::InvalidElementCount {
2831                    tag: seg.tag().to_owned(),
2832                    min: min_elements,
2833                    max: max_elements,
2834                    actual,
2835                    span: seg.span,
2836                });
2837            }
2838
2839            def.for_each_mandatory_position::<std::convert::Infallible, _>(|idx, _de| {
2840                let is_present = seg.elements.get(idx).is_some_and(|elem| {
2841                    elem.components.iter().any(|(c, _)| !c.as_ref().is_empty())
2842                });
2843                if !is_present {
2844                    out.push(EdifactError::MissingRequiredElement {
2845                        tag: seg.tag().to_owned(),
2846                        element_index: idx,
2847                    });
2848                }
2849                Ok(())
2850            })
2851            .unwrap_or_else(|never| match never {});
2852
2853            // Mandatory *components* inside declared composites.  Only fires for
2854            // definitions built with `ElementRef::composite` / `with_components`;
2855            // an element that is absent entirely is already reported above as a
2856            // missing element, so only present elements are checked here.
2857            def.for_each_mandatory_component::<std::convert::Infallible, _>(
2858                |elem_idx, comp_idx, _de| {
2859                    let Some(elem) = seg.elements.get(elem_idx) else {
2860                        return Ok(());
2861                    };
2862                    let present = elem
2863                        .get_component(comp_idx)
2864                        .is_some_and(|value| !value.is_empty());
2865                    if !present {
2866                        out.push(EdifactError::MissingRequiredComponent {
2867                            tag: seg.tag().to_owned(),
2868                            element_index: elem_idx,
2869                            component_index: comp_idx,
2870                        });
2871                    }
2872                    Ok(())
2873                },
2874            )
2875            .unwrap_or_else(|never| match never {});
2876
2877            self.collect_component_count_issues(seg, &def, out);
2878            self.collect_repetition_issues(seg, &def, out);
2879            self.collect_representation_issues(seg, &def, syntax_version, out);
2880
2881            if let Some(rule) = &self.additional_structure_rule {
2882                if let Err(error) = rule(seg) {
2883                    out.push(error);
2884                }
2885            }
2886        }
2887
2888        if self.code_list_checks {
2889            self.collect_code_list_issues(seg, out);
2890        }
2891    }
2892}
2893
2894impl Validator for DirectoryValidator {
2895    fn set_message_type(&mut self, message_type: Option<&str>) {
2896        self.message_type = message_type.map(str::to_owned);
2897    }
2898
2899    fn validate_batch(
2900        &self,
2901        segments: &[Segment<'_>],
2902        report: &mut ValidationReport,
2903        _context: &ValidationRuleContext<'_>,
2904    ) {
2905        // The syntax version decides which form of a version-dependent
2906        // representation applies; `UNB` S001 DE 0002 is where it is stated.
2907        let syntax_version = detect_syntax_version(segments);
2908        let mut issues = Vec::new();
2909        for seg in segments {
2910            self.collect_segment_issues(seg, syntax_version, &mut issues);
2911            for err in issues.drain(..) {
2912                report_error(report, err);
2913            }
2914        }
2915
2916        if self.structure_checks {
2917            if let Some(message_type) = self.detect_message_type(segments) {
2918                // One pass recording each tag's first index answers both the
2919                // presence and the ordering question.  The previous shape ran two
2920                // full scans *per required tag* and invoked `required_segments`
2921                // twice, which is O(|required| x n) on every batch.
2922                let mut first_index: std::collections::HashMap<&str, usize> =
2923                    std::collections::HashMap::with_capacity(segments.len());
2924                for (i, seg) in segments.iter().enumerate() {
2925                    first_index.entry(seg.tag()).or_insert(i);
2926                }
2927
2928                let required = (self.required_segments)(&message_type);
2929                for required_tag in required {
2930                    if !first_index.contains_key(*required_tag) {
2931                        report.add_error(
2932                            ValidationIssue::new(
2933                                ValidationSeverity::Error,
2934                                format!(
2935                                    "required segment {} missing for message type {}",
2936                                    required_tag, message_type
2937                                ),
2938                            )
2939                            .with_segment(*required_tag)
2940                            .with_suggestion("Add the mandatory segment at the correct position"),
2941                        );
2942                    }
2943                }
2944
2945                let mut last_idx = None;
2946                for tag in required {
2947                    if let Some(&idx) = first_index.get(*tag) {
2948                        if let Some(prev) = last_idx {
2949                            if idx < prev {
2950                                report.add_error(
2951                                    ValidationIssue::new(
2952                                        ValidationSeverity::Error,
2953                                        format!(
2954                                            "segment sequence violation for message type {}: '{}' appears out of order",
2955                                            message_type, tag
2956                                        ),
2957                                    )
2958                                    .with_segment(*tag)
2959                                    .with_suggestion(
2960                                        "Ensure required segments follow UN/EDIFACT canonical order",
2961                                    ),
2962                                );
2963                            }
2964                        }
2965                        last_idx = Some(idx);
2966                    }
2967                }
2968            }
2969        }
2970    }
2971}
2972
2973// ── DirectoryValidatorBuilder ─────────────────────────────────────────────────
2974
2975/// Builder for [`DirectoryValidator`] using runtime-owned segment definitions.
2976///
2977/// Use this when segment definitions are loaded from an external source at
2978/// startup (JSON, database, YAML, …) rather than being available as `static`
2979/// arrays at compile time.
2980///
2981/// # Example
2982///
2983/// ```rust,ignore
2984/// let validator = DirectoryValidatorBuilder::new("my-profile")
2985///     .add_segment(
2986///         OwnedSegmentDef::new_unchecked(
2987///             "BGM".to_owned(),
2988///             "Beginning of message".to_owned(),
2989///             vec![OwnedElementRef::new_unchecked(1, "C002".to_owned(), Status::Mandatory, 1)],
2990///         ),
2991///     )
2992///     .build();
2993/// ```
2994#[derive(Debug, Default)]
2995pub struct DirectoryValidatorBuilder {
2996    directory_id: Option<String>,
2997    segments: Vec<OwnedSegmentDef>,
2998}
2999
3000impl DirectoryValidatorBuilder {
3001    /// Create a new builder with the given directory identifier.
3002    ///
3003    /// The identifier is used in error messages; set a human-readable value
3004    /// such as `"ORDERS-MIG-5.5"` or `"custom-profile"`.
3005    pub fn new(directory_id: impl Into<String>) -> Self {
3006        Self {
3007            directory_id: Some(directory_id.into()),
3008            segments: Vec::new(),
3009        }
3010    }
3011
3012    /// Add a segment definition to the builder.
3013    ///
3014    /// Definitions can be added in any order; the resulting validator looks
3015    /// them up by tag at validation time.
3016    pub fn add_segment(mut self, def: OwnedSegmentDef) -> Self {
3017        self.segments.push(def);
3018        self
3019    }
3020
3021    /// Extend the builder with multiple segment definitions at once.
3022    pub fn add_segments(mut self, defs: impl IntoIterator<Item = OwnedSegmentDef>) -> Self {
3023        self.segments.extend(defs);
3024        self
3025    }
3026
3027    /// Build the [`DirectoryValidator`].
3028    ///
3029    /// Returns a validator backed by the accumulated [`OwnedSegmentDef`]s.
3030    /// Code-list checks are disabled by default; chain
3031    /// [`DirectoryValidator::with_code_list_rules`] on the returned value to
3032    /// enable them.
3033    pub fn build(self) -> DirectoryValidator {
3034        let mut validator = DirectoryValidator::from_owned_definitions(self.segments);
3035        if let Some(id) = self.directory_id {
3036            validator.directory_id = id;
3037        }
3038        validator
3039    }
3040}
3041
3042#[cfg(test)]
3043mod tests {
3044    use super::*;
3045
3046    static TEST_ELEMENTS: &[ElementRef] = &[ElementRef::new(1, "C507", Status::Mandatory, 1)];
3047
3048    static TEST_SEGMENT: SegmentDefinition =
3049        SegmentDefinition::new("TST", "Test segment", TEST_ELEMENTS);
3050
3051    fn segment_lookup(tag: &str) -> Option<&'static SegmentDefinition> {
3052        match tag {
3053            "TST" => Some(&TEST_SEGMENT),
3054            _ => None,
3055        }
3056    }
3057
3058    fn code_valid(_de: &str, _code: &str) -> bool {
3059        true
3060    }
3061
3062    fn suggest_code(_de: &str, _code: &str) -> Option<&'static str> {
3063        None
3064    }
3065
3066    fn expected_components(_tag: &str, _idx: usize) -> Option<u8> {
3067        None
3068    }
3069
3070    #[test]
3071    fn mandatory_composite_present_when_any_component_non_empty() {
3072        let input = b"TST+:ABC'";
3073        let segments: Vec<_> = crate::from_bytes(input)
3074            .collect::<Result<Vec<_>, _>>()
3075            .expect("parse should succeed");
3076
3077        let validator = DirectoryValidator::new(
3078            "TEST",
3079            segment_lookup,
3080            code_valid,
3081            suggest_code,
3082            expected_components,
3083            None,
3084        );
3085
3086        let mut report = ValidationReport::default();
3087        validator.validate_batch(
3088            &segments,
3089            &mut report,
3090            &crate::validator::ValidationRuleContext::empty(),
3091        );
3092        assert!(!report.has_errors());
3093    }
3094
3095    // ── effective_component_count (ISO 9735-1 §8.7.2 trailing-empty-component trim) ──
3096
3097    fn parse_single(input: &[u8]) -> crate::OwnedSegment {
3098        crate::from_reader(std::io::Cursor::new(input))
3099            .collect::<Result<Vec<_>, _>>()
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;
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;
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;
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}