ocpi_kit/v2_2_1/
credentials.rs1use bon::Builder;
14use serde::{Deserialize, Serialize};
15
16use crate::types::validate_fields;
17use crate::types::{
18 CountryCode, Extensions, OcpiString, PartyId, PartyRef, Url, Validate, Validator, ViolationCode,
19};
20
21use super::locations::BusinessDetails;
22use super::types::Role;
23
24#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29#[builder(on(_, into))]
30pub struct Credentials {
31 pub token: OcpiString<64>,
33 pub url: Url,
35 pub roles: Vec<CredentialsRole>,
37 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
39 #[builder(default)]
40 pub extensions: Extensions,
41}
42
43impl Credentials {
44 pub fn parties(&self) -> impl Iterator<Item = PartyRef> + '_ {
46 self.roles.iter().map(CredentialsRole::party)
47 }
48
49 #[must_use]
51 pub fn hosts(&self, party: &PartyRef) -> bool {
52 self.roles.iter().any(|r| &r.party() == party)
53 }
54
55 #[must_use]
61 pub fn is_hub(&self) -> bool {
62 self.roles.iter().any(|r| r.role == Role::Hub)
63 }
64
65 #[must_use]
67 pub fn hub_party(&self) -> Option<PartyRef> {
68 self.roles.iter().find(|r| r.role == Role::Hub).map(CredentialsRole::party)
69 }
70}
71
72impl Validate for Credentials {
73 fn validate_in(&self, v: &mut Validator) {
74 validate_fields!(self, v, token, url, roles);
75 if self.roles.is_empty() {
76 v.report_at(
77 "roles",
78 ViolationCode::EmptyRequiredList,
79 "Credentials has cardinality `+` roles: at least one is required",
80 );
81 }
82 let mut seen: Vec<(Role, PartyRef)> = Vec::new();
83 for (i, role) in self.roles.iter().enumerate() {
84 let key = (role.role, role.party());
85 if seen.contains(&key) {
86 v.enter("roles");
87 v.enter(&i.to_string());
88 v.report(
89 ViolationCode::Inconsistent,
90 format!(
91 "the combination {} / {} appears more than once; every role needs a \
92 unique combination of role, party_id and country_code",
93 key.0, key.1
94 ),
95 );
96 v.leave();
97 v.leave();
98 }
99 seen.push(key);
100 }
101 if let Some(bad) = self.token.as_str().chars().find(|c| !matches!(c, '!'..='~')) {
102 v.report_at(
103 "token",
104 ViolationCode::IllegalCharacter,
105 format!("a credentials token may only contain U+0021..U+007E; found U+{:04X}", bad as u32),
106 );
107 }
108 }
109}
110
111#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
115#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
116#[builder(on(_, into))]
117pub struct CredentialsRole {
118 pub role: Role,
120 pub business_details: BusinessDetails,
122 pub party_id: PartyId,
124 pub country_code: CountryCode,
126 #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
128 #[builder(default)]
129 pub extensions: Extensions,
130}
131
132impl CredentialsRole {
133 #[must_use]
135 pub fn party(&self) -> PartyRef {
136 PartyRef { country_code: self.country_code.clone(), party_id: self.party_id.clone() }
137 }
138}
139
140impl Validate for CredentialsRole {
141 fn validate_in(&self, v: &mut Validator) {
142 validate_fields!(self, v, role, business_details, party_id, country_code);
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn a_hub_is_a_role_here_not_a_field() {
152 let c = Credentials::builder()
153 .token("ebf3b399-779f-4497-9b9d-ac6ad3cc44d2")
154 .url(Url::new("https://hub.example.com/ocpi/versions").unwrap())
155 .roles(vec![
156 CredentialsRole::builder()
157 .role(Role::Hub)
158 .business_details(BusinessDetails::builder().name("Example Hub").build())
159 .party_id("HUB")
160 .country_code("NL")
161 .build(),
162 ])
163 .build();
164 assert!(c.is_hub());
165 assert_eq!(c.hub_party(), Some(PartyRef::new("NL", "HUB").unwrap()));
166 assert!(c.validate().is_ok());
167 assert!(!serde_json::to_string(&c).unwrap().contains("hub_party_id"));
169 }
170}