1use 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#[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 #[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 #[must_use]
67 pub const fn version(&self) -> &VersionNumber {
68 &self.version
69 }
70
71 #[must_use]
73 pub const fn token(&self) -> &CredentialsToken {
74 &self.token
75 }
76
77 #[must_use]
79 pub const fn versions_url(&self) -> Option<&Url> {
80 self.versions_url.as_ref()
81 }
82
83 #[must_use]
85 pub fn parties(&self) -> &[PartyRef] {
86 &self.parties
87 }
88
89 #[must_use]
91 pub const fn hub(&self) -> Option<&PartyRef> {
92 self.hub.as_ref()
93 }
94
95 #[must_use]
97 pub const fn quirks(&self) -> &Quirks {
98 &self.quirks
99 }
100
101 pub fn set_quirks(&mut self, quirks: Quirks) {
103 self.quirks = quirks;
104 }
105
106 pub fn set_token(&mut self, token: CredentialsToken) {
108 self.token = token;
109 }
110
111 #[must_use]
113 pub fn implements(&self, module: &ModuleId, role: InterfaceRole) -> bool {
114 self.endpoint_url(module, role).is_some()
115 }
116
117 #[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 #[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 #[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 #[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 pub fn endpoints(&self) -> impl Iterator<Item = (&ModuleId, InterfaceRole, &Url)> {
156 self.endpoints.iter().map(|((m, r), url)| (m, *r, url))
157 }
158
159 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 #[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 #[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#[derive(Debug)]
193pub struct PeerBuilder {
194 peer: Peer,
195 quirks: Quirks,
196}
197
198impl PeerBuilder {
199 #[must_use]
201 pub fn versions_url(mut self, url: Url) -> Self {
202 self.peer.versions_url = Some(url);
203 self
204 }
205
206 #[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 #[must_use]
215 pub fn endpoints_from(mut self, details: &VersionDetails) -> Self {
216 self.peer.update_endpoints(details);
217 self
218 }
219
220 #[must_use]
222 pub fn party(mut self, party: PartyRef) -> Self {
223 self.peer.parties.push(party);
224 self
225 }
226
227 #[must_use]
229 pub fn hub(mut self, hub: PartyRef) -> Self {
230 self.peer.hub = Some(hub);
231 self
232 }
233
234 #[must_use]
236 pub fn quirks(mut self, quirks: Quirks) -> Self {
237 self.quirks = quirks;
238 self
239 }
240
241 #[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 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 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}