edifact-mapper 0.30.5

EDIFACT to BO4E bidirectional conversion for the German energy market
Documentation
//! An interchange crosses the API as bytes in the character set its `UNB`
//! declares. `UNB+UNOC:3` is ISO 8859-1: `ü` is the one byte `0xFC`, and a
//! `String` (UTF-8) carrying it under that header would be two.

use edifact_mapper::charset::{encode_interchange, CharsetError};
use edifact_mapper::{
    DataDir, EdifactParty, InterchangeEnvelope, InterchangeMessage, Mapper, MapperError,
};
use std::path::PathBuf;

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

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

fn mapper() -> Option<Mapper> {
    Mapper::from_data_dir(DataDir::path(repo_root().join("dist"))).ok()
}

/// A generated UTILMD sample, with a name containing `ü` put into its first NAD.
fn sample_with_umlaut() -> Option<String> {
    let edi =
        std::fs::read_to_string(repo_root().join("fixtures/generated/fv2504/utilmd/55001.edi"))
            .ok()?;
    let mut out = String::new();
    let mut done = false;
    for seg in edi.split_inclusive('\'') {
        if !done && seg.starts_with("NAD+Z") {
            let mut parts: Vec<String> = seg
                .trim_end_matches('\'')
                .split('+')
                .map(String::from)
                .collect();
            if parts.len() > 4 {
                let mut name: Vec<String> = parts[4].split(':').map(String::from).collect();
                name[0] = "Müller".to_string();
                parts[4] = name.join(":");
                out.push_str(&parts.join("+"));
                out.push('\'');
                done = true;
                continue;
            }
        }
        out.push_str(seg);
    }
    done.then_some(out)
}

fn render_bytes(mapper: &Mapper, ic: &Dynamic) -> Result<Vec<u8>, MapperError> {
    let msg = &ic.nachrichten[0];
    mapper.to_edifact_interchange_bytes(
        &InterchangeEnvelope {
            sender: EdifactParty::bdew("9900000000001"),
            receiver: EdifactParty::bdew("9900000000002"),
            interchange_ref: "1".to_string(),
        },
        &[InterchangeMessage {
            message_ref: "1".to_string(),
            msg_stammdaten: msg.stammdaten.clone(),
            tx_stammdaten: msg.transaktionen.clone(),
            fv: "FV2504".to_string(),
            variant: "UTILMD_Strom".to_string(),
            pid: "55001".to_string(),
            ..Default::default()
        }],
    )
}

#[test]
fn latin1_bytes_convert_and_render_back_as_latin1() {
    let (Some(mapper), Some(text)) = (mapper(), sample_with_umlaut()) else {
        eprintln!("skipping: bundle or sample missing");
        return;
    };
    assert!(text.contains("UNB+UNOC:3"));
    let latin1 = encode_interchange(&text).expect("the sample fits ISO 8859-1");
    assert!(
        latin1.windows(2).any(|w| w == [b'M', 0xFC]),
        "ü is the byte 0xFC"
    );

    let ic: Dynamic = mapper
        .from_edifact_bytes(&latin1, "FV2504", "UTILMD_Strom", "55001")
        .expect("a UNOC interchange given as bytes converts");
    let json = serde_json::to_string(&ic).unwrap();
    assert!(json.contains("Müller"), "the umlaut is read as itself");

    let bytes = render_bytes(&mapper, &ic).expect("renders");
    assert!(
        bytes.windows(2).any(|w| w == [b'M', 0xFC]),
        "rendered as ISO 8859-1"
    );
    assert!(
        std::str::from_utf8(&bytes).is_err(),
        "not UTF-8 under a UNOC header"
    );
}

#[test]
fn a_character_outside_latin1_is_refused_not_sent() {
    let Some(mapper) = mapper() else {
        return;
    };
    let Some(text) = sample_with_umlaut() else {
        return;
    };
    let ic: Dynamic = mapper
        .from_edifact(
            &text.replace("Müller", "Müller €"),
            "FV2504",
            "UTILMD_Strom",
            "55001",
        )
        .expect("converts");
    let err = render_bytes(&mapper, &ic).unwrap_err();
    assert!(
        matches!(
            err,
            MapperError::Charset(CharsetError::Unrepresentable {
                character: '€', ..
            })
        ),
        "{err}"
    );
}