Skip to main content

ocpi_kit/hub/
mod.rs

1//! A roaming hub: routing, broadcast push, open routing, GET All, and version bridging.
2//!
3//! # The four routing arrangements
4//!
5//! A hub can tell which one it is looking at from the headers and the method alone, which is what
6//! [`Forwardable::scenario`] does:
7//!
8//! | The `OCPI-to-*` headers say | Method | It is |
9//! |---|---|---|
10//! | another party | any | [`Direct`](crate::transport::RoutingScenario::Direct) — relay it |
11//! | the hub itself | `GET` on a Sender interface | [`GetAllViaHub`](crate::transport::RoutingScenario::GetAllViaHub) — merge every party's objects |
12//! | the hub itself | a write on a Receiver interface | [`BroadcastPush`](crate::transport::RoutingScenario::BroadcastPush) — fan out to the opposite roles |
13//! | nothing | any | [`OpenRoutingRequest`](crate::transport::RoutingScenario::OpenRoutingRequest) — decide from the content |
14//! | the hub itself | anything else | refused: [`OcpiError::NotRoutable`](crate::transport::OcpiError::NotRoutable), a `2001` |
15//!
16//! Addressing the hub is the one ambiguous case, and two of its four combinations are not
17//! scenarios at all: a `GET` on a Receiver interface is not a Broadcast Push, because *"GET SHALL
18//! NOT be used in combination with Broadcast Push"*, and a write on a Sender interface is neither
19//! a push to the connected parties nor a read to merge. Guessing the nearest scenario would mean
20//! a hub quietly broadcasting a read, so [`Forwardable::scenario`] refuses, with the advice the
21//! specification itself gives: omit the `OCPI-to-` headers and make it an Open Routing Request.
22//!
23//! # The rules the hub must not break
24//!
25//! * **A new `X-Request-ID`, the same `X-Correlation-ID`.** [`RequestIds::forwarded`](crate::transport::RequestIds::forwarded).
26//! * **`last_updated` is never touched.** *"When OCPI Objects are sent via Hubs, the
27//!   `last_updated` fields SHALL NOT be updated by the Hub."* Nothing in this module writes it.
28//! * **`GET` is never broadcast.** *"GET SHALL NOT be used in combination with Broadcast Push."*
29//! * **Configuration modules are never routed.** `credentials`, `versions` and `hubclientinfo`
30//!   are platform-to-hub conversations.
31//! * **Vendor data survives.** Because every object keeps its unknown fields in
32//!   [`Extensions`](crate::types::Extensions) and every `OpenEnum` keeps values it does not know,
33//!   a hub built on this crate forwards an extension it has never seen without damaging it. That
34//!   is what OCPI 2.3.0's extensibility chapter asks for, and it is the single most common way a
35//!   hub loses data.
36//!
37//! # Version bridging
38//!
39//! A 2.2.1 CPO and a 2.3.0 eMSP talk through the hub without either of them knowing: [`Forwarder`]
40//! translates the request body into the version the receiving platform speaks, translates the
41//! response back, and appends what the crossing cost to the `status_message` so the requesting
42//! party can see it. That is [`convert`](crate::convert) doing the work and
43//! [`Lossy`](crate::convert::Lossy) doing the reporting; no object is ever handed to a party in a
44//! version it did not ask for, and nothing is dropped silently.
45//!
46//! Only the 2.2.1 ↔ 2.3.0 crossing has conversions today. A message between two versions this
47//! build cannot translate is **refused** by default rather than relayed — see [`Unbridgeable`] for
48//! the reasoning and for how to relay it anyway. [`bridge`] exposes the same classification for a
49//! hub that wants to translate an object itself.
50//!
51//! Spec: 2.3.0 §transport_and_format_message_routing, §status_codes_4xxx_hub_errors
52
53mod forwarder;
54mod routing_table;
55
56pub use forwarder::{
57    AggregatePolicy, BodyOwnerRouter, Forwardable, Forwarder, OpenRouter, Relayed, Unbridgeable, aggregate,
58};
59pub use routing_table::{ConnectedPlatform, RoutingTable};
60
61use crate::VersionNumber;
62
63/// What a hub must do to a message crossing between two OCPI versions.
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
65#[non_exhaustive]
66pub enum Bridge {
67    /// Both sides speak the same version; forward the bytes unchanged.
68    ///
69    /// This is the fast path, and the one that cannot lose anything.
70    Passthrough,
71    /// The receiver speaks a newer version; upgrade.
72    Upgrade,
73    /// The receiver speaks an older version; downgrade, and report what did not fit.
74    Downgrade,
75    /// This build has no conversions between the two versions, so nothing can be translated.
76    ///
77    /// Today that is any crossing involving OCPI 2.1.1 — which has no owner fields, no routing
78    /// and no `Price`, so carrying an object across it is a decision about the deployment rather
79    /// than a translation — or a version this crate does not model at all.
80    ///
81    /// Relaying the bytes unchanged is still an option, and for two parties that understand each
82    /// other by some arrangement outside this crate it is the right one; the decision belongs to
83    /// the operator, so [`Forwarder`] takes it as [`Unbridgeable`] rather than assuming.
84    Unsupported,
85}
86
87/// What the hub must do to carry a message from `sender` to `receiver`.
88///
89/// ```
90/// use ocpi_kit::hub::{bridge, Bridge};
91/// use ocpi_kit::VersionNumber;
92///
93/// assert_eq!(bridge(&VersionNumber::V2_3_0, &VersionNumber::V2_3_0), Bridge::Passthrough);
94/// assert_eq!(bridge(&VersionNumber::V2_2_1, &VersionNumber::V2_3_0), Bridge::Upgrade);
95/// assert_eq!(bridge(&VersionNumber::V2_3_0, &VersionNumber::V2_2_1), Bridge::Downgrade);
96/// assert_eq!(bridge(&VersionNumber::V2_3_0, &"3.0".into()), Bridge::Unsupported);
97/// ```
98#[must_use]
99pub fn bridge(sender: &VersionNumber, receiver: &VersionNumber) -> Bridge {
100    if sender == receiver {
101        return Bridge::Passthrough;
102    }
103    // Whether the *models* exist is not the question; whether the *conversions* do is. A build
104    // with `v2_1_1` on models 2.1.1 perfectly well and still cannot carry an object out of it.
105    if !crate::convert::wire::bridgeable(sender, receiver) {
106        return Bridge::Unsupported;
107    }
108    if receiver.release_rank() > sender.release_rank() { Bridge::Upgrade } else { Bridge::Downgrade }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn the_same_version_is_a_passthrough() {
117        assert_eq!(bridge(&VersionNumber::V2_2_1, &VersionNumber::V2_2_1), Bridge::Passthrough);
118        // Even for a version this build does not model: two 3.0 parties can still talk.
119        let future: VersionNumber = "3.0".into();
120        assert_eq!(bridge(&future, &future), Bridge::Passthrough);
121    }
122
123    #[test]
124    fn direction_follows_the_release_order() {
125        assert_eq!(bridge(&VersionNumber::V2_2_1, &VersionNumber::V2_3_0), Bridge::Upgrade);
126        assert_eq!(bridge(&VersionNumber::V2_3_0, &VersionNumber::V2_2_1), Bridge::Downgrade);
127    }
128
129    #[test]
130    fn a_crossing_with_no_conversions_is_surfaced_rather_than_guessed_at() {
131        assert_eq!(bridge(&"3.0".into(), &VersionNumber::V2_3_0), Bridge::Unsupported);
132        assert_eq!(bridge(&VersionNumber::V2_0, &VersionNumber::V2_3_0), Bridge::Unsupported);
133        // 2.1.1 is modelled by this crate and still has no conversions: claiming an `Upgrade`
134        // here would promise a translation that does not exist.
135        assert_eq!(bridge(&VersionNumber::V2_1_1, &VersionNumber::V2_3_0), Bridge::Unsupported);
136        assert_eq!(bridge(&VersionNumber::V2_3_0, &VersionNumber::V2_1_1), Bridge::Unsupported);
137    }
138}