1use crate::scope::Scope;
20use rustlavel_core::Result;
21use std::collections::HashMap;
22use std::future::Future;
23use std::pin::Pin;
24use std::sync::{Arc, RwLock};
25
26pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
36
37pub trait FlagStore: Send + Sync + 'static {
43 fn name(&self) -> &'static str;
45
46 fn get<'a>(&'a self, flag: &'a str, scope: &'a Scope) -> BoxFuture<'a, Result<Option<bool>>>;
53
54 fn set<'a>(&'a self, flag: &'a str, scope: &'a Scope, value: bool)
56 -> BoxFuture<'a, Result<()>>;
57
58 fn forget<'a>(&'a self, flag: &'a str, scope: &'a Scope) -> BoxFuture<'a, Result<()>>;
63
64 fn flush(&self) -> BoxFuture<'_, Result<()>>;
70}
71
72#[derive(Clone, Default)]
85pub struct MemoryStore {
86 entries: Arc<RwLock<HashMap<(String, String), bool>>>,
87}
88
89impl MemoryStore {
90 pub fn new() -> Self {
91 MemoryStore::default()
92 }
93
94 pub fn len(&self) -> usize {
96 self.read().len()
97 }
98
99 pub fn is_empty(&self) -> bool {
100 self.len() == 0
101 }
102
103 fn read(&self) -> std::sync::RwLockReadGuard<'_, HashMap<(String, String), bool>> {
108 self.entries.read().unwrap_or_else(|poisoned| poisoned.into_inner())
109 }
110
111 fn write(&self) -> std::sync::RwLockWriteGuard<'_, HashMap<(String, String), bool>> {
112 self.entries.write().unwrap_or_else(|poisoned| poisoned.into_inner())
113 }
114}
115
116impl FlagStore for MemoryStore {
117 fn name(&self) -> &'static str {
118 "memory"
119 }
120
121 fn get<'a>(&'a self, flag: &'a str, scope: &'a Scope) -> BoxFuture<'a, Result<Option<bool>>> {
122 Box::pin(async move {
123 let key = (flag.to_string(), scope.key());
124 Ok(self.read().get(&key).copied())
125 })
126 }
127
128 fn set<'a>(
129 &'a self,
130 flag: &'a str,
131 scope: &'a Scope,
132 value: bool,
133 ) -> BoxFuture<'a, Result<()>> {
134 Box::pin(async move {
135 self.write().insert((flag.to_string(), scope.key()), value);
136 Ok(())
137 })
138 }
139
140 fn forget<'a>(&'a self, flag: &'a str, scope: &'a Scope) -> BoxFuture<'a, Result<()>> {
141 Box::pin(async move {
142 self.write().remove(&(flag.to_string(), scope.key()));
143 Ok(())
144 })
145 }
146
147 fn flush(&self) -> BoxFuture<'_, Result<()>> {
148 Box::pin(async move {
149 self.write().clear();
150 Ok(())
151 })
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[tokio::test]
160 async fn an_override_round_trips() {
161 let store = MemoryStore::new();
162 let scope = Scope::user_id(41);
163
164 assert_eq!(store.get("new-checkout", &scope).await.unwrap(), None);
165
166 store.set("new-checkout", &scope, true).await.unwrap();
167 assert_eq!(store.get("new-checkout", &scope).await.unwrap(), Some(true));
168
169 store.set("new-checkout", &scope, false).await.unwrap();
170 assert_eq!(store.get("new-checkout", &scope).await.unwrap(), Some(false));
171
172 store.forget("new-checkout", &scope).await.unwrap();
173 assert_eq!(store.get("new-checkout", &scope).await.unwrap(), None);
174 }
175
176 #[tokio::test]
177 async fn a_stored_false_is_not_the_same_answer_as_nothing_stored() {
178 let store = MemoryStore::new();
179 let scope = Scope::user_id(7);
180
181 store.set("beta-search", &scope, false).await.unwrap();
182
183 assert_eq!(store.get("beta-search", &scope).await.unwrap(), Some(false));
185 assert_eq!(store.get("never-set", &scope).await.unwrap(), None);
186 }
187
188 #[tokio::test]
189 async fn scopes_and_flags_are_kept_apart() {
190 let store = MemoryStore::new();
191
192 store.set("new-checkout", &Scope::user("acme"), true).await.unwrap();
193
194 assert_eq!(store.get("new-checkout", &Scope::tenant("acme")).await.unwrap(), None);
195 assert_eq!(store.get("new-checkout", &Scope::user("other")).await.unwrap(), None);
196 assert_eq!(store.get("beta-search", &Scope::user("acme")).await.unwrap(), None);
197 assert_eq!(store.get("new-checkout", &Scope::none()).await.unwrap(), None);
198 }
199
200 #[tokio::test]
201 async fn forgetting_something_that_was_never_there_is_not_an_error() {
202 let store = MemoryStore::new();
203 store.forget("never-set", &Scope::none()).await.unwrap();
204 }
205
206 #[tokio::test]
207 async fn flush_empties_the_store() {
208 let store = MemoryStore::new();
209 store.set("a", &Scope::user_id(1), true).await.unwrap();
210 store.set("b", &Scope::none(), false).await.unwrap();
211 assert_eq!(store.len(), 2);
212
213 store.flush().await.unwrap();
214
215 assert!(store.is_empty());
216 assert_eq!(store.get("a", &Scope::user_id(1)).await.unwrap(), None);
217 }
218
219 #[tokio::test]
220 async fn a_clone_shares_the_same_entries() {
221 let store = MemoryStore::new();
224 let handle = store.clone();
225
226 handle.set("new-checkout", &Scope::none(), true).await.unwrap();
227
228 assert_eq!(store.get("new-checkout", &Scope::none()).await.unwrap(), Some(true));
229 assert_eq!(store.name(), "memory");
230 }
231
232 #[tokio::test]
233 async fn it_works_through_a_trait_object() {
234 let store: Arc<dyn FlagStore> = Arc::new(MemoryStore::new());
235
236 store.set("new-checkout", &Scope::user_id(41), true).await.unwrap();
237
238 assert_eq!(store.get("new-checkout", &Scope::user_id(41)).await.unwrap(), Some(true));
239 }
240}