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#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct OidStr {
21 prefix: Prefix,
22 uuid: Uuid,
23}
24
25impl OidStr {
26 #[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 #[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 #[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 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 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 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 pub fn prefix(&self) -> &Prefix { &self.prefix }
104
105 pub fn value(&self) -> String { BASE32HEX_NOPAD.encode(self.uuid.as_bytes()) }
108
109 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}