scufflecloud_core/
id.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 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            // get last _
123            let parts: Vec<&str> = s.rsplitn(2, '_').collect();
124            // guaranteed to contain at least two parts here because s contains '_'
125
126            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}