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    // `skip_unknown_segments` lets the assembler advance past AHB-foreign segments
63    // (surfaced once as diagnostics below) instead of stalling; `strict_code_matching`
64    // disambiguates merged sibling slots. Mirrors the v2 route's config exactly.
65    let assembler = Assembler::with_config(
66        filtered_mig,
67        AssemblerConfig {
68            strict_code_matching: true,
69            skip_unknown_segments: true,
70            ..Default::default()
71        },
72    );
73    let (tree, mut structure_diagnostics) = assembler.assemble_with_diagnostics(all_segments);
74    if let Some(pid_mig) = pid_mig {
75        structure_diagnostics.extend(mig_assembly::structure_check::check_structure(
76            &tree, pid_mig,
77        ));
78    }
79
80    let validated_tree = build_validated_tree(workflow, &tree);
81    let navigator = AssembledTreeNavigator::new(&tree);
82    let validator = EdifactValidator::new(evaluator);
83    let mut report = validator.validate_tree(
84        &validated_tree,
85        all_segments,
86        external,
87        level,
88        Some(&navigator),
89    );
90
91    if let Some(issue) = validate_unt_segment_count(all_segments) {
92        report.add_issue(issue);
93    }
94
95    for diag in structure_diagnostics {
96        // A missing mandatory segment or group makes the message invalid; the
97        // other structure findings describe content the conversion could
98        // still place (or had to skip) and stay warnings.
99        let severity = match diag.kind {
100            StructureDiagnosticKind::MissingRequiredSegment
101            | StructureDiagnosticKind::MissingRequiredGroup => Severity::Error,
102            _ => Severity::Warning,
103        };
104        let kind = IssueKind::StructureDiagnostic {
105            kind: diag.kind,
106            segment_id: diag.segment_id.clone(),
107            position: diag.position,
108            detail: diag.message,
109        };
110        report.add_issue(ValidationIssue::new(severity, kind));
111    }
112
113    report
114}