Skip to main content

ocpi_kit/v2_1_1/
tokens.rs

1//! The *Tokens* module of OCPI 2.1.1.
2//!
3//! The 2.1.1 Token is much smaller: no owner fields, no `contract_id` (its `auth_id` plays that
4//! role), no `group_id`, no energy contract and no default charging profile. `TokenType` has two
5//! values.
6//!
7//! Spec: 2.1.1 §mod_tokens
8
9use bon::Builder;
10use serde::{Deserialize, Serialize};
11
12use crate::ocpi_lenient_enum;
13use crate::types::validate_fields;
14use crate::types::{DateTime, DisplayText, Extensions, OcpiString, Validate, Validator, ViolationCode};
15
16/// A token an EV driver uses to authorize charging, in OCPI 2.1.1.
17///
18/// Spec: 2.1.1 §mod_tokens_token_object
19#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
20#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
21#[builder(on(_, into))]
22pub struct Token {
23    /// Identification used by the CPO system to identify this token.
24    pub uid: OcpiString<36>,
25    /// Type of the token.
26    #[serde(rename = "type")]
27    pub token_type: TokenType,
28    /// Uniquely identifies the EV driver contract token within the eMSP's platform.
29    ///
30    /// Renamed to `contract_id` in OCPI 2.2, where `auth_id` disappeared entirely.
31    pub auth_id: OcpiString<36>,
32    /// Visual readable number/identification as printed on the Token.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub visual_number: Option<OcpiString<64>>,
35    /// Issuing company, most of the time the name printed on the token.
36    pub issuer: OcpiString<64>,
37    /// Whether this Token is valid.
38    pub valid: bool,
39    /// What type of white-listing is allowed.
40    pub whitelist: WhitelistType,
41    /// Language Code ISO 639-1: the Token owner's preferred interface language.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub language: Option<OcpiString<2>>,
44    /// Timestamp when this Token was last updated (or created).
45    pub last_updated: DateTime,
46    /// Undocumented JSON fields, preserved verbatim.
47    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
48    #[builder(default)]
49    pub extensions: Extensions,
50}
51
52impl Token {
53    /// What a CPO should do with this Token when a driver presents it.
54    ///
55    /// The whitelist semantics are unchanged across every OCPI version; see
56    /// [`v2_3_0::tokens::Token::authorization_decision`](crate::v2_3_0::tokens::Token::authorization_decision).
57    #[must_use]
58    pub fn authorization_decision(&self, online: bool) -> AuthorizationDecision {
59        match self.whitelist {
60            WhitelistType::Always => AuthorizationDecision::AllowFromCache,
61            WhitelistType::Allowed => {
62                if online {
63                    AuthorizationDecision::AuthorizeRealtime
64                } else if self.valid {
65                    AuthorizationDecision::AllowFromCache
66                } else {
67                    AuthorizationDecision::Deny
68                }
69            }
70            WhitelistType::AllowedOffline => {
71                if online {
72                    AuthorizationDecision::AuthorizeRealtime
73                } else {
74                    AuthorizationDecision::AllowFromCache
75                }
76            }
77            WhitelistType::Never => {
78                if online {
79                    AuthorizationDecision::AuthorizeRealtime
80                } else {
81                    AuthorizationDecision::Deny
82                }
83            }
84        }
85    }
86}
87
88impl Validate for Token {
89    fn validate_in(&self, v: &mut Validator) {
90        validate_fields!(
91            self, v, uid, token_type as "type", auth_id, visual_number, issuer, whitelist,
92            language, last_updated,
93        );
94    }
95}
96
97/// The response to a real-time authorization request, in OCPI 2.1.1.
98///
99/// Carries no `token` and no `authorization_reference`: both arrived in OCPI 2.2.
100///
101/// Spec: 2.1.1 §mod_tokens_authorizationinfo_object
102#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
103#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
104#[builder(on(_, into))]
105pub struct AuthorizationInfo {
106    /// Status of the Token, and whether charging is allowed at the optionally given location.
107    pub allowed: AllowedType,
108    /// The location, if it was in the request and the driver may charge there.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub location: Option<LocationReferences>,
111    /// Additional information to display to the EV driver.
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub info: Option<DisplayText>,
114    /// Undocumented JSON fields, preserved verbatim.
115    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
116    #[builder(default)]
117    pub extensions: Extensions,
118}
119
120impl Validate for AuthorizationInfo {
121    fn validate_in(&self, v: &mut Validator) {
122        validate_fields!(self, v, allowed, location, info);
123        if self.allowed != AllowedType::Allowed && self.location.is_some() {
124            v.report_at(
125                "location",
126                ViolationCode::Inconsistent,
127                "a location is only returned when the driver is allowed to charge there",
128            );
129        }
130    }
131}
132
133/// References to a location, its EVSEs and their connectors.
134///
135/// OCPI 2.2 dropped `connector_ids`: authorization is per EVSE from there on.
136///
137/// Spec: 2.1.1 §mod_tokens_locationreferences_class
138#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
139#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
140pub struct LocationReferences {
141    /// Unique identifier for the location.
142    pub location_id: OcpiString<39>,
143    /// Unique identifiers for EVSEs within the given location.
144    #[serde(default, skip_serializing_if = "Vec::is_empty")]
145    pub evse_uids: Vec<OcpiString<39>>,
146    /// Identifies the connectors within the given EVSEs.
147    #[serde(default, skip_serializing_if = "Vec::is_empty")]
148    pub connector_ids: Vec<OcpiString<36>>,
149    /// Undocumented JSON fields, preserved verbatim.
150    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
151    pub extensions: Extensions,
152}
153
154impl Validate for LocationReferences {
155    fn validate_in(&self, v: &mut Validator) {
156        validate_fields!(self, v, location_id, evse_uids, connector_ids);
157        if !self.connector_ids.is_empty() && self.evse_uids.is_empty() {
158            v.report_at(
159                "evse_uids",
160                ViolationCode::MissingConditional,
161                "connectors are identified within an EVSE, so naming connectors without naming \
162                 the EVSE they belong to is ambiguous",
163            );
164        }
165    }
166}
167
168// Wire-identical to OCPI 2.3.0.
169pub use crate::v2_3_0::tokens::{AllowedType, AuthorizationDecision, WhitelistType};
170
171ocpi_lenient_enum! {
172    /// The type of a Token, in OCPI 2.1.1.
173    ///
174    /// Two values. `APP_USER` and `AD_HOC_USER` arrived in OCPI 2.2, `EMAID` in 2.3.0.
175    ///
176    /// Spec: 2.1.1 §mod_tokens_tokentype_enum
177    pub enum TokenType {
178        /// Other type of token.
179        Other = "OTHER",
180        /// RFID Token.
181        Rfid = "RFID",
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn the_2_1_1_token_type_has_two_values() {
191        assert_eq!(TokenType::ALL_KNOWN.len(), 2);
192        let app_user: TokenType = "APP_USER".into();
193        assert!(!app_user.is_known(), "APP_USER arrived in OCPI 2.2");
194        assert_eq!(serde_json::to_string(&app_user).unwrap(), "\"APP_USER\"");
195    }
196
197    #[test]
198    fn a_2_1_1_token_round_trips() {
199        let json = r#"{"uid":"012345678","type":"RFID","auth_id":"DE8ACC12E46L89","visual_number":"DF000-2001-8999","issuer":"TheNewMotion","valid":true,"whitelist":"ALLOWED","last_updated":"2018-12-10T17:16:15Z"}"#;
200        let token: Token = serde_json::from_str(json).unwrap();
201        assert!(token.validate().is_ok());
202        assert_eq!(serde_json::to_string(&token).unwrap(), json);
203    }
204
205    #[test]
206    fn naming_connectors_without_their_evse_is_ambiguous() {
207        let refs = LocationReferences {
208            location_id: OcpiString::new("LOC1").unwrap(),
209            evse_uids: Vec::new(),
210            connector_ids: vec![OcpiString::new("1").unwrap()],
211            extensions: Extensions::new(),
212        };
213        assert_eq!(refs.validate().unwrap_err().as_slice()[0].pointer, "/evse_uids");
214    }
215}