Skip to main content

ocpi_kit/v2_3_0/
versions.rs

1//! The *Versions* module of OCPI 2.3.0: the starting point of every OCPI connection.
2//!
3//! *Module Identifier: `versions`* — required for all implementations.
4//!
5//! Spec: 2.3.0 §versions_module
6
7use serde::{Deserialize, Serialize};
8
9use crate::types::validate_fields;
10use crate::types::{Extensions, Url, Validate, Validator, ViolationCode};
11use crate::{InterfaceRole, ModuleId, VersionNumber};
12
13/// One supported OCPI version and where to find its details.
14///
15/// Spec: 2.3.0 §version_information_endpoint_version_class
16#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18pub struct Version {
19    /// The version number.
20    pub version: VersionNumber,
21    /// URL to the endpoint containing version specific information.
22    pub url: Url,
23    /// Undocumented JSON fields, preserved verbatim.
24    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
25    pub extensions: Extensions,
26}
27
28impl Version {
29    /// Creates a version entry.
30    #[must_use]
31    pub fn new(version: VersionNumber, url: Url) -> Self {
32        Self { version, url, extensions: Extensions::new() }
33    }
34}
35
36impl Validate for Version {
37    fn validate_in(&self, v: &mut Validator) {
38        validate_fields!(self, v, version, url);
39    }
40}
41
42/// The endpoints a party implements for one version.
43///
44/// Spec: 2.3.0 §version_information_get_details_endpoint_data
45#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
46#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
47pub struct VersionDetails {
48    /// The version number these endpoints belong to.
49    pub version: VersionNumber,
50    /// The supported endpoints for this version. Cardinality `+`.
51    pub endpoints: Vec<Endpoint>,
52    /// Undocumented JSON fields, preserved verbatim.
53    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
54    pub extensions: Extensions,
55}
56
57impl VersionDetails {
58    /// Creates version details.
59    #[must_use]
60    pub fn new(version: VersionNumber, endpoints: Vec<Endpoint>) -> Self {
61        Self { version, endpoints, extensions: Extensions::new() }
62    }
63
64    /// The endpoint for a module and interface role, if the peer implements it.
65    ///
66    /// Module identifiers are matched case-insensitively, which matters for the `Booking` module:
67    /// the spec writes it in mixed case and implementations differ. See
68    /// [`ModuleId::matches`].
69    #[must_use]
70    pub fn endpoint(&self, module: &ModuleId, role: InterfaceRole) -> Option<&Endpoint> {
71        self.endpoints.iter().find(|e| e.identifier.matches(module) && e.role == role)
72    }
73
74    /// The URL of a module's endpoint for one interface role.
75    #[must_use]
76    pub fn url(&self, module: &ModuleId, role: InterfaceRole) -> Option<&Url> {
77        self.endpoint(module, role).map(|e| &e.url)
78    }
79
80    /// The credentials endpoint.
81    ///
82    /// > *NOTE: for the `credentials` module, the value of the role property is not relevant as
83    /// > this module is the same for all roles. It is advised to send "SENDER" as the
84    /// > InterfaceRole for one's own credentials endpoint and to disregard the value of the role
85    /// > property of the Endpoint object for other platforms' credentials modules.*
86    ///
87    /// So this ignores the role entirely, as the spec instructs.
88    #[must_use]
89    pub fn credentials_url(&self) -> Option<&Url> {
90        self.endpoints.iter().find(|e| e.identifier.matches(&ModuleId::Credentials)).map(|e| &e.url)
91    }
92
93    /// Whether the peer implements every module in `required`, in the given role.
94    ///
95    /// > *In case the Sender (starting the credentials exchange process) cannot find the
96    /// > endpoints it expects, it is expected NOT to send the POST request with credentials to
97    /// > the Receiver.*
98    ///
99    /// Spec: 2.3.0 §credentials_required_endpoints_not_available
100    #[must_use]
101    pub fn missing(&self, required: &[(ModuleId, InterfaceRole)]) -> Vec<(ModuleId, InterfaceRole)> {
102        required.iter().filter(|(m, r)| self.endpoint(m, *r).is_none()).cloned().collect()
103    }
104}
105
106impl Validate for VersionDetails {
107    fn validate_in(&self, v: &mut Validator) {
108        validate_fields!(self, v, version, endpoints);
109        if self.endpoints.is_empty() {
110            v.report_at(
111                "endpoints",
112                ViolationCode::EmptyRequiredList,
113                "version details have cardinality `+` endpoints: at least one is required",
114            );
115        }
116        // The credentials module is "Required for all implementations".
117        if !self.endpoints.iter().any(|e| e.identifier.matches(&ModuleId::Credentials)) {
118            v.report_at(
119                "endpoints",
120                ViolationCode::MissingConditional,
121                "the `credentials` module is required for all implementations",
122            );
123        }
124        let mut seen: Vec<(&ModuleId, InterfaceRole)> = Vec::new();
125        for (i, e) in self.endpoints.iter().enumerate() {
126            let key = (&e.identifier, e.role);
127            if seen.contains(&key) {
128                v.enter("endpoints");
129                v.enter(&i.to_string());
130                v.report(
131                    ViolationCode::Inconsistent,
132                    format!("{} / {} is listed more than once", e.identifier, e.role),
133                );
134                v.leave();
135                v.leave();
136            }
137            seen.push(key);
138            if !e.identifier.exists_in(&self.version) {
139                v.enter("endpoints");
140                v.enter(&i.to_string());
141                v.report_at(
142                    "identifier",
143                    ViolationCode::Inconsistent,
144                    format!("the {} module does not exist in OCPI {}", e.identifier, self.version),
145                );
146                v.leave();
147                v.leave();
148            }
149        }
150    }
151}
152
153/// One module endpoint of a party, for one interface role.
154///
155/// Spec: 2.3.0 §version_information_endpoint_endpoint_class
156#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
157#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
158pub struct Endpoint {
159    /// Endpoint identifier.
160    pub identifier: ModuleId,
161    /// Interface role this endpoint implements.
162    pub role: InterfaceRole,
163    /// URL to the endpoint.
164    pub url: Url,
165    /// Undocumented JSON fields, preserved verbatim.
166    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
167    pub extensions: Extensions,
168}
169
170impl Endpoint {
171    /// Creates an endpoint entry.
172    #[must_use]
173    pub fn new(identifier: ModuleId, role: InterfaceRole, url: Url) -> Self {
174        Self { identifier, role, url, extensions: Extensions::new() }
175    }
176}
177
178impl Validate for Endpoint {
179    fn validate_in(&self, v: &mut Validator) {
180        validate_fields!(self, v, identifier, role, url);
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    fn url(path: &str) -> Url {
189        Url::new(format!("https://example.com/ocpi/cpo/2.3.0/{path}")).unwrap()
190    }
191
192    fn details(endpoints: Vec<Endpoint>) -> VersionDetails {
193        VersionDetails::new(VersionNumber::V2_3_0, endpoints)
194    }
195
196    #[test]
197    fn endpoint_lookup_ignores_the_role_for_credentials() {
198        let d = details(vec![
199            Endpoint::new(ModuleId::Credentials, InterfaceRole::Receiver, url("credentials")),
200            Endpoint::new(ModuleId::Locations, InterfaceRole::Sender, url("locations")),
201        ]);
202        // The spec says to disregard the role on the credentials endpoint.
203        assert_eq!(d.credentials_url(), Some(&url("credentials")));
204        assert_eq!(d.url(&ModuleId::Locations, InterfaceRole::Sender), Some(&url("locations")));
205        assert_eq!(d.url(&ModuleId::Locations, InterfaceRole::Receiver), None);
206    }
207
208    #[test]
209    fn missing_required_endpoints_are_listed_for_the_handshake() {
210        let d =
211            details(vec![Endpoint::new(ModuleId::Credentials, InterfaceRole::Sender, url("credentials"))]);
212        let missing = d.missing(&[
213            (ModuleId::Credentials, InterfaceRole::Sender),
214            (ModuleId::Cdrs, InterfaceRole::Receiver),
215        ]);
216        assert_eq!(missing, vec![(ModuleId::Cdrs, InterfaceRole::Receiver)]);
217    }
218
219    #[test]
220    fn a_module_that_does_not_exist_in_the_version_is_reported() {
221        let d = VersionDetails::new(
222            VersionNumber::V2_1_1,
223            vec![
224                Endpoint::new(ModuleId::Credentials, InterfaceRole::Sender, url("credentials")),
225                Endpoint::new(ModuleId::ChargingProfiles, InterfaceRole::Receiver, url("cp")),
226            ],
227        );
228        let err = d.validate().unwrap_err();
229        assert!(err.as_slice().iter().any(|x| x.pointer == "/endpoints/1/identifier"), "{err}");
230    }
231
232    #[test]
233    fn credentials_is_required_in_version_details() {
234        let d = details(vec![Endpoint::new(ModuleId::Locations, InterfaceRole::Sender, url("locations"))]);
235        assert!(
236            d.validate().unwrap_err().as_slice().iter().any(|x| x.code == ViolationCode::MissingConditional)
237        );
238    }
239
240    #[test]
241    fn unknown_modules_and_versions_survive_discovery() {
242        let json = r#"{"version":"3.0","endpoints":[{"identifier":"credentials","role":"SENDER","url":"https://example.com/ocpi/3.0/credentials"},{"identifier":"nltnm-tokens","role":"RECEIVER","url":"https://example.com/ocpi/3.0/x"}]}"#;
243        let d: VersionDetails = serde_json::from_str(json).unwrap();
244        assert!(!d.version.is_known());
245        assert!(!d.endpoints[1].identifier.is_known());
246        assert_eq!(serde_json::to_string(&d).unwrap(), json);
247    }
248}