automapper-validation 0.20.0

AHB condition expression parsing, evaluation, and EDIFACT validation
Documentation
//! Conditions outside UTILMD that compare this group instance with others:
//! IFTSTA "nicht identisch mit dem SG15 RFF+ACW der SG15 STS+Z27", MSCONS "der
//! früheste angegebene Zeitpunkt" among the SG9 with the same PIA+5, PRICAT
//! Artikel-ID sequences and price zones, UTILTS Zählzeiten. Each case asks
//! from sibling instances where the answer differs.

use std::collections::BTreeMap;

use automapper_validation::eval::{
    ConditionResult, EvaluationContext, GroupScope, MapExternalProvider,
};
use automapper_validation::evaluator_factory::evaluator_for;
use mig_assembly::assembler::{
    AssembledGroup, AssembledGroupInstance, AssembledSegment, AssembledTree,
};
use mig_assembly::navigator::AssembledTreeNavigator;
use mig_types::segment::OwnedSegment;

use ConditionResult::{False, True, Unknown};

/// A segment from its EDIFACT text: `STS+Z20+Z32+A99:E_0278`.
fn seg(text: &str) -> AssembledSegment {
    let mut parts = text.split('+');
    let tag = parts.next().unwrap().to_string();
    AssembledSegment {
        tag,
        elements: parts
            .map(|e| e.split(':').map(str::to_string).collect())
            .collect(),
        mig_number: None,
        segment_number: None,
    }
}

fn inst(segments: &[&str], child_groups: Vec<AssembledGroup>) -> AssembledGroupInstance {
    AssembledGroupInstance {
        segments: segments.iter().map(|s| seg(s)).collect(),
        child_groups,
        entry_mig_number: None,
        variant_mig_numbers: vec![],
        skipped_segments: vec![],
        skipped_positions: Vec::new(),
    }
}

fn grp(id: &str, repetitions: Vec<AssembledGroupInstance>) -> AssembledGroup {
    AssembledGroup {
        group_id: id.to_string(),
        repetitions,
    }
}

fn tree(groups: Vec<AssembledGroup>) -> AssembledTree {
    AssembledTree {
        segments: vec![],
        groups,
        post_group_start: 0,
        inter_group_segments: BTreeMap::new(),
    }
}

fn flat(tree: &AssembledTree) -> Vec<OwnedSegment> {
    fn walk(groups: &[AssembledGroup], out: &mut Vec<OwnedSegment>) {
        for group in groups {
            for instance in &group.repetitions {
                for s in &instance.segments {
                    out.push(OwnedSegment {
                        id: s.tag.clone(),
                        elements: s.elements.clone(),
                        segment_number: out.len() as u32,
                    });
                }
                walk(&instance.child_groups, out);
            }
        }
    }
    let mut out = Vec::new();
    walk(&tree.groups, &mut out);
    out
}

type Case = (u32, &'static [(&'static str, usize)], ConditionResult);

/// Runs `cases` for `variant` in every format version of `fvs` against `tree`.
fn check(
    variant: &str,
    fvs: &[&str],
    tree: &AssembledTree,
    cases: &[Case],
    wrong: &mut Vec<String>,
) {
    let navigator = AssembledTreeNavigator::new(tree);
    let external = MapExternalProvider::new(Default::default());
    let segments = flat(tree);
    let ctx = EvaluationContext::with_navigator("00000", &external, &segments, &navigator);
    for fv in fvs {
        let evaluator = evaluator_for(variant, fv).unwrap_or_else(|| panic!("{variant} {fv}"));
        for &(condition, path, expected) in cases {
            let got = evaluator.evaluate(condition, &ctx.with_scope(GroupScope { path }));
            if got != expected {
                wrong.push(format!(
                    "{variant} {fv} [{condition}] at {path:?}: {got:?}, expected {expected:?}"
                ));
            }
        }
    }
}

#[test]
fn each_instance_is_compared_with_its_siblings() {
    let mut wrong = Vec::new();

    // IFTSTA: the SG15 STS+Z27..Z30 of this SG14 carry the reference ACWs.
    let sg15 = |sts: &str, acw: &str| inst(&[sts, acw], vec![]);
    let iftsta = tree(vec![grp(
        "SG14",
        vec![
            inst(
                &["CNI+1"],
                vec![grp(
                    "SG15",
                    vec![
                        sg15("STS+Z27", "RFF+ACW:A1"),
                        sg15("STS+Z28", "RFF+ACW:A2"),
                        sg15("STS+Z29", "RFF+ACW:A1"),
                        sg15("STS+Z30", "RFF+ACW:A3"),
                        sg15("STS+Z20", "RFF+ACW:A1"),
                        sg15("STS+Z20", "RFF+ACW:A9"),
                    ],
                )],
            ),
            inst(
                &["CNI+2"],
                vec![grp("SG15", vec![sg15("STS+Z20", "RFF+ACW:A9")])],
            ),
        ],
    )]);
    let at = |i: usize, j: usize| -> &'static [(&'static str, usize)] {
        Box::leak(vec![("SG14", i), ("SG15", j)].into_boxed_slice())
    };
    check(
        "IFTSTA",
        &["FV2504", "FV2510"],
        &iftsta,
        &[
            (56, at(0, 4), False),
            (56, at(0, 5), True),
            (56, at(1, 0), Unknown),
            (57, at(0, 4), True),
            (57, at(0, 1), False),
            (58, at(0, 4), False),
            (58, at(0, 5), True),
            (59, at(0, 4), True),
            (59, at(0, 3), False),
        ],
        &mut wrong,
    );

    // MSCONS: SG6 LOC+172 / SG9 LIN+PIA+5 / SG10 QTY+DTM+163/164.
    let sg10 = |dtms: &[&str]| {
        let mut segs = vec!["QTY+220:1"];
        segs.extend_from_slice(dtms);
        inst(&segs, vec![])
    };
    let mscons = tree(vec![grp(
        "SG5",
        vec![inst(
            &["NAD+DP"],
            vec![grp(
                "SG6",
                vec![inst(
                    &["LOC+172+DE0001"],
                    vec![grp(
                        "SG9",
                        vec![
                            inst(
                                &["LIN+1", "PIA+5+OBIS1"],
                                vec![grp(
                                    "SG10",
                                    vec![
                                        sg10(&["DTM+163:20250101:102", "DTM+164:20250102:102"]),
                                        sg10(&["DTM+163:20250102:102", "DTM+164:20250103:102"]),
                                    ],
                                )],
                            ),
                            inst(
                                &["LIN+2", "PIA+5+OBIS1"],
                                vec![grp(
                                    "SG10",
                                    vec![sg10(&["DTM+163:20241231:102", "DTM+164:20250105:102"])],
                                )],
                            ),
                            inst(
                                &["LIN+3", "PIA+5+OBIS2"],
                                vec![grp(
                                    "SG10",
                                    vec![sg10(&["DTM+163:20240101:102", "DTM+164:20260101:102"])],
                                )],
                            ),
                        ],
                    )],
                )],
            )],
        )],
    )]);
    let m = |i: usize, j: usize| -> &'static [(&'static str, usize)] {
        Box::leak(vec![("SG5", 0), ("SG6", 0), ("SG9", i), ("SG10", j)].into_boxed_slice())
    };
    check(
        "MSCONS",
        &["FV2504", "FV2510", "FV2604"],
        &mscons,
        &[
            (87, m(1, 0), True),
            (87, m(0, 0), False),
            (87, m(2, 0), True),
            (88, m(1, 0), True),
            (88, m(0, 1), False),
            (88, m(2, 0), True),
        ],
        &mut wrong,
    );

    // PRICAT: SG17 PGI / SG36 LIN (Artikel-ID) / SG40 PRI+RNG (zones).
    let zone = |rng: &str| inst(&["PRI+CAL:1", rng], vec![]);
    let article =
        |lin: &str, zones: Vec<AssembledGroupInstance>| inst(&[lin], vec![grp("SG40", zones)]);
    let pricat = tree(vec![grp(
        "SG17",
        vec![
            inst(
                &["PGI+9"],
                vec![grp(
                    "SG36",
                    vec![
                        article(
                            "LIN+1++A-01:Z12",
                            vec![zone("RNG+10+KWH:0:100"), zone("RNG+10+KWH:100:200")],
                        ),
                        article("LIN+2++A-02:Z12", vec![zone("RNG+10+KWH:0:150")]),
                        article("LIN+3++A-03:Z12", vec![zone("RNG+10+KWH:150:300")]),
                        article("LIN+4++A-04:Z12", vec![zone("RNG+10+KWH:999:1000")]),
                    ],
                )],
            ),
            inst(
                &["PGI+9"],
                vec![grp(
                    "SG36",
                    vec![article("LIN+1++B-01:Z12", vec![zone("RNG+10+KWH:0:10")])],
                )],
            ),
        ],
    )]);
    let p = |g: usize, a: usize, z: usize| -> &'static [(&'static str, usize)] {
        Box::leak(vec![("SG17", g), ("SG36", a), ("SG40", z)].into_boxed_slice())
    };
    check(
        "PRICAT",
        &["FV2510"],
        &pricat,
        &[
            (10, p(0, 0, 0), True),
            (10, p(0, 3, 0), False),
            (10, p(1, 0, 0), False),
            (55, p(0, 0, 0), True),
            (55, p(0, 0, 1), False),
            (66, p(0, 1, 0), True),
            (66, p(0, 2, 0), True),
            (66, p(0, 3, 0), False),
            (67, p(0, 0, 0), True),
            (67, p(0, 3, 0), False),
            (72, p(0, 2, 0), True),
            (72, p(0, 3, 0), False),
            (72, p(0, 0, 0), True),
        ],
        &mut wrong,
    );

    // UTILTS: SG5 / SG8 Zählzeitdefinition (SEQ+Z42, DTM+Z33, SG9 CCI+Z39)
    // and register SG8 (RFF+Z27).
    let utilts = tree(vec![grp(
        "SG5",
        vec![inst(
            &["IDE+24+1"],
            vec![grp(
                "SG8",
                vec![
                    inst(
                        &["SEQ+Z42", "DTM+Z33:202501010000:303"],
                        vec![grp("SG9", vec![inst(&["CCI+Z39++ZZ1"], vec![])])],
                    ),
                    inst(&["SEQ+Z42", "DTM+Z33:202501010000:303"], vec![]),
                    inst(&["SEQ+Z42", "DTM+Z33:202502010000:303"], vec![]),
                    inst(&["SEQ+Z43", "RFF+Z27:ZZ1"], vec![]),
                    inst(&["SEQ+Z43", "RFF+Z27:ZZ1"], vec![]),
                    inst(&["SEQ+Z43", "RFF+Z27:ZZ2"], vec![]),
                ],
            )],
        )],
    )]);
    let u = |i: usize| -> &'static [(&'static str, usize)] {
        Box::leak(vec![("SG5", 0), ("SG8", i)].into_boxed_slice())
    };
    check(
        "UTILTS",
        &["FV2504"],
        &utilts,
        &[
            (511, u(0), False),
            (511, u(2), True),
            (2002, u(3), True),
            (2002, u(5), False),
        ],
        &mut wrong,
    );

    assert!(wrong.is_empty(), "{wrong:#?}");
}