1use crate::error::RepoError;
8
9const TID_CHARSET: &[u8; 32] = b"234567abcdefghijklmnopqrstuvwxyz";
11
12const TID_LEN: usize = 13;
14
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub struct Tid(String);
23
24impl Tid {
25 pub fn now() -> Self {
30 use rand::Rng;
31 use std::time::{SystemTime, UNIX_EPOCH};
32
33 let micros = SystemTime::now()
34 .duration_since(UNIX_EPOCH)
35 .expect("system clock before Unix epoch")
36 .as_micros() as u64;
37
38 let clock_id: u64 = rand::thread_rng().gen_range(0..1024);
39
40 let tid_value = (micros << 10) | clock_id;
42
43 Self(encode_base32_sortable(tid_value))
44 }
45
46 pub fn parse(s: &str) -> Result<Self, RepoError> {
55 if s.len() != TID_LEN {
56 return Err(RepoError::InvalidTid(format!(
57 "TID must be {TID_LEN} characters, got {}",
58 s.len()
59 )));
60 }
61
62 for (i, ch) in s.chars().enumerate() {
63 if !TID_CHARSET.contains(&(ch as u8)) {
64 return Err(RepoError::InvalidTid(format!(
65 "invalid character '{}' at position {} in TID",
66 ch, i
67 )));
68 }
69 }
70
71 Ok(Self(s.to_string()))
72 }
73
74 pub fn as_str(&self) -> &str {
76 &self.0
77 }
78
79 pub fn into_string(self) -> String {
81 self.0
82 }
83}
84
85impl std::fmt::Display for Tid {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.write_str(&self.0)
88 }
89}
90
91impl serde::Serialize for Tid {
92 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
93 serializer.serialize_str(&self.0)
94 }
95}
96
97impl<'de> serde::Deserialize<'de> for Tid {
98 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
99 let s = String::deserialize(deserializer)?;
100 Tid::parse(&s).map_err(serde::de::Error::custom)
101 }
102}
103
104fn encode_base32_sortable(mut value: u64) -> String {
106 let mut buf = [b'2'; TID_LEN]; for i in (0..TID_LEN).rev() {
109 let idx = (value & 0x1F) as usize;
110 buf[i] = TID_CHARSET[idx];
111 value >>= 5;
112 }
113
114 String::from_utf8(buf.to_vec()).expect("TID charset is valid UTF-8")
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn now_produces_13_chars() {
124 let tid = Tid::now();
125 assert_eq!(tid.as_str().len(), 13);
126 }
127
128 #[test]
129 fn now_uses_valid_charset() {
130 let tid = Tid::now();
131 for ch in tid.as_str().chars() {
132 assert!(
133 TID_CHARSET.contains(&(ch as u8)),
134 "unexpected character '{ch}' in TID"
135 );
136 }
137 }
138
139 #[test]
140 fn now_is_monotonically_increasing() {
141 let a = Tid::now();
142 std::thread::sleep(std::time::Duration::from_millis(2));
144 let b = Tid::now();
145 assert!(b.as_str() >= a.as_str(), "TIDs should be sortable by time");
146 }
147
148 #[test]
149 fn parse_valid_tid() {
150 let tid = Tid::parse("2222222222222").unwrap();
152 assert_eq!(tid.as_str(), "2222222222222");
153 }
154
155 #[test]
156 fn parse_rejects_too_short() {
157 let result = Tid::parse("222222");
158 assert!(result.is_err());
159 match result.unwrap_err() {
160 RepoError::InvalidTid(msg) => assert!(msg.contains("13 characters")),
161 other => panic!("expected InvalidTid, got: {other}"),
162 }
163 }
164
165 #[test]
166 fn parse_rejects_too_long() {
167 let result = Tid::parse("22222222222222");
168 assert!(result.is_err());
169 }
170
171 #[test]
172 fn parse_rejects_invalid_chars() {
173 let result = Tid::parse("222222222222A");
174 assert!(result.is_err());
175 match result.unwrap_err() {
176 RepoError::InvalidTid(msg) => assert!(msg.contains("invalid character")),
177 other => panic!("expected InvalidTid, got: {other}"),
178 }
179 }
180
181 #[test]
182 fn parse_rejects_uppercase() {
183 let result = Tid::parse("222222222222Z");
184 assert!(result.is_err());
185 }
186
187 #[test]
188 fn parse_rejects_0_and_1() {
189 assert!(Tid::parse("0222222222222").is_err());
191 assert!(Tid::parse("1222222222222").is_err());
192 }
193
194 #[test]
195 fn display_matches_as_str() {
196 let tid = Tid::now();
197 assert_eq!(format!("{tid}"), tid.as_str());
198 }
199
200 #[test]
201 fn roundtrip_through_generated() {
202 let tid = Tid::now();
203 let parsed = Tid::parse(tid.as_str()).unwrap();
204 assert_eq!(tid, parsed);
205 }
206
207 #[test]
208 fn encode_zero_value() {
209 let encoded = encode_base32_sortable(0);
210 assert_eq!(encoded, "2222222222222");
211 }
212
213 #[test]
214 fn test_into_string() {
215 let tid = Tid::now();
216 let s = tid.clone().into_string();
217 assert_eq!(s.len(), 13);
218 assert_eq!(s, tid.as_str());
219 }
220
221 #[test]
222 fn test_serde_roundtrip() {
223 let tid = Tid::now();
224 let json = serde_json::to_string(&tid).unwrap();
225 let deserialized: Tid = serde_json::from_str(&json).unwrap();
226 assert_eq!(deserialized.as_str(), tid.as_str());
227 }
228
229 #[test]
230 fn test_deserialize_invalid_tid_wrong_length() {
231 let result = serde_json::from_str::<Tid>(r#""abc""#);
232 assert!(result.is_err());
233 }
234
235 #[test]
236 fn test_deserialize_invalid_tid_bad_chars() {
237 let result = serde_json::from_str::<Tid>(r#""AAAAAAAAAAAAA""#);
239 assert!(result.is_err());
240 }
241
242 #[test]
243 fn test_deserialize_valid_tid() {
244 let tid: Tid = serde_json::from_str(r#""2222222222222""#).unwrap();
245 assert_eq!(tid.as_str(), "2222222222222");
246 }
247}