use std::collections::{BTreeMap, BTreeSet};
use std::future::{Future, ready};
use std::marker::PhantomData;
use std::sync::{Arc, RwLock};
use chrono::{DateTime, Utc};
use crate::error::KeepsakeError;
use crate::model::{
ActiveRelation, Keepsake, KeepsakeId, RelationDefinition, RelationId, RelationKey,
RelationSpec, SubjectRef,
};
use super::{ActiveRelationSource, ProviderResult};
#[derive(Debug, thiserror::Error)]
pub enum InMemoryActiveRelationsError {
#[error("in-memory active relation source lock poisoned")]
Poisoned,
#[error(transparent)]
Keepsake(#[from] KeepsakeError),
}
#[derive(Debug, Clone, Default)]
pub struct InMemoryActiveRelations {
active: Arc<RwLock<Vec<ActiveRelation>>>,
}
#[derive(Debug, Clone)]
pub struct ActiveRelationSeed<Spec> {
keepsake_id: KeepsakeId,
subject: SubjectRef,
active_at: DateTime<Utc>,
metadata: BTreeMap<String, String>,
_spec: PhantomData<fn() -> Spec>,
}
impl<Spec> ActiveRelationSeed<Spec>
where
Spec: RelationSpec,
{
#[must_use]
pub fn new(keepsake_id: KeepsakeId, subject: SubjectRef, active_at: DateTime<Utc>) -> Self {
Self {
keepsake_id,
subject,
active_at,
metadata: BTreeMap::new(),
_spec: PhantomData,
}
}
#[must_use]
pub fn from_u128(instance_id: u128, subject: SubjectRef, active_at: DateTime<Utc>) -> Self {
Self::new(uuid::Uuid::from_u128(instance_id), subject, active_at)
}
#[must_use]
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(key.into(), value.into());
self
}
#[must_use]
pub fn with_attributes<K, V>(mut self, attributes: impl IntoIterator<Item = (K, V)>) -> Self
where
K: Into<String>,
V: Into<String>,
{
self.metadata.extend(
attributes
.into_iter()
.map(|(key, value)| (key.into(), value.into())),
);
self
}
fn into_active_relation(self) -> Result<ActiveRelation, KeepsakeError> {
let relation = RelationDefinition::from_spec::<Spec>(self.active_at)?;
let keepsake = Keepsake::applied(
self.keepsake_id,
self.subject,
&relation,
self.active_at,
self.metadata,
)?;
ActiveRelation::new(keepsake, relation)
}
}
impl InMemoryActiveRelations {
#[must_use]
pub fn empty() -> Self {
Self::default()
}
#[must_use]
pub fn new(active: impl IntoIterator<Item = ActiveRelation>) -> Self {
Self {
active: Arc::new(RwLock::new(active.into_iter().collect())),
}
}
pub fn insert(
&self,
active: ActiveRelation,
) -> ProviderResult<(), InMemoryActiveRelationsError> {
self.active
.write()
.map_err(|_| InMemoryActiveRelationsError::Poisoned)?
.push(active);
Ok(())
}
pub fn insert_active_for_spec<Spec>(
&self,
instance_id: u128,
subject: SubjectRef,
active_at: DateTime<Utc>,
) -> ProviderResult<(), InMemoryActiveRelationsError>
where
Spec: RelationSpec,
{
self.insert_active_relation(ActiveRelationSeed::<Spec>::from_u128(
instance_id,
subject,
active_at,
))
}
pub fn insert_active_relation<Spec>(
&self,
seed: ActiveRelationSeed<Spec>,
) -> ProviderResult<(), InMemoryActiveRelationsError>
where
Spec: RelationSpec,
{
self.insert(seed.into_active_relation()?)
}
pub fn insert_for_spec<Spec>(
&self,
keepsake_id: KeepsakeId,
subject: SubjectRef,
applied_at: DateTime<Utc>,
metadata: BTreeMap<String, String>,
) -> ProviderResult<(), InMemoryActiveRelationsError>
where
Spec: RelationSpec,
{
self.insert_active_relation(
ActiveRelationSeed::<Spec>::new(keepsake_id, subject, applied_at)
.with_attributes(metadata),
)
}
fn active_for_subject(
&self,
subject: &SubjectRef,
) -> ProviderResult<Vec<ActiveRelation>, InMemoryActiveRelationsError> {
let mut active = self
.active
.read()
.map_err(|_| InMemoryActiveRelationsError::Poisoned)?
.iter()
.filter(|active| active.keepsake().subject() == subject)
.cloned()
.collect::<Vec<_>>();
sort_active_relations(&mut active);
Ok(active)
}
fn active_for_subject_by_ids(
&self,
subject: &SubjectRef,
relation_ids: &[RelationId],
) -> ProviderResult<Vec<ActiveRelation>, InMemoryActiveRelationsError> {
if relation_ids.is_empty() {
return Ok(Vec::new());
}
let requested = relation_ids.iter().copied().collect::<BTreeSet<_>>();
let mut active = self
.active
.read()
.map_err(|_| InMemoryActiveRelationsError::Poisoned)?
.iter()
.filter(|active| {
active.keepsake().subject() == subject
&& requested.contains(&active.keepsake().relation_id())
})
.cloned()
.collect::<Vec<_>>();
sort_active_relations(&mut active);
Ok(active)
}
fn active_for_subject_by_keys(
&self,
subject: &SubjectRef,
keys: &[RelationKey],
) -> ProviderResult<Vec<ActiveRelation>, InMemoryActiveRelationsError> {
if keys.is_empty() {
return Ok(Vec::new());
}
let requested = keys.iter().collect::<BTreeSet<_>>();
let mut active = self
.active
.read()
.map_err(|_| InMemoryActiveRelationsError::Poisoned)?
.iter()
.filter(|active| {
active.keepsake().subject() == subject && requested.contains(&active.relation().key)
})
.cloned()
.collect::<Vec<_>>();
sort_active_relations(&mut active);
Ok(active)
}
}
impl ActiveRelationSource for InMemoryActiveRelations {
type Error = InMemoryActiveRelationsError;
fn active_relations_for_subject<'a>(
&'a self,
subject: &'a SubjectRef,
) -> impl Future<Output = ProviderResult<Vec<ActiveRelation>, Self::Error>> + Send + 'a {
ready(self.active_for_subject(subject))
}
fn active_relations_for_subject_by_ids<'a>(
&'a self,
subject: &'a SubjectRef,
relation_ids: &'a [RelationId],
) -> impl Future<Output = ProviderResult<Vec<ActiveRelation>, Self::Error>> + Send + 'a {
ready(self.active_for_subject_by_ids(subject, relation_ids))
}
fn active_relations_for_subject_by_keys<'a>(
&'a self,
subject: &'a SubjectRef,
keys: &'a [RelationKey],
) -> impl Future<Output = ProviderResult<Vec<ActiveRelation>, Self::Error>> + Send + 'a {
ready(self.active_for_subject_by_keys(subject, keys))
}
}
fn sort_active_relations(active: &mut [ActiveRelation]) {
active.sort_by_key(|active| (active.keepsake().relation_id(), active.keepsake().id()));
}
#[cfg(test)]
mod tests {
use uuid::Uuid;
use super::*;
use crate::{ExpiryPolicy, StaticRelationKey};
type TestResult<T> = core::result::Result<T, TestError>;
#[derive(Debug, thiserror::Error)]
enum TestError {
#[error(transparent)]
Chrono(#[from] chrono::ParseError),
#[error(transparent)]
InMemory(#[from] InMemoryActiveRelationsError),
#[error(transparent)]
Keepsake(#[from] KeepsakeError),
}
struct TrustedTag;
impl RelationSpec for TrustedTag {
const ID: RelationId = Uuid::from_u128(1);
const KEY: StaticRelationKey = StaticRelationKey::new("tag", "trusted");
fn expiry(_at: DateTime<Utc>) -> ExpiryPolicy {
ExpiryPolicy::ManualOnly
}
}
struct AdminTag;
impl RelationSpec for AdminTag {
const ID: RelationId = Uuid::from_u128(2);
const KEY: StaticRelationKey = StaticRelationKey::new("tag", "admin");
fn expiry(_at: DateTime<Utc>) -> ExpiryPolicy {
ExpiryPolicy::ManualOnly
}
}
fn ts(value: &str) -> core::result::Result<DateTime<Utc>, chrono::ParseError> {
DateTime::parse_from_rfc3339(value).map(|timestamp| timestamp.with_timezone(&Utc))
}
#[test]
fn reads_active_relations_by_subject_ids_and_keys() -> TestResult<()> {
let source = InMemoryActiveRelations::empty();
let subject = SubjectRef::new("account", "acct_123")?;
let other_subject = SubjectRef::new("account", "acct_456")?;
let at = ts("2026-01-01T00:00:00Z")?;
source.insert_for_spec::<TrustedTag>(
Uuid::from_u128(10),
subject.clone(),
at,
BTreeMap::new(),
)?;
source.insert_for_spec::<AdminTag>(
Uuid::from_u128(20),
subject.clone(),
at,
BTreeMap::new(),
)?;
source.insert_for_spec::<AdminTag>(
Uuid::from_u128(30),
other_subject,
at,
BTreeMap::new(),
)?;
let all = source.active_for_subject(&subject)?;
assert_eq!(
all.iter()
.map(|active| active.keepsake().id())
.collect::<Vec<_>>(),
vec![Uuid::from_u128(10), Uuid::from_u128(20)]
);
let by_ids = source.active_for_subject_by_ids(
&subject,
&[AdminTag::ID, AdminTag::ID, Uuid::from_u128(99)],
)?;
assert_eq!(by_ids.len(), 1);
assert_eq!(by_ids[0].relation().id, AdminTag::ID);
let keys = [
TrustedTag::KEY.to_relation_key()?,
TrustedTag::KEY.to_relation_key()?,
RelationKey::new("tag", "missing")?,
];
let by_keys = source.active_for_subject_by_keys(&subject, &keys)?;
assert_eq!(by_keys.len(), 1);
assert_eq!(by_keys[0].relation().id, TrustedTag::ID);
assert!(source.active_for_subject_by_ids(&subject, &[])?.is_empty());
assert!(source.active_for_subject_by_keys(&subject, &[])?.is_empty());
Ok(())
}
#[test]
fn inserts_active_for_spec_with_explicit_id_time_and_empty_metadata() -> TestResult<()> {
let source = InMemoryActiveRelations::empty();
let subject = SubjectRef::new("account", "acct_123")?;
let at = ts("2026-01-01T00:00:00Z")?;
source.insert_active_for_spec::<TrustedTag>(
0xaaaa_aaaa_aaaa_aaaa_aaaa_aaaa_aaaa_aaaa,
subject.clone(),
at,
)?;
let active = source.active_for_subject(&subject)?;
assert_eq!(active.len(), 1);
assert_eq!(
active[0].keepsake().id(),
Uuid::from_u128(0xaaaa_aaaa_aaaa_aaaa_aaaa_aaaa_aaaa_aaaa)
);
assert_eq!(active[0].keepsake().applied_at(), at);
assert_eq!(active[0].relation().id, TrustedTag::ID);
assert!(active[0].keepsake().metadata().is_empty());
Ok(())
}
#[test]
fn active_relation_seed_preserves_attributes() -> TestResult<()> {
let source = InMemoryActiveRelations::empty();
let subject = SubjectRef::new("account", "acct_123")?;
let at = ts("2026-01-01T00:00:00Z")?;
source.insert_active_relation(
ActiveRelationSeed::<AdminTag>::new(
Uuid::from_u128(0xbbbb_bbbb_bbbb_bbbb_bbbb_bbbb_bbbb_bbbb),
subject.clone(),
at,
)
.with_attribute("ticket", "case-1")
.with_attributes([("source", "fixture")]),
)?;
let active = source.active_for_subject(&subject)?;
assert_eq!(active.len(), 1);
assert_eq!(active[0].keepsake().applied_at(), at);
assert_eq!(active[0].relation().id, AdminTag::ID);
assert_eq!(
active[0]
.keepsake()
.metadata()
.get("ticket")
.map(String::as_str),
Some("case-1")
);
assert_eq!(
active[0]
.keepsake()
.metadata()
.get("source")
.map(String::as_str),
Some("fixture")
);
Ok(())
}
}