Initial commit
This commit is contained in:
commit
35c41c2a95
|
@ -0,0 +1 @@
|
|||
/target
|
|
@ -0,0 +1,271 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[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",
|
||||
"serde",
|
||||
"time",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
|
||||
|
||||
[[package]]
|
||||
name = "nrid"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"rand",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[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 = "pin-project-lite"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439697af366c49a6d0a010c56a0d97685bc140ce0d377b13a2ea2aa42d64a827"
|
||||
|
||||
[[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.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_hc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||
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 0.10.0+wasi-snapshot-preview1",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80e0ccfc3378da0cce270c946b676a376943f5cd16aeba64568e7939806f4ada"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[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.9.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||
|
||||
[[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"
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "nrid"
|
||||
version = "0.1.0"
|
||||
authors = ["Michael Pfaff <michael@pfaff.dev>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.11", features = ["serde"] }
|
||||
rand = { version = "0.7.3" }
|
||||
#regex = { version = "1.3" }
|
||||
serde = { version = "1" }
|
||||
thiserror = { version = "1" }
|
||||
tracing = { version = "0.1.15" }
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# 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.
|
||||
|
|
@ -0,0 +1,322 @@
|
|||
/*!
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
/*!
|
||||
* Provides traits and helpers for generating and manipulating written lanuage constructs
|
||||
*/
|
||||
|
||||
/// A type that can be compiled into a sentence.
|
||||
pub trait ToSentence {
|
||||
/// Sentencifies self. The result should be suitable to be placed within a sentence.
|
||||
fn to_sentence(&self) -> String;
|
||||
|
||||
/// Sentencifies self, upper-casing the first letter and adding a period to the end. Using this is discouraged due to its lack of flexibility (i.e. needing alternate punctuation or a lowercase first letter).
|
||||
fn to_sentence_finalize(&self) -> String {
|
||||
let s = self.to_sentence();
|
||||
let mut c = s.chars();
|
||||
match s.len() {
|
||||
0 => format!(""),
|
||||
1 => format!("{}.", s.to_uppercase()),
|
||||
_ => format!("{}{}.", c.nth(0).unwrap().to_uppercase(), c.as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> ToSentence for T where T: AsRef<[&'a str]> {
|
||||
fn to_sentence(&self) -> String {
|
||||
let s = self.as_ref();
|
||||
let len = s.len();
|
||||
let mut str = String::new();
|
||||
for (i, seg) in s.iter().map(|s| s.as_ref()).enumerate() {
|
||||
if i == 0 {
|
||||
str.push_str(seg);
|
||||
} else if i == len-1 {
|
||||
if len == 2 {
|
||||
str.push_str(&format!(" and {}", seg));
|
||||
} else {
|
||||
str.push_str(&format!(", and {}", seg));
|
||||
}
|
||||
} else {
|
||||
str.push_str(&format!(", {}", seg));
|
||||
}
|
||||
}
|
||||
str
|
||||
}
|
||||
}
|
||||
|
||||
// impl<T> ToSentence for T where T: AsRef<[String]> {
|
||||
// fn to_sentence(&self) -> String {
|
||||
// let s = self.as_ref();
|
||||
// let len = s.len();
|
||||
// let mut str = String::new();
|
||||
// for (i, seg) in s.iter().map(|s| s.as_ref()).enumerate() {
|
||||
// if i == 0 {
|
||||
// str.push_str(seg);
|
||||
// } else if i == len {
|
||||
// if len == 2 {
|
||||
// str.push_str(&format!(" and {}", seg));
|
||||
// } else {
|
||||
// str.push_str(&format!(", and {}", seg));
|
||||
// }
|
||||
// } else {
|
||||
// str.push_str(&format!(", {}", seg));
|
||||
// }
|
||||
// }
|
||||
// str
|
||||
// }
|
||||
// }
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ToSentence;
|
||||
|
||||
const STRING_SLICE_A: &'static [&'static str] = &[
|
||||
"a",
|
||||
];
|
||||
|
||||
const STRING_SLICE_B: &'static [&'static str] = &[
|
||||
"a",
|
||||
"b",
|
||||
];
|
||||
|
||||
const STRING_SLICE_C: &'static [&'static str] = &[
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g",
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn str_slice_to_sentence() {
|
||||
assert_eq!(STRING_SLICE_A.to_sentence(), "a");
|
||||
assert_eq!(STRING_SLICE_B.to_sentence(), "a and b");
|
||||
assert_eq!(STRING_SLICE_C.to_sentence(), "a, b, c, d, e, f, and g");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_slice_to_sentence_finalize() {
|
||||
assert_eq!(STRING_SLICE_A.to_sentence_finalize(), "A.");
|
||||
assert_eq!(STRING_SLICE_B.to_sentence_finalize(), "A and b.");
|
||||
assert_eq!(STRING_SLICE_C.to_sentence_finalize(), "A, b, c, d, e, f, and g.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_vec_to_sentence() {
|
||||
let vec: Vec<String> = vec!{
|
||||
"a".to_owned(),
|
||||
};
|
||||
|
||||
assert_eq!(vec.iter().map(|e| e.as_ref()).collect::<Vec<&str>>().to_sentence(), "a");
|
||||
|
||||
let vec: Vec<String> = vec!{
|
||||
"a".to_owned(),
|
||||
"b".to_owned(),
|
||||
};
|
||||
|
||||
assert_eq!(vec.iter().map(|e| e.as_ref()).collect::<Vec<&str>>().to_sentence(), "a and b");
|
||||
|
||||
let vec: Vec<String> = vec!{
|
||||
"a".to_owned(),
|
||||
"b".to_owned(),
|
||||
"c".to_owned(),
|
||||
"d".to_owned(),
|
||||
"e".to_owned(),
|
||||
"f".to_owned(),
|
||||
"g".to_owned(),
|
||||
};
|
||||
|
||||
assert_eq!(vec.iter().map(|e| e.as_ref()).collect::<Vec<&str>>().to_sentence(), "a, b, c, d, e, f, and g");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_vec_to_sentence_finalize() {
|
||||
let vec: Vec<String> = vec!{
|
||||
"a".to_owned(),
|
||||
};
|
||||
|
||||
assert_eq!(vec.iter().map(|e| e.as_ref()).collect::<Vec<&str>>().to_sentence_finalize(), "A.");
|
||||
|
||||
let vec: Vec<String> = vec!{
|
||||
"a".to_owned(),
|
||||
"b".to_owned(),
|
||||
};
|
||||
|
||||
assert_eq!(vec.iter().map(|e| e.as_ref()).collect::<Vec<&str>>().to_sentence_finalize(), "A and b.");
|
||||
|
||||
let vec: Vec<String> = vec!{
|
||||
"a".to_owned(),
|
||||
"b".to_owned(),
|
||||
"c".to_owned(),
|
||||
"d".to_owned(),
|
||||
"e".to_owned(),
|
||||
"f".to_owned(),
|
||||
"g".to_owned(),
|
||||
};
|
||||
|
||||
assert_eq!(vec.iter().map(|e| e.as_ref()).collect::<Vec<&str>>().to_sentence_finalize(), "A, b, c, d, e, f, and g.");
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue