use http::Method;
use crate::convert::wire::ObjectKind;
use crate::transport::{CredentialsToken, OcpiError, OcpiRequest, Quirks};
use crate::types::{PartyRef, Url};
use crate::v2_3_0::credentials::Credentials;
use crate::v2_3_0::versions::{Version, VersionDetails};
use crate::{InterfaceRole, ModuleId, VersionNumber};
use super::http::Transport;
use super::peer::Peer;
#[derive(Debug)]
pub struct Registration {
versions_url: Url,
token_a: CredentialsToken,
quirks_override: Option<Quirks>,
}
impl Registration {
#[must_use]
pub fn new(versions_url: Url, token_a: CredentialsToken) -> Self {
Self { versions_url, token_a, quirks_override: None }
}
#[must_use]
pub fn with_quirks(mut self, quirks: Quirks) -> Self {
self.quirks_override = Some(quirks);
self
}
pub async fn discover(self, transport: &Transport) -> Result<Discovered, OcpiError> {
let quirks = self.quirks_override.clone().unwrap_or_default();
let request = OcpiRequest::new(Method::GET, self.versions_url.clone(), ModuleId::Versions);
let versions: Vec<Version> = transport.send(&request, &self.token_a, &quirks).await?;
Ok(Discovered {
versions_url: self.versions_url,
token_a: self.token_a,
quirks_override: self.quirks_override,
versions,
})
}
}
#[derive(Debug)]
pub struct Discovered {
versions_url: Url,
token_a: CredentialsToken,
quirks_override: Option<Quirks>,
versions: Vec<Version>,
}
impl Discovered {
#[must_use]
pub fn versions(&self) -> &[Version] {
&self.versions
}
#[must_use]
pub fn best_common_version(&self) -> Option<&Version> {
self.versions
.iter()
.filter(|v| v.version.is_supported())
.max_by(|a, b| a.version.cmp_by_release(&b.version))
}
pub async fn select_best(self, transport: &Transport) -> Result<Selected, OcpiError> {
let chosen =
self.best_common_version().map(|v| v.version.clone()).ok_or_else(|| OcpiError::Remote {
status_code: crate::transport::StatusCode::UNSUPPORTED_VERSION,
status_message: Some(format!(
"peer supports {}, this build supports {}",
self.versions.iter().map(|v| v.version.to_string()).collect::<Vec<_>>().join(", "),
VersionNumber::supported().iter().map(ToString::to_string).collect::<Vec<_>>().join(", ")
)),
})?;
self.select(transport, &chosen).await
}
pub async fn select(self, transport: &Transport, version: &VersionNumber) -> Result<Selected, OcpiError> {
let entry =
self.versions.iter().find(|v| v.version == *version).ok_or_else(|| OcpiError::Remote {
status_code: crate::transport::StatusCode::UNSUPPORTED_VERSION,
status_message: Some(format!("peer does not offer OCPI {version}")),
})?;
let quirks = self.quirks_override.clone().unwrap_or_else(|| Quirks::for_version(version));
let request = OcpiRequest::new(Method::GET, entry.url.clone(), ModuleId::Versions);
let details: VersionDetails = transport.send(&request, &self.token_a, &quirks).await?;
Ok(Selected {
versions_url: self.versions_url,
token_a: self.token_a,
quirks,
version: version.clone(),
details,
})
}
}
#[derive(Debug)]
pub struct Selected {
versions_url: Url,
token_a: CredentialsToken,
quirks: Quirks,
version: VersionNumber,
details: VersionDetails,
}
impl Selected {
#[must_use]
pub const fn version(&self) -> &VersionNumber {
&self.version
}
#[must_use]
pub const fn details(&self) -> &VersionDetails {
&self.details
}
pub fn require(&self, required: &[(ModuleId, InterfaceRole)]) -> Result<(), OcpiError> {
let missing = self.details.missing(required);
if missing.is_empty() {
return Ok(());
}
Err(OcpiError::Remote {
status_code: crate::transport::StatusCode::NO_MATCHING_ENDPOINTS,
status_message: Some(format!(
"peer does not implement {}",
missing.iter().map(|(m, r)| format!("{m}/{r}")).collect::<Vec<_>>().join(", ")
)),
})
}
pub async fn register(self, transport: &Transport, credentials: &Credentials) -> Result<Peer, OcpiError> {
super::http::check_outgoing(credentials, transport.config())?;
let url = self.details.credentials_url().cloned().ok_or_else(|| OcpiError::Remote {
status_code: crate::transport::StatusCode::NO_MATCHING_ENDPOINTS,
status_message: Some(
"peer advertised no credentials endpoint, which every implementation must have".to_owned(),
),
})?;
let request = self.credentials_request(Method::POST, url, credentials)?;
let theirs = self.their_credentials(transport, &request, &self.token_a).await?;
Ok(peer_from(self.version, self.quirks, self.versions_url, &self.details, &theirs))
}
pub async fn update(
self,
transport: &Transport,
current_token: &CredentialsToken,
credentials: &Credentials,
) -> Result<Peer, OcpiError> {
super::http::check_outgoing(credentials, transport.config())?;
let url = self.details.credentials_url().cloned().ok_or_else(|| OcpiError::Remote {
status_code: crate::transport::StatusCode::NO_MATCHING_ENDPOINTS,
status_message: Some("peer advertised no credentials endpoint".to_owned()),
})?;
let request = self.credentials_request(Method::PUT, url, credentials)?;
let theirs = self.their_credentials(transport, &request, current_token).await?;
Ok(peer_from(self.version, self.quirks, self.versions_url, &self.details, &theirs))
}
fn credentials_request(
&self,
method: Method,
url: Url,
credentials: &Credentials,
) -> Result<OcpiRequest, OcpiError> {
let request = OcpiRequest::new(method, url, ModuleId::Credentials);
match self.bridge_out(credentials)? {
Some(value) => request.with_body(&value),
None => request.with_body(credentials),
}
}
fn bridge_out(&self, credentials: &Credentials) -> Result<Option<serde_json::Value>, OcpiError> {
if self.version == crate::CANONICAL_VERSION {
return Ok(None);
}
let value = serde_json::to_value(credentials)
.map_err(|e| OcpiError::Decode { path: "/".to_owned(), message: e.to_string() })?;
let converted = ObjectKind::Credentials
.bridge(&crate::CANONICAL_VERSION, &self.version, value)
.map_err(|e| OcpiError::Unsupported(e.to_string()))?;
if let Some(note) = converted.lossy.to_status_message() {
tracing::warn!(ocpi.peer_version = %self.version, "{note}");
}
Ok(Some(converted.value))
}
async fn their_credentials(
&self,
transport: &Transport,
request: &OcpiRequest,
token: &CredentialsToken,
) -> Result<Credentials, OcpiError> {
if self.version == crate::CANONICAL_VERSION {
return transport.send(request, token, &self.quirks).await;
}
let value: serde_json::Value = transport.send(request, token, &self.quirks).await?;
let converted = ObjectKind::Credentials
.bridge(&self.version, &crate::CANONICAL_VERSION, value)
.map_err(|e| OcpiError::Unsupported(e.to_string()))?;
serde_json::from_value(converted.value)
.map_err(|e| OcpiError::Decode { path: "/".to_owned(), message: e.to_string() })
}
}
fn peer_from(
version: VersionNumber,
quirks: Quirks,
versions_url: Url,
details: &VersionDetails,
theirs: &Credentials,
) -> Peer {
let mut builder = Peer::builder(version, CredentialsToken::new_lenient(theirs.token.as_str()))
.versions_url(versions_url)
.endpoints_from(details)
.quirks(quirks);
for party in theirs.parties() {
builder = builder.party(party);
}
if let Some(hub) = theirs.hub_party() {
builder = builder.hub(hub);
}
builder.build()
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum PeerState {
Bootstrapped {
versions_url: Url,
token_a: CredentialsToken,
},
Registered {
version: VersionNumber,
our_token_for_them: CredentialsToken,
their_token_for_us: CredentialsToken,
parties: Vec<PartyRef>,
},
Unregistered,
}
impl PeerState {
#[must_use]
pub const fn may_use_functional_modules(&self) -> bool {
matches!(self, Self::Registered { .. })
}
#[must_use]
pub const fn accepts_credentials_post(&self) -> bool {
matches!(self, Self::Bootstrapped { .. } | Self::Unregistered)
}
#[must_use]
pub const fn accepts_credentials_put_or_delete(&self) -> bool {
matches!(self, Self::Registered { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn token(v: &str) -> CredentialsToken {
CredentialsToken::new(v).unwrap()
}
#[test]
fn credentials_methods_are_gated_on_the_registration_state() {
let bootstrapped = PeerState::Bootstrapped {
versions_url: Url::new("https://e.com/versions").unwrap(),
token_a: token("A"),
};
assert!(bootstrapped.accepts_credentials_post());
assert!(!bootstrapped.accepts_credentials_put_or_delete(), "405 until registered");
assert!(!bootstrapped.may_use_functional_modules());
let registered = PeerState::Registered {
version: VersionNumber::V2_3_0,
our_token_for_them: token("C"),
their_token_for_us: token("B"),
parties: vec![PartyRef::new("NL", "TNM").unwrap()],
};
assert!(!registered.accepts_credentials_post(), "405 once registered");
assert!(registered.accepts_credentials_put_or_delete());
assert!(registered.may_use_functional_modules());
assert!(PeerState::Unregistered.accepts_credentials_post());
assert!(!PeerState::Unregistered.may_use_functional_modules());
}
#[test]
fn the_newest_common_version_is_selected() {
let versions = vec![
Version::new(VersionNumber::V2_1_1, Url::new("https://e.com/2.1.1").unwrap()),
Version::new(VersionNumber::V2_2_1, Url::new("https://e.com/2.2.1").unwrap()),
Version::new("3.0".into(), Url::new("https://e.com/3.0").unwrap()),
];
let discovered = Discovered {
versions_url: Url::new("https://e.com/versions").unwrap(),
token_a: token("A"),
quirks_override: None,
versions,
};
assert_eq!(discovered.best_common_version().map(|v| v.version.clone()), Some(VersionNumber::V2_2_1));
}
#[test]
fn missing_required_endpoints_stop_the_handshake_before_anything_is_sent() {
use crate::v2_3_0::versions::Endpoint;
let details = VersionDetails::new(
VersionNumber::V2_3_0,
vec![Endpoint::new(
ModuleId::Credentials,
InterfaceRole::Sender,
Url::new("https://e.com/credentials").unwrap(),
)],
);
let selected = Selected {
versions_url: Url::new("https://e.com/versions").unwrap(),
token_a: token("A"),
quirks: Quirks::default(),
version: VersionNumber::V2_3_0,
details,
};
assert!(selected.require(&[(ModuleId::Credentials, InterfaceRole::Sender)]).is_ok());
let err = selected.require(&[(ModuleId::Cdrs, InterfaceRole::Receiver)]).unwrap_err();
assert_eq!(err.status_code(), crate::transport::StatusCode::NO_MATCHING_ENDPOINTS);
assert!(err.to_string().contains("cdrs"), "{err}");
}
}