Skip to main content

bo4e_core/
additional_attribute.rs

1//! Additional attributes for extensibility.
2
3use serde::{Deserialize, Serialize};
4
5/// Value type for additional attributes.
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
8#[serde(untagged)]
9pub enum AttributeValue {
10    /// String value
11    String(String),
12    /// Numeric value (integer or float)
13    Number(f64),
14    /// Boolean value
15    Boolean(bool),
16    /// Nested object with more attributes
17    Object(Vec<AdditionalAttribute>),
18    /// Array of values
19    Array(Vec<AttributeValue>),
20    /// Null value
21    Null,
22}
23
24/// Additional attribute for external system IDs and custom metadata.
25///
26/// This enables interoperability with external systems that need to attach
27/// their own identifiers or metadata to BO4E objects.
28///
29/// # Example
30///
31/// ```rust
32/// use bo4e_core::AdditionalAttribute;
33/// use bo4e_core::additional_attribute::AttributeValue;
34///
35/// let attr = AdditionalAttribute {
36///     name: "sap_id".to_string(),
37///     value: Some(AttributeValue::String("SAP123".to_string())),
38/// };
39/// ```
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
42#[serde(rename_all = "camelCase")]
43pub struct AdditionalAttribute {
44    /// Name/key of the attribute
45    pub name: String,
46    /// Value of the attribute (optional)
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub value: Option<AttributeValue>,
49}
50
51impl AdditionalAttribute {
52    /// Create a new additional attribute with a string value.
53    pub fn string(name: impl Into<String>, value: impl Into<String>) -> Self {
54        Self {
55            name: name.into(),
56            value: Some(AttributeValue::String(value.into())),
57        }
58    }
59
60    /// Create a new additional attribute with a numeric value.
61    pub fn number(name: impl Into<String>, value: f64) -> Self {
62        Self {
63            name: name.into(),
64            value: Some(AttributeValue::Number(value)),
65        }
66    }
67
68    /// Create a new additional attribute with a boolean value.
69    pub fn boolean(name: impl Into<String>, value: bool) -> Self {
70        Self {
71            name: name.into(),
72            value: Some(AttributeValue::Boolean(value)),
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_string_attribute() {
83        let attr = AdditionalAttribute::string("system_id", "ABC123");
84        assert_eq!(attr.name, "system_id");
85        assert_eq!(
86            attr.value,
87            Some(AttributeValue::String("ABC123".to_string()))
88        );
89    }
90
91    #[test]
92    fn test_number_attribute() {
93        let attr = AdditionalAttribute::number("priority", 42.0);
94        assert_eq!(attr.name, "priority");
95        assert_eq!(attr.value, Some(AttributeValue::Number(42.0)));
96    }
97
98    #[test]
99    fn test_serialize_attribute() {
100        let attr = AdditionalAttribute::string("key", "value");
101        let json = serde_json::to_string(&attr).unwrap();
102        assert_eq!(json, r#"{"name":"key","value":"value"}"#);
103    }
104
105    #[test]
106    fn test_deserialize_attribute() {
107        let json = r#"{"name":"key","value":"value"}"#;
108        let attr: AdditionalAttribute = serde_json::from_str(json).unwrap();
109        assert_eq!(attr.name, "key");
110        assert_eq!(
111            attr.value,
112            Some(AttributeValue::String("value".to_string()))
113        );
114    }
115
116    #[test]
117    fn test_deserialize_nested_attribute() {
118        let json = r#"{"name":"parent","value":[{"name":"child","value":123}]}"#;
119        let attr: AdditionalAttribute = serde_json::from_str(json).unwrap();
120        assert_eq!(attr.name, "parent");
121        match attr.value {
122            Some(AttributeValue::Object(children)) => {
123                assert_eq!(children.len(), 1);
124                assert_eq!(children[0].name, "child");
125            }
126            _ => panic!("Expected nested object"),
127        }
128    }
129}