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}