Skip to main content

edifact_mapper/
transaction_view.rs

1//! One transaction-shaped reading of a message.
2//!
3//! The dialect places each entity where its group sits in the MIG: what the
4//! message says once (the partners in SG2, ORDERS' Lieferort NAD+DP / LOC+172,
5//! ORDRSP's answer code AJT and order reference RFF+ON) is in the message's
6//! `stammdaten`, what each position or transaction says is in its
7//! `transaktionen[i]`. Some messages have no transaction at all: the AHB of
8//! ORDRSP 19015 / 19016 / 19002 lists no Positionsteil.
9//!
10//! A consumer built around transactions can read the message as one view per
11//! transaction instead — requested by mako-twin, whose events carry a
12//! transaction subtree. Each view keeps the published placement:
13//!
14//! ```json
15//! { "transaktionsdaten": …, "stammdaten": { …message-level and the transaction's entities… },
16//!   "nachricht": { "dokumentennummer": …, "dokumenttyp": …, … } }
17//! ```
18//!
19//! `nachricht` is the message's own data (`Nachrichtendaten::nachricht`, BGM,
20//! DTM and the like); the UNH bookkeeping — `unhReferenz`, `nachrichtenTyp`,
21//! `zuordnungsreferenz`, `uebermittlungsfolgenummer`,
22//! `uebermittlungsabschnitt` — stays out and is handed back to
23//! [`Mapper::from_transaction_views`]. A message without a transaction gives
24//! one view whose `transaktionsdaten` is null; that it stands for no
25//! transaction is said beside it ([`TransactionView::synthetic`]), never inside
26//! the JSON. Nothing about the dialect changes: the views are a reading of
27//! [`Nachricht`], and [`Mapper::from_transaction_views`] gives it back.
28
29use std::collections::BTreeSet;
30
31use mig_bo4e::model::{Nachricht, Nachrichtendaten, MSG_METADATA_ENTITY};
32use serde_json::{Map, Value};
33
34use crate::{Mapper, MapperError};
35
36/// One transaction of a message, with the message-level content merged in.
37#[derive(Debug, Clone, PartialEq)]
38pub struct TransactionView {
39    /// `{"transaktionsdaten", "stammdaten", "nachricht"}`.
40    pub value: Value,
41    /// The message has no transaction; this view carries its message-level
42    /// content only and renders to no transaction.
43    pub synthetic: bool,
44}
45
46fn view_error(message: String) -> MapperError {
47    MapperError::TransactionView(message)
48}
49
50fn object(value: &Value) -> Map<String, Value> {
51    value.as_object().cloned().unwrap_or_default()
52}
53
54impl Mapper {
55    /// The message as one [`TransactionView`] per transaction — one synthetic
56    /// view when it has none.
57    ///
58    /// Each view's `stammdaten` is the message-level entities merged with the
59    /// transaction's own. An entity name both use is refused rather than
60    /// overwritten.
61    pub fn transaction_views(
62        &self,
63        nachricht: &Nachricht<Value, Value>,
64    ) -> Result<Vec<TransactionView>, MapperError> {
65        let message = object(&nachricht.stammdaten);
66        let own = Value::Object(nachricht.nachrichtendaten.nachricht.clone());
67        let view = |transaktionsdaten: Value, stammdaten: Map<String, Value>| {
68            serde_json::json!({
69                "transaktionsdaten": transaktionsdaten,
70                "stammdaten": Value::Object(stammdaten),
71                "nachricht": own.clone(),
72            })
73        };
74        if nachricht.transaktionen.is_empty() {
75            return Ok(vec![TransactionView {
76                value: view(Value::Null, message),
77                synthetic: true,
78            }]);
79        }
80        nachricht
81            .transaktionen
82            .iter()
83            .map(|tx| {
84                let mut stammdaten = message.clone();
85                for (key, entity) in object(&tx["stammdaten"]) {
86                    if stammdaten.contains_key(&key) {
87                        return Err(view_error(format!(
88                            "entity `{key}` is both message-level and in a transaction"
89                        )));
90                    }
91                    stammdaten.insert(key, entity);
92                }
93                let transaktionsdaten = tx.get("transaktionsdaten").cloned().unwrap_or(Value::Null);
94                Ok(TransactionView {
95                    value: view(transaktionsdaten, stammdaten),
96                    synthetic: false,
97                })
98            })
99            .collect()
100    }
101
102    /// The message `views` read, for rendering: the inverse of
103    /// [`transaction_views`](Self::transaction_views).
104    ///
105    /// An entity is message-level when the PID's message-level mapping writes
106    /// it; the views must agree on those, and on `nachricht`. A synthetic view
107    /// gives no transaction and must hold no transaction-level entity.
108    /// `kopf` supplies the UNH bookkeeping; its `nachricht` is replaced by the
109    /// views'.
110    pub fn from_transaction_views(
111        &self,
112        views: &[TransactionView],
113        kopf: &Nachrichtendaten,
114        fv: &str,
115        variant: &str,
116        pid: &str,
117    ) -> Result<Nachricht<Value, Value>, MapperError> {
118        let message_keys = self.message_entity_keys(fv, variant, pid)?;
119        let Some(first) = views.first() else {
120            return Err(view_error("no view".to_string()));
121        };
122
123        let own = object(&first.value["nachricht"]);
124        let mut message: Option<Map<String, Value>> = None;
125        let mut transaktionen = Vec::new();
126        for view in views {
127            if object(&view.value["nachricht"]) != own {
128                return Err(view_error("the views disagree on `nachricht`".to_string()));
129            }
130            let (here, transaction): (Map<String, Value>, Map<String, Value>) =
131                object(&view.value["stammdaten"])
132                    .into_iter()
133                    .partition(|(key, _)| message_keys.contains(key));
134            match &message {
135                None => message = Some(here),
136                Some(seen) => {
137                    let keys: BTreeSet<&String> = seen.keys().chain(here.keys()).collect();
138                    if let Some(key) = keys.into_iter().find(|k| seen.get(*k) != here.get(*k)) {
139                        return Err(view_error(format!(
140                            "the views disagree on message-level entity `{key}`"
141                        )));
142                    }
143                }
144            }
145            let transaktionsdaten = view.value["transaktionsdaten"].clone();
146            if view.synthetic {
147                if let Some(key) = transaction.keys().next() {
148                    return Err(view_error(format!(
149                        "a view standing for no transaction holds transaction-level entity `{key}`"
150                    )));
151                }
152                if !transaktionsdaten.is_null() {
153                    return Err(view_error(
154                        "a view standing for no transaction holds `transaktionsdaten`".to_string(),
155                    ));
156                }
157                continue;
158            }
159            let mut tx = Map::new();
160            tx.insert("transaktionsdaten".to_string(), transaktionsdaten);
161            tx.insert("stammdaten".to_string(), Value::Object(transaction));
162            transaktionen.push(Value::Object(tx));
163        }
164
165        let mut nachrichtendaten = kopf.clone();
166        nachrichtendaten.nachricht = own;
167        Ok(Nachricht {
168            nachrichtendaten,
169            stammdaten: Value::Object(message.unwrap_or_default()),
170            transaktionen,
171        })
172    }
173
174    /// The `stammdaten` keys the PID's message-level mapping writes.
175    fn message_entity_keys(
176        &self,
177        fv: &str,
178        variant: &str,
179        pid: &str,
180    ) -> Result<BTreeSet<String>, MapperError> {
181        let definitions = self.message_definitions(fv, variant, pid)?;
182        Ok(definitions
183            .iter()
184            .map(|d| {
185                let mut chars = d.meta.entity.chars();
186                match chars.next() {
187                    Some(c) => c.to_lowercase().chain(chars).collect(),
188                    None => String::new(),
189                }
190            })
191            .filter(|key| key != MSG_METADATA_ENTITY)
192            .collect())
193    }
194}