Skip to main content

bo4e_edifact_types/
model.rs

1use serde::{Deserialize, Serialize};
2
3/// EDIFACT interchange envelope metadata (UNB/UNZ).
4#[derive(Debug, Clone, Default, Serialize, Deserialize)]
5#[serde(rename_all = "camelCase")]
6pub struct Interchangedaten {
7    pub syntax_kennung: Option<String>,
8    pub absender_code: Option<String>,
9    pub empfaenger_code: Option<String>,
10    pub datum: Option<String>,
11    pub zeit: Option<String>,
12    pub interchange_ref: Option<String>,
13}
14
15/// Message-level metadata: the UNH/UNT envelope plus the BGM/DTM/FTX document
16/// fields the mappings model as the `Nachricht` entity.
17///
18/// One metadata slot per level is the shape: `Interchangedaten` for the UNB/UNZ
19/// interchange, this for the message, `transaktionsdaten` for each transaction.
20/// `Nachricht` is metadata *about* the message rather than a business object the
21/// message is about, so it belongs here and not among the BOs in `stammdaten`.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct Nachrichtendaten {
25    pub unh_referenz: String,
26    pub nachrichten_typ: String,
27
28    /// BGM/DTM/FTX document metadata, flattened onto this object so the wire
29    /// carries one object per level rather than a nested pair.
30    ///
31    /// Held as a map, not as a typed `bo4e_mako::com::Nachricht`: the engine is
32    /// data-driven (see #92), so deserializing here would mean a fallible
33    /// conversion in the middle of the pipeline whose only honest failure
34    /// handling is to drop the fields. The typed view is proven instead by the
35    /// typed-hop gates, which cannot silently lose anything.
36    #[serde(flatten, default)]
37    pub nachricht: serde_json::Map<String, serde_json::Value>,
38}
39
40/// A complete EDIFACT interchange. M = message stammdaten, T = transaction stammdaten.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct Interchange<M, T> {
44    pub interchangedaten: Interchangedaten,
45    pub nachrichten: Vec<Nachricht<M, T>>,
46}
47
48/// A single EDIFACT message within an interchange.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct Nachricht<M, T> {
52    pub nachrichtendaten: Nachrichtendaten,
53    pub stammdaten: M,
54    pub transaktionen: Vec<T>,
55}
56
57/// One transaction: the business objects it is about, and the metadata about
58/// the transaction itself.
59///
60/// `D` is the transaction metadata (the `Prozessdaten` entity), `S` the
61/// business objects. Splitting them is what gives every level the same shape —
62/// interchange, message and transaction each carry their own metadata slot
63/// beside their content.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub struct Transaktion<D, S> {
67    /// Defaulted so `{"stammdaten": …}` alone deserializes: a transaction that
68    /// carries no process data is normal, and rejecting it would make the
69    /// metadata slot mandatory on input when it is not mandatory in the data.
70    #[serde(default)]
71    pub transaktionsdaten: D,
72    pub stammdaten: S,
73}
74
75/// Dynamic variant for untyped transaction usage.
76pub type DynamicTransaktion = Transaktion<serde_json::Value, serde_json::Value>;
77/// Dynamic variant for untyped usage.
78pub type DynamicInterchange = Interchange<serde_json::Value, DynamicTransaktion>;
79/// Dynamic variant for untyped message usage.
80pub type DynamicNachricht = Nachricht<serde_json::Value, DynamicTransaktion>;
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use serde_json::json;
86
87    #[test]
88    fn test_dynamic_interchange_serde_roundtrip() {
89        let interchange: DynamicInterchange = Interchange {
90            interchangedaten: Interchangedaten {
91                syntax_kennung: Some("UNOC".to_string()),
92                absender_code: Some("1234567890123".to_string()),
93                empfaenger_code: Some("9876543210987".to_string()),
94                datum: Some("260326".to_string()),
95                zeit: Some("1430".to_string()),
96                interchange_ref: Some("00001".to_string()),
97            },
98            nachrichten: vec![Nachricht {
99                nachrichtendaten: Nachrichtendaten {
100                    unh_referenz: "00001".to_string(),
101                    nachrichten_typ: "UTILMD".to_string(),
102                    nachricht: Default::default(),
103                },
104                stammdaten: json!({"key": "value"}),
105                transaktionen: vec![
106                    Transaktion {
107                        transaktionsdaten: json!({"vorgangId": "1"}),
108                        stammdaten: json!({"tx": 1}),
109                    },
110                    Transaktion {
111                        transaktionsdaten: json!({"vorgangId": "2"}),
112                        stammdaten: json!({"tx": 2}),
113                    },
114                ],
115            }],
116        };
117
118        let json_str = serde_json::to_string(&interchange).unwrap();
119        let deserialized: DynamicInterchange = serde_json::from_str(&json_str).unwrap();
120
121        assert_eq!(
122            deserialized.interchangedaten.syntax_kennung,
123            Some("UNOC".to_string())
124        );
125        assert_eq!(
126            deserialized.interchangedaten.absender_code,
127            Some("1234567890123".to_string())
128        );
129        assert_eq!(
130            deserialized.interchangedaten.empfaenger_code,
131            Some("9876543210987".to_string())
132        );
133        assert_eq!(deserialized.nachrichten.len(), 1);
134        assert_eq!(
135            deserialized.nachrichten[0].nachrichtendaten.unh_referenz,
136            "00001"
137        );
138        assert_eq!(
139            deserialized.nachrichten[0].nachrichtendaten.nachrichten_typ,
140            "UTILMD"
141        );
142        assert_eq!(
143            deserialized.nachrichten[0].stammdaten,
144            json!({"key": "value"})
145        );
146        assert_eq!(deserialized.nachrichten[0].transaktionen.len(), 2);
147    }
148
149    #[test]
150    fn test_typed_interchange_serde_roundtrip() {
151        #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152        struct TestStamm {
153            name: String,
154        }
155
156        #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
157        struct TestTx {
158            id: u32,
159        }
160
161        let interchange: Interchange<TestStamm, TestTx> = Interchange {
162            interchangedaten: Interchangedaten::default(),
163            nachrichten: vec![Nachricht {
164                nachrichtendaten: Nachrichtendaten {
165                    unh_referenz: "REF1".to_string(),
166                    nachrichten_typ: "ORDERS".to_string(),
167                    nachricht: Default::default(),
168                },
169                stammdaten: TestStamm {
170                    name: "test".to_string(),
171                },
172                transaktionen: vec![TestTx { id: 1 }, TestTx { id: 2 }],
173            }],
174        };
175
176        let json_str = serde_json::to_string(&interchange).unwrap();
177        let deserialized: Interchange<TestStamm, TestTx> = serde_json::from_str(&json_str).unwrap();
178
179        assert_eq!(deserialized.nachrichten.len(), 1);
180        assert_eq!(
181            deserialized.nachrichten[0].stammdaten,
182            TestStamm {
183                name: "test".to_string()
184            }
185        );
186        assert_eq!(deserialized.nachrichten[0].transaktionen.len(), 2);
187        assert_eq!(
188            deserialized.nachrichten[0].transaktionen[0],
189            TestTx { id: 1 }
190        );
191        assert_eq!(
192            deserialized.nachrichten[0].transaktionen[1],
193            TestTx { id: 2 }
194        );
195    }
196
197    #[test]
198    fn test_interchangedaten_camel_case_serialization() {
199        let data = Interchangedaten {
200            syntax_kennung: Some("UNOC".to_string()),
201            absender_code: Some("SENDER".to_string()),
202            empfaenger_code: None,
203            datum: None,
204            zeit: None,
205            interchange_ref: Some("REF".to_string()),
206        };
207
208        let json_value: serde_json::Value = serde_json::to_value(&data).unwrap();
209        let obj = json_value.as_object().unwrap();
210
211        // Verify camelCase keys
212        assert!(obj.contains_key("syntaxKennung"));
213        assert!(obj.contains_key("absenderCode"));
214        assert!(obj.contains_key("empfaengerCode"));
215        assert!(obj.contains_key("datum"));
216        assert!(obj.contains_key("zeit"));
217        assert!(obj.contains_key("interchangeRef"));
218
219        // Verify no snake_case keys
220        assert!(!obj.contains_key("syntax_kennung"));
221        assert!(!obj.contains_key("absender_code"));
222        assert!(!obj.contains_key("empfaenger_code"));
223        assert!(!obj.contains_key("interchange_ref"));
224    }
225}