1use bon::Builder;
10use serde::{Deserialize, Serialize};
11
12use crate::ocpi_lenient_enum;
13use crate::types::validate_fields;
14use crate::types::{
15 CiString, ContractId, CountryCode, DateTime, DisplayText, Extensions, OcpiString, PartyId, PartyRef,
16 Validate, Validator, ViolationCode,
17};
18
19use super::sessions::ProfileType;
20
21pub use crate::v2_3_0::tokens::{
23 AllowedType, AuthorizationDecision, EnergyContract, LocationReferences, WhitelistType,
24};
25
26#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
30#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
31#[builder(on(_, into))]
32pub struct Token {
33 pub country_code: CountryCode,
35 pub party_id: PartyId,
37 pub uid: CiString<36>,
39 #[serde(rename = "type")]
41 pub token_type: TokenType,
42 pub contract_id: ContractId,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub visual_number: Option<OcpiString<64>>,
47 pub issuer: OcpiString<64>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub group_id: Option<CiString<36>>,
52 pub valid: bool,
54 pub whitelist: WhitelistType,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub language: Option<OcpiString<2>>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub default_profile_type: Option<ProfileType>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub energy_contract: Option<EnergyContract>,
65 pub last_updated: DateTime,
67 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
69 #[builder(default)]
70 pub extensions: Extensions,
71}
72
73impl Token {
74 #[must_use]
76 pub fn owner_party(&self) -> PartyRef {
77 PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
78 }
79
80 #[must_use]
85 pub fn authorization_decision(&self, online: bool) -> AuthorizationDecision {
86 match self.whitelist {
87 WhitelistType::Always => AuthorizationDecision::AllowFromCache,
88 WhitelistType::Allowed => {
89 if online {
90 AuthorizationDecision::AuthorizeRealtime
91 } else if self.valid {
92 AuthorizationDecision::AllowFromCache
93 } else {
94 AuthorizationDecision::Deny
95 }
96 }
97 WhitelistType::AllowedOffline => {
98 if online {
99 AuthorizationDecision::AuthorizeRealtime
100 } else {
101 AuthorizationDecision::AllowFromCache
102 }
103 }
104 WhitelistType::Never => {
105 if online {
106 AuthorizationDecision::AuthorizeRealtime
107 } else {
108 AuthorizationDecision::Deny
109 }
110 }
111 }
112 }
113}
114
115impl Validate for Token {
116 fn validate_in(&self, v: &mut Validator) {
117 validate_fields!(
118 self, v, country_code, party_id, uid, token_type as "type", contract_id,
119 visual_number, issuer, group_id, whitelist, language, default_profile_type,
120 energy_contract, last_updated,
121 );
122 if self.group_id.as_ref().is_some_and(|g| g.len() > 20) {
123 v.report_at(
124 "group_id",
125 ViolationCode::Inconsistent,
126 "OCPP 1.5/1.6 only supports group IDs up to 20 characters",
127 );
128 }
129 }
130}
131
132#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
136#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
137#[builder(on(_, into))]
138pub struct AuthorizationInfo {
139 pub allowed: AllowedType,
141 pub token: Token,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub location: Option<LocationReferences>,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub authorization_reference: Option<CiString<36>>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub info: Option<DisplayText>,
152 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
154 #[builder(default)]
155 pub extensions: Extensions,
156}
157
158impl Validate for AuthorizationInfo {
159 fn validate_in(&self, v: &mut Validator) {
160 validate_fields!(self, v, allowed, token, location, authorization_reference, info);
161 if self.allowed != AllowedType::Allowed && self.location.is_some() {
162 v.report_at(
163 "location",
164 ViolationCode::Inconsistent,
165 "a location is only returned when the driver is allowed to charge there",
166 );
167 }
168 }
169}
170
171ocpi_lenient_enum! {
172 pub enum TokenType {
179 AdHocUser = "AD_HOC_USER",
181 AppUser = "APP_USER",
183 Other = "OTHER",
185 Rfid = "RFID",
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 #[test]
195 fn emaid_arrived_in_2_3_0() {
196 assert_eq!(TokenType::ALL_KNOWN.len(), 4);
197 let emaid: TokenType = "EMAID".into();
198 assert!(!emaid.is_known());
199 assert!(emaid.validate().is_err(), "2.2.1 declares TokenType closed");
200 assert_eq!(serde_json::to_string(&emaid).unwrap(), "\"EMAID\"", "but the value survives");
201 }
202
203 #[test]
204 fn whitelist_semantics_are_unchanged_between_versions() {
205 let t = Token::builder()
206 .country_code("NL")
207 .party_id("TNM")
208 .uid("012345678")
209 .token_type(TokenType::Rfid)
210 .contract_id("NL-TNM-C12345678-X")
211 .issuer("TheNewMotion")
212 .valid(true)
213 .whitelist(WhitelistType::Never)
214 .last_updated("2024-01-01T00:00:00Z".parse::<DateTime>().unwrap())
215 .build();
216 assert_eq!(t.authorization_decision(false), AuthorizationDecision::Deny);
217 assert_eq!(t.authorization_decision(true), AuthorizationDecision::AuthorizeRealtime);
218 assert!(t.validate().is_ok());
219 }
220}