use std::collections::HashMap;
use std::sync::{Arc, Mutex};
pub(crate) const FACET_LABEL: &str = "facet";
#[derive(Clone, Default)]
pub struct FacetMap {
inner: Arc<Mutex<HashMap<String, String>>>,
}
impl FacetMap {
pub fn new() -> Self {
Self::default()
}
pub(crate) fn register(&self, revocation_id_hex: String, facet_uuid: String) {
let mut guard = self.inner.lock().expect("FacetMap mutex poisoned");
guard.insert(revocation_id_hex, facet_uuid);
}
pub(crate) fn lookup(&self, revocation_id_hex: &str) -> Option<String> {
let guard = self.inner.lock().expect("FacetMap mutex poisoned");
guard.get(revocation_id_hex).cloned()
}
pub(crate) fn verify_and_consume_atomic<F>(
&self,
revocation_id_hex: &str,
verify: F,
) -> Result<(), crate::EngineError>
where
F: FnOnce(Option<&str>) -> Result<(), crate::EngineError>,
{
let mut guard = self.inner.lock().expect("FacetMap mutex poisoned");
let facet = guard.get(revocation_id_hex).cloned();
let result = verify(facet.as_deref());
if result.is_ok() && facet.is_some() {
guard.remove(revocation_id_hex);
}
result
}
pub fn len(&self) -> usize {
self.inner.lock().expect("FacetMap mutex poisoned").len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl std::fmt::Debug for FacetMap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FacetMap")
.field("entries", &self.len())
.finish()
}
}
pub(crate) fn generate_facet_uuid() -> String {
uuid::Uuid::new_v4().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lookup_returns_registered_uuid() {
let map = FacetMap::new();
map.register("rev-a".into(), "uuid-1".into());
assert_eq!(map.lookup("rev-a").as_deref(), Some("uuid-1"));
assert!(map.lookup("rev-other").is_none());
}
#[test]
fn atomic_consume_removes_on_ok() {
let map = FacetMap::new();
map.register("rev-a".into(), "uuid-1".into());
let result = map.verify_and_consume_atomic("rev-a", |facet| {
assert_eq!(facet, Some("uuid-1"));
Ok(())
});
assert!(result.is_ok());
assert!(map.lookup("rev-a").is_none());
}
#[test]
fn atomic_consume_leaves_entry_on_err() {
let map = FacetMap::new();
map.register("rev-a".into(), "uuid-1".into());
let result = map.verify_and_consume_atomic("rev-a", |_facet| {
Err(crate::EngineError::TokenOperation("simulated".into()))
});
assert!(result.is_err());
assert_eq!(map.lookup("rev-a").as_deref(), Some("uuid-1"));
}
#[test]
fn atomic_helper_passes_none_when_entry_absent() {
let map = FacetMap::new();
let result = map.verify_and_consume_atomic("rev-missing", |facet| {
assert!(facet.is_none(), "helper hands None to the closure");
Ok(())
});
assert!(
result.is_ok(),
"closure controls the outcome, not the helper"
);
}
#[test]
fn clone_shares_storage() {
let a = FacetMap::new();
let b = a.clone();
a.register("rev-a".into(), "uuid-1".into());
assert_eq!(b.lookup("rev-a").as_deref(), Some("uuid-1"));
let _ = b.verify_and_consume_atomic("rev-a", |_| Ok(()));
assert!(a.lookup("rev-a").is_none());
}
#[test]
fn generate_uuid_is_unique() {
let a = generate_facet_uuid();
let b = generate_facet_uuid();
assert_ne!(a, b);
}
}