1use serde::Serialize;
31use serde::de::DeserializeOwned;
32use serde_json::Value;
33
34use crate::{InterfaceRole, ModuleId, VersionNumber};
35
36use super::{Converted, Downgrade, Lossy, Upgrade};
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum Payload {
42 Request,
44 Response,
46}
47
48#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
50#[non_exhaustive]
51pub enum BridgeError {
52 #[error("this build cannot translate OCPI {from} to OCPI {to}")]
54 Unsupported {
55 from: VersionNumber,
57 to: VersionNumber,
59 },
60 #[error("the document is not a valid OCPI {version} {kind}: {message}")]
62 Decode {
63 version: VersionNumber,
65 kind: ObjectKind,
67 message: String,
69 },
70}
71
72#[must_use]
78pub fn bridgeable(from: &VersionNumber, to: &VersionNumber) -> bool {
79 if from == to {
80 return true;
81 }
82 matches!(
83 (from, to),
84 (VersionNumber::V2_2_1, VersionNumber::V2_3_0) | (VersionNumber::V2_3_0, VersionNumber::V2_2_1)
85 )
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
94#[non_exhaustive]
95pub enum ObjectKind {
96 Location,
98 Evse,
100 Connector,
102 Session,
104 Cdr,
106 Tariff,
108 Token,
110 AuthorizationInfo,
112 Credentials,
114 ClientInfo,
116 StartSession,
118 ReserveNow,
120}
121
122impl core::fmt::Display for ObjectKind {
123 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
124 f.write_str(match self {
125 Self::Location => "Location",
126 Self::Evse => "EVSE",
127 Self::Connector => "Connector",
128 Self::Session => "Session",
129 Self::Cdr => "CDR",
130 Self::Tariff => "Tariff",
131 Self::Token => "Token",
132 Self::AuthorizationInfo => "AuthorizationInfo",
133 Self::Credentials => "Credentials",
134 Self::ClientInfo => "ClientInfo",
135 Self::StartSession => "StartSession",
136 Self::ReserveNow => "ReserveNow",
137 })
138 }
139}
140
141macro_rules! bridge_kinds {
143 ($($kind:ident => $old:path, $new:path;)*) => {
144 impl ObjectKind {
145 fn bridge_one(
146 self,
147 from: &VersionNumber,
148 to: &VersionNumber,
149 value: Value,
150 ) -> Result<Converted<Value>, BridgeError> {
151 use VersionNumber::{V2_2_1, V2_3_0};
152 match (from, to) {
153 (V2_2_1, V2_3_0) => match self {
154 $(Self::$kind => up::<$old, $new>(self, value),)*
155 },
156 (V2_3_0, V2_2_1) => match self {
157 $(Self::$kind => down::<$new, $old>(self, value),)*
158 },
159 _ => Err(BridgeError::Unsupported { from: from.clone(), to: to.clone() }),
160 }
161 }
162 }
163 };
164}
165
166bridge_kinds! {
167 Location => crate::v2_2_1::locations::Location, crate::v2_3_0::locations::Location;
168 Evse => crate::v2_2_1::locations::Evse, crate::v2_3_0::locations::Evse;
169 Connector => crate::v2_2_1::locations::Connector, crate::v2_3_0::locations::Connector;
170 Session => crate::v2_2_1::sessions::Session, crate::v2_3_0::sessions::Session;
171 Cdr => crate::v2_2_1::cdrs::Cdr, crate::v2_3_0::cdrs::Cdr;
172 Tariff => crate::v2_2_1::tariffs::Tariff, crate::v2_3_0::tariffs::Tariff;
173 Token => crate::v2_2_1::tokens::Token, crate::v2_3_0::tokens::Token;
174 AuthorizationInfo =>
175 crate::v2_2_1::tokens::AuthorizationInfo, crate::v2_3_0::tokens::AuthorizationInfo;
176 Credentials => crate::v2_2_1::credentials::Credentials, crate::v2_3_0::credentials::Credentials;
177 ClientInfo =>
178 crate::v2_2_1::hub_client_info::ClientInfo, crate::v2_3_0::hub_client_info::ClientInfo;
179 StartSession => crate::v2_2_1::commands::StartSession, crate::v2_3_0::commands::StartSession;
180 ReserveNow => crate::v2_2_1::commands::ReserveNow, crate::v2_3_0::commands::ReserveNow;
181}
182
183fn up<O, N>(kind: ObjectKind, value: Value) -> Result<Converted<Value>, BridgeError>
184where
185 O: DeserializeOwned + Upgrade<N>,
186 N: Serialize,
187{
188 let old: O = serde_json::from_value(value).map_err(|e| BridgeError::Decode {
189 version: VersionNumber::V2_2_1,
190 kind,
191 message: e.to_string(),
192 })?;
193 Ok(reserialise(kind, VersionNumber::V2_3_0, old.upgrade()))
194}
195
196fn down<N, O>(kind: ObjectKind, value: Value) -> Result<Converted<Value>, BridgeError>
197where
198 N: DeserializeOwned + Downgrade<O>,
199 O: Serialize,
200{
201 let new: N = serde_json::from_value(value).map_err(|e| BridgeError::Decode {
202 version: VersionNumber::V2_3_0,
203 kind,
204 message: e.to_string(),
205 })?;
206 Ok(reserialise(kind, VersionNumber::V2_2_1, new.downgrade()))
207}
208
209fn reserialise<T: Serialize>(
210 kind: ObjectKind,
211 into: VersionNumber,
212 converted: Converted<T>,
213) -> Converted<Value> {
214 let value = serde_json::to_value(&converted.value).unwrap_or(Value::Null);
218 debug_assert!(!value.is_null(), "a bridged {kind} serialised to null on the way to {into}");
219 Converted::new(value, converted.lossy)
220}
221
222impl ObjectKind {
223 pub fn bridge(
234 self,
235 from: &VersionNumber,
236 to: &VersionNumber,
237 value: Value,
238 ) -> Result<Converted<Value>, BridgeError> {
239 if from == to {
240 return Ok(Converted::lossless(value));
241 }
242 match value {
243 Value::Null => Ok(Converted::lossless(Value::Null)),
244 Value::Array(items) => {
245 let mut out = Vec::with_capacity(items.len());
246 let mut lossy = Lossy::none();
247 for (index, item) in items.into_iter().enumerate() {
248 let converted = self.bridge_one(from, to, item)?;
249 lossy.absorb(&format!("/{index}"), converted.lossy);
250 out.push(converted.value);
251 }
252 Ok(Converted::new(Value::Array(out), lossy))
253 }
254 other => self.bridge_one(from, to, other),
255 }
256 }
257
258 #[must_use]
267 pub const fn divergent_fields(self) -> &'static [&'static str] {
268 match self {
269 Self::Location => &["evses", "parking_places", "help_phone"],
270 Self::Evse => &["connectors", "parking", "accepted_service_providers"],
271 Self::Connector => &["capabilities"],
272 Self::Session => &["total_cost"],
273 Self::Cdr => &[
274 "tariffs",
275 "booking_id",
276 "total_cost",
277 "total_fixed_cost",
278 "total_energy_cost",
279 "total_time_cost",
280 "total_parking_cost",
281 "total_reservation_cost",
282 ],
283 Self::Tariff => &["min_price", "max_price", "tax_included", "preauthorize_amount"],
284 Self::Credentials => &["roles", "hub_party_id"],
285 Self::ClientInfo => &["role"],
286 Self::Token | Self::AuthorizationInfo | Self::StartSession | Self::ReserveNow => &[],
289 }
290 }
291
292 #[must_use]
296 pub fn patch_crosses_unchanged(self, fields: &[&str]) -> bool {
297 let divergent = self.divergent_fields();
298 !fields.iter().any(|f| divergent.contains(f))
299 }
300
301 #[must_use]
311 pub fn for_endpoint(
312 module: &ModuleId,
313 interface: InterfaceRole,
314 path: &str,
315 payload: Payload,
316 ) -> Option<Self> {
317 let segments: Vec<&str> =
318 path.split('?').next().unwrap_or("").split('/').filter(|s| !s.is_empty()).collect();
319 let owned = interface == InterfaceRole::Receiver;
322 match module {
323 ModuleId::Locations => match (segments.len(), owned) {
324 (0 | 1, false) | (3, true) => Some(Self::Location),
325 (2, false) | (4, true) => Some(Self::Evse),
326 (3, false) | (5, true) => Some(Self::Connector),
327 _ => None,
328 },
329 ModuleId::Sessions => match (segments.len(), owned) {
330 (0, false) | (3, true) => Some(Self::Session),
331 _ => None,
333 },
334 ModuleId::Cdrs => (segments.len() <= 1).then_some(Self::Cdr),
337 ModuleId::Tariffs => match (segments.len(), owned) {
338 (0, false) | (3, true) => Some(Self::Tariff),
339 _ => None,
340 },
341 ModuleId::Tokens => match (segments.last(), owned) {
342 (Some(&"authorize"), false) => {
344 (payload == Payload::Response).then_some(Self::AuthorizationInfo)
345 }
346 _ => match (segments.len(), owned) {
347 (0, false) | (3, true) => Some(Self::Token),
348 _ => None,
349 },
350 },
351 ModuleId::Commands if payload == Payload::Request => match segments.first() {
354 Some(&"START_SESSION") => Some(Self::StartSession),
355 Some(&"RESERVE_NOW") => Some(Self::ReserveNow),
356 _ => None,
357 },
358 ModuleId::Credentials => segments.is_empty().then_some(Self::Credentials),
359 ModuleId::HubClientInfo => match (segments.len(), owned) {
360 (0, false) | (2, true) => Some(Self::ClientInfo),
361 _ => None,
362 },
363 _ => None,
364 }
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use serde_json::json;
372
373 fn location_2_2_1() -> Value {
374 json!({
375 "country_code": "BE", "party_id": "BEC", "id": "LOC1", "publish": true,
376 "address": "F.Rooseveltlaan 3A", "city": "Gent", "country": "BEL",
377 "coordinates": {"latitude": "51.047599", "longitude": "3.729944"},
378 "time_zone": "Europe/Brussels", "last_updated": "2019-06-24T12:39:09Z"
379 })
380 }
381
382 #[test]
383 fn a_page_reports_each_objects_losses_under_its_own_index() {
384 let mut location = location_2_2_1();
385 let page = Value::Array(vec![location.clone(), location.clone()]);
386 let up = ObjectKind::Location.bridge(&VersionNumber::V2_2_1, &VersionNumber::V2_3_0, page).unwrap();
387 assert!(up.lossy.is_empty(), "2.2.1 → 2.3.0 adds fields, it does not drop them");
388
389 location["help_phone"] = json!("+3212345678");
391 let up = ObjectKind::Location
392 .bridge(&VersionNumber::V2_2_1, &VersionNumber::V2_3_0, location_2_2_1())
393 .unwrap();
394 let mut with_phone = up.value.clone();
395 with_phone["help_phone"] = json!("+3212345678");
396 let page = Value::Array(vec![up.value, with_phone]);
397 let down = ObjectKind::Location.bridge(&VersionNumber::V2_3_0, &VersionNumber::V2_2_1, page).unwrap();
398 assert_eq!(down.lossy.len(), 1);
399 assert_eq!(down.lossy.as_slice()[0].pointer, "/1/help_phone");
400 }
401
402 #[test]
403 fn a_version_to_itself_is_the_identity_and_costs_nothing() {
404 let value = location_2_2_1();
405 let same = ObjectKind::Location
406 .bridge(&VersionNumber::V2_2_1, &VersionNumber::V2_2_1, value.clone())
407 .unwrap();
408 assert_eq!(same.value, value);
409 assert!(same.lossy.is_empty());
410 }
411
412 #[test]
413 fn a_crossing_this_build_cannot_make_is_refused_rather_than_guessed_at() {
414 let error = ObjectKind::Location
415 .bridge(&VersionNumber::V2_1_1, &VersionNumber::V2_3_0, location_2_2_1())
416 .unwrap_err();
417 assert!(matches!(error, BridgeError::Unsupported { .. }), "{error}");
418 assert!(!bridgeable(&VersionNumber::V2_1_1, &VersionNumber::V2_3_0));
419 assert!(bridgeable(&VersionNumber::V2_2_1, &VersionNumber::V2_3_0));
420 assert!(bridgeable(&VersionNumber::V2_1_1, &VersionNumber::V2_1_1));
421 }
422
423 #[test]
424 fn a_document_that_is_not_the_object_the_endpoint_carries_is_named_as_such() {
425 let error = ObjectKind::Tariff
426 .bridge(&VersionNumber::V2_2_1, &VersionNumber::V2_3_0, json!({"id": "1"}))
427 .unwrap_err();
428 match error {
429 BridgeError::Decode { kind, version, .. } => {
430 assert_eq!(kind, ObjectKind::Tariff);
431 assert_eq!(version, VersionNumber::V2_2_1);
432 }
433 other => panic!("{other}"),
434 }
435 }
436
437 #[test]
438 fn an_absent_data_field_survives() {
439 let out =
440 ObjectKind::Cdr.bridge(&VersionNumber::V2_3_0, &VersionNumber::V2_2_1, Value::Null).unwrap();
441 assert_eq!(out.value, Value::Null);
442 }
443
444 #[test]
445 fn the_locations_url_shapes_name_the_object_they_carry() {
446 let sender = |p: &str| {
447 ObjectKind::for_endpoint(&ModuleId::Locations, InterfaceRole::Sender, p, Payload::Response)
448 };
449 assert_eq!(sender(""), Some(ObjectKind::Location));
450 assert_eq!(sender("LOC1"), Some(ObjectKind::Location));
451 assert_eq!(sender("LOC1/3256"), Some(ObjectKind::Evse));
452 assert_eq!(sender("/LOC1/3256/1/"), Some(ObjectKind::Connector));
453
454 let receiver = |p: &str| {
455 ObjectKind::for_endpoint(&ModuleId::Locations, InterfaceRole::Receiver, p, Payload::Request)
456 };
457 assert_eq!(receiver("NL/TNM/LOC1"), Some(ObjectKind::Location));
458 assert_eq!(receiver("NL/TNM/LOC1/3256"), Some(ObjectKind::Evse));
459 assert_eq!(receiver("NL/TNM/LOC1/3256/1"), Some(ObjectKind::Connector));
460 }
461
462 #[test]
463 fn the_two_endpoints_whose_halves_differ_are_told_apart() {
464 let authorize = |payload| {
466 ObjectKind::for_endpoint(&ModuleId::Tokens, InterfaceRole::Sender, "012345/authorize", payload)
467 };
468 assert_eq!(authorize(Payload::Request), None);
469 assert_eq!(authorize(Payload::Response), Some(ObjectKind::AuthorizationInfo));
470
471 let command = |name: &str, payload| {
473 ObjectKind::for_endpoint(&ModuleId::Commands, InterfaceRole::Receiver, name, payload)
474 };
475 assert_eq!(command("START_SESSION", Payload::Request), Some(ObjectKind::StartSession));
476 assert_eq!(command("RESERVE_NOW", Payload::Request), Some(ObjectKind::ReserveNow));
477 assert_eq!(command("STOP_SESSION", Payload::Request), None);
478 assert_eq!(command("START_SESSION", Payload::Response), None);
479 }
480
481 #[test]
482 fn an_endpoint_whose_object_did_not_change_asks_for_no_work() {
483 let query = |module| ObjectKind::for_endpoint(module, InterfaceRole::Sender, "", Payload::Response);
484 assert_eq!(query(&ModuleId::ChargingProfiles), None);
485 assert_eq!(query(&ModuleId::Payments), None);
486 assert_eq!(query(&ModuleId::Versions), None);
487 assert_eq!(
488 ObjectKind::for_endpoint(
489 &ModuleId::Sessions,
490 InterfaceRole::Sender,
491 "SESS1/charging_preferences",
492 Payload::Request,
493 ),
494 None,
495 );
496 }
497}