Skip to main content

pas_client/
types.rs

1use derive_more::{Display, From, FromStr, Into};
2use serde::{Deserialize, Serialize};
3use ulid::Ulid;
4
5use crate::error::Error;
6
7/// PAS ppnum identifier (OAuth `sub` claim, ULID format).
8///
9/// Immutable, unique per Ppoppo account. Returned as `sub` in OAuth tokens.
10/// Consumers store this as the sole link to PAS identity.
11#[derive(
12    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display, FromStr, From, Into,
13)]
14#[serde(transparent)]
15pub struct PpnumId(pub Ulid);
16
17/// Validated Ppoppo Number (11-digit, "777" prefix).
18///
19/// Guaranteed valid by construction: holding a `Ppnum` proves the format is correct.
20/// Use `"77712345678".parse::<Ppnum>()` or `Ppnum::try_from(string)` to create.
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(try_from = "String", into = "String")]
23pub struct Ppnum(String);
24
25impl Ppnum {
26    #[must_use]
27    pub fn as_str(&self) -> &str {
28        &self.0
29    }
30}
31
32impl std::fmt::Display for Ppnum {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        f.write_str(&self.0)
35    }
36}
37
38impl std::str::FromStr for Ppnum {
39    type Err = Error;
40
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        Self::try_from(s.to_owned())
43    }
44}
45
46impl TryFrom<String> for Ppnum {
47    type Error = Error;
48
49    fn try_from(s: String) -> Result<Self, Self::Error> {
50        if s.len() == 11 && s.starts_with("777") && s.bytes().all(|b| b.is_ascii_digit()) {
51            Ok(Self(s))
52        } else {
53            Err(Error::InvalidPpnum(s))
54        }
55    }
56}
57
58impl From<Ppnum> for String {
59    fn from(p: Ppnum) -> Self {
60        p.0
61    }
62}
63
64/// Consumer-defined user identifier (opaque string).
65///
66/// Returned by [`AccountResolver::resolve`](crate::middleware::AccountResolver::resolve).
67/// The consumer chooses the format (ULID, UUID, etc.).
68#[derive(
69    Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, From, Into,
70)]
71#[serde(transparent)]
72pub struct UserId(pub String);
73
74/// Consumer-defined session identifier (opaque string).
75///
76/// Returned by [`SessionStore::create`](crate::middleware::SessionStore::create).
77/// The consumer chooses the format (ULID, UUID, etc.).
78#[derive(
79    Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, From, Into,
80)]
81#[serde(transparent)]
82pub struct SessionId(pub String);
83
84/// PASERK key identifier.
85#[derive(
86    Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, From, Into,
87)]
88#[serde(transparent)]
89pub struct KeyId(pub String);
90
91#[cfg(test)]
92#[allow(clippy::unwrap_used)]
93mod tests {
94    use super::*;
95    use static_assertions::assert_impl_all;
96
97    assert_impl_all!(PpnumId: Send, Sync, Copy);
98    assert_impl_all!(Ppnum: Send, Sync);
99    assert_impl_all!(UserId: Send, Sync);
100    assert_impl_all!(SessionId: Send, Sync);
101    assert_impl_all!(KeyId: Send, Sync);
102
103    #[test]
104    fn valid_ppnum() {
105        assert!("77712345678".parse::<Ppnum>().is_ok());
106        assert!("77700000000".parse::<Ppnum>().is_ok());
107        assert!("77799999999".parse::<Ppnum>().is_ok());
108    }
109
110    #[test]
111    fn invalid_ppnum_wrong_prefix() {
112        assert!("12345678901".parse::<Ppnum>().is_err());
113        assert!("77812345678".parse::<Ppnum>().is_err());
114    }
115
116    #[test]
117    fn invalid_ppnum_wrong_length() {
118        assert!("7771234567".parse::<Ppnum>().is_err());
119        assert!("777123456789".parse::<Ppnum>().is_err());
120        assert!("".parse::<Ppnum>().is_err());
121    }
122
123    #[test]
124    fn invalid_ppnum_non_digits() {
125        assert!("777abcdefgh".parse::<Ppnum>().is_err());
126        assert!("7771234567a".parse::<Ppnum>().is_err());
127    }
128
129    #[test]
130    fn ppnum_serde_roundtrip() {
131        let ppnum: Ppnum = "77712345678".parse().unwrap();
132        let json = serde_json::to_string(&ppnum).unwrap();
133        assert_eq!(json, "\"77712345678\"");
134        let parsed: Ppnum = serde_json::from_str(&json).unwrap();
135        assert_eq!(parsed, ppnum);
136    }
137
138    #[test]
139    fn ppnum_id_serde_roundtrip() {
140        let id = PpnumId(Ulid::nil());
141        let json = serde_json::to_string(&id).unwrap();
142        let parsed: PpnumId = serde_json::from_str(&json).unwrap();
143        assert_eq!(parsed, id);
144    }
145
146    #[test]
147    fn user_id_from_string() {
148        let id = UserId::from("user-123".to_string());
149        assert_eq!(id.to_string(), "user-123");
150    }
151
152    #[test]
153    fn session_id_from_string() {
154        let id = SessionId::from("sess-abc".to_string());
155        assert_eq!(id.to_string(), "sess-abc");
156    }
157
158    #[test]
159    fn newtypes_prevent_mixing() {
160        fn takes_user_id(_: &UserId) {}
161        fn takes_session_id(_: &SessionId) {}
162
163        let user = UserId::from("id".to_string());
164        let session = SessionId::from("id".to_string());
165
166        takes_user_id(&user);
167        takes_session_id(&session);
168        // takes_user_id(&session);  // Compile error!
169        // takes_session_id(&user);  // Compile error!
170    }
171}