automapper-validation 0.17.0

AHB condition expression parsing, evaluation, and EDIFACT validation
Documentation
//! Full raw-EDIFACT validation pipeline, shared by the v2 API route
//! (`automapper-api`'s `run_validation`) and the `edifact-mapper` library entry
//! point (`Mapper::validate_edifact`), so both produce byte-identical findings.

use mig_assembly::assembler::{Assembler, AssemblerConfig};
use mig_assembly::navigator::AssembledTreeNavigator;
use mig_assembly::StructureDiagnosticKind;
use mig_types::schema::mig::MigSchema;
use mig_types::segment::OwnedSegment;

use crate::eval::{ConditionEvaluator, ExternalConditionProvider};
use crate::{
    build_validated_tree, validate_unt_segment_count, AhbWorkflow, EdifactValidator, IssueKind,
    Severity, ValidationIssue, ValidationLevel, ValidationReport,
};

/// Validate one message's assembled segments against its AHB workflow.
///
/// Callers resolve `filtered_mig`, `workflow`, and `evaluator` from wherever they
/// hold the data (the API from its `MigServiceRegistry`, `edifact-mapper` from the
/// data bundle) and pass the already-collected `all_segments` (message body plus
/// envelope for MIGs that cover it). Assembly config, validated-tree construction,
/// UNT-count and structure diagnostics all live here so the two entry points can
/// never drift.
pub fn validate_edifact_message<E: ConditionEvaluator>(
    all_segments: &[OwnedSegment],
    filtered_mig: &MigSchema,
    workflow: &AhbWorkflow,
    evaluator: E,
    external: &dyn ExternalConditionProvider,
    level: ValidationLevel,
) -> ValidationReport {
    validate_edifact_message_with_structure(
        all_segments,
        filtered_mig,
        None,
        workflow,
        evaluator,
        external,
        level,
    )
}

/// [`validate_edifact_message`] plus the structure check against the MIG's
/// statuses and repetition limits (`mig_assembly::structure_check`):
/// missing mandatory segments and groups (STR001 / STR005), too many
/// repetitions (STR002 / STR006).
///
/// `pid_mig` is the PID's **unmerged** view of the MIG
/// (`mig_assembly::pid_filter::filter_mig_for_pid_unmerged`) — the merged
/// `filtered_mig` has lost which variant of a group requires what. `None`
/// skips the check.
pub fn validate_edifact_message_with_structure<E: ConditionEvaluator>(
    all_segments: &[OwnedSegment],
    filtered_mig: &MigSchema,
    pid_mig: Option<&MigSchema>,
    workflow: &AhbWorkflow,
    evaluator: E,
    external: &dyn ExternalConditionProvider,
    level: ValidationLevel,
) -> ValidationReport {
    // `skip_unknown_segments` lets the assembler advance past AHB-foreign segments
    // (surfaced once as diagnostics below) instead of stalling; `strict_code_matching`
    // disambiguates merged sibling slots. Mirrors the v2 route's config exactly.
    let assembler = Assembler::with_config(
        filtered_mig,
        AssemblerConfig {
            strict_code_matching: true,
            skip_unknown_segments: true,
            ..Default::default()
        },
    );
    let (tree, mut structure_diagnostics) = assembler.assemble_with_diagnostics(all_segments);
    if let Some(pid_mig) = pid_mig {
        structure_diagnostics.extend(mig_assembly::structure_check::check_structure(
            &tree, pid_mig,
        ));
    }

    let validated_tree = build_validated_tree(workflow, &tree);
    let navigator = AssembledTreeNavigator::new(&tree);
    let validator = EdifactValidator::new(evaluator);
    let mut report = validator.validate_tree(
        &validated_tree,
        all_segments,
        external,
        level,
        Some(&navigator),
    );

    if let Some(issue) = validate_unt_segment_count(all_segments) {
        report.add_issue(issue);
    }

    for diag in structure_diagnostics {
        // A missing mandatory segment or group makes the message invalid; the
        // other structure findings describe content the conversion could
        // still place (or had to skip) and stay warnings.
        let severity = match diag.kind {
            StructureDiagnosticKind::MissingRequiredSegment
            | StructureDiagnosticKind::MissingRequiredGroup => Severity::Error,
            _ => Severity::Warning,
        };
        let kind = IssueKind::StructureDiagnostic {
            kind: diag.kind,
            segment_id: diag.segment_id.clone(),
            position: diag.position,
            detail: diag.message,
        };
        report.add_issue(ValidationIssue::new(severity, kind));
    }

    report
}