use std::{
fmt,
hash::{Hash, Hasher},
str::FromStr,
};
use data_encoding::BASE32HEX_NOPAD;
#[cfg(feature = "uuid_v7")]
use uuid::timestamp::{context::NoContext, Timestamp};
use uuid::Uuid;
use crate::{
error::{Error, Result},
prefix::Prefix,
uuid::uuid_from_str_b32h,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OidStr {
prefix: Prefix,
uuid: Uuid,
}
impl OidStr {
#[cfg(feature = "uuid_v4")]
#[cfg_attr(docsrs, doc(cfg(feature = "uuid_v4")))]
pub fn new_v4<P>(prefix: P) -> Result<Self>
where
P: TryInto<Prefix, Error = Error>,
{
Ok(Self {
prefix: prefix.try_into()?,
uuid: Uuid::new_v4(),
})
}
#[cfg(feature = "uuid_v7")]
#[cfg_attr(docsrs, doc(cfg(feature = "uuid_v7")))]
pub fn new_v7_now<P>(prefix: P) -> Result<Self>
where
P: TryInto<Prefix, Error = Error>,
{
Ok(Self {
prefix: prefix.try_into()?,
uuid: Uuid::new_v7(Timestamp::now(NoContext)),
})
}
#[cfg(feature = "uuid_v7")]
#[cfg_attr(docsrs, doc(cfg(feature = "uuid_v7")))]
pub fn new_v7<P>(prefix: P, ts: Timestamp) -> Result<Self>
where
P: TryInto<Prefix, Error = Error>,
{
Self::with_uuid(prefix, Uuid::new_v7(ts))
}
pub fn with_uuid<P>(prefix: P, uuid: Uuid) -> Result<Self>
where
P: TryInto<Prefix, Error = Error>,
{
Ok(Self {
prefix: prefix.try_into()?,
uuid,
})
}
pub fn try_with_uuid<P, S>(prefix: P, uuid: S) -> Result<Self>
where
P: TryInto<Prefix, Error = Error>,
S: AsRef<str>,
{
Self::with_uuid(prefix, uuid.as_ref().try_into()?)
}
pub fn try_with_uuid_base32<P, S>(prefix: P, base32_uuid: S) -> Result<Self>
where
P: TryInto<Prefix, Error = Error>,
S: AsRef<str>,
{
Self::with_uuid(prefix, uuid_from_str_b32h(base32_uuid.as_ref())?)
}
pub fn prefix(&self) -> &Prefix { &self.prefix }
pub fn value(&self) -> String { BASE32HEX_NOPAD.encode(self.uuid.as_bytes()) }
pub fn uuid(&self) -> &Uuid { &self.uuid }
}
impl FromStr for OidStr {
type Err = Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if let Some((pfx, val)) = s.split_once('-') {
if pfx.is_empty() {
return Err(Error::MissingPrefix);
}
return Ok(Self {
prefix: pfx.parse()?,
uuid: uuid_from_str_b32h(val)?,
});
}
Err(Error::MissingSeparator)
}
}
impl fmt::Display for OidStr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}-{}", self.prefix, self.value())
}
}
impl Hash for OidStr {
fn hash<H: Hasher>(&self, state: &mut H) {
self.prefix.hash(state);
self.uuid.hash(state);
}
}
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl ::serde::Serialize for OidStr {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: ::serde::ser::Serializer,
{
serializer.collect_str(self)
}
}
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl<'de> ::serde::Deserialize<'de> for OidStr {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: ::serde::de::Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse()
.map_err(::serde::de::Error::custom)
}
}
#[cfg(test)]
#[cfg(any(feature = "uuid_v4", feature = "uuid_v7"))]
mod oid_tests {
use wildmatch::WildMatch;
use super::*;
#[test]
#[cfg(feature = "uuid_v4")]
fn oid_to_str_v4() -> Result<()> {
let oid = OidStr::new_v4("TST")?;
assert!(WildMatch::new("TST-??????????????????????????").matches(&oid.to_string()));
Ok(())
}
#[test]
#[cfg(feature = "uuid_v7")]
fn oid_to_str_v7() -> Result<()> {
let oid = OidStr::new_v7_now("TST")?;
assert!(WildMatch::new("TST-??????????????????????????").matches(&oid.to_string()));
Ok(())
}
#[test]
fn str_to_oid() {
let res = "TST-0OQPKOAADLRUJ000J7U2UGNS2G".parse::<OidStr>();
assert_eq!(
res.unwrap(),
OidStr {
prefix: "TST".parse().unwrap(),
uuid: "06359a61-4a6d-77e9-8000-99fc2f42fc14".parse().unwrap(),
}
);
}
#[test]
fn str_to_oid_long() {
let res = "TestingTesting-0OQPKOAADLRUJ000J7U2UGNS2G".parse::<OidStr>();
assert_eq!(
res.unwrap(),
OidStr {
prefix: "TestingTesting".parse().unwrap(),
uuid: "06359a61-4a6d-77e9-8000-99fc2f42fc14".parse().unwrap(),
}
);
}
#[test]
fn str_to_oid_err_prefix() {
let res = "-0OQPKOAADLRUJ000J7U2UGNS2G".parse::<OidStr>();
assert!(res.is_err());
assert_eq!(res.unwrap_err(), Error::MissingPrefix);
}
#[test]
fn str_to_oid_err_value() {
let res = "TST-".parse::<OidStr>();
assert!(res.is_err());
assert_eq!(res.unwrap_err(), Error::MissingValue);
}
#[test]
fn str_to_oid_err_decode() {
let res = "TST-&OQPKOAADLRUJ000J7U2UGNS2G".parse::<OidStr>();
assert!(res.is_err());
assert!(matches!(res.unwrap_err(), Error::Base32Decode(_)));
}
#[test]
fn str_to_oid_err_no_sep() {
let res = "0OQPKOAADLRUJ000J7U2UGNS2G".parse::<OidStr>();
assert!(res.is_err());
assert_eq!(res.unwrap_err(), Error::MissingSeparator);
}
#[test]
fn str_to_oid_err_two_sep() {
let res = "TST-0OQPKOAAD-LRUJ000J7U2UGNS2G".parse::<OidStr>();
assert!(res.is_err());
assert!(matches!(res.unwrap_err(), Error::Base32Decode(_)));
}
#[test]
fn oid_to_uuid() {
let oid: OidStr = "TST-0OQPKOAADLRUJ000J7U2UGNS2G".parse().unwrap();
assert_eq!(
oid.uuid(),
&"06359a61-4a6d-77e9-8000-99fc2f42fc14"
.parse::<Uuid>()
.unwrap()
);
}
#[test]
fn from_uuid_str() {
let oid = OidStr::try_with_uuid("Tst", "063dc3a0-3925-7c7f-8000-ca84a12ee183").unwrap();
assert!(
WildMatch::new("Tst-??????????????????????????").matches(&oid.to_string()),
"{oid}"
);
}
#[test]
fn from_uuid_str_b32h() {
let oid = OidStr::try_with_uuid_base32("Tst", "0OUS781P4LU7V000PA2A2BN1GC").unwrap();
assert_eq!("Tst-0OUS781P4LU7V000PA2A2BN1GC", &oid.to_string());
}
#[test]
fn hash() {
use std::collections::HashMap;
let oid: OidStr = "TST-0OQPKOAADLRUJ000J7U2UGNS2G".parse().unwrap();
let mut map = HashMap::new();
map.insert(oid, "test");
}
}