use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::sync::Mutex;
use async_trait::async_trait;
use crate::types::{Approval, ApprovalStatus, LogEntry, Mission, Person};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Spend {
WithinBudget,
Approved,
}
#[derive(Debug)]
pub struct StoreError(Box<dyn std::error::Error + Send + Sync>);
impl StoreError {
pub fn new(error: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
Self(error.into())
}
}
impl std::fmt::Display for StoreError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "store unavailable: {}", self.0)
}
}
impl std::error::Error for StoreError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.0.as_ref())
}
}
pub type StoreResult<T> = Result<T, StoreError>;
#[async_trait]
pub trait Store: Send + Sync {
async fn binding(&self, tenant: &str, jkt: &str) -> StoreResult<Option<Person>>;
async fn bind(&self, tenant: &str, jkt: &str, person: &Person) -> StoreResult<Person>;
async fn mission(&self, tenant: &str, s256: &str) -> StoreResult<Option<Mission>>;
async fn insert_mission(&self, tenant: &str, mission: &Mission) -> StoreResult<bool>;
async fn log_mission(
&self,
tenant: &str,
s256: &str,
entry: LogEntry,
now: i64,
) -> StoreResult<bool>;
async fn end_mission(&self, tenant: &str, s256: &str, at: i64) -> StoreResult<Option<Mission>>;
async fn spend(
&self,
tenant: &str,
s256: &str,
amount: u64,
rule: Spend,
now: i64,
) -> StoreResult<Option<Mission>>;
async fn missions(&self, tenant: &str) -> StoreResult<Vec<Mission>>;
async fn approval(&self, tenant: &str, id: &str) -> StoreResult<Option<Approval>>;
async fn insert_approval(&self, tenant: &str, approval: &Approval) -> StoreResult<()>;
async fn transition_approval(
&self,
tenant: &str,
expected: ApprovalStatus,
next: &Approval,
) -> StoreResult<bool>;
async fn approvals(&self, tenant: &str) -> StoreResult<Vec<Approval>>;
}
type Key = (String, String);
fn key(tenant: &str, id: &str) -> Key {
(tenant.to_string(), id.to_string())
}
#[derive(Default)]
struct State {
bindings: HashMap<Key, Person>,
missions: HashMap<Key, Mission>,
approvals: HashMap<Key, Approval>,
}
const APPROVAL_RETENTION_SECS: i64 = 24 * 60 * 60;
#[derive(Default)]
pub struct MemoryStore(Mutex<State>);
impl MemoryStore {
pub fn new() -> Self {
Self::default()
}
fn with<T>(&self, f: impl FnOnce(&mut State) -> T) -> StoreResult<T> {
let mut state = self
.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
Ok(f(&mut state))
}
}
#[async_trait]
impl Store for MemoryStore {
async fn binding(&self, tenant: &str, jkt: &str) -> StoreResult<Option<Person>> {
self.with(|s| s.bindings.get(&key(tenant, jkt)).cloned())
}
async fn bind(&self, tenant: &str, jkt: &str, person: &Person) -> StoreResult<Person> {
self.with(|s| {
s.bindings
.entry(key(tenant, jkt))
.or_insert_with(|| person.clone())
.clone()
})
}
async fn mission(&self, tenant: &str, s256: &str) -> StoreResult<Option<Mission>> {
self.with(|s| s.missions.get(&key(tenant, s256)).cloned())
}
async fn insert_mission(&self, tenant: &str, mission: &Mission) -> StoreResult<bool> {
self.with(|s| match s.missions.entry(key(tenant, &mission.s256)) {
Entry::Occupied(_) => false,
Entry::Vacant(slot) => {
slot.insert(mission.clone());
true
}
})
}
async fn log_mission(
&self,
tenant: &str,
s256: &str,
entry: LogEntry,
now: i64,
) -> StoreResult<bool> {
self.with(|s| match s.missions.get_mut(&key(tenant, s256)) {
Some(mission) if mission.is_live(now) => {
mission.log.push(entry);
true
}
_ => false,
})
}
async fn end_mission(&self, tenant: &str, s256: &str, at: i64) -> StoreResult<Option<Mission>> {
self.with(|s| {
let mission = s.missions.get_mut(&key(tenant, s256))?;
mission.completed_at.get_or_insert(at);
Some(mission.clone())
})
}
async fn spend(
&self,
tenant: &str,
s256: &str,
amount: u64,
rule: Spend,
now: i64,
) -> StoreResult<Option<Mission>> {
self.with(|s| {
let mission = s.missions.get_mut(&key(tenant, s256))?;
if !mission.is_live(now) {
return None;
}
if rule == Spend::WithinBudget {
let left = mission.budget.as_ref()?.amount.checked_sub(mission.spent)?;
if left < amount {
return None;
}
}
mission.spent = mission.spent.saturating_add(amount);
Some(mission.clone())
})
}
async fn missions(&self, tenant: &str) -> StoreResult<Vec<Mission>> {
self.with(|s| {
s.missions
.iter()
.filter(|((t, _), _)| t == tenant)
.map(|(_, m)| m.clone())
.collect()
})
}
async fn approval(&self, tenant: &str, id: &str) -> StoreResult<Option<Approval>> {
self.with(|s| s.approvals.get(&key(tenant, id)).cloned())
}
async fn insert_approval(&self, tenant: &str, approval: &Approval) -> StoreResult<()> {
self.with(|s| {
let now = approval.created_at;
s.approvals
.retain(|_, a| a.expires_at + APPROVAL_RETENTION_SECS > now);
s.approvals
.insert(key(tenant, &approval.id), approval.clone());
})
}
async fn transition_approval(
&self,
tenant: &str,
expected: ApprovalStatus,
next: &Approval,
) -> StoreResult<bool> {
self.with(|s| match s.approvals.get_mut(&key(tenant, &next.id)) {
Some(current) if current.status == expected => {
*current = next.clone();
true
}
_ => false,
})
}
async fn approvals(&self, tenant: &str) -> StoreResult<Vec<Approval>> {
self.with(|s| {
s.approvals
.iter()
.filter(|((t, _), _)| t == tenant)
.map(|(_, a)| a.clone())
.collect()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mission(person: &str) -> Mission {
Mission {
s256: "h".into(),
mission: "{}".into(),
agent_id: "aauth:travel@agents.example".into(),
person: Person::new(person),
resources: Vec::new(),
budget: None,
spent: 0,
log: Vec::new(),
expires_at: i64::MAX,
completed_at: None,
tenant: "default".into(),
}
}
#[tokio::test]
async fn a_mission_is_never_replaced() {
let store = MemoryStore::default();
assert!(store
.insert_mission("default", &mission("sam"))
.await
.unwrap());
store.end_mission("default", "h", 10).await.unwrap();
assert!(!store
.insert_mission("default", &mission("dana"))
.await
.unwrap());
let kept = store.mission("default", "h").await.unwrap().unwrap();
assert_eq!(kept.person.id, "sam");
assert_eq!(kept.completed_at, Some(10));
}
#[tokio::test]
async fn each_tenant_sees_only_its_own() {
let store = MemoryStore::default();
assert!(store.insert_mission("acme", &mission("sam")).await.unwrap());
assert!(store
.insert_mission("globex", &mission("hank"))
.await
.unwrap());
let at = |t| store.mission(t, "h");
assert_eq!(at("acme").await.unwrap().unwrap().person.id, "sam");
assert_eq!(at("globex").await.unwrap().unwrap().person.id, "hank");
assert!(at("default").await.unwrap().is_none());
assert_eq!(store.missions("acme").await.unwrap().len(), 1);
store.bind("acme", "k", &Person::new("sam")).await.unwrap();
assert!(store.binding("globex", "k").await.unwrap().is_none());
}
}