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