edifact-mapper 0.17.0

EDIFACT to BO4E bidirectional conversion for the German energy market
Documentation
//! The structure check reports each MIG violation under its own code (#81).
//!
//! The assembler only ever told "unexpected" and "skipped" segments apart,
//! and every finding but a skip came out as STR003. A missing mandatory
//! segment or group, and a segment or group repeating beyond its `MaxRep`,
//! were not detected at all, so STR001 / STR002 / STR005 / STR006 — and the
//! CONTRL codes 13 and 16 behind them — never occurred.

use edifact_mapper::{DataDir, Mapper, ValidationLevel};
use std::path::PathBuf;

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

fn setup() -> Option<(Mapper, Vec<String>)> {
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .and_then(|p| p.parent())
        .unwrap()
        .to_path_buf();
    if !root.join(format!("dist/edifact-data-{FV}.bin")).exists() {
        eprintln!("skipping: build the {FV} bundle 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(root.join(format!("fixtures/generated/fv2604/utilmd/{PID}.edi")))
            .expect("fixture");
    let segments = edifact
        .split('\'')
        .map(str::trim)
        .filter(|s| !s.is_empty())
        .map(str::to_string)
        .collect();
    Some((mapper, segments))
}

/// The segment list as a message, with a correct UNT count.
fn message(mut segments: Vec<String>) -> String {
    let unh = segments.iter().position(|s| s.starts_with("UNH")).unwrap();
    let unt = segments.iter().position(|s| s.starts_with("UNT")).unwrap();
    let reference = segments[unt].rsplit('+').next().unwrap().to_string();
    segments[unt] = format!("UNT+{}+{reference}", unt - unh + 1);
    segments.join("'") + "'"
}

/// `(code, message, severity)` of every structure finding the check adds.
fn structure_codes(mapper: &Mapper, edifact: &str) -> Vec<(String, String, String)> {
    let report = mapper
        .validate_edifact_for_pid(edifact, FV, VARIANT, PID, ValidationLevel::Full)
        .expect("validate");
    report
        .issues
        .iter()
        .filter(|i| matches!(i.code(), "STR001" | "STR002" | "STR005" | "STR006"))
        .map(|i| {
            let detail = match &i.kind {
                automapper_validation::IssueKind::StructureDiagnostic { detail, .. } => {
                    detail.clone()
                }
                _ => String::new(),
            };
            (i.code().to_string(), detail, format!("{:?}", i.severity))
        })
        .collect()
}

#[test]
fn a_well_formed_sample_has_no_structure_findings() {
    let Some((mapper, segments)) = setup() else {
        return;
    };
    let found = structure_codes(&mapper, &message(segments));
    assert!(found.is_empty(), "{found:?}");
}

#[test]
fn a_second_sender_is_a_group_repeating_too_often() {
    let Some((mapper, mut segments)) = setup() else {
        return;
    };
    // SG2 NAD+MS: MaxRep 1.
    let ms = segments
        .iter()
        .position(|s| s.starts_with("NAD+MS"))
        .expect("NAD+MS");
    let copy = segments[ms].clone();
    segments.insert(ms + 1, copy);
    let found = structure_codes(&mapper, &message(segments));
    assert!(
        found.iter().any(|(code, detail, severity)| code == "STR006"
            && detail.contains("NAD+MS")
            && severity == "Warning"),
        "{found:?}"
    );
}

#[test]
fn a_missing_recipient_is_a_missing_mandatory_group() {
    let Some((mapper, mut segments)) = setup() else {
        return;
    };
    // SG2 NAD+MR: status R. Drop it with its contact segments.
    let mr = segments
        .iter()
        .position(|s| s.starts_with("NAD+MR"))
        .expect("NAD+MR");
    segments.remove(mr);
    while segments
        .get(mr)
        .is_some_and(|s| s.starts_with("CTA") || s.starts_with("COM"))
    {
        segments.remove(mr);
    }
    let found = structure_codes(&mapper, &message(segments));
    assert!(
        found
            .iter()
            // A missing mandatory group makes the message invalid.
            .any(|(code, detail, severity)| code == "STR005"
                && detail.contains("NAD+MR")
                && severity == "Error"),
        "{found:?}"
    );
}