Skip to main content

ocpp_types/
envelope.rs

1//! OCPP-J WebSocket message envelopes (`CALL`/`CALLRESULT`/`CALLERROR`, and
2//! 2.1's `CALLRESULTERROR`/`SEND`) -- the array-based wrapper every OCPP
3//! message travels in, as opposed to the message payload types themselves
4//! (`v16`/`v201`/`v21`). See the OCPP-J specification, section 4.
5
6/// A unique identifier correlating a `Call`/`SendMessage` with its
7/// `CallResult`/`CallError`/`CallResultError`. Identical across 1.6J,
8/// 2.0.1, and 2.1: a string of at most 36 characters, to allow for
9/// UUIDs/GUIDs (per the OCPP-J specification).
10#[derive(Debug, Clone, PartialEq, Eq)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct MessageId(heapless::String<36>);
13
14impl MessageId {
15    pub fn as_str(&self) -> &str {
16        &self.0
17    }
18}
19
20impl TryFrom<&str> for MessageId {
21    type Error = ();
22
23    fn try_from(value: &str) -> Result<Self, Self::Error> {
24        // See `v16::primitives::IdTag`: `heapless` 0.9's `CapacityError`
25        // is deliberately not part of this crate's public API.
26        heapless::String::try_from(value).map(Self).map_err(|_| ())
27    }
28}
29
30/// Placeholder for OCPP-J's `errorDetails`, which the specification
31/// deliberately leaves undefined in shape ("this JSON object describes
32/// error details in an undefined way") and recommends leaving empty in
33/// practice. Serializes as `{}`; deserializes by accepting and discarding
34/// whatever object is present, so it round-trips regardless of what a
35/// real implementation happens to send. Use a custom type via
36/// [`CallError`]/[`CallResultError`]'s `D` parameter for typed details.
37#[cfg(feature = "serde")]
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub struct EmptyPayload;
40
41#[cfg(feature = "serde")]
42impl serde::Serialize for EmptyPayload {
43    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
44        use serde::ser::SerializeMap;
45        serializer.serialize_map(Some(0))?.end()
46    }
47}
48
49#[cfg(feature = "serde")]
50impl<'de> serde::Deserialize<'de> for EmptyPayload {
51    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
52        struct EmptyPayloadVisitor;
53
54        impl<'de> serde::de::Visitor<'de> for EmptyPayloadVisitor {
55            type Value = EmptyPayload;
56
57            fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
58                f.write_str("a JSON object")
59            }
60
61            fn visit_map<A: serde::de::MapAccess<'de>>(
62                self,
63                mut map: A,
64            ) -> Result<Self::Value, A::Error> {
65                while map
66                    .next_entry::<serde::de::IgnoredAny, serde::de::IgnoredAny>()?
67                    .is_some()
68                {}
69                Ok(EmptyPayload)
70            }
71        }
72
73        deserializer.deserialize_map(EmptyPayloadVisitor)
74    }
75}
76
77/// An OCPP-J `CALL`: `[2, "<MessageId>", "<Action>", {Payload}]`. `T`'s
78/// action name is written on serialize and validated (not merely assumed)
79/// on deserialize, so a `Call<T>` you get back always really is the `T`
80/// you asked for.
81#[cfg(feature = "serde")]
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct Call<T> {
84    pub message_id: MessageId,
85    pub payload: T,
86}
87
88#[cfg(feature = "serde")]
89impl<T: crate::Action + serde::Serialize> serde::Serialize for Call<T> {
90    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
91        use serde::ser::SerializeTuple;
92
93        let mut tup = serializer.serialize_tuple(4)?;
94        tup.serialize_element(&2u8)?;
95        tup.serialize_element(&self.message_id)?;
96        tup.serialize_element(T::ACTION)?;
97        tup.serialize_element(&self.payload)?;
98        tup.end()
99    }
100}
101
102#[cfg(feature = "serde")]
103impl<'de, T: crate::Action + serde::Deserialize<'de>> serde::Deserialize<'de> for Call<T> {
104    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
105        struct CallVisitor<T>(core::marker::PhantomData<T>);
106
107        impl<'de, T: crate::Action + serde::Deserialize<'de>> serde::de::Visitor<'de> for CallVisitor<T> {
108            type Value = Call<T>;
109
110            fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
111                write!(f, "a CALL array [2, messageId, \"{}\", payload]", T::ACTION)
112            }
113
114            fn visit_seq<A: serde::de::SeqAccess<'de>>(
115                self,
116                mut seq: A,
117            ) -> Result<Self::Value, A::Error> {
118                let message_type_id: u8 = seq
119                    .next_element()?
120                    .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
121                if message_type_id != 2 {
122                    return Err(serde::de::Error::invalid_value(
123                        serde::de::Unexpected::Unsigned(message_type_id as u64),
124                        &"2 (CALL)",
125                    ));
126                }
127
128                let message_id: MessageId = seq
129                    .next_element()?
130                    .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
131
132                let action: &str = seq
133                    .next_element()?
134                    .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
135                if action != T::ACTION {
136                    return Err(serde::de::Error::invalid_value(
137                        serde::de::Unexpected::Str(action),
138                        &T::ACTION,
139                    ));
140                }
141
142                let payload: T = seq
143                    .next_element()?
144                    .ok_or_else(|| serde::de::Error::invalid_length(3, &self))?;
145
146                Ok(Call {
147                    message_id,
148                    payload,
149                })
150            }
151        }
152
153        deserializer.deserialize_tuple(4, CallVisitor(core::marker::PhantomData))
154    }
155}
156
157/// An OCPP-J `CALLRESULT`: `[3, "<MessageId>", {Payload}]`. Unlike
158/// [`Call`], there's no action name on the wire -- the recipient already
159/// knows which action a result is for by correlating `message_id` against
160/// the `Call` it sent, which is exactly what choosing `T` here represents.
161#[cfg(feature = "serde")]
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct CallResult<T> {
164    pub message_id: MessageId,
165    pub payload: T,
166}
167
168#[cfg(feature = "serde")]
169impl<T: serde::Serialize> serde::Serialize for CallResult<T> {
170    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
171        use serde::ser::SerializeTuple;
172
173        let mut tup = serializer.serialize_tuple(3)?;
174        tup.serialize_element(&3u8)?;
175        tup.serialize_element(&self.message_id)?;
176        tup.serialize_element(&self.payload)?;
177        tup.end()
178    }
179}
180
181#[cfg(feature = "serde")]
182impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for CallResult<T> {
183    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
184        struct CallResultVisitor<T>(core::marker::PhantomData<T>);
185
186        impl<'de, T: serde::Deserialize<'de>> serde::de::Visitor<'de> for CallResultVisitor<T> {
187            type Value = CallResult<T>;
188
189            fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
190                f.write_str("a CALLRESULT array [3, messageId, payload]")
191            }
192
193            fn visit_seq<A: serde::de::SeqAccess<'de>>(
194                self,
195                mut seq: A,
196            ) -> Result<Self::Value, A::Error> {
197                let message_type_id: u8 = seq
198                    .next_element()?
199                    .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
200                if message_type_id != 3 {
201                    return Err(serde::de::Error::invalid_value(
202                        serde::de::Unexpected::Unsigned(message_type_id as u64),
203                        &"3 (CALLRESULT)",
204                    ));
205                }
206
207                let message_id: MessageId = seq
208                    .next_element()?
209                    .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
210
211                let payload: T = seq
212                    .next_element()?
213                    .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
214
215                Ok(CallResult {
216                    message_id,
217                    payload,
218                })
219            }
220        }
221
222        deserializer.deserialize_tuple(3, CallResultVisitor(core::marker::PhantomData))
223    }
224}
225
226/// Defines an error envelope type (`CallError`/`CallResultError`): both
227/// are `[<discriminant>, "<MessageId>", "<errorCode>", "<errorDescription>",
228/// {errorDetails}]`, differing only in which prior message they're a
229/// response to and their `MessageTypeId`. A macro rather than two
230/// hand-written copies, so a fix to one can't drift from the other.
231macro_rules! error_envelope {
232    ($name:ident, $discriminant:literal, $expecting:literal) => {
233        #[cfg(feature = "serde")]
234        #[derive(Debug, Clone, PartialEq, Eq)]
235        pub struct $name<E, D = EmptyPayload, const DESC_CAP: usize = 1024> {
236            pub message_id: MessageId,
237            pub error_code: E,
238            pub error_description: heapless::String<DESC_CAP>,
239            pub error_details: D,
240        }
241
242        #[cfg(feature = "serde")]
243        impl<E: serde::Serialize, D: serde::Serialize, const DESC_CAP: usize> serde::Serialize
244            for $name<E, D, DESC_CAP>
245        {
246            fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
247                use serde::ser::SerializeTuple;
248
249                let mut tup = serializer.serialize_tuple(5)?;
250                tup.serialize_element(&$discriminant)?;
251                tup.serialize_element(&self.message_id)?;
252                tup.serialize_element(&self.error_code)?;
253                tup.serialize_element(&self.error_description)?;
254                tup.serialize_element(&self.error_details)?;
255                tup.end()
256            }
257        }
258
259        #[cfg(feature = "serde")]
260        impl<'de, E: serde::Deserialize<'de>, D: serde::Deserialize<'de>, const DESC_CAP: usize>
261            serde::Deserialize<'de> for $name<E, D, DESC_CAP>
262        {
263            fn deserialize<Dz: serde::Deserializer<'de>>(deserializer: Dz) -> Result<Self, Dz::Error> {
264                struct Visitor<E, D, const DESC_CAP: usize>(core::marker::PhantomData<(E, D)>);
265
266                impl<'de, E: serde::Deserialize<'de>, D: serde::Deserialize<'de>, const DESC_CAP: usize>
267                    serde::de::Visitor<'de> for Visitor<E, D, DESC_CAP>
268                {
269                    type Value = $name<E, D, DESC_CAP>;
270
271                    fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
272                        f.write_str($expecting)
273                    }
274
275                    fn visit_seq<A: serde::de::SeqAccess<'de>>(
276                        self,
277                        mut seq: A,
278                    ) -> Result<Self::Value, A::Error> {
279                        let message_type_id: u8 = seq
280                            .next_element()?
281                            .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
282                        if message_type_id != $discriminant {
283                            return Err(serde::de::Error::invalid_value(
284                                serde::de::Unexpected::Unsigned(message_type_id as u64),
285                                &$expecting,
286                            ));
287                        }
288
289                        let message_id = seq
290                            .next_element()?
291                            .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
292                        let error_code = seq
293                            .next_element()?
294                            .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
295                        let error_description = seq
296                            .next_element()?
297                            .ok_or_else(|| serde::de::Error::invalid_length(3, &self))?;
298                        let error_details = seq
299                            .next_element()?
300                            .ok_or_else(|| serde::de::Error::invalid_length(4, &self))?;
301
302                        Ok($name {
303                            message_id,
304                            error_code,
305                            error_description,
306                            error_details,
307                        })
308                    }
309                }
310
311                deserializer.deserialize_tuple(5, Visitor(core::marker::PhantomData))
312            }
313        }
314    };
315}
316
317// An OCPP-J `CALLERROR`: `[4, "<MessageId>", "<errorCode>",
318// "<errorDescription>", {errorDetails}]`. `E` is the version's own
319// `RpcErrorCode` (e.g. `v16::RpcErrorCode`) -- error codes aren't shared
320// across versions, so nothing here picks one for you. `D` defaults to
321// `EmptyPayload` since the spec leaves `errorDetails` undefined in shape
322// and recommends leaving it empty; supply your own `D` for typed details.
323// `errorDescription` has no spec-given bound either, so it's a
324// caller-choosable const generic (default 1024) rather than a guess.
325error_envelope!(
326    CallError,
327    4u8,
328    "a CALLERROR array [4, messageId, errorCode, errorDescription, errorDetails]"
329);
330
331// An OCPP-J `CALLRESULTERROR` (OCPP 2.1 only): identical shape to
332// `CallError`, sent in response to a `CallResult` that itself contained
333// errors, rather than to a `Call`.
334error_envelope!(
335    CallResultError,
336    5u8,
337    "a CALLRESULTERROR array [5, messageId, errorCode, errorDescription, errorDetails]"
338);
339
340/// An OCPP-J `SEND` (OCPP 2.1 only): `[6, "<MessageId>", "<Action>",
341/// {Payload}]`. Structurally identical to [`Call`], but the receiver
342/// SHALL NOT respond to it -- used for fire-and-forget messages like
343/// frequent periodic monitoring values.
344#[cfg(feature = "serde")]
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct SendMessage<T> {
347    pub message_id: MessageId,
348    pub payload: T,
349}
350
351#[cfg(feature = "serde")]
352impl<T: crate::Action + serde::Serialize> serde::Serialize for SendMessage<T> {
353    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
354        use serde::ser::SerializeTuple;
355
356        let mut tup = serializer.serialize_tuple(4)?;
357        tup.serialize_element(&6u8)?;
358        tup.serialize_element(&self.message_id)?;
359        tup.serialize_element(T::ACTION)?;
360        tup.serialize_element(&self.payload)?;
361        tup.end()
362    }
363}
364
365#[cfg(feature = "serde")]
366impl<'de, T: crate::Action + serde::Deserialize<'de>> serde::Deserialize<'de> for SendMessage<T> {
367    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
368        struct SendVisitor<T>(core::marker::PhantomData<T>);
369
370        impl<'de, T: crate::Action + serde::Deserialize<'de>> serde::de::Visitor<'de> for SendVisitor<T> {
371            type Value = SendMessage<T>;
372
373            fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
374                write!(f, "a SEND array [6, messageId, \"{}\", payload]", T::ACTION)
375            }
376
377            fn visit_seq<A: serde::de::SeqAccess<'de>>(
378                self,
379                mut seq: A,
380            ) -> Result<Self::Value, A::Error> {
381                let message_type_id: u8 = seq
382                    .next_element()?
383                    .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
384                if message_type_id != 6 {
385                    return Err(serde::de::Error::invalid_value(
386                        serde::de::Unexpected::Unsigned(message_type_id as u64),
387                        &"6 (SEND)",
388                    ));
389                }
390
391                let message_id: MessageId = seq
392                    .next_element()?
393                    .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
394
395                let action: &str = seq
396                    .next_element()?
397                    .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?;
398                if action != T::ACTION {
399                    return Err(serde::de::Error::invalid_value(
400                        serde::de::Unexpected::Str(action),
401                        &T::ACTION,
402                    ));
403                }
404
405                let payload: T = seq
406                    .next_element()?
407                    .ok_or_else(|| serde::de::Error::invalid_length(3, &self))?;
408
409                Ok(SendMessage {
410                    message_id,
411                    payload,
412                })
413            }
414        }
415
416        deserializer.deserialize_tuple(4, SendVisitor(core::marker::PhantomData))
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    // The crate is `#![no_std]`, so `std` isn't implicitly in scope even in
425    // test builds -- only needed here to spawn a thread with a larger
426    // stack (see `send_message_serializes_and_round_trips_a_real_send_type_message`).
427    extern crate std;
428
429    #[cfg(feature = "serde")]
430    #[test]
431    fn empty_payload_serializes_as_an_empty_object() {
432        let mut buf = [0u8; 16];
433        let len = serde_json_core::to_slice(&EmptyPayload, &mut buf).unwrap();
434        assert_eq!(&buf[..len], b"{}");
435    }
436
437    #[cfg(feature = "serde")]
438    #[test]
439    fn empty_payload_accepts_and_discards_a_populated_object() {
440        let (parsed, _): (EmptyPayload, usize) =
441            serde_json_core::from_str(r#"{"foo": 1, "bar": [1, 2, 3]}"#).unwrap();
442        assert_eq!(parsed, EmptyPayload);
443    }
444
445    #[cfg(feature = "serde")]
446    #[test]
447    fn call_serializes_and_round_trips_a_real_message_type() {
448        use crate::v16::{AuthorizeRequest, IdTag};
449
450        let call = Call {
451            message_id: MessageId::try_from("19223201").unwrap(),
452            payload: AuthorizeRequest {
453                id_tag: IdTag::try_from("ABC123").unwrap(),
454            },
455        };
456
457        let mut buf = [0u8; 128];
458        let len = serde_json_core::to_slice(&call, &mut buf).unwrap();
459        assert_eq!(
460            core::str::from_utf8(&buf[..len]).unwrap(),
461            r#"[2,"19223201","Authorize",{"idTag":"ABC123"}]"#
462        );
463
464        let (parsed, _): (Call<AuthorizeRequest>, usize) =
465            serde_json_core::from_slice(&buf[..len]).unwrap();
466        assert_eq!(parsed, call);
467    }
468
469    #[cfg(feature = "serde")]
470    #[test]
471    fn call_rejects_the_wrong_message_type_id() {
472        use crate::v16::AuthorizeRequest;
473
474        let json = r#"[3,"1","Authorize",{"idTag":"ABC123"}]"#;
475        let result: Result<(Call<AuthorizeRequest>, usize), _> = serde_json_core::from_str(json);
476        assert!(result.is_err());
477    }
478
479    #[cfg(feature = "serde")]
480    #[test]
481    fn call_rejects_a_mismatched_action() {
482        use crate::v16::AuthorizeRequest;
483
484        let json = r#"[2,"1","Heartbeat",{"idTag":"ABC123"}]"#;
485        let result: Result<(Call<AuthorizeRequest>, usize), _> = serde_json_core::from_str(json);
486        assert!(result.is_err());
487    }
488
489    // `HeartbeatResponse::current_time` is `heapless::String<N>` without
490    // `alloc` and `alloc::string::String` with it -- this test constructs
491    // the no-`alloc` shape specifically.
492    #[cfg(all(feature = "serde", not(feature = "alloc")))]
493    #[test]
494    fn call_result_serializes_and_round_trips_a_real_message_type() {
495        use crate::v16::HeartbeatResponse;
496
497        let result: CallResult<HeartbeatResponse> = CallResult {
498            message_id: MessageId::try_from("19223201").unwrap(),
499            payload: HeartbeatResponse {
500                current_time: crate::OcppTimestamp::parse_rfc3339("2013-02-01T20:53:32.486Z").unwrap(),
501            },
502        };
503
504        let mut buf = [0u8; 128];
505        let len = serde_json_core::to_slice(&result, &mut buf).unwrap();
506        assert_eq!(
507            core::str::from_utf8(&buf[..len]).unwrap(),
508            r#"[3,"19223201",{"currentTime":"2013-02-01T20:53:32.486Z"}]"#
509        );
510
511        let (parsed, _): (CallResult<HeartbeatResponse>, usize) =
512            serde_json_core::from_slice(&buf[..len]).unwrap();
513        assert_eq!(parsed, result);
514    }
515
516    #[cfg(feature = "serde")]
517    #[test]
518    fn call_result_rejects_the_wrong_message_type_id() {
519        use crate::v16::HeartbeatResponse;
520
521        let json = r#"[2,"1",{"currentTime":"2013-02-01T20:53:32.486Z"}]"#;
522        let result: Result<(CallResult<HeartbeatResponse>, usize), _> =
523            serde_json_core::from_str(json);
524        assert!(result.is_err());
525    }
526
527    #[cfg(feature = "serde")]
528    #[test]
529    fn call_error_serializes_and_round_trips_a_real_error_code() {
530        use crate::v16::RpcErrorCode;
531
532        let error: CallError<RpcErrorCode> = CallError {
533            message_id: MessageId::try_from("19223201").unwrap(),
534            error_code: RpcErrorCode::NotImplemented,
535            error_description: heapless::String::try_from("unrecognized action").unwrap(),
536            error_details: EmptyPayload,
537        };
538
539        let mut buf = [0u8; 128];
540        let len = serde_json_core::to_slice(&error, &mut buf).unwrap();
541        assert_eq!(
542            core::str::from_utf8(&buf[..len]).unwrap(),
543            r#"[4,"19223201","NotImplemented","unrecognized action",{}]"#
544        );
545
546        let (parsed, _): (CallError<RpcErrorCode>, usize) =
547            serde_json_core::from_slice(&buf[..len]).unwrap();
548        assert_eq!(parsed, error);
549    }
550
551    #[cfg(feature = "serde")]
552    #[test]
553    fn call_error_rejects_the_wrong_message_type_id() {
554        use crate::v16::RpcErrorCode;
555
556        let json = r#"[5,"1","NotImplemented","",{}]"#;
557        let result: Result<(CallError<RpcErrorCode>, usize), _> = serde_json_core::from_str(json);
558        assert!(result.is_err());
559    }
560
561    #[cfg(feature = "serde")]
562    #[test]
563    fn call_result_error_uses_discriminant_five_not_four() {
564        use crate::v201::RpcErrorCode;
565
566        let error: CallResultError<RpcErrorCode> = CallResultError {
567            message_id: MessageId::try_from("1").unwrap(),
568            error_code: RpcErrorCode::FormatViolation,
569            error_description: heapless::String::try_from("").unwrap(),
570            error_details: EmptyPayload,
571        };
572
573        let mut buf = [0u8; 128];
574        let len = serde_json_core::to_slice(&error, &mut buf).unwrap();
575        assert!(core::str::from_utf8(&buf[..len]).unwrap().starts_with("[5,"));
576
577        // A CALLERROR-shaped (discriminant 4) frame must NOT parse as a
578        // CallResultError, even though every other field lines up.
579        let call_error_json = r#"[4,"1","FormatViolation","",{}]"#;
580        let result: Result<(CallResultError<RpcErrorCode>, usize), _> =
581            serde_json_core::from_str(call_error_json);
582        assert!(result.is_err());
583    }
584
585    #[cfg(all(feature = "serde", not(feature = "alloc")))]
586    #[test]
587    fn send_message_serializes_and_round_trips_a_real_send_type_message() {
588        // `NotifyPeriodicEventStream` (OCPP 2.1) has no Request/Response
589        // suffix at all in the spec -- it's a genuine SEND-only message,
590        // not a CALL with an unusual name.
591        //
592        // This runs on a manually-spawned thread with a larger stack:
593        // deserializing an envelope wrapping a type with two large
594        // const-generic-defaulted fields (a 1024-byte string, a 16-item
595        // vec) goes through several layers of serde's Visitor pattern,
596        // and in an unoptimized debug build (no inlining) that's enough
597        // to overflow the default test-thread stack. Release builds are
598        // unaffected -- verified separately -- so this is purely a
599        // `cargo test`-in-debug-mode artifact, not a real embedded-target
600        // concern, but the test still needs to not crash in CI.
601        std::thread::Builder::new()
602            .stack_size(16 * 1024 * 1024)
603            .spawn(|| {
604                use crate::v21::NotifyPeriodicEventStream;
605
606                let send: SendMessage<NotifyPeriodicEventStream> = SendMessage {
607                    message_id: MessageId::try_from("19223201").unwrap(),
608                    payload: NotifyPeriodicEventStream {
609                        basetime: crate::OcppTimestamp::parse_rfc3339("2024-08-27T12:30:40Z").unwrap(),
610                        custom_data: None,
611                        data: heapless::Vec::new(),
612                        id: 123,
613                        pending: 0,
614                    },
615                };
616
617                let mut buf = [0u8; 256];
618                let len = serde_json_core::to_slice(&send, &mut buf).unwrap();
619                let json = core::str::from_utf8(&buf[..len]).unwrap();
620                assert!(json.starts_with(r#"[6,"19223201","NotifyPeriodicEventStream","#));
621
622                let (parsed, _): (SendMessage<NotifyPeriodicEventStream>, usize) =
623                    serde_json_core::from_slice(&buf[..len]).unwrap();
624                assert_eq!(parsed, send);
625            })
626            .unwrap()
627            .join()
628            .unwrap();
629    }
630
631    #[test]
632    fn message_id_round_trips_a_short_string() {
633        let id = MessageId::try_from("19223201").unwrap();
634        assert_eq!(id.as_str(), "19223201");
635    }
636
637    #[test]
638    fn message_id_rejects_strings_over_36_bytes() {
639        let too_long = "a".repeat(37);
640        assert!(MessageId::try_from(too_long.as_str()).is_err());
641    }
642
643    #[test]
644    fn message_id_accepts_exactly_36_bytes() {
645        let exactly_36 = "a".repeat(36);
646        assert!(MessageId::try_from(exactly_36.as_str()).is_ok());
647    }
648
649    /// `heapless` 0.9 changes `String::try_from`'s error from `()` to
650    /// `CapacityError`. `MessageId`'s own error type must not follow it:
651    /// that's this crate's public API, and changing it would break every
652    /// downstream `match`/`map_err`. The bound is the assertion -- this
653    /// stops compiling if the associated type ever drifts.
654    #[test]
655    fn message_id_error_type_stays_unit_regardless_of_the_heapless_version() {
656        fn assert_unit_error<T>()
657        where
658            for<'a> T: TryFrom<&'a str, Error = ()>,
659        {
660        }
661
662        assert_unit_error::<MessageId>();
663    }
664}