This repository has been archived on 2022-05-30. You can view files and clone it, but cannot push or open issues or pull requests.
nrid.rs/src/lib.rs

222 lines
6.5 KiB
Rust

/*!
* 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 core::cmp::max;
use core::cmp::min;
use core::convert::TryFrom;
/// rules: consist of 12 hex characters, a hyphen, 9 hex characters, a hyphen, and 9 hex characters
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum NridError {
#[error("must be a total of 32 characters long")]
WrongLength,
#[error("must consist of hexadecimal characters and hyphens")]
IllegalCharacters,
#[error("segment {0} must be {1} hex characters long")]
WrongSegmentLength(u8, u8),
}
/// Nano-Random IDentifier. Stores the seconds since the epoch and nanoseconds since the second, along with 4 bytes of randomness.
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub struct Nrid {
seconds: u64,
nanoseconds: u32,
randomness: u32,
}
impl Nrid {
/// Returns a new `Nrid` with its timestamp taken from [`Utc::now`].
#[inline]
pub fn now() -> Self {
let now = chrono::Utc::now();
use rand::RngCore;
Self {
// we ensure that the seconds value is not negative before casting out of an abundance of safety.
seconds: max(now.timestamp(), 0) as u64,
nanoseconds: now.timestamp_subsec_nanos(),
randomness: rand::rngs::OsRng.next_u32(),
}
}
}
impl core::fmt::Debug for Nrid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(
f,
"{:012x}-{:09x}-{:09x}",
self.seconds, self.nanoseconds, self.randomness
)
}
}
impl core::fmt::Display for Nrid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(
f,
"{:012x}-{:09x}-{:09x}",
self.seconds, self.nanoseconds, self.randomness
)
}
}
impl From<Nrid> for String {
fn from(value: Nrid) -> Self {
value.to_string()
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Nrid {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
#[cfg(feature = "serde")]
struct NridVisitor;
#[cfg(feature = "serde")]
impl<'de> serde::de::Visitor<'de> for NridVisitor {
type Value = Nrid;
fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str(&format!("a string that must consist of 12 hex characters, a hyphen, 9 hex characters, a hyphen, and 9 hex characters"))
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match Nrid::try_from(value) {
Ok(nrid) => Ok(nrid),
Err(err) => Err(serde::de::Error::custom(format!("{}", err))),
}
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Nrid {
fn deserialize<D>(deserializer: D) -> Result<Nrid, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(NridVisitor)
}
}
impl From<Nrid> for chrono::DateTime<chrono::Utc> {
fn from(value: Nrid) -> Self {
// we ensure that the seconds value is not greater than the maximum i64, rather than letting the value overflow to 0 (IIRC).
chrono::DateTime::from_utc(
chrono::NaiveDateTime::from_timestamp(
min(value.seconds, i64::MAX as u64) as i64,
value.nanoseconds,
),
chrono::Utc,
)
}
}
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 TryFrom<&str> for Nrid {
type Error = NridError;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
let value_len = value.len();
if value_len != (SECONDS_LEN + 1 + NANOSECONDS_LEN + 1 + RANDOMNESS_LEN) {
return Err(NridError::WrongLength);
}
for (i, c) in value.chars().enumerate() {
if i == NANOSECONDS_OFFSET-1 || i == RANDOMNESS_OFFSET-1 {
if c != '-' {
return Err(NridError::IllegalCharacters);
}
} else {
if !char::is_ascii_hexdigit(&c) {
return Err(NridError::IllegalCharacters);
}
}
}
let sec = &value[SECONDS_OFFSET..SECONDS_OFFSET+SECONDS_LEN];
match u64::from_str_radix(sec, 16) {
Ok(sec) => {
let nsec = &value[NANOSECONDS_OFFSET..NANOSECONDS_OFFSET+NANOSECONDS_LEN];
match u32::from_str_radix(nsec, 16) {
Ok(nsec) => {
let rand = &value[RANDOMNESS_OFFSET..RANDOMNESS_OFFSET+RANDOMNESS_LEN];
match u32::from_str_radix(rand, 16) {
Ok(rand) => Ok(Self {
seconds: sec,
nanoseconds: nsec,
randomness: rand,
}),
Err(_) => {
return Err(NridError::WrongSegmentLength(3, 9));
}
}
}
Err(_) => {
return Err(NridError::WrongSegmentLength(2, 9));
}
}
}
Err(_) => {
return Err(NridError::WrongSegmentLength(1, 12));
}
}
}
}
impl core::str::FromStr for Nrid {
type Err = NridError;
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
Self::try_from(value)
}
}
#[cfg(test)]
mod test {
use core::convert::TryFrom;
use super::Nrid;
const NRID_STRING: &'static str = "00000d0525a4-0000052fa-000069540";
const NRID: Nrid = Nrid {
seconds: 218441124,
nanoseconds: 21242,
randomness: 431424,
};
#[test]
fn parse_nrid() {
assert_eq!(Nrid::try_from(NRID_STRING).unwrap(), NRID);
}
#[test]
fn serialize_nrid() {
assert_eq!(NRID.to_string(), NRID_STRING);
}
}