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
239impl 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 let ulid = DieselUlid::from_str("01ARZ3NDEKTSV4RRFFQ69G5FAV").unwrap();
271 assert_eq!(ulid.to_string(), "01ARZ3NDEKTSV4RRFFQ69G5FAV".to_string());
272
273 let orig_ulid = rusty_ulid::Ulid::from(ulid);
275 assert_eq!(
276 orig_ulid.to_string(),
277 "01ARZ3NDEKTSV4RRFFQ69G5FAV".to_string()
278 );
279
280 let into_before = DieselUlid::from(orig_ulid);
282 assert_eq!(ulid, into_before);
283
284 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 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 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 #[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 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}