Skip to main content

bo4e_edifact_types/
code_enum.rs

1//! Code enums and map keys of the generated typed PID modules.
2//!
3//! The mapping engine writes a code field as the value its `enum_map` translates
4//! the EDIFACT code to (e.g. `"kundeMsb"`), or as the raw code when there is no
5//! mapping; with code enrichment on, the value is wrapped in a
6//! `{code, meaning, enum}` object. [`code_enum!`] generates enums that serialize
7//! as the written value and deserialize from the written value, any raw EDIFACT
8//! code producing it, or an enriched object.
9//!
10//! [`MapKey`] describes how the engine's reverse mapping resolves a group
11//! qualifier from an element's fields; the typed modules key map-keyed entities
12//! with it (see [`crate::serde_helpers::deserialize_keyed_map`]).
13
14use serde::de::{self, Deserializer, IgnoredAny, MapAccess, Visitor};
15use std::fmt;
16
17/// A BO4E code value set (generated by [`code_enum!`] or [`key_enum!`]).
18pub trait CodeEnum: Sized + Clone + 'static {
19    /// Written values, one per known variant.
20    const VALUES: &'static [&'static str];
21
22    /// The value the mapping engine writes into BO4E JSON.
23    fn as_str(&self) -> &str;
24
25    /// The variant for a written value or any raw EDIFACT code alias. Open
26    /// enums ([`code_enum!`]) keep any other code; closed ones return `None`.
27    fn from_code(code: &str) -> Option<Self>;
28}
29
30/// Qualifier key of a map-keyed entity collection.
31///
32/// Each variant is a raw EDIFACT qualifier code. [`CodeEnum::as_str`] is the
33/// code itself (the map key); the element carries it in [`Self::FIELD`] (and
34/// [`Self::ALSO_FIELD`]) as the engine writes it.
35pub trait MapKey: CodeEnum + Ord {
36    /// Field of an element that carries the qualifier (e.g. `"partnerrolle"`).
37    const FIELD: &'static str;
38    /// Second field of a dual decomposition (e.g. `"datenqualitaet"`).
39    const ALSO_FIELD: Option<&'static str>;
40    /// `(raw code, value written to FIELD, value written to ALSO_FIELD)`, sorted
41    /// by raw code — the order the engine's `enum_map` lookups iterate in.
42    const CODES: &'static [(&'static str, &'static str, Option<&'static str>)];
43
44    /// Resolve the key of an engine JSON element the way the reverse mapping
45    /// resolves the qualifier: joint match of both fields, else the first code
46    /// written as the field's value, else the field's value as a raw code.
47    fn of_element(element: &serde_json::Value) -> Result<Self, String> {
48        let value = code_str(element.get(Self::FIELD))
49            .ok_or_else(|| format!("element has no `{}` to derive its key from", Self::FIELD))?;
50        let also = Self::ALSO_FIELD.and_then(|f| code_str(element.get(f)));
51        let code = also
52            .and_then(|a| {
53                Self::CODES
54                    .iter()
55                    .find(|(_, w, al)| *w == value && *al == Some(a))
56            })
57            .or_else(|| Self::CODES.iter().find(|(_, w, _)| *w == value))
58            .map(|(code, _, _)| *code)
59            .unwrap_or(value);
60        Self::from_code(code)
61            .filter(|k| k.as_str() == code)
62            .ok_or_else(|| format!("`{}` = {value:?} is not a known qualifier", Self::FIELD))
63    }
64
65    /// Write the key's qualifier fields into `element` where they are absent.
66    fn fill_element(&self, element: &mut serde_json::Value) {
67        let Some(obj) = element.as_object_mut() else {
68            return;
69        };
70        let Some((_, written, also)) = Self::CODES.iter().find(|(c, _, _)| *c == self.as_str())
71        else {
72            return;
73        };
74        obj.entry(Self::FIELD)
75            .or_insert_with(|| serde_json::Value::String((*written).to_string()));
76        if let (Some(field), Some(also)) = (Self::ALSO_FIELD, also) {
77            obj.entry(field)
78                .or_insert_with(|| serde_json::Value::String((*also).to_string()));
79        }
80    }
81}
82
83/// A code field's string: the value itself or an enriched object's `code`.
84fn code_str(value: Option<&serde_json::Value>) -> Option<&str> {
85    let value = value?;
86    value
87        .as_str()
88        .or_else(|| value.get("code").and_then(|c| c.as_str()))
89}
90
91/// Deserialize a code enum from a string or an enriched `{code, ...}` object.
92pub fn deserialize_code<'de, D, T>(deserializer: D) -> Result<T, D::Error>
93where
94    D: Deserializer<'de>,
95    T: CodeEnum,
96{
97    struct CodeVisitor<T>(std::marker::PhantomData<T>);
98
99    impl<T: CodeEnum> CodeVisitor<T> {
100        fn resolve<E: de::Error>(code: &str) -> Result<T, E> {
101            T::from_code(code).ok_or_else(|| de::Error::unknown_variant(code, T::VALUES))
102        }
103    }
104
105    impl<'de, T: CodeEnum> Visitor<'de> for CodeVisitor<T> {
106        type Value = T;
107
108        fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
109            f.write_str("a code string or a {code, meaning} object")
110        }
111
112        fn visit_str<E: de::Error>(self, v: &str) -> Result<T, E> {
113            Self::resolve(v)
114        }
115
116        fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<T, M::Error> {
117            let mut code: Option<String> = None;
118            while let Some(key) = map.next_key::<String>()? {
119                if key == "code" {
120                    code = Some(map.next_value()?);
121                } else {
122                    map.next_value::<IgnoredAny>()?;
123                }
124            }
125            let code = code.ok_or_else(|| de::Error::missing_field("code"))?;
126            Self::resolve(&code)
127        }
128    }
129
130    deserializer.deserialize_any(CodeVisitor(std::marker::PhantomData))
131}
132
133/// Define a code enum of a typed PID module.
134///
135/// ```ignore
136/// code_enum! {
137///     /// partnerrolle for PID 55042
138///     pub enum Partnerrolle {
139///         /// Kunde des MSB (Z07)
140///         KundeMsb = "kundeMsb" | "Z07",
141///     }
142/// }
143/// ```
144///
145/// Each variant serializes as its first literal (the value the engine writes)
146/// and deserializes from any of its literals, or from an enriched
147/// `{code, meaning, enum}` object whose `code` is one of them. The engine passes
148/// codes through without checking them against the PID's code list, so any other
149/// code is kept as `Other(code)` and serialized unchanged.
150#[allow(unused_macros)]
151macro_rules! code_enum {
152    (
153        $(#[$meta:meta])*
154        $vis:vis enum $name:ident {
155            $(
156                $(#[$vmeta:meta])*
157                $variant:ident = $value:literal $(| $alias:literal)*
158            ),+ $(,)?
159        }
160    ) => {
161        $(#[$meta])*
162        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
163        $vis enum $name {
164            $( $(#[$vmeta])* $variant, )+
165            /// A code outside this PID's code list, kept as written.
166            Other(String),
167        }
168
169        impl $crate::code_enum::CodeEnum for $name {
170            const VALUES: &'static [&'static str] = &[$($value),+];
171
172            fn as_str(&self) -> &str {
173                match self {
174                    $(Self::$variant => $value,)+
175                    Self::Other(code) => code,
176                }
177            }
178
179            fn from_code(code: &str) -> Option<Self> {
180                Some(match code {
181                    $($value $(| $alias)* => Self::$variant,)+
182                    other => Self::Other(other.to_string()),
183                })
184            }
185        }
186
187        $crate::code_enum::code_enum!(@serde $name);
188    };
189    (@serde $name:ident) => {
190        impl ::std::fmt::Display for $name {
191            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
192                f.write_str($crate::code_enum::CodeEnum::as_str(self))
193            }
194        }
195
196        impl ::serde::Serialize for $name {
197            fn serialize<S: ::serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
198                s.serialize_str($crate::code_enum::CodeEnum::as_str(self))
199            }
200        }
201
202        impl<'de> ::serde::Deserialize<'de> for $name {
203            fn deserialize<D: ::serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
204                $crate::code_enum::deserialize_code(d)
205            }
206        }
207    };
208}
209
210/// Define a closed key enum: one variant per raw qualifier code, no fallback.
211///
212/// ```ignore
213/// key_enum! {
214///     pub enum GeschaeftspartnerKey {
215///         Z07 = "Z07",
216///     }
217/// }
218/// ```
219///
220/// Variants order (and so a `BTreeMap` iterates) in declaration order.
221#[allow(unused_macros)]
222macro_rules! key_enum {
223    (
224        $(#[$meta:meta])*
225        $vis:vis enum $name:ident {
226            $(
227                $(#[$vmeta:meta])*
228                $variant:ident = $value:literal
229            ),+ $(,)?
230        }
231    ) => {
232        $(#[$meta])*
233        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
234        $vis enum $name {
235            $( $(#[$vmeta])* $variant, )+
236        }
237
238        impl $crate::code_enum::CodeEnum for $name {
239            const VALUES: &'static [&'static str] = &[$($value),+];
240
241            fn as_str(&self) -> &str {
242                match self {
243                    $(Self::$variant => $value,)+
244                }
245            }
246
247            fn from_code(code: &str) -> Option<Self> {
248                match code {
249                    $($value => Some(Self::$variant),)+
250                    _ => None,
251                }
252            }
253        }
254
255        $crate::code_enum::code_enum!(@serde $name);
256    };
257}
258
259/// Implement [`MapKey`] for a key enum defined with [`key_enum!`].
260///
261/// ```ignore
262/// map_key! {
263///     GeschaeftspartnerKey: "partnerrolle", also "datenqualitaet" {
264///         "Z07" => "kundeMsb", Some("base"),
265///     }
266/// }
267/// ```
268#[allow(unused_macros)]
269macro_rules! map_key {
270    (
271        $name:ident: $field:literal $(, also $also:literal)? {
272            $($code:literal => $written:literal, $also_value:expr),+ $(,)?
273        }
274    ) => {
275        impl $crate::code_enum::MapKey for $name {
276            const FIELD: &'static str = $field;
277            const ALSO_FIELD: Option<&'static str> = $crate::code_enum::map_key!(@also $($also)?);
278            const CODES: &'static [(&'static str, &'static str, Option<&'static str>)] =
279                &[$(($code, $written, $also_value)),+];
280        }
281    };
282    (@also $also:literal) => { Some($also) };
283    (@also) => { None };
284}
285
286#[allow(unused_imports)]
287pub(crate) use code_enum;
288#[allow(unused_imports)]
289pub(crate) use key_enum;
290#[allow(unused_imports)]
291pub(crate) use map_key;
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use serde_json::json;
297
298    code_enum! {
299        /// Test partnerrolle: many-to-one (Z39, Z40 → kundeMsb).
300        pub enum Partnerrolle {
301            /// Kunde des MSB
302            KundeMsb = "kundeMsb" | "Z39" | "Z40",
303            /// Korrespondenzanschrift
304            Korrespondenz = "korrespondenzKundeMsb" | "Z08",
305            /// Unmapped raw code
306            Z99 = "Z99",
307        }
308    }
309
310    key_enum! {
311        pub enum TestKey {
312            Z08 = "Z08",
313            Z39 = "Z39",
314            Z40 = "Z40",
315        }
316    }
317
318    map_key! {
319        TestKey: "partnerrolle", also "datenqualitaet" {
320            "Z08" => "korrespondenzKundeMsb", None,
321            "Z39" => "kundeMsb", Some("base"),
322            "Z40" => "kundeMsb", Some("informative"),
323        }
324    }
325
326    #[test]
327    fn serializes_as_written_value() {
328        assert_eq!(json!(Partnerrolle::KundeMsb), json!("kundeMsb"));
329        assert_eq!(
330            Partnerrolle::Korrespondenz.to_string(),
331            "korrespondenzKundeMsb"
332        );
333    }
334
335    #[test]
336    fn deserializes_written_value_and_raw_code_aliases() {
337        for input in ["kundeMsb", "Z39", "Z40"] {
338            let v: Partnerrolle = serde_json::from_value(json!(input)).unwrap();
339            assert_eq!(v, Partnerrolle::KundeMsb, "{input}");
340        }
341        let v: Partnerrolle = serde_json::from_value(json!("Z99")).unwrap();
342        assert_eq!(v, Partnerrolle::Z99);
343    }
344
345    #[test]
346    fn keeps_codes_outside_the_code_list() {
347        let v: Partnerrolle = serde_json::from_value(json!("Z77")).unwrap();
348        assert_eq!(v, Partnerrolle::Other("Z77".into()));
349        assert_eq!(json!(v), json!("Z77"));
350        let err = serde_json::from_value::<TestKey>(json!("Z77")).unwrap_err();
351        assert!(err.to_string().contains("Z77"), "{err}");
352    }
353
354    #[test]
355    fn deserializes_enriched_object() {
356        let v: Partnerrolle = serde_json::from_value(
357            json!({"code": "korrespondenzKundeMsb", "meaning": "Korrespondenz", "enum": "X"}),
358        )
359        .unwrap();
360        assert_eq!(v, Partnerrolle::Korrespondenz);
361        let v: Option<Partnerrolle> =
362            serde_json::from_value(json!({"meaning": null, "code": "Z40"})).unwrap();
363        assert_eq!(v, Some(Partnerrolle::KundeMsb));
364    }
365
366    #[test]
367    fn map_key_resolves_many_to_one_via_also_field() {
368        let key = |v| TestKey::of_element(&v);
369        assert_eq!(
370            key(json!({"partnerrolle": "kundeMsb", "datenqualitaet": "informative"})),
371            Ok(TestKey::Z40)
372        );
373        assert_eq!(
374            key(
375                json!({"partnerrolle": {"code": "kundeMsb", "meaning": null}, "datenqualitaet": "base"})
376            ),
377            Ok(TestKey::Z39)
378        );
379        // Without the second field the engine falls back to the first code.
380        assert_eq!(key(json!({"partnerrolle": "kundeMsb"})), Ok(TestKey::Z39));
381        // Raw codes resolve to themselves.
382        assert_eq!(key(json!({"partnerrolle": "Z40"})), Ok(TestKey::Z40));
383        assert!(key(json!({"name1": "x"})).is_err());
384        assert!(key(json!({"partnerrolle": "unbekannt"})).is_err());
385    }
386
387    #[test]
388    fn map_key_fills_missing_fields_only() {
389        let mut element = json!({"name1": "x"});
390        TestKey::Z40.fill_element(&mut element);
391        assert_eq!(
392            element,
393            json!({"name1": "x", "partnerrolle": "kundeMsb", "datenqualitaet": "informative"})
394        );
395        let mut element = json!({"partnerrolle": "Z40"});
396        TestKey::Z40.fill_element(&mut element);
397        assert_eq!(element["partnerrolle"], "Z40");
398    }
399}