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::validator::{ValidationRuleContext, Validator, report_error};
4use crate::{EdifactError, Segment, ValidationIssue, ValidationReport, ValidationSeverity};
5use std::sync::Arc;
6
7/// Mandatory/Conditional status of a data element within a segment.
8///
9/// Marked `#[non_exhaustive]` because UN/EDIFACT also defines Required, Advised,
10/// Dependent, and Not-used statuses; adding one must not break downstream `match`
11/// arms.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[non_exhaustive]
14pub enum Status {
15    /// Element must be present.
16    Mandatory,
17    /// Element is optional unless additional rules require it.
18    Conditional,
19}
20
21/// Reference to a component data element within a composite data element.
22///
23/// Composites such as `C507` (DTM date/time/period) are addressed by the
24/// identifier of their *own* components (`2005`, `2380`, `2379`), which is what
25/// makes code-addressed access — [`Segment::value_by_code`][crate::Segment::value_by_code]
26/// and `#[edifact(element = "2005")]` — resolve to the right slot instead of a
27/// hand-counted index.
28///
29/// Fields are private to enforce the one-based position invariant.
30#[derive(Debug, Clone, Copy)]
31pub struct ComponentRef {
32    /// One-based position of the **first** slot this component occupies.
33    position: u8,
34    /// UN/EDIFACT component data element identifier.
35    data_element: &'static str,
36    /// Requirement status of the component.
37    status: Status,
38    /// How many consecutive slots this component occupies; `1` unless the
39    /// composite repeats it by design.
40    repeat_count: u8,
41}
42
43impl ComponentRef {
44    /// Construct a `ComponentRef` with compile-time position validation.
45    ///
46    /// `position` must be ≥ 1 (one-based).  In a `const` context a zero
47    /// `position` is a **compile-time error**; at runtime it panics.
48    ///
49    /// # Panics
50    ///
51    /// Panics if `position == 0`.
52    ///
53    /// # Example
54    ///
55    /// ```rust
56    /// use edifact_rs::{ComponentRef, Status};
57    ///
58    /// const DTM_2005: ComponentRef = ComponentRef::new(1, "2005", Status::Mandatory);
59    /// ```
60    #[must_use]
61    pub const fn new(position: u8, data_element: &'static str, status: Status) -> Self {
62        assert!(
63            position != 0,
64            "ComponentRef position must be >= 1 (one-based)"
65        );
66        Self {
67            position,
68            data_element,
69            status,
70            repeat_count: 1,
71        }
72    }
73
74    /// Declare a component the composite repeats by design.
75    ///
76    /// Several standard composites carry the same data element several times
77    /// over: `C080 PARTY NAME` is `3036` five times followed by `3045`, `C059
78    /// STREET` is `3042` four times.  Spelling that out as five separate
79    /// [`new`][Self::new] entries made the code count as five positions, so
80    /// [`resolve_code`][SegmentLayout::resolve_code] reported it
81    /// [ambiguous][EdifactError::AmbiguousDataElement] and the component could
82    /// not be code-addressed at all — a faithful declaration was punished, and
83    /// the only way to use named access was to declare the composite
84    /// incompletely and disagree with the directory it claims to model.
85    ///
86    /// One `repeated` entry is one position, so `3036` resolves to occurrence 1
87    /// — what "the party name" means in every real message — while the
88    /// definition still records that five slots belong to it.
89    ///
90    /// `position` is the **first** slot; the next component follows at
91    /// `position + repeat_count`.
92    ///
93    /// # Panics
94    ///
95    /// Panics if `position == 0` or `repeat_count == 0`.
96    ///
97    /// # Example
98    ///
99    /// ```rust
100    /// use edifact_rs::{ComponentRef, ElementRef, SegmentDefinition, SegmentLayout, Status};
101    ///
102    /// // C080 PARTY NAME: 3036 ×5, then 3045 at position 6.
103    /// static C080: &[ComponentRef] = &[
104    ///     ComponentRef::repeated(1, "3036", Status::Mandatory, 5),
105    ///     ComponentRef::new(6, "3045", Status::Conditional),
106    /// ];
107    /// static NAD_ELEMENTS: &[ElementRef] = &[
108    ///     ElementRef::new(1, "3035", Status::Mandatory, 1),
109    ///     ElementRef::composite(4, "C080", Status::Conditional, 1, C080),
110    /// ];
111    /// static NAD: SegmentDefinition = SegmentDefinition::new("NAD", "Name and address", NAD_ELEMENTS);
112    ///
113    /// // Addressable, and it points at the first occurrence.
114    /// let path = NAD.resolve_code("3036")?;
115    /// assert_eq!((path.element, path.component), (3, Some(0)));
116    /// # Ok::<(), edifact_rs::EdifactError>(())
117    /// ```
118    #[must_use]
119    pub const fn repeated(
120        position: u8,
121        data_element: &'static str,
122        status: Status,
123        repeat_count: u8,
124    ) -> Self {
125        assert!(
126            position != 0,
127            "ComponentRef position must be >= 1 (one-based)"
128        );
129        assert!(
130            repeat_count != 0,
131            "ComponentRef repeat_count must be >= 1; use `new` for a component that does not repeat"
132        );
133        Self {
134            position,
135            data_element,
136            status,
137            repeat_count,
138        }
139    }
140
141    /// How many consecutive slots this component occupies.
142    ///
143    /// `1` for a component declared with [`new`][Self::new].
144    #[must_use]
145    #[inline]
146    pub const fn repeat_count(&self) -> u8 {
147        self.repeat_count
148    }
149
150    /// One-based component position within the composite.
151    #[must_use]
152    #[inline]
153    pub const fn position(&self) -> u8 {
154        self.position
155    }
156
157    /// UN/EDIFACT component data element identifier.
158    #[must_use]
159    #[inline]
160    pub const fn data_element(&self) -> &'static str {
161        self.data_element
162    }
163
164    /// Requirement status of the component.
165    #[must_use]
166    #[inline]
167    pub const fn status(&self) -> Status {
168        self.status
169    }
170}
171
172/// Reference to a data element within a segment definition.
173///
174/// Fields are private to enforce the one-based position invariant through the
175/// [`ElementRef::new`] constructor.  Use [`ElementRef::new`] for a simple data
176/// element and [`ElementRef::composite`] for a composite whose components are
177/// themselves named (panics at compile time when `position == 0`).
178///
179/// Use [`OwnedElementRef`] for runtime-constructed element refs.
180#[derive(Debug, Clone, Copy)]
181pub struct ElementRef {
182    /// One-based element position in the segment definition.
183    position: u8,
184    /// UN/EDIFACT data element identifier.
185    data_element: &'static str,
186    /// Requirement status of the element.
187    status: Status,
188    /// Maximum repetition count for this element.
189    max_repeat: u8,
190    /// Component definitions when this element is a composite; empty for a
191    /// simple data element.
192    components: &'static [ComponentRef],
193}
194
195impl ElementRef {
196    /// Construct an `ElementRef` for a simple data element.
197    ///
198    /// `position` must be ≥ 1 (one-based).  When called in a `const` context
199    /// (e.g. inside a `static` array initialiser), a zero `position` causes a
200    /// **compile-time error**.  At runtime it panics.
201    ///
202    /// Use [`composite`][Self::composite] when the element is a composite whose
203    /// components carry their own UN/EDIFACT identifiers.
204    ///
205    /// # Panics
206    ///
207    /// Panics if `position == 0`.
208    ///
209    /// # Example
210    ///
211    /// ```rust
212    /// use edifact_rs::{ElementRef, Status};
213    ///
214    /// const BGM_1001: ElementRef = ElementRef::new(1, "1001", Status::Mandatory, 1);
215    /// ```
216    #[must_use]
217    pub const fn new(
218        position: u8,
219        data_element: &'static str,
220        status: Status,
221        max_repeat: u8,
222    ) -> Self {
223        assert!(
224            position != 0,
225            "ElementRef position must be >= 1 (one-based)"
226        );
227        Self {
228            position,
229            data_element,
230            status,
231            max_repeat,
232            components: &[],
233        }
234    }
235
236    /// Construct an `ElementRef` for a composite data element with named components.
237    ///
238    /// Declaring components is what lets code-addressed access reach *inside* a
239    /// composite: `value_by_code(&DTM, "2380")` resolves to element 1,
240    /// component 2 without the caller counting positions.  Declared components
241    /// also make the mandatory-component check in [`DirectoryValidator`] active
242    /// for this element.
243    ///
244    /// # Panics
245    ///
246    /// Panics if `position == 0`.
247    ///
248    /// # Example
249    ///
250    /// ```rust
251    /// use edifact_rs::{ComponentRef, ElementRef, Status};
252    ///
253    /// static C507: &[ComponentRef] = &[
254    ///     ComponentRef::new(1, "2005", Status::Mandatory),
255    ///     ComponentRef::new(2, "2380", Status::Conditional),
256    ///     ComponentRef::new(3, "2379", Status::Conditional),
257    /// ];
258    /// const DTM_C507: ElementRef =
259    ///     ElementRef::composite(1, "C507", Status::Mandatory, 1, C507);
260    /// ```
261    #[must_use]
262    pub const fn composite(
263        position: u8,
264        data_element: &'static str,
265        status: Status,
266        max_repeat: u8,
267        components: &'static [ComponentRef],
268    ) -> Self {
269        assert!(
270            position != 0,
271            "ElementRef position must be >= 1 (one-based)"
272        );
273        Self {
274            position,
275            data_element,
276            status,
277            max_repeat,
278            components,
279        }
280    }
281
282    /// One-based element position in the segment definition.
283    #[must_use]
284    #[inline]
285    pub const fn position(&self) -> u8 {
286        self.position
287    }
288
289    /// UN/EDIFACT data element identifier.
290    #[must_use]
291    #[inline]
292    pub const fn data_element(&self) -> &'static str {
293        self.data_element
294    }
295
296    /// Requirement status of the element.
297    #[must_use]
298    #[inline]
299    pub const fn status(&self) -> Status {
300        self.status
301    }
302
303    /// Maximum repetition count for this element.
304    #[must_use]
305    #[inline]
306    pub const fn max_repeat(&self) -> u8 {
307        self.max_repeat
308    }
309
310    /// Component definitions; empty when this is a simple data element.
311    #[must_use]
312    #[inline]
313    pub const fn components(&self) -> &'static [ComponentRef] {
314        self.components
315    }
316}
317
318/// Definition of an EDIFACT segment (tag + element structure).
319///
320/// Construct with [`SegmentDefinition::new`] rather than a struct literal, so
321/// that future fields (max repeat, description, …) are not a breaking change.
322#[derive(Debug)]
323#[non_exhaustive]
324pub struct SegmentDefinition {
325    /// Segment tag.
326    pub tag: &'static str,
327    /// Human-readable segment name.
328    pub name: &'static str,
329    /// Ordered element definitions.
330    pub elements: &'static [ElementRef],
331}
332
333/// Byte-wise string equality usable in a `const` context.
334///
335/// `str::eq` is not `const`, and code resolution has to run at compile time so
336/// that a mistyped data element identifier in `#[edifact(element = "3055")]`
337/// fails the build rather than reading the wrong slot at runtime.
338const fn const_str_eq(a: &str, b: &str) -> bool {
339    let (a, b) = (a.as_bytes(), b.as_bytes());
340    if a.len() != b.len() {
341        return false;
342    }
343    let mut i = 0;
344    while i < a.len() {
345        if a[i] != b[i] {
346            return false;
347        }
348        i += 1;
349    }
350    true
351}
352
353/// The resolved position of a UN/EDIFACT data element within a segment.
354///
355/// Produced by [`SegmentLayout::resolve_code`] and consumed by the `*_at`
356/// accessors on [`crate::Segment`], [`crate::BorrowedSegment`] and
357/// [`crate::OwnedSegment`].
358///
359/// Both indices are **zero-based**, matching the positional accessors — the
360/// one-based positions used in directory definitions are converted during
361/// resolution.
362#[derive(Debug, Clone, Copy, PartialEq, Eq)]
363pub struct ElementPath {
364    /// Zero-based index of the data element within the segment.
365    pub element: usize,
366    /// Zero-based index of the component within a composite.
367    ///
368    /// `None` when the code names the data element itself (a simple element, or
369    /// a composite addressed as a whole).  Value lookups treat `None` as
370    /// component 0, which is the first — and for a simple element, only —
371    /// component.
372    pub component: Option<usize>,
373}
374
375impl ElementPath {
376    /// Path to a whole data element.
377    #[must_use]
378    #[inline]
379    pub const fn element(element: usize) -> Self {
380        Self {
381            element,
382            component: None,
383        }
384    }
385
386    /// Path to a component within a composite data element.
387    #[must_use]
388    #[inline]
389    pub const fn component(element: usize, component: usize) -> Self {
390        Self {
391            element,
392            component: Some(component),
393        }
394    }
395
396    /// Zero-based component index, treating "whole element" as component 0.
397    #[must_use]
398    #[inline]
399    pub const fn component_index(&self) -> usize {
400        match self.component {
401            Some(c) => c,
402            None => 0,
403        }
404    }
405}
406
407/// Directory metadata that maps UN/EDIFACT data element identifiers to positions.
408///
409/// Implemented by [`SegmentDefinition`] (compile-time tables) and
410/// [`OwnedSegmentDef`] (runtime-loaded definitions), so the same code-addressed
411/// accessors work against either source.
412///
413/// # Example
414///
415/// ```rust
416/// use edifact_rs::{ElementRef, SegmentDefinition, SegmentLayout, Status};
417///
418/// static BGM_ELEMENTS: &[ElementRef] = &[
419///     ElementRef::new(1, "C002", Status::Conditional, 1),
420///     ElementRef::new(2, "C106", Status::Conditional, 1),
421///     ElementRef::new(3, "1225", Status::Conditional, 1),
422/// ];
423/// static BGM: SegmentDefinition =
424///     SegmentDefinition::new("BGM", "Beginning of message", BGM_ELEMENTS);
425///
426/// let path = BGM.resolve_code("1225")?;
427/// assert_eq!(path.element, 2);
428/// assert!(BGM.resolve_code("9999").is_err());
429/// # Ok::<(), edifact_rs::EdifactError>(())
430/// ```
431pub trait SegmentLayout {
432    /// The segment tag this layout describes (e.g. `"NAD"`).
433    fn layout_tag(&self) -> &str;
434
435    /// Resolve a UN/EDIFACT data element identifier to a position.
436    ///
437    /// # Errors
438    ///
439    /// Returns [`EdifactError::UnknownDataElement`] when the identifier does not
440    /// appear in this definition, and [`EdifactError::AmbiguousDataElement`]
441    /// when it appears at more than one position.
442    fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError>;
443}
444
445impl SegmentDefinition {
446    /// Create a segment definition.
447    ///
448    /// `const` so directory tables can still be built at compile time despite
449    /// the `#[non_exhaustive]` attribute blocking external struct literals.
450    #[must_use]
451    pub const fn new(
452        tag: &'static str,
453        name: &'static str,
454        elements: &'static [ElementRef],
455    ) -> Self {
456        Self {
457            tag,
458            name,
459            elements,
460        }
461    }
462
463    /// Number of positions in this definition that carry `data_element`.
464    ///
465    /// `0` means unknown, `1` means unambiguously addressable, and anything
466    /// larger means the identifier is repeated and cannot be code-addressed.
467    /// `const`, so a derive macro can assert on it at compile time.
468    ///
469    /// # Example
470    ///
471    /// ```rust
472    /// # use edifact_rs::{ElementRef, SegmentDefinition, Status};
473    /// # static E: &[ElementRef] = &[ElementRef::new(1, "3035", Status::Mandatory, 1)];
474    /// static NAD: SegmentDefinition = SegmentDefinition::new("NAD", "Name and address", E);
475    /// const _: () = assert!(NAD.code_positions("3035") == 1);
476    /// ```
477    #[must_use]
478    pub const fn code_positions(&self, data_element: &str) -> usize {
479        let mut hits = 0;
480        let mut i = 0;
481        while i < self.elements.len() {
482            let el = &self.elements[i];
483            if const_str_eq(el.data_element, data_element) {
484                hits += 1;
485            }
486            let mut c = 0;
487            while c < el.components.len() {
488                if const_str_eq(el.components[c].data_element, data_element) {
489                    hits += 1;
490                }
491                c += 1;
492            }
493            i += 1;
494        }
495        hits
496    }
497
498    /// Zero-based element index for `data_element`, resolved at compile time.
499    ///
500    /// # Panics
501    ///
502    /// Panics when the identifier is unknown or appears at more than one
503    /// position.  In a `const` context — which is how the derive macro uses it —
504    /// that panic is a **compile error**, so a mistyped identifier can never
505    /// reach runtime.  Guard with [`code_positions`][Self::code_positions] for a
506    /// message that names the offending field.
507    #[must_use]
508    pub const fn element_slot(&self, data_element: &str) -> usize {
509        // Two asserts rather than one: a const panic message cannot be
510        // formatted, so naming the identifier is impossible — but saying which
511        // of the two problems occurred is not, and it is the part that decides
512        // what the author has to change.
513        assert!(
514            self.code_positions(data_element) != 0,
515            "this segment definition declares no such data element identifier — check it against the directory"
516        );
517        assert!(
518            self.code_positions(data_element) == 1,
519            "this data element identifier is declared at more than one position; address it positionally, or declare the repeat with ComponentRef::repeated"
520        );
521        let mut i = 0;
522        while i < self.elements.len() {
523            let el = &self.elements[i];
524            if const_str_eq(el.data_element, data_element) {
525                return el.position as usize - 1;
526            }
527            let mut c = 0;
528            while c < el.components.len() {
529                if const_str_eq(el.components[c].data_element, data_element) {
530                    return el.position as usize - 1;
531                }
532                c += 1;
533            }
534            i += 1;
535        }
536        unreachable!()
537    }
538
539    /// Zero-based component index for `data_element`, resolved at compile time.
540    ///
541    /// Returns `0` when the identifier names a data element rather than a
542    /// component inside a composite — component 0 is the first (and for a simple
543    /// element, only) component, so the same accessor works for both shapes.
544    ///
545    /// # Panics
546    ///
547    /// Panics when the identifier is unknown or appears at more than one
548    /// position; see [`element_slot`][Self::element_slot].
549    #[must_use]
550    pub const fn component_slot(&self, data_element: &str) -> usize {
551        assert!(
552            self.code_positions(data_element) != 0,
553            "this segment definition declares no such data element identifier — check it against the directory"
554        );
555        assert!(
556            self.code_positions(data_element) == 1,
557            "this data element identifier is declared at more than one position; address it positionally, or declare the repeat with ComponentRef::repeated"
558        );
559        let mut i = 0;
560        while i < self.elements.len() {
561            let el = &self.elements[i];
562            if const_str_eq(el.data_element, data_element) {
563                return 0;
564            }
565            let mut c = 0;
566            while c < el.components.len() {
567                if const_str_eq(el.components[c].data_element, data_element) {
568                    return el.components[c].position as usize - 1;
569                }
570                c += 1;
571            }
572            i += 1;
573        }
574        unreachable!()
575    }
576
577    /// `true` when `data_element` names a component *inside* a composite rather
578    /// than a data element of the segment.
579    ///
580    /// Lets a caller — the derive macro, in practice — pick the right
581    /// "missing required" error variant without a second lookup:
582    /// [`EdifactError::MissingRequiredComponent`] rather than
583    /// [`EdifactError::MissingRequiredElement`]. `component_slot` alone cannot
584    /// answer this, because a code naming the *first* component of a composite
585    /// also resolves to component index 0.
586    ///
587    /// Returns `false` for an unknown identifier; pair with
588    /// [`code_positions`][Self::code_positions] when that case matters.
589    #[must_use]
590    pub const fn code_is_component(&self, data_element: &str) -> bool {
591        let mut i = 0;
592        while i < self.elements.len() {
593            let el = &self.elements[i];
594            let mut c = 0;
595            while c < el.components.len() {
596                if const_str_eq(el.components[c].data_element, data_element) {
597                    return true;
598                }
599                c += 1;
600            }
601            i += 1;
602        }
603        false
604    }
605}
606
607impl SegmentLayout for SegmentDefinition {
608    #[inline]
609    fn layout_tag(&self) -> &str {
610        self.tag
611    }
612
613    fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError> {
614        // One pass, not four: this runs per lookup on hot validation paths, and
615        // composing the `const` helpers would rescan the table for each of the
616        // count, the element index, and the component index.
617        let mut hits = 0usize;
618        let mut found = None;
619        for el in self.elements {
620            if el.data_element == data_element {
621                hits += 1;
622                found.get_or_insert(ElementPath::element(el.position as usize - 1));
623            }
624            for comp in el.components {
625                if comp.data_element == data_element {
626                    hits += 1;
627                    found.get_or_insert(ElementPath::component(
628                        el.position as usize - 1,
629                        comp.position as usize - 1,
630                    ));
631                }
632            }
633        }
634        resolve_outcome(self.tag, data_element, hits, found)
635    }
636}
637
638/// Turn a resolution scan's `(hit count, first match)` into a `Result`.
639///
640/// Shared by both [`SegmentLayout`] impls so the static and runtime tables
641/// cannot drift on which condition maps to which error.
642fn resolve_outcome(
643    tag: &str,
644    data_element: &str,
645    hits: usize,
646    found: Option<ElementPath>,
647) -> Result<ElementPath, EdifactError> {
648    match (hits, found) {
649        (1, Some(path)) => Ok(path),
650        (0, _) => Err(EdifactError::UnknownDataElement {
651            tag: tag.to_owned(),
652            data_element: data_element.to_owned(),
653        }),
654        _ => Err(EdifactError::AmbiguousDataElement {
655            tag: tag.to_owned(),
656            data_element: data_element.to_owned(),
657        }),
658    }
659}
660
661/// Owned runtime equivalent of [`ElementRef`].
662///
663/// Used by [`DirectoryValidatorBuilder`] and [`DirectoryValidator::from_owned_definitions`]
664/// to construct validators from data that is not available at compile time (e.g. loaded
665/// from JSON or a database at startup).
666///
667/// Use [`OwnedElementRef::new_unchecked`] for compile-time-known positions (panics on invalid
668/// input, no error handling noise) or [`OwnedElementRef::try_new`] when the position
669/// comes from an external source and you need a `Result`. Fields are private to prevent
670/// bypassing the position invariant through struct-literal syntax.
671#[derive(Debug, Clone)]
672pub struct OwnedElementRef {
673    /// One-based element position.
674    position: u8,
675    /// UN/EDIFACT data element identifier.
676    data_element: String,
677    /// Requirement status.
678    status: Status,
679    /// Maximum repetition count.
680    max_repeat: u8,
681    /// Component definitions when this element is a composite; empty for a
682    /// simple data element.
683    components: Vec<OwnedComponentRef>,
684}
685
686/// Owned runtime equivalent of [`ComponentRef`].
687///
688/// Attach these to an [`OwnedElementRef`] with
689/// [`OwnedElementRef::with_components`] so that runtime-loaded definitions
690/// support code-addressed access into composites, exactly like compile-time
691/// [`SegmentDefinition`] tables do.
692#[derive(Debug, Clone)]
693pub struct OwnedComponentRef {
694    /// One-based position of the first slot this component occupies.
695    position: u8,
696    /// UN/EDIFACT component data element identifier.
697    data_element: String,
698    /// Requirement status.
699    status: Status,
700    /// How many consecutive slots this component occupies.
701    repeat_count: u8,
702}
703
704impl OwnedComponentRef {
705    /// Construct an owned component reference.
706    ///
707    /// # Panics
708    ///
709    /// Panics if `position` is `0` (positions are one-based).
710    pub fn new_unchecked(position: u8, data_element: String, status: Status) -> Self {
711        assert!(
712            position != 0,
713            "OwnedComponentRef::new_unchecked: position must be >= 1 (one-based), got 0"
714        );
715        Self {
716            position,
717            data_element,
718            status,
719            repeat_count: 1,
720        }
721    }
722
723    /// Runtime counterpart of [`ComponentRef::repeated`].
724    ///
725    /// # Panics
726    ///
727    /// Panics if `position == 0` or `repeat_count == 0`.
728    #[must_use]
729    pub fn repeated(position: u8, data_element: String, status: Status, repeat_count: u8) -> Self {
730        assert!(
731            position != 0,
732            "OwnedComponentRef::repeated: position must be >= 1 (one-based), got 0"
733        );
734        assert!(
735            repeat_count != 0,
736            "OwnedComponentRef::repeated: repeat_count must be >= 1"
737        );
738        Self {
739            position,
740            data_element,
741            status,
742            repeat_count,
743        }
744    }
745
746    /// How many consecutive slots this component occupies.
747    #[inline]
748    #[must_use]
749    pub fn repeat_count(&self) -> u8 {
750        self.repeat_count
751    }
752
753    /// Construct an owned component reference, returning an error for position `0`.
754    ///
755    /// # Errors
756    ///
757    /// Returns [`EdifactError::InvalidElementPosition`] if `position` is `0`.
758    pub fn try_new(
759        position: u8,
760        data_element: String,
761        status: Status,
762    ) -> Result<Self, EdifactError> {
763        if position == 0 {
764            return Err(EdifactError::InvalidElementPosition);
765        }
766        Ok(Self {
767            position,
768            data_element,
769            status,
770            repeat_count: 1,
771        })
772    }
773
774    /// One-based component position (always >= 1).
775    #[inline]
776    pub fn position(&self) -> u8 {
777        self.position
778    }
779
780    /// UN/EDIFACT component data element identifier.
781    #[inline]
782    pub fn data_element(&self) -> &str {
783        &self.data_element
784    }
785
786    /// Requirement status of this component.
787    #[inline]
788    pub fn status(&self) -> Status {
789        self.status
790    }
791}
792
793/// Owned runtime equivalent of [`SegmentDefinition`].
794///
795/// Used by [`DirectoryValidatorBuilder`] and [`DirectoryValidator::from_owned_definitions`].
796///
797/// Use [`OwnedSegmentDef::new_unchecked`] for compile-time-known tags (panics on invalid input,
798/// no error handling noise) or [`OwnedSegmentDef::try_new`] when the tag comes from
799/// an external source and you need a `Result`. Fields are private to prevent bypassing
800/// the tag invariant through struct-literal syntax.
801#[derive(Debug, Clone)]
802pub struct OwnedSegmentDef {
803    /// Segment tag (e.g. `"BGM"`).
804    tag: String,
805    /// Human-readable segment name.
806    name: String,
807    /// Ordered element definitions.
808    elements: Vec<OwnedElementRef>,
809}
810
811impl OwnedSegmentDef {
812    /// Construct an owned segment definition.
813    ///
814    /// This is the ergonomic constructor for compile-time-known tags (e.g.
815    /// `"BGM"`, `"UNH"`).  It panics immediately on invalid input so that
816    /// call sites with literal tag strings require no `.unwrap()` / `.expect()`
817    /// boilerplate.
818    ///
819    /// Use [`try_new`][Self::try_new] instead when the tag originates from an
820    /// external source (user input, config file, database) and you need a
821    /// `Result` to propagate errors gracefully.
822    ///
823    /// # Panics
824    ///
825    /// Panics if `tag` is not exactly three ASCII uppercase letters.
826    pub fn new_unchecked(tag: String, name: String, elements: Vec<OwnedElementRef>) -> Self {
827        assert!(
828            tag.len() == 3 && tag.bytes().all(|b| b.is_ascii_uppercase()),
829            "OwnedSegmentDef::new_unchecked: tag must be exactly three ASCII uppercase letters, got {tag:?}"
830        );
831        Self {
832            tag,
833            name,
834            elements,
835        }
836    }
837
838    /// Construct an owned segment definition, returning an error for invalid tags.
839    ///
840    /// Prefer this over [`new_unchecked`][Self::new_unchecked] when the tag comes from an external
841    /// source (user input, config file, database) and you want to handle the
842    /// error without panicking.
843    ///
844    /// # Errors
845    ///
846    /// Returns [`EdifactError::InvalidSegmentTag`] if `tag` is not exactly three
847    /// ASCII uppercase letters.
848    pub fn try_new(
849        tag: String,
850        name: String,
851        elements: Vec<OwnedElementRef>,
852    ) -> Result<Self, EdifactError> {
853        if tag.len() != 3 || !tag.bytes().all(|b| b.is_ascii_uppercase()) {
854            return Err(EdifactError::InvalidSegmentTag(tag));
855        }
856        Ok(Self {
857            tag,
858            name,
859            elements,
860        })
861    }
862
863    /// Segment tag (e.g. `"BGM"`).
864    #[inline]
865    pub fn tag(&self) -> &str {
866        &self.tag
867    }
868
869    /// Human-readable segment name.
870    #[inline]
871    pub fn name(&self) -> &str {
872        &self.name
873    }
874
875    /// Element definitions for this segment.
876    #[inline]
877    pub fn elements(&self) -> &[OwnedElementRef] {
878        &self.elements
879    }
880
881    /// Number of positions in this definition that carry `data_element`.
882    ///
883    /// Runtime counterpart of [`SegmentDefinition::code_positions`].
884    #[must_use]
885    pub fn code_positions(&self, data_element: &str) -> usize {
886        self.elements
887            .iter()
888            .map(|el| {
889                usize::from(el.data_element == data_element)
890                    + el.components
891                        .iter()
892                        .filter(|c| c.data_element == data_element)
893                        .count()
894            })
895            .sum()
896    }
897}
898
899impl SegmentLayout for OwnedSegmentDef {
900    #[inline]
901    fn layout_tag(&self) -> &str {
902        &self.tag
903    }
904
905    fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError> {
906        let mut hits = 0usize;
907        let mut found = None;
908        for el in &self.elements {
909            if el.data_element == data_element {
910                hits += 1;
911                found.get_or_insert(ElementPath::element(el.position as usize - 1));
912            }
913            for comp in &el.components {
914                if comp.data_element == data_element {
915                    hits += 1;
916                    found.get_or_insert(ElementPath::component(
917                        el.position as usize - 1,
918                        comp.position as usize - 1,
919                    ));
920                }
921            }
922        }
923        resolve_outcome(&self.tag, data_element, hits, found)
924    }
925}
926
927impl OwnedElementRef {
928    /// Construct an owned element reference.
929    ///
930    /// This is the ergonomic constructor for compile-time-known positions.
931    /// It panics immediately on invalid input so that call sites with literal
932    /// position numbers require no `.unwrap()` / `.expect()` boilerplate.
933    ///
934    /// Use [`try_new`][Self::try_new] instead when the position originates from
935    /// an external source (user input, config file, database) and you need a
936    /// `Result` to propagate errors gracefully.
937    ///
938    /// # Panics
939    ///
940    /// Panics if `position` is `0` (positions are one-based).
941    pub fn new_unchecked(
942        position: u8,
943        data_element: String,
944        status: Status,
945        max_repeat: u8,
946    ) -> Self {
947        assert!(
948            position != 0,
949            "OwnedElementRef::new_unchecked: position must be >= 1 (one-based), got 0"
950        );
951        Self {
952            position,
953            data_element,
954            status,
955            max_repeat,
956            components: Vec::new(),
957        }
958    }
959
960    /// Construct an owned element reference, returning an error for position `0`.
961    ///
962    /// Prefer this over [`new_unchecked`][Self::new_unchecked] when the position comes from an
963    /// external source (user input, config file, database) and you want to
964    /// handle the error without panicking.
965    ///
966    /// # Errors
967    ///
968    /// Returns [`EdifactError::InvalidElementPosition`] if `position` is `0`.
969    pub fn try_new(
970        position: u8,
971        data_element: String,
972        status: Status,
973        max_repeat: u8,
974    ) -> Result<Self, EdifactError> {
975        if position == 0 {
976            return Err(EdifactError::InvalidElementPosition);
977        }
978        Ok(Self {
979            position,
980            data_element,
981            status,
982            max_repeat,
983            components: Vec::new(),
984        })
985    }
986
987    /// Attach component definitions, marking this element as a composite.
988    ///
989    /// Declared components make code-addressed access resolve *into* the
990    /// composite and activate the mandatory-component check in
991    /// [`DirectoryValidator`].
992    ///
993    /// # Example
994    ///
995    /// ```rust
996    /// use edifact_rs::{OwnedComponentRef, OwnedElementRef, Status};
997    ///
998    /// let dtm = OwnedElementRef::new_unchecked(1, "C507".to_owned(), Status::Mandatory, 1)
999    ///     .with_components(vec![
1000    ///         OwnedComponentRef::new_unchecked(1, "2005".to_owned(), Status::Mandatory),
1001    ///         OwnedComponentRef::new_unchecked(2, "2380".to_owned(), Status::Conditional),
1002    ///     ]);
1003    /// assert_eq!(dtm.components().len(), 2);
1004    /// ```
1005    #[must_use]
1006    pub fn with_components(mut self, components: Vec<OwnedComponentRef>) -> Self {
1007        self.components = components;
1008        self
1009    }
1010
1011    /// Component definitions; empty when this is a simple data element.
1012    #[inline]
1013    pub fn components(&self) -> &[OwnedComponentRef] {
1014        &self.components
1015    }
1016
1017    /// One-based element position (always >= 1).
1018    #[inline]
1019    pub fn position(&self) -> u8 {
1020        self.position
1021    }
1022
1023    /// UN/EDIFACT data element identifier.
1024    #[inline]
1025    pub fn data_element(&self) -> &str {
1026        &self.data_element
1027    }
1028
1029    /// Requirement status of this element.
1030    #[inline]
1031    pub fn status(&self) -> Status {
1032        self.status
1033    }
1034
1035    /// Maximum repetition count for this element.
1036    #[inline]
1037    pub fn max_repeat(&self) -> u8 {
1038        self.max_repeat
1039    }
1040}
1041
1042type SegmentLookupFn = Arc<dyn Fn(&str) -> Option<&'static SegmentDefinition> + Send + Sync>;
1043type IsCodeValidFn = Arc<dyn Fn(&str, &str) -> bool + Send + Sync>;
1044type SuggestCodeFn = Arc<dyn Fn(&str, &str) -> Option<&'static str> + Send + Sync>;
1045type ExpectedComponentsFn = Arc<dyn Fn(&str, usize) -> Option<u8> + Send + Sync>;
1046type AdditionalStructureRuleRefFn = fn(&Segment<'_>) -> Result<(), EdifactError>;
1047type AdditionalStructureRuleFn =
1048    Arc<dyn Fn(&Segment<'_>) -> Result<(), EdifactError> + Send + Sync>;
1049/// Returns the `(element_index, component_index, data_element_id)` tuples to
1050/// validate against a code list for the given segment tag.
1051type CodeListRulesFn = Arc<dyn Fn(&str) -> &'static [(usize, usize, &'static str)] + Send + Sync>;
1052/// Returns the mandatory segment tags for a given EDIFACT message type.
1053///
1054/// The slice should contain every tag that must appear at least once in a
1055/// conformant message of the given type.  The tags are also used to check
1056/// canonical ordering — their relative order in the returned slice is taken
1057/// as the expected order in the message.
1058type RequiredSegmentsFn = Arc<dyn Fn(&str) -> &'static [&'static str] + Send + Sync>;
1059
1060/// Internal enum that unifies lookup results from static and owned segment definitions.
1061///
1062/// Allows `validate_segment` to handle both code-generated (`&'static`) and
1063/// runtime-constructed ([`OwnedSegmentDef`]) definitions without duplication.
1064enum SegmentDefRef<'a> {
1065    Static(&'static SegmentDefinition),
1066    Owned(&'a OwnedSegmentDef),
1067}
1068
1069impl SegmentDefRef<'_> {
1070    /// Returns the highest defined element position (one-based → used directly as
1071    /// the maximum zero-based slot count for element-count validation).
1072    ///
1073    /// For owned definitions the highest `position` value may exceed the number
1074    /// of entries in the `elements` vec when positions are non-consecutive.
1075    fn max_element_position(&self) -> usize {
1076        match self {
1077            Self::Static(d) => d
1078                .elements
1079                .iter()
1080                .map(|e| e.position as usize)
1081                .max()
1082                .unwrap_or(0),
1083            Self::Owned(d) => d
1084                .elements
1085                .iter()
1086                .map(|e| e.position as usize)
1087                .max()
1088                .unwrap_or(0),
1089        }
1090    }
1091
1092    /// Returns the highest position number among mandatory elements (one-based).
1093    ///
1094    /// This equals the minimum number of elements that must be present in a
1095    /// segment: if the highest-positioned mandatory element is at position 5,
1096    /// the segment must supply at least 5 elements.
1097    fn last_mandatory_position(&self) -> usize {
1098        match self {
1099            Self::Static(d) => d
1100                .elements
1101                .iter()
1102                .filter(|e| e.status == Status::Mandatory)
1103                .map(|e| e.position as usize)
1104                .max()
1105                .unwrap_or(0),
1106            Self::Owned(d) => d
1107                .elements
1108                .iter()
1109                .filter(|e| e.status == Status::Mandatory)
1110                .map(|e| e.position as usize)
1111                .max()
1112                .unwrap_or(0),
1113        }
1114    }
1115
1116    /// Iterate over mandatory element positions without heap allocation.
1117    ///
1118    /// Calls `f(zero_based_index, data_element_id)` for each element whose
1119    /// status is [`Status::Mandatory`].  Returns `Err` immediately if `f`
1120    /// returns `Err`, short-circuiting the remaining elements.
1121    fn for_each_mandatory_position<E, F>(&self, mut f: F) -> Result<(), E>
1122    where
1123        F: FnMut(usize, &str) -> Result<(), E>,
1124    {
1125        match self {
1126            Self::Static(d) => {
1127                for e in d.elements.iter().filter(|e| e.status == Status::Mandatory) {
1128                    f((e.position as usize).saturating_sub(1), e.data_element)?;
1129                }
1130            }
1131            Self::Owned(d) => {
1132                for e in d.elements.iter().filter(|e| e.status == Status::Mandatory) {
1133                    f(
1134                        (e.position as usize).saturating_sub(1),
1135                        e.data_element.as_str(),
1136                    )?;
1137                }
1138            }
1139        }
1140        Ok(())
1141    }
1142
1143    /// Iterate over mandatory *component* positions without heap allocation.
1144    ///
1145    /// Calls `f(element_index, component_index, data_element_id)` — both indices
1146    /// zero-based — for every declared component whose status is
1147    /// [`Status::Mandatory`].  Definitions that declare no components (the shape
1148    /// every pre-0.13 directory table had) yield nothing, so this check is
1149    /// inert until a directory opts in by declaring composites with
1150    /// [`ElementRef::composite`].
1151    fn for_each_mandatory_component<E, F>(&self, mut f: F) -> Result<(), E>
1152    where
1153        F: FnMut(usize, usize, &str) -> Result<(), E>,
1154    {
1155        match self {
1156            Self::Static(d) => {
1157                for e in d.elements {
1158                    for c in e
1159                        .components
1160                        .iter()
1161                        .filter(|c| c.status == Status::Mandatory)
1162                    {
1163                        f(
1164                            (e.position as usize).saturating_sub(1),
1165                            (c.position as usize).saturating_sub(1),
1166                            c.data_element,
1167                        )?;
1168                    }
1169                }
1170            }
1171            Self::Owned(d) => {
1172                for e in &d.elements {
1173                    for c in e
1174                        .components
1175                        .iter()
1176                        .filter(|c| c.status == Status::Mandatory)
1177                    {
1178                        f(
1179                            (e.position as usize).saturating_sub(1),
1180                            (c.position as usize).saturating_sub(1),
1181                            c.data_element.as_str(),
1182                        )?;
1183                    }
1184                }
1185            }
1186        }
1187        Ok(())
1188    }
1189
1190    /// Number of declared component **slots** for the element at zero-based `index`.
1191    ///
1192    /// A component declared with [`ComponentRef::repeated`] occupies several
1193    /// slots, so this sums repeat counts rather than counting entries: counting
1194    /// entries would cap `C080` at two components and reject the four extra
1195    /// `3036` occurrences the composite is defined to carry.
1196    ///
1197    /// `None` when the element is not defined, or is defined without
1198    /// components — in which case its arity is not constrained by the layout.
1199    fn declared_component_count(&self, index: usize) -> Option<u8> {
1200        let position = u8::try_from(index.checked_add(1)?).ok()?;
1201        let count: u32 = match self {
1202            Self::Static(d) => d
1203                .elements
1204                .iter()
1205                .find(|e| e.position == position)
1206                .map(|e| e.components.iter().map(|c| u32::from(c.repeat_count)).sum())?,
1207            Self::Owned(d) => d
1208                .elements
1209                .iter()
1210                .find(|e| e.position == position)
1211                .map(|e| e.components.iter().map(|c| u32::from(c.repeat_count)).sum())?,
1212        };
1213        if count == 0 {
1214            return None;
1215        }
1216        u8::try_from(count).ok()
1217    }
1218}
1219
1220/// Default required-segments mapping used when no custom function is provided.
1221///
1222/// Returns the universal minimum: every EDIFACT message must begin with `UNH`
1223/// and end with `UNT`.  Message-type-specific mandatory segments (such as
1224/// `BGM` for ORDERS/INVOIC) must be enforced by a
1225/// [`ProfileRulePack`][crate::ProfileRulePack] or a custom
1226/// [`DirectoryValidatorBuilder::with_required_segments`] function to avoid
1227/// false positives for message types that do not require `BGM`.
1228fn default_required_segments(_message_type: &str) -> &'static [&'static str] {
1229    &["UNH", "UNT"]
1230}
1231
1232/// Code-list validation rules common to all UN/EDIFACT directory releases.
1233///
1234/// Each entry is `(element_index, component_index, data_element_id)`.
1235/// `element_index` and `component_index` are zero-based.
1236///
1237/// Covers the most frequently validated qualifier/code elements across ORDERS,
1238/// INVOIC, and similar message types.
1239pub(crate) fn base_code_list_rules(tag: &str) -> &'static [(usize, usize, &'static str)] {
1240    match tag {
1241        "BGM" => &[(0, 0, "1001")],
1242        "DTM" => &[(0, 0, "2005")],
1243        "NAD" => &[(0, 0, "3035")],
1244        "QTY" => &[(0, 0, "6063")],
1245        "RFF" => &[(0, 0, "1153")],
1246        "MOA" => &[(0, 0, "5025")],
1247        "PRI" => &[(0, 0, "5125")],
1248        "LOC" => &[(0, 0, "3227")],
1249        _ => &[],
1250    }
1251}
1252
1253/// Shared validator implementation that is configured per UN/EDIFACT directory release.
1254///
1255/// # Scope and limitations
1256///
1257/// `DirectoryValidator` validates individual segment *content* (element counts,
1258/// component counts, code-list values, and conditional rules) and checks that
1259/// every *mandatory* segment type is present at least once.  It does **not**
1260/// validate segment *sequence* or *repetition cardinality* — i.e., it cannot
1261/// tell you that a `BGM` segment appears more than once, or that a `RFF` group
1262/// appears in the wrong position.  Full sequence validation requires a
1263/// state-machine per message type (UN/EDIFACT Segment Tables) which is outside
1264/// the scope of this implementation.
1265#[derive(Clone)]
1266pub struct DirectoryValidator {
1267    directory_id: String,
1268    segment_lookup: SegmentLookupFn,
1269    /// Runtime-owned segment definitions (from builder / JSON / DB).
1270    ///
1271    /// When `Some`, takes precedence over `segment_lookup` for tag resolution.
1272    owned_defs: Option<Arc<Vec<OwnedSegmentDef>>>,
1273    /// Tag -> index into `owned_defs`.  Without this, `resolve_def` was a linear
1274    /// scan per segment, making validation O(n_segments x n_definitions).
1275    owned_index: Option<Arc<std::collections::HashMap<String, usize>>>,
1276    is_code_valid: IsCodeValidFn,
1277    suggest_code: SuggestCodeFn,
1278    expected_components: ExpectedComponentsFn,
1279    code_list_rules: CodeListRulesFn,
1280    additional_structure_rule: Option<AdditionalStructureRuleFn>,
1281    /// Configurable mapping from message type to required segment tags.
1282    required_segments: RequiredSegmentsFn,
1283    message_type: Option<String>,
1284    enforce_known_tags: bool,
1285    structure_checks: bool,
1286    code_list_checks: bool,
1287}
1288
1289impl std::fmt::Debug for DirectoryValidator {
1290    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1291        f.debug_struct("DirectoryValidator")
1292            .field("directory_id", &self.directory_id)
1293            .field("message_type", &self.message_type)
1294            .field("enforce_known_tags", &self.enforce_known_tags)
1295            .field("structure_checks", &self.structure_checks)
1296            .field("code_list_checks", &self.code_list_checks)
1297            .finish_non_exhaustive()
1298    }
1299}
1300
1301impl DirectoryValidator {
1302    /// Create a validator for a specific directory release with injected lookup/check hooks.
1303    pub fn new(
1304        directory_id: &'static str,
1305        segment_lookup: fn(&str) -> Option<&'static SegmentDefinition>,
1306        is_code_valid: fn(&str, &str) -> bool,
1307        suggest_code: fn(&str, &str) -> Option<&'static str>,
1308        expected_components: fn(&str, usize) -> Option<u8>,
1309        additional_structure_rule: Option<AdditionalStructureRuleRefFn>,
1310    ) -> Self {
1311        Self {
1312            directory_id: directory_id.to_owned(),
1313            segment_lookup: Arc::new(segment_lookup),
1314            owned_defs: None,
1315            owned_index: None,
1316            is_code_valid: Arc::new(is_code_valid),
1317            suggest_code: Arc::new(suggest_code),
1318            expected_components: Arc::new(expected_components),
1319            code_list_rules: Arc::new(base_code_list_rules),
1320            additional_structure_rule: additional_structure_rule
1321                .map(|f| Arc::new(f) as AdditionalStructureRuleFn),
1322            required_segments: Arc::new(default_required_segments),
1323            message_type: None,
1324            enforce_known_tags: true,
1325            structure_checks: true,
1326            code_list_checks: true,
1327        }
1328    }
1329
1330    /// Create a validator from a static slice of [`SegmentDefinition`]s.
1331    ///
1332    /// This is the preferred constructor when code-generating directory data as
1333    /// a `static` array: no manual fn-pointer boilerplate is required.
1334    ///
1335    /// Code-list checks are **disabled** by default (the built-in `is_code_valid`
1336    /// always returns `true`).  Call [`with_code_list_rules`][Self::with_code_list_rules]
1337    /// to register directory-specific rules that actually validate code values.
1338    ///
1339    /// # Example
1340    ///
1341    /// ```rust,ignore
1342    /// static MY_SEGMENTS: &[SegmentDefinition] = &[ /* … */ ];
1343    ///
1344    /// let validator = DirectoryValidator::from_definitions(MY_SEGMENTS)
1345    ///     .with_code_list_rules(my_code_list_rules);
1346    /// ```
1347    pub fn from_definitions(definitions: &'static [SegmentDefinition]) -> Self {
1348        let lookup_map: std::collections::HashMap<&'static str, &'static SegmentDefinition> =
1349            definitions.iter().map(|d| (d.tag, d)).collect();
1350        let lookup_map = Arc::new(lookup_map);
1351        Self {
1352            directory_id: "custom".to_owned(),
1353            segment_lookup: Arc::new(move |tag: &str| lookup_map.get(tag).copied()),
1354            owned_defs: None,
1355            owned_index: None,
1356            is_code_valid: Arc::new(|_de: &str, _code: &str| true),
1357            suggest_code: Arc::new(|_de: &str, _code: &str| None),
1358            expected_components: Arc::new(|_tag: &str, _idx: usize| None),
1359            code_list_rules: Arc::new(base_code_list_rules),
1360            additional_structure_rule: None,
1361            required_segments: Arc::new(default_required_segments),
1362            message_type: None,
1363            enforce_known_tags: true,
1364            structure_checks: true,
1365            code_list_checks: false,
1366        }
1367    }
1368
1369    /// Create a validator from a runtime-owned collection of segment definitions.
1370    ///
1371    /// Use this (or [`DirectoryValidatorBuilder`]) when segment definitions are
1372    /// loaded from an external source at startup (JSON, database, YAML, …) rather
1373    /// than being known at compile time.
1374    ///
1375    /// Code-list checks are **disabled** by default; enable them by chaining
1376    /// [`with_code_list_rules`][Self::with_code_list_rules] and setting
1377    /// `is_code_valid` via a custom [`new`][Self::new] call or by subclassing
1378    /// the builder.
1379    ///
1380    /// # Example
1381    ///
1382    /// ```rust,ignore
1383    /// let defs = vec![
1384    ///     OwnedSegmentDef::new_unchecked(
1385    ///         "BGM".to_owned(),
1386    ///         "Beginning of message".to_owned(),
1387    ///         vec![OwnedElementRef::new_unchecked(1, "C002".to_owned(), Status::Mandatory, 1)],
1388    ///     ),
1389    /// ];
1390    /// let validator = DirectoryValidator::from_owned_definitions(defs)
1391    ///     .with_directory_id("runtime-profile");
1392    /// ```
1393    pub fn from_owned_definitions(definitions: Vec<OwnedSegmentDef>) -> Self {
1394        Self {
1395            directory_id: "custom".to_owned(),
1396            // The static lookup is never consulted when `owned_defs` is `Some`.
1397            segment_lookup: Arc::new(|_| None),
1398            owned_index: Some(Arc::new(
1399                definitions
1400                    .iter()
1401                    .enumerate()
1402                    .map(|(i, d)| (d.tag.clone(), i))
1403                    .collect(),
1404            )),
1405            owned_defs: Some(Arc::new(definitions)),
1406            is_code_valid: Arc::new(|_de: &str, _code: &str| true),
1407            suggest_code: Arc::new(|_de: &str, _code: &str| None),
1408            expected_components: Arc::new(|_tag: &str, _idx: usize| None),
1409            code_list_rules: Arc::new(base_code_list_rules),
1410            additional_structure_rule: None,
1411            required_segments: Arc::new(default_required_segments),
1412            message_type: None,
1413            enforce_known_tags: true,
1414            structure_checks: true,
1415            code_list_checks: false,
1416        }
1417    }
1418
1419    /// Set the directory identifier string (used in error messages).
1420    pub fn with_directory_id(mut self, id: impl Into<String>) -> Self {
1421        self.directory_id = id.into();
1422        self
1423    }
1424
1425    /// Override the code-list rules function.
1426    ///
1427    /// Directories can supply a directory-specific implementation that extends or
1428    /// replaces the base rules from `base_code_list_rules`.
1429    pub fn with_code_list_rules(
1430        mut self,
1431        f: impl Fn(&str) -> &'static [(usize, usize, &'static str)] + Send + Sync + 'static,
1432    ) -> Self {
1433        self.code_list_rules = Arc::new(f);
1434        self
1435    }
1436
1437    /// Enable only structure checks and disable code-list checks.
1438    pub fn structure_only(mut self) -> Self {
1439        self.structure_checks = true;
1440        self.code_list_checks = false;
1441        self
1442    }
1443
1444    /// Enable only code-list checks and disable structure checks.
1445    pub fn code_list_only(mut self) -> Self {
1446        self.structure_checks = false;
1447        self.code_list_checks = true;
1448        self
1449    }
1450
1451    /// Configure whether unknown segment tags should be rejected.
1452    pub fn enforce_known_tags(mut self, enforce: bool) -> Self {
1453        self.enforce_known_tags = enforce;
1454        self
1455    }
1456
1457    /// Override the required-segments mapping used for structural validation.
1458    ///
1459    /// The supplied function receives an EDIFACT message type string (e.g. `"ORDERS"`)
1460    /// and must return a `'static` slice of segment tags that are mandatory for that
1461    /// type.  The tags are checked both for *presence* and for *canonical ordering*
1462    /// within the message.
1463    ///
1464    /// # Example
1465    ///
1466    /// ```rust,ignore
1467    /// fn my_required_segments(msg_type: &str) -> &'static [&'static str] {
1468    ///     match msg_type {
1469    ///         "DESADV" => &["UNH", "BGM", "SHP", "UNT"],
1470    ///         "INVOIC" => &["UNH", "BGM", "MOA", "UNT"],
1471    ///         _ => &["UNH", "UNT"],
1472    ///     }
1473    /// }
1474    ///
1475    /// let validator = DirectoryValidator::from_definitions(DEFS)
1476    ///     .with_required_segments(my_required_segments);
1477    /// ```
1478    pub fn with_required_segments(
1479        mut self,
1480        f: impl Fn(&str) -> &'static [&'static str] + Send + Sync + 'static,
1481    ) -> Self {
1482        self.required_segments = Arc::new(f);
1483        self
1484    }
1485
1486    fn detect_message_type(&self, segments: &[Segment<'_>]) -> Option<String> {
1487        if let Some(explicit) = self.message_type.as_deref() {
1488            return Some(explicit.to_owned());
1489        }
1490
1491        segments
1492            .iter()
1493            .find(|s| s.tag == "UNH")
1494            .and_then(|s| s.get_element(1))
1495            .and_then(|e| e.get_component(0))
1496            .map(str::to_owned)
1497    }
1498
1499    /// Count the non-trailing-empty components in element `element_idx` of `seg`.
1500    ///
1501    /// Per ISO 9735-1 §3.3 ("Trailing empty component data elements may be omitted"),
1502    /// a sender is not required to transmit trailing empty components; this function
1503    /// therefore strips them before checking against the expected count so that
1504    /// conformant messages with omitted trailing components are still accepted.
1505    ///
1506    /// # Examples
1507    ///
1508    /// - `DTM+137:20200101:` has three declared components but only 2 non-empty → effective=2
1509    /// - `NAD+MS++::293` has a composite with 3 components, last two empty → effective=1
1510    fn effective_component_count(seg: &Segment<'_>, element_idx: usize) -> Option<u8> {
1511        let elem = seg.elements.get(element_idx)?;
1512        let mut count = elem.components.len();
1513        while count > 0 && elem.components[count - 1].0.as_ref().is_empty() {
1514            count -= 1;
1515        }
1516        u8::try_from(count).ok()
1517    }
1518
1519    fn collect_component_count_issues(
1520        &self,
1521        seg: &Segment<'_>,
1522        def: &SegmentDefRef<'_>,
1523        out: &mut Vec<EdifactError>,
1524    ) {
1525        for idx in 0..seg.elements.len() {
1526            let actual = Self::effective_component_count(seg, idx).unwrap_or(0);
1527            // The `expected_components` hook is an exact count and wins when set.
1528            if let Some(expected) = (self.expected_components)(seg.tag, idx) {
1529                if actual != expected {
1530                    out.push(EdifactError::InvalidComponentCount {
1531                        tag: seg.tag.to_owned(),
1532                        element_index: idx,
1533                        expected,
1534                        actual,
1535                        span: seg.element_span(idx).unwrap_or(seg.span),
1536                    });
1537                }
1538                continue;
1539            }
1540            // Otherwise a composite that declares its components caps them:
1541            // more components than the directory defines is a structural error,
1542            // while fewer is normal (conditional components may be omitted).
1543            if let Some(declared) = def.declared_component_count(idx) {
1544                if actual > declared {
1545                    out.push(EdifactError::InvalidComponentCount {
1546                        tag: seg.tag.to_owned(),
1547                        element_index: idx,
1548                        expected: declared,
1549                        actual,
1550                        span: seg.element_span(idx).unwrap_or(seg.span),
1551                    });
1552                }
1553            }
1554        }
1555    }
1556
1557    fn collect_code_list_issues(&self, seg: &Segment<'_>, out: &mut Vec<EdifactError>) {
1558        for (elem_idx, comp_idx, de) in (self.code_list_rules)(seg.tag) {
1559            let value = seg
1560                .get_element(*elem_idx)
1561                .and_then(|e| e.get_component(*comp_idx))
1562                .unwrap_or("");
1563            if !value.is_empty() && !(self.is_code_valid)(de, value) {
1564                let suggestion = (self.suggest_code)(de, value);
1565                // Point at the offending *value*, not the whole segment, so
1566                // rendered diagnostics underline the code that failed.
1567                let span = seg
1568                    .get_element(*elem_idx)
1569                    .and_then(|e| e.component_span(*comp_idx))
1570                    .unwrap_or(seg.span);
1571                out.push(EdifactError::InvalidCodeValue {
1572                    tag: seg.tag.to_owned(),
1573                    element_index: *elem_idx,
1574                    value: value.to_owned(),
1575                    code_list: (*de).to_owned(),
1576                    span,
1577                    suggestion,
1578                });
1579            }
1580        }
1581    }
1582}
1583
1584impl DirectoryValidator {
1585    fn resolve_def<'a>(&'a self, tag: &str) -> Option<SegmentDefRef<'a>> {
1586        if let Some(owned) = &self.owned_defs {
1587            let index = self.owned_index.as_ref()?;
1588            owned.get(*index.get(tag)?).map(SegmentDefRef::Owned)
1589        } else {
1590            (self.segment_lookup)(tag).map(SegmentDefRef::Static)
1591        }
1592    }
1593
1594    /// Check one segment, appending **every** violation found to `out`.
1595    ///
1596    /// Reporting continues past the first fault: a segment missing two mandatory
1597    /// elements and carrying an invalid code is three findings, and a validator
1598    /// whose whole purpose is an exhaustive report has no business hiding two of
1599    /// them.  Only the checks that cannot proceed without a resolved definition
1600    /// short-circuit.
1601    fn collect_segment_issues(&self, seg: &Segment<'_>, out: &mut Vec<EdifactError>) {
1602        if !self.structure_checks && !self.code_list_checks {
1603            return;
1604        }
1605
1606        let Some(def) = self.resolve_def(seg.tag) else {
1607            if self.structure_checks && self.enforce_known_tags {
1608                out.push(EdifactError::InvalidSegmentForMessage {
1609                    tag: seg.tag.to_owned(),
1610                    message_type: self
1611                        .message_type
1612                        .clone()
1613                        .unwrap_or_else(|| self.directory_id.clone()),
1614                    span: seg.tag_span,
1615                });
1616            }
1617            // Without a definition there is nothing further to check against.
1618            return;
1619        };
1620
1621        if self.structure_checks {
1622            let max_elements = def.max_element_position();
1623            let min_elements = def.last_mandatory_position();
1624            let actual = seg.elements.len();
1625            if actual < min_elements || actual > max_elements {
1626                out.push(EdifactError::InvalidElementCount {
1627                    tag: seg.tag.to_owned(),
1628                    min: min_elements,
1629                    max: max_elements,
1630                    actual,
1631                    span: seg.span,
1632                });
1633            }
1634
1635            def.for_each_mandatory_position::<std::convert::Infallible, _>(|idx, _de| {
1636                let is_present = seg.elements.get(idx).is_some_and(|elem| {
1637                    elem.components.iter().any(|(c, _)| !c.as_ref().is_empty())
1638                });
1639                if !is_present {
1640                    out.push(EdifactError::MissingRequiredElement {
1641                        tag: seg.tag.to_owned(),
1642                        element_index: idx,
1643                    });
1644                }
1645                Ok(())
1646            })
1647            .unwrap_or_else(|never| match never {});
1648
1649            // Mandatory *components* inside declared composites.  Only fires for
1650            // definitions built with `ElementRef::composite` / `with_components`;
1651            // an element that is absent entirely is already reported above as a
1652            // missing element, so only present elements are checked here.
1653            def.for_each_mandatory_component::<std::convert::Infallible, _>(
1654                |elem_idx, comp_idx, _de| {
1655                    let Some(elem) = seg.elements.get(elem_idx) else {
1656                        return Ok(());
1657                    };
1658                    let present = elem
1659                        .get_component(comp_idx)
1660                        .is_some_and(|value| !value.is_empty());
1661                    if !present {
1662                        out.push(EdifactError::MissingRequiredComponent {
1663                            tag: seg.tag.to_owned(),
1664                            element_index: elem_idx,
1665                            component_index: comp_idx,
1666                        });
1667                    }
1668                    Ok(())
1669                },
1670            )
1671            .unwrap_or_else(|never| match never {});
1672
1673            self.collect_component_count_issues(seg, &def, out);
1674
1675            if let Some(rule) = &self.additional_structure_rule {
1676                if let Err(error) = rule(seg) {
1677                    out.push(error);
1678                }
1679            }
1680        }
1681
1682        if self.code_list_checks {
1683            self.collect_code_list_issues(seg, out);
1684        }
1685    }
1686}
1687
1688impl Validator for DirectoryValidator {
1689    fn set_message_type(&mut self, message_type: Option<&str>) {
1690        self.message_type = message_type.map(str::to_owned);
1691    }
1692
1693    fn validate_batch(
1694        &self,
1695        segments: &[Segment<'_>],
1696        report: &mut ValidationReport,
1697        _context: &ValidationRuleContext<'_>,
1698    ) {
1699        let mut issues = Vec::new();
1700        for seg in segments {
1701            self.collect_segment_issues(seg, &mut issues);
1702            for err in issues.drain(..) {
1703                report_error(report, err);
1704            }
1705        }
1706
1707        if self.structure_checks {
1708            if let Some(message_type) = self.detect_message_type(segments) {
1709                // One pass recording each tag's first index answers both the
1710                // presence and the ordering question.  The previous shape ran two
1711                // full scans *per required tag* and invoked `required_segments`
1712                // twice, which is O(|required| x n) on every batch.
1713                let mut first_index: std::collections::HashMap<&str, usize> =
1714                    std::collections::HashMap::with_capacity(segments.len());
1715                for (i, seg) in segments.iter().enumerate() {
1716                    first_index.entry(seg.tag).or_insert(i);
1717                }
1718
1719                let required = (self.required_segments)(&message_type);
1720                for required_tag in required {
1721                    if !first_index.contains_key(*required_tag) {
1722                        report.add_error(
1723                            ValidationIssue::new(
1724                                ValidationSeverity::Error,
1725                                format!(
1726                                    "required segment {} missing for message type {}",
1727                                    required_tag, message_type
1728                                ),
1729                            )
1730                            .with_segment(*required_tag)
1731                            .with_suggestion("Add the mandatory segment at the correct position"),
1732                        );
1733                    }
1734                }
1735
1736                let mut last_idx = None;
1737                for tag in required {
1738                    if let Some(&idx) = first_index.get(*tag) {
1739                        if let Some(prev) = last_idx {
1740                            if idx < prev {
1741                                report.add_error(
1742                                    ValidationIssue::new(
1743                                        ValidationSeverity::Error,
1744                                        format!(
1745                                            "segment sequence violation for message type {}: '{}' appears out of order",
1746                                            message_type, tag
1747                                        ),
1748                                    )
1749                                    .with_segment(*tag)
1750                                    .with_suggestion(
1751                                        "Ensure required segments follow UN/EDIFACT canonical order",
1752                                    ),
1753                                );
1754                            }
1755                        }
1756                        last_idx = Some(idx);
1757                    }
1758                }
1759            }
1760        }
1761    }
1762}
1763
1764// ── DirectoryValidatorBuilder ─────────────────────────────────────────────────
1765
1766/// Builder for [`DirectoryValidator`] using runtime-owned segment definitions.
1767///
1768/// Use this when segment definitions are loaded from an external source at
1769/// startup (JSON, database, YAML, …) rather than being available as `static`
1770/// arrays at compile time.
1771///
1772/// # Example
1773///
1774/// ```rust,ignore
1775/// let validator = DirectoryValidatorBuilder::new("my-profile")
1776///     .add_segment(
1777///         OwnedSegmentDef::new_unchecked(
1778///             "BGM".to_owned(),
1779///             "Beginning of message".to_owned(),
1780///             vec![OwnedElementRef::new_unchecked(1, "C002".to_owned(), Status::Mandatory, 1)],
1781///         ),
1782///     )
1783///     .build();
1784/// ```
1785#[derive(Debug, Default)]
1786pub struct DirectoryValidatorBuilder {
1787    directory_id: Option<String>,
1788    segments: Vec<OwnedSegmentDef>,
1789}
1790
1791impl DirectoryValidatorBuilder {
1792    /// Create a new builder with the given directory identifier.
1793    ///
1794    /// The identifier is used in error messages; set a human-readable value
1795    /// such as `"ORDERS-MIG-5.5"` or `"custom-profile"`.
1796    pub fn new(directory_id: impl Into<String>) -> Self {
1797        Self {
1798            directory_id: Some(directory_id.into()),
1799            segments: Vec::new(),
1800        }
1801    }
1802
1803    /// Add a segment definition to the builder.
1804    ///
1805    /// Definitions can be added in any order; the resulting validator looks
1806    /// them up by tag at validation time.
1807    pub fn add_segment(mut self, def: OwnedSegmentDef) -> Self {
1808        self.segments.push(def);
1809        self
1810    }
1811
1812    /// Extend the builder with multiple segment definitions at once.
1813    pub fn add_segments(mut self, defs: impl IntoIterator<Item = OwnedSegmentDef>) -> Self {
1814        self.segments.extend(defs);
1815        self
1816    }
1817
1818    /// Build the [`DirectoryValidator`].
1819    ///
1820    /// Returns a validator backed by the accumulated [`OwnedSegmentDef`]s.
1821    /// Code-list checks are disabled by default; chain
1822    /// [`DirectoryValidator::with_code_list_rules`] on the returned value to
1823    /// enable them.
1824    pub fn build(self) -> DirectoryValidator {
1825        let mut validator = DirectoryValidator::from_owned_definitions(self.segments);
1826        if let Some(id) = self.directory_id {
1827            validator.directory_id = id;
1828        }
1829        validator
1830    }
1831}
1832
1833#[cfg(test)]
1834mod tests {
1835    use super::*;
1836
1837    static TEST_ELEMENTS: &[ElementRef] = &[ElementRef::new(1, "C507", Status::Mandatory, 1)];
1838
1839    static TEST_SEGMENT: SegmentDefinition =
1840        SegmentDefinition::new("TST", "Test segment", TEST_ELEMENTS);
1841
1842    fn segment_lookup(tag: &str) -> Option<&'static SegmentDefinition> {
1843        match tag {
1844            "TST" => Some(&TEST_SEGMENT),
1845            _ => None,
1846        }
1847    }
1848
1849    fn code_valid(_de: &str, _code: &str) -> bool {
1850        true
1851    }
1852
1853    fn suggest_code(_de: &str, _code: &str) -> Option<&'static str> {
1854        None
1855    }
1856
1857    fn expected_components(_tag: &str, _idx: usize) -> Option<u8> {
1858        None
1859    }
1860
1861    #[test]
1862    fn mandatory_composite_present_when_any_component_non_empty() {
1863        let input = b"TST+:ABC'";
1864        let segments: Vec<_> = crate::from_bytes(input)
1865            .collect::<Result<Vec<_>, _>>()
1866            .expect("parse should succeed");
1867
1868        let validator = DirectoryValidator::new(
1869            "TEST",
1870            segment_lookup,
1871            code_valid,
1872            suggest_code,
1873            expected_components,
1874            None,
1875        );
1876
1877        let mut report = ValidationReport::default();
1878        validator.validate_batch(
1879            &segments,
1880            &mut report,
1881            &crate::validator::ValidationRuleContext::empty(),
1882        );
1883        assert!(!report.has_errors());
1884    }
1885
1886    // ── effective_component_count (ISO 9735-1 §3.3 trailing-empty-component trim) ──
1887
1888    fn parse_single(input: &[u8]) -> crate::OwnedSegment {
1889        crate::from_reader_collect(std::io::Cursor::new(input))
1890            .expect("parse should succeed")
1891            .into_iter()
1892            .next()
1893            .expect("at least one segment")
1894    }
1895
1896    #[test]
1897    fn trailing_empty_component_stripped_from_dtm() {
1898        // DTM+137:20200101: has three components in element 0; the third is empty.
1899        // ISO 9735-1 §3.3 says trailing empty components may be omitted,
1900        // so effective count should be 2.
1901        let owned = parse_single(b"DTM+137:20200101:'");
1902        let seg = owned.as_borrowed();
1903        let count = DirectoryValidator::effective_component_count(&seg, 0);
1904        assert_eq!(
1905            count,
1906            Some(2),
1907            "trailing empty component should be stripped"
1908        );
1909    }
1910
1911    #[test]
1912    fn all_empty_components_result_in_zero() {
1913        // NAD+MS++: → element 2 is ":" with two empty components → effective=0
1914        let owned = parse_single(b"NAD+MS++:'");
1915        let seg = owned.as_borrowed();
1916        let count = DirectoryValidator::effective_component_count(&seg, 2);
1917        assert_eq!(
1918            count,
1919            Some(0),
1920            "all-empty composite should have effective count 0"
1921        );
1922    }
1923
1924    #[test]
1925    fn non_empty_component_not_stripped() {
1926        // DTM+137:20200101:102 — all three components are non-empty
1927        let owned = parse_single(b"DTM+137:20200101:102'");
1928        let seg = owned.as_borrowed();
1929        let count = DirectoryValidator::effective_component_count(&seg, 0);
1930        assert_eq!(
1931            count,
1932            Some(3),
1933            "no components should be stripped when all non-empty"
1934        );
1935    }
1936
1937    #[test]
1938    fn with_code_list_rules_overrides_base() {
1939        // Override code-list rules to require element 0 of TST to be a specific code.
1940        fn custom_rules(tag: &str) -> &'static [(usize, usize, &'static str)] {
1941            match tag {
1942                "TST" => &[(0, 0, "CUSTOM_DE")],
1943                _ => &[],
1944            }
1945        }
1946        fn custom_code_valid(_de: &str, code: &str) -> bool {
1947            code == "VALID"
1948        }
1949        fn no_suggestion(_de: &str, _code: &str) -> Option<&'static str> {
1950            None
1951        }
1952
1953        let input = b"TST+INVALID'";
1954        let segments: Vec<_> = crate::from_bytes(input)
1955            .collect::<Result<Vec<_>, _>>()
1956            .expect("parse should succeed");
1957
1958        let validator = DirectoryValidator::new(
1959            "TEST",
1960            segment_lookup,
1961            custom_code_valid,
1962            no_suggestion,
1963            expected_components,
1964            None,
1965        )
1966        .with_code_list_rules(custom_rules);
1967
1968        let mut report = ValidationReport::default();
1969        validator.validate_batch(
1970            &segments,
1971            &mut report,
1972            &crate::validator::ValidationRuleContext::empty(),
1973        );
1974        assert!(
1975            report.has_warnings(),
1976            "INVALID is not in the custom code list so validation must warn"
1977        );
1978    }
1979}