edifact-mapper 0.8.0

EDIFACT to BO4E bidirectional conversion for the German energy market
Documentation
//! IFTSTA PID 21039: each SG15 status variant (STS+Z37, STS+Z38) carries its own
//! free text (SG25 GID/FTX). BO4E holds no ordering, so the free text must be
//! nested in its status object — never paired with `status[]` by array position.
//! Reversing from JSON alone (`Mapper::to_edifact`, no `nesting_info`) with the
//! statuses swapped must still place each free text under its own STS.

use std::path::PathBuf;

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

fn repo_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("../..")
        .canonicalize()
        .unwrap()
}

/// The generated 21039 fixture with distinct free text nested under each STS.
fn input_with_distinct_free_text(fv: &str) -> String {
    let fixture = repo_root()
        .join("fixtures/generated")
        .join(fv.to_lowercase())
        .join("iftsta/21039.edi");
    let raw = std::fs::read_to_string(&fixture).unwrap();
    let mut out = Vec::new();
    let mut current_sts: Option<String> = None;
    let flush = |out: &mut Vec<String>, sts: &Option<String>| {
        if let Some(q) = sts {
            out.push(format!("GID+{}", &q[1..]));
            out.push(format!("FTX+ACB+++Text {q}"));
        }
    };
    for segment in raw.split('\'').map(str::trim).filter(|s| !s.is_empty()) {
        if segment.starts_with("GID+") || segment.starts_with("FTX+") {
            continue;
        }
        if segment.starts_with("STS+") || segment.starts_with("UNT+") {
            flush(&mut out, &current_sts);
            current_sts = segment
                .strip_prefix("STS+")
                .map(|rest| rest.split('+').next().unwrap().to_string());
        }
        out.push(segment.to_string());
    }
    out.iter().map(|s| format!("{s}'")).collect()
}

fn position(haystack: &str, needle: &str) -> usize {
    haystack
        .find(needle)
        .unwrap_or_else(|| panic!("`{needle}` missing in:\n{haystack}"))
}

#[test]
fn free_text_stays_with_its_status_when_statuses_are_swapped() {
    let dist = repo_root().join("dist");
    for fv in ["FV2504", "FV2510", "FV2604", "FV2610"] {
        let mapper = Mapper::from_data_dir(DataDir::path(&dist).eager(&[fv])).unwrap();
        let edifact = input_with_distinct_free_text(fv);
        let ic = mapper
            .from_edifact::<Value, Value>(&edifact, fv, "IFTSTA", "21039")
            .unwrap_or_else(|e| panic!("{fv}: forward failed: {e}"));
        let nachricht = &ic.nachrichten[0];
        // `from_edifact` hands a transaction out as `{stammdaten,
        // transaktionsdaten}`; the entity map this test reads and re-renders is
        // the `stammdaten` half, with the metadata entity merged back in so the
        // reverse can still rebuild the segments it feeds.
        let mut txs: Vec<Value> = nachricht
            .transaktionen
            .iter()
            .map(|t| match t.get("stammdaten") {
                Some(st) => {
                    let mut out = st.clone();
                    mig_bo4e::model::restore_entity(
                        &mut out,
                        mig_bo4e::model::TX_METADATA_ENTITY,
                        t.get("transaktionsdaten").unwrap_or(&Value::Null),
                    );
                    out
                }
                None => t.clone(),
            })
            .collect();
        assert_eq!(txs.len(), 1, "{fv}");

        let status = txs[0]
            .get_mut("status")
            .and_then(Value::as_array_mut)
            .unwrap_or_else(|| panic!("{fv}: status array: {}", nachricht.transaktionen[0]));
        assert_eq!(status.len(), 2, "{fv}");
        for s in status.iter() {
            assert!(
                s.get("freitext").is_some_and(Value::is_array),
                "{fv}: free text must be nested in its status: {s}"
            );
        }
        assert!(
            txs[0].get("freitext").is_none(),
            "{fv}: no top-level free text paired by position: {}",
            txs[0]
        );
        txs[0]["status"].as_array_mut().unwrap().reverse();

        let rendered = mapper
            .to_edifact(&nachricht.stammdaten, &txs, fv, "IFTSTA", "21039")
            .unwrap_or_else(|e| panic!("{fv}: reverse failed: {e}"));
        let z37 = position(&rendered, "STS+Z37");
        let text_z37 = position(&rendered, "Text Z37");
        let z38 = position(&rendered, "STS+Z38");
        let text_z38 = position(&rendered, "Text Z38");
        assert!(
            z37 < text_z37 && text_z37 < z38 && z38 < text_z38,
            "{fv}: each free text must follow its own STS:\n{rendered}"
        );
    }
}