use bon::Builder;
use serde::{Deserialize, Serialize};
use crate::types::validate_fields;
use crate::types::{
CountryCode, Extensions, OcpiString, PartyId, PartyRef, Url, Validate, Validator, ViolationCode,
};
use super::locations::BusinessDetails;
use super::types::Role;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct Credentials {
pub token: OcpiString<64>,
pub url: Url,
pub roles: Vec<CredentialsRole>,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
#[builder(default)]
pub extensions: Extensions,
}
impl Credentials {
pub fn parties(&self) -> impl Iterator<Item = PartyRef> + '_ {
self.roles.iter().map(CredentialsRole::party)
}
#[must_use]
pub fn hosts(&self, party: &PartyRef) -> bool {
self.roles.iter().any(|r| &r.party() == party)
}
#[must_use]
pub fn is_hub(&self) -> bool {
self.roles.iter().any(|r| r.role == Role::Hub)
}
#[must_use]
pub fn hub_party(&self) -> Option<PartyRef> {
self.roles.iter().find(|r| r.role == Role::Hub).map(CredentialsRole::party)
}
}
impl Validate for Credentials {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(self, v, token, url, roles);
if self.roles.is_empty() {
v.report_at(
"roles",
ViolationCode::EmptyRequiredList,
"Credentials has cardinality `+` roles: at least one is required",
);
}
let mut seen: Vec<(Role, PartyRef)> = Vec::new();
for (i, role) in self.roles.iter().enumerate() {
let key = (role.role, role.party());
if seen.contains(&key) {
v.enter("roles");
v.enter(&i.to_string());
v.report(
ViolationCode::Inconsistent,
format!(
"the combination {} / {} appears more than once; every role needs a \
unique combination of role, party_id and country_code",
key.0, key.1
),
);
v.leave();
v.leave();
}
seen.push(key);
}
if let Some(bad) = self.token.as_str().chars().find(|c| !matches!(c, '!'..='~')) {
v.report_at(
"token",
ViolationCode::IllegalCharacter,
format!("a credentials token may only contain U+0021..U+007E; found U+{:04X}", bad as u32),
);
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[builder(on(_, into))]
pub struct CredentialsRole {
pub role: Role,
pub business_details: BusinessDetails,
pub party_id: PartyId,
pub country_code: CountryCode,
#[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
#[builder(default)]
pub extensions: Extensions,
}
impl CredentialsRole {
#[must_use]
pub fn party(&self) -> PartyRef {
PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
}
}
impl Validate for CredentialsRole {
fn validate_in(&self, v: &mut Validator) {
validate_fields!(self, v, role, business_details, party_id, country_code);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_hub_is_a_role_here_not_a_field() {
let c = Credentials::builder()
.token("ebf3b399-779f-4497-9b9d-ac6ad3cc44d2")
.url(Url::new("https://hub.example.com/ocpi/versions").unwrap())
.roles(vec![
CredentialsRole::builder()
.role(Role::Hub)
.business_details(BusinessDetails::builder().name("Example Hub").build())
.party_id("HUB")
.country_code("NL")
.build(),
])
.build();
assert!(c.is_hub());
assert_eq!(c.hub_party(), Some(PartyRef::new("NL", "HUB").unwrap()));
assert!(c.validate().is_ok());
assert!(!serde_json::to_string(&c).unwrap().contains("hub_party_id"));
}
}