use bon::Builder;
use serde::{Deserialize, Serialize};
use crate::ocpi_lenient_enum;
use crate::types::validate_fields;
use crate::types::{
CiString, ContractId, CountryCode, DateTime, DisplayText, Extensions, OcpiString, PartyId, PartyRef,
Validate, Validator, ViolationCode,
};
use super::sessions::ProfileType;
pub use crate::v2_3_0::tokens::{
AllowedType, AuthorizationDecision, EnergyContract, LocationReferences, WhitelistType,
};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct Token {
pub country_code: CountryCode,
pub party_id: PartyId,
pub uid: CiString<36>,
#[serde(rename = "type")]
pub token_type: TokenType,
pub contract_id: ContractId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visual_number: Option<OcpiString<64>>,
pub issuer: OcpiString<64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub group_id: Option<CiString<36>>,
pub valid: bool,
pub whitelist: WhitelistType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<OcpiString<2>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_profile_type: Option<ProfileType>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub energy_contract: Option<EnergyContract>,
pub last_updated: DateTime,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
#[builder(default)]
pub extensions: Extensions,
}
impl Token {
#[must_use]
pub fn owner_party(&self) -> PartyRef {
PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
}
#[must_use]
pub fn authorization_decision(&self, online: bool) -> AuthorizationDecision {
match self.whitelist {
WhitelistType::Always => AuthorizationDecision::AllowFromCache,
WhitelistType::Allowed => {
if online {
AuthorizationDecision::AuthorizeRealtime
} else if self.valid {
AuthorizationDecision::AllowFromCache
} else {
AuthorizationDecision::Deny
}
}
WhitelistType::AllowedOffline => {
if online {
AuthorizationDecision::AuthorizeRealtime
} else {
AuthorizationDecision::AllowFromCache
}
}
WhitelistType::Never => {
if online {
AuthorizationDecision::AuthorizeRealtime
} else {
AuthorizationDecision::Deny
}
}
}
}
}
impl Validate for Token {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(
self, v, country_code, party_id, uid, token_type as "type", contract_id,
visual_number, issuer, group_id, whitelist, language, default_profile_type,
energy_contract, last_updated,
);
if self.group_id.as_ref().is_some_and(|g| g.len() > 20) {
v.report_at(
"group_id",
ViolationCode::Inconsistent,
"OCPP 1.5/1.6 only supports group IDs up to 20 characters",
);
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct AuthorizationInfo {
pub allowed: AllowedType,
pub token: Token,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location: Option<LocationReferences>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub authorization_reference: Option<CiString<36>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub info: Option<DisplayText>,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
#[builder(default)]
pub extensions: Extensions,
}
impl Validate for AuthorizationInfo {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(self, v, allowed, token, location, authorization_reference, info);
if self.allowed != AllowedType::Allowed && self.location.is_some() {
v.report_at(
"location",
ViolationCode::Inconsistent,
"a location is only returned when the driver is allowed to charge there",
);
}
}
}
ocpi_lenient_enum! {
pub enum TokenType {
AdHocUser = "AD_HOC_USER",
AppUser = "APP_USER",
Other = "OTHER",
Rfid = "RFID",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn emaid_arrived_in_2_3_0() {
assert_eq!(TokenType::ALL_KNOWN.len(), 4);
let emaid: TokenType = "EMAID".into();
assert!(!emaid.is_known());
assert!(emaid.validate().is_err(), "2.2.1 declares TokenType closed");
assert_eq!(serde_json::to_string(&emaid).unwrap(), "\"EMAID\"", "but the value survives");
}
#[test]
fn whitelist_semantics_are_unchanged_between_versions() {
let t = Token::builder()
.country_code("NL")
.party_id("TNM")
.uid("012345678")
.token_type(TokenType::Rfid)
.contract_id("NL-TNM-C12345678-X")
.issuer("TheNewMotion")
.valid(true)
.whitelist(WhitelistType::Never)
.last_updated("2024-01-01T00:00:00Z".parse::<DateTime>().unwrap())
.build();
assert_eq!(t.authorization_decision(false), AuthorizationDecision::Deny);
assert_eq!(t.authorization_decision(true), AuthorizationDecision::AuthorizeRealtime);
assert!(t.validate().is_ok());
}
}