1use derive_more::{Display, From, FromStr, Into};
2use serde::{Deserialize, Serialize};
3use ulid::Ulid;
4
5use crate::error::Error;
6
7#[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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(try_from = "String", into = "String")]
36pub struct Ppnum(String);
37
38impl Ppnum {
39 #[must_use]
40 pub fn as_str(&self) -> &str {
41 &self.0
42 }
43}
44
45impl std::fmt::Display for Ppnum {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.write_str(&self.0)
48 }
49}
50
51impl std::str::FromStr for Ppnum {
52 type Err = Error;
53
54 fn from_str(s: &str) -> Result<Self, Self::Err> {
55 Self::try_from(s.to_owned())
56 }
57}
58
59impl TryFrom<String> for Ppnum {
60 type Error = Error;
61
62 fn try_from(s: String) -> Result<Self, Self::Error> {
63 if s.len() >= 11 && s.bytes().all(|b| b.is_ascii_digit()) {
67 Ok(Self(s))
68 } else {
69 Err(Error::InvalidPpnum(s))
70 }
71 }
72}
73
74impl From<Ppnum> for String {
75 fn from(p: Ppnum) -> Self {
76 p.0
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, From, Into)]
85#[serde(transparent)]
86pub struct UserId(pub String);
87
88#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, From, Into)]
93#[serde(transparent)]
94pub struct SessionId(pub String);
95
96#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, From, Into)]
98#[serde(transparent)]
99pub struct KeyId(pub String);
100
101#[cfg(test)]
102#[allow(clippy::unwrap_used)]
103mod tests {
104 use super::*;
105 use static_assertions::assert_impl_all;
106
107 assert_impl_all!(PpnumId: Send, Sync, Copy);
108 assert_impl_all!(Ppnum: Send, Sync);
109 assert_impl_all!(UserId: Send, Sync);
110 assert_impl_all!(SessionId: Send, Sync);
111 assert_impl_all!(KeyId: Send, Sync);
112
113 #[test]
114 fn valid_ppnum_independent_11_digits() {
115 assert!("12312345678".parse::<Ppnum>().is_ok()); assert!("77712345678".parse::<Ppnum>().is_ok()); assert!("00000000000".parse::<Ppnum>().is_ok()); assert!("99999999999".parse::<Ppnum>().is_ok());
120 }
121
122 #[test]
123 fn valid_ppnum_dependent_variable_length() {
124 assert!("123123456780001".parse::<Ppnum>().is_ok()); assert!("1231234567800010001".parse::<Ppnum>().is_ok()); assert!("12312345678000100010001".parse::<Ppnum>().is_ok()); }
129
130 #[test]
131 fn invalid_ppnum_too_short() {
132 assert!(matches!("1234567890".parse::<Ppnum>(), Err(Error::InvalidPpnum(_)))); assert!(matches!("123".parse::<Ppnum>(), Err(Error::InvalidPpnum(_)))); assert!(matches!("".parse::<Ppnum>(), Err(Error::InvalidPpnum(_)))); }
139
140 #[test]
141 fn invalid_ppnum_non_digits() {
142 assert!(matches!("123abcdefgh".parse::<Ppnum>(), Err(Error::InvalidPpnum(_)))); assert!(matches!("12312345678a".parse::<Ppnum>(), Err(Error::InvalidPpnum(_)))); assert!(matches!("123-1234-5678".parse::<Ppnum>(), Err(Error::InvalidPpnum(_)))); assert!(matches!("12312345678 ".parse::<Ppnum>(), Err(Error::InvalidPpnum(_)))); }
147
148 #[test]
149 fn invalid_ppnum_unicode_digits() {
150 assert!(matches!("12312345678".parse::<Ppnum>(), Err(Error::InvalidPpnum(_)))); assert!(matches!("١٢٣١٢٣٤٥٦٧٨".parse::<Ppnum>(), Err(Error::InvalidPpnum(_)))); assert!(matches!("১২৩১২৩৪৫৬৭৮".parse::<Ppnum>(), Err(Error::InvalidPpnum(_)))); }
158
159 #[test]
160 fn ppnum_serde_rejects_invalid() {
161 assert!(serde_json::from_str::<Ppnum>("\"123\"").is_err()); assert!(serde_json::from_str::<Ppnum>("\"abc12345678\"").is_err()); assert!(serde_json::from_str::<Ppnum>("\"\"").is_err()); assert!(serde_json::from_str::<Ppnum>("\"123-1234-5678\"").is_err()); }
169
170 #[test]
171 fn ppnum_serde_roundtrip() {
172 let ppnum: Ppnum = "12312345678".parse().unwrap();
173 let json = serde_json::to_string(&ppnum).unwrap();
174 assert_eq!(json, "\"12312345678\"");
175 let parsed: Ppnum = serde_json::from_str(&json).unwrap();
176 assert_eq!(parsed, ppnum);
177 }
178
179 #[test]
180 fn ppnum_id_serde_roundtrip() {
181 let id = PpnumId(Ulid::nil());
182 let json = serde_json::to_string(&id).unwrap();
183 let parsed: PpnumId = serde_json::from_str(&json).unwrap();
184 assert_eq!(parsed, id);
185 }
186
187 #[test]
188 fn user_id_from_string() {
189 let id = UserId::from("user-123".to_string());
190 assert_eq!(id.to_string(), "user-123");
191 }
192
193 #[test]
194 fn session_id_from_string() {
195 let id = SessionId::from("sess-abc".to_string());
196 assert_eq!(id.to_string(), "sess-abc");
197 }
198
199 #[test]
200 fn newtypes_prevent_mixing() {
201 fn takes_user_id(_: &UserId) {}
202 fn takes_session_id(_: &SessionId) {}
203
204 let user = UserId::from("id".to_string());
205 let session = SessionId::from("id".to_string());
206
207 takes_user_id(&user);
208 takes_session_id(&session);
209 }
212}