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}