edifact_rs/validator/context.rs
1//! Validation context: `ValidationContext`, `ValidationContextBuilder`, `LayeredValidator`.
2
3use super::pack::ProfileRulePack;
4use super::{EnvelopeValidator, ValidationLayer, ValidationRuleContext, Validator};
5use crate::{OwnedSegment, Segment, ValidationReport, ValidationSeverity};
6use std::any::Any;
7use std::sync::Arc;
8
9pub(super) struct LayeredValidator {
10 pub(super) layer: ValidationLayer,
11 pub(super) validator: Box<dyn Validator + Send + Sync>,
12}
13
14/// Runtime validation context for progressive layered validation.
15///
16/// # Architecture
17///
18/// `edifact-rs` validation is organized into **four independent layers**, each
19/// responsible for a distinct class of checks. All layers run against the same
20/// segment slice; their issues are collected into a single [`ValidationReport`].
21///
22/// | Layer | [`ValidationLayer`] variant | Default | Type |
23/// |---|---|---|---|
24/// | **Envelope** | `Envelope` | disabled | [`EnvelopeValidator`] |
25/// | **Structure** | `Structure` | enabled | external (e.g. `DirectoryValidator`) |
26/// | **Code-list** | `CodeList` | enabled | external |
27/// | **Profile** | `Profile` | enabled | [`ProfileRulePack`] / `Arc<ProfileRulePack>` |
28///
29/// Validators are run in registration order within each enabled layer. Layers
30/// themselves have no enforced ordering beyond the order in which they are added
31/// via the builder.
32///
33/// ## Envelope layer
34///
35/// Checks `UNB`/`UNH`/`UNT`/`UNZ` structural invariants: presence, message
36/// count, and segment count. Enabled by calling
37/// [`ValidationContextBuilder::with_envelope_validation`]. When enabled, the
38/// envelope segments (`UNB`, `UNZ`, `UNG`, `UNE`) are *excluded* from the slice
39/// passed to validators in subsequent layers.
40///
41/// ## Structure layer
42///
43/// Validates segment presence, order, and arity against an EDIFACT directory.
44/// Implemented by `DirectoryValidator` (registered as a `Structure`-layer
45/// validator via [`ValidationContextBuilder::with_validator`]).
46///
47/// ## Code-list layer
48///
49/// Validates DE values against EDIFACT code lists from the directory. Also
50/// implemented by `DirectoryValidator`.
51///
52/// ## Profile layer
53///
54/// Applies downstream business rules (BDEW AHB / MIG rules, custom constraints)
55/// via [`ProfileRulePack`]. A pack can be scoped to specific EDIFACT message
56/// types (`for_message_type`) and association-assigned codes (`for_release`).
57///
58/// ## Group-aware validation
59///
60/// Validators that implement [`Validator::validate_group_batch`] can additionally
61/// enforce rules scoped to specific segment groups (e.g. "DTM must appear in every
62/// SG5 occurrence"). Call [`validate_lenient_grouped`] with a pre-built
63/// [`SegmentGroupIndexed`] tree to activate both the flat and group passes.
64///
65/// [`SegmentGroupIndexed`]: crate::SegmentGroupIndexed
66/// [`validate_lenient_grouped`]: ValidationContext::validate_lenient_grouped
67///
68/// # Example — building a context
69///
70/// ```rust,ignore
71/// use std::sync::{Arc, LazyLock};
72/// use edifact_rs::{ProfileRulePack, ValidationContext, ValidationLayer};
73///
74/// static ORDERS_PACK: LazyLock<Arc<ProfileRulePack>> = LazyLock::new(|| {
75/// Arc::new(
76/// ProfileRulePack::new("ORDERS-MIG-5.5")
77/// .for_message_type("ORDERS")
78/// .require_segment("BGM", "MIG-BGM-M")
79/// .require_segment_in_group("SG2", "NAD", "SG2-NAD-M"),
80/// )
81/// });
82///
83/// let ctx = ValidationContext::builder()
84/// .with_envelope_validation()
85/// .with_profile_pack_arc(Arc::clone(&*ORDERS_PACK))
86/// .build();
87///
88/// let report = ctx.validate_lenient(&segments);
89/// ```
90pub struct ValidationContext {
91 pub(super) validators: Vec<LayeredValidator>,
92 pub(super) envelope_enabled: bool,
93 pub(super) structure_enabled: bool,
94 pub(super) code_list_enabled: bool,
95 pub(super) profile_enabled: bool,
96 /// Stop evaluating all remaining validators as soon as a `Critical`-severity
97 /// issue appears in the report.
98 pub(super) bail_on_first_critical: bool,
99 pub(super) message_type: Option<String>,
100 /// Injected into every emitted `ValidationIssue` when set.
101 pub(super) message_ref: Option<String>,
102 pub(super) metadata: Option<Arc<dyn Any + Send + Sync>>,
103 /// Advisory issues unconditionally appended to every report produced by
104 /// this context — regardless of what segments are validated.
105 ///
106 /// Use [`ValidationContextBuilder::with_static_issue`] to populate.
107 pub(super) static_issues: Vec<crate::ValidationIssue>,
108}
109
110/// Builder for [`ValidationContext`].
111#[must_use = "call `.build()` to produce a `ValidationContext`"]
112pub struct ValidationContextBuilder {
113 pub(super) inner: ValidationContext,
114}
115
116impl Default for ValidationContextBuilder {
117 fn default() -> Self {
118 Self::new()
119 }
120}
121
122impl ValidationContextBuilder {
123 /// Create a new context builder.
124 ///
125 /// Structure, code-list, and profile layers are enabled by default.
126 /// The envelope layer is **disabled** by default.
127 pub fn new() -> Self {
128 Self {
129 inner: ValidationContext {
130 validators: Vec::new(),
131 envelope_enabled: false,
132 structure_enabled: true,
133 code_list_enabled: true,
134 profile_enabled: true,
135 bail_on_first_critical: false,
136 message_type: None,
137 message_ref: None,
138 metadata: None,
139 static_issues: Vec::new(),
140 },
141 }
142 }
143
144 /// Attach typed metadata accessible to context-aware profile rules.
145 pub fn with_metadata<T: Any + Send + Sync + 'static>(mut self, value: T) -> Self {
146 self.inner.metadata = Some(Arc::new(value));
147 self
148 }
149
150 /// Stamp every issue produced by this context with the given message reference.
151 pub fn with_message_ref(mut self, message_ref: impl Into<String>) -> Self {
152 self.inner.message_ref = Some(message_ref.into());
153 self
154 }
155
156 /// Set message type metadata for downstream validators.
157 pub fn with_message_type(mut self, message_type: impl Into<String>) -> Self {
158 self.inner.message_type = Some(message_type.into());
159 let configured = self.inner.message_type.as_deref();
160 for layered in &mut self.inner.validators {
161 layered.validator.set_message_type(configured);
162 }
163 self
164 }
165
166 /// Enable/disable structure validators.
167 pub fn structure(mut self, enabled: bool) -> Self {
168 self.inner.structure_enabled = enabled;
169 self
170 }
171
172 /// Enable/disable code-list validators.
173 pub fn code_list(mut self, enabled: bool) -> Self {
174 self.inner.code_list_enabled = enabled;
175 self
176 }
177
178 /// Enable/disable profile validators.
179 pub fn profile(mut self, enabled: bool) -> Self {
180 self.inner.profile_enabled = enabled;
181 self
182 }
183
184 /// Stop all validation as soon as the first `Critical`-severity issue is produced.
185 ///
186 /// When set, [`ValidationContext::validate_lenient`] returns immediately after the
187 /// first `Critical` issue from any validator, skipping all remaining packs and layers.
188 ///
189 /// Default: `false` (collect all issues across all layers).
190 pub fn bail_on_first_critical(mut self, bail: bool) -> Self {
191 self.inner.bail_on_first_critical = bail;
192 self
193 }
194
195 /// Enable/disable envelope layer validators.
196 ///
197 /// Off by default. Call [`with_envelope_validation`][Self::with_envelope_validation]
198 /// to add the built-in [`EnvelopeValidator`] and enable the layer in one step.
199 pub fn envelope(mut self, enabled: bool) -> Self {
200 self.inner.envelope_enabled = enabled;
201 self
202 }
203
204 /// Add the built-in [`EnvelopeValidator`] and enable the envelope layer.
205 pub fn with_envelope_validation(mut self) -> Self {
206 self.inner.envelope_enabled = true;
207 self.inner.validators.push(LayeredValidator {
208 layer: ValidationLayer::Envelope,
209 validator: Box::new(EnvelopeValidator),
210 });
211 self
212 }
213
214 /// Add a validator assigned to `layer`.
215 pub fn with_validator<V>(mut self, layer: ValidationLayer, mut validator: V) -> Self
216 where
217 V: Validator + 'static,
218 {
219 validator.set_message_type(self.inner.message_type.as_deref());
220 self.inner.validators.push(LayeredValidator {
221 layer,
222 validator: Box::new(validator),
223 });
224 self
225 }
226
227 /// Add a profile rule pack to the profile layer.
228 ///
229 /// The pack's own message-type scoping — set with
230 /// [`ProfileRulePack::for_message_type`] — is what decides whether its rules
231 /// run. The context's message type does not narrow an unscoped pack.
232 pub fn with_profile_pack(self, pack: ProfileRulePack) -> Self {
233 self.with_profile_pack_inner(pack)
234 }
235
236 fn with_profile_pack_inner(mut self, pack: ProfileRulePack) -> Self {
237 self.inner.validators.push(LayeredValidator {
238 layer: ValidationLayer::Profile,
239 validator: Box::new(pack),
240 });
241 self
242 }
243
244 /// Add a reference-counted profile rule pack to the profile layer.
245 ///
246 /// Unlike [`with_profile_pack`](Self::with_profile_pack), this method stores the pack
247 /// behind an [`Arc`] so context forking (via
248 /// [`ValidationContext::fork_with_message_ref`]) only increments the reference count
249 /// instead of deep-cloning the rule vec.
250 ///
251 /// This is the preferred API for downstream code that caches packs in static
252 /// storage (`LazyLock`, `OnceLock`) and reuses them across many validation calls.
253 ///
254 /// # Example
255 ///
256 /// ```rust,ignore
257 /// use std::sync::{Arc, LazyLock};
258 /// use edifact_rs::{ProfileRulePack, ValidationContext};
259 ///
260 /// static PACK: LazyLock<Arc<ProfileRulePack>> = LazyLock::new(|| {
261 /// Arc::new(ProfileRulePack::new("MIG").require_segment("BGM", "MIG-BGM-M"))
262 /// });
263 ///
264 /// let ctx = ValidationContext::builder()
265 /// .with_profile_pack_arc(Arc::clone(&*PACK))
266 /// .build();
267 /// ```
268 pub fn with_profile_pack_arc(mut self, pack: std::sync::Arc<ProfileRulePack>) -> Self {
269 self.inner.validators.push(LayeredValidator {
270 layer: ValidationLayer::Profile,
271 validator: Box::new(pack),
272 });
273 self
274 }
275
276 /// Unconditionally append `issue` to every report produced by this context.
277 ///
278 /// Static issues are emitted on every `validate_*` call — they are not
279 /// evaluated against segments. This is useful for advisory notices that
280 /// should always be present regardless of message content (e.g. "AHB layer
281 /// is inactive for this message type").
282 pub fn with_static_issue(mut self, issue: crate::ValidationIssue) -> Self {
283 self.inner.static_issues.push(issue);
284 self
285 }
286
287 /// Finalize builder and create context.
288 #[must_use = "call `.validate_lenient()` or `.validate_strict()` on the resulting context"]
289 pub fn build(self) -> ValidationContext {
290 self.inner
291 }
292}
293
294impl ValidationContext {
295 /// Start building a validation context.
296 pub fn builder() -> ValidationContextBuilder {
297 ValidationContextBuilder::new()
298 }
299
300 /// Execute validators in lenient mode for enabled layers.
301 pub fn validate_lenient(&self, segments: &[Segment<'_>]) -> ValidationReport {
302 self.validate_with_context(segments, &self.build_rule_context())
303 }
304
305 /// Execute flat + group-aware validators in lenient mode.
306 ///
307 /// This method runs the full flat validation pass (same as
308 /// [`validate_lenient`](Self::validate_lenient)) **and** then runs the
309 /// group-aware pass by calling [`Validator::validate_group_batch`] on every
310 /// validator. Validators without group rules treat `validate_group_batch`
311 /// as a no-op, so this is safe to call for any context.
312 ///
313 /// # When to use
314 ///
315 /// Use this method when you have already grouped your segments with
316 /// [`group_segments_indexed`][crate::group_segments_indexed] or
317 /// [`group_owned_segments_indexed`][crate::group_owned_segments_indexed] and
318 /// want group-presence or cross-group rules (via
319 /// [`ProfileRulePack::with_scoped_group_rule_fn`][crate::ProfileRulePack::with_scoped_group_rule_fn])
320 /// to fire.
321 ///
322 /// # Example
323 ///
324 /// ```rust,ignore
325 /// use edifact_rs::{group_segments_indexed, ValidationContext};
326 /// use edifact_rs::group::GroupDef;
327 ///
328 /// static SCHEMA: &[GroupDef] = &[
329 /// GroupDef { name: "SG5", trigger: "LOC", children: &[] },
330 /// ];
331 ///
332 /// let tree = group_segments_indexed(&segments, SCHEMA, "ROOT");
333 /// let pack = ProfileRulePack::new("AHB")
334 /// .require_segment_in_group("SG5", "DTM", "SG5-DTM-M");
335 /// let ctx = ValidationContext::builder().with_profile_pack(pack).build();
336 ///
337 /// let report = ctx.validate_lenient_grouped(&tree, &segments);
338 /// ```
339 pub fn validate_lenient_grouped(
340 &self,
341 root: &crate::group::SegmentGroupIndexed,
342 segments: &[Segment<'_>],
343 ) -> ValidationReport {
344 let base_ctx = self.build_rule_context();
345 // Phase 1: flat validation.
346 let mut report = self.validate_with_context(segments, &base_ctx);
347 // Phase 2: group-aware validation with pre-extracted UNH message type.
348 let unh_mt = segments
349 .iter()
350 .find(|s| s.tag == "UNH")
351 .and_then(|s| s.get_element(1))
352 .and_then(|e| e.get_component(0));
353 let ctx_with_type;
354 let group_ctx: &ValidationRuleContext<'_> = if let Some(mt) = unh_mt {
355 ctx_with_type = ValidationRuleContext {
356 metadata: base_ctx.metadata,
357 message_ref: base_ctx.message_ref,
358 message_type: Some(mt),
359 };
360 &ctx_with_type
361 } else {
362 &base_ctx
363 };
364 self.run_group_pass(root, segments, &mut report, group_ctx);
365 report
366 }
367
368 /// Execute flat + group-aware validators in strict mode.
369 pub fn validate_strict_grouped(
370 &self,
371 root: &crate::group::SegmentGroupIndexed,
372 segments: &[Segment<'_>],
373 ) -> Result<ValidationReport, ValidationReport> {
374 self.validate_lenient_grouped(root, segments).result()
375 }
376
377 /// Execute flat + group-aware validators against owned segments in lenient mode.
378 pub fn validate_lenient_grouped_owned(
379 &self,
380 root: &crate::group::SegmentGroupIndexed,
381 segments: &[crate::OwnedSegment],
382 ) -> ValidationReport {
383 let base_ctx = self.build_rule_context();
384 // Phase 1: flat validation.
385 let mut report = self.validate_with_context_owned(segments, &base_ctx);
386 // Phase 2: group-aware validation — skip early if no validator has group
387 // rules, avoiding the O(n) borrowed-segment allocation entirely.
388 if !self
389 .validators
390 .iter()
391 .any(|lv| self.layer_enabled(lv.layer) && lv.validator.has_group_rules())
392 {
393 return report;
394 }
395 let borrowed: Vec<Segment<'_>> = segments.iter().map(|s| s.as_borrowed()).collect();
396 let unh_mt = borrowed
397 .iter()
398 .find(|s| s.tag == "UNH")
399 .and_then(|s| s.get_element(1))
400 .and_then(|e| e.get_component(0));
401 let ctx_with_type;
402 let group_ctx: &ValidationRuleContext<'_> = if let Some(mt) = unh_mt {
403 ctx_with_type = ValidationRuleContext {
404 metadata: base_ctx.metadata,
405 message_ref: base_ctx.message_ref,
406 message_type: Some(mt),
407 };
408 &ctx_with_type
409 } else {
410 &base_ctx
411 };
412 self.run_group_pass(root, &borrowed, &mut report, group_ctx);
413 report
414 }
415
416 /// Execute flat + group-aware validators against owned segments in strict mode.
417 pub fn validate_strict_grouped_owned(
418 &self,
419 root: &crate::group::SegmentGroupIndexed,
420 segments: &[crate::OwnedSegment],
421 ) -> Result<ValidationReport, ValidationReport> {
422 self.validate_lenient_grouped_owned(root, segments).result()
423 }
424
425 /// Phase-2 group pass: call `validate_group_batch` on each enabled validator.
426 fn run_group_pass(
427 &self,
428 root: &crate::group::SegmentGroupIndexed,
429 segments: &[Segment<'_>],
430 report: &mut ValidationReport,
431 context: &ValidationRuleContext<'_>,
432 ) {
433 // Short-circuit: skip the entire DFS tree walk when no enabled validator
434 // has group rules. This avoids the O(n) borrowed-segment allocation in
435 // `validate_lenient_grouped_owned` for the common case where the context
436 // only has flat (envelope/structure/code-list) validators.
437 if !self
438 .validators
439 .iter()
440 .any(|lv| self.layer_enabled(lv.layer) && lv.validator.has_group_rules())
441 {
442 return;
443 }
444 for lv in &self.validators {
445 if !self.layer_enabled(lv.layer) {
446 continue;
447 }
448 lv.validator
449 .validate_group_batch(root, segments, report, context);
450 if self.bail_on_first_critical && report.has_critical_errors() {
451 break;
452 }
453 }
454 }
455
456 /// Execute validators with per-call typed metadata.
457 ///
458 /// `message_type` is set to `None` here; the concrete validation method
459 /// (`validate_with_context`) re-extracts the message type from the `UNH`
460 /// segment, so there is no information loss.
461 pub fn validate_lenient_with<T: Any + Send + Sync>(
462 &self,
463 segments: &[Segment<'_>],
464 value: &T,
465 ) -> ValidationReport {
466 let ctx = ValidationRuleContext {
467 metadata: Some(value as &(dyn Any + Send + Sync)),
468 message_ref: self.message_ref.as_deref(),
469 message_type: None,
470 };
471 self.validate_with_context(segments, &ctx)
472 }
473
474 /// Execute validators in strict mode for enabled layers.
475 pub fn validate_strict(
476 &self,
477 segments: &[Segment<'_>],
478 ) -> Result<ValidationReport, ValidationReport> {
479 self.validate_lenient(segments).result()
480 }
481
482 /// Execute validators in strict mode with per-call typed metadata.
483 pub fn validate_strict_with<T: Any + Send + Sync>(
484 &self,
485 segments: &[Segment<'_>],
486 value: &T,
487 ) -> Result<ValidationReport, ValidationReport> {
488 self.validate_lenient_with(segments, value).result()
489 }
490
491 /// Execute validators in lenient mode against an owned-segment slice.
492 pub fn validate_lenient_owned(&self, segments: &[OwnedSegment]) -> ValidationReport {
493 self.validate_with_context_owned(segments, &self.build_rule_context())
494 }
495
496 fn build_rule_context(&self) -> ValidationRuleContext<'_> {
497 self.metadata
498 .as_ref()
499 .map(|arc| ValidationRuleContext {
500 metadata: Some(arc.as_ref() as &(dyn Any + Send + Sync)),
501 message_ref: self.message_ref.as_deref(),
502 message_type: None,
503 })
504 .unwrap_or_else(|| ValidationRuleContext {
505 metadata: None,
506 message_ref: self.message_ref.as_deref(),
507 message_type: None,
508 })
509 }
510
511 fn validate_with_context_owned(
512 &self,
513 segments: &[OwnedSegment],
514 context: &ValidationRuleContext<'_>,
515 ) -> ValidationReport {
516 let mut report = ValidationReport::default();
517 // Pre-extract UNH message type once (F-017).
518 let unh_message_type: Option<String> = segments
519 .iter()
520 .find(|s| s.tag == "UNH")
521 .and_then(|s| s.component_str(1, 0))
522 .map(str::to_owned);
523 let ctx_with_type;
524 let effective_ctx: &ValidationRuleContext<'_> = if let Some(ref mt) = unh_message_type {
525 ctx_with_type = ValidationRuleContext {
526 metadata: context.metadata,
527 message_ref: context.message_ref,
528 message_type: Some(mt.as_str()),
529 };
530 &ctx_with_type
531 } else {
532 context
533 };
534 let mut full_borrowed: Option<Vec<Segment<'_>>> = None;
535 let mut filtered_borrowed: Option<Vec<Segment<'_>>> = None;
536 // Decided once, up front. Deriving this from a flag flipped as the loop
537 // walks `self.validators` made the filtering depend on *registration*
538 // order, so moving `.with_envelope_validation()` in the builder chain
539 // silently changed which segments later layers saw.
540 let envelope_active = self.envelope_layer_active();
541
542 for lv in &self.validators {
543 if !self.layer_enabled(lv.layer) {
544 continue;
545 }
546 if lv.layer == ValidationLayer::Envelope {
547 let active = full_borrowed
548 .get_or_insert_with(|| segments.iter().map(|s| s.as_borrowed()).collect());
549 lv.validator
550 .validate_batch(active, &mut report, effective_ctx);
551 } else if envelope_active {
552 let active = filtered_borrowed.get_or_insert_with(|| {
553 segments
554 .iter()
555 .filter(|s| !matches!(s.tag.as_str(), "UNB" | "UNZ" | "UNG" | "UNE"))
556 .map(|s| s.as_borrowed())
557 .collect()
558 });
559 lv.validator
560 .validate_batch(active, &mut report, effective_ctx);
561 } else {
562 let active = full_borrowed
563 .get_or_insert_with(|| segments.iter().map(|s| s.as_borrowed()).collect());
564 lv.validator
565 .validate_batch(active, &mut report, effective_ctx);
566 }
567 if self.bail_on_first_critical && report.has_critical_errors() {
568 break;
569 }
570 }
571
572 if let Some(ref msg_ref) = self.message_ref {
573 for issue in report
574 .errors
575 .iter_mut()
576 .chain(report.warnings.iter_mut())
577 .chain(report.infos.iter_mut())
578 {
579 if issue.message_ref.is_none() {
580 issue.message_ref = Some(msg_ref.clone());
581 }
582 }
583 }
584 for issue in &self.static_issues {
585 match issue.severity {
586 ValidationSeverity::Critical | ValidationSeverity::Error => {
587 report.add_error(issue.clone());
588 }
589 ValidationSeverity::Warning => {
590 report.warnings.push(issue.clone());
591 }
592 ValidationSeverity::Info => {
593 report.infos.push(issue.clone());
594 }
595 }
596 }
597 report
598 }
599
600 /// Execute validators in strict mode against an owned-segment slice.
601 pub fn validate_strict_owned(
602 &self,
603 segments: &[OwnedSegment],
604 ) -> Result<ValidationReport, ValidationReport> {
605 self.validate_lenient_owned(segments).result()
606 }
607
608 fn validate_with_context(
609 &self,
610 segments: &[Segment<'_>],
611 context: &ValidationRuleContext<'_>,
612 ) -> ValidationReport {
613 let mut report = ValidationReport::default();
614 // Pre-extract UNH message type once (F-017).
615 let unh_message_type = segments
616 .iter()
617 .find(|s| s.tag == "UNH")
618 .and_then(|s| s.get_element(1))
619 .and_then(|e| e.get_component(0));
620 let ctx_with_type;
621 let effective_ctx: &ValidationRuleContext<'_> = if let Some(mt) = unh_message_type {
622 ctx_with_type = ValidationRuleContext {
623 metadata: context.metadata,
624 message_ref: context.message_ref,
625 message_type: Some(mt),
626 };
627 &ctx_with_type
628 } else {
629 context
630 };
631 let mut filtered: Option<Vec<Segment<'_>>> = None;
632 // See the owned path: computed once so filtering is independent of the
633 // order in which validators were registered.
634 let envelope_active = self.envelope_layer_active();
635
636 for lv in &self.validators {
637 if !self.layer_enabled(lv.layer) {
638 continue;
639 }
640 if lv.layer == ValidationLayer::Envelope {
641 lv.validator
642 .validate_batch(segments, &mut report, effective_ctx);
643 } else {
644 let active: &[Segment<'_>] = if envelope_active {
645 match envelope_interior(segments) {
646 // Common case: UNB/UNZ bracket the message and no
647 // UNG/UNE appear inside, so a sub-slice suffices and no
648 // segment has to be deep-cloned.
649 Some(interior) => interior,
650 None => filtered.get_or_insert_with(|| {
651 segments
652 .iter()
653 .filter(|s| !matches!(s.tag, "UNB" | "UNZ" | "UNG" | "UNE"))
654 .cloned()
655 .collect()
656 }),
657 }
658 } else {
659 segments
660 };
661 lv.validator
662 .validate_batch(active, &mut report, effective_ctx);
663 }
664 if self.bail_on_first_critical && report.has_critical_errors() {
665 break;
666 }
667 }
668
669 if let Some(ref msg_ref) = self.message_ref {
670 for issue in report
671 .errors
672 .iter_mut()
673 .chain(report.warnings.iter_mut())
674 .chain(report.infos.iter_mut())
675 {
676 if issue.message_ref.is_none() {
677 issue.message_ref = Some(msg_ref.clone());
678 }
679 }
680 }
681 // Append static advisory issues unconditionally.
682 for issue in &self.static_issues {
683 match issue.severity {
684 ValidationSeverity::Critical | ValidationSeverity::Error => {
685 report.add_error(issue.clone());
686 }
687 ValidationSeverity::Warning => {
688 report.warnings.push(issue.clone());
689 }
690 ValidationSeverity::Info => {
691 report.infos.push(issue.clone());
692 }
693 }
694 }
695 report
696 }
697
698 /// Message type metadata associated with this context, if provided.
699 pub fn message_type(&self) -> Option<&str> {
700 self.message_type.as_deref()
701 }
702
703 /// Message reference (`UNH` element 0) associated with this context, if provided.
704 pub fn message_ref(&self) -> Option<&str> {
705 self.message_ref.as_deref()
706 }
707
708 /// Create a child context that inherits all rules and configuration from `self`
709 /// but is scoped to a specific message reference (UNH DE 0062).
710 ///
711 /// Issues produced by the child context are automatically stamped with
712 /// `message_ref`, making it easy to correlate findings in a multi-message
713 /// interchange back to the originating `UNH`/`UNT` envelope.
714 ///
715 /// # Example
716 ///
717 /// ```rust,ignore
718 /// let base_ctx = ValidationContext::builder()
719 /// .with_profile_pack(mig_pack)
720 /// .build();
721 ///
722 /// for (ref_no, message_segments) in messages {
723 /// let child = base_ctx.fork_with_message_ref(&ref_no);
724 /// let report = child.validate_lenient(&message_segments);
725 /// }
726 /// ```
727 pub fn fork_with_message_ref(&self, message_ref: impl Into<String>) -> Self {
728 let validators: Vec<LayeredValidator> = self
729 .validators
730 .iter()
731 .filter_map(|lv| {
732 lv.validator.fork().map(|forked| LayeredValidator {
733 layer: lv.layer,
734 validator: forked,
735 })
736 })
737 .collect();
738 // Count how many validators were excluded (non-forkable).
739 let excluded_count = self.validators.len() - validators.len();
740
741 let mut static_issues = self.static_issues.clone();
742 if excluded_count > 0 {
743 static_issues.push(
744 crate::ValidationIssue::new(
745 crate::ValidationSeverity::Info,
746 format!(
747 "{excluded_count} validator(s) excluded from forked context \
748 because fork() returned None; all their rules (flat and \
749 group-pass) will not run for this message",
750 ),
751 )
752 .with_rule_id("edifact-rs::fork::excluded-validator"),
753 );
754 }
755
756 Self {
757 validators,
758 envelope_enabled: self.envelope_enabled,
759 structure_enabled: self.structure_enabled,
760 code_list_enabled: self.code_list_enabled,
761 profile_enabled: self.profile_enabled,
762 bail_on_first_critical: self.bail_on_first_critical,
763 message_type: self.message_type.clone(),
764 message_ref: Some(message_ref.into()),
765 metadata: self.metadata.clone(),
766 static_issues,
767 }
768 }
769
770 fn layer_enabled(&self, layer: ValidationLayer) -> bool {
771 match layer {
772 ValidationLayer::Envelope => self.envelope_enabled,
773 ValidationLayer::Structure => self.structure_enabled,
774 ValidationLayer::CodeList => self.code_list_enabled,
775 ValidationLayer::Profile => self.profile_enabled,
776 }
777 }
778
779 /// Whether an enabled envelope-layer validator is registered.
780 ///
781 /// Determines whether envelope segments are hidden from later layers. It is
782 /// a property of the context as a whole, not of how far the validator loop
783 /// has progressed.
784 fn envelope_layer_active(&self) -> bool {
785 self.envelope_enabled
786 && self
787 .validators
788 .iter()
789 .any(|lv| lv.layer == ValidationLayer::Envelope)
790 }
791}
792
793/// Return the message body as a sub-slice when the envelope segments form a
794/// clean `UNB` … `UNZ` bracket with no functional groups inside.
795///
796/// Returns `None` when the caller must fall back to filter-and-clone (functional
797/// groups present, or the interchange is not bracketed as expected).
798fn envelope_interior<'s, 'a>(segments: &'s [Segment<'a>]) -> Option<&'s [Segment<'a>]> {
799 let (first, last) = (segments.first()?, segments.last()?);
800 if segments.len() < 2 || first.tag != "UNB" || last.tag != "UNZ" {
801 return None;
802 }
803 let interior = &segments[1..segments.len() - 1];
804 if interior
805 .iter()
806 .any(|s| matches!(s.tag, "UNB" | "UNZ" | "UNG" | "UNE"))
807 {
808 return None;
809 }
810 Some(interior)
811}