edifact_rs/directory_validator.rs
1//! Shared UN/EDIFACT directory validation engine used by D.11A, D.01B and D.96A.
2
3use crate::error::Insignificant;
4use crate::validator::{ValidationRuleContext, Validator, report_error};
5use crate::{EdifactError, Segment, ValidationIssue, ValidationReport, ValidationSeverity};
6use std::sync::Arc;
7
8/// Mandatory/Conditional status of a data element within a segment.
9///
10/// Marked `#[non_exhaustive]` because UN/EDIFACT also defines Required, Advised,
11/// Dependent, and Not-used statuses; adding one must not break downstream `match`
12/// arms.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[non_exhaustive]
15pub enum Status {
16 /// Element must be present.
17 Mandatory,
18 /// Element is optional unless additional rules require it.
19 Conditional,
20}
21
22/// The character class of a data element value — the `a` / `n` / `an` of a
23/// directory's representation column.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
25#[non_exhaustive]
26pub enum ReprKind {
27 /// `a` — alphabetic. Digits are not permitted.
28 Alphabetic,
29 /// `n` — numeric, in the ISO 6093 forms ISO 9735-1 §10 admits: digits, an
30 /// optional leading minus, a decimal mark (`.` or `,`), and an exponent.
31 /// The space character and the plus sign are explicitly not allowed.
32 Numeric,
33 /// `an` — alphanumeric. Any character the interchange's repertoire permits.
34 Alphanumeric,
35}
36
37impl ReprKind {
38 /// The directory's abbreviation for this class.
39 #[must_use]
40 pub const fn as_str(self) -> &'static str {
41 match self {
42 Self::Alphabetic => "a",
43 Self::Numeric => "n",
44 Self::Alphanumeric => "an",
45 }
46 }
47}
48
49/// A data element's representation — `an..35`, `n8`, `a1` and friends.
50///
51/// This is the column every UN/EDIFACT directory prints beside a data element,
52/// and the thing partners actually reject on: a sender identification of 40
53/// characters where the standard says `an..35` is refused at the far end, long
54/// after it was sent.
55///
56/// # Length is counted in characters
57///
58/// ISO 9735-1 §6: "one graphic character shall be counted as one character,
59/// irrespective of the number of bytes/octets required to encode it" — so `ü`
60/// is one, not two. §5 excludes the release character from the count, which is
61/// automatic here because release sequences are already resolved by the time a
62/// value reaches validation.
63///
64/// For a numeric value §10 excludes more: "the length ... shall not include the
65/// minus sign (-), the decimal mark (. or ,), or the exponent mark (E or e) and
66/// its exponent". `-123.45` is therefore five characters long, not seven.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub struct Repr {
69 kind: ReprKind,
70 max: u16,
71 fixed: bool,
72}
73
74impl Repr {
75 /// `an{max}` — exactly `max` alphanumeric characters.
76 #[must_use]
77 pub const fn an(max: u16) -> Self {
78 Self {
79 kind: ReprKind::Alphanumeric,
80 max,
81 fixed: true,
82 }
83 }
84
85 /// `an..{max}` — up to `max` alphanumeric characters.
86 #[must_use]
87 pub const fn an_up_to(max: u16) -> Self {
88 Self {
89 kind: ReprKind::Alphanumeric,
90 max,
91 fixed: false,
92 }
93 }
94
95 /// `a{max}` — exactly `max` alphabetic characters.
96 #[must_use]
97 pub const fn a(max: u16) -> Self {
98 Self {
99 kind: ReprKind::Alphabetic,
100 max,
101 fixed: true,
102 }
103 }
104
105 /// `a..{max}` — up to `max` alphabetic characters.
106 #[must_use]
107 pub const fn a_up_to(max: u16) -> Self {
108 Self {
109 kind: ReprKind::Alphabetic,
110 max,
111 fixed: false,
112 }
113 }
114
115 /// `n{max}` — exactly `max` numeric characters.
116 #[must_use]
117 pub const fn n(max: u16) -> Self {
118 Self {
119 kind: ReprKind::Numeric,
120 max,
121 fixed: true,
122 }
123 }
124
125 /// `n..{max}` — up to `max` numeric characters.
126 #[must_use]
127 pub const fn n_up_to(max: u16) -> Self {
128 Self {
129 kind: ReprKind::Numeric,
130 max,
131 fixed: false,
132 }
133 }
134
135 /// The character class.
136 #[must_use]
137 pub const fn kind(self) -> ReprKind {
138 self.kind
139 }
140
141 /// The maximum number of characters.
142 #[must_use]
143 pub const fn max_length(self) -> u16 {
144 self.max
145 }
146
147 /// The minimum number of characters — `max_length` when fixed, else 1.
148 ///
149 /// A data element is "present" only when it carries at least one character
150 /// (§8.1), so a variable-length minimum is never zero.
151 #[must_use]
152 pub const fn min_length(self) -> u16 {
153 if self.fixed { self.max } else { 1 }
154 }
155
156 /// Whether the length is fixed (`an3`) rather than variable (`an..3`).
157 #[must_use]
158 pub const fn is_fixed(self) -> bool {
159 self.fixed
160 }
161
162 /// The number of characters `value` contributes toward its length limit.
163 ///
164 /// For a numeric value this is §10's count, which excludes the sign, the
165 /// decimal mark, and the exponent.
166 #[must_use]
167 pub fn measure(self, value: &str) -> usize {
168 match self.kind {
169 ReprKind::Numeric => {
170 let mantissa = value
171 .split_once(['E', 'e'])
172 .map_or(value, |(mantissa, _exponent)| mantissa);
173 mantissa
174 .chars()
175 .filter(|c| !matches!(c, '-' | '.' | ','))
176 .count()
177 }
178 _ => value.chars().count(),
179 }
180 }
181
182 /// Whether every character of `value` belongs to this representation's class.
183 #[must_use]
184 pub fn permits_characters(self, value: &str) -> bool {
185 match self.kind {
186 ReprKind::Alphanumeric => true,
187 ReprKind::Alphabetic => !value.chars().any(|c| c.is_ascii_digit()),
188 ReprKind::Numeric => is_iso6093_numeric(value),
189 }
190 }
191}
192
193impl std::fmt::Display for Repr {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 if self.fixed {
196 write!(f, "{}{}", self.kind.as_str(), self.max)
197 } else {
198 write!(f, "{}..{}", self.kind.as_str(), self.max)
199 }
200 }
201}
202
203/// Whether `value` is one of the numeric forms ISO 9735-1 §10 admits.
204///
205/// §10 takes ISO 6093's representations and subtracts: "The space character and
206/// plus sign shall not be allowed", and "when a decimal mark is transferred,
207/// there shall be at least one digit after the decimal mark" — which is why
208/// `1.` and `.` are rejected while `.5` and `2.00` are not.
209fn is_iso6093_numeric(value: &str) -> bool {
210 let (mantissa, exponent) = match value.split_once(['E', 'e']) {
211 Some((mantissa, exponent)) => (mantissa, Some(exponent)),
212 None => (value, None),
213 };
214 if let Some(exponent) = exponent {
215 let digits = exponent.strip_prefix('-').unwrap_or(exponent);
216 if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
217 return false;
218 }
219 }
220 let digits = mantissa.strip_prefix('-').unwrap_or(mantissa);
221 if digits.is_empty() {
222 return false;
223 }
224 match digits.split_once(['.', ',']) {
225 Some((integer, fraction)) => {
226 // At least one digit after the mark; the integer part may be empty.
227 !fraction.is_empty()
228 && fraction.chars().all(|c| c.is_ascii_digit())
229 && integer.chars().all(|c| c.is_ascii_digit())
230 }
231 None => digits.chars().all(|c| c.is_ascii_digit()),
232 }
233}
234
235/// What a value at one position must satisfy.
236///
237/// Usually a single [`Repr`]. Two only where the syntax versions disagree and
238/// the interchange has not said which it is — see
239/// [`ComponentRef::with_repr_by_syntax_version`].
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241struct ReprRequirement {
242 primary: Repr,
243 /// Accepted as well, when the syntax version could not be determined.
244 alternative: Option<Repr>,
245}
246
247impl ReprRequirement {
248 const fn single(repr: Repr) -> Self {
249 Self {
250 primary: repr,
251 alternative: None,
252 }
253 }
254
255 /// Whether `value` satisfies the class of any accepted representation.
256 fn permits_characters(&self, value: &str) -> bool {
257 self.primary.permits_characters(value)
258 || self
259 .alternative
260 .is_some_and(|repr| repr.permits_characters(value))
261 }
262
263 /// Whether `value`'s length satisfies any accepted representation.
264 fn permits_length(&self, value: &str) -> bool {
265 let fits = |repr: Repr| {
266 let length = repr.measure(value);
267 length >= usize::from(repr.min_length()) && length <= usize::from(repr.max_length())
268 };
269 fits(self.primary) || self.alternative.is_some_and(fits)
270 }
271
272 /// `true` when every accepted representation is longer than `value`.
273 fn is_too_short(&self, value: &str) -> bool {
274 let short = |repr: Repr| repr.measure(value) < usize::from(repr.min_length());
275 short(self.primary) && self.alternative.is_none_or(short)
276 }
277
278 /// The measured length under the primary representation, for reporting.
279 fn measure(&self, value: &str) -> usize {
280 self.primary.measure(value)
281 }
282}
283
284impl std::fmt::Display for ReprRequirement {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 match self.alternative {
287 Some(alternative) => write!(f, "{} or {alternative}", self.primary),
288 None => write!(f, "{}", self.primary),
289 }
290 }
291}
292
293/// Reference to a component data element within a composite data element.
294///
295/// Composites such as `C507` (DTM date/time/period) are addressed by the
296/// identifier of their *own* components (`2005`, `2380`, `2379`), which is what
297/// makes code-addressed access — [`Segment::value_by_code`][crate::Segment::value_by_code]
298/// and `#[edifact(element = "2005")]` — resolve to the right slot instead of a
299/// hand-counted index.
300///
301/// Fields are private to enforce the one-based position invariant.
302#[derive(Debug, Clone, Copy)]
303pub struct ComponentRef {
304 /// One-based position of the **first** slot this component occupies.
305 position: u8,
306 /// UN/EDIFACT component data element identifier.
307 data_element: &'static str,
308 /// Requirement status of the component.
309 status: Status,
310 /// How many consecutive slots this component occupies; `1` unless the
311 /// composite repeats it by design.
312 repeat_count: u8,
313 /// The directory's representation, when the definition states one.
314 repr: Option<Repr>,
315 /// The representation from syntax version 4 onward, where it differs.
316 repr_from_v4: Option<Repr>,
317}
318
319impl ComponentRef {
320 /// Construct a `ComponentRef` with compile-time position validation.
321 ///
322 /// `position` must be ≥ 1 (one-based). In a `const` context a zero
323 /// `position` is a **compile-time error**; at runtime it panics.
324 ///
325 /// # Panics
326 ///
327 /// Panics if `position == 0`.
328 ///
329 /// # Example
330 ///
331 /// ```rust
332 /// use edifact_rs::{ComponentRef, Status};
333 ///
334 /// const DTM_2005: ComponentRef = ComponentRef::new(1, "2005", Status::Mandatory);
335 /// ```
336 #[must_use]
337 pub const fn new(position: u8, data_element: &'static str, status: Status) -> Self {
338 assert!(
339 position != 0,
340 "ComponentRef position must be >= 1 (one-based)"
341 );
342 Self {
343 position,
344 data_element,
345 status,
346 repeat_count: 1,
347 repr: None,
348 repr_from_v4: None,
349 }
350 }
351
352 /// Declare a component the composite repeats by design.
353 ///
354 /// Several standard composites carry the same data element several times
355 /// over: `C080 PARTY NAME` is `3036` five times followed by `3045`, `C059
356 /// STREET` is `3042` four times. Spelling that out as five separate
357 /// [`new`][Self::new] entries made the code count as five positions, so
358 /// [`resolve_code`][SegmentLayout::resolve_code] reported it
359 /// [ambiguous][EdifactError::AmbiguousDataElement] and the component could
360 /// not be code-addressed at all — a faithful declaration was punished, and
361 /// the only way to use named access was to declare the composite
362 /// incompletely and disagree with the directory it claims to model.
363 ///
364 /// One `repeated` entry is one position, so `3036` resolves to occurrence 1
365 /// — what "the party name" means in every real message — while the
366 /// definition still records that five slots belong to it.
367 ///
368 /// `position` is the **first** slot; the next component follows at
369 /// `position + repeat_count`.
370 ///
371 /// # Panics
372 ///
373 /// Panics if `position == 0` or `repeat_count == 0`.
374 ///
375 /// # Example
376 ///
377 /// ```rust
378 /// use edifact_rs::{ComponentRef, ElementRef, SegmentDefinition, SegmentLayout, Status};
379 ///
380 /// // C080 PARTY NAME: 3036 ×5, then 3045 at position 6.
381 /// static C080: &[ComponentRef] = &[
382 /// ComponentRef::repeated(1, "3036", Status::Mandatory, 5),
383 /// ComponentRef::new(6, "3045", Status::Conditional),
384 /// ];
385 /// static NAD_ELEMENTS: &[ElementRef] = &[
386 /// ElementRef::new(1, "3035", Status::Mandatory, 1),
387 /// ElementRef::composite(4, "C080", Status::Conditional, 1, C080),
388 /// ];
389 /// static NAD: SegmentDefinition = SegmentDefinition::new("NAD", "Name and address", NAD_ELEMENTS);
390 ///
391 /// // Addressable, and it points at the first occurrence.
392 /// let path = NAD.resolve_code("3036")?;
393 /// assert_eq!((path.element, path.component), (3, Some(0)));
394 /// # Ok::<(), edifact_rs::EdifactError>(())
395 /// ```
396 #[must_use]
397 pub const fn repeated(
398 position: u8,
399 data_element: &'static str,
400 status: Status,
401 repeat_count: u8,
402 ) -> Self {
403 assert!(
404 position != 0,
405 "ComponentRef position must be >= 1 (one-based)"
406 );
407 assert!(
408 repeat_count != 0,
409 "ComponentRef repeat_count must be >= 1; use `new` for a component that does not repeat"
410 );
411 Self {
412 position,
413 data_element,
414 status,
415 repeat_count,
416 repr: None,
417 repr_from_v4: None,
418 }
419 }
420
421 /// How many consecutive slots this component occupies.
422 ///
423 /// `1` for a component declared with [`new`][Self::new].
424 #[must_use]
425 #[inline]
426 pub const fn repeat_count(&self) -> u8 {
427 self.repeat_count
428 }
429
430 /// One-based component position within the composite.
431 #[must_use]
432 #[inline]
433 pub const fn position(&self) -> u8 {
434 self.position
435 }
436
437 /// UN/EDIFACT component data element identifier.
438 #[must_use]
439 #[inline]
440 pub const fn data_element(&self) -> &'static str {
441 self.data_element
442 }
443
444 /// Requirement status of the component.
445 #[must_use]
446 #[inline]
447 pub const fn status(&self) -> Status {
448 self.status
449 }
450
451 /// Attach the directory's representation, e.g. `an..35`.
452 ///
453 /// Without one, a definition still resolves identifiers and checks presence;
454 /// with one, [`DirectoryValidator`] also checks length and character class —
455 /// which is what a partner's translator does before it rejects the file.
456 ///
457 /// # Example
458 ///
459 /// ```
460 /// use edifact_rs::{ComponentRef, Repr, Status};
461 ///
462 /// const SENDER: ComponentRef =
463 /// ComponentRef::new(1, "0004", Status::Mandatory).with_repr(Repr::an_up_to(35));
464 /// ```
465 #[must_use]
466 pub const fn with_repr(mut self, repr: Repr) -> Self {
467 self.repr = Some(repr);
468 self
469 }
470
471 /// The declared representation, if the definition states one.
472 #[must_use]
473 #[inline]
474 pub const fn repr(&self) -> Option<Repr> {
475 self.repr
476 }
477 /// Declare a representation that changed between syntax versions.
478 ///
479 /// The service directory has exactly one such position: `S004` DE 0017, the
480 /// date of preparation. Version 3 transfers `YYMMDD` (`n6`); version 4
481 /// widened it to `CCYYMMDD` (`n8`) to be year-2000 correct. It is the only
482 /// place where version 4 is *not* a superset of version 3.
483 ///
484 /// Declaring both keeps each version checked exactly. Collapsing them into
485 /// `n..8` would have accepted a six-digit date in a version 4 interchange
486 /// and a seven-digit one in either — validating neither version correctly.
487 ///
488 /// The syntax version comes from `UNB` S001 DE 0002. When it cannot be
489 /// determined — validating a bare message window, say — **both** are
490 /// accepted, because guessing would reject conformant data.
491 #[must_use]
492 pub const fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
493 self.repr = Some(up_to_v3);
494 self.repr_from_v4 = Some(from_v4);
495 self
496 }
497
498 /// The representation used from syntax version 4 onward, when it differs.
499 #[must_use]
500 #[inline]
501 pub const fn repr_from_v4(&self) -> Option<Repr> {
502 self.repr_from_v4
503 }
504}
505
506/// Reference to a data element within a segment definition.
507///
508/// Fields are private to enforce the one-based position invariant through the
509/// [`ElementRef::new`] constructor. Use [`ElementRef::new`] for a simple data
510/// element and [`ElementRef::composite`] for a composite whose components are
511/// themselves named (panics at compile time when `position == 0`).
512///
513/// Use [`OwnedElementRef`] for runtime-constructed element refs.
514#[derive(Debug, Clone, Copy)]
515pub struct ElementRef {
516 /// One-based element position in the segment definition.
517 position: u8,
518 /// UN/EDIFACT data element identifier.
519 data_element: &'static str,
520 /// Requirement status of the element.
521 status: Status,
522 /// Maximum repetition count for this element.
523 max_repeat: u8,
524 /// Component definitions when this element is a composite; empty for a
525 /// simple data element.
526 components: &'static [ComponentRef],
527 /// The directory's representation for a *simple* element; composites carry
528 /// theirs on each component.
529 repr: Option<Repr>,
530 /// The representation from syntax version 4 onward, where it differs.
531 repr_from_v4: Option<Repr>,
532}
533
534impl ElementRef {
535 /// Construct an `ElementRef` for a simple data element.
536 ///
537 /// `position` must be ≥ 1 (one-based). When called in a `const` context
538 /// (e.g. inside a `static` array initialiser), a zero `position` causes a
539 /// **compile-time error**. At runtime it panics.
540 ///
541 /// Use [`composite`][Self::composite] when the element is a composite whose
542 /// components carry their own UN/EDIFACT identifiers.
543 ///
544 /// # Panics
545 ///
546 /// Panics if `position == 0`.
547 ///
548 /// # Example
549 ///
550 /// ```rust
551 /// use edifact_rs::{ElementRef, Status};
552 ///
553 /// const BGM_1001: ElementRef = ElementRef::new(1, "1001", Status::Mandatory, 1);
554 /// ```
555 #[must_use]
556 pub const fn new(
557 position: u8,
558 data_element: &'static str,
559 status: Status,
560 max_repeat: u8,
561 ) -> Self {
562 assert!(
563 position != 0,
564 "ElementRef position must be >= 1 (one-based)"
565 );
566 Self {
567 position,
568 data_element,
569 status,
570 max_repeat,
571 components: &[],
572 repr: None,
573 repr_from_v4: None,
574 }
575 }
576
577 /// Construct an `ElementRef` for a composite data element with named components.
578 ///
579 /// Declaring components is what lets code-addressed access reach *inside* a
580 /// composite: `value_by_code(&DTM, "2380")` resolves to element 1,
581 /// component 2 without the caller counting positions. Declared components
582 /// also make the mandatory-component check in [`DirectoryValidator`] active
583 /// for this element.
584 ///
585 /// # Panics
586 ///
587 /// Panics if `position == 0`.
588 ///
589 /// # Example
590 ///
591 /// ```rust
592 /// use edifact_rs::{ComponentRef, ElementRef, Status};
593 ///
594 /// static C507: &[ComponentRef] = &[
595 /// ComponentRef::new(1, "2005", Status::Mandatory),
596 /// ComponentRef::new(2, "2380", Status::Conditional),
597 /// ComponentRef::new(3, "2379", Status::Conditional),
598 /// ];
599 /// const DTM_C507: ElementRef =
600 /// ElementRef::composite(1, "C507", Status::Mandatory, 1, C507);
601 /// ```
602 #[must_use]
603 pub const fn composite(
604 position: u8,
605 data_element: &'static str,
606 status: Status,
607 max_repeat: u8,
608 components: &'static [ComponentRef],
609 ) -> Self {
610 assert!(
611 position != 0,
612 "ElementRef position must be >= 1 (one-based)"
613 );
614 Self {
615 position,
616 data_element,
617 status,
618 max_repeat,
619 components,
620 repr: None,
621 repr_from_v4: None,
622 }
623 }
624
625 /// One-based element position in the segment definition.
626 #[must_use]
627 #[inline]
628 pub const fn position(&self) -> u8 {
629 self.position
630 }
631
632 /// UN/EDIFACT data element identifier.
633 #[must_use]
634 #[inline]
635 pub const fn data_element(&self) -> &'static str {
636 self.data_element
637 }
638
639 /// Requirement status of the element.
640 #[must_use]
641 #[inline]
642 pub const fn status(&self) -> Status {
643 self.status
644 }
645
646 /// Maximum repetition count for this element.
647 #[must_use]
648 #[inline]
649 pub const fn max_repeat(&self) -> u8 {
650 self.max_repeat
651 }
652
653 /// Component definitions; empty when this is a simple data element.
654 #[must_use]
655 #[inline]
656 pub const fn components(&self) -> &'static [ComponentRef] {
657 self.components
658 }
659
660 /// Attach the directory's representation for a simple data element.
661 ///
662 /// A composite carries its representations on the components instead, so
663 /// this is ignored when `components` is non-empty.
664 #[must_use]
665 pub const fn with_repr(mut self, repr: Repr) -> Self {
666 self.repr = Some(repr);
667 self
668 }
669
670 /// The declared representation, if the definition states one.
671 #[must_use]
672 #[inline]
673 pub const fn repr(&self) -> Option<Repr> {
674 self.repr
675 }
676 /// Declare a representation that changed between syntax versions.
677 ///
678 /// The service directory has exactly one such position: `S004` DE 0017, the
679 /// date of preparation. Version 3 transfers `YYMMDD` (`n6`); version 4
680 /// widened it to `CCYYMMDD` (`n8`) to be year-2000 correct. It is the only
681 /// place where version 4 is *not* a superset of version 3.
682 ///
683 /// Declaring both keeps each version checked exactly. Collapsing them into
684 /// `n..8` would have accepted a six-digit date in a version 4 interchange
685 /// and a seven-digit one in either — validating neither version correctly.
686 ///
687 /// The syntax version comes from `UNB` S001 DE 0002. When it cannot be
688 /// determined — validating a bare message window, say — **both** are
689 /// accepted, because guessing would reject conformant data.
690 #[must_use]
691 pub const fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
692 self.repr = Some(up_to_v3);
693 self.repr_from_v4 = Some(from_v4);
694 self
695 }
696
697 /// The representation used from syntax version 4 onward, when it differs.
698 #[must_use]
699 #[inline]
700 pub const fn repr_from_v4(&self) -> Option<Repr> {
701 self.repr_from_v4
702 }
703}
704
705/// Definition of an EDIFACT segment (tag + element structure).
706///
707/// Construct with [`SegmentDefinition::new`] rather than a struct literal, so
708/// that future fields (max repeat, description, …) are not a breaking change.
709#[derive(Debug)]
710#[non_exhaustive]
711pub struct SegmentDefinition {
712 /// Segment tag.
713 pub tag: &'static str,
714 /// Human-readable segment name.
715 pub name: &'static str,
716 /// Ordered element definitions.
717 pub elements: &'static [ElementRef],
718}
719
720/// Byte-wise string equality usable in a `const` context.
721///
722/// `str::eq` is not `const`, and code resolution has to run at compile time so
723/// that a mistyped data element identifier in `#[edifact(element = "3055")]`
724/// fails the build rather than reading the wrong slot at runtime.
725const fn const_str_eq(a: &str, b: &str) -> bool {
726 let (a, b) = (a.as_bytes(), b.as_bytes());
727 if a.len() != b.len() {
728 return false;
729 }
730 let mut i = 0;
731 while i < a.len() {
732 if a[i] != b[i] {
733 return false;
734 }
735 i += 1;
736 }
737 true
738}
739
740/// The resolved position of a UN/EDIFACT data element within a segment.
741///
742/// Produced by [`SegmentLayout::resolve_code`] and consumed by the `*_at`
743/// accessors on [`Segment`] — and therefore on `OwnedSegment` too.
744///
745/// Both indices are **zero-based**, matching the positional accessors — the
746/// one-based positions used in directory definitions are converted during
747/// resolution.
748#[derive(Debug, Clone, Copy, PartialEq, Eq)]
749pub struct ElementPath {
750 /// Zero-based index of the data element within the segment.
751 pub element: usize,
752 /// Zero-based index of the component within a composite.
753 ///
754 /// `None` when the code names the data element itself (a simple element, or
755 /// a composite addressed as a whole). Value lookups treat `None` as
756 /// component 0, which is the first — and for a simple element, only —
757 /// component.
758 pub component: Option<usize>,
759}
760
761impl ElementPath {
762 /// Path to a whole data element.
763 #[must_use]
764 #[inline]
765 pub const fn element(element: usize) -> Self {
766 Self {
767 element,
768 component: None,
769 }
770 }
771
772 /// Path to a component within a composite data element.
773 #[must_use]
774 #[inline]
775 pub const fn component(element: usize, component: usize) -> Self {
776 Self {
777 element,
778 component: Some(component),
779 }
780 }
781
782 /// Zero-based component index, treating "whole element" as component 0.
783 #[must_use]
784 #[inline]
785 pub const fn component_index(&self) -> usize {
786 match self.component {
787 Some(c) => c,
788 None => 0,
789 }
790 }
791}
792
793/// Directory metadata that maps UN/EDIFACT data element identifiers to positions.
794///
795/// Implemented by [`SegmentDefinition`] (compile-time tables) and
796/// [`OwnedSegmentDef`] (runtime-loaded definitions), so the same code-addressed
797/// accessors work against either source.
798///
799/// # Example
800///
801/// ```rust
802/// use edifact_rs::{ElementRef, SegmentDefinition, SegmentLayout, Status};
803///
804/// static BGM_ELEMENTS: &[ElementRef] = &[
805/// ElementRef::new(1, "C002", Status::Conditional, 1),
806/// ElementRef::new(2, "C106", Status::Conditional, 1),
807/// ElementRef::new(3, "1225", Status::Conditional, 1),
808/// ];
809/// static BGM: SegmentDefinition =
810/// SegmentDefinition::new("BGM", "Beginning of message", BGM_ELEMENTS);
811///
812/// let path = BGM.resolve_code("1225")?;
813/// assert_eq!(path.element, 2);
814/// assert!(BGM.resolve_code("9999").is_err());
815/// # Ok::<(), edifact_rs::EdifactError>(())
816/// ```
817pub trait SegmentLayout {
818 /// The segment tag this layout describes (e.g. `"NAD"`).
819 fn layout_tag(&self) -> &str;
820
821 /// Resolve a UN/EDIFACT data element identifier to a position.
822 ///
823 /// # Errors
824 ///
825 /// Returns [`EdifactError::UnknownDataElement`] when the identifier does not
826 /// appear in this definition, and [`EdifactError::AmbiguousDataElement`]
827 /// when it appears at more than one position.
828 fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError>;
829
830 /// Every position this layout declares, flattened and in order.
831 ///
832 /// Implemented by both the compile-time and runtime definitions, so tooling
833 /// can walk a layout without knowing which one it holds.
834 fn slots(&self) -> Vec<LayoutSlot>;
835
836 /// Check this layout against real messages and report what does not line up.
837 ///
838 /// Hand-authoring a segment definition has a silent failure mode: a layout
839 /// that disagrees with the wire resolves `value_by_code` to the *wrong
840 /// component*, returns a plausible value, and every test still passes. There
841 /// is no way to notice from inside the program — the definition is the only
842 /// thing that says what the positions mean.
843 ///
844 /// Pointing the definition at a corpus is what breaks that circle. Three
845 /// kinds of finding come back, and the third is the one that matters most:
846 ///
847 /// | Finding | Means |
848 /// |---|---|
849 /// | [`UndeclaredElement`][LayoutFinding::UndeclaredElement] / [`UndeclaredComponent`][LayoutFinding::UndeclaredComponent] | The wire carries a value the layout has no slot for — the layout is **wrong**. |
850 /// | [`MandatoryNeverPopulated`][LayoutFinding::MandatoryNeverPopulated] | A slot the layout calls mandatory is empty everywhere — the status or the position is **wrong**. |
851 /// | [`NeverObserved`][LayoutFinding::NeverObserved] | Nothing in the corpus reaches this slot, so **the corpus cannot confirm it**. |
852 ///
853 /// `NeverObserved` is not a defect. It is the honest answer to "does my
854 /// definition match the directory?" when the fixtures are too thin to tell,
855 /// and it names exactly which positions to go and check by hand.
856 ///
857 /// Only segments whose tag matches [`layout_tag`][Self::layout_tag] are
858 /// examined; the rest of the slice is ignored, so a whole interchange can be
859 /// passed in as-is.
860 ///
861 /// # Example
862 ///
863 /// ```
864 /// use edifact_rs::{ComponentRef, ElementRef, SegmentDefinition, SegmentLayout, Status, from_bytes};
865 ///
866 /// // A hand-authored C507 that stops one component short of the directory.
867 /// static C507: &[ComponentRef] = &[
868 /// ComponentRef::new(1, "2005", Status::Mandatory),
869 /// ComponentRef::new(2, "2380", Status::Conditional),
870 /// ];
871 /// static DTM_ELEMENTS: &[ElementRef] =
872 /// &[ElementRef::composite(1, "C507", Status::Mandatory, 1, C507)];
873 /// static DTM: SegmentDefinition =
874 /// SegmentDefinition::new("DTM", "Date/time/period", DTM_ELEMENTS);
875 ///
876 /// let corpus: Vec<_> = from_bytes(b"DTM+137:20260101:102'").collect::<Result<Vec<_>, _>>()?;
877 /// let audit = DTM.audit(&corpus);
878 ///
879 /// // The format qualifier `102` has nowhere to go — the layout is short.
880 /// assert!(audit.has_contradictions());
881 /// assert_eq!(audit.segments_examined(), 1);
882 /// # Ok::<(), edifact_rs::EdifactError>(())
883 /// ```
884 fn audit(&self, segments: &[crate::Segment<'_>]) -> LayoutAudit {
885 audit_layout(self.layout_tag(), &self.slots(), segments)
886 }
887}
888
889/// One declared position in a [`SegmentLayout`], flattened.
890///
891/// Produced by [`SegmentLayout::slots`].
892#[derive(Debug, Clone, PartialEq, Eq)]
893pub struct LayoutSlot {
894 /// Zero-based data element index within the segment.
895 pub element: usize,
896 /// Zero-based component index, or `None` for a simple data element.
897 pub component: Option<usize>,
898 /// The UN/EDIFACT identifier declared at this position.
899 pub data_element: String,
900 /// Whether the layout calls this position mandatory.
901 pub status: Status,
902 /// The status of the **enclosing data element**.
903 ///
904 /// Equal to `status` for a simple data element. For a component it is the
905 /// composite's own status, which is what decides whether a mandatory
906 /// component is actually required: ISO 9735-1 §8.6 makes it mandatory "if
907 /// the composite data element is present", not unconditionally.
908 pub element_status: Status,
909}
910
911impl LayoutSlot {
912 /// Component index treating a simple data element as component 0.
913 #[must_use]
914 pub fn component_index(&self) -> usize {
915 self.component.unwrap_or(0)
916 }
917}
918
919/// One way a layout and a corpus disagree — or fail to inform each other.
920#[derive(Debug, Clone, PartialEq, Eq)]
921#[non_exhaustive]
922pub enum LayoutFinding {
923 /// A segment carried a populated data element beyond the last one declared.
924 UndeclaredElement {
925 /// Zero-based index of the undeclared element.
926 element: usize,
927 /// Byte span of the segment that carried it.
928 span: crate::Span,
929 },
930 /// An element carried a populated component beyond the last one declared.
931 UndeclaredComponent {
932 /// Zero-based element index.
933 element: usize,
934 /// Zero-based index of the undeclared component.
935 component: usize,
936 /// Byte span of the segment that carried it.
937 span: crate::Span,
938 },
939 /// A position the layout calls mandatory was empty in every segment.
940 MandatoryNeverPopulated {
941 /// The declared position.
942 slot: LayoutSlot,
943 },
944 /// No segment in the corpus populated this position.
945 ///
946 /// Evidence of nothing rather than evidence of a fault: the corpus is too
947 /// thin to confirm or refute the slot.
948 NeverObserved {
949 /// The declared position.
950 slot: LayoutSlot,
951 },
952}
953
954/// The result of checking a [`SegmentLayout`] against a corpus.
955///
956/// See [`SegmentLayout::audit`].
957#[derive(Debug, Clone, Default)]
958pub struct LayoutAudit {
959 tag: String,
960 segments_examined: usize,
961 findings: Vec<LayoutFinding>,
962}
963
964impl LayoutAudit {
965 /// The segment tag that was audited.
966 #[must_use]
967 pub fn tag(&self) -> &str {
968 &self.tag
969 }
970
971 /// How many segments in the corpus carried that tag.
972 ///
973 /// Zero means the audit proved nothing at all — worth asserting on.
974 #[must_use]
975 pub fn segments_examined(&self) -> usize {
976 self.segments_examined
977 }
978
979 /// Every finding, in declaration order.
980 #[must_use]
981 pub fn findings(&self) -> &[LayoutFinding] {
982 &self.findings
983 }
984
985 /// Findings that mean the layout is **wrong**, as opposed to unconfirmed.
986 ///
987 /// [`NeverObserved`][LayoutFinding::NeverObserved] is excluded: a corpus
988 /// that never reaches a slot says nothing about whether the slot is right.
989 pub fn contradictions(&self) -> impl Iterator<Item = &LayoutFinding> {
990 self.findings
991 .iter()
992 .filter(|f| !matches!(f, LayoutFinding::NeverObserved { .. }))
993 }
994
995 /// `true` when the corpus contradicts the layout.
996 ///
997 /// This is the assertion to put in a test: it fails on a layout the wire
998 /// disproves, and stays quiet about slots the fixtures simply never exercise.
999 #[must_use]
1000 pub fn has_contradictions(&self) -> bool {
1001 self.contradictions().next().is_some()
1002 }
1003
1004 /// Positions the corpus never reached, in declaration order.
1005 ///
1006 /// Each one is a slot to verify against the directory by hand — or a gap to
1007 /// fill with a fixture.
1008 pub fn unconfirmed(&self) -> impl Iterator<Item = &LayoutSlot> {
1009 self.findings.iter().filter_map(|f| match f {
1010 LayoutFinding::NeverObserved { slot } => Some(slot),
1011 _ => None,
1012 })
1013 }
1014}
1015
1016impl std::fmt::Display for LayoutAudit {
1017 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1018 writeln!(
1019 f,
1020 "{}: {} segment(s) examined, {} contradiction(s), {} unconfirmed slot(s)",
1021 self.tag,
1022 self.segments_examined,
1023 self.contradictions().count(),
1024 self.unconfirmed().count(),
1025 )?;
1026 for finding in &self.findings {
1027 match finding {
1028 LayoutFinding::UndeclaredElement { element, span } => writeln!(
1029 f,
1030 " element {element} is populated at bytes {span} but the layout declares no such element",
1031 )?,
1032 LayoutFinding::UndeclaredComponent {
1033 element,
1034 component,
1035 span,
1036 } => writeln!(
1037 f,
1038 " element {element} component {component} is populated at bytes {span} but the layout declares no such component",
1039 )?,
1040 LayoutFinding::MandatoryNeverPopulated { slot } => writeln!(
1041 f,
1042 " {} is declared mandatory but is empty in every segment",
1043 describe_slot(slot),
1044 )?,
1045 LayoutFinding::NeverObserved { slot } => writeln!(
1046 f,
1047 " {} was never populated — this corpus cannot confirm it",
1048 describe_slot(slot),
1049 )?,
1050 }
1051 }
1052 Ok(())
1053 }
1054}
1055
1056fn describe_slot(slot: &LayoutSlot) -> String {
1057 match slot.component {
1058 Some(component) => format!(
1059 "DE {} (element {}, component {component})",
1060 slot.data_element, slot.element
1061 ),
1062 None => format!("DE {} (element {})", slot.data_element, slot.element),
1063 }
1064}
1065
1066/// Audit every layout a corpus actually exercises, in one call.
1067///
1068/// [`SegmentLayout::audit`] answers for one segment. A hand-authored directory
1069/// has dozens, and the question worth asking is about all of them at once:
1070/// *which of my definitions does this corpus disprove, and which can it not
1071/// speak to?*
1072///
1073/// Only tags present in the corpus are audited — a definition the fixtures never
1074/// exercise would produce nothing but `NeverObserved` noise and drown the
1075/// findings that matter. Ask [`SegmentLayout::audit`] directly for those.
1076///
1077/// Results come back in the order the tags first appear, so the report reads in
1078/// message order.
1079///
1080/// # Example
1081///
1082/// ```
1083/// use edifact_rs::{audit_directory, from_bytes, service};
1084///
1085/// let corpus: Vec<_> = from_bytes(
1086/// b"UNB+UNOC:3+S+R+260101:0900+IC1'UNH+M1+ORDERS:D:96A:UN'UNT+2+M1'UNZ+1+IC1'",
1087/// )
1088/// .collect::<Result<Vec<_>, _>>()?;
1089///
1090/// let audits = audit_directory(service::lookup, &corpus);
1091///
1092/// // One audit per distinct tag the corpus contains.
1093/// assert_eq!(audits.len(), 4);
1094/// // The shipped service tables are not disproved by conformant input.
1095/// assert!(audits.iter().all(|a| !a.has_contradictions()));
1096/// # Ok::<(), edifact_rs::EdifactError>(())
1097/// ```
1098pub fn audit_directory<'a, L, F>(lookup: F, segments: &[crate::Segment<'_>]) -> Vec<LayoutAudit>
1099where
1100 L: SegmentLayout + ?Sized + 'a,
1101 F: Fn(&str) -> Option<&'a L>,
1102{
1103 let mut seen: Vec<&str> = Vec::new();
1104 for segment in segments {
1105 if !seen.contains(&segment.tag()) {
1106 seen.push(segment.tag());
1107 }
1108 }
1109 seen.into_iter()
1110 .filter_map(|tag| lookup(tag).map(|layout| layout.audit(segments)))
1111 .collect()
1112}
1113
1114/// Shared implementation behind [`SegmentLayout::audit`].
1115fn audit_layout(tag: &str, slots: &[LayoutSlot], segments: &[crate::Segment<'_>]) -> LayoutAudit {
1116 let mut audit = LayoutAudit {
1117 tag: tag.to_owned(),
1118 segments_examined: 0,
1119 findings: Vec::new(),
1120 };
1121
1122 // Highest declared index per element, so "beyond the layout" is decidable.
1123 let declared_elements = slots.iter().map(|s| s.element + 1).max().unwrap_or(0);
1124 let mut declared_components: Vec<usize> = vec![0; declared_elements];
1125 for slot in slots {
1126 let width = slot.component_index() + 1;
1127 if width > declared_components[slot.element] {
1128 declared_components[slot.element] = width;
1129 }
1130 }
1131
1132 let mut populated: Vec<Vec<bool>> = declared_components
1133 .iter()
1134 .map(|width| vec![false; *width])
1135 .collect();
1136
1137 for segment in segments.iter().filter(|s| s.tag == tag) {
1138 audit.segments_examined += 1;
1139 for (element_index, element) in segment.elements.iter().enumerate() {
1140 // Every occurrence counts: a repeating element populates the same
1141 // declared positions each time (ISO 9735-1 §8.6).
1142 for occurrence in element.repetitions() {
1143 for (component_index, (value, _)) in occurrence.iter().enumerate() {
1144 // A trailing empty component is how EDIFACT spells "absent"
1145 // (§8.7.2), so only a populated one is evidence of anything.
1146 if value.is_empty() {
1147 continue;
1148 }
1149 if element_index >= declared_elements {
1150 push_once(
1151 &mut audit.findings,
1152 LayoutFinding::UndeclaredElement {
1153 element: element_index,
1154 span: segment.span,
1155 },
1156 );
1157 continue;
1158 }
1159 if component_index >= declared_components[element_index] {
1160 push_once(
1161 &mut audit.findings,
1162 LayoutFinding::UndeclaredComponent {
1163 element: element_index,
1164 component: component_index,
1165 span: segment.span,
1166 },
1167 );
1168 continue;
1169 }
1170 populated[element_index][component_index] = true;
1171 }
1172 }
1173 }
1174 }
1175
1176 // Whether any position of each element was populated — which is exactly
1177 // ISO 9735-1 §8.1's definition of a composite being "present".
1178 let element_populated: Vec<bool> = populated
1179 .iter()
1180 .map(|components| components.iter().any(|seen| *seen))
1181 .collect();
1182
1183 for slot in slots {
1184 if populated[slot.element][slot.component_index()] {
1185 continue;
1186 }
1187 // §8.6: "A mandatory component data element in a composite data element
1188 // shall be present **if the composite data element is present**." A
1189 // conditional composite that the corpus never carries therefore says
1190 // nothing about its mandatory components — reporting them as violations
1191 // would condemn every optional composite in the definition.
1192 let required_here = slot.status == Status::Mandatory
1193 && (slot.component.is_none()
1194 || slot.element_status == Status::Mandatory
1195 || element_populated[slot.element]);
1196 audit.findings.push(if required_here {
1197 LayoutFinding::MandatoryNeverPopulated { slot: slot.clone() }
1198 } else {
1199 LayoutFinding::NeverObserved { slot: slot.clone() }
1200 });
1201 }
1202
1203 audit
1204}
1205
1206/// Record a finding unless an equivalent one is already present.
1207///
1208/// A corpus of 360 fixtures would otherwise report the same undeclared
1209/// component 360 times, burying every other finding.
1210fn push_once(findings: &mut Vec<LayoutFinding>, finding: LayoutFinding) {
1211 let duplicate = findings.iter().any(|existing| match (existing, &finding) {
1212 (
1213 LayoutFinding::UndeclaredElement { element: a, .. },
1214 LayoutFinding::UndeclaredElement { element: b, .. },
1215 ) => a == b,
1216 (
1217 LayoutFinding::UndeclaredComponent {
1218 element: a,
1219 component: c,
1220 ..
1221 },
1222 LayoutFinding::UndeclaredComponent {
1223 element: b,
1224 component: d,
1225 ..
1226 },
1227 ) => a == b && c == d,
1228 _ => false,
1229 });
1230 if !duplicate {
1231 findings.push(finding);
1232 }
1233}
1234
1235impl SegmentDefinition {
1236 /// Create a segment definition.
1237 ///
1238 /// `const` so directory tables can still be built at compile time despite
1239 /// the `#[non_exhaustive]` attribute blocking external struct literals.
1240 #[must_use]
1241 pub const fn new(
1242 tag: &'static str,
1243 name: &'static str,
1244 elements: &'static [ElementRef],
1245 ) -> Self {
1246 Self {
1247 tag,
1248 name,
1249 elements,
1250 }
1251 }
1252
1253 /// Number of positions in this definition that carry `data_element`.
1254 ///
1255 /// `0` means unknown, `1` means unambiguously addressable, and anything
1256 /// larger means the identifier is repeated and cannot be code-addressed.
1257 /// `const`, so a derive macro can assert on it at compile time.
1258 ///
1259 /// # Example
1260 ///
1261 /// ```rust
1262 /// # use edifact_rs::{ElementRef, SegmentDefinition, Status};
1263 /// # static E: &[ElementRef] = &[ElementRef::new(1, "3035", Status::Mandatory, 1)];
1264 /// static NAD: SegmentDefinition = SegmentDefinition::new("NAD", "Name and address", E);
1265 /// const _: () = assert!(NAD.code_positions("3035") == 1);
1266 /// ```
1267 #[must_use]
1268 pub const fn code_positions(&self, data_element: &str) -> usize {
1269 let mut hits = 0;
1270 let mut i = 0;
1271 while i < self.elements.len() {
1272 let el = &self.elements[i];
1273 if const_str_eq(el.data_element, data_element) {
1274 hits += 1;
1275 }
1276 let mut c = 0;
1277 while c < el.components.len() {
1278 if const_str_eq(el.components[c].data_element, data_element) {
1279 hits += 1;
1280 }
1281 c += 1;
1282 }
1283 i += 1;
1284 }
1285 hits
1286 }
1287
1288 /// Zero-based element index for `data_element`, resolved at compile time.
1289 ///
1290 /// # Panics
1291 ///
1292 /// Panics when the identifier is unknown or appears at more than one
1293 /// position. In a `const` context — which is how the derive macro uses it —
1294 /// that panic is a **compile error**, so a mistyped identifier can never
1295 /// reach runtime. Guard with [`code_positions`][Self::code_positions] for a
1296 /// message that names the offending field.
1297 #[must_use]
1298 pub const fn element_slot(&self, data_element: &str) -> usize {
1299 // Two asserts rather than one: a const panic message cannot be
1300 // formatted, so naming the identifier is impossible — but saying which
1301 // of the two problems occurred is not, and it is the part that decides
1302 // what the author has to change.
1303 assert!(
1304 self.code_positions(data_element) != 0,
1305 "this segment definition declares no such data element identifier — check it against the directory"
1306 );
1307 assert!(
1308 self.code_positions(data_element) == 1,
1309 "this data element identifier is declared at more than one position; address it positionally, or declare the repeat with ComponentRef::repeated"
1310 );
1311 let mut i = 0;
1312 while i < self.elements.len() {
1313 let el = &self.elements[i];
1314 if const_str_eq(el.data_element, data_element) {
1315 return el.position as usize - 1;
1316 }
1317 let mut c = 0;
1318 while c < el.components.len() {
1319 if const_str_eq(el.components[c].data_element, data_element) {
1320 return el.position as usize - 1;
1321 }
1322 c += 1;
1323 }
1324 i += 1;
1325 }
1326 unreachable!()
1327 }
1328
1329 /// Zero-based component index for `data_element`, resolved at compile time.
1330 ///
1331 /// Returns `0` when the identifier names a data element rather than a
1332 /// component inside a composite — component 0 is the first (and for a simple
1333 /// element, only) component, so the same accessor works for both shapes.
1334 ///
1335 /// # Panics
1336 ///
1337 /// Panics when the identifier is unknown or appears at more than one
1338 /// position; see [`element_slot`][Self::element_slot].
1339 #[must_use]
1340 pub const fn component_slot(&self, data_element: &str) -> usize {
1341 assert!(
1342 self.code_positions(data_element) != 0,
1343 "this segment definition declares no such data element identifier — check it against the directory"
1344 );
1345 assert!(
1346 self.code_positions(data_element) == 1,
1347 "this data element identifier is declared at more than one position; address it positionally, or declare the repeat with ComponentRef::repeated"
1348 );
1349 let mut i = 0;
1350 while i < self.elements.len() {
1351 let el = &self.elements[i];
1352 if const_str_eq(el.data_element, data_element) {
1353 return 0;
1354 }
1355 let mut c = 0;
1356 while c < el.components.len() {
1357 if const_str_eq(el.components[c].data_element, data_element) {
1358 return el.components[c].position as usize - 1;
1359 }
1360 c += 1;
1361 }
1362 i += 1;
1363 }
1364 unreachable!()
1365 }
1366
1367 /// `true` when `data_element` names a component *inside* a composite rather
1368 /// than a data element of the segment.
1369 ///
1370 /// Lets a caller — the derive macro, in practice — pick the right
1371 /// "missing required" error variant without a second lookup:
1372 /// [`EdifactError::MissingRequiredComponent`] rather than
1373 /// [`EdifactError::MissingRequiredElement`]. `component_slot` alone cannot
1374 /// answer this, because a code naming the *first* component of a composite
1375 /// also resolves to component index 0.
1376 ///
1377 /// Returns `false` for an unknown identifier; pair with
1378 /// [`code_positions`][Self::code_positions] when that case matters.
1379 #[must_use]
1380 pub const fn code_is_component(&self, data_element: &str) -> bool {
1381 let mut i = 0;
1382 while i < self.elements.len() {
1383 let el = &self.elements[i];
1384 let mut c = 0;
1385 while c < el.components.len() {
1386 if const_str_eq(el.components[c].data_element, data_element) {
1387 return true;
1388 }
1389 c += 1;
1390 }
1391 i += 1;
1392 }
1393 false
1394 }
1395}
1396
1397impl SegmentLayout for SegmentDefinition {
1398 #[inline]
1399 fn layout_tag(&self) -> &str {
1400 self.tag
1401 }
1402
1403 fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError> {
1404 // One pass, not four: this runs per lookup on hot validation paths, and
1405 // composing the `const` helpers would rescan the table for each of the
1406 // count, the element index, and the component index.
1407 let mut hits = 0usize;
1408 let mut found = None;
1409 for el in self.elements {
1410 if el.data_element == data_element {
1411 hits += 1;
1412 found.get_or_insert(ElementPath::element(el.position as usize - 1));
1413 }
1414 for comp in el.components {
1415 if comp.data_element == data_element {
1416 hits += 1;
1417 found.get_or_insert(ElementPath::component(
1418 el.position as usize - 1,
1419 comp.position as usize - 1,
1420 ));
1421 }
1422 }
1423 }
1424 resolve_outcome(self.tag, data_element, hits, found)
1425 }
1426
1427 fn slots(&self) -> Vec<LayoutSlot> {
1428 let mut out = Vec::new();
1429 for element in self.elements {
1430 if element.components.is_empty() {
1431 out.push(LayoutSlot {
1432 element: element.position as usize - 1,
1433 component: None,
1434 data_element: element.data_element.to_owned(),
1435 status: element.status,
1436 element_status: element.status,
1437 });
1438 continue;
1439 }
1440 for component in element.components {
1441 out.push(LayoutSlot {
1442 element: element.position as usize - 1,
1443 component: Some(component.position as usize - 1),
1444 data_element: component.data_element.to_owned(),
1445 status: component.status,
1446 element_status: element.status,
1447 });
1448 }
1449 }
1450 out
1451 }
1452}
1453
1454/// Turn a resolution scan's `(hit count, first match)` into a `Result`.
1455///
1456/// Shared by both [`SegmentLayout`] impls so the static and runtime tables
1457/// cannot drift on which condition maps to which error.
1458fn resolve_outcome(
1459 tag: &str,
1460 data_element: &str,
1461 hits: usize,
1462 found: Option<ElementPath>,
1463) -> Result<ElementPath, EdifactError> {
1464 match (hits, found) {
1465 (1, Some(path)) => Ok(path),
1466 (0, _) => Err(EdifactError::UnknownDataElement {
1467 tag: tag.to_owned(),
1468 data_element: data_element.to_owned(),
1469 }),
1470 _ => Err(EdifactError::AmbiguousDataElement {
1471 tag: tag.to_owned(),
1472 data_element: data_element.to_owned(),
1473 }),
1474 }
1475}
1476
1477/// Owned runtime equivalent of [`ElementRef`].
1478///
1479/// Used by [`DirectoryValidatorBuilder`] and [`DirectoryValidator::from_owned_definitions`]
1480/// to construct validators from data that is not available at compile time (e.g. loaded
1481/// from JSON or a database at startup).
1482///
1483/// Use [`OwnedElementRef::new_unchecked`] for compile-time-known positions (panics on invalid
1484/// input, no error handling noise) or [`OwnedElementRef::try_new`] when the position
1485/// comes from an external source and you need a `Result`. Fields are private to prevent
1486/// bypassing the position invariant through struct-literal syntax.
1487#[derive(Debug, Clone)]
1488pub struct OwnedElementRef {
1489 /// One-based element position.
1490 position: u8,
1491 /// UN/EDIFACT data element identifier.
1492 data_element: String,
1493 /// Requirement status.
1494 status: Status,
1495 /// Maximum repetition count.
1496 max_repeat: u8,
1497 /// The directory's representation for a *simple* element.
1498 repr: Option<Repr>,
1499 /// The representation from syntax version 4 onward, where it differs.
1500 repr_from_v4: Option<Repr>,
1501 /// Component definitions when this element is a composite; empty for a
1502 /// simple data element.
1503 components: Vec<OwnedComponentRef>,
1504}
1505
1506/// Owned runtime equivalent of [`ComponentRef`].
1507///
1508/// Attach these to an [`OwnedElementRef`] with
1509/// [`OwnedElementRef::with_components`] so that runtime-loaded definitions
1510/// support code-addressed access into composites, exactly like compile-time
1511/// [`SegmentDefinition`] tables do.
1512#[derive(Debug, Clone)]
1513pub struct OwnedComponentRef {
1514 /// One-based position of the first slot this component occupies.
1515 position: u8,
1516 /// UN/EDIFACT component data element identifier.
1517 data_element: String,
1518 /// Requirement status.
1519 status: Status,
1520 /// The directory's representation, when the definition states one.
1521 repr: Option<Repr>,
1522 /// The representation from syntax version 4 onward, where it differs.
1523 repr_from_v4: Option<Repr>,
1524 /// How many consecutive slots this component occupies.
1525 repeat_count: u8,
1526}
1527
1528impl OwnedComponentRef {
1529 /// Construct an owned component reference.
1530 ///
1531 /// # Panics
1532 ///
1533 /// Panics if `position` is `0` (positions are one-based).
1534 pub fn new_unchecked(position: u8, data_element: String, status: Status) -> Self {
1535 assert!(
1536 position != 0,
1537 "OwnedComponentRef::new_unchecked: position must be >= 1 (one-based), got 0"
1538 );
1539 Self {
1540 position,
1541 data_element,
1542 status,
1543 repeat_count: 1,
1544 repr: None,
1545 repr_from_v4: None,
1546 }
1547 }
1548
1549 /// Runtime counterpart of [`ComponentRef::repeated`].
1550 ///
1551 /// # Panics
1552 ///
1553 /// Panics if `position == 0` or `repeat_count == 0`.
1554 #[must_use]
1555 pub fn repeated(position: u8, data_element: String, status: Status, repeat_count: u8) -> Self {
1556 assert!(
1557 position != 0,
1558 "OwnedComponentRef::repeated: position must be >= 1 (one-based), got 0"
1559 );
1560 assert!(
1561 repeat_count != 0,
1562 "OwnedComponentRef::repeated: repeat_count must be >= 1"
1563 );
1564 Self {
1565 position,
1566 data_element,
1567 status,
1568 repeat_count,
1569 repr: None,
1570 repr_from_v4: None,
1571 }
1572 }
1573
1574 /// How many consecutive slots this component occupies.
1575 #[inline]
1576 #[must_use]
1577 pub fn repeat_count(&self) -> u8 {
1578 self.repeat_count
1579 }
1580
1581 /// Construct an owned component reference, returning an error for position `0`.
1582 ///
1583 /// # Errors
1584 ///
1585 /// Returns [`EdifactError::InvalidElementPosition`] if `position` is `0`.
1586 pub fn try_new(
1587 position: u8,
1588 data_element: String,
1589 status: Status,
1590 ) -> Result<Self, EdifactError> {
1591 if position == 0 {
1592 return Err(EdifactError::InvalidElementPosition);
1593 }
1594 Ok(Self {
1595 position,
1596 data_element,
1597 status,
1598 repeat_count: 1,
1599 repr: None,
1600 repr_from_v4: None,
1601 })
1602 }
1603
1604 /// One-based component position (always >= 1).
1605 #[inline]
1606 pub fn position(&self) -> u8 {
1607 self.position
1608 }
1609
1610 /// UN/EDIFACT component data element identifier.
1611 #[inline]
1612 pub fn data_element(&self) -> &str {
1613 &self.data_element
1614 }
1615
1616 /// Requirement status of this component.
1617 #[inline]
1618 pub fn status(&self) -> Status {
1619 self.status
1620 }
1621
1622 /// Attach the directory's representation, e.g. `an..35`.
1623 #[must_use]
1624 pub fn with_repr(mut self, repr: Repr) -> Self {
1625 self.repr = Some(repr);
1626 self
1627 }
1628
1629 /// The declared representation, if the definition states one.
1630 #[inline]
1631 #[must_use]
1632 pub fn repr(&self) -> Option<Repr> {
1633 self.repr
1634 }
1635
1636 /// Declare a representation that changed between syntax versions.
1637 ///
1638 /// See [`ComponentRef::with_repr_by_syntax_version`].
1639 #[must_use]
1640 pub fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
1641 self.repr = Some(up_to_v3);
1642 self.repr_from_v4 = Some(from_v4);
1643 self
1644 }
1645
1646 /// The representation used from syntax version 4 onward, when it differs.
1647 #[inline]
1648 #[must_use]
1649 pub fn repr_from_v4(&self) -> Option<Repr> {
1650 self.repr_from_v4
1651 }
1652}
1653
1654/// Owned runtime equivalent of [`SegmentDefinition`].
1655///
1656/// Used by [`DirectoryValidatorBuilder`] and [`DirectoryValidator::from_owned_definitions`].
1657///
1658/// Use [`OwnedSegmentDef::new_unchecked`] for compile-time-known tags (panics on invalid input,
1659/// no error handling noise) or [`OwnedSegmentDef::try_new`] when the tag comes from
1660/// an external source and you need a `Result`. Fields are private to prevent bypassing
1661/// the tag invariant through struct-literal syntax.
1662#[derive(Debug, Clone)]
1663pub struct OwnedSegmentDef {
1664 /// Segment tag (e.g. `"BGM"`).
1665 tag: String,
1666 /// Human-readable segment name.
1667 name: String,
1668 /// Ordered element definitions.
1669 elements: Vec<OwnedElementRef>,
1670}
1671
1672impl OwnedSegmentDef {
1673 /// Construct an owned segment definition.
1674 ///
1675 /// This is the ergonomic constructor for compile-time-known tags (e.g.
1676 /// `"BGM"`, `"UNH"`). It panics immediately on invalid input so that
1677 /// call sites with literal tag strings require no `.unwrap()` / `.expect()`
1678 /// boilerplate.
1679 ///
1680 /// Use [`try_new`][Self::try_new] instead when the tag originates from an
1681 /// external source (user input, config file, database) and you need a
1682 /// `Result` to propagate errors gracefully.
1683 ///
1684 /// # Panics
1685 ///
1686 /// Panics if `tag` is not exactly three ASCII uppercase letters.
1687 pub fn new_unchecked(tag: String, name: String, elements: Vec<OwnedElementRef>) -> Self {
1688 assert!(
1689 tag.len() == 3 && tag.bytes().all(|b| b.is_ascii_uppercase()),
1690 "OwnedSegmentDef::new_unchecked: tag must be exactly three ASCII uppercase letters, got {tag:?}"
1691 );
1692 Self {
1693 tag,
1694 name,
1695 elements,
1696 }
1697 }
1698
1699 /// Construct an owned segment definition, returning an error for invalid tags.
1700 ///
1701 /// Prefer this over [`new_unchecked`][Self::new_unchecked] when the tag comes from an external
1702 /// source (user input, config file, database) and you want to handle the
1703 /// error without panicking.
1704 ///
1705 /// # Errors
1706 ///
1707 /// Returns [`EdifactError::InvalidSegmentTag`] if `tag` is not exactly three
1708 /// ASCII uppercase letters.
1709 pub fn try_new(
1710 tag: String,
1711 name: String,
1712 elements: Vec<OwnedElementRef>,
1713 ) -> Result<Self, EdifactError> {
1714 if tag.len() != 3 || !tag.bytes().all(|b| b.is_ascii_uppercase()) {
1715 return Err(EdifactError::InvalidSegmentTag(tag));
1716 }
1717 Ok(Self {
1718 tag,
1719 name,
1720 elements,
1721 })
1722 }
1723
1724 /// Segment tag (e.g. `"BGM"`).
1725 #[inline]
1726 pub fn tag(&self) -> &str {
1727 &self.tag
1728 }
1729
1730 /// Human-readable segment name.
1731 #[inline]
1732 pub fn name(&self) -> &str {
1733 &self.name
1734 }
1735
1736 /// Element definitions for this segment.
1737 #[inline]
1738 pub fn elements(&self) -> &[OwnedElementRef] {
1739 &self.elements
1740 }
1741
1742 /// Number of positions in this definition that carry `data_element`.
1743 ///
1744 /// Runtime counterpart of [`SegmentDefinition::code_positions`].
1745 #[must_use]
1746 pub fn code_positions(&self, data_element: &str) -> usize {
1747 self.elements
1748 .iter()
1749 .map(|el| {
1750 usize::from(el.data_element == data_element)
1751 + el.components
1752 .iter()
1753 .filter(|c| c.data_element == data_element)
1754 .count()
1755 })
1756 .sum()
1757 }
1758}
1759
1760impl SegmentLayout for OwnedSegmentDef {
1761 #[inline]
1762 fn layout_tag(&self) -> &str {
1763 &self.tag
1764 }
1765
1766 fn resolve_code(&self, data_element: &str) -> Result<ElementPath, EdifactError> {
1767 let mut hits = 0usize;
1768 let mut found = None;
1769 for el in &self.elements {
1770 if el.data_element == data_element {
1771 hits += 1;
1772 found.get_or_insert(ElementPath::element(el.position as usize - 1));
1773 }
1774 for comp in &el.components {
1775 if comp.data_element == data_element {
1776 hits += 1;
1777 found.get_or_insert(ElementPath::component(
1778 el.position as usize - 1,
1779 comp.position as usize - 1,
1780 ));
1781 }
1782 }
1783 }
1784 resolve_outcome(&self.tag, data_element, hits, found)
1785 }
1786
1787 fn slots(&self) -> Vec<LayoutSlot> {
1788 let mut out = Vec::new();
1789 for element in &self.elements {
1790 if element.components.is_empty() {
1791 out.push(LayoutSlot {
1792 element: element.position as usize - 1,
1793 component: None,
1794 data_element: element.data_element.clone(),
1795 status: element.status,
1796 element_status: element.status,
1797 });
1798 continue;
1799 }
1800 for component in &element.components {
1801 out.push(LayoutSlot {
1802 element: element.position as usize - 1,
1803 component: Some(component.position as usize - 1),
1804 data_element: component.data_element.clone(),
1805 status: component.status,
1806 element_status: element.status,
1807 });
1808 }
1809 }
1810 out
1811 }
1812}
1813
1814impl OwnedElementRef {
1815 /// Construct an owned element reference.
1816 ///
1817 /// This is the ergonomic constructor for compile-time-known positions.
1818 /// It panics immediately on invalid input so that call sites with literal
1819 /// position numbers require no `.unwrap()` / `.expect()` boilerplate.
1820 ///
1821 /// Use [`try_new`][Self::try_new] instead when the position originates from
1822 /// an external source (user input, config file, database) and you need a
1823 /// `Result` to propagate errors gracefully.
1824 ///
1825 /// # Panics
1826 ///
1827 /// Panics if `position` is `0` (positions are one-based).
1828 pub fn new_unchecked(
1829 position: u8,
1830 data_element: String,
1831 status: Status,
1832 max_repeat: u8,
1833 ) -> Self {
1834 assert!(
1835 position != 0,
1836 "OwnedElementRef::new_unchecked: position must be >= 1 (one-based), got 0"
1837 );
1838 Self {
1839 position,
1840 data_element,
1841 status,
1842 max_repeat,
1843 repr: None,
1844 repr_from_v4: None,
1845 components: Vec::new(),
1846 }
1847 }
1848
1849 /// Construct an owned element reference, returning an error for position `0`.
1850 ///
1851 /// Prefer this over [`new_unchecked`][Self::new_unchecked] when the position comes from an
1852 /// external source (user input, config file, database) and you want to
1853 /// handle the error without panicking.
1854 ///
1855 /// # Errors
1856 ///
1857 /// Returns [`EdifactError::InvalidElementPosition`] if `position` is `0`.
1858 pub fn try_new(
1859 position: u8,
1860 data_element: String,
1861 status: Status,
1862 max_repeat: u8,
1863 ) -> Result<Self, EdifactError> {
1864 if position == 0 {
1865 return Err(EdifactError::InvalidElementPosition);
1866 }
1867 Ok(Self {
1868 position,
1869 data_element,
1870 status,
1871 max_repeat,
1872 repr: None,
1873 repr_from_v4: None,
1874 components: Vec::new(),
1875 })
1876 }
1877
1878 /// Attach component definitions, marking this element as a composite.
1879 ///
1880 /// Declared components make code-addressed access resolve *into* the
1881 /// composite and activate the mandatory-component check in
1882 /// [`DirectoryValidator`].
1883 ///
1884 /// # Example
1885 ///
1886 /// ```rust
1887 /// use edifact_rs::{OwnedComponentRef, OwnedElementRef, Status};
1888 ///
1889 /// let dtm = OwnedElementRef::new_unchecked(1, "C507".to_owned(), Status::Mandatory, 1)
1890 /// .with_components(vec![
1891 /// OwnedComponentRef::new_unchecked(1, "2005".to_owned(), Status::Mandatory),
1892 /// OwnedComponentRef::new_unchecked(2, "2380".to_owned(), Status::Conditional),
1893 /// ]);
1894 /// assert_eq!(dtm.components().len(), 2);
1895 /// ```
1896 #[must_use]
1897 pub fn with_components(mut self, components: Vec<OwnedComponentRef>) -> Self {
1898 self.components = components;
1899 self
1900 }
1901
1902 /// Component definitions; empty when this is a simple data element.
1903 #[inline]
1904 pub fn components(&self) -> &[OwnedComponentRef] {
1905 &self.components
1906 }
1907
1908 /// One-based element position (always >= 1).
1909 #[inline]
1910 pub fn position(&self) -> u8 {
1911 self.position
1912 }
1913
1914 /// UN/EDIFACT data element identifier.
1915 #[inline]
1916 pub fn data_element(&self) -> &str {
1917 &self.data_element
1918 }
1919
1920 /// Requirement status of this element.
1921 #[inline]
1922 pub fn status(&self) -> Status {
1923 self.status
1924 }
1925
1926 /// Maximum repetition count for this element.
1927 #[inline]
1928 pub fn max_repeat(&self) -> u8 {
1929 self.max_repeat
1930 }
1931
1932 /// Attach the directory's representation for a simple data element.
1933 #[must_use]
1934 pub fn with_repr(mut self, repr: Repr) -> Self {
1935 self.repr = Some(repr);
1936 self
1937 }
1938
1939 /// The declared representation, if the definition states one.
1940 #[inline]
1941 #[must_use]
1942 pub fn repr(&self) -> Option<Repr> {
1943 self.repr
1944 }
1945
1946 /// Declare a representation that changed between syntax versions.
1947 ///
1948 /// See [`ComponentRef::with_repr_by_syntax_version`].
1949 #[must_use]
1950 pub fn with_repr_by_syntax_version(mut self, up_to_v3: Repr, from_v4: Repr) -> Self {
1951 self.repr = Some(up_to_v3);
1952 self.repr_from_v4 = Some(from_v4);
1953 self
1954 }
1955
1956 /// The representation used from syntax version 4 onward, when it differs.
1957 #[inline]
1958 #[must_use]
1959 pub fn repr_from_v4(&self) -> Option<Repr> {
1960 self.repr_from_v4
1961 }
1962}
1963
1964type SegmentLookupFn = Arc<dyn Fn(&str) -> Option<&'static SegmentDefinition> + Send + Sync>;
1965type IsCodeValidFn = Arc<dyn Fn(&str, &str) -> bool + Send + Sync>;
1966type SuggestCodeFn = Arc<dyn Fn(&str, &str) -> Option<&'static str> + Send + Sync>;
1967type ExpectedComponentsFn = Arc<dyn Fn(&str, usize) -> Option<u8> + Send + Sync>;
1968type AdditionalStructureRuleRefFn = fn(&Segment<'_>) -> Result<(), EdifactError>;
1969type AdditionalStructureRuleFn =
1970 Arc<dyn Fn(&Segment<'_>) -> Result<(), EdifactError> + Send + Sync>;
1971/// Returns the `(element_index, component_index, data_element_id)` tuples to
1972/// validate against a code list for the given segment tag.
1973type CodeListRulesFn = Arc<dyn Fn(&str) -> &'static [(usize, usize, &'static str)] + Send + Sync>;
1974/// Returns the mandatory segment tags for a given EDIFACT message type.
1975///
1976/// The slice should contain every tag that must appear at least once in a
1977/// conformant message of the given type. The tags are also used to check
1978/// canonical ordering — their relative order in the returned slice is taken
1979/// as the expected order in the message.
1980type RequiredSegmentsFn = Arc<dyn Fn(&str) -> &'static [&'static str] + Send + Sync>;
1981
1982/// Internal enum that unifies lookup results from static and owned segment definitions.
1983///
1984/// Allows `validate_segment` to handle both code-generated (`&'static`) and
1985/// runtime-constructed ([`OwnedSegmentDef`]) definitions without duplication.
1986enum SegmentDefRef<'a> {
1987 Static(&'static SegmentDefinition),
1988 Owned(&'a OwnedSegmentDef),
1989}
1990
1991impl SegmentDefRef<'_> {
1992 /// Returns the highest defined element position (one-based → used directly as
1993 /// the maximum zero-based slot count for element-count validation).
1994 ///
1995 /// For owned definitions the highest `position` value may exceed the number
1996 /// of entries in the `elements` vec when positions are non-consecutive.
1997 fn max_element_position(&self) -> usize {
1998 match self {
1999 Self::Static(d) => d
2000 .elements
2001 .iter()
2002 .map(|e| e.position as usize)
2003 .max()
2004 .unwrap_or(0),
2005 Self::Owned(d) => d
2006 .elements
2007 .iter()
2008 .map(|e| e.position as usize)
2009 .max()
2010 .unwrap_or(0),
2011 }
2012 }
2013
2014 /// Returns the highest position number among mandatory elements (one-based).
2015 ///
2016 /// This equals the minimum number of elements that must be present in a
2017 /// segment: if the highest-positioned mandatory element is at position 5,
2018 /// the segment must supply at least 5 elements.
2019 fn last_mandatory_position(&self) -> usize {
2020 match self {
2021 Self::Static(d) => d
2022 .elements
2023 .iter()
2024 .filter(|e| e.status == Status::Mandatory)
2025 .map(|e| e.position as usize)
2026 .max()
2027 .unwrap_or(0),
2028 Self::Owned(d) => d
2029 .elements
2030 .iter()
2031 .filter(|e| e.status == Status::Mandatory)
2032 .map(|e| e.position as usize)
2033 .max()
2034 .unwrap_or(0),
2035 }
2036 }
2037
2038 /// Iterate over mandatory element positions without heap allocation.
2039 ///
2040 /// Calls `f(zero_based_index, data_element_id)` for each element whose
2041 /// status is [`Status::Mandatory`]. Returns `Err` immediately if `f`
2042 /// returns `Err`, short-circuiting the remaining elements.
2043 fn for_each_mandatory_position<E, F>(&self, mut f: F) -> Result<(), E>
2044 where
2045 F: FnMut(usize, &str) -> Result<(), E>,
2046 {
2047 match self {
2048 Self::Static(d) => {
2049 for e in d.elements.iter().filter(|e| e.status == Status::Mandatory) {
2050 f((e.position as usize).saturating_sub(1), e.data_element)?;
2051 }
2052 }
2053 Self::Owned(d) => {
2054 for e in d.elements.iter().filter(|e| e.status == Status::Mandatory) {
2055 f(
2056 (e.position as usize).saturating_sub(1),
2057 e.data_element.as_str(),
2058 )?;
2059 }
2060 }
2061 }
2062 Ok(())
2063 }
2064
2065 /// Iterate over mandatory *component* positions without heap allocation.
2066 ///
2067 /// Calls `f(element_index, component_index, data_element_id)` — both indices
2068 /// zero-based — for every declared component whose status is
2069 /// [`Status::Mandatory`]. Definitions that declare no components (the shape
2070 /// every pre-0.13 directory table had) yield nothing, so this check is
2071 /// inert until a directory opts in by declaring composites with
2072 /// [`ElementRef::composite`].
2073 fn for_each_mandatory_component<E, F>(&self, mut f: F) -> Result<(), E>
2074 where
2075 F: FnMut(usize, usize, &str) -> Result<(), E>,
2076 {
2077 match self {
2078 Self::Static(d) => {
2079 for e in d.elements {
2080 for c in e
2081 .components
2082 .iter()
2083 .filter(|c| c.status == Status::Mandatory)
2084 {
2085 f(
2086 (e.position as usize).saturating_sub(1),
2087 (c.position as usize).saturating_sub(1),
2088 c.data_element,
2089 )?;
2090 }
2091 }
2092 }
2093 Self::Owned(d) => {
2094 for e in &d.elements {
2095 for c in e
2096 .components
2097 .iter()
2098 .filter(|c| c.status == Status::Mandatory)
2099 {
2100 f(
2101 (e.position as usize).saturating_sub(1),
2102 (c.position as usize).saturating_sub(1),
2103 c.data_element.as_str(),
2104 )?;
2105 }
2106 }
2107 }
2108 }
2109 Ok(())
2110 }
2111
2112 /// Number of declared component **slots** for the element at zero-based `index`.
2113 ///
2114 /// A component declared with [`ComponentRef::repeated`] occupies several
2115 /// slots, so this sums repeat counts rather than counting entries: counting
2116 /// entries would cap `C080` at two components and reject the four extra
2117 /// `3036` occurrences the composite is defined to carry.
2118 ///
2119 /// `None` when the element is not defined, or is defined without
2120 /// components — in which case its arity is not constrained by the layout.
2121 /// The declared maximum occurrence count for the element at `index`.
2122 fn max_repeat_at(&self, index: usize) -> Option<u8> {
2123 let position = u8::try_from(index.checked_add(1)?).ok()?;
2124 match self {
2125 Self::Static(d) => d
2126 .elements
2127 .iter()
2128 .find(|e| e.position == position)
2129 .map(ElementRef::max_repeat),
2130 Self::Owned(d) => d
2131 .elements
2132 .iter()
2133 .find(|e| e.position == position)
2134 .map(OwnedElementRef::max_repeat),
2135 }
2136 }
2137
2138 /// The representation required at `(element, component)`, if any.
2139 ///
2140 /// A composite states its representations on the components; a simple data
2141 /// element states one on the element itself and only at component 0.
2142 ///
2143 /// `syntax_version` selects between the two forms of a position that changed
2144 /// between versions; `None` accepts either, because guessing would reject
2145 /// conformant data.
2146 fn repr_at(
2147 &self,
2148 element: usize,
2149 component: usize,
2150 syntax_version: Option<u8>,
2151 ) -> Option<ReprRequirement> {
2152 let position = u8::try_from(element.checked_add(1)?).ok()?;
2153 let component_position = u8::try_from(component.checked_add(1)?).ok()?;
2154 match self {
2155 Self::Static(d) => {
2156 let element = d.elements.iter().find(|e| e.position == position)?;
2157 if element.components.is_empty() {
2158 return if component == 0 {
2159 select_repr(element.repr(), element.repr_from_v4(), syntax_version)
2160 } else {
2161 None
2162 };
2163 }
2164 let component_ref = element.components.iter().find(|c| {
2165 // A component declared with `repeated` spans several
2166 // consecutive slots, all of the same representation.
2167 let first = c.position();
2168 let last = first.saturating_add(c.repeat_count().saturating_sub(1));
2169 (first..=last).contains(&component_position)
2170 })?;
2171 select_repr(
2172 component_ref.repr(),
2173 component_ref.repr_from_v4(),
2174 syntax_version,
2175 )
2176 }
2177 Self::Owned(d) => {
2178 let element = d.elements.iter().find(|e| e.position == position)?;
2179 if element.components.is_empty() {
2180 return if component == 0 {
2181 select_repr(
2182 OwnedElementRef::repr(element),
2183 OwnedElementRef::repr_from_v4(element),
2184 syntax_version,
2185 )
2186 } else {
2187 None
2188 };
2189 }
2190 let component_ref = element.components.iter().find(|c| {
2191 let first = c.position();
2192 let last = first.saturating_add(c.repeat_count().saturating_sub(1));
2193 (first..=last).contains(&component_position)
2194 })?;
2195 select_repr(
2196 component_ref.repr(),
2197 component_ref.repr_from_v4(),
2198 syntax_version,
2199 )
2200 }
2201 }
2202 }
2203
2204 fn declared_component_count(&self, index: usize) -> Option<u8> {
2205 let position = u8::try_from(index.checked_add(1)?).ok()?;
2206 let count: u32 = match self {
2207 Self::Static(d) => d
2208 .elements
2209 .iter()
2210 .find(|e| e.position == position)
2211 .map(|e| e.components.iter().map(|c| u32::from(c.repeat_count)).sum())?,
2212 Self::Owned(d) => d
2213 .elements
2214 .iter()
2215 .find(|e| e.position == position)
2216 .map(|e| e.components.iter().map(|c| u32::from(c.repeat_count)).sum())?,
2217 };
2218 if count == 0 {
2219 return None;
2220 }
2221 u8::try_from(count).ok()
2222 }
2223}
2224
2225/// Read the syntax version number from `UNB` S001 DE 0002.
2226///
2227/// `None` when the slice carries no readable `UNB` — a message window, say.
2228fn detect_syntax_version(segments: &[Segment<'_>]) -> Option<u8> {
2229 segments
2230 .iter()
2231 .find(|s| s.tag == "UNB")
2232 .and_then(|unb| unb.component_str(0, 1))
2233 .and_then(|version| version.parse().ok())
2234}
2235
2236/// Pick the representation that applies to `syntax_version`.
2237///
2238/// Version 4 onward uses `from_v4` when the definition states one. An unknown
2239/// version accepts either, because a definition that distinguishes them is
2240/// distinguishing a real incompatibility — guessing would reject conformant data
2241/// from whichever version we guessed against.
2242fn select_repr(
2243 base: Option<Repr>,
2244 from_v4: Option<Repr>,
2245 syntax_version: Option<u8>,
2246) -> Option<ReprRequirement> {
2247 match (base, from_v4) {
2248 (_, None) => base.map(ReprRequirement::single),
2249 (None, Some(v4)) => Some(ReprRequirement::single(v4)),
2250 (Some(base), Some(v4)) => Some(match syntax_version {
2251 Some(version) if version >= 4 => ReprRequirement::single(v4),
2252 Some(_) => ReprRequirement::single(base),
2253 None => ReprRequirement {
2254 primary: base,
2255 alternative: Some(v4),
2256 },
2257 }),
2258 }
2259}
2260
2261/// Default required-segments mapping used when no custom function is provided.
2262///
2263/// Returns the universal minimum: every EDIFACT message must begin with `UNH`
2264/// and end with `UNT`. Message-type-specific mandatory segments (such as
2265/// `BGM` for ORDERS/INVOIC) must be enforced by a
2266/// [`ProfileRulePack`][crate::ProfileRulePack] or a custom
2267/// [`DirectoryValidatorBuilder::with_required_segments`] function to avoid
2268/// false positives for message types that do not require `BGM`.
2269fn default_required_segments(_message_type: &str) -> &'static [&'static str] {
2270 &["UNH", "UNT"]
2271}
2272
2273/// Code-list validation rules common to all UN/EDIFACT directory releases.
2274///
2275/// Each entry is `(element_index, component_index, data_element_id)`.
2276/// `element_index` and `component_index` are zero-based.
2277///
2278/// Covers the most frequently validated qualifier/code elements across ORDERS,
2279/// INVOIC, and similar message types.
2280pub(crate) fn base_code_list_rules(tag: &str) -> &'static [(usize, usize, &'static str)] {
2281 match tag {
2282 "BGM" => &[(0, 0, "1001")],
2283 "DTM" => &[(0, 0, "2005")],
2284 "NAD" => &[(0, 0, "3035")],
2285 "QTY" => &[(0, 0, "6063")],
2286 "RFF" => &[(0, 0, "1153")],
2287 "MOA" => &[(0, 0, "5025")],
2288 "PRI" => &[(0, 0, "5125")],
2289 "LOC" => &[(0, 0, "3227")],
2290 _ => &[],
2291 }
2292}
2293
2294/// Shared validator implementation that is configured per UN/EDIFACT directory release.
2295///
2296/// # Scope and limitations
2297///
2298/// `DirectoryValidator` validates individual segment *content* (element counts,
2299/// component counts, code-list values, and conditional rules) and checks that
2300/// every *mandatory* segment type is present at least once. It does **not**
2301/// validate segment *sequence* or *repetition cardinality* — i.e., it cannot
2302/// tell you that a `BGM` segment appears more than once, or that a `RFF` group
2303/// appears in the wrong position. Full sequence validation requires a
2304/// state-machine per message type (UN/EDIFACT Segment Tables) which is outside
2305/// the scope of this implementation.
2306#[derive(Clone)]
2307pub struct DirectoryValidator {
2308 directory_id: String,
2309 segment_lookup: SegmentLookupFn,
2310 /// Runtime-owned segment definitions (from builder / JSON / DB).
2311 ///
2312 /// When `Some`, takes precedence over `segment_lookup` for tag resolution.
2313 owned_defs: Option<Arc<Vec<OwnedSegmentDef>>>,
2314 /// Tag -> index into `owned_defs`. Without this, `resolve_def` was a linear
2315 /// scan per segment, making validation O(n_segments x n_definitions).
2316 owned_index: Option<Arc<std::collections::HashMap<String, usize>>>,
2317 is_code_valid: IsCodeValidFn,
2318 suggest_code: SuggestCodeFn,
2319 expected_components: ExpectedComponentsFn,
2320 code_list_rules: CodeListRulesFn,
2321 additional_structure_rule: Option<AdditionalStructureRuleFn>,
2322 /// Configurable mapping from message type to required segment tags.
2323 required_segments: RequiredSegmentsFn,
2324 message_type: Option<String>,
2325 enforce_known_tags: bool,
2326 structure_checks: bool,
2327 code_list_checks: bool,
2328}
2329
2330impl std::fmt::Debug for DirectoryValidator {
2331 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2332 f.debug_struct("DirectoryValidator")
2333 .field("directory_id", &self.directory_id)
2334 .field("message_type", &self.message_type)
2335 .field("enforce_known_tags", &self.enforce_known_tags)
2336 .field("structure_checks", &self.structure_checks)
2337 .field("code_list_checks", &self.code_list_checks)
2338 .finish_non_exhaustive()
2339 }
2340}
2341
2342impl DirectoryValidator {
2343 /// Create a validator for a specific directory release with injected lookup/check hooks.
2344 pub fn new(
2345 directory_id: &'static str,
2346 segment_lookup: fn(&str) -> Option<&'static SegmentDefinition>,
2347 is_code_valid: fn(&str, &str) -> bool,
2348 suggest_code: fn(&str, &str) -> Option<&'static str>,
2349 expected_components: fn(&str, usize) -> Option<u8>,
2350 additional_structure_rule: Option<AdditionalStructureRuleRefFn>,
2351 ) -> Self {
2352 Self {
2353 directory_id: directory_id.to_owned(),
2354 segment_lookup: Arc::new(segment_lookup),
2355 owned_defs: None,
2356 owned_index: None,
2357 is_code_valid: Arc::new(is_code_valid),
2358 suggest_code: Arc::new(suggest_code),
2359 expected_components: Arc::new(expected_components),
2360 code_list_rules: Arc::new(base_code_list_rules),
2361 additional_structure_rule: additional_structure_rule
2362 .map(|f| Arc::new(f) as AdditionalStructureRuleFn),
2363 required_segments: Arc::new(default_required_segments),
2364 message_type: None,
2365 enforce_known_tags: true,
2366 structure_checks: true,
2367 code_list_checks: true,
2368 }
2369 }
2370
2371 /// Create a validator from a static slice of [`SegmentDefinition`]s.
2372 ///
2373 /// This is the preferred constructor when code-generating directory data as
2374 /// a `static` array: no manual fn-pointer boilerplate is required.
2375 ///
2376 /// Code-list checks are **disabled** by default (the built-in `is_code_valid`
2377 /// always returns `true`). Call [`with_code_list_rules`][Self::with_code_list_rules]
2378 /// to register directory-specific rules that actually validate code values.
2379 ///
2380 /// # Example
2381 ///
2382 /// ```rust,ignore
2383 /// static MY_SEGMENTS: &[SegmentDefinition] = &[ /* … */ ];
2384 ///
2385 /// let validator = DirectoryValidator::from_definitions(MY_SEGMENTS)
2386 /// .with_code_list_rules(my_code_list_rules);
2387 /// ```
2388 pub fn from_definitions(definitions: &'static [SegmentDefinition]) -> Self {
2389 let lookup_map: std::collections::HashMap<&'static str, &'static SegmentDefinition> =
2390 definitions.iter().map(|d| (d.tag, d)).collect();
2391 let lookup_map = Arc::new(lookup_map);
2392 Self {
2393 directory_id: "custom".to_owned(),
2394 segment_lookup: Arc::new(move |tag: &str| lookup_map.get(tag).copied()),
2395 owned_defs: None,
2396 owned_index: None,
2397 is_code_valid: Arc::new(|_de: &str, _code: &str| true),
2398 suggest_code: Arc::new(|_de: &str, _code: &str| None),
2399 expected_components: Arc::new(|_tag: &str, _idx: usize| None),
2400 code_list_rules: Arc::new(base_code_list_rules),
2401 additional_structure_rule: None,
2402 required_segments: Arc::new(default_required_segments),
2403 message_type: None,
2404 enforce_known_tags: true,
2405 structure_checks: true,
2406 code_list_checks: false,
2407 }
2408 }
2409
2410 /// Create a validator from a runtime-owned collection of segment definitions.
2411 ///
2412 /// Use this (or [`DirectoryValidatorBuilder`]) when segment definitions are
2413 /// loaded from an external source at startup (JSON, database, YAML, …) rather
2414 /// than being known at compile time.
2415 ///
2416 /// Code-list checks are **disabled** by default; enable them by chaining
2417 /// [`with_code_list_rules`][Self::with_code_list_rules] and setting
2418 /// `is_code_valid` via a custom [`new`][Self::new] call or by subclassing
2419 /// the builder.
2420 ///
2421 /// # Example
2422 ///
2423 /// ```rust,ignore
2424 /// let defs = vec![
2425 /// OwnedSegmentDef::new_unchecked(
2426 /// "BGM".to_owned(),
2427 /// "Beginning of message".to_owned(),
2428 /// vec![OwnedElementRef::new_unchecked(1, "C002".to_owned(), Status::Mandatory, 1)],
2429 /// ),
2430 /// ];
2431 /// let validator = DirectoryValidator::from_owned_definitions(defs)
2432 /// .with_directory_id("runtime-profile");
2433 /// ```
2434 pub fn from_owned_definitions(definitions: Vec<OwnedSegmentDef>) -> Self {
2435 Self {
2436 directory_id: "custom".to_owned(),
2437 // The static lookup is never consulted when `owned_defs` is `Some`.
2438 segment_lookup: Arc::new(|_| None),
2439 owned_index: Some(Arc::new(
2440 definitions
2441 .iter()
2442 .enumerate()
2443 .map(|(i, d)| (d.tag.clone(), i))
2444 .collect(),
2445 )),
2446 owned_defs: Some(Arc::new(definitions)),
2447 is_code_valid: Arc::new(|_de: &str, _code: &str| true),
2448 suggest_code: Arc::new(|_de: &str, _code: &str| None),
2449 expected_components: Arc::new(|_tag: &str, _idx: usize| None),
2450 code_list_rules: Arc::new(base_code_list_rules),
2451 additional_structure_rule: None,
2452 required_segments: Arc::new(default_required_segments),
2453 message_type: None,
2454 enforce_known_tags: true,
2455 structure_checks: true,
2456 code_list_checks: false,
2457 }
2458 }
2459
2460 /// Set the directory identifier string (used in error messages).
2461 pub fn with_directory_id(mut self, id: impl Into<String>) -> Self {
2462 self.directory_id = id.into();
2463 self
2464 }
2465
2466 /// Override the code-list rules function.
2467 ///
2468 /// Directories can supply a directory-specific implementation that extends or
2469 /// replaces the base rules from `base_code_list_rules`.
2470 pub fn with_code_list_rules(
2471 mut self,
2472 f: impl Fn(&str) -> &'static [(usize, usize, &'static str)] + Send + Sync + 'static,
2473 ) -> Self {
2474 self.code_list_rules = Arc::new(f);
2475 self
2476 }
2477
2478 /// Enable only structure checks and disable code-list checks.
2479 pub fn structure_only(mut self) -> Self {
2480 self.structure_checks = true;
2481 self.code_list_checks = false;
2482 self
2483 }
2484
2485 /// Enable only code-list checks and disable structure checks.
2486 pub fn code_list_only(mut self) -> Self {
2487 self.structure_checks = false;
2488 self.code_list_checks = true;
2489 self
2490 }
2491
2492 /// Configure whether unknown segment tags should be rejected.
2493 pub fn enforce_known_tags(mut self, enforce: bool) -> Self {
2494 self.enforce_known_tags = enforce;
2495 self
2496 }
2497
2498 /// Override the required-segments mapping used for structural validation.
2499 ///
2500 /// The supplied function receives an EDIFACT message type string (e.g. `"ORDERS"`)
2501 /// and must return a `'static` slice of segment tags that are mandatory for that
2502 /// type. The tags are checked both for *presence* and for *canonical ordering*
2503 /// within the message.
2504 ///
2505 /// # Example
2506 ///
2507 /// ```rust,ignore
2508 /// fn my_required_segments(msg_type: &str) -> &'static [&'static str] {
2509 /// match msg_type {
2510 /// "DESADV" => &["UNH", "BGM", "SHP", "UNT"],
2511 /// "INVOIC" => &["UNH", "BGM", "MOA", "UNT"],
2512 /// _ => &["UNH", "UNT"],
2513 /// }
2514 /// }
2515 ///
2516 /// let validator = DirectoryValidator::from_definitions(DEFS)
2517 /// .with_required_segments(my_required_segments);
2518 /// ```
2519 pub fn with_required_segments(
2520 mut self,
2521 f: impl Fn(&str) -> &'static [&'static str] + Send + Sync + 'static,
2522 ) -> Self {
2523 self.required_segments = Arc::new(f);
2524 self
2525 }
2526
2527 fn detect_message_type(&self, segments: &[Segment<'_>]) -> Option<String> {
2528 if let Some(explicit) = self.message_type.as_deref() {
2529 return Some(explicit.to_owned());
2530 }
2531
2532 segments
2533 .iter()
2534 .find(|s| s.tag == "UNH")
2535 .and_then(|s| s.get_element(1))
2536 .and_then(|e| e.get_component(0))
2537 .map(str::to_owned)
2538 }
2539
2540 /// Count the non-trailing-empty components in element `element_idx` of `seg`.
2541 ///
2542 /// Per ISO 9735-1 §8.7.2 ("Trailing empty component data elements may be omitted"),
2543 /// a sender is not required to transmit trailing empty components; this function
2544 /// therefore strips them before checking against the expected count so that
2545 /// conformant messages with omitted trailing components are still accepted.
2546 ///
2547 /// # Examples
2548 ///
2549 /// - `DTM+137:20200101:` has three declared components but only 2 non-empty → effective=2
2550 /// - `NAD+MS++::293` has a composite with 3 components, last two empty → effective=1
2551 fn effective_component_count(seg: &Segment<'_>, element_idx: usize) -> Option<u8> {
2552 let elem = seg.elements.get(element_idx)?;
2553 let mut count = elem.components.len();
2554 while count > 0 && elem.components[count - 1].0.as_ref().is_empty() {
2555 count -= 1;
2556 }
2557 u8::try_from(count).ok()
2558 }
2559
2560 fn collect_component_count_issues(
2561 &self,
2562 seg: &Segment<'_>,
2563 def: &SegmentDefRef<'_>,
2564 out: &mut Vec<EdifactError>,
2565 ) {
2566 for idx in 0..seg.elements.len() {
2567 let actual = Self::effective_component_count(seg, idx).unwrap_or(0);
2568 // The `expected_components` hook is an exact count and wins when set.
2569 if let Some(expected) = (self.expected_components)(seg.tag(), idx) {
2570 if actual != expected {
2571 out.push(EdifactError::InvalidComponentCount {
2572 tag: seg.tag().to_owned(),
2573 element_index: idx,
2574 expected,
2575 actual,
2576 span: seg.element_span(idx).unwrap_or(seg.span),
2577 });
2578 }
2579 continue;
2580 }
2581 // Otherwise a composite that declares its components caps them:
2582 // more components than the directory defines is a structural error,
2583 // while fewer is normal (conditional components may be omitted).
2584 if let Some(declared) = def.declared_component_count(idx) {
2585 if actual > declared {
2586 out.push(EdifactError::InvalidComponentCount {
2587 tag: seg.tag().to_owned(),
2588 element_index: idx,
2589 expected: declared,
2590 actual,
2591 span: seg.element_span(idx).unwrap_or(seg.span),
2592 });
2593 }
2594 }
2595 }
2596 }
2597
2598 /// Enforce each element's declared maximum number of occurrences.
2599 ///
2600 /// `max_repeat` had been carried on every `ElementRef` since the type
2601 /// existed, exposed by a getter, and read by nothing — so a definition that
2602 /// said "this element occurs once" constrained nothing at all, and a caller
2603 /// who wrote it believed otherwise.
2604 fn collect_repetition_issues(
2605 &self,
2606 seg: &Segment<'_>,
2607 def: &SegmentDefRef<'_>,
2608 out: &mut Vec<EdifactError>,
2609 ) {
2610 for (index, element) in seg.elements.iter().enumerate() {
2611 let Some(max) = def.max_repeat_at(index) else {
2612 continue;
2613 };
2614 // A declared maximum of zero would forbid the element outright,
2615 // which is what `Status` is for; treat it as "unconstrained".
2616 if max == 0 {
2617 continue;
2618 }
2619 let actual = element.repeat_count();
2620 if actual > usize::from(max) {
2621 out.push(EdifactError::TooManyRepetitions {
2622 tag: seg.tag().to_owned(),
2623 element_index: index,
2624 max,
2625 actual,
2626 span: element.span,
2627 });
2628 }
2629 }
2630 }
2631
2632 /// Check every populated value against its declared representation.
2633 ///
2634 /// Only positions the definition actually states a representation for are
2635 /// checked, so a partial table stays useful rather than becoming a source of
2636 /// false findings.
2637 fn collect_representation_issues(
2638 &self,
2639 seg: &Segment<'_>,
2640 def: &SegmentDefRef<'_>,
2641 syntax_version: Option<u8>,
2642 out: &mut Vec<EdifactError>,
2643 ) {
2644 for (element_index, element) in seg.elements.iter().enumerate() {
2645 for occurrence in element.repetitions() {
2646 for (component_index, (value, span)) in occurrence.iter().enumerate() {
2647 // An empty value is an absent one (§8.1); its presence is
2648 // the mandatory check's business, not the representation's.
2649 if value.is_empty() {
2650 continue;
2651 }
2652 let Some(repr) = def.repr_at(element_index, component_index, syntax_version)
2653 else {
2654 continue;
2655 };
2656 if !repr.permits_characters(value) {
2657 out.push(EdifactError::InvalidCharacterType {
2658 tag: seg.tag().to_owned(),
2659 element_index,
2660 component_index,
2661 repr: repr.to_string(),
2662 value: value.to_string(),
2663 span: *span,
2664 });
2665 // The length of a value that is not of the declared
2666 // class is not meaningful — §10's numeric count in
2667 // particular assumes a numeric value.
2668 continue;
2669 }
2670 self.collect_insignificant_characters(
2671 seg,
2672 element_index,
2673 component_index,
2674 value,
2675 *span,
2676 repr.primary,
2677 out,
2678 );
2679 if repr.permits_length(value) {
2680 continue;
2681 }
2682 let actual = repr.measure(value);
2683 out.push(if repr.is_too_short(value) {
2684 EdifactError::DataElementTooShort {
2685 tag: seg.tag().to_owned(),
2686 element_index,
2687 component_index,
2688 repr: repr.to_string(),
2689 actual,
2690 span: *span,
2691 }
2692 } else {
2693 EdifactError::DataElementTooLong {
2694 tag: seg.tag().to_owned(),
2695 element_index,
2696 component_index,
2697 repr: repr.to_string(),
2698 actual,
2699 span: *span,
2700 }
2701 });
2702 }
2703 }
2704 }
2705 }
2706
2707 /// Report characters ISO 9735-1 §9.1 requires the sender to suppress.
2708 ///
2709 /// Only **variable length** elements are covered, which is what §9.1 says:
2710 /// a fixed-length numeric field is padded with leading zeroes by design, and
2711 /// a fixed-length text one with trailing spaces.
2712 #[allow(clippy::too_many_arguments)]
2713 fn collect_insignificant_characters(
2714 &self,
2715 seg: &Segment<'_>,
2716 element_index: usize,
2717 component_index: usize,
2718 value: &str,
2719 span: crate::Span,
2720 repr: Repr,
2721 out: &mut Vec<EdifactError>,
2722 ) {
2723 if repr.is_fixed() {
2724 return;
2725 }
2726 let kind = match repr.kind() {
2727 ReprKind::Numeric => {
2728 let digits = value.strip_prefix('-').unwrap_or(value);
2729 // "Nevertheless, a single zero before a decimal mark is
2730 // allowed", so `0.5` is correct and only `00…` is not.
2731 let leading_zeroes = digits.starts_with('0')
2732 && digits.len() > 1
2733 && !digits.starts_with("0.")
2734 && !digits.starts_with("0,");
2735 if !leading_zeroes {
2736 return;
2737 }
2738 Insignificant::LeadingZeroes
2739 }
2740 ReprKind::Alphabetic | ReprKind::Alphanumeric => {
2741 if !value.ends_with(' ') {
2742 return;
2743 }
2744 Insignificant::TrailingSpaces
2745 }
2746 };
2747 out.push(EdifactError::InsignificantCharacters {
2748 tag: seg.tag().to_owned(),
2749 element_index,
2750 component_index,
2751 kind,
2752 span,
2753 });
2754 }
2755
2756 fn collect_code_list_issues(&self, seg: &Segment<'_>, out: &mut Vec<EdifactError>) {
2757 for (elem_idx, comp_idx, de) in (self.code_list_rules)(seg.tag()) {
2758 let value = seg
2759 .get_element(*elem_idx)
2760 .and_then(|e| e.get_component(*comp_idx))
2761 .unwrap_or("");
2762 if !value.is_empty() && !(self.is_code_valid)(de, value) {
2763 let suggestion = (self.suggest_code)(de, value);
2764 // Point at the offending *value*, not the whole segment, so
2765 // rendered diagnostics underline the code that failed.
2766 let span = seg
2767 .get_element(*elem_idx)
2768 .and_then(|e| e.component_span(*comp_idx))
2769 .unwrap_or(seg.span);
2770 out.push(EdifactError::InvalidCodeValue {
2771 tag: seg.tag().to_owned(),
2772 element_index: *elem_idx,
2773 value: value.to_owned(),
2774 code_list: (*de).to_owned(),
2775 span,
2776 suggestion,
2777 });
2778 }
2779 }
2780 }
2781}
2782
2783impl DirectoryValidator {
2784 fn resolve_def<'a>(&'a self, tag: &str) -> Option<SegmentDefRef<'a>> {
2785 if let Some(owned) = &self.owned_defs {
2786 let index = self.owned_index.as_ref()?;
2787 owned.get(*index.get(tag)?).map(SegmentDefRef::Owned)
2788 } else {
2789 (self.segment_lookup)(tag).map(SegmentDefRef::Static)
2790 }
2791 }
2792
2793 /// Check one segment, appending **every** violation found to `out`.
2794 ///
2795 /// Reporting continues past the first fault: a segment missing two mandatory
2796 /// elements and carrying an invalid code is three findings, and a validator
2797 /// whose whole purpose is an exhaustive report has no business hiding two of
2798 /// them. Only the checks that cannot proceed without a resolved definition
2799 /// short-circuit.
2800 fn collect_segment_issues(
2801 &self,
2802 seg: &Segment<'_>,
2803 syntax_version: Option<u8>,
2804 out: &mut Vec<EdifactError>,
2805 ) {
2806 if !self.structure_checks && !self.code_list_checks {
2807 return;
2808 }
2809
2810 let Some(def) = self.resolve_def(seg.tag()) else {
2811 if self.structure_checks && self.enforce_known_tags {
2812 out.push(EdifactError::InvalidSegmentForMessage {
2813 tag: seg.tag().to_owned(),
2814 message_type: self
2815 .message_type
2816 .clone()
2817 .unwrap_or_else(|| self.directory_id.clone()),
2818 span: seg.tag_span,
2819 });
2820 }
2821 // Without a definition there is nothing further to check against.
2822 return;
2823 };
2824
2825 if self.structure_checks {
2826 let max_elements = def.max_element_position();
2827 let min_elements = def.last_mandatory_position();
2828 let actual = seg.elements.len();
2829 if actual < min_elements || actual > max_elements {
2830 out.push(EdifactError::InvalidElementCount {
2831 tag: seg.tag().to_owned(),
2832 min: min_elements,
2833 max: max_elements,
2834 actual,
2835 span: seg.span,
2836 });
2837 }
2838
2839 def.for_each_mandatory_position::<std::convert::Infallible, _>(|idx, _de| {
2840 let is_present = seg.elements.get(idx).is_some_and(|elem| {
2841 elem.components.iter().any(|(c, _)| !c.as_ref().is_empty())
2842 });
2843 if !is_present {
2844 out.push(EdifactError::MissingRequiredElement {
2845 tag: seg.tag().to_owned(),
2846 element_index: idx,
2847 });
2848 }
2849 Ok(())
2850 })
2851 .unwrap_or_else(|never| match never {});
2852
2853 // Mandatory *components* inside declared composites. Only fires for
2854 // definitions built with `ElementRef::composite` / `with_components`;
2855 // an element that is absent entirely is already reported above as a
2856 // missing element, so only present elements are checked here.
2857 def.for_each_mandatory_component::<std::convert::Infallible, _>(
2858 |elem_idx, comp_idx, _de| {
2859 let Some(elem) = seg.elements.get(elem_idx) else {
2860 return Ok(());
2861 };
2862 let present = elem
2863 .get_component(comp_idx)
2864 .is_some_and(|value| !value.is_empty());
2865 if !present {
2866 out.push(EdifactError::MissingRequiredComponent {
2867 tag: seg.tag().to_owned(),
2868 element_index: elem_idx,
2869 component_index: comp_idx,
2870 });
2871 }
2872 Ok(())
2873 },
2874 )
2875 .unwrap_or_else(|never| match never {});
2876
2877 self.collect_component_count_issues(seg, &def, out);
2878 self.collect_repetition_issues(seg, &def, out);
2879 self.collect_representation_issues(seg, &def, syntax_version, out);
2880
2881 if let Some(rule) = &self.additional_structure_rule {
2882 if let Err(error) = rule(seg) {
2883 out.push(error);
2884 }
2885 }
2886 }
2887
2888 if self.code_list_checks {
2889 self.collect_code_list_issues(seg, out);
2890 }
2891 }
2892}
2893
2894impl Validator for DirectoryValidator {
2895 fn set_message_type(&mut self, message_type: Option<&str>) {
2896 self.message_type = message_type.map(str::to_owned);
2897 }
2898
2899 fn validate_batch(
2900 &self,
2901 segments: &[Segment<'_>],
2902 report: &mut ValidationReport,
2903 _context: &ValidationRuleContext<'_>,
2904 ) {
2905 // The syntax version decides which form of a version-dependent
2906 // representation applies; `UNB` S001 DE 0002 is where it is stated.
2907 let syntax_version = detect_syntax_version(segments);
2908 let mut issues = Vec::new();
2909 for seg in segments {
2910 self.collect_segment_issues(seg, syntax_version, &mut issues);
2911 for err in issues.drain(..) {
2912 report_error(report, err);
2913 }
2914 }
2915
2916 if self.structure_checks {
2917 if let Some(message_type) = self.detect_message_type(segments) {
2918 // One pass recording each tag's first index answers both the
2919 // presence and the ordering question. The previous shape ran two
2920 // full scans *per required tag* and invoked `required_segments`
2921 // twice, which is O(|required| x n) on every batch.
2922 let mut first_index: std::collections::HashMap<&str, usize> =
2923 std::collections::HashMap::with_capacity(segments.len());
2924 for (i, seg) in segments.iter().enumerate() {
2925 first_index.entry(seg.tag()).or_insert(i);
2926 }
2927
2928 let required = (self.required_segments)(&message_type);
2929 for required_tag in required {
2930 if !first_index.contains_key(*required_tag) {
2931 report.add_error(
2932 ValidationIssue::new(
2933 ValidationSeverity::Error,
2934 format!(
2935 "required segment {} missing for message type {}",
2936 required_tag, message_type
2937 ),
2938 )
2939 .with_segment(*required_tag)
2940 .with_suggestion("Add the mandatory segment at the correct position"),
2941 );
2942 }
2943 }
2944
2945 let mut last_idx = None;
2946 for tag in required {
2947 if let Some(&idx) = first_index.get(*tag) {
2948 if let Some(prev) = last_idx {
2949 if idx < prev {
2950 report.add_error(
2951 ValidationIssue::new(
2952 ValidationSeverity::Error,
2953 format!(
2954 "segment sequence violation for message type {}: '{}' appears out of order",
2955 message_type, tag
2956 ),
2957 )
2958 .with_segment(*tag)
2959 .with_suggestion(
2960 "Ensure required segments follow UN/EDIFACT canonical order",
2961 ),
2962 );
2963 }
2964 }
2965 last_idx = Some(idx);
2966 }
2967 }
2968 }
2969 }
2970 }
2971}
2972
2973// ── DirectoryValidatorBuilder ─────────────────────────────────────────────────
2974
2975/// Builder for [`DirectoryValidator`] using runtime-owned segment definitions.
2976///
2977/// Use this when segment definitions are loaded from an external source at
2978/// startup (JSON, database, YAML, …) rather than being available as `static`
2979/// arrays at compile time.
2980///
2981/// # Example
2982///
2983/// ```rust,ignore
2984/// let validator = DirectoryValidatorBuilder::new("my-profile")
2985/// .add_segment(
2986/// OwnedSegmentDef::new_unchecked(
2987/// "BGM".to_owned(),
2988/// "Beginning of message".to_owned(),
2989/// vec![OwnedElementRef::new_unchecked(1, "C002".to_owned(), Status::Mandatory, 1)],
2990/// ),
2991/// )
2992/// .build();
2993/// ```
2994#[derive(Debug, Default)]
2995pub struct DirectoryValidatorBuilder {
2996 directory_id: Option<String>,
2997 segments: Vec<OwnedSegmentDef>,
2998}
2999
3000impl DirectoryValidatorBuilder {
3001 /// Create a new builder with the given directory identifier.
3002 ///
3003 /// The identifier is used in error messages; set a human-readable value
3004 /// such as `"ORDERS-MIG-5.5"` or `"custom-profile"`.
3005 pub fn new(directory_id: impl Into<String>) -> Self {
3006 Self {
3007 directory_id: Some(directory_id.into()),
3008 segments: Vec::new(),
3009 }
3010 }
3011
3012 /// Add a segment definition to the builder.
3013 ///
3014 /// Definitions can be added in any order; the resulting validator looks
3015 /// them up by tag at validation time.
3016 pub fn add_segment(mut self, def: OwnedSegmentDef) -> Self {
3017 self.segments.push(def);
3018 self
3019 }
3020
3021 /// Extend the builder with multiple segment definitions at once.
3022 pub fn add_segments(mut self, defs: impl IntoIterator<Item = OwnedSegmentDef>) -> Self {
3023 self.segments.extend(defs);
3024 self
3025 }
3026
3027 /// Build the [`DirectoryValidator`].
3028 ///
3029 /// Returns a validator backed by the accumulated [`OwnedSegmentDef`]s.
3030 /// Code-list checks are disabled by default; chain
3031 /// [`DirectoryValidator::with_code_list_rules`] on the returned value to
3032 /// enable them.
3033 pub fn build(self) -> DirectoryValidator {
3034 let mut validator = DirectoryValidator::from_owned_definitions(self.segments);
3035 if let Some(id) = self.directory_id {
3036 validator.directory_id = id;
3037 }
3038 validator
3039 }
3040}
3041
3042#[cfg(test)]
3043mod tests {
3044 use super::*;
3045
3046 static TEST_ELEMENTS: &[ElementRef] = &[ElementRef::new(1, "C507", Status::Mandatory, 1)];
3047
3048 static TEST_SEGMENT: SegmentDefinition =
3049 SegmentDefinition::new("TST", "Test segment", TEST_ELEMENTS);
3050
3051 fn segment_lookup(tag: &str) -> Option<&'static SegmentDefinition> {
3052 match tag {
3053 "TST" => Some(&TEST_SEGMENT),
3054 _ => None,
3055 }
3056 }
3057
3058 fn code_valid(_de: &str, _code: &str) -> bool {
3059 true
3060 }
3061
3062 fn suggest_code(_de: &str, _code: &str) -> Option<&'static str> {
3063 None
3064 }
3065
3066 fn expected_components(_tag: &str, _idx: usize) -> Option<u8> {
3067 None
3068 }
3069
3070 #[test]
3071 fn mandatory_composite_present_when_any_component_non_empty() {
3072 let input = b"TST+:ABC'";
3073 let segments: Vec<_> = crate::from_bytes(input)
3074 .collect::<Result<Vec<_>, _>>()
3075 .expect("parse should succeed");
3076
3077 let validator = DirectoryValidator::new(
3078 "TEST",
3079 segment_lookup,
3080 code_valid,
3081 suggest_code,
3082 expected_components,
3083 None,
3084 );
3085
3086 let mut report = ValidationReport::default();
3087 validator.validate_batch(
3088 &segments,
3089 &mut report,
3090 &crate::validator::ValidationRuleContext::empty(),
3091 );
3092 assert!(!report.has_errors());
3093 }
3094
3095 // ── effective_component_count (ISO 9735-1 §8.7.2 trailing-empty-component trim) ──
3096
3097 fn parse_single(input: &[u8]) -> crate::OwnedSegment {
3098 crate::from_reader(std::io::Cursor::new(input))
3099 .collect::<Result<Vec<_>, _>>()
3100 .expect("parse should succeed")
3101 .into_iter()
3102 .next()
3103 .expect("at least one segment")
3104 }
3105
3106 #[test]
3107 fn trailing_empty_component_stripped_from_dtm() {
3108 // DTM+137:20200101: has three components in element 0; the third is empty.
3109 // ISO 9735-1 §8.7.2 says trailing empty components may be omitted,
3110 // so effective count should be 2.
3111 let owned = parse_single(b"DTM+137:20200101:'");
3112 let seg = &owned;
3113 let count = DirectoryValidator::effective_component_count(seg, 0);
3114 assert_eq!(
3115 count,
3116 Some(2),
3117 "trailing empty component should be stripped"
3118 );
3119 }
3120
3121 #[test]
3122 fn all_empty_components_result_in_zero() {
3123 // NAD+MS++: → element 2 is ":" with two empty components → effective=0
3124 let owned = parse_single(b"NAD+MS++:'");
3125 let seg = &owned;
3126 let count = DirectoryValidator::effective_component_count(seg, 2);
3127 assert_eq!(
3128 count,
3129 Some(0),
3130 "all-empty composite should have effective count 0"
3131 );
3132 }
3133
3134 #[test]
3135 fn non_empty_component_not_stripped() {
3136 // DTM+137:20200101:102 — all three components are non-empty
3137 let owned = parse_single(b"DTM+137:20200101:102'");
3138 let seg = &owned;
3139 let count = DirectoryValidator::effective_component_count(seg, 0);
3140 assert_eq!(
3141 count,
3142 Some(3),
3143 "no components should be stripped when all non-empty"
3144 );
3145 }
3146
3147 #[test]
3148 fn with_code_list_rules_overrides_base() {
3149 // Override code-list rules to require element 0 of TST to be a specific code.
3150 fn custom_rules(tag: &str) -> &'static [(usize, usize, &'static str)] {
3151 match tag {
3152 "TST" => &[(0, 0, "CUSTOM_DE")],
3153 _ => &[],
3154 }
3155 }
3156 fn custom_code_valid(_de: &str, code: &str) -> bool {
3157 code == "VALID"
3158 }
3159 fn no_suggestion(_de: &str, _code: &str) -> Option<&'static str> {
3160 None
3161 }
3162
3163 let input = b"TST+INVALID'";
3164 let segments: Vec<_> = crate::from_bytes(input)
3165 .collect::<Result<Vec<_>, _>>()
3166 .expect("parse should succeed");
3167
3168 let validator = DirectoryValidator::new(
3169 "TEST",
3170 segment_lookup,
3171 custom_code_valid,
3172 no_suggestion,
3173 expected_components,
3174 None,
3175 )
3176 .with_code_list_rules(custom_rules);
3177
3178 let mut report = ValidationReport::default();
3179 validator.validate_batch(
3180 &segments,
3181 &mut report,
3182 &crate::validator::ValidationRuleContext::empty(),
3183 );
3184 assert!(
3185 report.has_warnings(),
3186 "INVALID is not in the custom code list so validation must warn"
3187 );
3188 }
3189}