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}