323 lines
10 KiB
Rust
323 lines
10 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.
|
|
*/
|
|
|
|
#[macro_use]
|
|
extern crate tracing;
|
|
|
|
use core::cmp::max;
|
|
use core::cmp::min;
|
|
use core::convert::TryFrom;
|
|
|
|
mod sentence;
|
|
|
|
/// Nano Unique 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 `ObjectKey` 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()
|
|
}
|
|
}
|
|
|
|
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())
|
|
}
|
|
}
|
|
|
|
struct NridVisitor;
|
|
|
|
impl<'de> serde::de::Visitor<'de> for NridVisitor {
|
|
type Value = Nrid;
|
|
|
|
fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
use crate::sentence::ToSentence;
|
|
formatter.write_str(&format!("a string that must {}", NRID_RULES.to_sentence()))
|
|
}
|
|
|
|
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))),
|
|
}
|
|
}
|
|
}
|
|
|
|
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,
|
|
)
|
|
}
|
|
}
|
|
|
|
// commented out because it can't be deterministic due to the randomness in a Nrid not being representable in a DateTime.
|
|
// impl From<&DateTime<Utc>> for Nrid {
|
|
// #[inline]
|
|
// fn from(value: &DateTime<Utc>) -> Self {
|
|
// // let value = value.borrow();
|
|
// Self(
|
|
// // we ensure that the seconds value is not negative before casting out of an abundance of safety.
|
|
// max(value.timestamp(), 0) as u64,
|
|
// value.timestamp_subsec_nanos(),
|
|
// )
|
|
// }
|
|
// }
|
|
|
|
const NRID_RULES: &'static [&'static str] =
|
|
&["consist of 12 hex characters, a hyphen, 9 hex characters, a hyphen, and 9 hex characters"];
|
|
const NRID_LEN_RULES: &'static [&'static str] = &["be 32 characters long"];
|
|
const NRID_CHARS_RULES: &'static [&'static str] = &["consist of hexadecimal characters and hyphens"];
|
|
|
|
const SECONDS_RULES: &'static [&'static str] = &["be 12 hex characters"];
|
|
const NANOSECONDS_RULES: &'static [&'static str] = &["be 9 hex characters"];
|
|
const RANDOMNESS_RULES: &'static [&'static str] = &["be 9 hex characters"];
|
|
|
|
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 = ValidationError;
|
|
|
|
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(ValidationError::Rules {
|
|
rules: NRID_LEN_RULES,
|
|
offending_segment: value.to_string(),
|
|
}
|
|
.into());
|
|
}
|
|
|
|
for (i, c) in value.chars().enumerate() {
|
|
if i == NANOSECONDS_OFFSET-1 || i == RANDOMNESS_OFFSET-1 {
|
|
if c != '-' {
|
|
return Err(ValidationError::Rules {
|
|
rules: NRID_CHARS_RULES,
|
|
offending_segment: c.to_string(),
|
|
}
|
|
.into());
|
|
}
|
|
} else {
|
|
if !char::is_ascii_hexdigit(&c) {
|
|
return Err(ValidationError::Rules {
|
|
rules: NRID_CHARS_RULES,
|
|
offending_segment: c.to_string(),
|
|
}
|
|
.into());
|
|
}
|
|
}
|
|
}
|
|
|
|
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(err) => {
|
|
trace!(error = ?err, "parse randomness failed");
|
|
return Err(ValidationError::Rules {
|
|
rules: RANDOMNESS_RULES,
|
|
offending_segment: rand.to_string(),
|
|
}
|
|
.into());
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
trace!(error = ?err, "parse nanoseconds failed");
|
|
return Err(ValidationError::Rules {
|
|
rules: NANOSECONDS_RULES,
|
|
offending_segment: nsec.to_string(),
|
|
}
|
|
.into());
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
trace!(error = ?err, "parse seconds failed");
|
|
return Err(ValidationError::Rules {
|
|
rules: SECONDS_RULES,
|
|
offending_segment: sec.to_string(),
|
|
}
|
|
.into());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl core::str::FromStr for Nrid {
|
|
type Err = ValidationError;
|
|
|
|
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
|
|
Self::try_from(value)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
|
pub enum ValidationError {
|
|
/// Length of input was not as expected.
|
|
LengthEqual {
|
|
/// The expected input length.
|
|
expected: usize,
|
|
/// The input segment that failed validation.
|
|
offending_segment: String,
|
|
},
|
|
/// Length of input was greater than expected.
|
|
LengthMax {
|
|
/// The maximum expected input length.
|
|
max: usize,
|
|
/// The input segment that failed validation.
|
|
offending_segment: String,
|
|
},
|
|
/// Length of input was less than expected
|
|
LengthMin {
|
|
/// The minimum expected input length.
|
|
min: usize,
|
|
|
|
/// The input segment that failed validation.
|
|
offending_segment: String,
|
|
},
|
|
|
|
/// Input failed to match rules.
|
|
Rules {
|
|
/// The rules the input must follow.
|
|
rules: &'static [&'static str],
|
|
|
|
/// The input segment that failed validation.
|
|
offending_segment: String,
|
|
},
|
|
}
|
|
|
|
impl std::fmt::Display for ValidationError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
|
use crate::sentence::ToSentence;
|
|
match self {
|
|
Self::LengthEqual {
|
|
expected,
|
|
offending_segment,
|
|
} => write!(f, "`{}` is not equal to {}", offending_segment, expected),
|
|
Self::LengthMax {
|
|
max,
|
|
offending_segment,
|
|
} => write!(f, "`{}` is longer than {}", offending_segment, max),
|
|
Self::LengthMin {
|
|
min,
|
|
offending_segment,
|
|
} => write!(f, "`{}` is shorter than {}", offending_segment, min),
|
|
Self::Rules {
|
|
rules,
|
|
offending_segment,
|
|
} => write!(
|
|
f,
|
|
"`{}` must {} and fails one or more",
|
|
offending_segment,
|
|
rules.to_sentence()
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|
|
|