1pub mod error;
70
71use std::{any::type_name, fmt, marker::PhantomData, str::FromStr};
72
73use data_encoding::Encoding;
74use data_encoding_macro::new_encoding;
75use uuid::Uuid;
76
77use crate::error::{Error, Result};
78
79const BASE32_LOWER: Encoding = new_encoding! {
80 symbols: "0123456789abcdefghijklmnopqrstuv",
81};
82
83fn uuid_from_str(s: &str) -> Result<Uuid> {
84 if s.is_empty() {
85 return Err(Error::MissingValue);
86 }
87 Ok(Uuid::from_slice(&BASE32_LOWER.decode(s.as_bytes())?)?)
88}
89
90#[derive(Debug, Copy, Clone, PartialEq, Eq)]
92pub struct Prefix<const N: usize = 3> {
93 bytes: [u8; N],
94}
95
96impl<const N: usize> fmt::Display for Prefix<N> {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 unsafe { write!(f, "{}", std::str::from_utf8_unchecked(self.bytes.as_slice())) }
102 }
103}
104
105impl<const N: usize> Prefix<N> {
106 pub fn from_slice(slice: &[u8]) -> Result<Self> {
109 if !slice
111 .iter()
112 .all(|&c| (c > b'/' && c < b':') || (c > b'`' && c < b'{'))
113 {
114 return Err(Error::InvalidPrefixChar);
115 }
116 if slice.len() != N {
117 return Err(Error::PrefixByteLength);
118 }
119 let mut pfx = Prefix { bytes: [0_u8; N] };
120 pfx.bytes.copy_from_slice(slice);
121 Ok(pfx)
122 }
123}
124
125impl<const N: usize> FromStr for Prefix<N> {
126 type Err = Error;
127
128 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
130 Self::from_slice(s.to_ascii_lowercase().as_bytes())
131 }
132}
133
134impl<const N: usize> From<[u8; N]> for Prefix<N> {
135 fn from(arr: [u8; N]) -> Self { Self { bytes: arr } }
136}
137
138impl<const N: usize> TryFrom<&[u8]> for Prefix<N> {
139 type Error = Error;
140 fn try_from(slice: &[u8]) -> std::result::Result<Self, Self::Error> { Self::from_slice(slice) }
141}
142
143impl<const N: usize> TryFrom<&str> for Prefix<N> {
144 type Error = Error;
145 fn try_from(s: &str) -> std::result::Result<Self, Self::Error> { s.parse() }
146}
147
148#[cfg(test)]
149mod prefix_tests {
150 use super::*;
151
152 #[test]
153 fn prefix_from_str() {
154 let pfx = "frm".parse::<Prefix>();
155 assert!(pfx.is_ok());
156 assert_eq!(pfx.unwrap(), Prefix { bytes: [b'f', b'r', b'm'] });
157 }
158
159 #[test]
160 fn prefix_from_str_err_len() {
161 let pfx = "frmx".parse::<Prefix<3>>();
162 assert!(pfx.is_err());
163 assert_eq!(pfx.unwrap_err(), Error::PrefixByteLength);
164 }
165
166 #[test]
167 fn prefix_from_str_err_char() {
168 let pfx = "fr[".parse::<Prefix<3>>();
169 assert!(pfx.is_err());
170 assert_eq!(pfx.unwrap_err(), Error::InvalidPrefixChar);
171 }
172
173 #[test]
174 fn prefix_from_str_uppercase_ok() {
175 let pfx = "frM".parse::<Prefix>();
176 assert!(pfx.is_ok());
177 assert_eq!(pfx.unwrap(), Prefix { bytes: [b'f', b'r', b'm'] });
178 }
179
180 #[test]
181 fn prefix_from_slice() {
182 let arr: [u8; 3] = [b'f', b'r', b'm'];
183 let pfx = Prefix::from_slice(arr.as_slice());
184 assert!(pfx.is_ok());
185 assert_eq!(pfx.unwrap(), Prefix { bytes: arr });
186 }
187
188 #[test]
189 fn prefix_from_slice_err_len() {
190 let arr: [u8; 4] = [b'f', b'r', b'm', b'x'];
191 let pfx = Prefix::<3>::from_slice(arr.as_slice());
192 assert!(pfx.is_err());
193 assert_eq!(pfx.unwrap_err(), Error::PrefixByteLength);
194 }
195
196 #[test]
197 fn prefix_from_slice_err_char() {
198 let arr: [u8; 3] = [b'f', b'r', b']'];
199 let pfx = Prefix::<3>::from_slice(arr.as_slice());
200 assert!(pfx.is_err());
201 assert_eq!(pfx.unwrap_err(), Error::InvalidPrefixChar);
202 }
203
204 #[test]
205 fn prefix_from_slice_err_uppercase() {
206 let arr: [u8; 3] = [b'f', b'r', b'M'];
207 let pfx = Prefix::<3>::from_slice(arr.as_slice());
208 assert!(pfx.is_err());
209 assert_eq!(pfx.unwrap_err(), Error::InvalidPrefixChar);
210 }
211
212 #[test]
213 fn prefix_to_string() {
214 let pfx: Prefix = "frM".parse().unwrap();
215 assert_eq!("frm".to_string(), pfx.to_string());
216 }
217}
218
219#[derive(Debug, Copy, Clone, PartialEq, Eq)]
221pub struct Oid {
222 prefix: Prefix<3>,
223 uuid: Uuid,
224}
225
226impl Oid {
227 pub fn new<P>(prefix: P) -> Result<Self>
232 where
233 P: TryInto<Prefix, Error = Error>,
234 {
235 Self::with_uuid(prefix, Uuid::new_v4())
236 }
237
238 pub fn with_uuid<P>(prefix: P, uuid: Uuid) -> Result<Self>
244 where
245 P: TryInto<Prefix, Error = Error>,
246 {
247 Ok(Self { prefix: prefix.try_into()?, uuid })
248 }
249
250 pub fn prefix(&self) -> Prefix { self.prefix }
252
253 pub fn value(&self) -> String { BASE32_LOWER.encode(self.uuid.as_bytes()) }
256
257 pub fn uuid(&self) -> &Uuid { &self.uuid }
259}
260
261impl FromStr for Oid {
262 type Err = Error;
263
264 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
265 if let Some((pfx, val)) = s.split_once('-') {
266 if pfx.is_empty() {
267 return Err(Error::MissingPrefix);
268 }
269
270 return Ok(Self { prefix: pfx.parse()?, uuid: uuid_from_str(val)? });
271 }
272
273 Err(Error::MissingSeparator)
274 }
275}
276
277impl fmt::Display for Oid {
278 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279 write!(f, "{}-{}", self.prefix, self.value())
280 }
281}
282
283#[cfg(feature = "serde")]
284impl ::serde::Serialize for Oid {
285 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
286 where
287 S: ::serde::ser::Serializer,
288 {
289 serializer.collect_str(self)
290 }
291}
292
293#[cfg(feature = "serde")]
294impl<'de> ::serde::Deserialize<'de> for Oid {
295 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
296 where
297 D: ::serde::de::Deserializer<'de>,
298 {
299 String::deserialize(deserializer)?
300 .parse()
301 .map_err(::serde::de::Error::custom)
302 }
303}
304
305#[cfg(test)]
306mod oid_tests {
307 use wildmatch::WildMatch;
308
309 use super::*;
310
311 #[test]
312 fn oid_to_str() -> Result<()> {
313 let oid = Oid::new("tst")?;
314 assert!(WildMatch::new("tst-??????????????????????????").matches(&oid.to_string()));
315 Ok(())
316 }
317
318 #[test]
319 fn str_to_oid() {
320 let res = "tst-0oqpkoaadlruj000j7u2ugns2g".parse::<Oid>();
321 assert_eq!(
322 res.unwrap(),
323 Oid {
324 prefix: "tst".parse().unwrap(),
325 uuid: "06359a61-4a6d-77e9-8000-99fc2f42fc14".parse().unwrap(),
326 }
327 );
328 }
329
330 #[test]
331 fn str_to_oid_err_prefix() {
332 let res = "-0oqpkoaadlruj000j7u2ugns2g".parse::<Oid>();
333 assert!(res.is_err());
334 assert_eq!(res.unwrap_err(), Error::MissingPrefix);
335 }
336
337 #[test]
338 fn str_to_oid_err_value() {
339 let res = "tst-".parse::<Oid>();
340 assert!(res.is_err());
341 assert_eq!(res.unwrap_err(), Error::MissingValue);
342 }
343
344 #[test]
345 fn str_to_oid_err_decode() {
346 let res = "tst-&oqpkoaadlruj000j7u2ugns2g".parse::<Oid>();
347 assert!(res.is_err());
348 assert!(matches!(res.unwrap_err(), Error::Base32Decode(_)));
349 }
350
351 #[test]
352 fn str_to_oid_err_no_sep() {
353 let res = "0oqpkoaadlruj000j7u2ugns2g".parse::<Oid>();
354 assert!(res.is_err());
355 assert_eq!(res.unwrap_err(), Error::MissingSeparator);
356 }
357
358 #[test]
359 fn str_to_oid_err_two_sep() {
360 let res = "tst-0oqpkoaad-lruj000j7u2ugns2g".parse::<Oid>();
361 assert!(res.is_err());
362 assert!(matches!(res.unwrap_err(), Error::Base32Decode(_)));
363 }
364
365 #[test]
366 fn oid_to_uuid() {
367 let oid: Oid = "tst-0oqpkoaadlruj000j7u2ugns2g".parse().unwrap();
368 assert_eq!(
369 oid.uuid(),
370 &"06359a61-4a6d-77e9-8000-99fc2f42fc14"
371 .parse::<Uuid>()
372 .unwrap()
373 );
374 }
375}
376
377pub trait OidPrefix {
378 fn string_prefix() -> String {
379 type_name::<Self>()
380 .split(':')
381 .last()
382 .map(|s| s.to_ascii_lowercase())
383 .unwrap()
384 }
385
386 fn long_name() -> String { Self::string_prefix() }
387}
388
389#[derive(Debug, Copy, Clone, PartialEq, Eq)]
414pub struct TypedOid<P: OidPrefix> {
415 uuid: Uuid,
416 _prefix: PhantomData<P>,
417}
418
419impl<P: OidPrefix> TypedOid<P> {
420 pub fn new() -> Self { Self::with_uuid(Uuid::new_v4()) }
422
423 pub fn with_uuid(uuid: Uuid) -> Self { Self { uuid, _prefix: PhantomData } }
425
426 pub fn prefix(&self) -> Prefix {
432 Prefix::from_str(&P::string_prefix()).expect("Invalid Prefix")
433 }
434
435 pub fn value(&self) -> String { BASE32_LOWER.encode(self.uuid.as_bytes()) }
438
439 pub fn uuid(&self) -> &Uuid { &self.uuid }
441}
442
443impl<P: OidPrefix> Default for TypedOid<P> {
444 fn default() -> Self { Self::new() }
445}
446
447impl<P: OidPrefix> fmt::Display for TypedOid<P> {
448 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449 write!(f, "{}-{}", P::string_prefix(), self.value())
450 }
451}
452
453impl<P: OidPrefix> FromStr for TypedOid<P> {
454 type Err = Error;
455
456 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
457 if let Some((pfx, val)) = s.split_once('-') {
458 if pfx.is_empty() {
459 return Err(Error::MissingPrefix);
460 }
461
462 if pfx != P::string_prefix() {
463 return Err(Error::InvalidPrefixChar);
464 }
465
466 return Ok(Self { uuid: uuid_from_str(val)?, _prefix: PhantomData });
467 }
468
469 Err(Error::MissingSeparator)
470 }
471}
472
473#[cfg(feature = "serde")]
474impl<P: OidPrefix> ::serde::Serialize for TypedOid<P> {
475 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
476 where
477 S: ::serde::ser::Serializer,
478 {
479 serializer.collect_str(self)
480 }
481}
482
483#[cfg(feature = "serde")]
484impl<'de, P: OidPrefix> ::serde::Deserialize<'de> for TypedOid<P> {
485 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
486 where
487 D: ::serde::de::Deserializer<'de>,
488 {
489 String::deserialize(deserializer)?
490 .parse()
491 .map_err(::serde::de::Error::custom)
492 }
493}
494
495#[cfg(feature = "schemars")]
496impl<P: OidPrefix> ::schemars::JsonSchema for TypedOid<P> {
497 fn schema_name() -> String { P::long_name() }
498
499 fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
500 ::schemars::schema::SchemaObject {
501 instance_type: Some(::schemars::schema::InstanceType::String.into()),
502 string: Some(Box::new(::schemars::schema::StringValidation {
503 pattern: Some(format!("^{}-[0-9a-vA-V]{{26}}$", P::string_prefix())),
504 ..Default::default()
505 })),
506 ..Default::default()
507 }
508 .into()
509 }
510}
511
512#[cfg(test)]
513mod typed_oid_tests {
514 use wildmatch::WildMatch;
515
516 use super::*;
517
518 #[test]
519 fn typed_oid() {
520 #[derive(Debug)]
521 struct Tst;
522 impl OidPrefix for Tst {}
523
524 let oid: TypedOid<Tst> = TypedOid::new();
525 assert!(
526 WildMatch::new("tst-??????????????????????????").matches(&oid.to_string()),
527 "{oid}"
528 );
529
530 let res = "tst-0ous781p4lu7v000pa2a2bn1gc".parse::<TypedOid<Tst>>();
531 assert!(res.is_ok());
532 let oid: TypedOid<Tst> = res.unwrap();
533 assert_eq!(
534 oid.uuid(),
535 &"063dc3a0-3925-7c7f-8000-ca84a12ee183"
536 .parse::<Uuid>()
537 .unwrap()
538 );
539
540 let res = "frm-0ous781p4lu7v000pa2a2bn1gc".parse::<TypedOid<Tst>>();
541 assert!(res.is_err());
542 assert_eq!(res.unwrap_err(), Error::InvalidPrefixChar);
543 }
544}