Skip to main content

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