1use http::Method;
4
5use crate::client::Transport;
6use crate::convert::wire::{ObjectKind, Payload, bridgeable};
7use crate::convert::{Converted, Lossy};
8use crate::transport::{
9 OcpiError, OcpiRequest, OcpiResponse, RequestIds, RoutingHeaders, RoutingScenario, StatusCode,
10};
11use crate::types::{PartyRef, Url};
12use crate::{InterfaceRole, ModuleId, VersionNumber};
13
14use super::routing_table::RoutingTable;
15
16#[derive(Debug)]
18pub struct Forwardable {
19 pub method: Method,
21 pub module: ModuleId,
23 pub interface: InterfaceRole,
25 pub path: String,
27 pub query: Option<String>,
29 pub routing: RoutingHeaders,
31 pub ids: RequestIds,
33 pub body: Option<Vec<u8>>,
35 pub version: VersionNumber,
41}
42
43impl Forwardable {
44 pub fn scenario(&self, hub: &PartyRef) -> Result<RoutingScenario, OcpiError> {
76 match &self.routing.to {
77 None => Ok(RoutingScenario::OpenRoutingRequest),
78 Some(to) if to == hub => match (self.method == Method::GET, self.interface) {
79 (true, InterfaceRole::Sender) => Ok(RoutingScenario::GetAllViaHub { hub: hub.clone() }),
80 (false, InterfaceRole::Receiver) => Ok(RoutingScenario::BroadcastPush { hub: hub.clone() }),
81 (true, InterfaceRole::Receiver) => Err(OcpiError::NotRoutable(
82 "a GET addressed to the hub on a Receiver interface is neither a GET All (which is \
83 a GET on a Sender interface) nor a Broadcast Push (which SHALL NOT be a \
84 GET); omit the OCPI-to- headers to make it an Open Routing Request"
85 .to_owned(),
86 )),
87 (false, InterfaceRole::Sender) => Err(OcpiError::NotRoutable(format!(
88 "a {} addressed to the hub on a Sender interface is not a scenario the \
89 specification defines; address the receiving party directly, or omit the \
90 OCPI-to- headers for an Open Routing Request",
91 self.method
92 ))),
93 },
94 Some(_) => Ok(RoutingScenario::Direct),
95 }
96 }
97
98 #[must_use]
100 pub fn url_at(&self, base: &Url) -> Url {
101 let url = if self.path.is_empty() { base.clone() } else { base.join(&self.path) };
102 match &self.query {
103 Some(query) if !query.is_empty() => url.with_query(query),
104 _ => url,
105 }
106 }
107}
108
109#[derive(Debug)]
111pub struct Relayed {
112 pub party: PartyRef,
114 pub outcome: Result<OcpiResponse<serde_json::Value>, OcpiError>,
116}
117
118impl Relayed {
119 #[must_use]
121 pub fn is_success(&self) -> bool {
122 self.outcome.as_ref().is_ok_and(OcpiResponse::is_success)
123 }
124}
125
126#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
129#[non_exhaustive]
130pub enum Unbridgeable {
131 #[default]
137 Refuse,
138 RelayVerbatim,
141}
142
143#[derive(Debug)]
145pub struct Forwarder<'a> {
146 transport: &'a Transport,
147 table: &'a RoutingTable,
148 hub: PartyRef,
149 unbridgeable: Unbridgeable,
150 report_losses: bool,
151}
152
153impl<'a> Forwarder<'a> {
154 #[must_use]
156 pub fn new(transport: &'a Transport, table: &'a RoutingTable, hub: PartyRef) -> Self {
157 Self { transport, table, hub, unbridgeable: Unbridgeable::default(), report_losses: true }
158 }
159
160 #[must_use]
162 pub const fn on_unbridgeable(mut self, policy: Unbridgeable) -> Self {
163 self.unbridgeable = policy;
164 self
165 }
166
167 #[must_use]
170 pub const fn report_losses(mut self, report: bool) -> Self {
171 self.report_losses = report;
172 self
173 }
174
175 #[must_use]
177 pub const fn hub(&self) -> &PartyRef {
178 &self.hub
179 }
180
181 pub async fn relay(&self, request: &Forwardable, to: &PartyRef, routing: RoutingHeaders) -> Relayed {
196 let target = self.table.with_platform(to, |platform| {
197 platform.peer.endpoint_url(&request.module, request.interface).cloned().map(|base| {
198 (
199 base,
200 platform.peer.token().clone(),
201 platform.peer.quirks().clone(),
202 platform.peer.version().clone(),
203 )
204 })
205 });
206
207 let (base, token, quirks, their_version) = match target {
208 Err(e) => return Relayed { party: to.clone(), outcome: Err(e) },
209 Ok(None) => {
210 return Relayed {
211 party: to.clone(),
212 outcome: Err(OcpiError::Remote {
213 status_code: StatusCode::UNKNOWN_RECEIVER,
214 status_message: Some(format!(
215 "{to} does not implement the {} interface of {}",
216 request.interface, request.module
217 )),
218 }),
219 };
220 }
221 Ok(Some(target)) => target,
222 };
223
224 let outgoing_body = match self.carry_request(request, &their_version) {
225 Ok(body) => body,
226 Err(e) => return Relayed { party: to.clone(), outcome: Err(e) },
227 };
228
229 let mut outgoing =
230 OcpiRequest::new(request.method.clone(), request.url_at(&base), request.module.clone())
231 .routed(routing)
232 .with_ids(request.ids.forwarded());
233 outgoing.body = outgoing_body.value;
234
235 let outcome = self
236 .transport
237 .send_with_headers::<serde_json::Value>(&outgoing, &token, &quirks)
238 .await
239 .map(|(response, _)| response)
240 .map_err(map_hub_error);
241
242 let outcome = outcome
243 .and_then(|response| self.carry_response(request, &their_version, response, outgoing_body.lossy));
244
245 Relayed { party: to.clone(), outcome }
246 }
247
248 fn carry_request(
253 &self,
254 request: &Forwardable,
255 their_version: &VersionNumber,
256 ) -> Result<Converted<Option<Vec<u8>>>, OcpiError> {
257 let Some(body) = request.body.as_ref() else { return Ok(Converted::lossless(None)) };
258 if request.version == *their_version {
259 return Ok(Converted::lossless(Some(body.clone())));
260 }
261 let Some(kind) =
262 ObjectKind::for_endpoint(&request.module, request.interface, &request.path, Payload::Request)
263 else {
264 return Ok(Converted::lossless(Some(body.clone())));
265 };
266 if !bridgeable(&request.version, their_version) {
267 return self
268 .refuse_or_relay(&request.version, their_version)
269 .map(|()| Converted::lossless(Some(body.clone())));
270 }
271 let value: serde_json::Value =
272 serde_json::from_slice(body).map_err(|e| OcpiError::MalformedJson(e.to_string()))?;
273 let converted =
274 kind.bridge(&request.version, their_version, value).map_err(|e| OcpiError::Remote {
275 status_code: StatusCode::INVALID_PARAMETERS,
276 status_message: Some(e.to_string()),
277 })?;
278 let bytes =
279 serde_json::to_vec(&converted.value).map_err(|e| OcpiError::MalformedJson(e.to_string()))?;
280 Ok(Converted::new(Some(bytes), converted.lossy))
281 }
282
283 fn carry_response(
288 &self,
289 request: &Forwardable,
290 their_version: &VersionNumber,
291 mut response: OcpiResponse<serde_json::Value>,
292 mut lossy: Lossy,
293 ) -> Result<OcpiResponse<serde_json::Value>, OcpiError> {
294 let data = response.data.take();
295 let Some(data) = data else {
296 return Ok(self.annotate(response, lossy));
297 };
298 let kind = if request.version == *their_version {
299 None
300 } else {
301 ObjectKind::for_endpoint(&request.module, request.interface, &request.path, Payload::Response)
302 };
303 match kind {
304 None => response.data = Some(data),
305 Some(_) if !bridgeable(their_version, &request.version) => {
308 self.refuse_or_relay(their_version, &request.version)?;
309 response.data = Some(data);
310 }
311 Some(kind) => {
312 let converted =
313 kind.bridge(their_version, &request.version, data).map_err(|e| OcpiError::Remote {
314 status_code: StatusCode::HUB_ERROR,
315 status_message: Some(e.to_string()),
316 })?;
317 lossy.absorb("/data", converted.lossy);
318 response.data = Some(converted.value);
319 }
320 }
321 Ok(self.annotate(response, lossy))
322 }
323
324 fn annotate(
326 &self,
327 mut response: OcpiResponse<serde_json::Value>,
328 lossy: Lossy,
329 ) -> OcpiResponse<serde_json::Value> {
330 if !self.report_losses {
331 return response;
332 }
333 if let Some(note) = lossy.to_status_message() {
334 response.status_message = Some(match response.status_message.take() {
335 Some(existing) if !existing.is_empty() => format!("{existing}; {note}"),
336 _ => note,
337 });
338 }
339 response
340 }
341
342 fn refuse_or_relay(&self, from: &VersionNumber, to: &VersionNumber) -> Result<(), OcpiError> {
344 match self.unbridgeable {
345 Unbridgeable::RelayVerbatim => Ok(()),
346 Unbridgeable::Refuse => Err(OcpiError::NotRoutable(format!(
347 "this hub has no conversions between OCPI {from} and OCPI {to}, so it will not \
348 hand one party a document written for the other; set \
349 Forwarder::on_unbridgeable(Unbridgeable::RelayVerbatim) to relay the bytes \
350 unchanged instead"
351 ))),
352 }
353 }
354
355 pub async fn broadcast(
365 &self,
366 request: &Forwardable,
367 sender_role: crate::v2_3_0::types::Role,
368 ) -> Vec<Relayed> {
369 let targets = self.table.broadcast_targets(&request.routing.from, sender_role, &request.module);
370 let mut results = Vec::with_capacity(targets.len());
371 for (_, party) in targets {
372 let routing = RoutingHeaders::new(self.hub.clone(), party.clone());
374 results.push(self.relay(request, &party, routing).await);
375 }
376 results
377 }
378
379 pub async fn open_route(
391 &self,
392 request: &Forwardable,
393 router: &dyn OpenRouter,
394 ) -> Result<Relayed, OcpiError> {
395 let to = router.destination(request).ok_or_else(|| OcpiError::Remote {
396 status_code: StatusCode::UNKNOWN_RECEIVER,
397 status_message: Some("the hub could not determine a destination from the request".to_owned()),
398 })?;
399 let routing = RoutingHeaders::new(request.routing.from.clone(), to.clone());
401 Ok(self.relay(request, &to, routing).await)
402 }
403
404 pub async fn get_all(&self, request: &Forwardable) -> Vec<Relayed> {
416 let sources = self.table.get_all_sources(&request.routing.from, &request.module);
417 let mut results = Vec::with_capacity(sources.len());
418 for (_, party) in sources {
419 let routing = RoutingHeaders::new(request.routing.from.clone(), party.clone());
426 results.push(self.relay(request, &party, routing).await);
427 }
428 results
429 }
430}
431
432pub trait OpenRouter: Send + Sync {
438 fn destination(&self, request: &Forwardable) -> Option<PartyRef>;
440}
441
442#[derive(Debug, Default)]
446pub struct BodyOwnerRouter;
447
448impl OpenRouter for BodyOwnerRouter {
449 fn destination(&self, request: &Forwardable) -> Option<PartyRef> {
450 let body = request.body.as_ref()?;
451 let value: serde_json::Value = serde_json::from_slice(body).ok()?;
452 let country = value.get("country_code")?.as_str()?;
453 let party = value.get("party_id")?.as_str()?;
454 PartyRef::new(country, party).ok()
455 }
456}
457
458#[derive(Clone, Copy, Debug, PartialEq, Eq)]
460#[non_exhaustive]
461pub enum AggregatePolicy {
462 FirstErrorWins,
464 AnySuccess,
466 AlwaysSucceed,
468}
469
470#[must_use]
477pub fn aggregate(results: &[Relayed], policy: AggregatePolicy) -> StatusCode {
478 if results.is_empty() {
479 return StatusCode::CONNECTION_PROBLEM;
480 }
481 let succeeded = results.iter().filter(|r| r.is_success()).count();
482 match policy {
483 AggregatePolicy::AlwaysSucceed => StatusCode::SUCCESS,
484 AggregatePolicy::AnySuccess if succeeded > 0 => StatusCode::SUCCESS,
485 _ => {
486 if succeeded == results.len() {
487 return StatusCode::SUCCESS;
488 }
489 results.iter().find(|r| !r.is_success()).map_or(StatusCode::HUB_ERROR, |failed| {
490 match &failed.outcome {
491 Ok(response) => response.status_code,
492 Err(error) => error.status_code(),
493 }
494 })
495 }
496 }
497}
498
499fn map_hub_error(error: OcpiError) -> OcpiError {
501 match &error {
502 OcpiError::Transport(message) => {
503 let lower = message.to_ascii_lowercase();
504 let code = if lower.contains("timeout") || lower.contains("timed out") {
505 StatusCode::TIMEOUT_ON_FORWARDED_REQUEST
506 } else {
507 StatusCode::CONNECTION_PROBLEM
508 };
509 OcpiError::Remote { status_code: code, status_message: Some(message.clone()) }
510 }
511 _ => error,
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::*;
518 use crate::types::DateTime;
519
520 fn hub() -> PartyRef {
521 PartyRef::new("NL", "HUB").unwrap()
522 }
523
524 fn request(method: Method, to: Option<PartyRef>, interface: InterfaceRole) -> Forwardable {
525 Forwardable {
526 method,
527 module: ModuleId::Locations,
528 interface,
529 path: String::new(),
530 query: None,
531 routing: RoutingHeaders { to, from: PartyRef::new("NL", "TNM").unwrap() },
532 ids: RequestIds::generate(),
533 body: None,
534 version: VersionNumber::V2_3_0,
535 }
536 }
537
538 #[test]
539 fn the_scenario_is_read_off_the_headers_and_method() {
540 assert!(matches!(
543 request(Method::GET, Some(hub()), InterfaceRole::Sender).scenario(&hub()).unwrap(),
544 RoutingScenario::GetAllViaHub { .. }
545 ));
546 assert!(matches!(
548 request(Method::PUT, Some(hub()), InterfaceRole::Receiver).scenario(&hub()).unwrap(),
549 RoutingScenario::BroadcastPush { .. }
550 ));
551 assert_eq!(
553 request(Method::PUT, None, InterfaceRole::Receiver).scenario(&hub()).unwrap(),
554 RoutingScenario::OpenRoutingRequest
555 );
556 assert_eq!(
558 request(Method::GET, Some(PartyRef::new("DE", "ABC").unwrap()), InterfaceRole::Sender)
559 .scenario(&hub())
560 .unwrap(),
561 RoutingScenario::Direct
562 );
563 }
564
565 #[test]
566 fn a_get_addressed_to_the_hub_is_never_silently_broadcast() {
567 let error = request(Method::GET, Some(hub()), InterfaceRole::Receiver).scenario(&hub()).unwrap_err();
571 assert_eq!(error.status_code(), StatusCode::INVALID_PARAMETERS);
572 assert!(error.to_string().contains("Open Routing Request"), "{error}");
573
574 let error = request(Method::PUT, Some(hub()), InterfaceRole::Sender).scenario(&hub()).unwrap_err();
576 assert_eq!(error.status_code(), StatusCode::INVALID_PARAMETERS);
577 }
578
579 #[test]
580 fn every_scenario_agrees_with_what_it_says_it_allows() {
581 for (method, interface) in [
584 (Method::GET, InterfaceRole::Sender),
585 (Method::GET, InterfaceRole::Receiver),
586 (Method::PUT, InterfaceRole::Sender),
587 (Method::PUT, InterfaceRole::Receiver),
588 (Method::POST, InterfaceRole::Receiver),
589 (Method::DELETE, InterfaceRole::Receiver),
590 ] {
591 for to in [None, Some(hub()), Some(PartyRef::new("DE", "ABC").unwrap())] {
592 let r = request(method.clone(), to, interface);
593 let Ok(scenario) = r.scenario(&hub()) else { continue };
594 if method == Method::GET {
595 assert!(scenario.allows_get(), "{scenario:?} classified a GET it forbids");
596 } else {
597 assert!(scenario.allows_write(), "{scenario:?} classified a write it forbids");
598 }
599 }
600 }
601 }
602
603 #[test]
604 fn the_forwarded_url_keeps_the_path_and_query() {
605 let mut r = request(Method::GET, Some(hub()), InterfaceRole::Sender);
606 r.path = "NL/TNM/LOC1".to_owned();
607 r.query = Some("offset=50&limit=10".to_owned());
608 let base = Url::new("https://msp.example.com/ocpi/emsp/2.3.0/locations").unwrap();
609 assert_eq!(
610 r.url_at(&base).as_str(),
611 "https://msp.example.com/ocpi/emsp/2.3.0/locations/NL/TNM/LOC1?offset=50&limit=10"
612 );
613 }
614
615 #[test]
616 fn the_body_router_reads_the_owner_out_of_the_object() {
617 let mut r = request(Method::PUT, None, InterfaceRole::Receiver);
618 r.body = Some(br#"{"country_code":"DE","party_id":"ABC","id":"LOC1"}"#.to_vec());
619 assert_eq!(BodyOwnerRouter.destination(&r), Some(PartyRef::new("DE", "ABC").unwrap()));
620 r.body = Some(br#"{"id":"LOC1"}"#.to_vec());
621 assert_eq!(BodyOwnerRouter.destination(&r), None);
622 }
623
624 fn relayed(party: &str, status: StatusCode) -> Relayed {
625 Relayed {
626 party: PartyRef::new("DE", party).unwrap(),
627 outcome: Ok(OcpiResponse {
628 data: None,
629 status_code: status,
630 status_message: None,
631 timestamp: DateTime::from_unix_timestamp(0).unwrap(),
632 }),
633 }
634 }
635
636 #[test]
637 fn aggregation_surfaces_the_first_failure_by_default() {
638 let results =
639 vec![relayed("AAA", StatusCode::SUCCESS), relayed("BBB", StatusCode::INVALID_PARAMETERS)];
640 assert_eq!(
641 aggregate(&results, AggregatePolicy::FirstErrorWins),
642 StatusCode::INVALID_PARAMETERS,
643 "the sender needs to see the receiving party's own code"
644 );
645 assert_eq!(aggregate(&results, AggregatePolicy::AnySuccess), StatusCode::SUCCESS);
646 assert_eq!(aggregate(&results, AggregatePolicy::AlwaysSucceed), StatusCode::SUCCESS);
647 }
648
649 #[test]
650 fn a_broadcast_that_reached_nobody_is_a_connection_problem() {
651 assert_eq!(aggregate(&[], AggregatePolicy::AlwaysSucceed), StatusCode::CONNECTION_PROBLEM);
652 }
653
654 #[test]
655 fn a_timeout_becomes_4002_and_a_refused_connection_4003() {
656 let timeout = map_hub_error(OcpiError::Transport("operation timed out".into()));
657 assert_eq!(timeout.status_code(), StatusCode::TIMEOUT_ON_FORWARDED_REQUEST);
658 let refused = map_hub_error(OcpiError::Transport("connection refused".into()));
659 assert_eq!(refused.status_code(), StatusCode::CONNECTION_PROBLEM);
660 }
661}