edifact-mapper 0.8.0

EDIFACT to BO4E bidirectional conversion for the German energy market
Documentation
//! A distributed bundle must carry the tables its rules name.
//!
//! `VariantCache` is written beside a copy of `code_lists.toml` and repairs
//! itself from it on load. A bundle has no such neighbour: `edifact-data`
//! fetches a bare `edifact-data-<FV>.bin` into `~/.edifact/data`. A bundle that
//! does not carry its tables resolves no name at all — forward, the EDIFACT
//! code reaches the output untranslated; reverse, the BO4E name is written into
//! the EDIFACT slot verbatim.
//!
//! This went unnoticed because `DataBundle::load` calls the *correct*
//! constructor, just with an empty table set, so the debug assertion that
//! guards the wrong constructor could not see it. Every `edifact-mapper`
//! library consumer was affected; the API and the Docker image were not,
//! because they read `mappings/` and `cache/mappings/`, which do have the file.

use std::path::PathBuf;

use mig_bo4e::definition::FieldMapping;
use mig_bo4e::engine::DataBundle;

fn bundle_path(fv: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("../../dist")
        .join(format!("edifact-data-{fv}.bin"))
}

#[test]
fn a_bundle_whose_rules_name_a_code_list_carries_that_list() {
    for fv in ["FV2504", "FV2510", "FV2604", "FV2610"] {
        let path = bundle_path(fv);
        if !path.exists() {
            eprintln!("skipping {fv}: no bundle (build artifact)");
            continue;
        }
        let bundle = DataBundle::load(&path).unwrap_or_else(|e| panic!("{fv}: {e}"));

        let mut named: Vec<String> = Vec::new();
        for variant in bundle.variants.values() {
            for defs in std::iter::once(&variant.message_defs)
                .chain(variant.transaction_defs.values())
                .chain(variant.combined_defs.values())
            {
                for def in defs {
                    for mapping in def.fields.values() {
                        if let FieldMapping::Structured(s) = mapping {
                            named.extend(s.code_list.clone());
                            named.extend(s.also_code_list.clone());
                        }
                    }
                }
            }
        }
        if named.is_empty() {
            continue; // this format version names none yet
        }

        assert!(
            !bundle.code_lists.is_empty(),
            "{fv}: {} rule position(s) name a shared code list, but the bundle \
             carries no tables — every one of them reaches the output raw",
            named.len()
        );

        // and the names have to resolve, not merely be accompanied by *some* tables
        let missing: Vec<&String> = named
            .iter()
            .filter(|n| bundle.code_lists.get(n).is_none())
            .collect();
        assert!(
            missing.is_empty(),
            "{fv}: {} named table(s) are absent from the bundle: {:?}",
            missing.len(),
            missing.iter().take(5).collect::<Vec<_>>()
        );

        // and every variant's engines must be handed them
        for (name, variant) in &bundle.variants {
            assert!(
                !variant.code_lists.is_empty(),
                "{fv}/{name}: the bundle carries tables but this variant was \
                 not given them, so its engines resolve nothing"
            );
        }
    }
}