Skip to main content

ocpi_kit/v2_3_0/
hub_client_info.rs

1//! The *Hub Client Info* module of OCPI 2.3.0: which parties a hub has connected.
2//!
3//! *Module Identifier: `hubclientinfo`* — Data owner: Hub.
4//!
5//! A configuration module, so its requests are **never** routed and carry no `OCPI-to-*` or
6//! `OCPI-from-*` headers.
7//!
8//! Spec: 2.3.0 §mod_hub_client_info_module
9
10use serde::{Deserialize, Serialize};
11
12use crate::ocpi_enum;
13use crate::types::validate_fields;
14use crate::types::{CountryCode, DateTime, Extensions, PartyId, PartyRef, Validate, Validator};
15
16use super::types::Role;
17
18/// The connection status of one party at a hub.
19///
20/// Spec: 2.3.0 §mod_hub_client_info_hub_client_info_object
21#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
22#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
23pub struct ClientInfo {
24    /// CPO or eMSP ID of this party, as used in the credentials exchange.
25    pub party_id: PartyId,
26    /// Country code of the country this party is operating in.
27    pub country_code: CountryCode,
28    /// The role of the connected party.
29    pub role: Role,
30    /// Status of the connection to the party.
31    pub status: ConnectionStatus,
32    /// Timestamp when this ClientInfo object was last updated.
33    pub last_updated: DateTime,
34    /// Undocumented JSON fields, preserved verbatim.
35    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
36    pub extensions: Extensions,
37}
38
39impl ClientInfo {
40    /// Creates a client info entry.
41    #[must_use]
42    pub fn new(party: PartyRef, role: Role, status: ConnectionStatus, last_updated: DateTime) -> Self {
43        Self {
44            party_id: party.party_id,
45            country_code: party.country_code,
46            role,
47            status,
48            last_updated,
49            extensions: Extensions::new(),
50        }
51    }
52
53    /// The party this entry is about.
54    #[must_use]
55    pub fn party(&self) -> PartyRef {
56        PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
57    }
58
59    /// Whether messages can currently be delivered to this party.
60    #[must_use]
61    pub fn is_reachable(&self) -> bool {
62        self.status == ConnectionStatus::Connected
63    }
64}
65
66impl Validate for ClientInfo {
67    fn validate_in(&self, v: &mut Validator) {
68        validate_fields!(self, v, party_id, country_code, role, status, last_updated);
69    }
70}
71
72ocpi_enum! {
73    /// The state of a hub's connection to one party.
74    ///
75    /// Spec: 2.3.0 §mod_hub_client_info_hub_connection_type_enum
76    pub enum ConnectionStatus {
77        /// Party is connected.
78        Connected = "CONNECTED",
79        /// Party is currently not connected.
80        Offline = "OFFLINE",
81        /// Connection to this party is planned, but has never been connected.
82        Planned = "PLANNED",
83        /// Party is no longer active and will never connect again.
84        Suspended = "SUSPENDED",
85    }
86}
87
88impl ConnectionStatus {
89    /// Whether a still-alive check should be attempted against this party.
90    ///
91    /// A `PLANNED` party has never connected and a `SUSPENDED` one never will, so polling either
92    /// is wasted work.
93    #[must_use]
94    pub const fn should_poll(self) -> bool {
95        matches!(self, Self::Connected | Self::Offline)
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn only_connected_parties_are_reachable() {
105        let info = ClientInfo::new(
106            PartyRef::new("NL", "TNM").unwrap(),
107            Role::Cpo,
108            ConnectionStatus::Offline,
109            "2019-06-24T12:39:09Z".parse().unwrap(),
110        );
111        assert!(!info.is_reachable());
112        assert!(info.status.should_poll(), "an offline party may come back");
113        assert!(!ConnectionStatus::Suspended.should_poll());
114        assert!(!ConnectionStatus::Planned.should_poll());
115    }
116
117    #[test]
118    fn round_trips_the_spec_shape() {
119        let json = r#"{"party_id":"TNM","country_code":"NL","role":"CPO","status":"CONNECTED","last_updated":"2019-06-24T12:39:09Z"}"#;
120        let info: ClientInfo = serde_json::from_str(json).unwrap();
121        assert_eq!(info.party(), PartyRef::new("NL", "TNM").unwrap());
122        assert_eq!(serde_json::to_string(&info).unwrap(), json);
123    }
124}