edifact-mapper 0.8.0

EDIFACT to BO4E bidirectional conversion for the German energy market
Documentation
//! `validate_pid_with_conditions` must not demand what the AHB leaves to the
//! sender (FV2604 UTILMD_Strom 55043, reported by mako-twin).
//!
//! * **`Soll [166]` on an absent group.** [166] is "Wenn vorhanden": send the
//!   group if you have it. The generated evaluator answers it `True`, so the
//!   absent SG8 SEQ+Z59 (`MarktlokationProduktDatenZ59`, `Soll [166]`) and SG5
//!   LOC+Z21 (`Tranche`, `Soll [166] ∧ [674]`) were reported as missing.
//! * **A sibling group's fields.** SG5 LOC+Z16 and LOC+Z22 both feed
//!   `Marktlokation`. With only Z16 data the entity exists, so the Z22 group's
//!   `X [950]` field `ruhendeMarktlokationsId` was demanded, though the Z22
//!   group (`Soll [2003]`) is absent.
//!
//! Skips if the bundle / fixture aren't present (both are build artifacts).

use std::path::PathBuf;

use edifact_mapper::{DataDir, Mapper};
use mig_bo4e::PidValidationError;
use serde_json::Value;

type Dynamic = mig_bo4e::model::Interchange<Value, Value>;

const FV: &str = "FV2604";
const VARIANT: &str = "UTILMD_Strom";
const PID: &str = "55043";

fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(|p| p.parent())
        .expect("crate is two levels under the workspace root")
        .to_path_buf()
}

/// The generated 55043 fixture's transaction stammdaten, with the metadata
/// entity merged back in.
fn load() -> Option<(Mapper, Value)> {
    let root = repo_root();
    let fixture = root.join("fixtures/generated/fv2604/utilmd/55043.edi");
    if !root.join(format!("dist/edifact-data-{FV}.bin")).exists() || !fixture.exists() {
        eprintln!("skipping: regenerate the {FV} bundle + fixture first");
        return None;
    }
    let mapper =
        Mapper::from_data_dir(DataDir::path(root.join("dist")).eager(&[FV])).expect("load bundle");
    let edifact = std::fs::read_to_string(&fixture).expect("read fixture");
    let ic: Dynamic = mapper
        .from_edifact(&edifact, FV, VARIANT, PID)
        .expect("convert fixture");
    let raw = ic.nachrichten[0].transaktionen[0].clone();
    let mut tx = raw["stammdaten"].clone();
    mig_bo4e::model::restore_entity(
        &mut tx,
        mig_bo4e::model::TX_METADATA_ENTITY,
        raw.get("transaktionsdaten").unwrap_or(&Value::Null),
    );
    Some((mapper, tx))
}

fn validate(mapper: &Mapper, tx: &Value) -> Vec<PidValidationError> {
    mapper
        .validate_pid_with_conditions(tx, FV, VARIANT, PID)
        .unwrap()
}

fn mentions(errors: &[PidValidationError], entity: &str, field: Option<&str>) -> Vec<String> {
    errors
        .iter()
        .filter(|e| match (e, field) {
            (PidValidationError::MissingEntity { entity: en, .. }, None) => en == entity,
            (
                PidValidationError::MissingField {
                    entity: en,
                    field: f,
                    ..
                },
                Some(want),
            ) => en == entity && f == want,
            _ => false,
        })
        .map(|e| e.to_string())
        .collect()
}

#[test]
fn an_absent_soll_166_group_is_not_missing() {
    let Some((mapper, mut tx)) = load() else {
        return;
    };
    let obj = tx.as_object_mut().unwrap();
    obj.remove("marktlokationProduktDatenZ59");
    obj.remove("tranche");

    let errors = validate(&mapper, &tx);
    for entity in ["MarktlokationProduktDatenZ59", "Tranche"] {
        let found = mentions(&errors, entity, None);
        assert!(found.is_empty(), "{entity}: {found:?}");
    }
}

#[test]
fn an_absent_ruhende_marktlokation_demands_none_of_its_fields() {
    let Some((mapper, mut tx)) = load() else {
        return;
    };
    let malo = tx["marktlokation"].as_object_mut().expect("Marktlokation");
    assert!(malo.contains_key("marktlokationsId"), "{malo:?}");
    malo.retain(|k, _| !k.starts_with("ruhendeMarktlokation"));

    let errors = validate(&mapper, &tx);
    let found = mentions(&errors, "Marktlokation", Some("ruhendeMarktlokationsId"));
    assert!(found.is_empty(), "{found:?}");
}

#[test]
fn a_filled_ruhende_marktlokation_still_demands_its_id() {
    let Some((mapper, mut tx)) = load() else {
        return;
    };
    let malo = tx["marktlokation"].as_object_mut().expect("Marktlokation");
    malo.remove("ruhendeMarktlokationsId");
    malo.insert("ruhendeMarktlokationZeitraumId".into(), "1".into());

    let errors = validate(&mapper, &tx);
    let found = mentions(&errors, "Marktlokation", Some("ruhendeMarktlokationsId"));
    assert_eq!(found.len(), 1, "{errors:#?}");
}