scufflecloud_id/
lib.rs

1use std::fmt::{Debug, Display};
2use std::hash::Hash;
3use std::io::Write;
4use std::ops::Deref;
5use std::str::FromStr;
6
7use diesel::deserialize::{FromSql, FromSqlRow};
8use diesel::expression::AsExpression;
9use diesel::serialize::ToSql;
10
11pub trait PrefixedId: Sized {
12    const PREFIX: &str;
13}
14
15#[derive(FromSqlRow, AsExpression)]
16#[diesel(sql_type = diesel::sql_types::Uuid)]
17pub struct Id<T: PrefixedId> {
18    id: ulid::Ulid,
19    _phantom: std::marker::PhantomData<T>,
20}
21
22impl<T: PrefixedId> Id<T> {
23    pub fn unprefixed(&self) -> ulid::Ulid {
24        self.id
25    }
26}
27
28impl<T: PrefixedId> Default for Id<T> {
29    fn default() -> Self {
30        Self {
31            id: ulid::Ulid::new(),
32            _phantom: std::marker::PhantomData,
33        }
34    }
35}
36
37impl<T: PrefixedId> Id<T> {
38    pub fn new() -> Self {
39        Self::default()
40    }
41}
42
43impl<T: PrefixedId> Debug for Id<T> {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        Debug::fmt(&self.id, f)
46    }
47}
48
49impl<T: PrefixedId> PartialEq for Id<T> {
50    fn eq(&self, other: &Self) -> bool {
51        self.id == other.id
52    }
53}
54
55impl<T: PrefixedId> Eq for Id<T> {}
56
57impl<T: PrefixedId> Hash for Id<T> {
58    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
59        self.id.hash(state);
60    }
61}
62
63impl<T> Deref for Id<T>
64where
65    T: PrefixedId,
66{
67    type Target = ulid::Ulid;
68
69    fn deref(&self) -> &Self::Target {
70        &self.id
71    }
72}
73
74impl<T, I> From<I> for Id<T>
75where
76    T: PrefixedId,
77    I: Into<ulid::Ulid>,
78{
79    fn from(id: I) -> Self {
80        Self {
81            id: id.into(),
82            _phantom: std::marker::PhantomData,
83        }
84    }
85}
86
87impl<T: PrefixedId> Display for Id<T> {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        write!(f, "{}_{}", T::PREFIX, self.unprefixed())
90    }
91}
92
93impl<T: PrefixedId> Clone for Id<T> {
94    fn clone(&self) -> Self {
95        *self
96    }
97}
98
99impl<T: PrefixedId> Copy for Id<T> {}
100
101impl<T: PrefixedId> From<Id<T>> for uuid::Uuid {
102    fn from(value: Id<T>) -> Self {
103        uuid::Uuid::from(value.id)
104    }
105}
106
107#[derive(Debug, thiserror::Error)]
108pub enum IdParseError {
109    #[error("ID prefix does not match")]
110    PrefixMismatch,
111    #[error("invalid ID: {0}")]
112    Ulid(#[from] ulid::DecodeError),
113}
114
115impl<T: PrefixedId> FromStr for Id<T> {
116    type Err = IdParseError;
117
118    fn from_str(s: &str) -> Result<Self, Self::Err> {
119        let mut iter = s.rsplitn(2, '_');
120
121        let id = iter.next().ok_or(IdParseError::PrefixMismatch)?;
122        let prefix = iter.next().ok_or(IdParseError::PrefixMismatch)?;
123
124        if prefix != T::PREFIX {
125            return Err(IdParseError::PrefixMismatch);
126        }
127
128        let id = ulid::Ulid::from_str(id)?;
129        Ok(Self {
130            id,
131            _phantom: std::marker::PhantomData,
132        })
133    }
134}
135
136impl<T: PrefixedId> serde::Serialize for Id<T> {
137    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138    where
139        S: serde::Serializer,
140    {
141        self.to_string().serialize(serializer)
142    }
143}
144
145impl<T> FromSql<diesel::sql_types::Uuid, diesel::pg::Pg> for Id<T>
146where
147    T: PrefixedId,
148{
149    fn from_sql(bytes: diesel::pg::PgValue<'_>) -> diesel::deserialize::Result<Self> {
150        let uuid = uuid::Uuid::from_sql(bytes)?;
151
152        Ok(Self {
153            id: ulid::Ulid::from(uuid),
154            _phantom: std::marker::PhantomData,
155        })
156    }
157}
158
159impl<T> ToSql<diesel::sql_types::Uuid, diesel::pg::Pg> for Id<T>
160where
161    T: PrefixedId + Debug,
162{
163    fn to_sql<'b>(&'b self, out: &mut diesel::serialize::Output<'b, '_, diesel::pg::Pg>) -> diesel::serialize::Result {
164        out.write_all(&self.id.to_bytes())
165            .map(|_| diesel::serialize::IsNull::No)
166            .map_err(Into::into)
167    }
168}