use std::path::Path;
use moid::Alphabet;
use moid::SeededRng;
pub use prov_graph::identity::{BLADE_LEN, BLADE_RANDOM_LEN, Id, verify};
fn canonical_minter() -> moid::Minter {
moid::Minter::new(Alphabet::noid_xdigit(), BLADE_RANDOM_LEN)
}
pub const WORKSPACE_NAME_RANDOM_LEN: usize = 12;
pub const WORKSPACE_NAME_LEN: usize = WORKSPACE_NAME_RANDOM_LEN + 1;
pub fn mint_workspace_id(seed: u64) -> String {
moid::Minter::new(Alphabet::noid_xdigit(), WORKSPACE_NAME_RANDOM_LEN)
.mint_seeded(&mut SeededRng::new(seed))
}
#[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::*;
use moid::Alphabet;
#[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 mints_wide_opaque_workspace_names() {
let a = mint_workspace_id(42);
let b = mint_workspace_id(43);
assert_ne!(a, b);
for name in [&a, &b] {
assert_eq!(name.chars().count(), WORKSPACE_NAME_LEN);
assert!(!name.is_empty());
assert!(
!name
.chars()
.any(|c| c == '/' || c == ':' || c.is_whitespace()),
"{name} cannot be written as a reference qualifier"
);
}
}
const _: () = assert!(WORKSPACE_NAME_LEN > BLADE_LEN);
#[test]
fn a_workspace_name_is_wider_than_a_document_id() {
assert!(
mint_workspace_id(1).chars().count() > Minter::lazy(1).mint(Path::new("x")).0.len()
);
}
#[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"));
assert!(!verify("bcdfghy"));
}
#[test]
fn check_char_matches_the_noid_lineage() {
assert_eq!(Alphabet::noid_xdigit().check_char("bcdfgh"), 'n');
assert!(verify("bcdfghn"));
}
#[test]
fn an_id_may_be_all_digits() {
let check = Alphabet::noid_xdigit().check_char("012345");
assert!(verify(&format!("012345{check}")));
}
mod properties {
use super::*;
use proptest::prelude::*;
const XDIGIT: &str = "0123456789bcdfghjkmnpqrstvwxz";
fn minted() -> impl Strategy<Value = String> {
any::<u64>().prop_map(|seed| Minter::lazy(seed).mint(Path::new("x")).0)
}
proptest! {
#[test]
fn every_minted_id_verifies_and_is_the_declared_length(id in minted()) {
prop_assert_eq!(id.chars().count(), BLADE_LEN);
prop_assert!(verify(&id), "{id}");
}
#[test]
fn every_minted_character_is_in_the_alphabet(id in minted()) {
for c in id.chars() {
prop_assert!(XDIGIT.contains(c), "`{c}` of `{id}` is not an xdigit");
}
}
#[test]
fn no_single_character_slip_survives_verification(
id in minted(),
position in 0..BLADE_LEN,
replacement in 0..XDIGIT.chars().count(),
) {
let alphabet: Vec<char> = XDIGIT.chars().collect();
let mut chars: Vec<char> = id.chars().collect();
let replacement = alphabet[replacement];
prop_assume!(chars[position] != replacement);
chars[position] = replacement;
let typo: String = chars.into_iter().collect();
prop_assert!(
!verify(&typo),
"`{typo}` is one character from `{id}` and still verified"
);
}
#[test]
fn no_adjacent_transposition_survives_verification(
id in minted(),
position in 0..BLADE_LEN - 1,
) {
let mut chars: Vec<char> = id.chars().collect();
prop_assume!(chars[position] != chars[position + 1]);
chars.swap(position, position + 1);
let swapped: String = chars.into_iter().collect();
prop_assert!(
!verify(&swapped),
"`{swapped}` transposes two characters of `{id}` and still verified"
);
}
}
}
}