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