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