typed_oid/
oidstr.rs

1use std::{
2    fmt,
3    hash::{Hash, Hasher},
4    str::FromStr,
5};
6
7use data_encoding::BASE32HEX_NOPAD;
8#[cfg(feature = "uuid_v7")]
9use uuid::timestamp::{context::NoContext, Timestamp};
10use uuid::Uuid;
11
12use crate::{
13    error::{Error, Result},
14    prefix::Prefix,
15    uuid::uuid_from_str_b32h,
16};
17
18/// An Object ID
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct OidStr {
21    prefix: Prefix,
22    uuid: Uuid,
23}
24
25impl OidStr {
26    /// Create a new OID with a given [`Prefix`] and generating a new UUID
27    ///
28    /// > **NOTE:** The Prefix must be ASCII characters of `A-Z,a-z,0-9` (this
29    /// > restriction is arbitrary and could be lifted in the future.
30    #[cfg(feature = "uuid_v4")]
31    #[cfg_attr(docsrs, doc(cfg(feature = "uuid_v4")))]
32    pub fn new_v4<P>(prefix: P) -> Result<Self>
33    where
34        P: TryInto<Prefix, Error = Error>,
35    {
36        Ok(Self {
37            prefix: prefix.try_into()?,
38            uuid: Uuid::new_v4(),
39        })
40    }
41
42    /// Create a new OID with a given [`Prefix`] and generating a new UUIDv7
43    /// (UNIX Epoch based on current system clock)
44    #[cfg(feature = "uuid_v7")]
45    #[cfg_attr(docsrs, doc(cfg(feature = "uuid_v7")))]
46    pub fn new_v7_now<P>(prefix: P) -> Result<Self>
47    where
48        P: TryInto<Prefix, Error = Error>,
49    {
50        Ok(Self {
51            prefix: prefix.try_into()?,
52            uuid: Uuid::new_v7(Timestamp::now(NoContext)),
53        })
54    }
55
56    /// Create a new OID with a given [`Prefix`] and generating a new UUIDv7
57    /// (UNIX Epoch based)
58    #[cfg(feature = "uuid_v7")]
59    #[cfg_attr(docsrs, doc(cfg(feature = "uuid_v7")))]
60    pub fn new_v7<P>(prefix: P, ts: Timestamp) -> Result<Self>
61    where
62        P: TryInto<Prefix, Error = Error>,
63    {
64        Self::with_uuid(prefix, Uuid::new_v7(ts))
65    }
66
67    /// Create a new OID with a given [`Prefix`] and a given UUID.
68    ///
69    /// > **NOTE:** The Prefix must be ASCII characters of `A-Z,a-z,0-9` (this
70    /// > restriction is arbitrary and could be lifted in the future.
71    pub fn with_uuid<P>(prefix: P, uuid: Uuid) -> Result<Self>
72    where
73        P: TryInto<Prefix, Error = Error>,
74    {
75        Ok(Self {
76            prefix: prefix.try_into()?,
77            uuid,
78        })
79    }
80
81    /// Create a new OID with a given [`Prefix`] and a given string-ish UUID.
82    ///
83    /// > **NOTE:** The Prefix must be ASCII characters of `A-Z,a-z,0-9` (this
84    /// > restriction is arbitrary and could be lifted in the future.
85    pub fn try_with_uuid<P, S>(prefix: P, uuid: S) -> Result<Self>
86    where
87        P: TryInto<Prefix, Error = Error>,
88        S: AsRef<str>,
89    {
90        Self::with_uuid(prefix, uuid.as_ref().try_into()?)
91    }
92
93    /// Attemp to create an Oid from a base32hex encoded UUID string-ish value
94    pub fn try_with_uuid_base32<P, S>(prefix: P, base32_uuid: S) -> Result<Self>
95    where
96        P: TryInto<Prefix, Error = Error>,
97        S: AsRef<str>,
98    {
99        Self::with_uuid(prefix, uuid_from_str_b32h(base32_uuid.as_ref())?)
100    }
101
102    /// Get the [`Prefix`] of the OID
103    pub fn prefix(&self) -> &Prefix { &self.prefix }
104
105    /// Get the value portion of the  of the OID, which is the base32 encoded
106    /// string following the `-` separator
107    pub fn value(&self) -> String { BASE32HEX_NOPAD.encode(self.uuid.as_bytes()) }
108
109    /// Get the UUID of the OID
110    pub fn uuid(&self) -> &Uuid { &self.uuid }
111}
112
113impl FromStr for OidStr {
114    type Err = Error;
115
116    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
117        if let Some((pfx, val)) = s.split_once('-') {
118            if pfx.is_empty() {
119                return Err(Error::MissingPrefix);
120            }
121
122            return Ok(Self {
123                prefix: pfx.parse()?,
124                uuid: uuid_from_str_b32h(val)?,
125            });
126        }
127
128        Err(Error::MissingSeparator)
129    }
130}
131
132impl fmt::Display for OidStr {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        write!(f, "{}-{}", self.prefix, self.value())
135    }
136}
137
138impl Hash for OidStr {
139    fn hash<H: Hasher>(&self, state: &mut H) {
140        self.prefix.hash(state);
141        self.uuid.hash(state);
142    }
143}
144
145#[cfg(feature = "serde")]
146#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
147impl ::serde::Serialize for OidStr {
148    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
149    where
150        S: ::serde::ser::Serializer,
151    {
152        serializer.collect_str(self)
153    }
154}
155
156#[cfg(feature = "serde")]
157#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
158impl<'de> ::serde::Deserialize<'de> for OidStr {
159    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
160    where
161        D: ::serde::de::Deserializer<'de>,
162    {
163        String::deserialize(deserializer)?
164            .parse()
165            .map_err(::serde::de::Error::custom)
166    }
167}
168
169#[cfg(test)]
170#[cfg(any(feature = "uuid_v4", feature = "uuid_v7"))]
171mod oid_tests {
172    use wildmatch::WildMatch;
173
174    use super::*;
175
176    #[test]
177    #[cfg(feature = "uuid_v4")]
178    fn oid_to_str_v4() -> Result<()> {
179        let oid = OidStr::new_v4("TST")?;
180        assert!(WildMatch::new("TST-??????????????????????????").matches(&oid.to_string()));
181        Ok(())
182    }
183
184    #[test]
185    #[cfg(feature = "uuid_v7")]
186    fn oid_to_str_v7() -> Result<()> {
187        let oid = OidStr::new_v7_now("TST")?;
188        assert!(WildMatch::new("TST-??????????????????????????").matches(&oid.to_string()));
189        Ok(())
190    }
191
192    #[test]
193    fn str_to_oid() {
194        let res = "TST-0OQPKOAADLRUJ000J7U2UGNS2G".parse::<OidStr>();
195        assert_eq!(
196            res.unwrap(),
197            OidStr {
198                prefix: "TST".parse().unwrap(),
199                uuid: "06359a61-4a6d-77e9-8000-99fc2f42fc14".parse().unwrap(),
200            }
201        );
202    }
203
204    #[test]
205    fn str_to_oid_long() {
206        let res = "TestingTesting-0OQPKOAADLRUJ000J7U2UGNS2G".parse::<OidStr>();
207        assert_eq!(
208            res.unwrap(),
209            OidStr {
210                prefix: "TestingTesting".parse().unwrap(),
211                uuid: "06359a61-4a6d-77e9-8000-99fc2f42fc14".parse().unwrap(),
212            }
213        );
214    }
215
216    #[test]
217    fn str_to_oid_err_prefix() {
218        let res = "-0OQPKOAADLRUJ000J7U2UGNS2G".parse::<OidStr>();
219        assert!(res.is_err());
220        assert_eq!(res.unwrap_err(), Error::MissingPrefix);
221    }
222
223    #[test]
224    fn str_to_oid_err_value() {
225        let res = "TST-".parse::<OidStr>();
226        assert!(res.is_err());
227        assert_eq!(res.unwrap_err(), Error::MissingValue);
228    }
229
230    #[test]
231    fn str_to_oid_err_decode() {
232        let res = "TST-&OQPKOAADLRUJ000J7U2UGNS2G".parse::<OidStr>();
233        assert!(res.is_err());
234        assert!(matches!(res.unwrap_err(), Error::Base32Decode(_)));
235    }
236
237    #[test]
238    fn str_to_oid_err_no_sep() {
239        let res = "0OQPKOAADLRUJ000J7U2UGNS2G".parse::<OidStr>();
240        assert!(res.is_err());
241        assert_eq!(res.unwrap_err(), Error::MissingSeparator);
242    }
243
244    #[test]
245    fn str_to_oid_err_two_sep() {
246        let res = "TST-0OQPKOAAD-LRUJ000J7U2UGNS2G".parse::<OidStr>();
247        assert!(res.is_err());
248        assert!(matches!(res.unwrap_err(), Error::Base32Decode(_)));
249    }
250
251    #[test]
252    fn oid_to_uuid() {
253        let oid: OidStr = "TST-0OQPKOAADLRUJ000J7U2UGNS2G".parse().unwrap();
254        assert_eq!(
255            oid.uuid(),
256            &"06359a61-4a6d-77e9-8000-99fc2f42fc14"
257                .parse::<Uuid>()
258                .unwrap()
259        );
260    }
261
262    #[test]
263    fn from_uuid_str() {
264        let oid = OidStr::try_with_uuid("Tst", "063dc3a0-3925-7c7f-8000-ca84a12ee183").unwrap();
265        assert!(
266            WildMatch::new("Tst-??????????????????????????").matches(&oid.to_string()),
267            "{oid}"
268        );
269    }
270
271    #[test]
272    fn from_uuid_str_b32h() {
273        let oid = OidStr::try_with_uuid_base32("Tst", "0OUS781P4LU7V000PA2A2BN1GC").unwrap();
274        assert_eq!("Tst-0OUS781P4LU7V000PA2A2BN1GC", &oid.to_string());
275    }
276
277    #[test]
278    fn hash() {
279        use std::collections::HashMap;
280        let oid: OidStr = "TST-0OQPKOAADLRUJ000J7U2UGNS2G".parse().unwrap();
281
282        let mut map = HashMap::new();
283        map.insert(oid, "test");
284    }
285}