bo4e-edifact-types 0.23.0

Generic interchange model types for BO4E/EDIFACT conversion
Documentation
//! Code enums and map keys of the generated typed PID modules.
//!
//! The mapping engine writes a code field as the value its `enum_map` translates
//! the EDIFACT code to (e.g. `"kundeMsb"`), or as the raw code when there is no
//! mapping; with code enrichment on, the value is wrapped in a
//! `{code, meaning, enum}` object. [`code_enum!`] generates enums that serialize
//! as the written value and deserialize from the written value, any raw EDIFACT
//! code producing it, or an enriched object.
//!
//! [`MapKey`] describes how the engine's reverse mapping resolves a group
//! qualifier from an element's fields; the typed modules key map-keyed entities
//! with it (see [`crate::serde_helpers::deserialize_keyed_map`]).

use serde::de::{self, Deserializer, IgnoredAny, MapAccess, Visitor};
use std::fmt;

/// A BO4E code value set (generated by [`code_enum!`] or [`key_enum!`]).
pub trait CodeEnum: Sized + Clone + 'static {
    /// Written values, one per known variant.
    const VALUES: &'static [&'static str];

    /// The value the mapping engine writes into BO4E JSON.
    fn as_str(&self) -> &str;

    /// The variant for a written value or any raw EDIFACT code alias. Open
    /// enums ([`code_enum!`]) keep any other code; closed ones return `None`.
    fn from_code(code: &str) -> Option<Self>;
}

/// Qualifier key of a map-keyed entity collection.
///
/// Each variant is a raw EDIFACT qualifier code. [`CodeEnum::as_str`] is the
/// code itself (the map key); the element carries it in [`Self::FIELD`] (and
/// [`Self::ALSO_FIELD`]) as the engine writes it.
pub trait MapKey: CodeEnum + Ord {
    /// Field of an element that carries the qualifier (e.g. `"partnerrolle"`).
    const FIELD: &'static str;
    /// Second field of a dual decomposition (e.g. `"datenqualitaet"`).
    const ALSO_FIELD: Option<&'static str>;
    /// `(raw code, value written to FIELD, value written to ALSO_FIELD)`, sorted
    /// by raw code — the order the engine's `enum_map` lookups iterate in.
    const CODES: &'static [(&'static str, &'static str, Option<&'static str>)];

    /// Resolve the key of an engine JSON element the way the reverse mapping
    /// resolves the qualifier: joint match of both fields, else the first code
    /// written as the field's value, else the field's value as a raw code.
    fn of_element(element: &serde_json::Value) -> Result<Self, String> {
        let value = code_str(element.get(Self::FIELD))
            .ok_or_else(|| format!("element has no `{}` to derive its key from", Self::FIELD))?;
        let also = Self::ALSO_FIELD.and_then(|f| code_str(element.get(f)));
        let code = also
            .and_then(|a| {
                Self::CODES
                    .iter()
                    .find(|(_, w, al)| *w == value && *al == Some(a))
            })
            .or_else(|| Self::CODES.iter().find(|(_, w, _)| *w == value))
            .map(|(code, _, _)| *code)
            .unwrap_or(value);
        Self::from_code(code)
            .filter(|k| k.as_str() == code)
            .ok_or_else(|| format!("`{}` = {value:?} is not a known qualifier", Self::FIELD))
    }

    /// Write the key's qualifier fields into `element` where they are absent.
    fn fill_element(&self, element: &mut serde_json::Value) {
        let Some(obj) = element.as_object_mut() else {
            return;
        };
        let Some((_, written, also)) = Self::CODES.iter().find(|(c, _, _)| *c == self.as_str())
        else {
            return;
        };
        obj.entry(Self::FIELD)
            .or_insert_with(|| serde_json::Value::String((*written).to_string()));
        if let (Some(field), Some(also)) = (Self::ALSO_FIELD, also) {
            obj.entry(field)
                .or_insert_with(|| serde_json::Value::String((*also).to_string()));
        }
    }
}

/// A code field's string: the value itself or an enriched object's `code`.
fn code_str(value: Option<&serde_json::Value>) -> Option<&str> {
    let value = value?;
    value
        .as_str()
        .or_else(|| value.get("code").and_then(|c| c.as_str()))
}

/// Deserialize a code enum from a string or an enriched `{code, ...}` object.
pub fn deserialize_code<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
    D: Deserializer<'de>,
    T: CodeEnum,
{
    struct CodeVisitor<T>(std::marker::PhantomData<T>);

    impl<T: CodeEnum> CodeVisitor<T> {
        fn resolve<E: de::Error>(code: &str) -> Result<T, E> {
            T::from_code(code).ok_or_else(|| de::Error::unknown_variant(code, T::VALUES))
        }
    }

    impl<'de, T: CodeEnum> Visitor<'de> for CodeVisitor<T> {
        type Value = T;

        fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
            f.write_str("a code string or a {code, meaning} object")
        }

        fn visit_str<E: de::Error>(self, v: &str) -> Result<T, E> {
            Self::resolve(v)
        }

        fn visit_map<M: MapAccess<'de>>(self, mut map: M) -> Result<T, M::Error> {
            let mut code: Option<String> = None;
            while let Some(key) = map.next_key::<String>()? {
                if key == "code" {
                    code = Some(map.next_value()?);
                } else {
                    map.next_value::<IgnoredAny>()?;
                }
            }
            let code = code.ok_or_else(|| de::Error::missing_field("code"))?;
            Self::resolve(&code)
        }
    }

    deserializer.deserialize_any(CodeVisitor(std::marker::PhantomData))
}

/// Define a code enum of a typed PID module.
///
/// ```ignore
/// code_enum! {
///     /// partnerrolle for PID 55042
///     pub enum Partnerrolle {
///         /// Kunde des MSB (Z07)
///         KundeMsb = "kundeMsb" | "Z07",
///     }
/// }
/// ```
///
/// Each variant serializes as its first literal (the value the engine writes)
/// and deserializes from any of its literals, or from an enriched
/// `{code, meaning, enum}` object whose `code` is one of them. The engine passes
/// codes through without checking them against the PID's code list, so any other
/// code is kept as `Other(code)` and serialized unchanged.
#[allow(unused_macros)]
macro_rules! code_enum {
    (
        $(#[$meta:meta])*
        $vis:vis enum $name:ident {
            $(
                $(#[$vmeta:meta])*
                $variant:ident = $value:literal $(| $alias:literal)*
            ),+ $(,)?
        }
    ) => {
        $(#[$meta])*
        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
        $vis enum $name {
            $( $(#[$vmeta])* $variant, )+
            /// A code outside this PID's code list, kept as written.
            Other(String),
        }

        impl $crate::code_enum::CodeEnum for $name {
            const VALUES: &'static [&'static str] = &[$($value),+];

            fn as_str(&self) -> &str {
                match self {
                    $(Self::$variant => $value,)+
                    Self::Other(code) => code,
                }
            }

            fn from_code(code: &str) -> Option<Self> {
                Some(match code {
                    $($value $(| $alias)* => Self::$variant,)+
                    other => Self::Other(other.to_string()),
                })
            }
        }

        $crate::code_enum::code_enum!(@serde $name);
    };
    (@serde $name:ident) => {
        impl ::std::fmt::Display for $name {
            fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
                f.write_str($crate::code_enum::CodeEnum::as_str(self))
            }
        }

        impl ::serde::Serialize for $name {
            fn serialize<S: ::serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
                s.serialize_str($crate::code_enum::CodeEnum::as_str(self))
            }
        }

        impl<'de> ::serde::Deserialize<'de> for $name {
            fn deserialize<D: ::serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
                $crate::code_enum::deserialize_code(d)
            }
        }
    };
}

/// Define a closed key enum: one variant per raw qualifier code, no fallback.
///
/// ```ignore
/// key_enum! {
///     pub enum GeschaeftspartnerKey {
///         Z07 = "Z07",
///     }
/// }
/// ```
///
/// Variants order (and so a `BTreeMap` iterates) in declaration order.
#[allow(unused_macros)]
macro_rules! key_enum {
    (
        $(#[$meta:meta])*
        $vis:vis enum $name:ident {
            $(
                $(#[$vmeta:meta])*
                $variant:ident = $value:literal
            ),+ $(,)?
        }
    ) => {
        $(#[$meta])*
        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
        $vis enum $name {
            $( $(#[$vmeta])* $variant, )+
        }

        impl $crate::code_enum::CodeEnum for $name {
            const VALUES: &'static [&'static str] = &[$($value),+];

            fn as_str(&self) -> &str {
                match self {
                    $(Self::$variant => $value,)+
                }
            }

            fn from_code(code: &str) -> Option<Self> {
                match code {
                    $($value => Some(Self::$variant),)+
                    _ => None,
                }
            }
        }

        $crate::code_enum::code_enum!(@serde $name);
    };
}

/// Implement [`MapKey`] for a key enum defined with [`key_enum!`].
///
/// ```ignore
/// map_key! {
///     GeschaeftspartnerKey: "partnerrolle", also "datenqualitaet" {
///         "Z07" => "kundeMsb", Some("base"),
///     }
/// }
/// ```
#[allow(unused_macros)]
macro_rules! map_key {
    (
        $name:ident: $field:literal $(, also $also:literal)? {
            $($code:literal => $written:literal, $also_value:expr),+ $(,)?
        }
    ) => {
        impl $crate::code_enum::MapKey for $name {
            const FIELD: &'static str = $field;
            const ALSO_FIELD: Option<&'static str> = $crate::code_enum::map_key!(@also $($also)?);
            const CODES: &'static [(&'static str, &'static str, Option<&'static str>)] =
                &[$(($code, $written, $also_value)),+];
        }
    };
    (@also $also:literal) => { Some($also) };
    (@also) => { None };
}

#[allow(unused_imports)]
pub(crate) use code_enum;
#[allow(unused_imports)]
pub(crate) use key_enum;
#[allow(unused_imports)]
pub(crate) use map_key;

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

    code_enum! {
        /// Test partnerrolle: many-to-one (Z39, Z40 → kundeMsb).
        pub enum Partnerrolle {
            /// Kunde des MSB
            KundeMsb = "kundeMsb" | "Z39" | "Z40",
            /// Korrespondenzanschrift
            Korrespondenz = "korrespondenzKundeMsb" | "Z08",
            /// Unmapped raw code
            Z99 = "Z99",
        }
    }

    key_enum! {
        pub enum TestKey {
            Z08 = "Z08",
            Z39 = "Z39",
            Z40 = "Z40",
        }
    }

    map_key! {
        TestKey: "partnerrolle", also "datenqualitaet" {
            "Z08" => "korrespondenzKundeMsb", None,
            "Z39" => "kundeMsb", Some("base"),
            "Z40" => "kundeMsb", Some("informative"),
        }
    }

    #[test]
    fn serializes_as_written_value() {
        assert_eq!(json!(Partnerrolle::KundeMsb), json!("kundeMsb"));
        assert_eq!(
            Partnerrolle::Korrespondenz.to_string(),
            "korrespondenzKundeMsb"
        );
    }

    #[test]
    fn deserializes_written_value_and_raw_code_aliases() {
        for input in ["kundeMsb", "Z39", "Z40"] {
            let v: Partnerrolle = serde_json::from_value(json!(input)).unwrap();
            assert_eq!(v, Partnerrolle::KundeMsb, "{input}");
        }
        let v: Partnerrolle = serde_json::from_value(json!("Z99")).unwrap();
        assert_eq!(v, Partnerrolle::Z99);
    }

    #[test]
    fn keeps_codes_outside_the_code_list() {
        let v: Partnerrolle = serde_json::from_value(json!("Z77")).unwrap();
        assert_eq!(v, Partnerrolle::Other("Z77".into()));
        assert_eq!(json!(v), json!("Z77"));
        let err = serde_json::from_value::<TestKey>(json!("Z77")).unwrap_err();
        assert!(err.to_string().contains("Z77"), "{err}");
    }

    #[test]
    fn deserializes_enriched_object() {
        let v: Partnerrolle = serde_json::from_value(
            json!({"code": "korrespondenzKundeMsb", "meaning": "Korrespondenz", "enum": "X"}),
        )
        .unwrap();
        assert_eq!(v, Partnerrolle::Korrespondenz);
        let v: Option<Partnerrolle> =
            serde_json::from_value(json!({"meaning": null, "code": "Z40"})).unwrap();
        assert_eq!(v, Some(Partnerrolle::KundeMsb));
    }

    #[test]
    fn map_key_resolves_many_to_one_via_also_field() {
        let key = |v| TestKey::of_element(&v);
        assert_eq!(
            key(json!({"partnerrolle": "kundeMsb", "datenqualitaet": "informative"})),
            Ok(TestKey::Z40)
        );
        assert_eq!(
            key(
                json!({"partnerrolle": {"code": "kundeMsb", "meaning": null}, "datenqualitaet": "base"})
            ),
            Ok(TestKey::Z39)
        );
        // Without the second field the engine falls back to the first code.
        assert_eq!(key(json!({"partnerrolle": "kundeMsb"})), Ok(TestKey::Z39));
        // Raw codes resolve to themselves.
        assert_eq!(key(json!({"partnerrolle": "Z40"})), Ok(TestKey::Z40));
        assert!(key(json!({"name1": "x"})).is_err());
        assert!(key(json!({"partnerrolle": "unbekannt"})).is_err());
    }

    #[test]
    fn map_key_fills_missing_fields_only() {
        let mut element = json!({"name1": "x"});
        TestKey::Z40.fill_element(&mut element);
        assert_eq!(
            element,
            json!({"name1": "x", "partnerrolle": "kundeMsb", "datenqualitaet": "informative"})
        );
        let mut element = json!({"partnerrolle": "Z40"});
        TestKey::Z40.fill_element(&mut element);
        assert_eq!(element["partnerrolle"], "Z40");
    }
}