/*! * Nano-Random IDentifier. * * Made of a 64-bit seconds, 32-bit nanoseconds, and 32-bit secure-randomness, the NRID is suitable for cases where you want a secure-random, unique identifier like a UUID, but you also want the identifier to be correlated with the time of creation. */ use std::convert::TryFrom; use std::str::FromStr; pub mod error { #[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] #[error("Value for segment '{segment}' must be range {minimum}..={maximum}.")] pub struct SegmentRange { /// The segment in question. pub segment: &'static str, /// The minimum value for the segment, inclusive. pub minimum: u64, /// The maximum value for the segment, inclusive. pub maximum: u64, } #[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] #[error( "Encoded value for segment '{segment}' must be {expected_length} characters in length." )] pub struct SegmentLength { /// The segment in question. pub segment: &'static str, /// The expected length of the segment encoded as a string. pub expected_length: usize, } #[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] pub enum Segment { #[error("Invalid character in segment {segment}.")] Character { segment: &'static str }, #[error(transparent)] Length(#[from] SegmentLength), #[error(transparent)] Range(#[from] SegmentRange), #[error("Unknown error parsing segment")] Unknown, } impl Segment { #[inline] pub(crate) fn character(segment: &'static str) -> Self { Self::Character { segment } } #[inline] pub(crate) fn length(segment: &'static str, expected_length: usize) -> Self { SegmentLength { segment, expected_length, } .into() } #[inline] pub(crate) fn range(segment: &'static str, minimum: u64, maximum: u64) -> Self { SegmentRange { segment, minimum, maximum, } .into() } #[inline] pub(crate) fn new( segment: &'static str, expected_length: usize, minimum: u64, maximum: u64, err: std::num::ParseIntError, ) -> Self { use std::num::IntErrorKind; match err.kind() { IntErrorKind::Empty => Self::length(segment, expected_length), IntErrorKind::InvalidDigit => Self::character(segment), IntErrorKind::PosOverflow | IntErrorKind::NegOverflow => { Self::range(segment, minimum, maximum) } IntErrorKind::Zero => { assert!(minimum > 0); Self::range(segment, minimum, maximum) } _ => Self::Unknown, } } } #[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] pub enum Parse { #[error("Entire value must be 32 characters in length, not {0}.")] Length(usize), #[error("Invalid segment delimiter at {0}.")] Separator(usize), #[error(transparent)] Segment(Segment), } impl From for Parse where E: Into, { fn from(e: E) -> Self { >::into(e).into() } } /// An error in conversion to a foreign type. #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] #[error("{0}")] pub struct ConversionError(pub(crate) std::borrow::Cow<'static, str>); } /// **N**anosecond-precision **R**andom **Id**entifier. Stores the seconds since the epoch and nanoseconds offset, along with 4 bytes of randomness. #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[repr(C)] pub struct Nrid { secs: u64, nanos: u32, rand: u32, } impl Nrid { pub const fn new(secs: u64, nanos: u32, rand: u32) -> Result { if nanos > 999_999_999 { return Err(error::SegmentRange { segment: "nanos", minimum: 0, maximum: 999_999_999, }); } Ok(Self { secs, nanos, rand }) } /// Returns a new `Nrid` with its timestamp taken from [`Utc::now`]. pub fn now() -> Self { let now = time::OffsetDateTime::now_utc(); use rand::RngCore; assert!( now.unix_timestamp() >= 0, "System time is before the Unix Epoch!" ); assert!( now.nanosecond() <= 999_999_999, "Invalid nanosecond offset!" ); Self::new( now.unix_timestamp() as u64, now.nanosecond(), rand::rngs::OsRng.next_u32(), ) .unwrap() } /// The seconds component of the id. #[inline(always)] pub const fn secs(&self) -> u64 { self.secs } /// The seconds component of the id. #[inline(always)] pub const fn nanos(&self) -> u32 { self.nanos } /// The random component of the id. #[inline(always)] pub const fn rand(&self) -> u32 { self.rand } } impl std::fmt::Debug for Nrid { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { write!( f, "{:012x}-{:09x}-{:09x}", self.secs(), self.nanos(), self.rand() ) } } impl std::fmt::Display for Nrid { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { write!( f, "{:012x}-{:09x}-{:09x}", self.secs(), self.nanos(), self.rand() ) } } #[cfg(feature = "serde")] mod serde_mod { use super::*; #[derive(Debug, Clone, Copy, serde::Deserialize)] #[serde(field_identifier, rename_all = "lowercase")] pub enum Field { Secs, Nanos, Rand, } impl Field { #[inline] pub const fn as_str(self) -> &'static str { match self { Self::Secs => "secs", Self::Nanos => "nanos", Self::Rand => "rand", } } } pub const NRID_FIELDS: [&'static str; 3] = [ Field::Secs.as_str(), Field::Nanos.as_str(), Field::Rand.as_str(), ]; pub struct NridVisitor; impl<'de> serde::de::Visitor<'de> for NridVisitor { type Value = Nrid; fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { formatter.write_str("struct Nrid") } fn visit_str(self, value: &str) -> Result where E: serde::de::Error, { Nrid::try_from(value).map_err(|e| serde::de::Error::custom(format!("{}", e))) } fn visit_seq(self, mut seq: V) -> Result where V: serde::de::SeqAccess<'de>, { let secs = seq .next_element()? .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; let nanos = seq .next_element()? .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; let rand = seq .next_element()? .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?; Nrid::new(secs, nanos, rand).map_err(|e| serde::de::Error::custom(format!("{}", e))) } fn visit_map(self, mut map: V) -> Result where V: serde::de::MapAccess<'de>, { let mut secs = None; let mut nanos = None; let mut rand = None; while let Some(key) = map.next_key()? { match key { Field::Secs => { if secs.is_some() { return Err(serde::de::Error::duplicate_field(key.as_str())); } secs = Some(map.next_value()?); } Field::Nanos => { if nanos.is_some() { return Err(serde::de::Error::duplicate_field(key.as_str())); } nanos = Some(map.next_value()?); } Field::Rand => { if rand.is_some() { return Err(serde::de::Error::duplicate_field(key.as_str())); } rand = Some(map.next_value()?); } } } let secs = secs.ok_or_else(|| serde::de::Error::missing_field(Field::Secs.as_str()))?; let nanos = nanos.ok_or_else(|| serde::de::Error::missing_field(Field::Nanos.as_str()))?; let rand = rand.ok_or_else(|| serde::de::Error::missing_field(Field::Rand.as_str()))?; Nrid::new(secs, nanos, rand).map_err(|e| serde::de::Error::custom(format!("{}", e))) } } } #[cfg(feature = "serde")] impl serde::Serialize for Nrid { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { if serializer.is_human_readable() { serializer.serialize_str(&self.to_string()) } else { use serde::ser::SerializeStruct; let mut s = serializer.serialize_struct("Nrid", 3)?; s.serialize_field(serde_mod::Field::Secs.as_str(), &self.secs())?; s.serialize_field(serde_mod::Field::Nanos.as_str(), &self.nanos())?; s.serialize_field(serde_mod::Field::Rand.as_str(), &self.rand())?; s.end() } } } #[cfg(feature = "serde")] impl<'de> serde::Deserialize<'de> for Nrid { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { if deserializer.is_human_readable() { deserializer.deserialize_str(serde_mod::NridVisitor) } else { deserializer.deserialize_struct("Nrid", &serde_mod::NRID_FIELDS, serde_mod::NridVisitor) } } } #[cfg(feature = "chrono")] impl TryFrom for chrono::DateTime { type Error = error::ConversionError; fn try_from(value: Nrid) -> Result { if value.secs() > i64::MAX as u64 { return Err(error::ConversionError( concat!( stringify!(chrono::DateTime), " cannot handle secs greater than ", stringify!(i64::MAX) ) .into(), )); } Ok(chrono::DateTime::from_utc( chrono::NaiveDateTime::from_timestamp_opt(value.secs() as i64, value.nanos()) .ok_or_else(|| { error::ConversionError( concat!( stringify!(chrono::DateTime), " cannot handle the value" ) .into(), ) })?, chrono::Utc, )) } } #[cfg(feature = "time")] impl TryFrom for time::OffsetDateTime { type Error = error::ConversionError; fn try_from(value: Nrid) -> Result { if value.secs() > i64::MAX as u64 { return Err(error::ConversionError( concat!( stringify!(time::OffsetDateTime), " cannot handle secs greater than ", stringify!(i64::MAX) ) .into(), )); } Ok( time::OffsetDateTime::from_unix_timestamp(value.secs() as i64) .map_err(|e| error::ConversionError(e.to_string().into()))? .replace_nanosecond(value.nanos()) .map_err(|e| error::ConversionError(e.to_string().into()))?, ) } } const SECONDS_LEN: usize = 12; const NANOSECONDS_LEN: usize = 9; const RANDOMNESS_LEN: usize = 9; const SECONDS_OFFSET: usize = 0; const NANOSECONDS_OFFSET: usize = SECONDS_OFFSET + SECONDS_LEN + 1; const RANDOMNESS_OFFSET: usize = NANOSECONDS_OFFSET + NANOSECONDS_LEN + 1; impl FromStr for Nrid { type Err = error::Parse; fn from_str(value: &str) -> std::result::Result { if value.len() != (SECONDS_LEN + 1 + NANOSECONDS_LEN + 1 + RANDOMNESS_LEN) { return Err(error::Parse::Length(value.len())); } if &value[NANOSECONDS_OFFSET - 1..NANOSECONDS_OFFSET] != "-" { return Err(error::Parse::Separator(NANOSECONDS_OFFSET - 1)); } if &value[RANDOMNESS_OFFSET - 1..RANDOMNESS_OFFSET] != "-" { return Err(error::Parse::Separator(RANDOMNESS_OFFSET - 1)); } match u64::from_str_radix(&value[SECONDS_OFFSET..SECONDS_OFFSET + SECONDS_LEN], 16) { Ok(secs) => match u32::from_str_radix( &value[NANOSECONDS_OFFSET..NANOSECONDS_OFFSET + NANOSECONDS_LEN], 16, ) { Ok(nanos) => match u32::from_str_radix( &value[RANDOMNESS_OFFSET..RANDOMNESS_OFFSET + RANDOMNESS_LEN], 16, ) { Ok(rand) => Ok(Self::new(secs, nanos, rand)?), Err(e) => Err(error::Segment::new("rand", 9, 0, u32::MAX.into(), e).into()), }, Err(e) => Err(error::Segment::new("nanos", 9, 0, 999_999_999, e).into()), }, Err(e) => Err(error::Segment::new("secs", 12, 0, u64::MAX, e).into()), } } } impl TryFrom<&str> for Nrid { type Error = error::Parse; #[inline] fn try_from(value: &str) -> std::result::Result { Self::from_str(value) } } #[cfg(test)] mod test { use std::convert::TryFrom; use super::{error, Nrid}; const NRID_STRING: &'static str = "00000d0525a4-0000052fa-000069540"; const NRID: Result = Nrid::new(218441124, 21242, 431424); #[test] fn parse_nrid() { assert_eq!( Nrid::try_from(NRID_STRING).unwrap(), NRID.expect("invalid Nrid") ); } #[test] fn serialize_nrid() { assert_eq!(NRID.expect("invalid Nrid").to_string(), NRID_STRING); } }