mod future;
mod guard;
mod key;
mod site;
use std::{cell::Cell, fmt};
pub use future::*;
pub use guard::*;
pub use key::*;
pub use site::*;
use topcoat_core::fnv1a::Fnv1a;
thread_local! {
static CURRENT: Cell<Option<Identity>> = const { Cell::new(None) };
}
const TAG_SITE: u8 = 0;
const TAG_KEYED: u8 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Identity {
hash: u128,
ambiguity: Option<&'static str>,
}
impl Identity {
pub const ROOT: Self = Self {
hash: 0,
ambiguity: None,
};
#[must_use]
#[track_caller]
pub fn current() -> Self {
match Self::try_current() {
Ok(identity) => identity,
Err(error) => panic!("{error}"),
}
}
pub fn try_current() -> Result<Self, AmbiguousIdentityError> {
let identity = Self::current_raw();
match identity.ambiguity {
None => Ok(identity),
Some(label) => Err(AmbiguousIdentityError { label }),
}
}
fn current_raw() -> Self {
CURRENT.get().unwrap_or(Self::ROOT)
}
#[must_use]
pub const fn hash(self) -> u128 {
self.hash
}
#[must_use]
pub const fn child(self, site: SiteKey) -> Self {
Self {
hash: self.derive(TAG_SITE, site).finish(),
ambiguity: self.ambiguity,
}
}
#[must_use]
pub fn keyed_child(self, site: SiteKey, key: impl IdentityKey) -> Self {
Self {
hash: key
.write(KeyHasher::new(self.derive(TAG_KEYED, site)))
.finish(),
ambiguity: self.ambiguity,
}
}
#[must_use]
pub const fn ambiguous_child(self, site: SiteKey, label: &'static str) -> Self {
Self {
hash: self.derive(TAG_SITE, site).finish(),
ambiguity: match self.ambiguity {
Some(existing) => Some(existing),
None => Some(label),
},
}
}
const fn derive(self, tag: u8, site: SiteKey) -> Fnv1a<u128> {
Fnv1a::<u128>::new()
.write(&self.hash.to_le_bytes())
.write(&[tag])
.write(&site.0.to_le_bytes())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AmbiguousIdentityError {
label: &'static str,
}
impl AmbiguousIdentityError {
#[must_use]
pub const fn label(&self) -> &'static str {
self.label
}
}
impl fmt::Display for AmbiguousIdentityError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ambiguous component identity: {} repeats without a `key` argument; \
pass `key:` to give each repetition its own identity",
self.label,
)
}
}
impl std::error::Error for AmbiguousIdentityError {}
#[cfg(test)]
mod tests {
use super::*;
const SITE_A: SiteKey = SiteKey::new(file!(), line!(), column!(), 0);
const SITE_B: SiteKey = SiteKey::new(file!(), line!(), column!(), 0);
#[test]
fn current_is_root_outside_any_component() {
assert_eq!(Identity::current(), Identity::ROOT);
assert_eq!(Identity::try_current(), Ok(Identity::ROOT));
}
#[test]
fn derivation_is_deterministic() {
assert_eq!(Identity::ROOT.child(SITE_A), Identity::ROOT.child(SITE_A));
assert_ne!(Identity::ROOT.child(SITE_A), Identity::ROOT.child(SITE_B));
assert_ne!(Identity::ROOT.child(SITE_A), Identity::ROOT);
}
#[test]
fn keys_tell_repetitions_of_one_site_apart() {
let root = Identity::ROOT;
assert_eq!(root.keyed_child(SITE_A, 1), root.keyed_child(SITE_A, 1));
assert_ne!(root.keyed_child(SITE_A, 1), root.keyed_child(SITE_A, 2));
}
#[test]
fn the_site_stays_mixed_into_a_keyed_identity() {
let root = Identity::ROOT;
assert_ne!(root.keyed_child(SITE_A, 1), root.keyed_child(SITE_B, 1));
}
#[test]
fn keyed_and_unkeyed_children_never_collide() {
let root = Identity::ROOT;
assert_ne!(root.child(SITE_A), root.keyed_child(SITE_A, ""));
}
#[test]
fn ambiguity_poisons_keyed_descendants() {
let poisoned = Identity::ROOT.ambiguous_child(SITE_A, "outer");
assert_eq!(poisoned.keyed_child(SITE_B, 7).ambiguity, Some("outer"));
assert_eq!(poisoned.child(SITE_B).ambiguity, Some("outer"));
}
#[test]
fn the_outermost_ambiguity_wins() {
let poisoned = Identity::ROOT
.ambiguous_child(SITE_A, "outer")
.ambiguous_child(SITE_B, "inner");
assert_eq!(poisoned.ambiguity, Some("outer"));
}
}