Skip to main content

ocpi_kit/v2_2_1/
credentials.rs

1//! The *Credentials* module of OCPI 2.2.1, as a delta from
2//! [`v2_3_0::credentials`](crate::v2_3_0::credentials).
3//!
4//! Two differences, both about hubs:
5//!
6//! * there is no `hub_party_id` — a hub identifies itself with the `HUB`
7//!   [`Role`] instead, which 2.3.0 removed;
8//! * a hub's `roles` list does **not** include the parties reachable through it, which is what
9//!   2.3.0's note about Roaming Hubs changed.
10//!
11//! Spec: 2.2.1 §credentials_credentials_endpoint
12
13use 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/// The credentials one platform gives another, in OCPI 2.2.1.
25///
26/// Spec: 2.2.1 §credentials_credentials_object
27#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29#[builder(on(_, into))]
30pub struct Credentials {
31    /// The credentials token for the other party to authenticate in your system.
32    pub token: OcpiString<64>,
33    /// The URL to your API versions endpoint.
34    pub url: Url,
35    /// The roles this party provides. Cardinality `+`.
36    pub roles: Vec<CredentialsRole>,
37    /// Undocumented JSON fields, preserved verbatim.
38    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
39    #[builder(default)]
40    pub extensions: Extensions,
41}
42
43impl Credentials {
44    /// Every party this platform speaks for.
45    pub fn parties(&self) -> impl Iterator<Item = PartyRef> + '_ {
46        self.roles.iter().map(CredentialsRole::party)
47    }
48
49    /// Whether this platform hosts the given party.
50    #[must_use]
51    pub fn hosts(&self, party: &PartyRef) -> bool {
52        self.roles.iter().any(|r| &r.party() == party)
53    }
54
55    /// Whether this platform declares itself a hub.
56    ///
57    /// In 2.2.1 that is the `HUB` role. OCPI 2.3.0 removed the role and uses
58    /// `Credentials.hub_party_id` instead; see
59    /// [`v2_3_0::credentials::Credentials::is_routing_platform`](crate::v2_3_0::credentials::Credentials::is_routing_platform).
60    #[must_use]
61    pub fn is_hub(&self) -> bool {
62        self.roles.iter().any(|r| r.role == Role::Hub)
63    }
64
65    /// The party that fills the `HUB` role, if any.
66    #[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/// One role a platform provides, with the party that fills it, in OCPI 2.2.1.
112///
113/// Spec: 2.2.1 §credentials_credentials_role_class
114#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
115#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
116#[builder(on(_, into))]
117pub struct CredentialsRole {
118    /// Type of role.
119    pub role: Role,
120    /// Details of this party.
121    pub business_details: BusinessDetails,
122    /// CPO, eMSP (or other role) ID of this party.
123    pub party_id: PartyId,
124    /// ISO-3166 alpha-2 country code of the country this party is operating in.
125    pub country_code: CountryCode,
126    /// Undocumented JSON fields, preserved verbatim.
127    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
128    #[builder(default)]
129    pub extensions: Extensions,
130}
131
132impl CredentialsRole {
133    /// The party filling this role.
134    #[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        // No `hub_party_id` field exists in 2.2.1, so it would land in extensions.
168        assert!(!serde_json::to_string(&c).unwrap().contains("hub_party_id"));
169    }
170}