ocpi_kit/transport/routing.rs
1//! Message routing: the four `OCPI-*` headers and the scenarios that decide what goes in them.
2//!
3//! The spec gives five tables of who goes in `to` and who goes in `from`, one per scenario, and
4//! getting them wrong is the classic hub bug. [`RoutingScenario`] encodes all five, so an
5//! integrator picks a scenario rather than filling headers by hand.
6//!
7//! Spec: 2.3.0 §transport_and_format_message_routing
8
9use core::fmt;
10
11use http::{HeaderMap, HeaderValue};
12
13use crate::ModuleId;
14use crate::types::PartyRef;
15
16use super::headers::{
17 OCPI_FROM_COUNTRY_CODE, OCPI_FROM_PARTY_ID, OCPI_TO_COUNTRY_CODE, OCPI_TO_PARTY_ID, header_party,
18};
19
20/// The `OCPI-to-*` and `OCPI-from-*` headers of one message.
21///
22/// > *When implementing OCPI these four headers SHALL be implemented for any request/response
23/// > to/from a Functional Module. This does not mean they have to be present in all request.*
24///
25/// `to` is `None` for an [Open Routing Request](RoutingScenario::OpenRoutingRequest), where the
26/// hub decides the destination from the content.
27///
28/// Spec: 2.3.0 §transport_and_format_message_routing
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct RoutingHeaders {
31 /// The party this message is to be sent to, absent for an Open Routing Request.
32 pub to: Option<PartyRef>,
33 /// The party this message is sent from.
34 pub from: PartyRef,
35}
36
37impl RoutingHeaders {
38 /// Headers addressed from `from` to `to`.
39 #[must_use]
40 pub fn new(from: PartyRef, to: PartyRef) -> Self {
41 Self { to: Some(to), from }
42 }
43
44 /// Headers with no `to`, for an Open Routing Request.
45 ///
46 /// > *For an Open Routing Request, the TO headers in the request from the requesting party to
47 /// > the Hub MUST be omitted.*
48 #[must_use]
49 pub fn open(from: PartyRef) -> Self {
50 Self { to: None, from }
51 }
52
53 /// The headers of the response to this request, which swap `to` and `from`.
54 ///
55 /// For an Open Routing Request the responder is the receiving party, which the requester did
56 /// not know; pass it as `responder`.
57 #[must_use]
58 pub fn response_from(&self, responder: PartyRef) -> Self {
59 Self { to: Some(self.from.clone()), from: responder }
60 }
61
62 /// Reads the routing headers from a header map.
63 ///
64 /// Returns `None` when the `from` pair is absent, which is either a spec violation or a
65 /// configuration module — see [`RoutingHeaders::applies_to`].
66 #[must_use]
67 pub fn from_headers(headers: &HeaderMap) -> Option<Self> {
68 Some(Self {
69 to: header_party(headers, &OCPI_TO_COUNTRY_CODE, &OCPI_TO_PARTY_ID),
70 from: header_party(headers, &OCPI_FROM_COUNTRY_CODE, &OCPI_FROM_PARTY_ID)?,
71 })
72 }
73
74 /// Writes the routing headers into a header map.
75 pub fn write_to(&self, headers: &mut HeaderMap) {
76 if let Some(to) = &self.to {
77 insert_party(headers, &OCPI_TO_COUNTRY_CODE, &OCPI_TO_PARTY_ID, to);
78 } else {
79 headers.remove(OCPI_TO_COUNTRY_CODE);
80 headers.remove(OCPI_TO_PARTY_ID);
81 }
82 insert_party(headers, &OCPI_FROM_COUNTRY_CODE, &OCPI_FROM_PARTY_ID, &self.from);
83 }
84
85 /// Whether these headers belong on a request to `module`.
86 ///
87 /// > *The requests/responses to/from Configuration Modules: Credentials, Versions and Hub
88 /// > Client Info are not to be routed, and are for Platform-to-Platform or Platform-to-Hub
89 /// > communication. Thus routing headers SHALL NOT be used with these modules.*
90 #[must_use]
91 pub fn applies_to(module: &ModuleId) -> bool {
92 module.is_functional()
93 }
94}
95
96fn insert_party(
97 headers: &mut HeaderMap,
98 country_header: &http::HeaderName,
99 party_header: &http::HeaderName,
100 party: &PartyRef,
101) {
102 if let Ok(v) = HeaderValue::from_str(party.country_code.as_str()) {
103 headers.insert(country_header.clone(), v);
104 }
105 if let Ok(v) = HeaderValue::from_str(party.party_id.as_str()) {
106 headers.insert(party_header.clone(), v);
107 }
108}
109
110impl fmt::Display for RoutingHeaders {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 match &self.to {
113 Some(to) => write!(f, "{} -> {to}", self.from),
114 None => write!(f, "{} -> (open)", self.from),
115 }
116 }
117}
118
119/// One of the five routing arrangements the specification tabulates.
120///
121/// Each variant knows how to fill the headers for both the request and the response, in both
122/// directions across a hub. This is the whole of §transport_and_format_message_routing's
123/// "Overview of required/optional routing headers for different scenarios" in one type.
124///
125/// ```
126/// use ocpi_kit::transport::RoutingScenario;
127/// use ocpi_kit::types::PartyRef;
128///
129/// let cpo = PartyRef::new("NL", "TNM").unwrap();
130/// let msp = PartyRef::new("DE", "ABC").unwrap();
131/// let hub = PartyRef::new("NL", "HUB").unwrap();
132///
133/// // A broadcast push addresses the hub, not the eventual receivers.
134/// let scenario = RoutingScenario::BroadcastPush { hub: hub.clone() };
135/// let request = scenario.request_headers(&cpo, None);
136/// assert_eq!(request.to, Some(hub));
137/// assert_eq!(request.from, cpo);
138/// ```
139///
140/// Spec: 2.3.0 §transport_and_format_message_routing
141#[derive(Clone, Debug, PartialEq, Eq)]
142#[non_exhaustive]
143pub enum RoutingScenario {
144 /// Requesting platform to receiving platform, directly or through a hub that just relays.
145 ///
146 /// | | TO | FROM |
147 /// |---|---|---|
148 /// | request | receiving party | requesting party |
149 /// | response | requesting party | receiving party |
150 Direct,
151
152 /// A push to every connected party with the opposite role, fanned out by the hub.
153 ///
154 /// | | TO | FROM |
155 /// |---|---|---|
156 /// | requester → hub | **hub** | requesting party |
157 /// | hub → requester | requesting party | **hub** |
158 /// | hub → receiver | receiving party | **hub** |
159 /// | receiver → hub | **hub** | receiving party |
160 ///
161 /// > *GET SHALL NOT be used in combination with Broadcast Push.*
162 BroadcastPush {
163 /// The hub that fans the message out.
164 hub: PartyRef,
165 },
166
167 /// The requester does not know the destination; the hub decides from the content.
168 ///
169 /// | | TO | FROM |
170 /// |---|---|---|
171 /// | requester → hub | *omitted* | requesting party |
172 /// | hub → receiver | receiving party | requesting party |
173 /// | receiver → hub | requesting party | receiving party |
174 /// | hub → requester | requesting party | receiving party |
175 ///
176 /// > *Open Routing Requests are possible for GET (Not GET ALL), POST, PUT, PATCH and DELETE.*
177 OpenRoutingRequest,
178
179 /// A GET on a Sender interface implemented by the hub, merging several parties' objects.
180 ///
181 /// | | TO | FROM |
182 /// |---|---|---|
183 /// | requester → hub | **hub** | requesting party |
184 /// | hub → requester | requesting party | **hub** |
185 GetAllViaHub {
186 /// The hub that merges the objects.
187 hub: PartyRef,
188 },
189}
190
191impl RoutingScenario {
192 /// The headers of the request the requesting party sends.
193 ///
194 /// `receiver` is the destination party; it is ignored by the scenarios that address the hub
195 /// or omit the `to` headers entirely, and may be `None` there.
196 #[must_use]
197 pub fn request_headers(&self, requester: &PartyRef, receiver: Option<&PartyRef>) -> RoutingHeaders {
198 match self {
199 Self::Direct => RoutingHeaders { to: receiver.cloned(), from: requester.clone() },
200 Self::BroadcastPush { hub } | Self::GetAllViaHub { hub } => {
201 RoutingHeaders { to: Some(hub.clone()), from: requester.clone() }
202 }
203 Self::OpenRoutingRequest => RoutingHeaders::open(requester.clone()),
204 }
205 }
206
207 /// The headers of the response the requesting party will receive.
208 ///
209 /// `receiver` is the party that actually answered, which the hub knows even when the
210 /// requester did not.
211 #[must_use]
212 pub fn response_headers(&self, requester: &PartyRef, receiver: Option<&PartyRef>) -> RoutingHeaders {
213 match self {
214 Self::Direct | Self::OpenRoutingRequest => RoutingHeaders {
215 to: Some(requester.clone()),
216 from: receiver.cloned().unwrap_or_else(|| requester.clone()),
217 },
218 Self::BroadcastPush { hub } | Self::GetAllViaHub { hub } => {
219 RoutingHeaders { to: Some(requester.clone()), from: hub.clone() }
220 }
221 }
222 }
223
224 /// The headers the hub puts on the request it forwards to the receiving party.
225 ///
226 /// Returns `None` for [`GetAllViaHub`](Self::GetAllViaHub), where the hub answers from its
227 /// own merged view and forwards nothing verbatim.
228 #[must_use]
229 pub fn forwarded_request_headers(
230 &self,
231 requester: &PartyRef,
232 receiver: &PartyRef,
233 ) -> Option<RoutingHeaders> {
234 match self {
235 // "Direct request | Hub to receiving platform | Receiving-party | Requesting-party"
236 Self::Direct | Self::OpenRoutingRequest => {
237 Some(RoutingHeaders::new(requester.clone(), receiver.clone()))
238 }
239 // "Broadcast request | Hub to receiving platform | Receiving-party | Hub"
240 Self::BroadcastPush { hub } => Some(RoutingHeaders::new(hub.clone(), receiver.clone())),
241 Self::GetAllViaHub { .. } => None,
242 }
243 }
244
245 /// Whether a GET may use this scenario.
246 ///
247 /// > *GET SHALL NOT be used in combination with Broadcast Push. If the requesting party wants
248 /// > to GET information of which it does not know the receiving party, an Open Routing
249 /// > Request MUST be used.*
250 #[must_use]
251 pub const fn allows_get(&self) -> bool {
252 !matches!(self, Self::BroadcastPush { .. })
253 }
254
255 /// Whether a write (POST, PUT, PATCH, DELETE) may use this scenario.
256 ///
257 /// A GET All is by definition a read.
258 #[must_use]
259 pub const fn allows_write(&self) -> bool {
260 !matches!(self, Self::GetAllViaHub { .. })
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 fn cpo() -> PartyRef {
269 PartyRef::new("NL", "TNM").unwrap()
270 }
271 fn msp() -> PartyRef {
272 PartyRef::new("DE", "ABC").unwrap()
273 }
274 fn hub() -> PartyRef {
275 PartyRef::new("NL", "HUB").unwrap()
276 }
277
278 #[test]
279 fn direct_request_and_response_swap_the_parties() {
280 let s = RoutingScenario::Direct;
281 let req = s.request_headers(&cpo(), Some(&msp()));
282 assert_eq!(req.to, Some(msp()));
283 assert_eq!(req.from, cpo());
284 let resp = s.response_headers(&cpo(), Some(&msp()));
285 assert_eq!(resp.to, Some(cpo()));
286 assert_eq!(resp.from, msp());
287 }
288
289 #[test]
290 fn broadcast_push_addresses_the_hub_then_the_hub_speaks_for_itself() {
291 let s = RoutingScenario::BroadcastPush { hub: hub() };
292 // "Broadcast request | Requesting platform to Hub | Hub | Requesting-party"
293 let req = s.request_headers(&cpo(), None);
294 assert_eq!((req.to, req.from), (Some(hub()), cpo()));
295 // "Broadcast response | Hub to requesting platform | Requesting-party | Hub"
296 let resp = s.response_headers(&cpo(), None);
297 assert_eq!((resp.to, resp.from), (Some(cpo()), hub()));
298 // "Broadcast request | Hub to receiving platform | Receiving-party | Hub"
299 let fwd = s.forwarded_request_headers(&cpo(), &msp()).unwrap();
300 assert_eq!((fwd.to, fwd.from), (Some(msp()), hub()));
301 assert!(!s.allows_get(), "GET SHALL NOT be used with Broadcast Push");
302 }
303
304 #[test]
305 fn open_routing_omits_the_to_headers_only_on_the_first_hop() {
306 let s = RoutingScenario::OpenRoutingRequest;
307 let req = s.request_headers(&cpo(), None);
308 assert_eq!(req.to, None, "the TO headers MUST be omitted");
309 // "Open request | Hub to receiving platform | Receiving-party | Requesting-party"
310 let fwd = s.forwarded_request_headers(&cpo(), &msp()).unwrap();
311 assert_eq!((fwd.to, fwd.from), (Some(msp()), cpo()));
312 assert!(s.allows_get() && s.allows_write());
313 }
314
315 #[test]
316 fn get_all_via_hub_is_answered_by_the_hub_itself() {
317 let s = RoutingScenario::GetAllViaHub { hub: hub() };
318 let req = s.request_headers(&msp(), None);
319 assert_eq!((req.to, req.from), (Some(hub()), msp()));
320 let resp = s.response_headers(&msp(), None);
321 assert_eq!((resp.to, resp.from), (Some(msp()), hub()));
322 assert_eq!(s.forwarded_request_headers(&msp(), &cpo()), None);
323 assert!(!s.allows_write(), "a GET All is a read");
324 }
325
326 #[test]
327 fn configuration_modules_are_never_routed() {
328 assert!(!RoutingHeaders::applies_to(&ModuleId::Credentials));
329 assert!(!RoutingHeaders::applies_to(&ModuleId::Versions));
330 assert!(!RoutingHeaders::applies_to(&ModuleId::HubClientInfo));
331 assert!(RoutingHeaders::applies_to(&ModuleId::Locations));
332 }
333
334 #[test]
335 fn headers_round_trip_and_an_open_request_has_no_to() {
336 let mut headers = HeaderMap::new();
337 RoutingHeaders::new(cpo(), msp()).write_to(&mut headers);
338 assert_eq!(RoutingHeaders::from_headers(&headers), Some(RoutingHeaders::new(cpo(), msp())));
339
340 RoutingHeaders::open(cpo()).write_to(&mut headers);
341 let parsed = RoutingHeaders::from_headers(&headers).unwrap();
342 assert_eq!(parsed.to, None, "writing an open request clears any previous TO headers");
343 assert_eq!(parsed.from, cpo());
344 }
345}