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