diesel_ulid/
lib.rs

1#![forbid(unsafe_code)]
2
3use bytes::BufMut;
4#[cfg(feature = "diesel")]
5use diesel::expression::AsExpression;
6#[cfg(feature = "diesel")]
7use diesel::serialize::Output;
8#[cfg(feature = "diesel")]
9use diesel::{
10    deserialize::{self, FromSql},
11    pg::{Pg, PgValue},
12    serialize::{self, IsNull, ToSql},
13    sql_types::Uuid as DieselUUID,
14    FromSqlRow,
15};
16#[cfg(feature = "postgres")]
17use postgres_types::private::BytesMut;
18#[cfg(feature = "postgres")]
19use postgres_types::{accepts, FromSql as PgFromSql, ToSql as PgToSql, Type};
20use rusty_ulid::{DecodingError, Ulid};
21use serde::de::{self, Unexpected, Visitor};
22use serde::Serialize;
23use serde::{Deserialize, Deserializer};
24use std::error::Error;
25use std::fmt::{self, Display};
26#[cfg(feature = "diesel")]
27use std::io::Write;
28use std::{fmt::Debug, ops::Deref, str::FromStr};
29use uuid::Uuid;
30
31#[cfg(feature = "diesel")]
32#[derive(
33    Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Hash, AsExpression, FromSqlRow,
34)]
35#[diesel(sql_type = DieselUUID)]
36pub struct DieselUlid(rusty_ulid::Ulid);
37
38#[cfg(feature = "postgres")]
39#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Hash)]
40pub struct DieselUlid(rusty_ulid::Ulid);
41
42impl DieselUlid {
43    pub fn generate() -> Self {
44        DieselUlid(Ulid::generate())
45    }
46
47    pub fn from_timestamp_millis(timestamp: u64) -> Result<Self, Box<dyn Error + Sync + Send>> {
48        if (timestamp & 0xFFFF_0000_0000_0000) != 0 {
49            return Err(Box::new(std::io::Error::new(
50                std::io::ErrorKind::InvalidInput,
51                "ULID does not support timestamps after +10889-08-02T05:31:50.655Z",
52            )));
53        }
54        Ok(DieselUlid::from(Ulid::from_timestamp_with_rng(
55            timestamp,
56            &mut rand::thread_rng(),
57        )))
58    }
59
60    pub fn as_byte_array(&self) -> [u8; 16] {
61        <[u8; 16]>::from(self.0)
62    }
63}
64
65#[cfg(feature = "postgres")]
66impl<'a> PgFromSql<'a> for DieselUlid {
67    fn from_sql(_: &Type, raw: &[u8]) -> Result<DieselUlid, Box<dyn Error + Sync + Send>> {
68        Ok(DieselUlid::try_from(raw)?)
69    }
70    accepts!(UUID);
71}
72
73impl<'de> Deserialize<'de> for DieselUlid {
74    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
75    where
76        D: Deserializer<'de>,
77    {
78        struct DieselUlidVisitor;
79
80        impl<'de> Visitor<'de> for DieselUlidVisitor {
81            type Value = DieselUlid;
82
83            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
84                write!(
85                    formatter,
86                    "a string or bytes containing a diesel_ulid as uuid or ulid"
87                )
88            }
89
90            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
91            where
92                E: de::Error,
93            {
94                match DieselUlid::from_str(s) {
95                    Ok(v) => Ok(v),
96                    Err(_) => match Uuid::from_str(s) {
97                        Ok(v) => Ok(DieselUlid::from(v)),
98                        Err(e) => Err(de::Error::invalid_value(
99                            Unexpected::Str(s),
100                            &e.to_string().as_str(),
101                        )),
102                    },
103                }
104            }
105
106            fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
107            where
108                E: de::Error,
109            {
110                self.visit_str(&v)
111            }
112
113            fn visit_u128<E>(self, v: u128) -> Result<Self::Value, E>
114            where
115                E: de::Error,
116            {
117                self.visit_bytes(&v.to_be_bytes())
118            }
119
120            fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
121            where
122                E: de::Error,
123            {
124                if v.len() == 16 {
125                    return match DieselUlid::try_from(v) {
126                        Ok(v) => Ok(v),
127                        Err(e) => Err(de::Error::invalid_value(
128                            Unexpected::Bytes(v),
129                            &e.to_string().as_str(),
130                        )),
131                    };
132                } else {
133                    self.visit_str(std::str::from_utf8(v).map_err(|e| {
134                        de::Error::invalid_value(Unexpected::Bytes(v), &e.to_string().as_str())
135                    })?)
136                }
137            }
138        }
139        deserializer.deserialize_bytes(DieselUlidVisitor)
140    }
141}
142
143#[cfg(feature = "postgres")]
144impl PgToSql for DieselUlid {
145    fn to_sql(
146        &self,
147        _: &Type,
148        w: &mut BytesMut,
149    ) -> Result<postgres_types::IsNull, Box<dyn Error + Sync + Send>> {
150        w.put_slice(&self.as_byte_array());
151        Ok(postgres_types::IsNull::No)
152    }
153    accepts!(UUID);
154    postgres_types::to_sql_checked!();
155}
156
157impl Default for DieselUlid {
158    fn default() -> Self {
159        DieselUlid(rusty_ulid::Ulid::from(0_u128))
160    }
161}
162
163impl TryFrom<&[u8]> for DieselUlid {
164    type Error = DecodingError;
165
166    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
167        Ok(DieselUlid(rusty_ulid::Ulid::try_from(value)?))
168    }
169}
170
171impl From<&[u8; 16]> for DieselUlid {
172    fn from(value: &[u8; 16]) -> Self {
173        DieselUlid(rusty_ulid::Ulid::from(*value))
174    }
175}
176
177impl From<[u8; 16]> for DieselUlid {
178    fn from(value: [u8; 16]) -> Self {
179        DieselUlid(rusty_ulid::Ulid::from(value))
180    }
181}
182
183impl Debug for DieselUlid {
184    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185        write!(f, "{}", &self)
186    }
187}
188
189impl Display for DieselUlid {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        write!(f, "{}", &self.0.to_string())
192    }
193}
194
195impl Deref for DieselUlid {
196    type Target = rusty_ulid::Ulid;
197
198    fn deref(&self) -> &Self::Target {
199        &self.0
200    }
201}
202
203impl FromStr for DieselUlid {
204    type Err = DecodingError;
205
206    fn from_str(s: &str) -> Result<Self, Self::Err> {
207        Ok(Self(Ulid::from_str(s)?))
208    }
209}
210
211impl From<rusty_ulid::Ulid> for DieselUlid {
212    fn from(value: rusty_ulid::Ulid) -> Self {
213        Self(value)
214    }
215}
216
217impl From<DieselUlid> for rusty_ulid::Ulid {
218    fn from(value: DieselUlid) -> Self {
219        value.0
220    }
221}
222
223#[cfg(feature = "diesel")]
224impl FromSql<DieselUUID, Pg> for DieselUlid {
225    fn from_sql(value: PgValue<'_>) -> deserialize::Result<Self> {
226        DieselUlid::try_from(value.as_bytes()).map_err(Into::into)
227    }
228}
229
230#[cfg(feature = "diesel")]
231impl ToSql<DieselUUID, Pg> for DieselUlid {
232    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
233        out.write_all(&self.as_byte_array())
234            .map(|_| IsNull::No)
235            .map_err(Into::into)
236    }
237}
238
239// UUID conversions
240impl From<uuid::Uuid> for DieselUlid {
241    fn from(value: uuid::Uuid) -> Self {
242        DieselUlid::from(value.as_bytes())
243    }
244}
245
246impl From<DieselUlid> for uuid::Uuid {
247    fn from(value: DieselUlid) -> Self {
248        uuid::Uuid::from_bytes(value.as_byte_array())
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use std::{collections::HashMap, str::FromStr};
255
256    #[cfg(feature = "diesel")]
257    use diesel::{
258        deserialize::FromSql,
259        pg::{Pg, PgValue, TypeOidLookup},
260        sql_types::Uuid as DieselUUID,
261    };
262    #[cfg(feature = "diesel")]
263    use std::num::NonZeroU32;
264
265    use crate::DieselUlid;
266
267    #[test]
268    fn conversions() {
269        // String
270        let ulid = DieselUlid::from_str("01ARZ3NDEKTSV4RRFFQ69G5FAV").unwrap();
271        assert_eq!(ulid.to_string(), "01ARZ3NDEKTSV4RRFFQ69G5FAV".to_string());
272
273        // Original
274        let orig_ulid = rusty_ulid::Ulid::from(ulid);
275        assert_eq!(
276            orig_ulid.to_string(),
277            "01ARZ3NDEKTSV4RRFFQ69G5FAV".to_string()
278        );
279
280        // Back
281        let into_before = DieselUlid::from(orig_ulid);
282        assert_eq!(ulid, into_before);
283
284        // Bytes
285        let bytes = [
286            0xFF_u8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
287            0x31, 0x32,
288        ];
289
290        let bytes_ulid = DieselUlid::try_from(bytes.as_slice()).unwrap();
291
292        assert_eq!("7ZZZZZZZZZZZZP2RK3CHJPCC9J", &bytes_ulid.to_string());
293
294        let direct_conv = DieselUlid::from(bytes);
295        assert_eq!(bytes_ulid, direct_conv);
296
297        let from_str = DieselUlid::from_str("7ZZZZZZZZZZZZP2RK3CHJPCC9J").unwrap();
298        assert_eq!(bytes, from_str.as_byte_array())
299    }
300
301    #[test]
302    fn conversions_uuid() {
303        let orig_string = "67e55044-10b1-426f-9247-bb680e5fe0c8";
304
305        let from_str = uuid::Uuid::from_str(orig_string).unwrap();
306
307        let as_ulid = DieselUlid::from(from_str);
308
309        let back_to_uuid = uuid::Uuid::from(as_ulid);
310
311        assert_eq!(orig_string, back_to_uuid.to_string().as_str())
312    }
313
314    #[test]
315    fn conversions_uuid_test_serde() {
316        let orig_string = r#"{"test" : "67e55044-10b1-426f-9247-bb680e5fe0c8"}"#;
317
318        let from_json: HashMap<String, DieselUlid> = serde_json::from_str(orig_string).unwrap();
319
320        assert_eq!(
321            from_json.get("test").unwrap().to_string().as_str(),
322            "37WN84845H89QS4HXVD075ZR68"
323        )
324    }
325
326    #[test]
327    fn conversions_uuid_test_serde_bin() {
328        let ulid = DieselUlid::generate();
329
330        let serialized = bincode::serialize(&ulid).unwrap();
331
332        let deserialized: DieselUlid = bincode::deserialize(&serialized).unwrap();
333
334        assert_eq!(ulid, deserialized)
335    }
336
337    #[test]
338    fn test_default() {
339        let ulid = DieselUlid::default();
340        let ulid_2 = DieselUlid::from(rusty_ulid::Ulid::from(
341            0x0000_0000_0000_0000_0000_0000_0000_0000,
342        ));
343
344        assert_eq!(ulid, ulid_2)
345    }
346
347    #[test]
348    fn test_generate() {
349        use chrono::Utc;
350        let ulid = DieselUlid::generate();
351        // Should be the same millisecond
352        assert!(ulid.datetime().timestamp_millis() - Utc::now().timestamp_millis() < 5)
353    }
354
355    #[test]
356    fn test_debug_display() {
357        let ulid = DieselUlid::generate();
358        // Should be the same millisecond
359        assert_eq!(format!("{ulid}"), format!("{:?}", ulid))
360    }
361
362    #[test]
363    fn test_format() {
364        let ulid = DieselUlid::from_str("7ZZZZZZZZZZZZP2RK3CHJPCC9J").unwrap();
365
366        assert_eq!(format!("{:?}", ulid), format!("7ZZZZZZZZZZZZP2RK3CHJPCC9J"))
367    }
368
369    #[test]
370    fn test_from_timestamp() {
371        let invalid_timestamp: u64 = u64::MAX;
372        let ulid = DieselUlid::from_timestamp_millis(invalid_timestamp);
373        assert!(ulid.is_err());
374
375        let timestamp: u64 = 1720507731354;
376        let ulid = DieselUlid::from_timestamp_millis(timestamp).unwrap();
377        assert_eq!(ulid.timestamp(), 1720507731354);
378    }
379
380    // Can not test to_sql because diesel does not export Output::test()
381    // #[test]
382    // fn uuid_to_sql() {
383    //     let mut buffer = Vec::new();
384    //     let bytes = [
385    //         0xFF_u8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
386    //         0x31, 0x32,
387    //     ];
388    //     let test_uuid = DieselUlid::try_from(bytes.as_slice()).unwrap();
389    //     let mut bytes = Output::test(&mut buffer);
390    //     ToSql::<Uuid, Pg>::to_sql(&test_uuid, &mut bytes).unwrap();
391    //     assert_eq!(&buffer, test_uuid.as_byte_array().as_slice());
392    // }
393
394    #[test]
395    #[cfg(feature = "diesel")]
396    fn some_uuid_from_sql() {
397        let bytes = [
398            0xFF_u8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
399            0x31, 0x32,
400        ];
401        let input_uuid = DieselUlid::try_from(bytes.as_slice()).unwrap();
402        let output_uuid = FromSql::<DieselUUID, Pg>::from_sql(PgValue::new(
403            input_uuid.as_byte_array().as_slice(),
404            &NonZeroU32::new(5).unwrap() as &dyn TypeOidLookup,
405        ))
406        .unwrap();
407        assert_eq!(input_uuid, output_uuid);
408    }
409
410    #[test]
411    #[cfg(feature = "diesel")]
412    fn bad_uuid_from_sql() {
413        let uuid = DieselUlid::from_sql(PgValue::new(
414            b"boom",
415            &NonZeroU32::new(5).unwrap() as &dyn TypeOidLookup,
416        ));
417        assert!(uuid.is_err());
418        // The error message changes slightly between different
419        // uuid versions, so we just check on the relevant parts
420        // The exact error messages are either:
421        // "invalid bytes length: expected 16, found 4"
422        // or
423        // "invalid length: expected 16 bytes, found 4"
424        let error_message = uuid.unwrap_err().to_string();
425        assert!(error_message.starts_with("invalid"));
426        assert!(error_message.contains("length"));
427    }
428
429    #[test]
430    #[cfg(feature = "diesel")]
431    fn no_uuid_from_sql() {
432        let uuid = DieselUlid::from_nullable_sql(None);
433        assert_eq!(
434            uuid.unwrap_err().to_string(),
435            "Unexpected null for non-null column"
436        );
437    }
438}