Skip to main content

ocpi_kit/v2_1_1/
credentials.rs

1//! The *Credentials* module of OCPI 2.1.1.
2//!
3//! The 2.1.1 credentials object is **flat**: one party, one role — implied by which interface the
4//! connection is on rather than stated. OCPI 2.2 replaced this with the `roles` list, which is
5//! what made platforms hosting several parties expressible at all.
6//!
7//! Spec: 2.1.1 §credentials
8
9use bon::Builder;
10use serde::{Deserialize, Serialize};
11
12use crate::types::validate_fields;
13use crate::types::{Extensions, OcpiString, PartyRef, Url, Validate, Validator, ViolationCode};
14
15use super::locations::BusinessDetails;
16
17/// The credentials one party gives another, in OCPI 2.1.1.
18///
19/// Spec: 2.1.1 §credentials_credentials_object
20#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Builder)]
21#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22#[builder(on(_, into))]
23pub struct Credentials {
24    /// The token for the other party to authenticate in your system.
25    pub token: OcpiString<64>,
26    /// The URL to your API versions endpoint.
27    pub url: Url,
28    /// Details of this party.
29    pub business_details: BusinessDetails,
30    /// CPO or eMSP ID of this party.
31    pub party_id: OcpiString<3>,
32    /// Country code of the country this party is operating in.
33    pub country_code: OcpiString<2>,
34    /// Undocumented JSON fields, preserved verbatim.
35    #[serde(flatten, default, skip_serializing_if = "Extensions::is_empty")]
36    #[builder(default)]
37    pub extensions: Extensions,
38}
39
40impl Credentials {
41    /// The single party these credentials speak for.
42    ///
43    /// # Errors
44    ///
45    /// Returns [`crate::types::InvalidString`] if the two fields are not a usable party
46    /// reference, which can happen for a value that came off the wire.
47    pub fn party(&self) -> Result<PartyRef, crate::types::InvalidString> {
48        PartyRef::new(self.country_code.as_str(), self.party_id.as_str())
49    }
50}
51
52impl Validate for Credentials {
53    fn validate_in(&self, v: &mut Validator) {
54        validate_fields!(self, v, token, url, business_details, party_id, country_code);
55        if let Some(bad) = self.token.as_str().chars().find(|c| !matches!(c, '!'..='~')) {
56            v.report_at(
57                "token",
58                ViolationCode::IllegalCharacter,
59                format!("a credentials token may only contain U+0021..U+007E; found U+{:04X}", bad as u32),
60            );
61        }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn the_2_1_1_credentials_object_is_flat() {
71        let json = r#"{"token":"ebf3b399-779f-4497-9b9d-ac6ad3cc44d2","url":"https://example.com/ocpi/cpo/","business_details":{"name":"Example Operator"},"party_id":"EXA","country_code":"NL"}"#;
72        let credentials: Credentials = serde_json::from_str(json).unwrap();
73        assert!(credentials.validate().is_ok());
74        assert_eq!(credentials.party().unwrap().to_string(), "NL/EXA");
75        assert_eq!(serde_json::to_string(&credentials).unwrap(), json);
76        // There is no `roles` list here; a 2.2-style body keeps it in extensions.
77        assert!(!json.contains("roles"));
78    }
79}