use std::path::Path;
use moid::{Alphabet, SeededRng};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Id(pub String);
impl Id {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
pub const BLADE_RANDOM_LEN: usize = 6;
pub const BLADE_LEN: usize = BLADE_RANDOM_LEN + 1;
fn canonical_minter() -> moid::Minter {
moid::Minter::new(Alphabet::betanumeric(), BLADE_RANDOM_LEN)
}
pub fn verify(id: &str) -> bool {
canonical_minter().validate(id).is_ok()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Registration {
pub on_create: bool,
pub on_link: bool,
pub on_publish: bool,
}
impl Registration {
pub const OFF: Self = Self {
on_create: false,
on_link: false,
on_publish: false,
};
pub const LAZY: Self = Self {
on_create: false,
on_link: true,
on_publish: true,
};
pub const EAGER: Self = Self {
on_create: true,
on_link: true,
on_publish: true,
};
pub fn is_active(&self) -> bool {
self.on_create || self.on_link || self.on_publish
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Trigger {
Create,
Link,
Publish,
}
impl Registration {
pub fn fires_on(&self, event: Trigger) -> bool {
match event {
Trigger::Create => self.on_create,
Trigger::Link => self.on_link,
Trigger::Publish => self.on_publish,
}
}
}
pub trait IdentityPolicy {
fn registration(&self) -> Registration;
fn mint(&mut self, path: &Path) -> Id;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NoIdentity;
impl IdentityPolicy for NoIdentity {
fn registration(&self) -> Registration {
Registration::OFF
}
fn mint(&mut self, _path: &Path) -> Id {
Id(String::new())
}
}
#[derive(Debug, Clone)]
pub struct Minter {
registration: Registration,
minter: moid::Minter,
rng: SeededRng,
}
impl Minter {
pub fn lazy(seed: u64) -> Self {
Self::with(Registration::LAZY, seed)
}
pub fn eager(seed: u64) -> Self {
Self::with(Registration::EAGER, seed)
}
pub fn with(registration: Registration, seed: u64) -> Self {
Self {
registration,
minter: canonical_minter(),
rng: SeededRng::new(seed),
}
}
}
impl IdentityPolicy for Minter {
fn registration(&self) -> Registration {
self.registration
}
fn mint(&mut self, _path: &Path) -> Id {
Id(self.minter.mint_seeded(&mut self.rng))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_identity_is_off() {
assert!(!NoIdentity.registration().is_active());
}
#[test]
fn lazy_registers_on_link_and_publish_only() {
let r = Minter::lazy(1).registration();
assert!(!r.fires_on(Trigger::Create));
assert!(r.fires_on(Trigger::Link));
assert!(r.fires_on(Trigger::Publish));
}
#[test]
fn eager_registers_on_create() {
assert!(Minter::eager(1).registration().fires_on(Trigger::Create));
}
#[test]
fn mints_verified_distinct_opaque_ids() {
let mut p = Minter::eager(42);
let a = p.mint(Path::new("a.md"));
let b = p.mint(Path::new("b.md"));
assert_ne!(a, b);
for id in [&a, &b] {
assert_eq!(id.as_str().len(), BLADE_LEN);
assert!(verify(id.as_str()), "{id}");
}
}
#[test]
fn same_seed_is_deterministic() {
let a = Minter::lazy(7).mint(Path::new("x"));
let b = Minter::lazy(7).mint(Path::new("y"));
assert_eq!(a, b, "path does not participate in the mint");
}
#[test]
fn verify_rejects_typos() {
let id = Minter::lazy(3).mint(Path::new("x")).0;
assert!(verify(&id));
let mut chars: Vec<char> = id.chars().collect();
chars[0] = if chars[0] == 'b' { 'c' } else { 'b' };
let typo: String = chars.iter().collect();
assert!(!verify(&typo), "{typo}");
assert!(!verify("bcd"));
assert!(!verify("aeiouAy"));
}
#[test]
fn check_char_matches_the_ark_lineage() {
assert_eq!(Alphabet::betanumeric().check_char("bcdfgh"), 't');
assert!(verify("bcdfght"));
}
}