sid/
lib.rs

1use std::cmp::Ordering;
2use std::fmt::{Debug, Display, Formatter};
3use std::str::FromStr;
4
5pub use label::Label;
6use sid_encode::{base32_encode, base32_decode, SHORT_LENGTH};
7pub use sid_encode::DecodeError;
8
9mod label;
10mod monotonic;
11#[cfg(feature = "sqlx")]
12mod sqlx;
13#[cfg(feature = "serde")]
14mod serde;
15
16pub use monotonic::MonotonicGenerator;
17
18#[cfg(target_arch = "wasm32")]
19fn unix_epoch_millis() -> u64 {
20    js_sys::Date::now() as u64
21}
22
23#[cfg(not(target_arch = "wasm32"))]
24fn unix_epoch_millis() -> u64 {
25    use std::time::SystemTime;
26    SystemTime::now()
27        .duration_since(SystemTime::UNIX_EPOCH)
28        .unwrap()
29        .as_millis() as u64
30}
31
32#[derive(Copy, Clone)]
33pub struct NoLabel;
34
35impl NoLabel {
36    pub fn sid() -> Sid {
37        Sid::<NoLabel>::new()
38    }
39}
40
41pub struct Sid<T = NoLabel> {
42    data: [u8; 16],
43    marker: std::marker::PhantomData<T>,
44}
45
46impl<T: Label> std::hash::Hash for Sid<T> {
47    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
48        self.data.hash(state)
49    }
50}
51
52impl<T> PartialEq<Self> for Sid<T> {
53    fn eq(&self, other: &Self) -> bool {
54        self.data == other.data
55    }
56}
57
58impl<T> PartialOrd<Self> for Sid<T> {
59    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
60        Some(self.cmp(other))
61    }
62}
63
64impl<T> Eq for Sid<T> {}
65
66impl<T> Ord for Sid<T> {
67    fn cmp(&self, other: &Self) -> Ordering {
68        self.data.cmp(&other.data)
69    }
70}
71
72impl<T> Clone for Sid<T> {
73    fn clone(&self) -> Self {
74        Self {
75            data: self.data,
76            marker: Default::default(),
77        }
78    }
79}
80
81impl<T> Copy for Sid<T> {}
82
83pub fn sid<T: Label>() -> Sid<T> {
84    Sid::<T>::new()
85}
86
87impl<T> Sid<T> {
88    #[cfg(feature = "uuid")]
89    pub fn uuid(&self) -> uuid::Uuid {
90        uuid::Uuid::from_bytes(self.data)
91    }
92}
93
94impl<T: Label> Sid<T> {
95    pub fn null() -> Self {
96        Self {
97            data: [0; 16],
98            marker: Default::default(),
99        }
100    }
101
102    pub fn new() -> Self {
103        Self::from_timestamp_with_rng(unix_epoch_millis(), &mut rand::thread_rng())
104    }
105
106    #[cfg(feature = "rand")]
107    pub fn from_timestamp_with_rng<R>(timestamp: u64, rng: &mut R) -> Self
108        where
109            R: rand::Rng,
110    {
111        if (timestamp >> 48) != 0 {
112            panic!("sid does not support timestamps after +10889-08-02T05:31:50.655Z");
113        }
114        let rand_high = rng.gen::<u32>() as u64 & ((1 << 16) - 1);
115        let high = timestamp << 16 | rand_high;
116        let low = rng.gen::<u64>();
117        let high = high.to_be_bytes();
118        let low = low.to_be_bytes();
119
120        let mut data: [u8; 16] = [0; 16];
121        data[..8].copy_from_slice(&high);
122        data[8..].copy_from_slice(&low);
123
124        Self {
125            data,
126            marker: Default::default(),
127        }
128    }
129
130    /// Only the short suffix of the sid, with label, e.g. usr_t40x
131    pub fn short(&self) -> String {
132        let encoded = base32_encode(self.data);
133        let label = T::label();
134        let separator = if label.is_empty() { "" } else { "_" };
135        format!("{}{}{}", label, separator, &encoded[SHORT_LENGTH + 1..])
136    }
137
138    pub fn is_null(&self) -> bool {
139        self.data.iter().all(|&b| b == 0)
140    }
141
142    pub fn data(&self) -> &[u8; 16] {
143        &self.data
144    }
145
146    pub fn timestamp(&self) -> u64 {
147        u64::from_be_bytes(self.data[0..8].try_into().unwrap())
148    }
149
150    // small difference compared to ULID. Rather than erroring if we overflow the random buffer
151    // we just increment the ms stamp.
152    pub(crate) fn increment(&self) -> Self {
153        let mut data = self.data;
154        let mut i = 15;
155        loop {
156            let (value, overflow) = data[i].overflowing_add(1);
157            data[i] = value;
158            if !overflow {
159                break;
160            }
161            if i == 0 {
162                panic!("sid overflow");
163            }
164            i -= 1;
165        }
166        Self {
167            data,
168            marker: Default::default(),
169        }
170    }
171}
172
173#[cfg(feature = "uuid")]
174impl<T> Into<uuid::Uuid> for Sid<T> {
175    fn into(self) -> uuid::Uuid {
176        uuid::Uuid::from_bytes(self.data)
177    }
178}
179
180#[cfg(feature = "uuid")]
181impl<T> From<uuid::Uuid> for Sid<T> {
182    fn from(value: uuid::Uuid) -> Self {
183        let bytes = value.as_ref();
184        let mut data: [u8; 16] = [0; 16];
185        data.copy_from_slice(bytes);
186        Self {
187            data,
188            marker: Default::default(),
189        }
190    }
191}
192
193impl<T> Sid<T> {
194    pub fn unlabel(self) -> Sid<NoLabel> {
195        Sid {
196            data: self.data,
197            marker: Default::default(),
198        }
199    }
200}
201
202impl Sid<NoLabel> {
203    pub fn into_labeled<U>(self) -> Sid<U> {
204        Sid {
205            data: self.data,
206            marker: Default::default(),
207        }
208    }
209}
210
211impl<T> From<[u8; 16]> for Sid<T> {
212    fn from(data: [u8; 16]) -> Self {
213        Self {
214            data,
215            marker: Default::default(),
216        }
217    }
218}
219
220impl<T> FromStr for Sid<T> {
221    type Err = DecodeError;
222
223    fn from_str(s: &str) -> Result<Self, Self::Err> {
224        let data = base32_decode(s)?;
225        Ok(Sid::<T>::from(data))
226    }
227}
228
229impl<T: Label> Debug for Sid<T> {
230    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
231        let encoded = base32_encode(self.data);
232        let label = T::label();
233        let sep = if label.is_empty() { "" } else { "_" };
234        f.write_str(label)?;
235        f.write_str(sep)?;
236        f.write_str(&encoded)
237    }
238}
239
240impl<T> Display for Sid<T> {
241    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
242        let encoded = base32_encode(self.data);
243        f.write_str(&encoded)
244    }
245}
246
247#[macro_export]
248macro_rules! sid {
249    ($value:ident) => {
250        sid!(stringify!($value))
251    };
252    ($value:expr) => {{
253        let value = $value;
254        let count = value.matches('_').count();
255        match count {
256            1 => Sid::from_str(value).unwrap(),
257            2 => {
258                let value = value.splitn(2, '_').nth(1).unwrap();
259                Sid::from_str(value).unwrap()
260            },
261            _ => panic!("sid must have 1 or 2 underscores."),
262        }
263    }};
264}
265
266
267#[cfg(test)]
268mod tests {
269    use label::Label;
270
271    use super::*;
272
273    label!(Team, "team");
274
275    #[test]
276    fn test_struct_sid_can_reference_itself() {
277        struct Team {
278            id: Sid<Self>,
279        }
280
281        impl Label for Team {
282            fn label() -> &'static str {
283                "tea_"
284            }
285        }
286
287        let f = Team { id: sid() };
288        let s = format!("{:?}", f.id);
289        assert!(s.starts_with("tea_"));
290    }
291
292    #[test]
293    fn it_works() {
294        let bytes = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
295        let sid = Sid::<Team>::from(bytes);
296        println!("{}", sid.short());
297        println!("{}", sid);
298        assert_eq!(sid.to_string(), "01081g81860w40j2gb1g6g_w3rg");
299        assert_eq!(sid.short(), "team_w3rg");
300    }
301
302    #[test]
303    fn test_null() {
304        let sid = Sid::<Team>::null();
305        println!("{}", sid.short());
306        println!("{}", sid);
307        assert_eq!(sid.to_string(), "0000000000000000000000_0000");
308        assert_eq!(sid.short(), "team_0000");
309        let sid = Sid::<NoLabel>::null();
310        assert_eq!(sid.to_string(), "0000000000000000000000_0000");
311    }
312
313    #[test]
314    #[cfg(feature = "uuid")]
315    fn test_uuid() {
316        let bytes = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
317        let sid = Sid::<Team>::from(bytes);
318        let uuid: uuid::Uuid = sid.clone().into();
319        assert_eq!(uuid.to_string(), "01020304-0506-0708-090a-0b0c0d0e0f10");
320        let uuid2 = sid.uuid();
321        assert_eq!(uuid, uuid2);
322    }
323
324    #[test]
325    fn test_macro() {
326        let sid: Sid<Team> = sid!("team_0000000000000000000000_0000");
327        assert!(sid.is_null(), "{}", sid);
328        let sid: Sid<Team> = sid!(team_0000000000000000000000_0000);
329        assert!(sid.is_null(), "{}", sid);
330        let sid: Sid<Team> = sid!(team_0da0fa0e02cssbhkanf04c_srb0);
331        assert_eq!(sid.to_string(), "0da0fa0e02cssbhkanf04c_srb0");
332    }
333
334    #[test]
335    fn test_size() {
336        assert_eq!(std::mem::size_of::<Sid<Team>>(), 16);
337    }
338
339    #[test]
340    fn test_sort() {
341        let ts = unix_epoch_millis();
342        let ts2 = ts + 1;
343        let ts3 = ts + 2;
344        let rng = &mut rand::thread_rng();
345        let sid1 = Sid::<NoLabel>::from_timestamp_with_rng(ts, rng);
346        let sid2 = Sid::from_timestamp_with_rng(ts2, rng);
347        let sid3 = Sid::from_timestamp_with_rng(ts3, rng);
348        let mut sids = vec![sid3.clone(), sid1.clone(), sid2.clone()];
349        sids.sort();
350        assert_eq!(sids, vec![sid1, sid2, sid3]);
351    }
352}