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 pub fn with_profile_pack(mut self, mut pack: ProfileRulePack) -> Self {
229 pack.set_message_type(self.inner.message_type.as_deref());
230 self.inner.validators.push(LayeredValidator {
231 layer: ValidationLayer::Profile,
232 validator: Box::new(pack),
233 });
234 self
235 }
236
237 /// Add a reference-counted profile rule pack to the profile layer.
238 ///
239 /// Unlike [`with_profile_pack`](Self::with_profile_pack), this method stores the pack
240 /// behind an [`Arc`] so context forking (via
241 /// [`ValidationContext::fork_with_message_ref`]) only increments the reference count
242 /// instead of deep-cloning the rule vec.
243 ///
244 /// This is the preferred API for downstream code that caches packs in static
245 /// storage (`LazyLock`, `OnceLock`) and reuses them across many validation calls.
246 ///
247 /// # Example
248 ///
249 /// ```rust,ignore
250 /// use std::sync::{Arc, LazyLock};
251 /// use edifact_rs::{ProfileRulePack, ValidationContext};
252 ///
253 /// static PACK: LazyLock<Arc<ProfileRulePack>> = LazyLock::new(|| {
254 /// Arc::new(ProfileRulePack::new("MIG").require_segment("BGM", "MIG-BGM-M"))
255 /// });
256 ///
257 /// let ctx = ValidationContext::builder()
258 /// .with_profile_pack_arc(Arc::clone(&*PACK))
259 /// .build();
260 /// ```
261 pub fn with_profile_pack_arc(mut self, pack: std::sync::Arc<ProfileRulePack>) -> Self {
262 self.inner.validators.push(LayeredValidator {
263 layer: ValidationLayer::Profile,
264 validator: Box::new(pack),
265 });
266 self
267 }
268
269 /// Unconditionally append `issue` to every report produced by this context.
270 ///
271 /// Static issues are emitted on every `validate_*` call — they are not
272 /// evaluated against segments. This is useful for advisory notices that
273 /// should always be present regardless of message content (e.g. "AHB layer
274 /// is inactive for this message type").
275 pub fn with_static_issue(mut self, issue: crate::ValidationIssue) -> Self {
276 self.inner.static_issues.push(issue);
277 self
278 }
279
280 /// Finalize builder and create context.
281 #[must_use = "call `.validate_lenient()` or `.validate_strict()` on the resulting context"]
282 pub fn build(self) -> ValidationContext {
283 self.inner
284 }
285}
286
287impl ValidationContext {
288 /// Start building a validation context.
289 pub fn builder() -> ValidationContextBuilder {
290 ValidationContextBuilder::new()
291 }
292
293 /// Execute validators in lenient mode for enabled layers.
294 pub fn validate_lenient(&self, segments: &[Segment<'_>]) -> ValidationReport {
295 self.validate_with_context(segments, &self.build_rule_context())
296 }
297
298 /// Execute flat + group-aware validators in lenient mode.
299 ///
300 /// This method runs the full flat validation pass (same as
301 /// [`validate_lenient`](Self::validate_lenient)) **and** then runs the
302 /// group-aware pass by calling [`Validator::validate_group_batch`] on every
303 /// validator. Validators without group rules treat `validate_group_batch`
304 /// as a no-op, so this is safe to call for any context.
305 ///
306 /// # When to use
307 ///
308 /// Use this method when you have already grouped your segments with
309 /// [`group_segments_indexed`][crate::group_segments_indexed] or
310 /// [`group_owned_segments_indexed`][crate::group_owned_segments_indexed] and
311 /// want group-presence or cross-group rules (via
312 /// [`ProfileRulePack::with_scoped_group_rule_fn`][crate::ProfileRulePack::with_scoped_group_rule_fn])
313 /// to fire.
314 ///
315 /// # Example
316 ///
317 /// ```rust,ignore
318 /// use edifact_rs::{group_segments_indexed, ValidationContext};
319 /// use edifact_rs::group::GroupDef;
320 ///
321 /// static SCHEMA: &[GroupDef] = &[
322 /// GroupDef { name: "SG5", trigger: "LOC", children: &[] },
323 /// ];
324 ///
325 /// let tree = group_segments_indexed(&segments, SCHEMA, "ROOT");
326 /// let pack = ProfileRulePack::new("AHB")
327 /// .require_segment_in_group("SG5", "DTM", "SG5-DTM-M");
328 /// let ctx = ValidationContext::builder().with_profile_pack(pack).build();
329 ///
330 /// let report = ctx.validate_lenient_grouped(&tree, &segments);
331 /// ```
332 pub fn validate_lenient_grouped(
333 &self,
334 root: &crate::group::SegmentGroupIndexed,
335 segments: &[Segment<'_>],
336 ) -> ValidationReport {
337 let base_ctx = self.build_rule_context();
338 // Phase 1: flat validation.
339 let mut report = self.validate_with_context(segments, &base_ctx);
340 // Phase 2: group-aware validation with pre-extracted UNH message type.
341 let unh_mt = segments
342 .iter()
343 .find(|s| s.tag == "UNH")
344 .and_then(|s| s.get_element(1))
345 .and_then(|e| e.get_component(0));
346 let ctx_with_type;
347 let group_ctx: &ValidationRuleContext<'_> = if let Some(mt) = unh_mt {
348 ctx_with_type = ValidationRuleContext {
349 metadata: base_ctx.metadata,
350 message_ref: base_ctx.message_ref,
351 message_type: Some(mt),
352 };
353 &ctx_with_type
354 } else {
355 &base_ctx
356 };
357 self.run_group_pass(root, segments, &mut report, group_ctx);
358 report
359 }
360
361 /// Execute flat + group-aware validators in strict mode.
362 pub fn validate_strict_grouped(
363 &self,
364 root: &crate::group::SegmentGroupIndexed,
365 segments: &[Segment<'_>],
366 ) -> Result<ValidationReport, ValidationReport> {
367 self.validate_lenient_grouped(root, segments).result()
368 }
369
370 /// Execute flat + group-aware validators against owned segments in lenient mode.
371 pub fn validate_lenient_grouped_owned(
372 &self,
373 root: &crate::group::SegmentGroupIndexed,
374 segments: &[crate::OwnedSegment],
375 ) -> ValidationReport {
376 let base_ctx = self.build_rule_context();
377 // Phase 1: flat validation.
378 let mut report = self.validate_with_context_owned(segments, &base_ctx);
379 // Phase 2: group-aware validation — skip early if no validator has group
380 // rules, avoiding the O(n) borrowed-segment allocation entirely.
381 if !self
382 .validators
383 .iter()
384 .any(|lv| self.layer_enabled(lv.layer) && lv.validator.has_group_rules())
385 {
386 return report;
387 }
388 let borrowed: Vec<Segment<'_>> = segments.iter().map(|s| s.as_borrowed()).collect();
389 let unh_mt = borrowed
390 .iter()
391 .find(|s| s.tag == "UNH")
392 .and_then(|s| s.get_element(1))
393 .and_then(|e| e.get_component(0));
394 let ctx_with_type;
395 let group_ctx: &ValidationRuleContext<'_> = if let Some(mt) = unh_mt {
396 ctx_with_type = ValidationRuleContext {
397 metadata: base_ctx.metadata,
398 message_ref: base_ctx.message_ref,
399 message_type: Some(mt),
400 };
401 &ctx_with_type
402 } else {
403 &base_ctx
404 };
405 self.run_group_pass(root, &borrowed, &mut report, group_ctx);
406 report
407 }
408
409 /// Execute flat + group-aware validators against owned segments in strict mode.
410 pub fn validate_strict_grouped_owned(
411 &self,
412 root: &crate::group::SegmentGroupIndexed,
413 segments: &[crate::OwnedSegment],
414 ) -> Result<ValidationReport, ValidationReport> {
415 self.validate_lenient_grouped_owned(root, segments).result()
416 }
417
418 /// Phase-2 group pass: call `validate_group_batch` on each enabled validator.
419 fn run_group_pass(
420 &self,
421 root: &crate::group::SegmentGroupIndexed,
422 segments: &[Segment<'_>],
423 report: &mut ValidationReport,
424 context: &ValidationRuleContext<'_>,
425 ) {
426 // Short-circuit: skip the entire DFS tree walk when no enabled validator
427 // has group rules. This avoids the O(n) borrowed-segment allocation in
428 // `validate_lenient_grouped_owned` for the common case where the context
429 // only has flat (envelope/structure/code-list) validators.
430 if !self
431 .validators
432 .iter()
433 .any(|lv| self.layer_enabled(lv.layer) && lv.validator.has_group_rules())
434 {
435 return;
436 }
437 for lv in &self.validators {
438 if !self.layer_enabled(lv.layer) {
439 continue;
440 }
441 lv.validator
442 .validate_group_batch(root, segments, report, context);
443 if self.bail_on_first_critical && report.critical_count > 0 {
444 break;
445 }
446 }
447 }
448
449 /// Execute validators with per-call typed metadata.
450 ///
451 /// `message_type` is set to `None` here; the concrete validation method
452 /// (`validate_with_context`) re-extracts the message type from the `UNH`
453 /// segment, so there is no information loss.
454 pub fn validate_lenient_with<T: Any + Send + Sync>(
455 &self,
456 segments: &[Segment<'_>],
457 value: &T,
458 ) -> ValidationReport {
459 let ctx = ValidationRuleContext {
460 metadata: Some(value as &(dyn Any + Send + Sync)),
461 message_ref: self.message_ref.as_deref(),
462 message_type: None,
463 };
464 self.validate_with_context(segments, &ctx)
465 }
466
467 /// Execute validators in strict mode for enabled layers.
468 pub fn validate_strict(
469 &self,
470 segments: &[Segment<'_>],
471 ) -> Result<ValidationReport, ValidationReport> {
472 self.validate_lenient(segments).result()
473 }
474
475 /// Execute validators in strict mode with per-call typed metadata.
476 pub fn validate_strict_with<T: Any + Send + Sync>(
477 &self,
478 segments: &[Segment<'_>],
479 value: &T,
480 ) -> Result<ValidationReport, ValidationReport> {
481 self.validate_lenient_with(segments, value).result()
482 }
483
484 /// Execute validators in lenient mode against an owned-segment slice.
485 pub fn validate_lenient_owned(&self, segments: &[OwnedSegment]) -> ValidationReport {
486 self.validate_with_context_owned(segments, &self.build_rule_context())
487 }
488
489 fn build_rule_context(&self) -> ValidationRuleContext<'_> {
490 self.metadata
491 .as_ref()
492 .map(|arc| ValidationRuleContext {
493 metadata: Some(arc.as_ref() as &(dyn Any + Send + Sync)),
494 message_ref: self.message_ref.as_deref(),
495 message_type: None,
496 })
497 .unwrap_or_else(|| ValidationRuleContext {
498 metadata: None,
499 message_ref: self.message_ref.as_deref(),
500 message_type: None,
501 })
502 }
503
504 fn validate_with_context_owned(
505 &self,
506 segments: &[OwnedSegment],
507 context: &ValidationRuleContext<'_>,
508 ) -> ValidationReport {
509 let mut report = ValidationReport::default();
510 // Pre-extract UNH message type once (F-017).
511 let unh_message_type: Option<String> = segments
512 .iter()
513 .find(|s| s.tag == "UNH")
514 .and_then(|s| s.component_str(1, 0))
515 .map(str::to_owned);
516 let ctx_with_type;
517 let effective_ctx: &ValidationRuleContext<'_> = if let Some(ref mt) = unh_message_type {
518 ctx_with_type = ValidationRuleContext {
519 metadata: context.metadata,
520 message_ref: context.message_ref,
521 message_type: Some(mt.as_str()),
522 };
523 &ctx_with_type
524 } else {
525 context
526 };
527 let mut full_borrowed: Option<Vec<Segment<'_>>> = None;
528 let mut filtered_borrowed: Option<Vec<Segment<'_>>> = None;
529 let mut envelope_ran = false;
530
531 for lv in &self.validators {
532 if !self.layer_enabled(lv.layer) {
533 continue;
534 }
535 if lv.layer == ValidationLayer::Envelope {
536 let borrowed: Vec<Segment<'_>> = segments.iter().map(|s| s.as_borrowed()).collect();
537 lv.validator
538 .validate_batch(&borrowed, &mut report, effective_ctx);
539 envelope_ran = true;
540 } else if envelope_ran {
541 let active = filtered_borrowed.get_or_insert_with(|| {
542 segments
543 .iter()
544 .filter(|s| !matches!(s.tag.as_str(), "UNB" | "UNZ" | "UNG" | "UNE"))
545 .map(|s| s.as_borrowed())
546 .collect()
547 });
548 lv.validator
549 .validate_batch(active, &mut report, effective_ctx);
550 } else {
551 let active = full_borrowed
552 .get_or_insert_with(|| segments.iter().map(|s| s.as_borrowed()).collect());
553 lv.validator
554 .validate_batch(active, &mut report, effective_ctx);
555 }
556 if self.bail_on_first_critical && report.critical_count > 0 {
557 break;
558 }
559 }
560
561 if let Some(ref msg_ref) = self.message_ref {
562 for issue in report
563 .errors
564 .iter_mut()
565 .chain(report.warnings.iter_mut())
566 .chain(report.infos.iter_mut())
567 {
568 if issue.message_ref.is_none() {
569 issue.message_ref = Some(msg_ref.clone());
570 }
571 }
572 }
573 for issue in &self.static_issues {
574 match issue.severity {
575 ValidationSeverity::Critical | ValidationSeverity::Error => {
576 report.add_error(issue.clone());
577 }
578 ValidationSeverity::Warning => {
579 report.warnings.push(issue.clone());
580 }
581 ValidationSeverity::Info => {
582 report.infos.push(issue.clone());
583 }
584 }
585 }
586 report
587 }
588
589 /// Execute validators in strict mode against an owned-segment slice.
590 pub fn validate_strict_owned(
591 &self,
592 segments: &[OwnedSegment],
593 ) -> Result<ValidationReport, ValidationReport> {
594 self.validate_lenient_owned(segments).result()
595 }
596
597 fn validate_with_context(
598 &self,
599 segments: &[Segment<'_>],
600 context: &ValidationRuleContext<'_>,
601 ) -> ValidationReport {
602 let mut report = ValidationReport::default();
603 // Pre-extract UNH message type once (F-017).
604 let unh_message_type = segments
605 .iter()
606 .find(|s| s.tag == "UNH")
607 .and_then(|s| s.get_element(1))
608 .and_then(|e| e.get_component(0));
609 let ctx_with_type;
610 let effective_ctx: &ValidationRuleContext<'_> = if let Some(mt) = unh_message_type {
611 ctx_with_type = ValidationRuleContext {
612 metadata: context.metadata,
613 message_ref: context.message_ref,
614 message_type: Some(mt),
615 };
616 &ctx_with_type
617 } else {
618 context
619 };
620 let mut filtered: Option<Vec<Segment<'_>>> = None;
621 let mut envelope_ran = false;
622
623 for lv in &self.validators {
624 if !self.layer_enabled(lv.layer) {
625 continue;
626 }
627 if lv.layer == ValidationLayer::Envelope {
628 lv.validator
629 .validate_batch(segments, &mut report, effective_ctx);
630 envelope_ran = true;
631 } else {
632 let active: &[Segment<'_>] = if envelope_ran {
633 filtered.get_or_insert_with(|| {
634 segments
635 .iter()
636 .filter(|s| !matches!(s.tag, "UNB" | "UNZ" | "UNG" | "UNE"))
637 .cloned()
638 .collect()
639 })
640 } else {
641 segments
642 };
643 lv.validator
644 .validate_batch(active, &mut report, effective_ctx);
645 }
646 if self.bail_on_first_critical && report.critical_count > 0 {
647 break;
648 }
649 }
650
651 if let Some(ref msg_ref) = self.message_ref {
652 for issue in report
653 .errors
654 .iter_mut()
655 .chain(report.warnings.iter_mut())
656 .chain(report.infos.iter_mut())
657 {
658 if issue.message_ref.is_none() {
659 issue.message_ref = Some(msg_ref.clone());
660 }
661 }
662 }
663 // Append static advisory issues unconditionally.
664 for issue in &self.static_issues {
665 match issue.severity {
666 ValidationSeverity::Critical | ValidationSeverity::Error => {
667 report.add_error(issue.clone());
668 }
669 ValidationSeverity::Warning => {
670 report.warnings.push(issue.clone());
671 }
672 ValidationSeverity::Info => {
673 report.infos.push(issue.clone());
674 }
675 }
676 }
677 report
678 }
679
680 /// Message type metadata associated with this context, if provided.
681 pub fn message_type(&self) -> Option<&str> {
682 self.message_type.as_deref()
683 }
684
685 /// Message reference (`UNH` element 0) associated with this context, if provided.
686 pub fn message_ref(&self) -> Option<&str> {
687 self.message_ref.as_deref()
688 }
689
690 /// Create a child context that inherits all rules and configuration from `self`
691 /// but is scoped to a specific message reference (UNH DE 0062).
692 ///
693 /// Issues produced by the child context are automatically stamped with
694 /// `message_ref`, making it easy to correlate findings in a multi-message
695 /// interchange back to the originating `UNH`/`UNT` envelope.
696 ///
697 /// # Example
698 ///
699 /// ```rust,ignore
700 /// let base_ctx = ValidationContext::builder()
701 /// .with_profile_pack(mig_pack)
702 /// .build();
703 ///
704 /// for (ref_no, message_segments) in messages {
705 /// let child = base_ctx.fork_with_message_ref(&ref_no);
706 /// let report = child.validate_lenient(&message_segments);
707 /// }
708 /// ```
709 pub fn fork_with_message_ref(&self, message_ref: impl Into<String>) -> Self {
710 let validators: Vec<LayeredValidator> = self
711 .validators
712 .iter()
713 .filter_map(|lv| {
714 lv.validator.fork().map(|forked| LayeredValidator {
715 layer: lv.layer,
716 validator: forked,
717 })
718 })
719 .collect();
720 // Count how many validators were excluded (non-forkable).
721 let excluded_count = self.validators.len() - validators.len();
722
723 let mut static_issues = self.static_issues.clone();
724 if excluded_count > 0 {
725 static_issues.push(
726 crate::ValidationIssue::new(
727 crate::ValidationSeverity::Info,
728 format!(
729 "{excluded_count} validator(s) excluded from forked context \
730 because fork() returned None; all their rules (flat and \
731 group-pass) will not run for this message",
732 ),
733 )
734 .with_rule_id("edifact-rs::fork::excluded-validator"),
735 );
736 }
737
738 Self {
739 validators,
740 envelope_enabled: self.envelope_enabled,
741 structure_enabled: self.structure_enabled,
742 code_list_enabled: self.code_list_enabled,
743 profile_enabled: self.profile_enabled,
744 bail_on_first_critical: self.bail_on_first_critical,
745 message_type: self.message_type.clone(),
746 message_ref: Some(message_ref.into()),
747 metadata: self.metadata.clone(),
748 static_issues,
749 }
750 }
751
752 fn layer_enabled(&self, layer: ValidationLayer) -> bool {
753 match layer {
754 ValidationLayer::Envelope => self.envelope_enabled,
755 ValidationLayer::Structure => self.structure_enabled,
756 ValidationLayer::CodeList => self.code_list_enabled,
757 ValidationLayer::Profile => self.profile_enabled,
758 }
759 }
760}