Skip to main content

ocpi_kit/client/
peer.rs

1//! What this process knows about one connected platform.
2
3use std::collections::BTreeMap;
4
5use crate::transport::{CredentialsToken, Quirks, ReceiverEndpoint, RoutingScenario, SenderEndpoint};
6use crate::types::{PartyRef, Url};
7use crate::v2_3_0::versions::VersionDetails;
8use crate::{InterfaceRole, ModuleId, VersionNumber};
9
10/// A connected platform: the version agreed with it, where its endpoints are, and the token to
11/// authenticate with.
12///
13/// A `Peer` is built by the [registration handshake](super::Registration) or, for a connection
14/// that was established before this process started, from stored state with
15/// [`Peer::builder`].
16///
17/// ```
18/// use ocpi_kit::client::Peer;
19/// use ocpi_kit::transport::CredentialsToken;
20/// use ocpi_kit::types::{PartyRef, Url};
21/// use ocpi_kit::{InterfaceRole, ModuleId, VersionNumber};
22///
23/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
24/// let peer = Peer::builder(VersionNumber::V2_3_0, CredentialsToken::new("token-c")?)
25///     .versions_url(Url::new("https://cpo.example.com/ocpi/versions")?)
26///     .endpoint(ModuleId::Locations, InterfaceRole::Sender,
27///               Url::new("https://cpo.example.com/ocpi/cpo/2.3.0/locations")?)
28///     .party(PartyRef::new("NL", "TNM")?)
29///     .build();
30///
31/// assert!(peer.implements(&ModuleId::Locations, InterfaceRole::Sender));
32/// assert!(!peer.implements(&ModuleId::Cdrs, InterfaceRole::Sender));
33/// # Ok(())
34/// # }
35/// ```
36#[derive(Clone, Debug)]
37pub struct Peer {
38    version: VersionNumber,
39    token: CredentialsToken,
40    versions_url: Option<Url>,
41    endpoints: BTreeMap<(ModuleId, InterfaceRole), Url>,
42    parties: Vec<PartyRef>,
43    hub: Option<PartyRef>,
44    quirks: Quirks,
45}
46
47impl Peer {
48    /// Starts building a peer that was registered in a previous run of this process.
49    #[must_use]
50    pub fn builder(version: VersionNumber, token: CredentialsToken) -> PeerBuilder {
51        PeerBuilder {
52            quirks: Quirks::for_version(&version),
53            peer: Self {
54                version,
55                token,
56                versions_url: None,
57                endpoints: BTreeMap::new(),
58                parties: Vec::new(),
59                hub: None,
60                quirks: Quirks::default(),
61            },
62        }
63    }
64
65    /// The OCPI version agreed with this peer.
66    #[must_use]
67    pub const fn version(&self) -> &VersionNumber {
68        &self.version
69    }
70
71    /// The credentials token to authenticate requests to this peer with.
72    #[must_use]
73    pub const fn token(&self) -> &CredentialsToken {
74        &self.token
75    }
76
77    /// The peer's `/versions` endpoint.
78    #[must_use]
79    pub const fn versions_url(&self) -> Option<&Url> {
80        self.versions_url.as_ref()
81    }
82
83    /// The parties this peer speaks for, from the `roles` of its credentials.
84    #[must_use]
85    pub fn parties(&self) -> &[PartyRef] {
86        &self.parties
87    }
88
89    /// The hub this peer routes through, if it declared one.
90    #[must_use]
91    pub const fn hub(&self) -> Option<&PartyRef> {
92        self.hub.as_ref()
93    }
94
95    /// The interoperability profile for this peer.
96    #[must_use]
97    pub const fn quirks(&self) -> &Quirks {
98        &self.quirks
99    }
100
101    /// Replaces the interoperability profile.
102    pub fn set_quirks(&mut self, quirks: Quirks) {
103        self.quirks = quirks;
104    }
105
106    /// Replaces the credentials token, as a credentials `PUT` does.
107    pub fn set_token(&mut self, token: CredentialsToken) {
108        self.token = token;
109    }
110
111    /// Whether the peer implements a module in a given role.
112    #[must_use]
113    pub fn implements(&self, module: &ModuleId, role: InterfaceRole) -> bool {
114        self.endpoint_url(module, role).is_some()
115    }
116
117    /// The peer's URL for a module and role.
118    ///
119    /// Module identifiers are matched case-insensitively when
120    /// [`Quirks::case_insensitive_module_ids`] is on, which matters for the `Booking` module.
121    #[must_use]
122    pub fn endpoint_url(&self, module: &ModuleId, role: InterfaceRole) -> Option<&Url> {
123        if let Some(url) = self.endpoints.get(&(module.clone(), role)) {
124            return Some(url);
125        }
126        if self.quirks.case_insensitive_module_ids {
127            return self
128                .endpoints
129                .iter()
130                .find(|((m, r), _)| *r == role && m.matches(module))
131                .map(|(_, url)| url);
132        }
133        None
134    }
135
136    /// The Sender-interface endpoint of a module.
137    #[must_use]
138    pub fn sender(&self, module: &ModuleId) -> Option<SenderEndpoint> {
139        self.endpoint_url(module, InterfaceRole::Sender).cloned().map(SenderEndpoint::new)
140    }
141
142    /// The Receiver-interface endpoint of a module.
143    #[must_use]
144    pub fn receiver(&self, module: &ModuleId) -> Option<ReceiverEndpoint> {
145        self.endpoint_url(module, InterfaceRole::Receiver).cloned().map(ReceiverEndpoint::new)
146    }
147
148    /// The credentials endpoint, ignoring the advertised role as the specification instructs.
149    #[must_use]
150    pub fn credentials_url(&self) -> Option<&Url> {
151        self.endpoints.iter().find(|((m, _), _)| m.matches(&ModuleId::Credentials)).map(|(_, url)| url)
152    }
153
154    /// Every endpoint the peer advertised.
155    pub fn endpoints(&self) -> impl Iterator<Item = (&ModuleId, InterfaceRole, &Url)> {
156        self.endpoints.iter().map(|((m, r), url)| (m, *r, url))
157    }
158
159    /// Replaces the endpoint map from freshly fetched version details.
160    ///
161    /// A credentials `PUT` requires this: *"The server must fetch the client's endpoints again,
162    /// even if the version has not changed."*
163    pub fn update_endpoints(&mut self, details: &VersionDetails) {
164        self.version = details.version.clone();
165        self.endpoints =
166            details.endpoints.iter().map(|e| ((e.identifier.clone(), e.role), e.url.clone())).collect();
167    }
168
169    /// The routing scenario for a direct request to `party`, or an open request when the
170    /// destination is unknown.
171    #[must_use]
172    pub fn routing_for(&self, party: Option<&PartyRef>) -> RoutingScenario {
173        match (party, self.hub.as_ref()) {
174            (None, Some(_)) => RoutingScenario::OpenRoutingRequest,
175            _ => RoutingScenario::Direct,
176        }
177    }
178
179    /// The party to address a functional request to, when the caller did not name one.
180    ///
181    /// A peer that speaks for exactly one party needs no explicit `to`.
182    #[must_use]
183    pub fn default_party(&self) -> Option<&PartyRef> {
184        match self.parties.as_slice() {
185            [only] => Some(only),
186            _ => None,
187        }
188    }
189}
190
191/// Builds a [`Peer`] from stored registration state.
192#[derive(Debug)]
193pub struct PeerBuilder {
194    peer: Peer,
195    quirks: Quirks,
196}
197
198impl PeerBuilder {
199    /// Sets the peer's `/versions` endpoint.
200    #[must_use]
201    pub fn versions_url(mut self, url: Url) -> Self {
202        self.peer.versions_url = Some(url);
203        self
204    }
205
206    /// Adds one endpoint.
207    #[must_use]
208    pub fn endpoint(mut self, module: ModuleId, role: InterfaceRole, url: Url) -> Self {
209        self.peer.endpoints.insert((module, role), url);
210        self
211    }
212
213    /// Adds every endpoint from a version details document.
214    #[must_use]
215    pub fn endpoints_from(mut self, details: &VersionDetails) -> Self {
216        self.peer.update_endpoints(details);
217        self
218    }
219
220    /// Adds a party this peer speaks for.
221    #[must_use]
222    pub fn party(mut self, party: PartyRef) -> Self {
223        self.peer.parties.push(party);
224        self
225    }
226
227    /// Names the hub this peer routes through.
228    #[must_use]
229    pub fn hub(mut self, hub: PartyRef) -> Self {
230        self.peer.hub = Some(hub);
231        self
232    }
233
234    /// Overrides the interoperability profile, which otherwise follows the version.
235    #[must_use]
236    pub fn quirks(mut self, quirks: Quirks) -> Self {
237        self.quirks = quirks;
238        self
239    }
240
241    /// Finishes the peer.
242    #[must_use]
243    pub fn build(mut self) -> Peer {
244        self.peer.quirks = self.quirks;
245        self.peer
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    fn url(path: &str) -> Url {
254        Url::new(format!("https://cpo.example.com/ocpi/{path}")).unwrap()
255    }
256
257    fn peer() -> Peer {
258        Peer::builder(VersionNumber::V2_3_0, CredentialsToken::new("token-c").unwrap())
259            .versions_url(url("versions"))
260            .endpoint(ModuleId::Credentials, InterfaceRole::Receiver, url("cpo/2.3.0/credentials"))
261            .endpoint(ModuleId::Locations, InterfaceRole::Sender, url("cpo/2.3.0/locations"))
262            .party(PartyRef::new("NL", "TNM").unwrap())
263            .build()
264    }
265
266    #[test]
267    fn endpoints_are_looked_up_by_module_and_role() {
268        let p = peer();
269        assert!(p.implements(&ModuleId::Locations, InterfaceRole::Sender));
270        assert!(!p.implements(&ModuleId::Locations, InterfaceRole::Receiver));
271        assert_eq!(p.sender(&ModuleId::Locations).unwrap().base(), &url("cpo/2.3.0/locations"));
272        assert!(p.receiver(&ModuleId::Locations).is_none());
273    }
274
275    #[test]
276    fn the_credentials_endpoint_ignores_the_advertised_role() {
277        // The spec: "disregard the value of the role property of the Endpoint object for other
278        // platforms' credentials modules".
279        assert_eq!(peer().credentials_url(), Some(&url("cpo/2.3.0/credentials")));
280    }
281
282    #[test]
283    fn module_ids_match_case_insensitively_by_default() {
284        let p = Peer::builder(VersionNumber::V2_3_0, CredentialsToken::new("t").unwrap())
285            .endpoint(ModuleId::Custom("bookings".into()), InterfaceRole::Sender, url("bookings"))
286            .build();
287        // The spec writes the identifier `Booking`; this peer wrote `bookings`.
288        assert!(p.implements(&ModuleId::Booking, InterfaceRole::Sender));
289
290        let strict = Peer::builder(VersionNumber::V2_3_0, CredentialsToken::new("t").unwrap())
291            .endpoint(ModuleId::Custom("bookings".into()), InterfaceRole::Sender, url("bookings"))
292            .quirks(Quirks::strict())
293            .build();
294        assert!(!strict.implements(&ModuleId::Booking, InterfaceRole::Sender));
295    }
296
297    #[test]
298    fn quirks_follow_the_version_unless_overridden() {
299        let legacy = Peer::builder(VersionNumber::V2_1_1, CredentialsToken::new("t").unwrap()).build();
300        assert!(legacy.quirks().send_unencoded_token, "2.1.1 peers do not Base64 the token");
301        assert!(legacy.quirks().omit_routing_headers);
302        assert!(!peer().quirks().send_unencoded_token);
303    }
304
305    #[test]
306    fn a_single_party_peer_needs_no_explicit_destination() {
307        assert_eq!(peer().default_party(), Some(&PartyRef::new("NL", "TNM").unwrap()));
308        let platform = Peer::builder(VersionNumber::V2_3_0, CredentialsToken::new("t").unwrap())
309            .party(PartyRef::new("NL", "AAA").unwrap())
310            .party(PartyRef::new("NL", "BBB").unwrap())
311            .build();
312        assert_eq!(platform.default_party(), None, "a multi-party platform must be addressed");
313    }
314}