use std::collections::HashSet;
use std::fmt::Debug;
use std::sync::{Arc, RwLock};
pub trait UcanRevocationStore: Send + Sync + Debug {
fn is_revoked(&self, ucan_cid: &str) -> bool;
}
#[derive(Debug, Default, Clone)]
pub struct InMemoryUcanRevocationStore {
revoked: Arc<RwLock<HashSet<String>>>,
}
impl InMemoryUcanRevocationStore {
pub fn new() -> Self {
Self::default()
}
pub fn revoke(&self, ucan_cid: impl Into<String>) {
self.revoked
.write()
.expect("InMemoryUcanRevocationStore lock poisoned")
.insert(ucan_cid.into());
}
pub fn len(&self) -> usize {
self.revoked
.read()
.expect("InMemoryUcanRevocationStore lock poisoned")
.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl UcanRevocationStore for InMemoryUcanRevocationStore {
fn is_revoked(&self, ucan_cid: &str) -> bool {
self.revoked
.read()
.expect("InMemoryUcanRevocationStore lock poisoned")
.contains(ucan_cid)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_store_reports_nothing_revoked() {
let s = InMemoryUcanRevocationStore::new();
assert!(!s.is_revoked("bafy-cid-anything"));
assert!(s.is_empty());
}
#[test]
fn revoke_then_is_revoked_returns_true() {
let s = InMemoryUcanRevocationStore::new();
s.revoke("cid-abc-123");
assert!(s.is_revoked("cid-abc-123"));
assert!(!s.is_revoked("cid-xyz-999"));
assert_eq!(s.len(), 1);
}
#[test]
fn revoke_is_idempotent() {
let s = InMemoryUcanRevocationStore::new();
s.revoke("cid-A");
s.revoke("cid-A");
s.revoke("cid-A");
assert_eq!(s.len(), 1);
assert!(s.is_revoked("cid-A"));
}
#[test]
fn store_clones_share_state_via_arc() {
let s = InMemoryUcanRevocationStore::new();
let s2 = s.clone();
s.revoke("cid-X");
assert!(s2.is_revoked("cid-X"));
}
#[test]
fn store_works_through_trait_object() {
let inner = Arc::new(InMemoryUcanRevocationStore::new());
inner.revoke("cid-T");
let dyn_store: Arc<dyn UcanRevocationStore> = inner;
assert!(dyn_store.is_revoked("cid-T"));
assert!(!dyn_store.is_revoked("cid-other"));
}
}