Skip to main content

automapper_validation/
pipeline.rs

1//! Full raw-EDIFACT validation pipeline, shared by the v2 API route
2//! (`automapper-api`'s `run_validation`) and the `edifact-mapper` library entry
3//! point (`Mapper::validate_edifact`), so both produce byte-identical findings.
4
5use mig_assembly::assembler::{Assembler, AssemblerConfig};
6use mig_assembly::StructureDiagnosticKind;
7use mig_types::schema::mig::MigSchema;
8use mig_types::segment::OwnedSegment;
9
10use crate::eval::{ConditionEvaluator, ExternalConditionProvider};
11use crate::{
12    build_validated_tree, validate_unt_segment_count, AhbWorkflow, EdifactValidator, IssueKind,
13    Severity, ValidationIssue, ValidationLevel, ValidationReport,
14};
15
16/// Validate one message's assembled segments against its AHB workflow.
17///
18/// Callers resolve `filtered_mig`, `workflow`, and `evaluator` from wherever they
19/// hold the data (the API from its `MigServiceRegistry`, `edifact-mapper` from the
20/// data bundle) and pass the already-collected `all_segments` (message body plus
21/// envelope for MIGs that cover it). Assembly config, validated-tree construction,
22/// UNT-count and structure diagnostics all live here so the two entry points can
23/// never drift.
24pub fn validate_edifact_message<E: ConditionEvaluator>(
25    all_segments: &[OwnedSegment],
26    filtered_mig: &MigSchema,
27    workflow: &AhbWorkflow,
28    evaluator: E,
29    external: &dyn ExternalConditionProvider,
30    level: ValidationLevel,
31) -> ValidationReport {
32    validate_edifact_message_with_structure(
33        all_segments,
34        filtered_mig,
35        None,
36        workflow,
37        evaluator,
38        external,
39        level,
40    )
41}
42
43/// [`validate_edifact_message`] plus the structure check against the MIG's
44/// statuses and repetition limits (`mig_assembly::structure_check`):
45/// missing mandatory segments and groups (STR001 / STR005), too many
46/// repetitions (STR002 / STR006).
47///
48/// `pid_mig` is the PID's **unmerged** view of the MIG
49/// (`mig_assembly::pid_filter::filter_mig_for_pid_unmerged`) — the merged
50/// `filtered_mig` has lost which variant of a group requires what. `None`
51/// skips the check.
52pub fn validate_edifact_message_with_structure<E: ConditionEvaluator>(
53    all_segments: &[OwnedSegment],
54    filtered_mig: &MigSchema,
55    pid_mig: Option<&MigSchema>,
56    workflow: &AhbWorkflow,
57    evaluator: E,
58    external: &dyn ExternalConditionProvider,
59    level: ValidationLevel,
60) -> ValidationReport {
61    validate_message(
62        all_segments,
63        filtered_mig,
64        pid_mig,
65        workflow,
66        EdifactValidator::new(evaluator),
67        external,
68        level,
69    )
70}
71
72/// [`validate_edifact_message`] that also reports segments sent although
73/// their own condition does not hold
74/// ([`IssueKind::SegmentNotAllowed`](crate::IssueKind::SegmentNotAllowed)),
75/// for the fixture generator's repair; see
76/// [`EdifactValidator::reporting_segments`].
77pub fn validate_edifact_message_reporting_segments<E: ConditionEvaluator>(
78    all_segments: &[OwnedSegment],
79    filtered_mig: &MigSchema,
80    workflow: &AhbWorkflow,
81    evaluator: E,
82    external: &dyn ExternalConditionProvider,
83    level: ValidationLevel,
84) -> ValidationReport {
85    validate_message(
86        all_segments,
87        filtered_mig,
88        None,
89        workflow,
90        EdifactValidator::new(evaluator).reporting_segments(),
91        external,
92        level,
93    )
94}
95
96fn validate_message<E: ConditionEvaluator>(
97    all_segments: &[OwnedSegment],
98    filtered_mig: &MigSchema,
99    pid_mig: Option<&MigSchema>,
100    workflow: &AhbWorkflow,
101    validator: EdifactValidator<E>,
102    external: &dyn ExternalConditionProvider,
103    level: ValidationLevel,
104) -> ValidationReport {
105    // `skip_unknown_segments` lets the assembler advance past AHB-foreign segments
106    // (surfaced once as diagnostics below) instead of stalling; `strict_code_matching`
107    // disambiguates merged sibling slots. Mirrors the v2 route's config exactly.
108    let assembler = Assembler::with_config(
109        filtered_mig,
110        AssemblerConfig {
111            strict_code_matching: true,
112            skip_unknown_segments: true,
113            ..Default::default()
114        },
115    );
116    let (tree, mut structure_diagnostics) = assembler.assemble_with_diagnostics(all_segments);
117    if let Some(pid_mig) = pid_mig {
118        structure_diagnostics.extend(mig_assembly::structure_check::check_structure(
119            &tree, pid_mig,
120        ));
121    }
122
123    let validated_tree = build_validated_tree(workflow, &tree);
124    let mut report =
125        validator.validate_assembled_tree(&validated_tree, all_segments, external, level, &tree);
126
127    if let Some(issue) = validate_unt_segment_count(all_segments) {
128        report.add_issue(issue);
129    }
130
131    for diag in structure_diagnostics {
132        // A missing mandatory segment or group, and one repeating beyond the
133        // MIG's MaxRep, make the message invalid (the receiver rejects it with
134        // CONTRL code 13 / 16). The other structure findings describe content
135        // the conversion could still place (or had to skip) and stay warnings.
136        let severity = match diag.kind {
137            StructureDiagnosticKind::MissingRequiredSegment
138            | StructureDiagnosticKind::MissingRequiredGroup
139            | StructureDiagnosticKind::MaxRepetitionsExceeded
140            | StructureDiagnosticKind::GroupMaxRepetitionsExceeded => Severity::Error,
141            _ => Severity::Warning,
142        };
143        let kind = IssueKind::StructureDiagnostic {
144            kind: diag.kind,
145            segment_id: diag.segment_id.clone(),
146            position: diag.position,
147            detail: diag.message,
148        };
149        report.add_issue(ValidationIssue::new(severity, kind));
150    }
151
152    report
153}