1use serde::{Deserialize, Serialize};
8
9use crate::types::validate_fields;
10use crate::types::{Extensions, Url, Validate, Validator, ViolationCode};
11use crate::{InterfaceRole, ModuleId, VersionNumber};
12
13#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18pub struct Version {
19 pub version: VersionNumber,
21 pub url: Url,
23 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
25 pub extensions: Extensions,
26}
27
28impl Version {
29 #[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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
46#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
47pub struct VersionDetails {
48 pub version: VersionNumber,
50 pub endpoints: Vec<Endpoint>,
52 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
54 pub extensions: Extensions,
55}
56
57impl VersionDetails {
58 #[must_use]
60 pub fn new(version: VersionNumber, endpoints: Vec<Endpoint>) -> Self {
61 Self { version, endpoints, extensions: Extensions::new() }
62 }
63
64 #[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 #[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 #[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 #[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 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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
157#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
158pub struct Endpoint {
159 pub identifier: ModuleId,
161 pub role: InterfaceRole,
163 pub url: Url,
165 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
167 pub extensions: Extensions,
168}
169
170impl Endpoint {
171 #[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 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}