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)]
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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, From, Into)]
69#[serde(transparent)]
70pub struct UserId(pub String);
71
72#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, From, Into)]
77#[serde(transparent)]
78pub struct SessionId(pub String);
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, From, Into)]
82#[serde(transparent)]
83pub struct KeyId(pub String);
84
85#[cfg(test)]
86#[allow(clippy::unwrap_used)]
87mod tests {
88 use super::*;
89 use static_assertions::assert_impl_all;
90
91 assert_impl_all!(PpnumId: Send, Sync, Copy);
92 assert_impl_all!(Ppnum: Send, Sync);
93 assert_impl_all!(UserId: Send, Sync);
94 assert_impl_all!(SessionId: Send, Sync);
95 assert_impl_all!(KeyId: Send, Sync);
96
97 #[test]
98 fn valid_ppnum() {
99 assert!("77712345678".parse::<Ppnum>().is_ok());
100 assert!("77700000000".parse::<Ppnum>().is_ok());
101 assert!("77799999999".parse::<Ppnum>().is_ok());
102 }
103
104 #[test]
105 fn invalid_ppnum_wrong_prefix() {
106 assert!("12345678901".parse::<Ppnum>().is_err());
107 assert!("77812345678".parse::<Ppnum>().is_err());
108 }
109
110 #[test]
111 fn invalid_ppnum_wrong_length() {
112 assert!("7771234567".parse::<Ppnum>().is_err());
113 assert!("777123456789".parse::<Ppnum>().is_err());
114 assert!("".parse::<Ppnum>().is_err());
115 }
116
117 #[test]
118 fn invalid_ppnum_non_digits() {
119 assert!("777abcdefgh".parse::<Ppnum>().is_err());
120 assert!("7771234567a".parse::<Ppnum>().is_err());
121 }
122
123 #[test]
124 fn ppnum_serde_roundtrip() {
125 let ppnum: Ppnum = "77712345678".parse().unwrap();
126 let json = serde_json::to_string(&ppnum).unwrap();
127 assert_eq!(json, "\"77712345678\"");
128 let parsed: Ppnum = serde_json::from_str(&json).unwrap();
129 assert_eq!(parsed, ppnum);
130 }
131
132 #[test]
133 fn ppnum_id_serde_roundtrip() {
134 let id = PpnumId(Ulid::nil());
135 let json = serde_json::to_string(&id).unwrap();
136 let parsed: PpnumId = serde_json::from_str(&json).unwrap();
137 assert_eq!(parsed, id);
138 }
139
140 #[test]
141 fn user_id_from_string() {
142 let id = UserId::from("user-123".to_string());
143 assert_eq!(id.to_string(), "user-123");
144 }
145
146 #[test]
147 fn session_id_from_string() {
148 let id = SessionId::from("sess-abc".to_string());
149 assert_eq!(id.to_string(), "sess-abc");
150 }
151
152 #[test]
153 fn newtypes_prevent_mixing() {
154 fn takes_user_id(_: &UserId) {}
155 fn takes_session_id(_: &SessionId) {}
156
157 let user = UserId::from("id".to_string());
158 let session = SessionId::from("id".to_string());
159
160 takes_user_id(&user);
161 takes_session_id(&session);
162 }
165}