use crate::scope::Scope;
use rustlavel_core::Result;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, RwLock};
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait FlagStore: Send + Sync + 'static {
fn name(&self) -> &'static str;
fn get<'a>(&'a self, flag: &'a str, scope: &'a Scope) -> BoxFuture<'a, Result<Option<bool>>>;
fn set<'a>(&'a self, flag: &'a str, scope: &'a Scope, value: bool)
-> BoxFuture<'a, Result<()>>;
fn forget<'a>(&'a self, flag: &'a str, scope: &'a Scope) -> BoxFuture<'a, Result<()>>;
fn flush(&self) -> BoxFuture<'_, Result<()>>;
}
#[derive(Clone, Default)]
pub struct MemoryStore {
entries: Arc<RwLock<HashMap<(String, String), bool>>>,
}
impl MemoryStore {
pub fn new() -> Self {
MemoryStore::default()
}
pub fn len(&self) -> usize {
self.read().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
fn read(&self) -> std::sync::RwLockReadGuard<'_, HashMap<(String, String), bool>> {
self.entries.read().unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn write(&self) -> std::sync::RwLockWriteGuard<'_, HashMap<(String, String), bool>> {
self.entries.write().unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
impl FlagStore for MemoryStore {
fn name(&self) -> &'static str {
"memory"
}
fn get<'a>(&'a self, flag: &'a str, scope: &'a Scope) -> BoxFuture<'a, Result<Option<bool>>> {
Box::pin(async move {
let key = (flag.to_string(), scope.key());
Ok(self.read().get(&key).copied())
})
}
fn set<'a>(
&'a self,
flag: &'a str,
scope: &'a Scope,
value: bool,
) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
self.write().insert((flag.to_string(), scope.key()), value);
Ok(())
})
}
fn forget<'a>(&'a self, flag: &'a str, scope: &'a Scope) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
self.write().remove(&(flag.to_string(), scope.key()));
Ok(())
})
}
fn flush(&self) -> BoxFuture<'_, Result<()>> {
Box::pin(async move {
self.write().clear();
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn an_override_round_trips() {
let store = MemoryStore::new();
let scope = Scope::user_id(41);
assert_eq!(store.get("new-checkout", &scope).await.unwrap(), None);
store.set("new-checkout", &scope, true).await.unwrap();
assert_eq!(store.get("new-checkout", &scope).await.unwrap(), Some(true));
store.set("new-checkout", &scope, false).await.unwrap();
assert_eq!(store.get("new-checkout", &scope).await.unwrap(), Some(false));
store.forget("new-checkout", &scope).await.unwrap();
assert_eq!(store.get("new-checkout", &scope).await.unwrap(), None);
}
#[tokio::test]
async fn a_stored_false_is_not_the_same_answer_as_nothing_stored() {
let store = MemoryStore::new();
let scope = Scope::user_id(7);
store.set("beta-search", &scope, false).await.unwrap();
assert_eq!(store.get("beta-search", &scope).await.unwrap(), Some(false));
assert_eq!(store.get("never-set", &scope).await.unwrap(), None);
}
#[tokio::test]
async fn scopes_and_flags_are_kept_apart() {
let store = MemoryStore::new();
store.set("new-checkout", &Scope::user("acme"), true).await.unwrap();
assert_eq!(store.get("new-checkout", &Scope::tenant("acme")).await.unwrap(), None);
assert_eq!(store.get("new-checkout", &Scope::user("other")).await.unwrap(), None);
assert_eq!(store.get("beta-search", &Scope::user("acme")).await.unwrap(), None);
assert_eq!(store.get("new-checkout", &Scope::none()).await.unwrap(), None);
}
#[tokio::test]
async fn forgetting_something_that_was_never_there_is_not_an_error() {
let store = MemoryStore::new();
store.forget("never-set", &Scope::none()).await.unwrap();
}
#[tokio::test]
async fn flush_empties_the_store() {
let store = MemoryStore::new();
store.set("a", &Scope::user_id(1), true).await.unwrap();
store.set("b", &Scope::none(), false).await.unwrap();
assert_eq!(store.len(), 2);
store.flush().await.unwrap();
assert!(store.is_empty());
assert_eq!(store.get("a", &Scope::user_id(1)).await.unwrap(), None);
}
#[tokio::test]
async fn a_clone_shares_the_same_entries() {
let store = MemoryStore::new();
let handle = store.clone();
handle.set("new-checkout", &Scope::none(), true).await.unwrap();
assert_eq!(store.get("new-checkout", &Scope::none()).await.unwrap(), Some(true));
assert_eq!(store.name(), "memory");
}
#[tokio::test]
async fn it_works_through_a_trait_object() {
let store: Arc<dyn FlagStore> = Arc::new(MemoryStore::new());
store.set("new-checkout", &Scope::user_id(41), true).await.unwrap();
assert_eq!(store.get("new-checkout", &Scope::user_id(41)).await.unwrap(), Some(true));
}
}