Overhaul the crate

This commit is contained in:
Michael Pfaff 2022-05-30 13:32:36 -04:00
parent bad337d9d7
commit eec8cfa79a
Signed by: michael
GPG Key ID: CF402C4A012AA9D4
2 changed files with 399 additions and 143 deletions

View File

@ -2,15 +2,20 @@
name = "nrid" name = "nrid"
version = "0.2.0" version = "0.2.0"
authors = ["Michael Pfaff <michael@pfaff.dev>"] authors = ["Michael Pfaff <michael@pfaff.dev>"]
edition = "2018" edition = "2021"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
[features] [features]
default = ["serde"] default = [ ]
chrono = [ "dep:chrono" ]
serde = [ "dep:serde" ]
# the source of the timestamp is an implementation detail, so a feature is required for the From impl
time = [ ]
[dependencies] [dependencies]
chrono = "0.4.19" chrono = { version = "0.4.19", default-features = false, optional = true }
rand = "0.8.3" rand = { version = "0.8.5", default-features = false, features = [ "getrandom" ] }
serde = { version = "1", optional = true } serde = { version = "1", default-features = false, features = [ "derive" ], optional = true }
thiserror = "1" thiserror = { version = "1", default-features = false }
time = { version = "0.3.9", default-features = false, features = [ "std" ] }

View File

@ -1,73 +1,311 @@
/*! /*!
* Nano-Random IDentifier. * 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. * 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 std::convert::TryFrom;
use core::cmp::min; use std::str::FromStr;
use core::convert::TryFrom;
/// rules: consist of 12 hex characters, a hyphen, 9 hex characters, a hyphen, and 9 hex characters pub mod error {
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] #[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum NridError { #[error("Value for segment '{segment}' must be range {minimum}..={maximum}.")]
#[error("must be a total of 32 characters long")] pub struct SegmentRange {
WrongLength, /// The segment in question.
pub segment: &'static str,
#[error("must consist of hexadecimal characters and hyphens")] /// The minimum value for the segment, inclusive.
IllegalCharacters, pub minimum: u64,
#[error("segment {0} must be {1} hex characters long")] /// The maximum value for the segment, inclusive.
WrongSegmentLength(u8, u8), 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<E> From<E> for Parse
where
E: Into<Segment>,
{
fn from(e: E) -> Self {
<E as Into<Segment>>::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>);
} }
/// Nano-Random IDentifier. Stores the seconds since the epoch and nanoseconds since the second, along with 4 bytes of randomness. /// **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)] #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)] #[repr(C)]
pub struct Nrid { pub struct Nrid {
seconds: u64, secs: u64,
nanoseconds: u32, nanos: u32,
randomness: u32, rand: u32,
} }
impl Nrid { impl Nrid {
pub const fn new(secs: u64, nanos: u32, rand: u32) -> Result<Self, error::SegmentRange> {
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`]. /// Returns a new `Nrid` with its timestamp taken from [`Utc::now`].
#[inline]
pub fn now() -> Self { pub fn now() -> Self {
let now = chrono::Utc::now(); let now = time::OffsetDateTime::now_utc();
use rand::RngCore; use rand::RngCore;
Self { assert!(
// we ensure that the seconds value is not negative before casting out of an abundance of safety. now.unix_timestamp() >= 0,
seconds: max(now.timestamp(), 0) as u64, "System time is before the Unix Epoch!"
nanoseconds: now.timestamp_subsec_nanos(), );
randomness: rand::rngs::OsRng.next_u32(), 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",
}
} }
} }
}
impl core::fmt::Debug for Nrid { pub const NRID_FIELDS: [&'static str; 3] = [
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { Field::Secs.as_str(),
write!( Field::Nanos.as_str(),
f, Field::Rand.as_str(),
"{:012x}-{:09x}-{:09x}", ];
self.seconds, self.nanoseconds, self.randomness
)
}
}
impl core::fmt::Display for Nrid { pub struct NridVisitor;
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 { impl<'de> serde::de::Visitor<'de> for NridVisitor {
fn from(value: Nrid) -> Self { type Value = Nrid;
value.to_string()
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("struct Nrid")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Nrid::try_from(value).map_err(|e| serde::de::Error::custom(format!("{}", e)))
}
fn visit_seq<V>(self, mut seq: V) -> Result<Nrid, V::Error>
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<V>(self, mut map: V) -> Result<Nrid, V::Error>
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)))
}
} }
} }
@ -77,29 +315,15 @@ impl serde::Serialize for Nrid {
where where
S: serde::Serializer, S: serde::Serializer,
{ {
serializer.serialize_str(&self.to_string()) if serializer.is_human_readable() {
} serializer.serialize_str(&self.to_string())
} } else {
use serde::ser::SerializeStruct;
#[cfg(feature = "serde")] let mut s = serializer.serialize_struct("Nrid", 3)?;
struct NridVisitor; 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())?;
#[cfg(feature = "serde")] s.end()
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))),
} }
} }
} }
@ -110,19 +334,65 @@ impl<'de> serde::Deserialize<'de> for Nrid {
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
deserializer.deserialize_str(NridVisitor) if deserializer.is_human_readable() {
deserializer.deserialize_str(serde_mod::NridVisitor)
} else {
deserializer.deserialize_struct("Nrid", &serde_mod::NRID_FIELDS, serde_mod::NridVisitor)
}
} }
} }
impl From<Nrid> for chrono::DateTime<chrono::Utc> { #[cfg(feature = "chrono")]
fn from(value: Nrid) -> Self { impl TryFrom<Nrid> for chrono::DateTime<chrono::Utc> {
// we ensure that the seconds value is not greater than the maximum i64, rather than letting the value overflow to 0 (IIRC). type Error = error::ConversionError;
chrono::DateTime::from_utc(
chrono::NaiveDateTime::from_timestamp( fn try_from(value: Nrid) -> Result<Self, Self::Error> {
min(value.seconds, i64::MAX as u64) as i64, if value.secs() > i64::MAX as u64 {
value.nanoseconds, return Err(error::ConversionError(
), concat!(
stringify!(chrono::DateTime<chrono::Utc>),
" 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<chrono::Utc>),
" cannot handle the value"
)
.into(),
)
})?,
chrono::Utc, chrono::Utc,
))
}
}
#[cfg(feature = "time")]
impl TryFrom<Nrid> for time::OffsetDateTime {
type Error = error::ConversionError;
fn try_from(value: Nrid) -> Result<Self, Self::Error> {
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()))?,
) )
} }
} }
@ -132,90 +402,71 @@ const NANOSECONDS_LEN: usize = 9;
const RANDOMNESS_LEN: usize = 9; const RANDOMNESS_LEN: usize = 9;
const SECONDS_OFFSET: usize = 0; const SECONDS_OFFSET: usize = 0;
const NANOSECONDS_OFFSET: usize = SECONDS_OFFSET+SECONDS_LEN+1; const NANOSECONDS_OFFSET: usize = SECONDS_OFFSET + SECONDS_LEN + 1;
const RANDOMNESS_OFFSET: usize = NANOSECONDS_OFFSET+NANOSECONDS_LEN+1; const RANDOMNESS_OFFSET: usize = NANOSECONDS_OFFSET + NANOSECONDS_LEN + 1;
impl TryFrom<&str> for Nrid { impl FromStr for Nrid {
type Error = NridError; type Err = error::Parse;
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> { fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
let value_len = value.len(); if value.len() != (SECONDS_LEN + 1 + NANOSECONDS_LEN + 1 + RANDOMNESS_LEN) {
return Err(error::Parse::Length(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 &value[NANOSECONDS_OFFSET - 1..NANOSECONDS_OFFSET] != "-" {
if i == NANOSECONDS_OFFSET-1 || i == RANDOMNESS_OFFSET-1 { return Err(error::Parse::Separator(NANOSECONDS_OFFSET - 1));
if c != '-' { }
return Err(NridError::IllegalCharacters); if &value[RANDOMNESS_OFFSET - 1..RANDOMNESS_OFFSET] != "-" {
} return Err(error::Parse::Separator(RANDOMNESS_OFFSET - 1));
} 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(&value[SECONDS_OFFSET..SECONDS_OFFSET + SECONDS_LEN], 16) {
match u64::from_str_radix(sec, 16) { Ok(secs) => match u32::from_str_radix(
Ok(sec) => { &value[NANOSECONDS_OFFSET..NANOSECONDS_OFFSET + NANOSECONDS_LEN],
let nsec = &value[NANOSECONDS_OFFSET..NANOSECONDS_OFFSET+NANOSECONDS_LEN]; 16,
match u32::from_str_radix(nsec, 16) { ) {
Ok(nsec) => { Ok(nanos) => match u32::from_str_radix(
let rand = &value[RANDOMNESS_OFFSET..RANDOMNESS_OFFSET+RANDOMNESS_LEN]; &value[RANDOMNESS_OFFSET..RANDOMNESS_OFFSET + RANDOMNESS_LEN],
match u32::from_str_radix(rand, 16) { 16,
Ok(rand) => Ok(Self { ) {
seconds: sec, Ok(rand) => Ok(Self::new(secs, nanos, rand)?),
nanoseconds: nsec, Err(e) => Err(error::Segment::new("rand", 9, 0, u32::MAX.into(), e).into()),
randomness: rand, },
}), Err(e) => Err(error::Segment::new("nanos", 9, 0, 999_999_999, e).into()),
Err(_) => { },
return Err(NridError::WrongSegmentLength(3, 9)); Err(e) => Err(error::Segment::new("secs", 12, 0, u64::MAX, e).into()),
}
}
}
Err(_) => {
return Err(NridError::WrongSegmentLength(2, 9));
}
}
}
Err(_) => {
return Err(NridError::WrongSegmentLength(1, 12));
}
} }
} }
} }
impl core::str::FromStr for Nrid { impl TryFrom<&str> for Nrid {
type Err = NridError; type Error = error::Parse;
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> { #[inline]
Self::try_from(value) fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
Self::from_str(value)
} }
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use core::convert::TryFrom; use std::convert::TryFrom;
use super::Nrid; use super::{error, Nrid};
const NRID_STRING: &'static str = "00000d0525a4-0000052fa-000069540"; const NRID_STRING: &'static str = "00000d0525a4-0000052fa-000069540";
const NRID: Nrid = Nrid { const NRID: Result<Nrid, error::SegmentRange> = Nrid::new(218441124, 21242, 431424);
seconds: 218441124,
nanoseconds: 21242,
randomness: 431424,
};
#[test] #[test]
fn parse_nrid() { fn parse_nrid() {
assert_eq!(Nrid::try_from(NRID_STRING).unwrap(), NRID); assert_eq!(
Nrid::try_from(NRID_STRING).unwrap(),
NRID.expect("invalid Nrid")
);
} }
#[test] #[test]
fn serialize_nrid() { fn serialize_nrid() {
assert_eq!(NRID.to_string(), NRID_STRING); assert_eq!(NRID.expect("invalid Nrid").to_string(), NRID_STRING);
} }
} }