1use http::Method;
32
33use crate::convert::wire::ObjectKind;
34use crate::transport::{CredentialsToken, OcpiError, OcpiRequest, Quirks};
35use crate::types::{PartyRef, Url};
36use crate::v2_3_0::credentials::Credentials;
37use crate::v2_3_0::versions::{Version, VersionDetails};
38use crate::{InterfaceRole, ModuleId, VersionNumber};
39
40use super::http::Transport;
41use super::peer::Peer;
42
43#[derive(Debug)]
48pub struct Registration {
49 versions_url: Url,
50 token_a: CredentialsToken,
51 quirks_override: Option<Quirks>,
54}
55
56impl Registration {
57 #[must_use]
59 pub fn new(versions_url: Url, token_a: CredentialsToken) -> Self {
60 Self { versions_url, token_a, quirks_override: None }
61 }
62
63 #[must_use]
68 pub fn with_quirks(mut self, quirks: Quirks) -> Self {
69 self.quirks_override = Some(quirks);
70 self
71 }
72
73 pub async fn discover(self, transport: &Transport) -> Result<Discovered, OcpiError> {
79 let quirks = self.quirks_override.clone().unwrap_or_default();
82 let request = OcpiRequest::new(Method::GET, self.versions_url.clone(), ModuleId::Versions);
83 let versions: Vec<Version> = transport.send(&request, &self.token_a, &quirks).await?;
84 Ok(Discovered {
85 versions_url: self.versions_url,
86 token_a: self.token_a,
87 quirks_override: self.quirks_override,
88 versions,
89 })
90 }
91}
92
93#[derive(Debug)]
95pub struct Discovered {
96 versions_url: Url,
97 token_a: CredentialsToken,
98 quirks_override: Option<Quirks>,
99 versions: Vec<Version>,
100}
101
102impl Discovered {
103 #[must_use]
105 pub fn versions(&self) -> &[Version] {
106 &self.versions
107 }
108
109 #[must_use]
111 pub fn best_common_version(&self) -> Option<&Version> {
112 self.versions
113 .iter()
114 .filter(|v| v.version.is_supported())
115 .max_by(|a, b| a.version.cmp_by_release(&b.version))
116 }
117
118 pub async fn select_best(self, transport: &Transport) -> Result<Selected, OcpiError> {
125 let chosen =
126 self.best_common_version().map(|v| v.version.clone()).ok_or_else(|| OcpiError::Remote {
127 status_code: crate::transport::StatusCode::UNSUPPORTED_VERSION,
128 status_message: Some(format!(
129 "peer supports {}, this build supports {}",
130 self.versions.iter().map(|v| v.version.to_string()).collect::<Vec<_>>().join(", "),
131 VersionNumber::supported().iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
132 )),
133 })?;
134 self.select(transport, &chosen).await
135 }
136
137 pub async fn select(self, transport: &Transport, version: &VersionNumber) -> Result<Selected, OcpiError> {
143 let entry =
144 self.versions.iter().find(|v| v.version == *version).ok_or_else(|| OcpiError::Remote {
145 status_code: crate::transport::StatusCode::UNSUPPORTED_VERSION,
146 status_message: Some(format!("peer does not offer OCPI {version}")),
147 })?;
148 let quirks = self.quirks_override.clone().unwrap_or_else(|| Quirks::for_version(version));
150 let request = OcpiRequest::new(Method::GET, entry.url.clone(), ModuleId::Versions);
151 let details: VersionDetails = transport.send(&request, &self.token_a, &quirks).await?;
152 Ok(Selected {
153 versions_url: self.versions_url,
154 token_a: self.token_a,
155 quirks,
156 version: version.clone(),
157 details,
158 })
159 }
160}
161
162#[derive(Debug)]
165pub struct Selected {
166 versions_url: Url,
167 token_a: CredentialsToken,
168 quirks: Quirks,
169 version: VersionNumber,
170 details: VersionDetails,
171}
172
173impl Selected {
174 #[must_use]
176 pub const fn version(&self) -> &VersionNumber {
177 &self.version
178 }
179
180 #[must_use]
182 pub const fn details(&self) -> &VersionDetails {
183 &self.details
184 }
185
186 pub fn require(&self, required: &[(ModuleId, InterfaceRole)]) -> Result<(), OcpiError> {
198 let missing = self.details.missing(required);
199 if missing.is_empty() {
200 return Ok(());
201 }
202 Err(OcpiError::Remote {
203 status_code: crate::transport::StatusCode::NO_MATCHING_ENDPOINTS,
204 status_message: Some(format!(
205 "peer does not implement {}",
206 missing.iter().map(|(m, r)| format!("{m}/{r}")).collect::<Vec<_>>().join(", ")
207 )),
208 })
209 }
210
211 pub async fn register(self, transport: &Transport, credentials: &Credentials) -> Result<Peer, OcpiError> {
227 super::http::check_outgoing(credentials, transport.config())?;
228 let url = self.details.credentials_url().cloned().ok_or_else(|| OcpiError::Remote {
229 status_code: crate::transport::StatusCode::NO_MATCHING_ENDPOINTS,
230 status_message: Some(
231 "peer advertised no credentials endpoint, which every implementation must have".to_owned(),
232 ),
233 })?;
234 let request = self.credentials_request(Method::POST, url, credentials)?;
235 let theirs = self.their_credentials(transport, &request, &self.token_a).await?;
236 Ok(peer_from(self.version, self.quirks, self.versions_url, &self.details, &theirs))
237 }
238
239 pub async fn update(
250 self,
251 transport: &Transport,
252 current_token: &CredentialsToken,
253 credentials: &Credentials,
254 ) -> Result<Peer, OcpiError> {
255 super::http::check_outgoing(credentials, transport.config())?;
256 let url = self.details.credentials_url().cloned().ok_or_else(|| OcpiError::Remote {
257 status_code: crate::transport::StatusCode::NO_MATCHING_ENDPOINTS,
258 status_message: Some("peer advertised no credentials endpoint".to_owned()),
259 })?;
260 let request = self.credentials_request(Method::PUT, url, credentials)?;
261 let theirs = self.their_credentials(transport, &request, current_token).await?;
262 Ok(peer_from(self.version, self.quirks, self.versions_url, &self.details, &theirs))
263 }
264
265 fn credentials_request(
272 &self,
273 method: Method,
274 url: Url,
275 credentials: &Credentials,
276 ) -> Result<OcpiRequest, OcpiError> {
277 let request = OcpiRequest::new(method, url, ModuleId::Credentials);
278 match self.bridge_out(credentials)? {
279 Some(value) => request.with_body(&value),
280 None => request.with_body(credentials),
281 }
282 }
283
284 fn bridge_out(&self, credentials: &Credentials) -> Result<Option<serde_json::Value>, OcpiError> {
285 if self.version == crate::CANONICAL_VERSION {
286 return Ok(None);
287 }
288 let value = serde_json::to_value(credentials)
289 .map_err(|e| OcpiError::Decode { path: "/".to_owned(), message: e.to_string() })?;
290 let converted = ObjectKind::Credentials
291 .bridge(&crate::CANONICAL_VERSION, &self.version, value)
292 .map_err(|e| OcpiError::Unsupported(e.to_string()))?;
293 if let Some(note) = converted.lossy.to_status_message() {
294 tracing::warn!(ocpi.peer_version = %self.version, "{note}");
295 }
296 Ok(Some(converted.value))
297 }
298
299 async fn their_credentials(
300 &self,
301 transport: &Transport,
302 request: &OcpiRequest,
303 token: &CredentialsToken,
304 ) -> Result<Credentials, OcpiError> {
305 if self.version == crate::CANONICAL_VERSION {
306 return transport.send(request, token, &self.quirks).await;
307 }
308 let value: serde_json::Value = transport.send(request, token, &self.quirks).await?;
309 let converted = ObjectKind::Credentials
310 .bridge(&self.version, &crate::CANONICAL_VERSION, value)
311 .map_err(|e| OcpiError::Unsupported(e.to_string()))?;
312 serde_json::from_value(converted.value)
313 .map_err(|e| OcpiError::Decode { path: "/".to_owned(), message: e.to_string() })
314 }
315}
316
317fn peer_from(
318 version: VersionNumber,
319 quirks: Quirks,
320 versions_url: Url,
321 details: &VersionDetails,
322 theirs: &Credentials,
323) -> Peer {
324 let mut builder = Peer::builder(version, CredentialsToken::new_lenient(theirs.token.as_str()))
325 .versions_url(versions_url)
326 .endpoints_from(details)
327 .quirks(quirks);
328 for party in theirs.parties() {
329 builder = builder.party(party);
330 }
331 if let Some(hub) = theirs.hub_party() {
332 builder = builder.hub(hub);
333 }
334 builder.build()
335}
336
337#[derive(Clone, Debug, PartialEq)]
344#[non_exhaustive]
345pub enum PeerState {
346 Bootstrapped {
350 versions_url: Url,
352 token_a: CredentialsToken,
354 },
355 Registered {
357 version: VersionNumber,
359 our_token_for_them: CredentialsToken,
361 their_token_for_us: CredentialsToken,
363 parties: Vec<PartyRef>,
365 },
366 Unregistered,
370}
371
372impl PeerState {
373 #[must_use]
377 pub const fn may_use_functional_modules(&self) -> bool {
378 matches!(self, Self::Registered { .. })
379 }
380
381 #[must_use]
386 pub const fn accepts_credentials_post(&self) -> bool {
387 matches!(self, Self::Bootstrapped { .. } | Self::Unregistered)
388 }
389
390 #[must_use]
395 pub const fn accepts_credentials_put_or_delete(&self) -> bool {
396 matches!(self, Self::Registered { .. })
397 }
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 fn token(v: &str) -> CredentialsToken {
405 CredentialsToken::new(v).unwrap()
406 }
407
408 #[test]
409 fn credentials_methods_are_gated_on_the_registration_state() {
410 let bootstrapped = PeerState::Bootstrapped {
411 versions_url: Url::new("https://e.com/versions").unwrap(),
412 token_a: token("A"),
413 };
414 assert!(bootstrapped.accepts_credentials_post());
415 assert!(!bootstrapped.accepts_credentials_put_or_delete(), "405 until registered");
416 assert!(!bootstrapped.may_use_functional_modules());
417
418 let registered = PeerState::Registered {
419 version: VersionNumber::V2_3_0,
420 our_token_for_them: token("C"),
421 their_token_for_us: token("B"),
422 parties: vec![PartyRef::new("NL", "TNM").unwrap()],
423 };
424 assert!(!registered.accepts_credentials_post(), "405 once registered");
425 assert!(registered.accepts_credentials_put_or_delete());
426 assert!(registered.may_use_functional_modules());
427
428 assert!(PeerState::Unregistered.accepts_credentials_post());
429 assert!(!PeerState::Unregistered.may_use_functional_modules());
430 }
431
432 #[test]
433 fn the_newest_common_version_is_selected() {
434 let versions = vec![
435 Version::new(VersionNumber::V2_1_1, Url::new("https://e.com/2.1.1").unwrap()),
436 Version::new(VersionNumber::V2_2_1, Url::new("https://e.com/2.2.1").unwrap()),
437 Version::new("3.0".into(), Url::new("https://e.com/3.0").unwrap()),
438 ];
439 let discovered = Discovered {
440 versions_url: Url::new("https://e.com/versions").unwrap(),
441 token_a: token("A"),
442 quirks_override: None,
443 versions,
444 };
445 assert_eq!(discovered.best_common_version().map(|v| v.version.clone()), Some(VersionNumber::V2_2_1));
447 }
448
449 #[test]
450 fn missing_required_endpoints_stop_the_handshake_before_anything_is_sent() {
451 use crate::v2_3_0::versions::Endpoint;
452 let details = VersionDetails::new(
453 VersionNumber::V2_3_0,
454 vec![Endpoint::new(
455 ModuleId::Credentials,
456 InterfaceRole::Sender,
457 Url::new("https://e.com/credentials").unwrap(),
458 )],
459 );
460 let selected = Selected {
461 versions_url: Url::new("https://e.com/versions").unwrap(),
462 token_a: token("A"),
463 quirks: Quirks::default(),
464 version: VersionNumber::V2_3_0,
465 details,
466 };
467 assert!(selected.require(&[(ModuleId::Credentials, InterfaceRole::Sender)]).is_ok());
468 let err = selected.require(&[(ModuleId::Cdrs, InterfaceRole::Receiver)]).unwrap_err();
469 assert_eq!(err.status_code(), crate::transport::StatusCode::NO_MATCHING_ENDPOINTS);
470 assert!(err.to_string().contains("cdrs"), "{err}");
471 }
472}