automapper-validation 0.5.0

AHB condition expression parsing, evaluation, and EDIFACT validation
Documentation
//! Verifies [108] is scope-aware after Task B7 narrow migration.
//!
//! Pre-B7: ctx.has_qualifier(PIA, 0, Z02) checked message-wide.
//! Post-B7: ctx.scoped_find_segments_with_qualifier narrows to the
//! current SG8 instance via the GroupNavigator (when scope is set).

use automapper_validation::eval::{
    ConditionEvaluator, ConditionResult, EvaluationContext, GroupScope, MapExternalProvider,
};
use automapper_validation::generated::fv2510::UtilmdStromConditionEvaluatorFV2510;
use mig_types::navigator::GroupNavigator;
use mig_types::segment::OwnedSegment;

struct TwoSg8Nav {
    pias_per_instance: Vec<Vec<OwnedSegment>>,
}
impl GroupNavigator for TwoSg8Nav {
    fn find_segments_in_group(&self, tag: &str, _: &[&str], idx: usize) -> Vec<OwnedSegment> {
        self.pias_per_instance
            .get(idx)
            .cloned()
            .unwrap_or_default()
            .into_iter()
            .filter(|s| s.id == tag)
            .collect()
    }
    fn find_segments_with_qualifier_in_group(
        &self,
        tag: &str,
        elem: usize,
        qual: &str,
        _: &[&str],
        idx: usize,
    ) -> Vec<OwnedSegment> {
        self.pias_per_instance
            .get(idx)
            .cloned()
            .unwrap_or_default()
            .into_iter()
            .filter(|s| {
                s.id == tag
                    && s.elements
                        .get(elem)
                        .and_then(|e| e.first())
                        .is_some_and(|v| v == qual)
            })
            .collect()
    }
    fn group_instance_count(&self, _: &[&str]) -> usize {
        self.pias_per_instance.len()
    }
}

fn pia(qual: &str, code: &str) -> OwnedSegment {
    OwnedSegment {
        id: "PIA".to_string(),
        segment_number: 0,
        elements: vec![vec![qual.to_string()], vec![code.to_string()]],
    }
}

#[test]
fn evaluate_108_true_when_sg8_has_pia_z02_in_scope() {
    let nav = TwoSg8Nav {
        pias_per_instance: vec![
            vec![pia("Z02", "1-08-1-001")], // SG8[0] has PIA+Z02
            vec![pia("5", "other")],        // SG8[1] has PIA+5 (not Z02)
        ],
    };
    let external = MapExternalProvider::new(Default::default());
    let segments = vec![];
    let ctx = EvaluationContext::with_navigator("55218", &external, &segments, &nav);
    let path = ["SG4", "SG8"];
    let eval = UtilmdStromConditionEvaluatorFV2510::default();

    let ctx0 = ctx.with_scope(GroupScope {
        group_path: &path,
        instance_index: 0,
    });
    assert_eq!(
        eval.evaluate(108, &ctx0),
        ConditionResult::True,
        "SG8[0] has PIA+Z02"
    );

    let ctx1 = ctx.with_scope(GroupScope {
        group_path: &path,
        instance_index: 1,
    });
    assert_eq!(
        eval.evaluate(108, &ctx1),
        ConditionResult::False,
        "SG8[1] has no PIA+Z02"
    );
}

#[test]
fn evaluate_108_falls_back_to_message_wide_without_scope() {
    let external = MapExternalProvider::new(Default::default());
    let segments = vec![pia("Z02", "1-08-1-001")];
    let ctx = EvaluationContext::new("55218", &external, &segments);
    let eval = UtilmdStromConditionEvaluatorFV2510::default();
    assert_eq!(eval.evaluate(108, &ctx), ConditionResult::True);
}

#[test]
fn evaluate_108_requires_1_08_x_code_prefix() {
    // 1-02-0-001 is NOT in the 1-08-x range — should evaluate False.
    let external = MapExternalProvider::new(Default::default());
    let segments = vec![pia("Z02", "1-02-0-001")];
    let ctx = EvaluationContext::new("55218", &external, &segments);
    let eval = UtilmdStromConditionEvaluatorFV2510::default();
    assert_eq!(
        eval.evaluate(108, &ctx),
        ConditionResult::False,
        "[108] requires PIA+Z02 with code starting with 1-08-x"
    );
}