use std::collections::HashMap;
use std::sync::RwLock;
use async_trait::async_trait;
use openagent_aegis_core::{Delegation, DelegationError, Pagination};
#[async_trait]
pub trait DelegationStore: Send + Sync {
async fn store_delegation(&self, delegation: &Delegation) -> Result<(), DelegationError>;
async fn get_delegation(&self, id: &str) -> Result<Option<Delegation>, DelegationError>;
async fn list_by_delegator(
&self,
delegator_did: &str,
pagination: Pagination,
) -> Result<Vec<Delegation>, DelegationError>;
async fn list_by_delegate(
&self,
delegate_did: &str,
pagination: Pagination,
) -> Result<Vec<Delegation>, DelegationError>;
async fn delete_delegation(&self, id: &str) -> Result<bool, DelegationError>;
}
pub struct InMemoryDelegationStore {
delegations: RwLock<HashMap<String, Delegation>>,
}
impl InMemoryDelegationStore {
pub fn new() -> Self {
Self {
delegations: RwLock::new(HashMap::new()),
}
}
}
impl Default for InMemoryDelegationStore {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl DelegationStore for InMemoryDelegationStore {
async fn store_delegation(&self, delegation: &Delegation) -> Result<(), DelegationError> {
let mut store = self
.delegations
.write()
.map_err(|e| DelegationError::InvalidProof {
reason: format!("delegation store lock poisoned: {e}"),
})?;
store.insert(delegation.id.clone(), delegation.clone());
Ok(())
}
async fn get_delegation(&self, id: &str) -> Result<Option<Delegation>, DelegationError> {
let store = self
.delegations
.read()
.map_err(|e| DelegationError::InvalidProof {
reason: format!("delegation store lock poisoned: {e}"),
})?;
Ok(store.get(id).cloned())
}
async fn list_by_delegator(
&self,
delegator_did: &str,
pagination: Pagination,
) -> Result<Vec<Delegation>, DelegationError> {
let store = self
.delegations
.read()
.map_err(|e| DelegationError::InvalidProof {
reason: format!("delegation store lock poisoned: {e}"),
})?;
let matching: Vec<Delegation> = store
.values()
.filter(|d| d.delegator == delegator_did)
.skip(pagination.offset as usize)
.take(pagination.limit as usize)
.cloned()
.collect();
Ok(matching)
}
async fn list_by_delegate(
&self,
delegate_did: &str,
pagination: Pagination,
) -> Result<Vec<Delegation>, DelegationError> {
let store = self
.delegations
.read()
.map_err(|e| DelegationError::InvalidProof {
reason: format!("delegation store lock poisoned: {e}"),
})?;
let matching: Vec<Delegation> = store
.values()
.filter(|d| d.delegate == delegate_did)
.skip(pagination.offset as usize)
.take(pagination.limit as usize)
.cloned()
.collect();
Ok(matching)
}
async fn delete_delegation(&self, id: &str) -> Result<bool, DelegationError> {
let mut store = self
.delegations
.write()
.map_err(|e| DelegationError::InvalidProof {
reason: format!("delegation store lock poisoned: {e}"),
})?;
Ok(store.remove(id).is_some())
}
}
#[cfg(test)]
mod tests {
use super::*;
use openagent_aegis_core::{DelegationProof, DelegationScope};
use chrono::Utc;
fn make_delegation(id: &str, delegator: &str, delegate: &str) -> Delegation {
Delegation {
id: id.to_string(),
delegator: delegator.to_string(),
delegate: delegate.to_string(),
scope: DelegationScope {
actions: vec!["transfer".to_string()],
resources: vec![],
chains: vec![],
limits: None,
temporal: None,
},
created: Utc::now(),
expires: None,
revocable: true,
proof: DelegationProof {
proof_type: "AegisDelegationProof2025".to_string(),
verification_method: format!("{delegator}#key"),
created: Utc::now(),
jws: "test-jws".to_string(),
},
}
}
#[tokio::test]
async fn store_and_retrieve() {
let store = InMemoryDelegationStore::new();
let d = make_delegation("d1", "did:root", "did:agent");
let result = store.store_delegation(&d).await;
assert!(result.is_ok());
let retrieved = store.get_delegation("d1").await;
assert!(retrieved.is_ok());
let retrieved = retrieved.unwrap_or(None);
assert!(retrieved.is_some());
if let Some(del) = retrieved {
assert_eq!(del.id, "d1");
assert_eq!(del.delegator, "did:root");
assert_eq!(del.delegate, "did:agent");
}
}
#[tokio::test]
async fn get_missing_returns_none() {
let store = InMemoryDelegationStore::new();
let result = store.get_delegation("nonexistent").await;
assert!(result.is_ok());
assert!(result
.unwrap_or(Some(make_delegation("x", "x", "x")))
.is_none());
}
#[tokio::test]
async fn list_by_delegator() {
let store = InMemoryDelegationStore::new();
let d1 = make_delegation("d1", "did:root", "did:a");
let d2 = make_delegation("d2", "did:root", "did:b");
let d3 = make_delegation("d3", "did:other", "did:c");
let _ = store.store_delegation(&d1).await;
let _ = store.store_delegation(&d2).await;
let _ = store.store_delegation(&d3).await;
let root_delegations = store
.list_by_delegator("did:root", Pagination::default())
.await;
assert!(root_delegations.is_ok());
assert_eq!(root_delegations.unwrap_or_default().len(), 2);
let other_delegations = store
.list_by_delegator("did:other", Pagination::default())
.await;
assert!(other_delegations.is_ok());
assert_eq!(other_delegations.unwrap_or_default().len(), 1);
let empty = store
.list_by_delegator("did:unknown", Pagination::default())
.await;
assert!(empty.is_ok());
assert!(empty.unwrap_or_default().is_empty());
}
#[tokio::test]
async fn list_by_delegate() {
let store = InMemoryDelegationStore::new();
let d1 = make_delegation("d1", "did:root", "did:agent");
let d2 = make_delegation("d2", "did:other", "did:agent");
let d3 = make_delegation("d3", "did:root", "did:different");
let _ = store.store_delegation(&d1).await;
let _ = store.store_delegation(&d2).await;
let _ = store.store_delegation(&d3).await;
let agent_delegations = store
.list_by_delegate("did:agent", Pagination::default())
.await;
assert!(agent_delegations.is_ok());
assert_eq!(agent_delegations.unwrap_or_default().len(), 2);
let different = store
.list_by_delegate("did:different", Pagination::default())
.await;
assert!(different.is_ok());
assert_eq!(different.unwrap_or_default().len(), 1);
}
#[tokio::test]
async fn delete_existing() {
let store = InMemoryDelegationStore::new();
let d = make_delegation("d1", "did:root", "did:agent");
let _ = store.store_delegation(&d).await;
let deleted = store.delete_delegation("d1").await;
assert!(deleted.is_ok());
assert_eq!(deleted.unwrap_or(false), true);
let after = store.get_delegation("d1").await;
assert!(after.is_ok());
assert!(after
.unwrap_or(Some(make_delegation("x", "x", "x")))
.is_none());
}
#[tokio::test]
async fn delete_nonexistent_returns_false() {
let store = InMemoryDelegationStore::new();
let deleted = store.delete_delegation("nonexistent").await;
assert!(deleted.is_ok());
assert_eq!(deleted.unwrap_or(true), false);
}
#[tokio::test]
async fn store_overwrites_existing() {
let store = InMemoryDelegationStore::new();
let d1 = make_delegation("d1", "did:root", "did:agent-a");
let _ = store.store_delegation(&d1).await;
let d1_updated = make_delegation("d1", "did:root", "did:agent-b");
let _ = store.store_delegation(&d1_updated).await;
let retrieved = store.get_delegation("d1").await;
assert!(retrieved.is_ok());
if let Some(del) = retrieved.unwrap_or(None) {
assert_eq!(del.delegate, "did:agent-b");
}
}
#[tokio::test]
async fn default_constructor() {
let store = InMemoryDelegationStore::default();
let result = store.get_delegation("any").await;
assert!(result.is_ok());
assert!(result
.unwrap_or(Some(make_delegation("x", "x", "x")))
.is_none());
}
}