1use std::{
2 fmt,
3 hash::{Hash, Hasher},
4 marker::PhantomData,
5 str::FromStr,
6};
7
8use data_encoding::BASE32HEX_NOPAD;
9#[cfg(feature = "uuid_v7")]
10use uuid::timestamp::{context::NoContext, Timestamp};
11use uuid::Uuid;
12
13use crate::{
14 error::{Error, Result},
15 prefix::Prefix,
16 uuid::uuid_from_str_b32h,
17 OidPrefix,
18};
19
20#[derive(PartialEq, Eq)]
44pub struct Oid<P> {
45 uuid: Uuid,
46 _prefix: PhantomData<fn(P) -> P>,
49}
50
51impl<P> fmt::Debug for Oid<P> {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 f.debug_struct(&format!("Oid<{}>", std::any::type_name::<P>()))
54 .field("uuid", &self.uuid)
55 .finish()
56 }
57}
58
59impl<P> Copy for Oid<P> {}
62
63impl<P> Clone for Oid<P> {
64 fn clone(&self) -> Self { *self }
65}
66
67impl<P: OidPrefix> Oid<P> {
68 #[cfg(feature = "uuid_v4")]
70 #[cfg_attr(docsrs, doc(cfg(feature = "uuid_v4")))]
71 pub fn new_v4() -> Self { Self::with_uuid(Uuid::new_v4()) }
72
73 #[cfg(feature = "uuid_v7")]
76 #[cfg_attr(docsrs, doc(cfg(feature = "uuid_v7")))]
77 pub fn new_v7_now() -> Self { Self::with_uuid(Uuid::new_v7(Timestamp::now(NoContext))) }
78
79 #[cfg(feature = "uuid_v7")]
81 #[cfg_attr(docsrs, doc(cfg(feature = "uuid_v7")))]
82 pub fn new_v7(ts: Timestamp) -> Self { Self::with_uuid(Uuid::new_v7(ts)) }
83
84 pub fn with_uuid(uuid: Uuid) -> Self {
86 Self {
87 uuid,
88 _prefix: PhantomData,
89 }
90 }
91
92 pub fn try_with_uuid<S: AsRef<str>>(uuid: S) -> Result<Self> {
94 Ok(Self::with_uuid(uuid.as_ref().try_into()?))
95 }
96
97 pub fn try_with_uuid_base32<S: AsRef<str>>(base32_uuid: S) -> Result<Self> {
99 Ok(Self::with_uuid(uuid_from_str_b32h(base32_uuid.as_ref())?))
100 }
101
102 pub fn prefix(&self) -> Prefix { Prefix::from_str(P::prefix()).expect("Invalid Prefix") }
108
109 pub fn value(&self) -> String { BASE32HEX_NOPAD.encode(self.uuid.as_bytes()) }
112
113 pub fn uuid(&self) -> &Uuid { &self.uuid }
115}
116
117impl<P: OidPrefix> fmt::Display for Oid<P> {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 write!(f, "{}-{}", P::prefix(), self.value())
120 }
121}
122
123impl<P: OidPrefix> FromStr for Oid<P> {
124 type Err = Error;
125
126 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
127 if let Some((pfx, val)) = s.split_once('-') {
128 if pfx.is_empty() {
129 return Err(Error::MissingPrefix);
130 }
131 if pfx != P::prefix() && !P::str_partial_eq(pfx) {
132 return Err(Error::InvalidPrefix {
133 valid_until: pfx
134 .chars()
135 .zip(P::prefix().chars())
136 .enumerate()
137 .find(|(_i, (c1, c2))| c1 != c2)
138 .map(|(i, _)| i)
139 .unwrap(),
140 });
141 }
142
143 return Ok(Self {
144 uuid: uuid_from_str_b32h(val)?,
145 _prefix: PhantomData,
146 });
147 }
148
149 Err(Error::MissingSeparator)
150 }
151}
152
153impl<P> Hash for Oid<P>
154where
155 P: OidPrefix,
156{
157 fn hash<H: Hasher>(&self, state: &mut H) {
158 P::prefix().hash(state);
159 self.uuid.hash(state);
160 }
161}
162
163#[cfg(feature = "serde")]
164#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
165impl<P: OidPrefix> ::serde::Serialize for Oid<P> {
166 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
167 where
168 S: ::serde::ser::Serializer,
169 {
170 serializer.collect_str(self)
171 }
172}
173
174#[cfg(feature = "serde")]
175#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
176impl<'de, P: OidPrefix> ::serde::Deserialize<'de> for Oid<P> {
177 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
178 where
179 D: ::serde::de::Deserializer<'de>,
180 {
181 String::deserialize(deserializer)?
182 .parse()
183 .map_err(::serde::de::Error::custom)
184 }
185}
186
187#[cfg(feature = "surrealdb")]
188#[cfg_attr(docsrs, doc(cfg(feature = "surrealdb")))]
189use surrealdb::sql::Thing;
190
191#[cfg(feature = "surrealdb")]
192#[cfg_attr(docsrs, doc(cfg(feature = "surrealdb")))]
193impl<P: OidPrefix> TryFrom<Thing> for Oid<P> {
194 type Error = crate::Error;
195
196 fn try_from(thing: Thing) -> Result<Self> {
197 if !P::str_partial_eq(&thing.tb) {
198 return Err(Error::InvalidPrefix {
199 valid_until: thing
200 .tb
201 .chars()
202 .zip(P::prefix().chars())
203 .enumerate()
204 .find(|(_i, (c1, c2))| c1 != c2)
205 .map(|(i, _)| i)
206 .unwrap(),
207 });
208 }
209
210 let val = thing.id.to_raw();
211 Self::try_with_uuid(&val).or_else(|_| Self::try_with_uuid_base32(val))
212 }
213}
214
215#[cfg(test)]
216mod oid_tests {
217 #[cfg(any(feature = "uuid_v4", feature = "uuid_v7"))]
218 use wildmatch::WildMatch;
219
220 #[cfg(any(feature = "uuid_v4", feature = "uuid_v7"))]
221 use super::*;
222
223 #[test]
224 #[cfg(any(feature = "uuid_v4", feature = "uuid_v7"))]
225 fn typed_oid() {
226 #[derive(Debug)]
227 struct Tst;
228 impl OidPrefix for Tst {}
229
230 #[cfg_attr(all(feature = "uuid_v4", feature = "uuid_v7"), allow(unused_variables))]
231 #[cfg(feature = "uuid_v4")]
232 let oid: Oid<Tst> = Oid::new_v4();
233 #[cfg(feature = "uuid_v7")]
234 let oid: Oid<Tst> = Oid::new_v7_now();
235 assert!(
236 WildMatch::new("Tst-??????????????????????????").matches(&oid.to_string()),
237 "{oid}"
238 );
239
240 let res = "Tst-0OUS781P4LU7V000PA2A2BN1GC".parse::<Oid<Tst>>();
241 assert!(res.is_ok());
242 let oid: Oid<Tst> = res.unwrap();
243 assert_eq!(
244 oid.uuid(),
245 &"063dc3a0-3925-7c7f-8000-ca84a12ee183"
246 .parse::<Uuid>()
247 .unwrap()
248 );
249
250 let res = "Frm-0OUS781P4LU7V000PA2A2BN1GC".parse::<Oid<Tst>>();
251 assert!(res.is_err());
252 assert_eq!(res.unwrap_err(), Error::InvalidPrefix { valid_until: 0 });
253 }
254
255 #[test]
256 #[cfg(any(feature = "uuid_v4", feature = "uuid_v7"))]
257 fn from_uuid_str() {
258 #[derive(Debug)]
259 struct Tst;
260 impl OidPrefix for Tst {}
261
262 let oid: Oid<Tst> = Oid::try_with_uuid("063dc3a0-3925-7c7f-8000-ca84a12ee183").unwrap();
263 assert!(
264 WildMatch::new("Tst-??????????????????????????").matches(&oid.to_string()),
265 "{oid}"
266 );
267 }
268
269 #[test]
270 #[cfg(any(feature = "uuid_v4", feature = "uuid_v7"))]
271 fn from_uuid_str_b32h() {
272 #[derive(Debug)]
273 struct Tst;
274 impl OidPrefix for Tst {}
275
276 let oid: Oid<Tst> = Oid::try_with_uuid_base32("0OUS781P4LU7V000PA2A2BN1GC").unwrap();
277 assert_eq!("Tst-0OUS781P4LU7V000PA2A2BN1GC", &oid.to_string());
278 }
279
280 #[test]
281 #[cfg(any(feature = "uuid_v4", feature = "uuid_v7"))]
282 fn hash() {
283 use std::collections::HashMap;
284 #[derive(Debug, PartialEq, Eq)]
285 struct Tst;
286 impl OidPrefix for Tst {}
287
288 let oid: Oid<Tst> = Oid::try_with_uuid("063dc3a0-3925-7c7f-8000-ca84a12ee183").unwrap();
289
290 let mut map = HashMap::new();
291 map.insert(oid, "test");
292 }
293
294 #[test]
295 #[cfg(any(feature = "uuid_v4", feature = "uuid_v7"))]
296 fn long_typed_oid() {
297 #[derive(Debug)]
298 struct TestingTesting;
299 impl OidPrefix for TestingTesting {}
300
301 #[cfg_attr(all(feature = "uuid_v4", feature = "uuid_v7"), allow(unused_variables))]
302 #[cfg(feature = "uuid_v4")]
303 let oid: Oid<TestingTesting> = Oid::new_v4();
304 #[cfg(feature = "uuid_v7")]
305 let oid: Oid<TestingTesting> = Oid::new_v7_now();
306 assert!(
307 WildMatch::new("TestingTesting-??????????????????????????").matches(&oid.to_string()),
308 "{oid}"
309 );
310
311 let res = "TestingTesting-0OUS781P4LU7V000PA2A2BN1GC".parse::<Oid<TestingTesting>>();
312 assert!(res.is_ok());
313 let oid: Oid<TestingTesting> = res.unwrap();
314 assert_eq!(
315 oid.uuid(),
316 &"063dc3a0-3925-7c7f-8000-ca84a12ee183"
317 .parse::<Uuid>()
318 .unwrap()
319 );
320
321 let res = "Frm-0OUS781P4LU7V000PA2A2BN1GC".parse::<Oid<TestingTesting>>();
322 assert!(res.is_err());
323 assert_eq!(res.unwrap_err(), Error::InvalidPrefix { valid_until: 0 });
324 }
325}
326
327#[cfg(test)]
328#[cfg(feature = "surrealdb")]
329mod surreal_thing_oid_tests {
330 use surrealdb::sql::Id;
331
332 use super::*;
333
334 #[test]
335 fn uuid() {
336 #[derive(Debug)]
337 struct Tst;
338 impl OidPrefix for Tst {
339 fn str_partial_eq(s: &str) -> bool { "test" == s }
340 }
341
342 let thing = Thing {
343 tb: "test".to_string(),
344 id: Id::String("063dc3a0-3925-7c7f-8000-ca84a12ee183".to_string()),
345 };
346
347 let toid: Result<Oid<Tst>> = thing.try_into();
348 assert!(toid.is_ok());
349 }
350
351 #[test]
352 fn uuid_base32() {
353 #[derive(Debug)]
354 struct Tst;
355 impl OidPrefix for Tst {
356 fn str_partial_eq(s: &str) -> bool { "test" == s }
357 }
358
359 let thing = Thing {
360 tb: "test".to_string(),
361 id: Id::String("0OUS781P4LU7V000PA2A2BN1GC".to_string()),
362 };
363
364 let toid: Result<Oid<Tst>> = thing.try_into();
365 assert!(toid.is_ok());
366 }
367}