use core::fmt;
use http::{HeaderMap, HeaderValue};
use crate::ModuleId;
use crate::types::PartyRef;
use super::headers::{
OCPI_FROM_COUNTRY_CODE, OCPI_FROM_PARTY_ID, OCPI_TO_COUNTRY_CODE, OCPI_TO_PARTY_ID, header_party,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RoutingHeaders {
pub to: Option<PartyRef>,
pub from: PartyRef,
}
impl RoutingHeaders {
#[must_use]
pub fn new(from: PartyRef, to: PartyRef) -> Self {
Self { to: Some(to), from }
}
#[must_use]
pub fn open(from: PartyRef) -> Self {
Self { to: None, from }
}
#[must_use]
pub fn response_from(&self, responder: PartyRef) -> Self {
Self { to: Some(self.from.clone()), from: responder }
}
#[must_use]
pub fn from_headers(headers: &HeaderMap) -> Option<Self> {
Some(Self {
to: header_party(headers, &OCPI_TO_COUNTRY_CODE, &OCPI_TO_PARTY_ID),
from: header_party(headers, &OCPI_FROM_COUNTRY_CODE, &OCPI_FROM_PARTY_ID)?,
})
}
pub fn write_to(&self, headers: &mut HeaderMap) {
if let Some(to) = &self.to {
insert_party(headers, &OCPI_TO_COUNTRY_CODE, &OCPI_TO_PARTY_ID, to);
} else {
headers.remove(OCPI_TO_COUNTRY_CODE);
headers.remove(OCPI_TO_PARTY_ID);
}
insert_party(headers, &OCPI_FROM_COUNTRY_CODE, &OCPI_FROM_PARTY_ID, &self.from);
}
#[must_use]
pub fn applies_to(module: &ModuleId) -> bool {
module.is_functional()
}
}
fn insert_party(
headers: &mut HeaderMap,
country_header: &http::HeaderName,
party_header: &http::HeaderName,
party: &PartyRef,
) {
if let Ok(v) = HeaderValue::from_str(party.country_code.as_str()) {
headers.insert(country_header.clone(), v);
}
if let Ok(v) = HeaderValue::from_str(party.party_id.as_str()) {
headers.insert(party_header.clone(), v);
}
}
impl fmt::Display for RoutingHeaders {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.to {
Some(to) => write!(f, "{} -> {to}", self.from),
None => write!(f, "{} -> (open)", self.from),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum RoutingScenario {
Direct,
BroadcastPush {
hub: PartyRef,
},
OpenRoutingRequest,
GetAllViaHub {
hub: PartyRef,
},
}
impl RoutingScenario {
#[must_use]
pub fn request_headers(&self, requester: &PartyRef, receiver: Option<&PartyRef>) -> RoutingHeaders {
match self {
Self::Direct => RoutingHeaders { to: receiver.cloned(), from: requester.clone() },
Self::BroadcastPush { hub } | Self::GetAllViaHub { hub } => {
RoutingHeaders { to: Some(hub.clone()), from: requester.clone() }
}
Self::OpenRoutingRequest => RoutingHeaders::open(requester.clone()),
}
}
#[must_use]
pub fn response_headers(&self, requester: &PartyRef, receiver: Option<&PartyRef>) -> RoutingHeaders {
match self {
Self::Direct | Self::OpenRoutingRequest => RoutingHeaders {
to: Some(requester.clone()),
from: receiver.cloned().unwrap_or_else(|| requester.clone()),
},
Self::BroadcastPush { hub } | Self::GetAllViaHub { hub } => {
RoutingHeaders { to: Some(requester.clone()), from: hub.clone() }
}
}
}
#[must_use]
pub fn forwarded_request_headers(
&self,
requester: &PartyRef,
receiver: &PartyRef,
) -> Option<RoutingHeaders> {
match self {
Self::Direct | Self::OpenRoutingRequest => {
Some(RoutingHeaders::new(requester.clone(), receiver.clone()))
}
Self::BroadcastPush { hub } => Some(RoutingHeaders::new(hub.clone(), receiver.clone())),
Self::GetAllViaHub { .. } => None,
}
}
#[must_use]
pub const fn allows_get(&self) -> bool {
!matches!(self, Self::BroadcastPush { .. })
}
#[must_use]
pub const fn allows_write(&self) -> bool {
!matches!(self, Self::GetAllViaHub { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cpo() -> PartyRef {
PartyRef::new("NL", "TNM").unwrap()
}
fn msp() -> PartyRef {
PartyRef::new("DE", "ABC").unwrap()
}
fn hub() -> PartyRef {
PartyRef::new("NL", "HUB").unwrap()
}
#[test]
fn direct_request_and_response_swap_the_parties() {
let s = RoutingScenario::Direct;
let req = s.request_headers(&cpo(), Some(&msp()));
assert_eq!(req.to, Some(msp()));
assert_eq!(req.from, cpo());
let resp = s.response_headers(&cpo(), Some(&msp()));
assert_eq!(resp.to, Some(cpo()));
assert_eq!(resp.from, msp());
}
#[test]
fn broadcast_push_addresses_the_hub_then_the_hub_speaks_for_itself() {
let s = RoutingScenario::BroadcastPush { hub: hub() };
let req = s.request_headers(&cpo(), None);
assert_eq!((req.to, req.from), (Some(hub()), cpo()));
let resp = s.response_headers(&cpo(), None);
assert_eq!((resp.to, resp.from), (Some(cpo()), hub()));
let fwd = s.forwarded_request_headers(&cpo(), &msp()).unwrap();
assert_eq!((fwd.to, fwd.from), (Some(msp()), hub()));
assert!(!s.allows_get(), "GET SHALL NOT be used with Broadcast Push");
}
#[test]
fn open_routing_omits_the_to_headers_only_on_the_first_hop() {
let s = RoutingScenario::OpenRoutingRequest;
let req = s.request_headers(&cpo(), None);
assert_eq!(req.to, None, "the TO headers MUST be omitted");
let fwd = s.forwarded_request_headers(&cpo(), &msp()).unwrap();
assert_eq!((fwd.to, fwd.from), (Some(msp()), cpo()));
assert!(s.allows_get() && s.allows_write());
}
#[test]
fn get_all_via_hub_is_answered_by_the_hub_itself() {
let s = RoutingScenario::GetAllViaHub { hub: hub() };
let req = s.request_headers(&msp(), None);
assert_eq!((req.to, req.from), (Some(hub()), msp()));
let resp = s.response_headers(&msp(), None);
assert_eq!((resp.to, resp.from), (Some(msp()), hub()));
assert_eq!(s.forwarded_request_headers(&msp(), &cpo()), None);
assert!(!s.allows_write(), "a GET All is a read");
}
#[test]
fn configuration_modules_are_never_routed() {
assert!(!RoutingHeaders::applies_to(&ModuleId::Credentials));
assert!(!RoutingHeaders::applies_to(&ModuleId::Versions));
assert!(!RoutingHeaders::applies_to(&ModuleId::HubClientInfo));
assert!(RoutingHeaders::applies_to(&ModuleId::Locations));
}
#[test]
fn headers_round_trip_and_an_open_request_has_no_to() {
let mut headers = HeaderMap::new();
RoutingHeaders::new(cpo(), msp()).write_to(&mut headers);
assert_eq!(RoutingHeaders::from_headers(&headers), Some(RoutingHeaders::new(cpo(), msp())));
RoutingHeaders::open(cpo()).write_to(&mut headers);
let parsed = RoutingHeaders::from_headers(&headers).unwrap();
assert_eq!(parsed.to, None, "writing an open request clears any previous TO headers");
assert_eq!(parsed.from, cpo());
}
}