use std::collections::HashMap;
use crate::{
error::StoreError,
model::{Object, Relation, RelationTuple, User},
};
pub trait TupleStore: std::fmt::Debug {
fn read_tuples(
&self,
object: &Object,
relation: Option<&Relation>,
user: Option<&User>,
) -> Vec<RelationTuple>;
fn write_tuple(&mut self, tuple: RelationTuple) -> Result<(), StoreError>;
fn delete_tuple(&mut self, tuple: &RelationTuple) -> Result<(), StoreError>;
}
#[derive(Debug, Default)]
pub struct InMemoryTupleStore {
index: HashMap<Object, Vec<RelationTuple>>,
}
impl TupleStore for InMemoryTupleStore {
fn read_tuples(
&self,
object: &Object,
relation: Option<&Relation>,
user: Option<&User>,
) -> Vec<RelationTuple> {
let Some(tuples) = self.index.get(object) else {
return Vec::new();
};
tuples
.iter()
.filter(|t| {
relation.is_none_or(|r| t.relation == *r) && user.is_none_or(|u| t.user == *u)
})
.cloned()
.collect()
}
fn write_tuple(&mut self, tuple: RelationTuple) -> Result<(), StoreError> {
let tuples = self.index.entry(tuple.object.clone()).or_default();
if tuples.contains(&tuple) {
return Err(StoreError::DuplicateTuple);
}
tuples.push(tuple);
Ok(())
}
fn delete_tuple(&mut self, tuple: &RelationTuple) -> Result<(), StoreError> {
let tuples = self
.index
.get_mut(&tuple.object)
.ok_or(StoreError::TupleNotFound)?;
let pos = tuples
.iter()
.position(|t| t == tuple)
.ok_or(StoreError::TupleNotFound)?;
tuples.swap_remove(pos);
if tuples.is_empty() {
self.index.remove(&tuple.object);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_tuple(ns: &str, id: &str, rel: &str, user: &str) -> RelationTuple {
RelationTuple::new(Object::new(ns, id), Relation::new(rel), User::user_id(user))
}
#[test]
fn test_should_return_empty_for_empty_store() {
let store = InMemoryTupleStore::default();
let results = store.read_tuples(&Object::new("doc", "1"), None, None);
assert!(results.is_empty());
}
#[test]
fn test_should_return_empty_for_nonexistent_object() {
let mut store = InMemoryTupleStore::default();
store
.write_tuple(make_tuple("doc", "1", "owner", "alice"))
.unwrap();
let results = store.read_tuples(&Object::new("doc", "nonexistent"), None, None);
assert!(results.is_empty());
}
#[test]
fn test_should_read_with_no_relation_filter() {
let mut store = InMemoryTupleStore::default();
let obj = Object::new("doc", "1");
store
.write_tuple(make_tuple("doc", "1", "owner", "alice"))
.unwrap();
store
.write_tuple(make_tuple("doc", "1", "viewer", "bob"))
.unwrap();
let alice = User::user_id("alice");
let results = store.read_tuples(&obj, None, Some(&alice));
assert_eq!(results.len(), 1);
assert_eq!(results[0].relation, Relation::new("owner"));
}
#[test]
fn test_should_read_with_no_user_filter() {
let mut store = InMemoryTupleStore::default();
let obj = Object::new("doc", "1");
store
.write_tuple(make_tuple("doc", "1", "viewer", "alice"))
.unwrap();
store
.write_tuple(make_tuple("doc", "1", "viewer", "bob"))
.unwrap();
store
.write_tuple(make_tuple("doc", "1", "owner", "charlie"))
.unwrap();
let viewer = Relation::new("viewer");
let results = store.read_tuples(&obj, Some(&viewer), None);
assert_eq!(results.len(), 2);
}
#[test]
fn test_should_read_all_tuples_for_object() {
let mut store = InMemoryTupleStore::default();
let obj = Object::new("doc", "1");
store
.write_tuple(make_tuple("doc", "1", "owner", "alice"))
.unwrap();
store
.write_tuple(make_tuple("doc", "1", "viewer", "bob"))
.unwrap();
store
.write_tuple(make_tuple("doc", "1", "editor", "charlie"))
.unwrap();
let results = store.read_tuples(&obj, None, None);
assert_eq!(results.len(), 3);
}
#[test]
fn test_should_isolate_tuples_by_object() {
let mut store = InMemoryTupleStore::default();
store
.write_tuple(make_tuple("doc", "1", "owner", "alice"))
.unwrap();
store
.write_tuple(make_tuple("doc", "2", "owner", "bob"))
.unwrap();
let results = store.read_tuples(&Object::new("doc", "1"), None, None);
assert_eq!(results.len(), 1);
assert_eq!(results[0].user, User::user_id("alice"));
}
#[test]
fn test_should_store_and_read_userset_tuples() {
let mut store = InMemoryTupleStore::default();
let tuple = RelationTuple::new(
Object::new("doc", "1"),
Relation::new("parent"),
User::userset(Object::new("folder", "A"), Relation::new("viewer")),
);
store.write_tuple(tuple.clone()).unwrap();
let results = store.read_tuples(
&Object::new("doc", "1"),
Some(&Relation::new("parent")),
None,
);
assert_eq!(results.len(), 1);
assert_eq!(results[0], tuple);
}
#[test]
fn test_should_allow_rewrite_after_delete() {
let mut store = InMemoryTupleStore::default();
let tuple = make_tuple("doc", "1", "owner", "alice");
store.write_tuple(tuple.clone()).unwrap();
store.delete_tuple(&tuple).unwrap();
assert!(store.write_tuple(tuple).is_ok());
}
#[test]
fn test_should_clean_index_on_last_delete() {
let mut store = InMemoryTupleStore::default();
let tuple = make_tuple("doc", "1", "owner", "alice");
store.write_tuple(tuple.clone()).unwrap();
store.delete_tuple(&tuple).unwrap();
assert!(!store.index.contains_key(&Object::new("doc", "1")));
}
}