misskey_api/model/id/
aid.rs

1use std::convert::TryFrom;
2use std::fmt::{self, Display};
3use std::str::FromStr;
4
5use chrono::{DateTime, TimeZone, Utc};
6use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
7use thiserror::Error;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct Aid {
11    pub timestamp: i64,
12    /// 0 - 1295
13    pub random: u16,
14}
15
16impl Aid {
17    pub fn datetime(&self) -> DateTime<Utc> {
18        Utc.timestamp_millis(self.timestamp)
19    }
20}
21
22// https://github.com/syuilo/misskey/blob/develop/src/misc/id/aid.ts#L6
23const TIME2000: i64 = 946684800000;
24
25#[derive(Debug, Error, Clone)]
26#[error("invalid aid")]
27pub struct ParseAidError {
28    _priv: (),
29}
30
31impl FromStr for Aid {
32    type Err = ParseAidError;
33
34    fn from_str(s: &str) -> Result<Aid, Self::Err> {
35        let (timestamp_str, random_str) = s.split_at(s.len() - 2);
36
37        let timestamp = match i64::from_str_radix(timestamp_str, 36) {
38            Ok(x) => x + TIME2000,
39            Err(_) => return Err(ParseAidError { _priv: () }),
40        };
41
42        let random = match u16::from_str_radix(random_str, 36) {
43            Ok(x) => x,
44            Err(_) => return Err(ParseAidError { _priv: () }),
45        };
46
47        Ok(Aid { timestamp, random })
48    }
49}
50
51struct Radix36(u64);
52
53impl Radix36 {
54    fn new(x: impl Into<u64>) -> Radix36 {
55        Radix36(x.into())
56    }
57}
58
59impl Display for Radix36 {
60    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
61        use std::convert::TryInto;
62        use std::fmt::Write;
63
64        let width = f
65            .width()
66            .unwrap_or_else(|| (self.0 as f64).log(36.0).floor() as usize + 1);
67
68        (0..width)
69            .rev()
70            .map(|i| self.0 / 36_u64.pow(i.try_into().unwrap()) % 36)
71            .map(|d| std::char::from_digit(d.try_into().unwrap(), 36).unwrap())
72            .map(|c| f.write_char(c))
73            .collect()
74    }
75}
76
77impl Display for Aid {
78    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
79        let since_2000 = u64::try_from(self.timestamp - TIME2000).unwrap_or(0);
80        let timestamp_fmt = Radix36::new(since_2000);
81        let random_fmt = Radix36::new(self.random % 1296);
82        write!(f, "{:08}{:02}", timestamp_fmt, random_fmt)
83    }
84}
85
86impl Serialize for Aid {
87    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
88    where
89        S: Serializer,
90    {
91        serializer.collect_str(self)
92    }
93}
94
95impl<'de> Deserialize<'de> for Aid {
96    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
97    where
98        D: Deserializer<'de>,
99    {
100        String::deserialize(deserializer)?
101            .parse()
102            .map_err(de::Error::custom)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::Aid;
109    use chrono::{DateTime, Duration, TimeZone, Utc};
110    use rand::{self, Rng};
111
112    fn new() -> Aid {
113        from_datetime(Utc::now())
114    }
115
116    fn from_datetime<Tz>(datetime: DateTime<Tz>) -> Aid
117    where
118        Tz: TimeZone,
119    {
120        from_datetime_with_source(datetime, &mut rand::thread_rng())
121    }
122
123    fn from_datetime_with_source<Tz, R>(datetime: DateTime<Tz>, source: &mut R) -> Aid
124    where
125        Tz: TimeZone,
126        R: Rng,
127    {
128        let timestamp = datetime.timestamp_millis();
129        let random = source.gen::<u16>() % 1296;
130        Aid { timestamp, random }
131    }
132
133    #[test]
134    fn test_deserialize_const() {
135        let string = "8dhemt9ubf";
136        let aid: Aid = string.parse().expect("failed to parse");
137        assert_eq!(aid.datetime(), Utc.timestamp_millis(1602948787122));
138    }
139
140    #[test]
141    fn test_serialize_deserialize() {
142        let aid1 = new();
143        let string = aid1.to_string();
144        let aid2: Aid = string.parse().expect("failed to parse");
145        assert_eq!(aid1, aid2);
146    }
147
148    #[test]
149    fn test_deserialize_serialize() {
150        let string1 = "8dhe5zqidm";
151        let aid: Aid = string1.parse().expect("failed to parse");
152        let string2 = aid.to_string();
153        assert_eq!(string1, string2);
154    }
155
156    #[test]
157    fn test_deserialize_serialize2() {
158        let string1 = "8ejiidh50m";
159        let aid: Aid = string1.parse().expect("failed to parse");
160        let string2 = aid.to_string();
161        assert_eq!(string1, string2);
162    }
163
164    #[test]
165    fn test_order() {
166        let time = Utc::now();
167        let aid1 = from_datetime(time);
168        let aid2 = from_datetime(time + Duration::milliseconds(1));
169        assert!(aid1 < aid2);
170    }
171}