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}