ocpi_kit/v2_1_1/mod.rs
1//! The OCPI **2.1.1** wire model: the legacy version, and still in the field.
2//!
3//! OCPI 2.1.1 predates almost everything the later versions are built on. It has:
4//!
5//! * **no owner fields on objects** — `country_code` and `party_id` came with 2.2, so in 2.1.1
6//! the owner is known only from the URL and the credentials handshake;
7//! * **no message routing** — the four `OCPI-*` headers do not exist, so a connection is
8//! strictly peer-to-peer;
9//! * **a flat `Credentials` object** — one party, one role, no `roles` list;
10//! * **no `Price`** — a cost is a bare `number` excluding VAT, and a `PriceComponent` has no
11//! `vat` field at all;
12//! * **no `CommandResult`** — the asynchronous callback reuses `CommandResponse`, whose
13//! `CommandResponseType` therefore includes `TIMEOUT`;
14//! * **seven modules** — no Hub Client Info, no Charging Profiles, no Payments;
15//! * **`start_datetime`/`end_datetime`** on a Session, without the second underscore every later
16//! version uses.
17//!
18//! Everything here is decoded leniently: 2.1.1 declares every enum closed, and a peer still
19//! running it in 2026 will have plugs, token types and dimensions that the 2015 list does not
20//! contain. See [`ocpi_lenient_enum!`](crate::ocpi_lenient_enum).
21//!
22//! # Talking to a 2.1.1 peer
23//!
24//! [`Quirks::for_version`](crate::transport::Quirks::for_version) sets the two flags such a peer
25//! needs: it does not Base64-encode the `Authorization` token, and it has no routing headers.
26//!
27//! Spec: <https://github.com/ocpi/ocpi>, `release-2.1.1-bugfixes`
28
29pub mod cdrs;
30pub mod commands;
31pub mod credentials;
32pub mod locations;
33pub mod sessions;
34pub mod tariffs;
35pub mod tokens;
36
37/// The *Versions* module of OCPI 2.1.1.
38///
39/// Wire-identical to the later versions except that `Endpoint` has no `role` field:
40///
41/// > *NOTE: OCPI 2.2 introduced the role field in the version details. Older versions of OCPI do
42/// > not support this.*
43///
44/// This crate models `Endpoint.role` as required and defaults it to `SENDER` when a 2.1.1 peer
45/// omits it, which is what the specification advises for the credentials module and the only
46/// sensible reading for a version with no interface roles at all.
47///
48/// Spec: 2.1.1 §version_information_endpoint
49pub mod versions {
50 use serde::{Deserialize, Serialize};
51
52 use crate::types::validate_fields;
53 use crate::types::{Extensions, Url, Validate, Validator};
54 use crate::{InterfaceRole, ModuleId, VersionNumber};
55
56 pub use crate::v2_3_0::versions::Version;
57
58 /// The endpoints a 2.1.1 party implements for one version.
59 ///
60 /// Spec: 2.1.1 §version_information_endpoint
61 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
62 #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
63 pub struct VersionDetails {
64 /// The version number these endpoints belong to.
65 pub version: VersionNumber,
66 /// The supported endpoints for this version.
67 pub endpoints: Vec<Endpoint>,
68 /// Undocumented JSON fields, preserved verbatim.
69 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
70 pub extensions: Extensions,
71 }
72
73 impl VersionDetails {
74 /// The URL of a module's endpoint.
75 ///
76 /// There are no interface roles in 2.1.1, so a module has at most one endpoint.
77 #[must_use]
78 pub fn url(&self, module: &ModuleId) -> Option<&Url> {
79 self.endpoints.iter().find(|e| e.identifier.matches(module)).map(|e| &e.url)
80 }
81 }
82
83 impl Validate for VersionDetails {
84 fn validate_in(&self, v: &mut Validator) {
85 validate_fields!(self, v, version, endpoints);
86 }
87 }
88
89 /// One module endpoint of a 2.1.1 party.
90 ///
91 /// Spec: 2.1.1 §version_information_endpoint
92 #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
93 #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
94 pub struct Endpoint {
95 /// Endpoint identifier.
96 pub identifier: ModuleId,
97 /// URL to the endpoint.
98 pub url: Url,
99 /// Undocumented JSON fields, preserved verbatim.
100 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
101 pub extensions: Extensions,
102 }
103
104 impl Endpoint {
105 /// Creates an endpoint entry.
106 #[must_use]
107 pub fn new(identifier: ModuleId, url: Url) -> Self {
108 Self { identifier, url, extensions: Extensions::new() }
109 }
110
111 /// The interface role this endpoint would have in OCPI 2.2 and later.
112 ///
113 /// Always `SENDER`, since 2.1.1 has no roles and the specification advises sending
114 /// `SENDER` where one is required.
115 #[must_use]
116 pub const fn assumed_role(&self) -> InterfaceRole {
117 InterfaceRole::Sender
118 }
119 }
120
121 impl Validate for Endpoint {
122 fn validate_in(&self, v: &mut Validator) {
123 validate_fields!(self, v, identifier, url);
124 }
125 }
126
127 #[cfg(test)]
128 mod tests {
129 use super::*;
130
131 #[test]
132 fn a_2_1_1_endpoint_has_no_role_field() {
133 let json = r#"{"version":"2.1.1","endpoints":[{"identifier":"credentials","url":"https://example.com/ocpi/2.1.1/credentials"}]}"#;
134 let details: VersionDetails = serde_json::from_str(json).unwrap();
135 assert_eq!(details.endpoints[0].assumed_role(), InterfaceRole::Sender);
136 assert!(details.url(&ModuleId::Credentials).is_some());
137 assert_eq!(serde_json::to_string(&details).unwrap(), json);
138 }
139 }
140}
141
142pub use cdrs::Cdr;
143pub use credentials::Credentials;
144pub use locations::{Connector, Evse, Location};
145pub use sessions::Session;
146pub use tariffs::Tariff;
147pub use tokens::Token;
148pub use versions::{Endpoint, Version, VersionDetails};
149
150/// The version number this module implements.
151pub const VERSION: crate::VersionNumber = crate::VersionNumber::V2_1_1;