ocpp_types/lib.rs
1//! Strongly typed OCPP message types for 1.6J, 2.0.1, and 2.1.
2//!
3//! `ocpp-types` provides the request/response payload types for the Open
4//! Charge Point Protocol, generated from the official JSON schemas. It is
5//! `no_std` and allocation-free: field sizes are bounded at the type level
6//! with [`heapless`] collections, sized to the limits stated in each
7//! version's specification.
8//!
9//! Each protocol version lives in its own module -- [`v16`], [`v201`],
10//! [`v21`] -- since the same message name can differ in shape across
11//! versions.
12//!
13//! # Fields with no spec-given bound
14//!
15//! A handful of fields (free-text strings, a few arrays) have no
16//! `maxLength`/`maxItems` in the spec, so there's no size to give a
17//! `heapless` collection without guessing one. These become a const
18//! generic the caller picks (with a default, so most code never needs to
19//! think about it):
20//!
21//! ```
22//! # #[cfg(not(feature = "alloc"))]
23//! # {
24//! use ocpp_types::v16::HeartbeatResponse;
25//!
26//! // Uses the default capacity (1024):
27//! let response: HeartbeatResponse = HeartbeatResponse {
28//! current_time: heapless::String::try_from("2024-01-01T00:00:00Z").unwrap(),
29//! };
30//!
31//! // Or pick a smaller one explicitly:
32//! let response: HeartbeatResponse<64> = HeartbeatResponse {
33//! current_time: heapless::String::try_from("2024-01-01T00:00:00Z").unwrap(),
34//! };
35//! # }
36//! ```
37//!
38//! With the `alloc` feature enabled, these fields become plain
39//! `alloc::string::String`/`alloc::vec::Vec<T>` instead, and the const
40//! generic disappears entirely -- useful on targets with a real allocator
41//! (a CSMS backend, a simulator) that would rather not pick a bound at all.
42//!
43//! # Fields the spec leaves untyped
44//!
45//! 2.0.1 and 2.1's `DataTransfer` carries a `data` field the specification
46//! deliberately gives no type at all -- "open to implementation", agreed
47//! between the two parties. There's no single Rust type for arbitrary JSON
48//! without an allocator, so the payload type is the caller's to pick, as a
49//! type parameter defaulting to `()` (i.e. "this deployment sends no
50//! data"):
51//!
52//! ```
53//! use ocpp_types::v201::DataTransferRequest;
54//!
55//! // Whatever this vendor agreed on; add `serde::Serialize`/`Deserialize`
56//! // to send it on the wire.
57//! #[derive(Debug, Clone, PartialEq)]
58//! struct VendorPayload {
59//! session_id: u32,
60//! }
61//!
62//! let request: DataTransferRequest<VendorPayload> = DataTransferRequest {
63//! custom_data: None,
64//! data: Some(VendorPayload { session_id: 42 }),
65//! message_id: None,
66//! vendor_id: heapless::String::try_from("com.example").unwrap(),
67//! };
68//!
69//! // Or, sending no vendor payload at all, the default:
70//! let plain: DataTransferRequest = DataTransferRequest {
71//! custom_data: None,
72//! data: None,
73//! message_id: None,
74//! vendor_id: heapless::String::try_from("com.example").unwrap(),
75//! };
76//! ```
77//!
78//! 1.6J's `DataTransfer.data` is a plain string in that version's schema,
79//! so it stays `Option<heapless::String<N>>` and needs no parameter.
80//!
81//! # Example
82//!
83//! ```
84//! use ocpp_types::Action;
85//! use ocpp_types::v16::{AuthorizeRequest, IdTag};
86//!
87//! let request = AuthorizeRequest {
88//! id_tag: IdTag::try_from("ABC123").unwrap(),
89//! };
90//!
91//! assert_eq!(AuthorizeRequest::ACTION, "Authorize");
92//! ```
93//!
94//! # Serialization
95//!
96//! With the `serde` feature enabled, every message implements
97//! `serde::Serialize`/`serde::Deserialize`, and [`Action`] gains
98//! zero-allocation JSON helpers backed by
99//! [`serde-json-core`](https://docs.rs/serde-json-core) -- the caller owns
100//! the buffer, nothing is heap-allocated:
101//!
102//! ```ignore
103//! use ocpp_types::Action;
104//!
105//! let mut buf = [0u8; 256];
106//! let json: &str = request.to_json_str(&mut buf)?;
107//! let parsed = AuthorizeRequest::from_json_str(json)?;
108//! ```
109//!
110//! # RPC errors
111//!
112//! Each version also exposes an `RpcErrorCode` enum covering the
113//! `CALLERROR` codes defined by that version's OCPP-J specification (e.g.
114//! [`v16::RpcErrorCode`]), implementing [`core::error::Error`].
115//!
116//! # WebSocket envelopes
117//!
118//! With `serde`, `Call`/`CallResult`/`CallError` model the OCPP-J
119//! array-based envelope every message travels in (`[2, messageId, action,
120//! payload]`, etc.) -- generic over the payload type, so no per-version
121//! duplication is needed. `CallResultError`/`SendMessage` cover 2.1's
122//! additional `CALLRESULTERROR`/`SEND` message types (the shapes work for
123//! any version; whether a given deployment actually uses them is a
124//! protocol-level concern, not something the types enforce). See
125//! `examples/envelope.rs`.
126#![no_std]
127
128#[cfg(feature = "alloc")]
129extern crate alloc;
130
131mod action;
132mod envelope;
133
134#[cfg(test)]
135mod generics_test;
136
137#[cfg(test)]
138mod untyped_data_test;
139
140pub mod v16;
141pub mod v201;
142pub mod v21;
143
144pub use action::Action;
145#[cfg(feature = "serde")]
146pub use envelope::{Call, CallError, CallResult, CallResultError, EmptyPayload, SendMessage};
147pub use envelope::MessageId;
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn id_tag_round_trips_a_short_string() {
155 let tag = v16::IdTag::try_from("ABC123").unwrap();
156 assert_eq!(tag.as_str(), "ABC123");
157 }
158
159 #[test]
160 fn id_tag_rejects_strings_over_20_bytes() {
161 let too_long = "A".repeat(21);
162 assert!(v16::IdTag::try_from(too_long.as_str()).is_err());
163 }
164
165 struct DummyRequest;
166
167 impl Action for DummyRequest {
168 const ACTION: &'static str = "Dummy";
169 }
170
171 #[test]
172 fn action_trait_exposes_action_name() {
173 assert_eq!(DummyRequest::ACTION, "Dummy");
174 }
175}