edifact-mapper 0.25.0

EDIFACT to BO4E bidirectional conversion for the German energy market
Documentation
//! One transaction-shaped reading of a message.
//!
//! The dialect places each entity where its group sits in the MIG: what the
//! message says once (the partners in SG2, ORDERS' Lieferort NAD+DP / LOC+172,
//! ORDRSP's answer code AJT and order reference RFF+ON) is in the message's
//! `stammdaten`, what each position or transaction says is in its
//! `transaktionen[i]`. Some messages have no transaction at all: the AHB of
//! ORDRSP 19015 / 19016 / 19002 lists no Positionsteil.
//!
//! A consumer built around transactions can read the message as one view per
//! transaction instead — requested by mako-twin, whose events carry a
//! transaction subtree. Each view keeps the published placement:
//!
//! ```json
//! { "transaktionsdaten": …, "stammdaten": { …message-level and the transaction's entities… },
//!   "nachricht": { "dokumentennummer": …, "dokumenttyp": …, … } }
//! ```
//!
//! `nachricht` is the message's own data (`Nachrichtendaten::nachricht`, BGM,
//! DTM and the like); the UNH bookkeeping — `unhReferenz`, `nachrichtenTyp`,
//! `zuordnungsreferenz`, `uebermittlungsfolgenummer`,
//! `uebermittlungsabschnitt` — stays out and is handed back to
//! [`Mapper::from_transaction_views`]. A message without a transaction gives
//! one view whose `transaktionsdaten` is null; that it stands for no
//! transaction is said beside it ([`TransactionView::synthetic`]), never inside
//! the JSON. Nothing about the dialect changes: the views are a reading of
//! [`Nachricht`], and [`Mapper::from_transaction_views`] gives it back.

use std::collections::BTreeSet;

use mig_bo4e::model::{Nachricht, Nachrichtendaten, MSG_METADATA_ENTITY};
use serde_json::{Map, Value};

use crate::{Mapper, MapperError};

/// One transaction of a message, with the message-level content merged in.
#[derive(Debug, Clone, PartialEq)]
pub struct TransactionView {
    /// `{"transaktionsdaten", "stammdaten", "nachricht"}`.
    pub value: Value,
    /// The message has no transaction; this view carries its message-level
    /// content only and renders to no transaction.
    pub synthetic: bool,
}

fn view_error(message: String) -> MapperError {
    MapperError::TransactionView(message)
}

fn object(value: &Value) -> Map<String, Value> {
    value.as_object().cloned().unwrap_or_default()
}

impl Mapper {
    /// The message as one [`TransactionView`] per transaction — one synthetic
    /// view when it has none.
    ///
    /// Each view's `stammdaten` is the message-level entities merged with the
    /// transaction's own. An entity name both use is refused rather than
    /// overwritten.
    pub fn transaction_views(
        &self,
        nachricht: &Nachricht<Value, Value>,
    ) -> Result<Vec<TransactionView>, MapperError> {
        let message = object(&nachricht.stammdaten);
        let own = Value::Object(nachricht.nachrichtendaten.nachricht.clone());
        let view = |transaktionsdaten: Value, stammdaten: Map<String, Value>| {
            serde_json::json!({
                "transaktionsdaten": transaktionsdaten,
                "stammdaten": Value::Object(stammdaten),
                "nachricht": own.clone(),
            })
        };
        if nachricht.transaktionen.is_empty() {
            return Ok(vec![TransactionView {
                value: view(Value::Null, message),
                synthetic: true,
            }]);
        }
        nachricht
            .transaktionen
            .iter()
            .map(|tx| {
                let mut stammdaten = message.clone();
                for (key, entity) in object(&tx["stammdaten"]) {
                    if stammdaten.contains_key(&key) {
                        return Err(view_error(format!(
                            "entity `{key}` is both message-level and in a transaction"
                        )));
                    }
                    stammdaten.insert(key, entity);
                }
                let transaktionsdaten = tx.get("transaktionsdaten").cloned().unwrap_or(Value::Null);
                Ok(TransactionView {
                    value: view(transaktionsdaten, stammdaten),
                    synthetic: false,
                })
            })
            .collect()
    }

    /// The message `views` read, for rendering: the inverse of
    /// [`transaction_views`](Self::transaction_views).
    ///
    /// An entity is message-level when the PID's message-level mapping writes
    /// it; the views must agree on those, and on `nachricht`. A synthetic view
    /// gives no transaction and must hold no transaction-level entity.
    /// `kopf` supplies the UNH bookkeeping; its `nachricht` is replaced by the
    /// views'.
    pub fn from_transaction_views(
        &self,
        views: &[TransactionView],
        kopf: &Nachrichtendaten,
        fv: &str,
        variant: &str,
        pid: &str,
    ) -> Result<Nachricht<Value, Value>, MapperError> {
        let message_keys = self.message_entity_keys(fv, variant, pid)?;
        let Some(first) = views.first() else {
            return Err(view_error("no view".to_string()));
        };

        let own = object(&first.value["nachricht"]);
        let mut message: Option<Map<String, Value>> = None;
        let mut transaktionen = Vec::new();
        for view in views {
            if object(&view.value["nachricht"]) != own {
                return Err(view_error("the views disagree on `nachricht`".to_string()));
            }
            let (here, transaction): (Map<String, Value>, Map<String, Value>) =
                object(&view.value["stammdaten"])
                    .into_iter()
                    .partition(|(key, _)| message_keys.contains(key));
            match &message {
                None => message = Some(here),
                Some(seen) => {
                    let keys: BTreeSet<&String> = seen.keys().chain(here.keys()).collect();
                    if let Some(key) = keys.into_iter().find(|k| seen.get(*k) != here.get(*k)) {
                        return Err(view_error(format!(
                            "the views disagree on message-level entity `{key}`"
                        )));
                    }
                }
            }
            let transaktionsdaten = view.value["transaktionsdaten"].clone();
            if view.synthetic {
                if let Some(key) = transaction.keys().next() {
                    return Err(view_error(format!(
                        "a view standing for no transaction holds transaction-level entity `{key}`"
                    )));
                }
                if !transaktionsdaten.is_null() {
                    return Err(view_error(
                        "a view standing for no transaction holds `transaktionsdaten`".to_string(),
                    ));
                }
                continue;
            }
            let mut tx = Map::new();
            tx.insert("transaktionsdaten".to_string(), transaktionsdaten);
            tx.insert("stammdaten".to_string(), Value::Object(transaction));
            transaktionen.push(Value::Object(tx));
        }

        let mut nachrichtendaten = kopf.clone();
        nachrichtendaten.nachricht = own;
        Ok(Nachricht {
            nachrichtendaten,
            stammdaten: Value::Object(message.unwrap_or_default()),
            transaktionen,
        })
    }

    /// The `stammdaten` keys the PID's message-level mapping writes.
    fn message_entity_keys(
        &self,
        fv: &str,
        variant: &str,
        pid: &str,
    ) -> Result<BTreeSet<String>, MapperError> {
        let definitions = self.message_definitions(fv, variant, pid)?;
        Ok(definitions
            .iter()
            .map(|d| {
                let mut chars = d.meta.entity.chars();
                match chars.next() {
                    Some(c) => c.to_lowercase().chain(chars).collect(),
                    None => String::new(),
                }
            })
            .filter(|key| key != MSG_METADATA_ENTITY)
            .collect())
    }
}