use std::collections::BTreeMap;
use std::sync::RwLock;
use crate::client::Peer;
use crate::transport::{OcpiError, StatusCode};
use crate::types::PartyRef;
use crate::v2_3_0::hub_client_info::ConnectionStatus;
use crate::v2_3_0::types::Role;
use crate::{InterfaceRole, ModuleId};
#[derive(Debug)]
pub struct ConnectedPlatform {
pub platform_id: String,
pub peer: Peer,
pub parties: Vec<(PartyRef, Role)>,
pub status: ConnectionStatus,
}
impl ConnectedPlatform {
#[must_use]
pub fn hosts(&self, party: &PartyRef) -> bool {
self.parties.iter().any(|(p, _)| p == party)
}
#[must_use]
pub fn role_of(&self, party: &PartyRef) -> Option<Role> {
self.parties.iter().find(|(p, _)| p == party).map(|(_, r)| *r)
}
#[must_use]
pub fn is_reachable(&self) -> bool {
self.status == ConnectionStatus::Connected
}
#[must_use]
pub fn implements(&self, module: &ModuleId, role: InterfaceRole) -> bool {
self.peer.implements(module, role)
}
}
#[derive(Debug, Default)]
pub struct RoutingTable {
platforms: RwLock<BTreeMap<String, ConnectedPlatform>>,
}
impl RoutingTable {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn upsert(&self, platform: ConnectedPlatform) {
self.platforms
.write()
.expect("routing table lock poisoned")
.insert(platform.platform_id.clone(), platform);
}
pub fn remove(&self, platform_id: &str) -> bool {
self.platforms.write().expect("routing table lock poisoned").remove(platform_id).is_some()
}
pub fn set_status(&self, platform_id: &str, status: ConnectionStatus) -> bool {
let mut platforms = self.platforms.write().expect("routing table lock poisoned");
match platforms.get_mut(platform_id) {
Some(platform) => {
platform.status = status;
true
}
None => false,
}
}
pub fn with_platform<T>(
&self,
party: &PartyRef,
f: impl FnOnce(&ConnectedPlatform) -> T,
) -> Result<T, OcpiError> {
let platforms = self.platforms.read().expect("routing table lock poisoned");
let platform = platforms.values().find(|p| p.hosts(party)).ok_or_else(|| OcpiError::Remote {
status_code: StatusCode::UNKNOWN_RECEIVER,
status_message: Some(format!("the hub does not know {party}")),
})?;
if !platform.is_reachable() {
return Err(OcpiError::Remote {
status_code: StatusCode::CONNECTION_PROBLEM,
status_message: Some(format!("{party} is {}", platform.status)),
});
}
Ok(f(platform))
}
#[must_use]
pub fn knows(&self, party: &PartyRef) -> bool {
self.platforms.read().expect("routing table lock poisoned").values().any(|p| p.hosts(party))
}
#[must_use]
pub fn platform_of(&self, party: &PartyRef) -> Option<String> {
self.platforms
.read()
.expect("routing table lock poisoned")
.values()
.find(|p| p.hosts(party))
.map(|p| p.platform_id.clone())
}
#[must_use]
pub fn broadcast_targets(
&self,
sender: &PartyRef,
sender_role: Role,
module: &ModuleId,
) -> Vec<(String, PartyRef)> {
let platforms = self.platforms.read().expect("routing table lock poisoned");
let mut targets = Vec::new();
for platform in platforms.values() {
if !platform.is_reachable() || !platform.implements(module, InterfaceRole::Receiver) {
continue;
}
for (party, role) in &platform.parties {
if party == sender {
continue;
}
if role.receives_broadcast_from(sender_role) {
targets.push((platform.platform_id.clone(), party.clone()));
}
}
}
targets
}
#[must_use]
pub fn get_all_sources(&self, requester: &PartyRef, module: &ModuleId) -> Vec<(String, PartyRef)> {
let platforms = self.platforms.read().expect("routing table lock poisoned");
let mut sources = Vec::new();
for platform in platforms.values() {
if !platform.is_reachable() || !platform.implements(module, InterfaceRole::Sender) {
continue;
}
for (party, _) in &platform.parties {
if party != requester {
sources.push((platform.platform_id.clone(), party.clone()));
}
}
}
sources
}
#[must_use]
pub fn platform_ids(&self) -> Vec<String> {
self.platforms.read().expect("routing table lock poisoned").keys().cloned().collect()
}
#[must_use]
pub fn client_info(&self) -> Vec<(PartyRef, Role, ConnectionStatus)> {
let platforms = self.platforms.read().expect("routing table lock poisoned");
platforms
.values()
.flat_map(|p| p.parties.iter().map(move |(party, role)| (party.clone(), *role, p.status)))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::VersionNumber;
use crate::transport::CredentialsToken;
use crate::types::Url;
fn platform(
id: &str,
parties: &[(&str, &str, Role)],
status: ConnectionStatus,
modules: &[(ModuleId, InterfaceRole)],
) -> ConnectedPlatform {
let mut builder = Peer::builder(VersionNumber::V2_3_0, CredentialsToken::new("t").unwrap());
for (module, role) in modules {
builder = builder.endpoint(
module.clone(),
*role,
Url::new(format!("https://{id}.example.com/{module}")).unwrap(),
);
}
ConnectedPlatform {
platform_id: id.to_owned(),
peer: builder.build(),
parties: parties
.iter()
.map(|(cc, pid, role)| (PartyRef::new(*cc, *pid).unwrap(), *role))
.collect(),
status,
}
}
fn table() -> RoutingTable {
let t = RoutingTable::new();
t.upsert(platform(
"cpo",
&[("NL", "TNM", Role::Cpo)],
ConnectionStatus::Connected,
&[(ModuleId::Locations, InterfaceRole::Sender), (ModuleId::Tokens, InterfaceRole::Receiver)],
));
t.upsert(platform(
"msp",
&[("DE", "ABC", Role::Emsp), ("DE", "XYZ", Role::Nsp)],
ConnectionStatus::Connected,
&[(ModuleId::Locations, InterfaceRole::Receiver), (ModuleId::Tokens, InterfaceRole::Sender)],
));
t.upsert(platform(
"gone",
&[("FR", "OLD", Role::Emsp)],
ConnectionStatus::Offline,
&[(ModuleId::Locations, InterfaceRole::Receiver)],
));
t
}
#[test]
fn an_unknown_party_is_4001_and_a_disconnected_one_is_4003() {
let t = table();
assert!(t.with_platform(&PartyRef::new("NL", "TNM").unwrap(), |p| p.platform_id.clone()).is_ok());
let unknown = t.with_platform(&PartyRef::new("XX", "NON").unwrap(), |_| ()).unwrap_err();
assert_eq!(unknown.status_code(), StatusCode::UNKNOWN_RECEIVER);
let offline = t.with_platform(&PartyRef::new("FR", "OLD").unwrap(), |_| ()).unwrap_err();
assert_eq!(offline.status_code(), StatusCode::CONNECTION_PROBLEM);
assert!(t.knows(&PartyRef::new("FR", "OLD").unwrap()), "known, just not reachable");
}
#[test]
fn a_broadcast_from_a_cpo_reaches_the_emsp_like_roles_that_implement_the_module() {
let t = table();
let targets =
t.broadcast_targets(&PartyRef::new("NL", "TNM").unwrap(), Role::Cpo, &ModuleId::Locations);
let parties: Vec<String> = targets.iter().map(|(_, p)| p.to_string()).collect();
assert!(parties.contains(&"DE/ABC".to_owned()), "{parties:?}");
assert!(parties.contains(&"DE/XYZ".to_owned()), "an NSP receives Locations too");
assert!(!parties.contains(&"FR/OLD".to_owned()), "an offline platform is skipped");
assert!(!parties.contains(&"NL/TNM".to_owned()), "the sender is not a target");
}
#[test]
fn a_broadcast_skips_platforms_that_do_not_implement_the_module() {
let t = table();
let targets =
t.broadcast_targets(&PartyRef::new("DE", "ABC").unwrap(), Role::Emsp, &ModuleId::Tokens);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].1, PartyRef::new("NL", "TNM").unwrap());
}
#[test]
fn get_all_collects_every_sender_but_the_requester() {
let t = table();
let sources = t.get_all_sources(&PartyRef::new("DE", "ABC").unwrap(), &ModuleId::Locations);
assert_eq!(sources.len(), 1);
assert_eq!(sources[0].1, PartyRef::new("NL", "TNM").unwrap());
}
#[test]
fn status_changes_take_effect_immediately() {
let t = table();
assert!(t.set_status("cpo", ConnectionStatus::Offline));
assert!(!t.set_status("nope", ConnectionStatus::Offline));
let err = t.with_platform(&PartyRef::new("NL", "TNM").unwrap(), |_| ()).unwrap_err();
assert_eq!(err.status_code(), StatusCode::CONNECTION_PROBLEM);
assert_eq!(t.client_info().len(), 4);
}
}