use bon::Builder;
use serde::{Deserialize, Serialize};
use crate::ocpi_enum;
use crate::ocpi_open_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;
#[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 follows_whitelist_recommendation(&self) -> bool {
!matches!(self.token_type, TokenType::AdHocUser | TokenType::AppUser)
|| self.whitelist == WhitelistType::Never
}
#[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; the spec advises \
staying within that as long as drivers may charge at such a Charge Point",
);
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AuthorizationDecision {
AllowFromCache,
AuthorizeRealtime,
Deny,
}
#[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",
);
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct LocationReferences {
pub location_id: CiString<36>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub evse_uids: Vec<CiString<36>>,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
pub extensions: Extensions,
}
impl Validate for LocationReferences {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(self, v, location_id, evse_uids);
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct EnergyContract {
pub supplier_name: OcpiString<64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contract_id: Option<OcpiString<64>>,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
pub extensions: Extensions,
}
impl Validate for EnergyContract {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(self, v, supplier_name, contract_id);
}
}
ocpi_enum! {
pub enum AllowedType {
Allowed = "ALLOWED",
Blocked = "BLOCKED",
Expired = "EXPIRED",
NoCredit = "NO_CREDIT",
NotAllowed = "NOT_ALLOWED",
}
}
ocpi_open_enum! {
pub enum TokenType {
AdHocUser = "AD_HOC_USER",
AppUser = "APP_USER",
Emaid = "EMAID",
Other = "OTHER",
Rfid = "RFID",
}
}
ocpi_enum! {
pub enum WhitelistType {
Always = "ALWAYS",
Allowed = "ALLOWED",
AllowedOffline = "ALLOWED_OFFLINE",
Never = "NEVER",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn token(whitelist: WhitelistType, valid: bool) -> Token {
Token::builder()
.country_code("NL")
.party_id("TNM")
.uid("012345678")
.token_type(TokenType::Rfid)
.contract_id("NL-TNM-C12345678-X")
.issuer("TheNewMotion")
.valid(valid)
.whitelist(whitelist)
.last_updated("2024-01-01T00:00:00Z".parse::<DateTime>().unwrap())
.build()
}
#[test]
fn whitelist_semantics_become_one_decision() {
use AuthorizationDecision::{AllowFromCache, AuthorizeRealtime, Deny};
for online in [true, false] {
assert_eq!(token(WhitelistType::Always, false).authorization_decision(online), AllowFromCache);
}
assert_eq!(token(WhitelistType::Never, true).authorization_decision(true), AuthorizeRealtime);
assert_eq!(token(WhitelistType::Never, true).authorization_decision(false), Deny);
assert_eq!(token(WhitelistType::AllowedOffline, false).authorization_decision(false), AllowFromCache);
assert_eq!(token(WhitelistType::Allowed, false).authorization_decision(false), Deny);
assert_eq!(token(WhitelistType::Allowed, true).authorization_decision(false), AllowFromCache);
}
#[test]
fn the_whitelist_recommendation_is_a_query_not_a_violation() {
let mut t = token(WhitelistType::Allowed, true);
t.token_type = TokenType::AppUser;
assert!(!t.follows_whitelist_recommendation());
assert!(t.validate().is_ok());
t.whitelist = WhitelistType::Never;
assert!(t.follows_whitelist_recommendation());
assert!(token(WhitelistType::Always, true).follows_whitelist_recommendation());
}
#[test]
fn long_group_ids_are_flagged_for_ocpp_compatibility() {
let mut t = token(WhitelistType::Allowed, true);
t.group_id = Some(CiString::new("G".repeat(21)).unwrap());
assert!(t.validate().unwrap_err().as_slice().iter().any(|x| x.pointer == "/group_id"));
t.group_id = Some(CiString::new("G".repeat(20)).unwrap());
assert!(t.validate().is_ok());
}
#[test]
fn round_trips_with_the_spec_field_names() {
let json = r#"{"country_code":"NL","party_id":"TNM","uid":"012345678","type":"RFID","contract_id":"NL-TNM-C12345678-X","issuer":"TheNewMotion","valid":true,"whitelist":"ALWAYS","last_updated":"2018-12-10T17:16:15Z"}"#;
let t: Token = serde_json::from_str(json).unwrap();
assert_eq!(t.token_type, TokenType::Rfid);
assert_eq!(serde_json::to_string(&t).unwrap(), json);
}
}