Skip to main content

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}
104
105/// Builder for [`ValidationContext`].
106#[must_use = "call `.build()` to produce a `ValidationContext`"]
107pub struct ValidationContextBuilder {
108    pub(super) inner: ValidationContext,
109}
110
111impl Default for ValidationContextBuilder {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117impl ValidationContextBuilder {
118    /// Create a new context builder.
119    ///
120    /// Structure, code-list, and profile layers are enabled by default.
121    /// The envelope layer is **disabled** by default.
122    pub fn new() -> Self {
123        Self {
124            inner: ValidationContext {
125                validators: Vec::new(),
126                envelope_enabled: false,
127                structure_enabled: true,
128                code_list_enabled: true,
129                profile_enabled: true,
130                bail_on_first_critical: false,
131                message_type: None,
132                message_ref: None,
133                metadata: None,
134            },
135        }
136    }
137
138    /// Attach typed metadata accessible to context-aware profile rules.
139    pub fn with_metadata<T: Any + Send + Sync + 'static>(mut self, value: T) -> Self {
140        self.inner.metadata = Some(Arc::new(value));
141        self
142    }
143
144    /// Stamp every issue produced by this context with the given message reference.
145    pub fn with_message_ref(mut self, message_ref: impl Into<String>) -> Self {
146        self.inner.message_ref = Some(message_ref.into());
147        self
148    }
149
150    /// Set message type metadata for downstream validators.
151    pub fn with_message_type(mut self, message_type: impl Into<String>) -> Self {
152        self.inner.message_type = Some(message_type.into());
153        let configured = self.inner.message_type.as_deref();
154        for layered in &mut self.inner.validators {
155            layered.validator.set_message_type(configured);
156        }
157        self
158    }
159
160    /// Enable/disable structure validators.
161    pub fn structure(mut self, enabled: bool) -> Self {
162        self.inner.structure_enabled = enabled;
163        self
164    }
165
166    /// Enable/disable code-list validators.
167    pub fn code_list(mut self, enabled: bool) -> Self {
168        self.inner.code_list_enabled = enabled;
169        self
170    }
171
172    /// Enable/disable profile validators.
173    pub fn profile(mut self, enabled: bool) -> Self {
174        self.inner.profile_enabled = enabled;
175        self
176    }
177
178    /// Stop all validation as soon as the first `Critical`-severity issue is produced.
179    ///
180    /// When set, [`ValidationContext::validate_lenient`] returns immediately after the
181    /// first `Critical` issue from any validator, skipping all remaining packs and layers.
182    ///
183    /// Default: `false` (collect all issues across all layers).
184    pub fn bail_on_first_critical(mut self, bail: bool) -> Self {
185        self.inner.bail_on_first_critical = bail;
186        self
187    }
188
189    /// Enable/disable envelope layer validators.
190    ///
191    /// Off by default.  Call [`with_envelope_validation`][Self::with_envelope_validation]
192    /// to add the built-in [`EnvelopeValidator`] and enable the layer in one step.
193    pub fn envelope(mut self, enabled: bool) -> Self {
194        self.inner.envelope_enabled = enabled;
195        self
196    }
197
198    /// Add the built-in [`EnvelopeValidator`] and enable the envelope layer.
199    pub fn with_envelope_validation(mut self) -> Self {
200        self.inner.envelope_enabled = true;
201        self.inner.validators.push(LayeredValidator {
202            layer: ValidationLayer::Envelope,
203            validator: Box::new(EnvelopeValidator),
204        });
205        self
206    }
207
208    /// Add a validator assigned to `layer`.
209    pub fn with_validator<V>(mut self, layer: ValidationLayer, mut validator: V) -> Self
210    where
211        V: Validator + 'static,
212    {
213        validator.set_message_type(self.inner.message_type.as_deref());
214        self.inner.validators.push(LayeredValidator {
215            layer,
216            validator: Box::new(validator),
217        });
218        self
219    }
220
221    /// Add a profile rule pack to the profile layer.
222    pub fn with_profile_pack(mut self, mut pack: ProfileRulePack) -> Self {
223        pack.set_message_type(self.inner.message_type.as_deref());
224        self.inner.validators.push(LayeredValidator {
225            layer: ValidationLayer::Profile,
226            validator: Box::new(pack),
227        });
228        self
229    }
230
231    /// Add a reference-counted profile rule pack to the profile layer.
232    ///
233    /// Unlike [`with_profile_pack`](Self::with_profile_pack), this method stores the pack
234    /// behind an [`Arc`] so context forking (via
235    /// [`ValidationContext::fork_with_message_ref`]) only increments the reference count
236    /// instead of deep-cloning the rule vec.
237    ///
238    /// This is the preferred API for downstream code that caches packs in static
239    /// storage (`LazyLock`, `OnceLock`) and reuses them across many validation calls.
240    ///
241    /// # Example
242    ///
243    /// ```rust,ignore
244    /// use std::sync::{Arc, LazyLock};
245    /// use edifact_rs::{ProfileRulePack, ValidationContext};
246    ///
247    /// static PACK: LazyLock<Arc<ProfileRulePack>> = LazyLock::new(|| {
248    ///     Arc::new(ProfileRulePack::new("MIG").require_segment("BGM", "MIG-BGM-M"))
249    /// });
250    ///
251    /// let ctx = ValidationContext::builder()
252    ///     .with_profile_pack_arc(Arc::clone(&*PACK))
253    ///     .build();
254    /// ```
255    pub fn with_profile_pack_arc(mut self, pack: std::sync::Arc<ProfileRulePack>) -> Self {
256        self.inner.validators.push(LayeredValidator {
257            layer: ValidationLayer::Profile,
258            validator: Box::new(pack),
259        });
260        self
261    }
262
263    /// Finalize builder and create context.
264    #[must_use = "call `.validate_lenient()` or `.validate_strict()` on the resulting context"]
265    pub fn build(self) -> ValidationContext {
266        self.inner
267    }
268}
269
270impl ValidationContext {
271    /// Start building a validation context.
272    pub fn builder() -> ValidationContextBuilder {
273        ValidationContextBuilder::new()
274    }
275
276    /// Execute validators in lenient mode for enabled layers.
277    pub fn validate_lenient(&self, segments: &[Segment<'_>]) -> ValidationReport {
278        self.validate_with_context(segments, &self.build_rule_context())
279    }
280
281    /// Execute flat + group-aware validators in lenient mode.
282    ///
283    /// This method runs the full flat validation pass (same as
284    /// [`validate_lenient`](Self::validate_lenient)) **and** then runs the
285    /// group-aware pass by calling [`Validator::validate_group_batch`] on every
286    /// validator.  Validators without group rules treat `validate_group_batch`
287    /// as a no-op, so this is safe to call for any context.
288    ///
289    /// # When to use
290    ///
291    /// Use this method when you have already grouped your segments with
292    /// [`group_segments_indexed`][crate::group_segments_indexed] or
293    /// [`group_owned_segments_indexed`][crate::group_owned_segments_indexed] and
294    /// want group-presence or cross-group rules (via
295    /// [`ProfileRulePack::with_scoped_group_rule_fn`][crate::ProfileRulePack::with_scoped_group_rule_fn])
296    /// to fire.
297    ///
298    /// # Example
299    ///
300    /// ```rust,ignore
301    /// use edifact_rs::{group_segments_indexed, ValidationContext};
302    /// use edifact_rs::group::GroupDef;
303    ///
304    /// static SCHEMA: &[GroupDef] = &[
305    ///     GroupDef { name: "SG5", trigger: "LOC", children: &[] },
306    /// ];
307    ///
308    /// let tree = group_segments_indexed(&segments, SCHEMA, "ROOT");
309    /// let pack = ProfileRulePack::new("AHB")
310    ///     .require_segment_in_group("SG5", "DTM", "SG5-DTM-M");
311    /// let ctx = ValidationContext::builder().with_profile_pack(pack).build();
312    ///
313    /// let report = ctx.validate_lenient_grouped(&tree, &segments);
314    /// ```
315    pub fn validate_lenient_grouped(
316        &self,
317        root: &crate::group::SegmentGroupIndexed,
318        segments: &[Segment<'_>],
319    ) -> ValidationReport {
320        let base_ctx = self.build_rule_context();
321        // Phase 1: flat validation.
322        let mut report = self.validate_with_context(segments, &base_ctx);
323        // Phase 2: group-aware validation with pre-extracted UNH message type.
324        let unh_mt = segments
325            .iter()
326            .find(|s| s.tag == "UNH")
327            .and_then(|s| s.get_element(1))
328            .and_then(|e| e.get_component(0));
329        let ctx_with_type;
330        let group_ctx: &ValidationRuleContext<'_> = if let Some(mt) = unh_mt {
331            ctx_with_type = ValidationRuleContext {
332                metadata: base_ctx.metadata,
333                message_ref: base_ctx.message_ref,
334                message_type: Some(mt),
335            };
336            &ctx_with_type
337        } else {
338            &base_ctx
339        };
340        self.run_group_pass(root, segments, &mut report, group_ctx);
341        report
342    }
343
344    /// Execute flat + group-aware validators in strict mode.
345    pub fn validate_strict_grouped(
346        &self,
347        root: &crate::group::SegmentGroupIndexed,
348        segments: &[Segment<'_>],
349    ) -> Result<ValidationReport, ValidationReport> {
350        self.validate_lenient_grouped(root, segments).result()
351    }
352
353    /// Execute flat + group-aware validators against owned segments in lenient mode.
354    pub fn validate_lenient_grouped_owned(
355        &self,
356        root: &crate::group::SegmentGroupIndexed,
357        segments: &[crate::OwnedSegment],
358    ) -> ValidationReport {
359        let base_ctx = self.build_rule_context();
360        // Phase 1: flat validation.
361        let mut report = self.validate_with_context_owned(segments, &base_ctx);
362        // Phase 2: group-aware validation — borrow owned segments.
363        let borrowed: Vec<Segment<'_>> = segments.iter().map(|s| s.as_borrowed()).collect();
364        let unh_mt = borrowed
365            .iter()
366            .find(|s| s.tag == "UNH")
367            .and_then(|s| s.get_element(1))
368            .and_then(|e| e.get_component(0));
369        let ctx_with_type;
370        let group_ctx: &ValidationRuleContext<'_> = if let Some(mt) = unh_mt {
371            ctx_with_type = ValidationRuleContext {
372                metadata: base_ctx.metadata,
373                message_ref: base_ctx.message_ref,
374                message_type: Some(mt),
375            };
376            &ctx_with_type
377        } else {
378            &base_ctx
379        };
380        self.run_group_pass(root, &borrowed, &mut report, group_ctx);
381        report
382    }
383
384    /// Execute flat + group-aware validators against owned segments in strict mode.
385    pub fn validate_strict_grouped_owned(
386        &self,
387        root: &crate::group::SegmentGroupIndexed,
388        segments: &[crate::OwnedSegment],
389    ) -> Result<ValidationReport, ValidationReport> {
390        self.validate_lenient_grouped_owned(root, segments).result()
391    }
392
393    /// Phase-2 group pass: call `validate_group_batch` on each enabled validator.
394    fn run_group_pass(
395        &self,
396        root: &crate::group::SegmentGroupIndexed,
397        segments: &[Segment<'_>],
398        report: &mut ValidationReport,
399        context: &ValidationRuleContext<'_>,
400    ) {
401        for lv in &self.validators {
402            if !self.layer_enabled(lv.layer) {
403                continue;
404            }
405            lv.validator
406                .validate_group_batch(root, segments, report, context);
407            if self.bail_on_first_critical
408                && report
409                    .errors
410                    .iter()
411                    .any(|i| i.severity == ValidationSeverity::Critical)
412            {
413                break;
414            }
415        }
416    }
417
418    /// Execute validators with per-call typed metadata.
419    pub fn validate_lenient_with<T: Any + Send + Sync>(
420        &self,
421        segments: &[Segment<'_>],
422        value: &T,
423    ) -> ValidationReport {
424        let ctx = ValidationRuleContext {
425            metadata: Some(value as &(dyn Any + Send + Sync)),
426            message_ref: self.message_ref.as_deref(),
427            message_type: None,
428        };
429        self.validate_with_context(segments, &ctx)
430    }
431
432    /// Execute validators in strict mode for enabled layers.
433    pub fn validate_strict(
434        &self,
435        segments: &[Segment<'_>],
436    ) -> Result<ValidationReport, ValidationReport> {
437        self.validate_lenient(segments).result()
438    }
439
440    /// Execute validators in strict mode with per-call typed metadata.
441    pub fn validate_strict_with<T: Any + Send + Sync>(
442        &self,
443        segments: &[Segment<'_>],
444        value: &T,
445    ) -> Result<ValidationReport, ValidationReport> {
446        self.validate_lenient_with(segments, value).result()
447    }
448
449    /// Execute validators in lenient mode against an owned-segment slice.
450    pub fn validate_lenient_owned(&self, segments: &[OwnedSegment]) -> ValidationReport {
451        if self.validators.is_empty()
452            && !self.envelope_enabled
453            && !self.structure_enabled
454            && !self.code_list_enabled
455            && !self.profile_enabled
456        {
457            return ValidationReport::default();
458        }
459        self.validate_with_context_owned(segments, &self.build_rule_context())
460    }
461
462    fn build_rule_context(&self) -> ValidationRuleContext<'_> {
463        self.metadata
464            .as_ref()
465            .map(|arc| ValidationRuleContext {
466                metadata: Some(arc.as_ref() as &(dyn Any + Send + Sync)),
467                message_ref: self.message_ref.as_deref(),
468                message_type: None,
469            })
470            .unwrap_or_else(|| ValidationRuleContext {
471                metadata: None,
472                message_ref: self.message_ref.as_deref(),
473                message_type: None,
474            })
475    }
476
477    fn validate_with_context_owned(
478        &self,
479        segments: &[OwnedSegment],
480        context: &ValidationRuleContext<'_>,
481    ) -> ValidationReport {
482        let mut report = ValidationReport::default();
483        // Pre-extract UNH message type once (F-017).
484        let unh_message_type: Option<String> = segments
485            .iter()
486            .find(|s| s.tag == "UNH")
487            .and_then(|s| s.component_str(1, 0))
488            .map(str::to_owned);
489        let ctx_with_type;
490        let effective_ctx: &ValidationRuleContext<'_> = if let Some(ref mt) = unh_message_type {
491            ctx_with_type = ValidationRuleContext {
492                metadata: context.metadata,
493                message_ref: context.message_ref,
494                message_type: Some(mt.as_str()),
495            };
496            &ctx_with_type
497        } else {
498            context
499        };
500        let mut full_borrowed: Option<Vec<Segment<'_>>> = None;
501        let mut filtered_borrowed: Option<Vec<Segment<'_>>> = None;
502        let mut envelope_ran = false;
503
504        for lv in &self.validators {
505            if !self.layer_enabled(lv.layer) {
506                continue;
507            }
508            if lv.layer == ValidationLayer::Envelope {
509                let borrowed: Vec<Segment<'_>> = segments.iter().map(|s| s.as_borrowed()).collect();
510                lv.validator
511                    .validate_batch(&borrowed, &mut report, effective_ctx);
512                envelope_ran = true;
513            } else if envelope_ran {
514                let active = filtered_borrowed.get_or_insert_with(|| {
515                    segments
516                        .iter()
517                        .filter(|s| !matches!(s.tag.as_str(), "UNB" | "UNZ" | "UNG" | "UNE"))
518                        .map(|s| s.as_borrowed())
519                        .collect()
520                });
521                lv.validator
522                    .validate_batch(active, &mut report, effective_ctx);
523            } else {
524                let active = full_borrowed
525                    .get_or_insert_with(|| segments.iter().map(|s| s.as_borrowed()).collect());
526                lv.validator
527                    .validate_batch(active, &mut report, effective_ctx);
528            }
529            if self.bail_on_first_critical
530                && report
531                    .errors
532                    .iter()
533                    .any(|i| i.severity == ValidationSeverity::Critical)
534            {
535                break;
536            }
537        }
538
539        if let Some(ref msg_ref) = self.message_ref {
540            for issue in report
541                .errors
542                .iter_mut()
543                .chain(report.warnings.iter_mut())
544                .chain(report.infos.iter_mut())
545            {
546                if issue.message_ref.is_none() {
547                    issue.message_ref = Some(msg_ref.clone());
548                }
549            }
550        }
551        report
552    }
553
554    /// Execute validators in strict mode against an owned-segment slice.
555    pub fn validate_strict_owned(
556        &self,
557        segments: &[OwnedSegment],
558    ) -> Result<ValidationReport, ValidationReport> {
559        self.validate_lenient_owned(segments).result()
560    }
561
562    fn validate_with_context(
563        &self,
564        segments: &[Segment<'_>],
565        context: &ValidationRuleContext<'_>,
566    ) -> ValidationReport {
567        let mut report = ValidationReport::default();
568        // Pre-extract UNH message type once (F-017).
569        let unh_message_type = segments
570            .iter()
571            .find(|s| s.tag == "UNH")
572            .and_then(|s| s.get_element(1))
573            .and_then(|e| e.get_component(0));
574        let ctx_with_type;
575        let effective_ctx: &ValidationRuleContext<'_> = if let Some(mt) = unh_message_type {
576            ctx_with_type = ValidationRuleContext {
577                metadata: context.metadata,
578                message_ref: context.message_ref,
579                message_type: Some(mt),
580            };
581            &ctx_with_type
582        } else {
583            context
584        };
585        let mut filtered: Option<Vec<Segment<'_>>> = None;
586        let mut envelope_ran = false;
587
588        for lv in &self.validators {
589            if !self.layer_enabled(lv.layer) {
590                continue;
591            }
592            if lv.layer == ValidationLayer::Envelope {
593                lv.validator
594                    .validate_batch(segments, &mut report, effective_ctx);
595                envelope_ran = true;
596            } else {
597                let active: &[Segment<'_>] = if envelope_ran {
598                    filtered.get_or_insert_with(|| {
599                        segments
600                            .iter()
601                            .filter(|s| !matches!(s.tag, "UNB" | "UNZ" | "UNG" | "UNE"))
602                            .cloned()
603                            .collect()
604                    })
605                } else {
606                    segments
607                };
608                lv.validator
609                    .validate_batch(active, &mut report, effective_ctx);
610            }
611            if self.bail_on_first_critical
612                && report
613                    .errors
614                    .iter()
615                    .any(|i| i.severity == ValidationSeverity::Critical)
616            {
617                break;
618            }
619        }
620
621        if let Some(ref msg_ref) = self.message_ref {
622            for issue in report
623                .errors
624                .iter_mut()
625                .chain(report.warnings.iter_mut())
626                .chain(report.infos.iter_mut())
627            {
628                if issue.message_ref.is_none() {
629                    issue.message_ref = Some(msg_ref.clone());
630                }
631            }
632        }
633        report
634    }
635
636    /// Message type metadata associated with this context, if provided.
637    pub fn message_type(&self) -> Option<&str> {
638        self.message_type.as_deref()
639    }
640
641    /// Message reference (`UNH` element 0) associated with this context, if provided.
642    pub fn message_ref(&self) -> Option<&str> {
643        self.message_ref.as_deref()
644    }
645
646    /// Create a child context that inherits all rules and configuration from `self`
647    /// but is scoped to a specific message reference (UNH DE 0062).
648    ///
649    /// Issues produced by the child context are automatically stamped with
650    /// `message_ref`, making it easy to correlate findings in a multi-message
651    /// interchange back to the originating `UNH`/`UNT` envelope.
652    ///
653    /// # Example
654    ///
655    /// ```rust,ignore
656    /// let base_ctx = ValidationContext::builder()
657    ///     .with_profile_pack(mig_pack)
658    ///     .build();
659    ///
660    /// for (ref_no, message_segments) in messages {
661    ///     let child = base_ctx.fork_with_message_ref(&ref_no);
662    ///     let report = child.validate_lenient(&message_segments);
663    /// }
664    /// ```
665    pub fn fork_with_message_ref(&self, message_ref: impl Into<String>) -> Self {
666        Self {
667            validators: self
668                .validators
669                .iter()
670                .map(|lv| LayeredValidator {
671                    layer: lv.layer,
672                    validator: lv.validator.fork(),
673                })
674                .collect(),
675            envelope_enabled: self.envelope_enabled,
676            structure_enabled: self.structure_enabled,
677            code_list_enabled: self.code_list_enabled,
678            profile_enabled: self.profile_enabled,
679            bail_on_first_critical: self.bail_on_first_critical,
680            message_type: self.message_type.clone(),
681            message_ref: Some(message_ref.into()),
682            metadata: self.metadata.clone(),
683        }
684    }
685
686    fn layer_enabled(&self, layer: ValidationLayer) -> bool {
687        match layer {
688            ValidationLayer::Envelope => self.envelope_enabled,
689            ValidationLayer::Structure => self.structure_enabled,
690            ValidationLayer::CodeList => self.code_list_enabled,
691            ValidationLayer::Profile => self.profile_enabled,
692        }
693    }
694}