Skip to main content

ocpp_types/
custom_data.rs

1//! The default type for OCPP 2.x's `customData` extension point.
2
3/// Stands in for a `customData` object this deployment doesn't read.
4///
5/// 2.0.1 and 2.1 hang an optional `customData` field on nearly every object
6/// in the specification -- 151 structs in 2.1 alone -- for vendor
7/// extensions. Storing the spec's shape inline costs 264 bytes at *every*
8/// one of those nodes, and because structs nest by value that cost is
9/// multiplied by the whole type graph rather than paid once.
10///
11/// So the field's type is a parameter, and this is its default: a
12/// zero-sized type, making `Option<NoCustomData>` one byte.
13///
14/// It is deliberately **permissive, not empty**. A peer may send
15/// `customData` whenever it likes, so the default has to accept and discard
16/// it -- `Option<()>` would only accept `null` and would fail to parse any
17/// message that actually carried an extension. That would turn opting out
18/// of custom data into an interop bug.
19///
20/// The trade is that discarded data is not echoed back: re-serializing a
21/// value parsed into `NoCustomData` writes `{}`, not what arrived. A
22/// deployment that needs the contents names its own type instead:
23///
24/// ```
25/// # #[cfg(feature = "serde")] {
26/// use ocpp_types::v21::common::{Component, CustomData};
27///
28/// // The specification's own shape, opted into:
29/// let with_spec_shape: Component<CustomData> = Component {
30///     custom_data: None,
31///     evse: None,
32///     instance: None,
33///     name: heapless::String::try_from("EVSE").unwrap(),
34/// };
35/// # let _ = with_spec_shape;
36/// # }
37/// ```
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
39pub struct NoCustomData;
40
41#[cfg(feature = "serde")]
42impl serde::Serialize for NoCustomData {
43    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
44        use serde::ser::SerializeMap;
45
46        serializer.serialize_map(Some(0))?.end()
47    }
48}
49
50#[cfg(feature = "serde")]
51impl<'de> serde::Deserialize<'de> for NoCustomData {
52    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
53        struct Visitor;
54
55        impl<'de> serde::de::Visitor<'de> for Visitor {
56            type Value = NoCustomData;
57
58            fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
59                f.write_str("a customData object")
60            }
61
62            fn visit_map<A: serde::de::MapAccess<'de>>(
63                self,
64                mut map: A,
65            ) -> Result<Self::Value, A::Error> {
66                while map
67                    .next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()?
68                    .is_some()
69                {}
70
71                Ok(NoCustomData)
72            }
73
74            // A peer that sends `"customData": null` means the same thing as
75            // omitting it.
76            fn visit_unit<E: serde::de::Error>(self) -> Result<Self::Value, E> {
77                Ok(NoCustomData)
78            }
79        }
80
81        deserializer.deserialize_map(Visitor)
82    }
83}
84
85#[cfg(all(test, feature = "serde"))]
86mod tests {
87    use crate::Action;
88    use crate::v21::HeartbeatRequest;
89    use crate::v21::common::{Component, CustomData};
90
91    /// The default has to *accept* a `customData` object, not just tolerate
92    /// its absence: a peer may send one at any time, and `Option<()>` would
93    /// only accept `null`. Rejecting it would turn opting out of custom data
94    /// into an interop failure.
95    #[test]
96    fn the_default_discards_a_populated_custom_data_object() {
97        let json = r#"{"customData":{"vendorId":"com.acme","extra":[1,2,3]},"name":"EVSE"}"#;
98        let (component, _): (Component, _) = serde_json_core::from_str(json).unwrap();
99
100        assert_eq!(component.name.as_str(), "EVSE");
101        assert!(component.custom_data.is_some());
102    }
103
104    #[test]
105    fn the_default_still_accepts_a_message_without_custom_data() {
106        let (component, _): (Component, _) =
107            serde_json_core::from_str(r#"{"name":"EVSE"}"#).unwrap();
108
109        assert!(component.custom_data.is_none());
110    }
111
112    /// Opting back into the specification's own shape, which is what the
113    /// generated `CustomData` struct is for.
114    #[test]
115    fn a_caller_can_name_the_specs_custom_data_shape_and_read_the_vendor_id() {
116        let json = r#"{"customData":{"vendorId":"com.acme"},"name":"EVSE"}"#;
117        let (component, _): (Component<CustomData>, _) =
118            serde_json_core::from_str(json).unwrap();
119
120        assert_eq!(component.custom_data.unwrap().vendor_id.as_str(), "com.acme");
121    }
122
123    /// A deployment with its own extension shape names that instead.
124    #[test]
125    fn a_caller_can_supply_an_entirely_custom_shape() {
126        #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
127        struct AcmeExtension {
128            #[serde(rename = "vendorId")]
129            vendor_id: heapless::String<16>,
130            #[serde(rename = "siteId")]
131            site_id: u32,
132        }
133
134        let json = r#"{"customData":{"vendorId":"com.acme","siteId":42},"name":"EVSE"}"#;
135        let (component, _): (Component<AcmeExtension>, _) =
136            serde_json_core::from_str(json).unwrap();
137
138        let extension = component.custom_data.unwrap();
139        assert_eq!(extension.site_id, 42);
140        assert_eq!(extension.vendor_id.as_str(), "com.acme");
141    }
142
143    /// The parameter threads all the way to the message types, not just the
144    /// nested structs -- otherwise a caller could never choose it.
145    #[test]
146    fn the_parameter_reaches_the_message_types() {
147        let request: HeartbeatRequest<CustomData> = HeartbeatRequest {
148            custom_data: Some(CustomData {
149                vendor_id: heapless::String::try_from("com.acme").unwrap(),
150            }),
151        };
152
153        assert_eq!(HeartbeatRequest::<CustomData>::ACTION, "Heartbeat");
154        assert!(request.custom_data.is_some());
155    }
156
157    /// The whole point: one byte by default against 272 for the spec shape,
158    /// paid at every node of the type graph rather than once. (272, not 264:
159    /// `heapless::String` has no spare bit pattern, so `Option` cannot pack
160    /// its discriminant into the payload.)
161    #[test]
162    fn the_default_costs_one_byte_and_the_spec_shape_costs_the_full_field() {
163        assert_eq!(core::mem::size_of::<Option<super::NoCustomData>>(), 1);
164        assert_eq!(core::mem::size_of::<Option<CustomData>>(), 272);
165    }
166}