Compare commits
3 Commits
315677cbfc
...
ef7669e003
Author | SHA1 | Date |
---|---|---|
Michael Pfaff | ef7669e003 | |
Michael Pfaff | eec8cfa79a | |
Michael Pfaff | bad337d9d7 |
|
@ -1 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
|
|
|
@ -1,220 +0,0 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"time",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
|
||||
|
||||
[[package]]
|
||||
name = "nrid"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"rand",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_hc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bdd36f49e35b61d49efd8aa7fc068fd295961fd2286d0b2ee9a4c7a14e99cc3"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
19
Cargo.toml
19
Cargo.toml
|
@ -1,16 +1,21 @@
|
|||
[package]
|
||||
name = "nrid"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
authors = ["Michael Pfaff <michael@pfaff.dev>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[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]
|
||||
chrono = "0.4.19"
|
||||
rand = "0.8.3"
|
||||
serde = { version = "1", optional = true }
|
||||
thiserror = "1"
|
||||
chrono = { version = "0.4.19", default-features = false, optional = true }
|
||||
rand = { version = "0.8.5", default-features = false, features = [ "getrandom" ] }
|
||||
serde = { version = "1", default-features = false, features = [ "derive" ], optional = true }
|
||||
thiserror = { version = "1", default-features = false }
|
||||
time = { version = "0.3.9", default-features = false, features = [ "std" ] }
|
||||
|
||||
|
|
489
src/lib.rs
489
src/lib.rs
|
@ -4,70 +4,308 @@
|
|||
* 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;
|
||||
use std::convert::TryFrom;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// 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,
|
||||
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,
|
||||
|
||||
#[error("must consist of hexadecimal characters and hyphens")]
|
||||
IllegalCharacters,
|
||||
/// The minimum value for the segment, inclusive.
|
||||
pub minimum: u64,
|
||||
|
||||
#[error("segment {0} must be {1} hex characters long")]
|
||||
WrongSegmentLength(u8, u8),
|
||||
/// 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>);
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
#[repr(C)]
|
||||
pub struct Nrid {
|
||||
seconds: u64,
|
||||
nanoseconds: u32,
|
||||
randomness: u32,
|
||||
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`].
|
||||
#[inline]
|
||||
pub fn now() -> Self {
|
||||
let now = chrono::Utc::now();
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
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(),
|
||||
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 core::fmt::Debug for Nrid {
|
||||
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.seconds, self.nanoseconds, self.randomness
|
||||
self.secs(),
|
||||
self.nanos(),
|
||||
self.rand()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Nrid {
|
||||
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.seconds, self.nanoseconds, self.randomness
|
||||
self.secs(),
|
||||
self.nanos(),
|
||||
self.rand()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Nrid> for String {
|
||||
fn from(value: Nrid) -> Self {
|
||||
value.to_string()
|
||||
#[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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,29 +315,15 @@ impl serde::Serialize for Nrid {
|
|||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
if serializer.is_human_readable() {
|
||||
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))),
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -110,19 +334,65 @@ impl<'de> serde::Deserialize<'de> for Nrid {
|
|||
where
|
||||
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> {
|
||||
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,
|
||||
),
|
||||
#[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()))?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -132,90 +402,71 @@ 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;
|
||||
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;
|
||||
impl FromStr for Nrid {
|
||||
type Err = error::Parse;
|
||||
|
||||
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);
|
||||
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()));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
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 core::str::FromStr for Nrid {
|
||||
type Err = NridError;
|
||||
impl TryFrom<&str> for Nrid {
|
||||
type Error = error::Parse;
|
||||
|
||||
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
|
||||
Self::try_from(value)
|
||||
#[inline]
|
||||
fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
||||
Self::from_str(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(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: Nrid = Nrid {
|
||||
seconds: 218441124,
|
||||
nanoseconds: 21242,
|
||||
randomness: 431424,
|
||||
};
|
||||
const NRID: Result<Nrid, error::SegmentRange> = Nrid::new(218441124, 21242, 431424);
|
||||
|
||||
#[test]
|
||||
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]
|
||||
fn serialize_nrid() {
|
||||
assert_eq!(NRID.to_string(), NRID_STRING);
|
||||
assert_eq!(NRID.expect("invalid Nrid").to_string(), NRID_STRING);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Reference in New Issue