automapper-validation 0.27.0

AHB condition expression parsing, evaluation, and EDIFACT validation
Documentation
//! Which answer codes each EBD / Codeliste holds.
//!
//! A response names the list its answer comes from next to the answer: AJT
//! 1082 beside AJT 4465, STS C556/1131 beside C556/9013 — `E_0204` (an EBD),
//! `S_0066` (Codeliste Strom), `G_0059` (Codeliste Gas). The AHB says which
//! lists a PID may name, not what they hold; `AJT+ZB5+G_0059` satisfies it
//! though G_0059 holds no ZB5. The EBD document says what they hold.
//!
//! Data source: `machine-readable_mako-prozesse`
//! (`<FV>/pipeline/09_ebds/answer_codes.yaml`), extracted with
//! `scripts/extract_answer_code_lists.py`, plus the codes
//! `ebd_cluster_map.json` knows for those EBDs. FV2504 has no dataset there
//! and is read as FV2510.

use serde::Deserialize;
use std::collections::{HashMap, HashSet};
use std::sync::OnceLock;

const EMBEDDED_JSON: &str = include_str!("../../data/answer_code_lists.json");

#[derive(Debug, Deserialize)]
struct RawFile {
    lists: HashMap<String, HashMap<String, Vec<String>>>,
}

/// The answer codes of every EBD and Codeliste, per format version.
#[derive(Debug)]
pub struct AnswerCodeLists {
    by_fv: HashMap<String, HashMap<String, HashSet<String>>>,
}

impl AnswerCodeLists {
    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
        let raw: RawFile = serde_json::from_str(json)?;
        let by_fv = raw
            .lists
            .into_iter()
            .map(|(fv, lists)| {
                let lists = lists
                    .into_iter()
                    .map(|(id, codes)| (id, codes.into_iter().collect()))
                    .collect();
                (fv, lists)
            })
            .collect();
        Ok(Self { by_fv })
    }

    pub fn embedded() -> &'static Self {
        static CELL: OnceLock<AnswerCodeLists> = OnceLock::new();
        CELL.get_or_init(|| {
            AnswerCodeLists::from_json(EMBEDDED_JSON)
                .expect("embedded answer_code_lists.json is malformed")
        })
    }

    /// The answer codes `list` holds in `fv`, or `None` when the data does
    /// not know the list (or the format version).
    pub fn codes(&self, fv: &str, list: &str) -> Option<&HashSet<String>> {
        let fv = if fv == "FV2504" { "FV2510" } else { fv };
        self.by_fv.get(fv)?.get(list)
    }

    /// Whether `list` holds `answer` in `fv`; `None` when the list is unknown.
    pub fn holds(&self, fv: &str, list: &str, answer: &str) -> Option<bool> {
        self.codes(fv, list).map(|codes| codes.contains(answer))
    }
}

/// Whether `value` names an answer list: an EBD (`E_0204`) or a Codeliste
/// (`S_0066`, `G_0059`).
pub fn is_answer_list_id(value: &str) -> bool {
    let bytes = value.as_bytes();
    bytes.len() == 6
        && matches!(bytes[0], b'E' | b'S' | b'G')
        && bytes[1] == b'_'
        && bytes[2..].iter().all(u8::is_ascii_digit)
}

/// For a rule at `list_path` that names the list, the path of the answer
/// beside it: AJT `…/AJT/1082` → `…/AJT/4465`, STS `…/C556/1131` →
/// `…/C556/9013`. `None` for any other element — a LIN's 1082 is a position
/// number, not a list.
pub fn answer_path(list_path: &str) -> Option<String> {
    if let Some(prefix) = list_path.strip_suffix("AJT/1082") {
        return Some(format!("{prefix}AJT/4465"));
    }
    list_path
        .strip_suffix("C556/1131")
        .map(|prefix| format!("{prefix}C556/9013"))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_embedded_lists_name_what_the_ebd_document_says() {
        let lists = AnswerCodeLists::embedded();
        assert_eq!(lists.holds("FV2610", "S_0066", "ZB5"), Some(true));
        assert_eq!(lists.holds("FV2610", "S_0066", "ZB4"), Some(false));
        assert_eq!(lists.holds("FV2610", "S_0065", "ZB4"), Some(true));
        // FV2504 reads FV2510.
        assert_eq!(lists.holds("FV2504", "S_0066", "ZB5"), Some(true));
        assert_eq!(lists.holds("FV2610", "S_9999", "ZB5"), None);
    }

    #[test]
    fn only_ajt_and_sts_name_a_list() {
        assert_eq!(answer_path("SG2/AJT/1082").as_deref(), Some("SG2/AJT/4465"));
        assert_eq!(
            answer_path("SG4/STS/C556/1131").as_deref(),
            Some("SG4/STS/C556/9013")
        );
        assert_eq!(answer_path("SG26/LIN/1082"), None);
        assert!(is_answer_list_id("E_0204") && is_answer_list_id("G_0059"));
        assert!(!is_answer_list_id("Z07") && !is_answer_list_id("E_02041"));
    }
}