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