473 lines
14 KiB
Rust
473 lines
14 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 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<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>);
|
|
}
|
|
|
|
/// **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<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`].
|
|
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<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)))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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,
|
|
{
|
|
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<D>(deserializer: D) -> Result<Nrid, D::Error>
|
|
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<Nrid> for chrono::DateTime<chrono::Utc> {
|
|
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!(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,
|
|
))
|
|
}
|
|
}
|
|
|
|
#[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()))?,
|
|
)
|
|
}
|
|
}
|
|
|
|
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<Self, Self::Err> {
|
|
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, Self::Error> {
|
|
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, error::SegmentRange> = 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);
|
|
}
|
|
}
|