mig-bo4e 0.1.46

Declarative TOML-based MIG-tree to BO4E mapping engine
Documentation
//! Tests for the typed API surface (map_interchange_typed / map_interchange_reverse_typed).

use mig_bo4e::model::{
    DynamicInterchange, Interchange, Interchangedaten, Nachricht, Nachrichtendaten,
};
use mig_bo4e::MappingEngine;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TestMsg {
    marktteilnehmer: Option<Vec<serde_json::Value>>,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TestTx {
    marktlokation: Option<serde_json::Value>,
    prozessdaten: Option<serde_json::Value>,
}

#[test]
fn test_typed_forward_deserializes_from_dynamic() {
    // Verify that a DynamicInterchange can be round-tripped through JSON
    // into a typed Interchange<TestMsg, TestTx>.
    let dynamic = DynamicInterchange {
        interchangedaten: Interchangedaten {
            absender_code: Some("9900123".into()),
            ..Default::default()
        },
        nachrichten: vec![Nachricht {
            nachrichtendaten: Nachrichtendaten {
                unh_referenz: "00001".into(),
                nachrichten_typ: "UTILMD".into(),
            },
            stammdaten: serde_json::json!({
                "marktteilnehmer": [{"rollencodenummer": "9900123"}]
            }),
            transaktionen: vec![serde_json::json!({
                "marktlokation": {"marktlokationsId": "DE000111"},
                "prozessdaten": {"transaktionsgrund": "E01"}
            })],
        }],
    };

    let value = serde_json::to_value(&dynamic).unwrap();
    let typed: Interchange<TestMsg, TestTx> = serde_json::from_value(value).unwrap();

    assert_eq!(
        typed.interchangedaten.absender_code.as_deref(),
        Some("9900123")
    );
    assert_eq!(typed.nachrichten.len(), 1);
    let msg = &typed.nachrichten[0];
    assert_eq!(msg.nachrichtendaten.unh_referenz, "00001");
    assert_eq!(msg.nachrichtendaten.nachrichten_typ, "UTILMD");

    // Verify typed stammdaten fields
    let teilnehmer = msg.stammdaten.marktteilnehmer.as_ref().unwrap();
    assert_eq!(teilnehmer.len(), 1);
    assert_eq!(
        teilnehmer[0]["rollencodenummer"].as_str().unwrap(),
        "9900123"
    );

    // Verify typed transaction fields
    assert_eq!(msg.transaktionen.len(), 1);
    let tx = &msg.transaktionen[0];
    assert!(tx.marktlokation.is_some());
    assert!(tx.prozessdaten.is_some());
    assert_eq!(
        tx.marktlokation.as_ref().unwrap()["marktlokationsId"]
            .as_str()
            .unwrap(),
        "DE000111"
    );
}

#[test]
fn test_typed_reverse_serializes_to_dynamic() {
    // Build a typed Nachricht, serialize to JSON, then parse as MappedMessage fields.
    let nachricht: Nachricht<TestMsg, TestTx> = Nachricht {
        nachrichtendaten: Nachrichtendaten {
            unh_referenz: "00002".into(),
            nachrichten_typ: "UTILMD".into(),
        },
        stammdaten: TestMsg {
            marktteilnehmer: Some(vec![serde_json::json!({"rollencodenummer": "9900456"})]),
        },
        transaktionen: vec![TestTx {
            marktlokation: Some(serde_json::json!({"marktlokationsId": "DE000222"})),
            prozessdaten: Some(serde_json::json!({"transaktionsgrund": "E03"})),
        }],
    };

    // Verify the stammdaten serializes correctly
    let stammdaten_value = serde_json::to_value(&nachricht.stammdaten).unwrap();
    assert!(stammdaten_value["marktteilnehmer"].is_array());

    // Verify each transaction serializes correctly
    let tx_value = serde_json::to_value(&nachricht.transaktionen[0]).unwrap();
    assert_eq!(
        tx_value["marktlokation"]["marktlokationsId"]
            .as_str()
            .unwrap(),
        "DE000222"
    );
    assert_eq!(
        tx_value["prozessdaten"]["transaktionsgrund"]
            .as_str()
            .unwrap(),
        "E03"
    );
}

#[test]
fn test_typed_interchange_full_roundtrip_via_json() {
    // Build typed -> serialize to JSON -> deserialize back to typed
    let original: Interchange<TestMsg, TestTx> = Interchange {
        interchangedaten: Interchangedaten {
            absender_code: Some("9900111".into()),
            empfaenger_code: Some("9900222".into()),
            ..Default::default()
        },
        nachrichten: vec![Nachricht {
            nachrichtendaten: Nachrichtendaten {
                unh_referenz: "00003".into(),
                nachrichten_typ: "UTILMD".into(),
            },
            stammdaten: TestMsg {
                marktteilnehmer: Some(vec![serde_json::json!({"marktrolle": "LF"})]),
            },
            transaktionen: vec![
                TestTx {
                    marktlokation: Some(serde_json::json!({"marktlokationsId": "DE000AAA"})),
                    prozessdaten: Some(serde_json::json!({"transaktionsgrund": "E01"})),
                },
                TestTx {
                    marktlokation: Some(serde_json::json!({"marktlokationsId": "DE000BBB"})),
                    prozessdaten: None,
                },
            ],
        }],
    };

    let json = serde_json::to_string(&original).unwrap();
    let restored: Interchange<TestMsg, TestTx> = serde_json::from_str(&json).unwrap();

    assert_eq!(
        restored.interchangedaten.absender_code.as_deref(),
        Some("9900111")
    );
    assert_eq!(restored.nachrichten[0].transaktionen.len(), 2);
    assert!(restored.nachrichten[0].transaktionen[1]
        .prozessdaten
        .is_none());
    assert_eq!(
        restored.nachrichten[0].transaktionen[0]
            .marktlokation
            .as_ref()
            .unwrap()["marktlokationsId"]
            .as_str()
            .unwrap(),
        "DE000AAA"
    );
}

#[test]
fn test_map_interchange_typed_method_exists() {
    // Verify the typed methods compile and have the expected signatures.
    // We can't easily call them without a full MIG setup, but we can verify
    // the type inference works by constructing the call with empty engines.
    use mig_assembly::assembler::AssembledTree;
    use std::collections::BTreeMap;

    let tree = AssembledTree {
        segments: vec![],
        groups: vec![],
        post_group_start: 0,
        inter_group_segments: BTreeMap::new(),
    };

    // Load engines from a non-existent dir would fail, so we create minimal ones.
    // The point is that the generic method compiles with our test types.
    let dir = tempfile::tempdir().unwrap();
    let msg_engine = MappingEngine::load(dir.path()).unwrap();
    let tx_engine = MappingEngine::load(dir.path()).unwrap();

    let result = MappingEngine::map_interchange_typed::<TestMsg, TestTx>(
        &msg_engine,
        &tx_engine,
        &tree,
        "SG4",
        false,
        Nachrichtendaten {
            unh_referenz: "00001".into(),
            nachrichten_typ: "UTILMD".into(),
        },
        Interchangedaten::default(),
    );

    // With empty engines and empty tree, forward mapping produces empty stammdaten.
    // The typed deserialization should still succeed because all fields are Option.
    let interchange = result.unwrap();
    assert_eq!(interchange.nachrichten.len(), 1);
    assert!(interchange.nachrichten[0]
        .stammdaten
        .marktteilnehmer
        .is_none());
    assert!(interchange.nachrichten[0].transaktionen.is_empty());
}

#[test]
fn test_map_interchange_reverse_typed_method_exists() {
    // Verify the reverse typed method compiles and produces an AssembledTree.
    let nachricht: Nachricht<TestMsg, TestTx> = Nachricht {
        nachrichtendaten: Nachrichtendaten {
            unh_referenz: "00001".into(),
            nachrichten_typ: "UTILMD".into(),
        },
        stammdaten: TestMsg {
            marktteilnehmer: Some(vec![serde_json::json!({"rollencodenummer": "9900123"})]),
        },
        transaktionen: vec![TestTx {
            marktlokation: Some(serde_json::json!({"marktlokationsId": "DE000111"})),
            prozessdaten: Some(serde_json::json!({"transaktionsgrund": "E01"})),
        }],
    };

    let dir = tempfile::tempdir().unwrap();
    let msg_engine = MappingEngine::load(dir.path()).unwrap();
    let tx_engine = MappingEngine::load(dir.path()).unwrap();

    let tree =
        MappingEngine::map_interchange_reverse_typed(&msg_engine, &tx_engine, &nachricht, "SG4")
            .unwrap();

    // With empty engines, reverse still creates an SG4 group wrapper for each transaction,
    // but the instances will have no segments (no definitions to process).
    assert!(tree.segments.is_empty());
    // The tree should have produced an SG4 group with one empty rep (from the one transaction).
    let sg4 = tree.groups.iter().find(|g| g.group_id == "SG4");
    assert!(sg4.is_some(), "Expected SG4 group from the one transaction");
    assert_eq!(sg4.unwrap().repetitions.len(), 1);
}