Skip to main content

rustlavel_flags/
flags.rs

1//! The registry: defining flags, checking them, and overriding the answer.
2//!
3//! ```
4//! use rustlavel_flags::{Flags, Scope};
5//!
6//! # tokio::runtime::Runtime::new().unwrap().block_on(async {
7//! let flags = Flags::new()
8//!     .define("new-checkout", |scope| async move { scope.id().ends_with('7') })
9//!     .define("beta-search", |_| async { false })
10//!     .rollout("dark-mode", 25);
11//!
12//! assert!(flags.active("new-checkout", &Scope::user_id(17)).await.unwrap());
13//! assert!(flags.inactive("beta-search", &Scope::user_id(17)).await.unwrap());
14//!
15//! // ...except for this one customer, who asked.
16//! flags.activate_for("beta-search", &Scope::user_id(17)).await.unwrap();
17//! assert!(flags.active("beta-search", &Scope::user_id(17)).await.unwrap());
18//! # });
19//! ```
20
21use crate::rollout;
22use crate::scope::Scope;
23use crate::store::{FlagStore, MemoryStore};
24use rustlavel_core::{Config, Error, Result};
25use std::collections::{BTreeMap, BTreeSet, HashMap};
26use std::future::Future;
27use std::pin::Pin;
28use std::sync::{Arc, Mutex};
29
30/// What a resolver hands back once it has been boxed.
31type ResolverFuture = Pin<Box<dyn Future<Output = Result<bool>> + Send + 'static>>;
32
33/// A defined flag's code, ready to be called with a scope.
34type Resolver = Arc<dyn Fn(Scope) -> ResolverFuture + Send + Sync>;
35
36/// What a resolver may return.
37///
38/// A resolver that just looks at the scope returns `bool`, and one that has to
39/// go and ask a database returns `Result<bool>` — both are written the obvious
40/// way and this trait is what lets [`Flags::define`] take either. There is no
41/// third implementation and no reason for one: a flag is on or off, or the
42/// question could not be answered.
43pub trait FlagAnswer {
44    fn into_answer(self) -> Result<bool>;
45}
46
47impl FlagAnswer for bool {
48    fn into_answer(self) -> Result<bool> {
49        Ok(self)
50    }
51}
52
53impl FlagAnswer for Result<bool> {
54    fn into_answer(self) -> Result<bool> {
55        self
56    }
57}
58
59/// Every flag the application knows about, and the store behind them.
60///
61/// Cheap to clone — the definitions and the store are behind `Arc` — so it goes
62/// into application state once and every request gets a handle to the same one.
63///
64/// # Precedence
65///
66/// Five things can have an opinion about one flag, and the question this type
67/// exists to answer is "why is this on for this user?". So the answer is one
68/// short list, applied in order:
69///
70/// 1. **`flags.off` in configuration** — off, for everybody, full stop. This is
71///    the incident switch and nothing below it can reopen the flag.
72/// 2. **A stored override of `false`**, for this scope or globally — off.
73/// 3. **A stored override of `true`**, for this scope or globally — on.
74/// 4. **`flags.on` in configuration** — on.
75/// 5. **The resolver**, if the flag was defined.
76/// 6. Otherwise off. A flag nobody defined is not a flag.
77///
78/// Two of those deserve their reasons written down.
79///
80/// **Off beats on among stored overrides**, at every level, and a *global* off
81/// therefore beats a per-scope on. This is the same rule
82/// [`rustlavel_rbac`](https://docs.rs/rustlavel-rbac) applies to a deny, for
83/// the same reason: an off switch that something else can quietly overrule is
84/// not a switch, and the moment somebody reaches for one is the moment they
85/// cannot afford to audit what else might be set. The cost is real and worth
86/// knowing — you cannot exempt one customer from a global off, you have to
87/// clear the global off first — and it is the cheaper half of the trade.
88///
89/// **Configuration outranks the store in both directions.** `flags.off` is read
90/// from the environment, which means it survives a database being down, a
91/// store being unreachable, and a cache that has not caught up. That is exactly
92/// the situation you reach for it in.
93#[derive(Clone)]
94pub struct Flags {
95    definitions: Arc<HashMap<String, Resolver>>,
96    store: Arc<dyn FlagStore>,
97    forced_on: Arc<BTreeSet<String>>,
98    forced_off: Arc<BTreeSet<String>>,
99}
100
101impl Default for Flags {
102    fn default() -> Self {
103        Flags::new()
104    }
105}
106
107impl Flags {
108    /// An empty registry over an in-memory store.
109    pub fn new() -> Self {
110        Flags {
111            definitions: Arc::new(HashMap::new()),
112            store: Arc::new(MemoryStore::new()),
113            forced_on: Arc::new(BTreeSet::new()),
114            forced_off: Arc::new(BTreeSet::new()),
115        }
116    }
117
118    /// An empty registry, with `flags.on` and `flags.off` already applied.
119    ///
120    /// Define the flags on top of it: `Flags::from_config(&config).define(...)`.
121    pub fn from_config(config: &Config) -> Self {
122        Flags::new().configured(config)
123    }
124
125    /// Read `flags.on` and `flags.off` and add them to what is already forced.
126    ///
127    /// Both are lists, which in `.env` means the comma-separated spelling —
128    /// `FLAGS_OFF=new-checkout,beta-search` — and in a configuration file may
129    /// be a JSON array. [`Config::list`] accepts either.
130    ///
131    /// It *adds*: calling it twice, or calling it after [`Flags::force_off`],
132    /// leaves both sets of names forced. There is deliberately no way to
133    /// un-force a name from configuration, because the only reason to want one
134    /// is to undo an incident switch, and undoing an incident switch should
135    /// take an edit to the thing that set it.
136    ///
137    /// A name in both lists is off. It has to be — see the precedence chain on
138    /// [`Flags`] — and a deployment that has contradicted itself is one where
139    /// the safe reading of the contradiction is the closed one.
140    pub fn configured(self, config: &Config) -> Self {
141        self.force_on(config.list("flags.on")).force_off(config.list("flags.off"))
142    }
143
144    /// Put a store behind the flags: a database table, Redis, anything that
145    /// implements [`FlagStore`].
146    pub fn store(mut self, store: impl FlagStore) -> Self {
147        self.store = Arc::new(store);
148        self
149    }
150
151    /// Use a store somebody else already built and holds a handle to.
152    pub fn with_store(mut self, store: Arc<dyn FlagStore>) -> Self {
153        self.store = store;
154        self
155    }
156
157    /// Define a flag: a name, and code that decides it for a scope.
158    ///
159    /// The resolver is given the [`Scope`] by value and may do anything an
160    /// async function may do, including go to the database. It is called at
161    /// most once per scope inside a [`ScopedFlags`] batch, so a slow resolver
162    /// costs its slowness once per request rather than once per question.
163    ///
164    /// ```
165    /// # use rustlavel_flags::{Flags, Scope};
166    /// let flags = Flags::new()
167    ///     .define("new-checkout", |scope: Scope| async move { scope.id().ends_with('7') })
168    ///     .define("beta-search", |_| async { false });
169    /// # let _ = flags;
170    /// ```
171    ///
172    /// Defining a name twice replaces the first definition rather than
173    /// complaining. That is what makes a test able to say "this application,
174    /// but with `new-checkout` on" in one line.
175    pub fn define<F, Fut, A>(mut self, name: impl Into<String>, resolver: F) -> Self
176    where
177        F: Fn(Scope) -> Fut + Send + Sync + 'static,
178        Fut: Future<Output = A> + Send + 'static,
179        A: FlagAnswer,
180    {
181        let boxed: Resolver = Arc::new(move |scope| {
182            let future = resolver(scope);
183            Box::pin(async move { future.await.into_answer() })
184        });
185
186        // `make_mut` clones the map only while somebody else is holding a
187        // handle to it, which during the builder chain in `main.rs` nobody is.
188        Arc::make_mut(&mut self.definitions).insert(name.into(), boxed);
189        self
190    }
191
192    /// Define a flag as a stable percentage rollout.
193    ///
194    /// Shorthand for a resolver calling [`rollout::in_rollout`]; read that
195    /// module for why the bucket is hashed rather than drawn, and for what
196    /// widening a rollout does and does not do to the people already in it.
197    ///
198    /// Checked against [`Scope::none`] a rollout is not a percentage of
199    /// anything — there is one scope, so it lands in one bucket and stays
200    /// there, on or off for the whole installation. Percentages need a subject.
201    pub fn rollout(self, name: impl Into<String>, percent: u8) -> Self {
202        let name = name.into();
203        let flag = name.clone();
204
205        self.define(name, move |scope: Scope| {
206            let flag = flag.clone();
207            async move { rollout::in_rollout(&flag, &scope, percent) }
208        })
209    }
210
211    /// Force these flags on, below any stored override. See [`Flags::configured`].
212    pub fn force_on<I, S>(mut self, names: I) -> Self
213    where
214        I: IntoIterator<Item = S>,
215        S: Into<String>,
216    {
217        Arc::make_mut(&mut self.forced_on).extend(names.into_iter().map(Into::into));
218        self
219    }
220
221    /// Force these flags off, above everything. See [`Flags::configured`].
222    pub fn force_off<I, S>(mut self, names: I) -> Self
223    where
224        I: IntoIterator<Item = S>,
225        S: Into<String>,
226    {
227        Arc::make_mut(&mut self.forced_off).extend(names.into_iter().map(Into::into));
228        self
229    }
230
231    /// Every defined flag's name, sorted. Does not include names that only
232    /// exist as a forced value or a stored override.
233    pub fn defined(&self) -> Vec<&str> {
234        let mut names: Vec<&str> = self.definitions.keys().map(String::as_str).collect();
235        names.sort_unstable();
236        names
237    }
238
239    /// Whether a resolver was registered under this name.
240    ///
241    /// Worth checking in a boot-time assertion: an undefined flag is off, and a
242    /// misspelled one is indistinguishable from a feature that never shipped.
243    pub fn is_defined(&self, flag: &str) -> bool {
244        self.definitions.contains_key(flag)
245    }
246
247    /// The store behind the flags, for an admin route that writes overrides.
248    pub fn store_handle(&self) -> &Arc<dyn FlagStore> {
249        &self.store
250    }
251
252    /// Is this flag on for this scope?
253    ///
254    /// `Result`, and not `bool`, deliberately. "I could not find out" is a
255    /// third answer — a store that is down, a resolver whose query failed — and
256    /// collapsing it into `false` would silently turn a feature off for
257    /// everybody the moment something unrelated broke, which is exactly the
258    /// kind of outage nobody thinks to look for.
259    ///
260    /// This call does not remember anything. Use [`Flags::for_scope`] when a
261    /// request asks more than one question.
262    pub async fn active(&self, flag: &str, scope: &Scope) -> Result<bool> {
263        self.resolve(flag, scope).await
264    }
265
266    /// The negation, for the reading that comes out shorter.
267    pub async fn inactive(&self, flag: &str, scope: &Scope) -> Result<bool> {
268        Ok(!self.active(flag, scope).await?)
269    }
270
271    /// A view fixed on one scope, which remembers what it has already resolved.
272    ///
273    /// This is what a request should hold. Ten checks against one
274    /// [`ScopedFlags`] run each resolver at most once; ten checks against
275    /// [`Flags::active`] run each of them ten times.
276    pub fn for_scope(&self, scope: Scope) -> ScopedFlags {
277        ScopedFlags {
278            flags: self.clone(),
279            scope,
280            resolved: Arc::new(Mutex::new(HashMap::new())),
281        }
282    }
283
284    /// Turn a flag on for one scope, overriding whatever the resolver says.
285    pub async fn activate_for(&self, flag: &str, scope: &Scope) -> Result<()> {
286        self.store.set(flag, scope, true).await
287    }
288
289    /// Turn a flag off for one scope, overriding whatever the resolver says.
290    pub async fn deactivate_for(&self, flag: &str, scope: &Scope) -> Result<()> {
291        self.store.set(flag, scope, false).await
292    }
293
294    /// Drop one scope's override and let the resolver decide again.
295    pub async fn forget_for(&self, flag: &str, scope: &Scope) -> Result<()> {
296        self.store.forget(flag, scope).await
297    }
298
299    /// Turn a flag on for everybody who has no override of their own.
300    pub async fn activate(&self, flag: &str) -> Result<()> {
301        self.activate_for(flag, &Scope::none()).await
302    }
303
304    /// Turn a flag off for everybody, including scopes that have an override
305    /// of their own saying otherwise — see the precedence chain on [`Flags`].
306    pub async fn deactivate(&self, flag: &str) -> Result<()> {
307        self.deactivate_for(flag, &Scope::none()).await
308    }
309
310    /// Drop the global override. Per-scope overrides are left alone.
311    pub async fn forget(&self, flag: &str) -> Result<()> {
312        self.forget_for(flag, &Scope::none()).await
313    }
314
315    /// Drop every override in the store, for every flag and every scope.
316    pub async fn purge(&self) -> Result<()> {
317        self.store.flush().await
318    }
319
320    /// The precedence chain, in the order the doc comment on [`Flags`] lists it.
321    async fn resolve(&self, flag: &str, scope: &Scope) -> Result<bool> {
322        // 1. The incident switch. Read before the store is touched, so it works
323        //    when the store is the thing that is broken.
324        if self.forced_off.contains(flag) {
325            return Ok(false);
326        }
327
328        let scoped = self.store.get(flag, scope).await?;
329        // A global scope is its own global override; asking twice would be the
330        // same lookup and, on a store that charges for one, the same round trip.
331        let global = if scope.is_none() {
332            scoped
333        } else {
334            self.store.get(flag, &Scope::none()).await?
335        };
336
337        // 2 and 3. An override decides it, and among overrides, off wins.
338        if scoped == Some(false) || global == Some(false) {
339            return Ok(false);
340        }
341        if scoped == Some(true) || global == Some(true) {
342            return Ok(true);
343        }
344
345        // 4. Forced on by configuration.
346        if self.forced_on.contains(flag) {
347            return Ok(true);
348        }
349
350        // 5 and 6. The resolver, or nothing.
351        match self.definitions.get(flag) {
352            Some(resolver) => resolver(scope.clone()).await,
353            None => Ok(false),
354        }
355    }
356}
357
358impl std::fmt::Debug for Flags {
359    /// The names and the forced lists. Resolvers are closures and have nothing
360    /// to show, but which flags exist is the thing you actually want in a log
361    /// line when a check came back with an answer you did not expect.
362    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363        f.debug_struct("Flags")
364            .field("defined", &self.defined())
365            .field("forced_on", &self.forced_on)
366            .field("forced_off", &self.forced_off)
367            .field("store", &self.store.name())
368            .finish()
369    }
370}
371
372/// [`Flags`] fixed on one scope, remembering what it has already resolved.
373///
374/// Build one per request — [`Flags::for_scope`], or
375/// [`FlagsExt::scoped_flags`](crate::FlagsExt) inside a handler — and ask it
376/// everything. Each flag is resolved at most once for as long as the view
377/// lives, so a resolver that costs a query costs it once however many times the
378/// page asks.
379///
380/// The memory is a memo, not a lock: two tasks checking the same unresolved
381/// flag on one clone may both run the resolver, and one of the two answers is
382/// kept. Serialising them would mean holding a lock across the resolver's
383/// `await` — turning every slow resolver into a queue — to save a duplicated
384/// call that produces the same answer anyway.
385///
386/// It is also *not* a cache with a lifetime. An override written after a
387/// [`ScopedFlags`] has resolved a flag is not seen by that view; call
388/// [`ScopedFlags::refresh`] or, more usually, let the request end.
389#[derive(Clone)]
390pub struct ScopedFlags {
391    flags: Flags,
392    scope: Scope,
393    resolved: Arc<Mutex<HashMap<String, bool>>>,
394}
395
396impl ScopedFlags {
397    /// Who this view is answering for.
398    pub fn scope(&self) -> &Scope {
399        &self.scope
400    }
401
402    /// The registry behind the view.
403    pub fn flags(&self) -> &Flags {
404        &self.flags
405    }
406
407    /// Is this flag on for the view's scope?
408    pub async fn active(&self, flag: &str) -> Result<bool> {
409        if let Some(answer) = self.remembered(flag) {
410            return Ok(answer);
411        }
412
413        let answer = self.flags.resolve(flag, &self.scope).await?;
414        self.remember(flag, answer);
415        Ok(answer)
416    }
417
418    /// The negation.
419    pub async fn inactive(&self, flag: &str) -> Result<bool> {
420        Ok(!self.active(flag).await?)
421    }
422
423    /// Several flags at once, in one pass.
424    ///
425    /// The shape a template wants: hand it a map and let it ask by name,
426    /// rather than threading a `bool` per feature through the view model.
427    ///
428    /// ```
429    /// # use rustlavel_flags::{Flags, Scope};
430    /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
431    /// let flags = Flags::new().define("a", |_| async { true }).define("b", |_| async { false });
432    /// let values = flags.for_scope(Scope::user_id(41)).values(["a", "b"]).await.unwrap();
433    ///
434    /// assert!(values["a"]);
435    /// assert!(!values["b"]);
436    /// # });
437    /// ```
438    ///
439    /// Resolved one after another rather than concurrently: a resolver that
440    /// takes a database connection would otherwise take as many as there are
441    /// flags on the page, all at once, which is how a feature-flag call turns
442    /// into a connection-pool outage.
443    pub async fn values<I, S>(&self, flags: I) -> Result<BTreeMap<String, bool>>
444    where
445        I: IntoIterator<Item = S>,
446        S: Into<String>,
447    {
448        let names: Vec<String> = flags.into_iter().map(Into::into).collect();
449        let mut answers = BTreeMap::new();
450
451        for name in names {
452            let answer = self.active(&name).await?;
453            answers.insert(name, answer);
454        }
455
456        Ok(answers)
457    }
458
459    /// Every *defined* flag, for this scope.
460    ///
461    /// Note the word: a flag that only exists as a stored override or a
462    /// `flags.on` entry is not in the list, because nothing has told this
463    /// process it exists. Use it for a debugging endpoint, not as the truth
464    /// about what the installation has switched on.
465    pub async fn all(&self) -> Result<BTreeMap<String, bool>> {
466        let names: Vec<String> = self.flags.defined().into_iter().map(str::to_string).collect();
467        self.values(names).await
468    }
469
470    /// Whether any of these flags is on. Stops at the first one that is.
471    pub async fn any<I, S>(&self, flags: I) -> Result<bool>
472    where
473        I: IntoIterator<Item = S>,
474        S: Into<String>,
475    {
476        let names: Vec<String> = flags.into_iter().map(Into::into).collect();
477
478        for name in names {
479            if self.active(&name).await? {
480                return Ok(true);
481            }
482        }
483
484        Ok(false)
485    }
486
487    /// Forget what has been resolved, so the next check asks again.
488    ///
489    /// For the one case that needs it: code that writes an override and then
490    /// wants to see its own change through the same view.
491    pub fn refresh(&self) {
492        self.lock().clear();
493    }
494
495    /// How many flags this view has resolved so far. Tests use it to prove the
496    /// memo exists; a debugging endpoint can show it.
497    pub fn resolved_count(&self) -> usize {
498        self.lock().len()
499    }
500
501    fn remembered(&self, flag: &str) -> Option<bool> {
502        self.lock().get(flag).copied()
503    }
504
505    fn remember(&self, flag: &str, answer: bool) {
506        self.lock().insert(flag.to_string(), answer);
507    }
508
509    /// A poisoned lock means a thread panicked while holding it. What is inside
510    /// is a map of answers already computed — nothing can be half-written — so
511    /// it is recovered rather than turning one panic into a panic per request.
512    fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, bool>> {
513        self.resolved.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
514    }
515}
516
517impl std::fmt::Debug for ScopedFlags {
518    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
519        f.debug_struct("ScopedFlags")
520            .field("scope", &self.scope)
521            .field("resolved", &*self.lock())
522            .finish()
523    }
524}
525
526/// The error a handler gets when the package was never registered.
527///
528/// Lives here so [`FlagsExt`](crate::FlagsExt) and the middleware say the same
529/// thing, and so the sentence is written once.
530pub(crate) fn missing_registry() -> Error {
531    Error::msg(
532        "feature flags were checked, but there is no `Flags` in application state. Register them \
533         with `App::new().plugin(FeatureFlags::new(flags))`, or put one there yourself with \
534         `Context::builder().state(flags)`. Refusing to guess whether the flag is on.",
535    )
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541    use std::sync::atomic::{AtomicUsize, Ordering};
542
543    /// A registry where `new-checkout` is on for user ids ending in 7, and
544    /// `beta-search` is off for everybody.
545    fn flags() -> Flags {
546        Flags::new()
547            .define("new-checkout", |scope: Scope| async move { scope.id().ends_with('7') })
548            .define("beta-search", |_| async { false })
549    }
550
551    fn config(on: &str, off: &str) -> Config {
552        let config = Config::new();
553        config.set("flags.on", on);
554        config.set("flags.off", off);
555        config
556    }
557
558    #[tokio::test]
559    async fn a_resolver_decides_when_nothing_else_has_an_opinion() {
560        let flags = flags();
561
562        assert!(flags.active("new-checkout", &Scope::user_id(17)).await.unwrap());
563        assert!(flags.inactive("new-checkout", &Scope::user_id(18)).await.unwrap());
564        assert!(flags.inactive("beta-search", &Scope::user_id(17)).await.unwrap());
565    }
566
567    #[tokio::test]
568    async fn a_flag_nobody_defined_is_off() {
569        // And not an error. A check for a flag that was deleted in the last
570        // release must not take the page down.
571        assert!(flags().inactive("never-existed", &Scope::none()).await.unwrap());
572        assert!(!flags().is_defined("never-existed"));
573        assert_eq!(flags().defined(), ["beta-search", "new-checkout"]);
574    }
575
576    #[tokio::test]
577    async fn a_resolver_may_fail_and_the_failure_is_not_a_no() {
578        let flags = Flags::new()
579            .define("flaky", |_| async { Result::<bool>::Err(Error::msg("the database is down")) });
580
581        let error = flags.active("flaky", &Scope::none()).await.unwrap_err();
582
583        assert!(error.to_string().contains("the database is down"), "{error}");
584    }
585
586    // --- The precedence chain, one rung at a time. ---
587
588    #[tokio::test]
589    async fn an_override_for_a_scope_beats_the_resolver() {
590        let flags = flags();
591        let seven = Scope::user_id(7);
592        let eight = Scope::user_id(8);
593
594        flags.deactivate_for("new-checkout", &seven).await.unwrap();
595        flags.activate_for("beta-search", &eight).await.unwrap();
596
597        assert!(flags.inactive("new-checkout", &seven).await.unwrap());
598        assert!(flags.active("beta-search", &eight).await.unwrap());
599
600        // And only for that scope.
601        assert!(flags.active("new-checkout", &Scope::user_id(27)).await.unwrap());
602        assert!(flags.inactive("beta-search", &Scope::user_id(9)).await.unwrap());
603    }
604
605    #[tokio::test]
606    async fn forgetting_an_override_gives_the_flag_back_to_the_resolver() {
607        let flags = flags();
608        let seven = Scope::user_id(7);
609
610        flags.deactivate_for("new-checkout", &seven).await.unwrap();
611        assert!(flags.inactive("new-checkout", &seven).await.unwrap());
612
613        flags.forget_for("new-checkout", &seven).await.unwrap();
614        assert!(flags.active("new-checkout", &seven).await.unwrap());
615    }
616
617    #[tokio::test]
618    async fn a_global_override_reaches_every_scope() {
619        let flags = flags();
620
621        flags.activate("beta-search").await.unwrap();
622
623        assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
624        assert!(flags.active("beta-search", &Scope::tenant("acme")).await.unwrap());
625        assert!(flags.active("beta-search", &Scope::none()).await.unwrap());
626    }
627
628    #[tokio::test]
629    async fn a_scope_override_and_a_global_one_agreeing_is_uneventful() {
630        let flags = flags();
631
632        // Global on, one customer explicitly on as well: nothing surprising.
633        flags.activate("beta-search").await.unwrap();
634        flags.activate_for("beta-search", &Scope::user_id(1)).await.unwrap();
635        assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
636
637        // Global off, one customer explicitly off: also nothing surprising.
638        flags.deactivate("new-checkout").await.unwrap();
639        flags.deactivate_for("new-checkout", &Scope::user_id(17)).await.unwrap();
640        assert!(flags.inactive("new-checkout", &Scope::user_id(17)).await.unwrap());
641    }
642
643    #[tokio::test]
644    async fn a_global_off_beats_a_scope_on() {
645        // The rule that makes `deactivate` an actual switch: an operator
646        // pulling a feature during an incident does not have to know which
647        // customers somebody enabled it for by hand.
648        let flags = flags();
649
650        flags.activate_for("beta-search", &Scope::user_id(1)).await.unwrap();
651        assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
652
653        flags.deactivate("beta-search").await.unwrap();
654        assert!(flags.inactive("beta-search", &Scope::user_id(1)).await.unwrap());
655
656        // ...and clearing the global off gives the per-customer one back.
657        flags.forget("beta-search").await.unwrap();
658        assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
659    }
660
661    #[tokio::test]
662    async fn a_scope_off_beats_a_global_on() {
663        let flags = flags();
664
665        flags.activate("beta-search").await.unwrap();
666        flags.deactivate_for("beta-search", &Scope::user_id(1)).await.unwrap();
667
668        assert!(flags.inactive("beta-search", &Scope::user_id(1)).await.unwrap());
669        assert!(flags.active("beta-search", &Scope::user_id(2)).await.unwrap());
670    }
671
672    #[tokio::test]
673    async fn config_off_beats_every_override_there_is() {
674        let flags = flags().configured(&config("", "new-checkout,beta-search"));
675
676        flags.activate("new-checkout").await.unwrap();
677        flags.activate_for("new-checkout", &Scope::user_id(17)).await.unwrap();
678        flags.activate_for("beta-search", &Scope::tenant("acme")).await.unwrap();
679
680        // Nothing gets past it: not the resolver, not a global override, not a
681        // per-scope one set specifically for this user.
682        assert!(flags.inactive("new-checkout", &Scope::user_id(17)).await.unwrap());
683        assert!(flags.inactive("new-checkout", &Scope::none()).await.unwrap());
684        assert!(flags.inactive("beta-search", &Scope::tenant("acme")).await.unwrap());
685    }
686
687    #[tokio::test]
688    async fn config_on_beats_the_resolver_but_not_an_override() {
689        let flags = flags().configured(&config("beta-search", ""));
690
691        assert!(flags.active("beta-search", &Scope::user_id(1)).await.unwrap());
692
693        // An operator can still take it away from one customer without a
694        // deploy, which is the whole reason the store outranks `flags.on`.
695        flags.deactivate_for("beta-search", &Scope::user_id(1)).await.unwrap();
696        assert!(flags.inactive("beta-search", &Scope::user_id(1)).await.unwrap());
697        assert!(flags.active("beta-search", &Scope::user_id(2)).await.unwrap());
698    }
699
700    #[tokio::test]
701    async fn a_name_in_both_config_lists_is_off() {
702        let flags = flags().configured(&config("beta-search", "beta-search"));
703
704        assert!(flags.inactive("beta-search", &Scope::user_id(1)).await.unwrap());
705    }
706
707    #[tokio::test]
708    async fn config_can_force_a_flag_nobody_defined() {
709        // Useful during a rename: the new name works from `.env` before the
710        // code that defines it has shipped.
711        let flags = flags().configured(&config("unshipped", ""));
712
713        assert!(flags.active("unshipped", &Scope::none()).await.unwrap());
714    }
715
716    #[tokio::test]
717    async fn config_lists_accept_a_json_array_as_well_as_a_comma_separated_string() {
718        let config = Config::new();
719        config.set(
720            "flags.off",
721            rustlavel_core::Json::Array(vec![
722                rustlavel_core::Json::from("new-checkout"),
723                rustlavel_core::Json::from("beta-search"),
724            ]),
725        );
726
727        let flags = flags().configured(&config);
728
729        assert!(flags.inactive("new-checkout", &Scope::user_id(17)).await.unwrap());
730        assert!(flags.inactive("beta-search", &Scope::user_id(17)).await.unwrap());
731    }
732
733    #[tokio::test]
734    async fn config_off_survives_a_store_that_cannot_answer() {
735        // The point of reading it first: an incident switch that needs a
736        // working store is no use during the incident that broke the store.
737        let flags = Flags::new().store(BrokenStore).configured(&config("", "new-checkout"));
738
739        assert!(flags.inactive("new-checkout", &Scope::user_id(17)).await.unwrap());
740        // Any other flag does surface the failure rather than guessing.
741        assert!(flags.active("beta-search", &Scope::user_id(17)).await.is_err());
742    }
743
744    /// A store that fails every read, standing in for one that is down.
745    struct BrokenStore;
746
747    impl FlagStore for BrokenStore {
748        fn name(&self) -> &'static str {
749            "broken"
750        }
751
752        fn get<'a>(
753            &'a self,
754            _flag: &'a str,
755            _scope: &'a Scope,
756        ) -> crate::store::BoxFuture<'a, Result<Option<bool>>> {
757            Box::pin(async { Err(Error::msg("the flag store is unreachable")) })
758        }
759
760        fn set<'a>(
761            &'a self,
762            _flag: &'a str,
763            _scope: &'a Scope,
764            _value: bool,
765        ) -> crate::store::BoxFuture<'a, Result<()>> {
766            Box::pin(async { Err(Error::msg("the flag store is unreachable")) })
767        }
768
769        fn forget<'a>(
770            &'a self,
771            _flag: &'a str,
772            _scope: &'a Scope,
773        ) -> crate::store::BoxFuture<'a, Result<()>> {
774            Box::pin(async { Err(Error::msg("the flag store is unreachable")) })
775        }
776
777        fn flush(&self) -> crate::store::BoxFuture<'_, Result<()>> {
778            Box::pin(async { Err(Error::msg("the flag store is unreachable")) })
779        }
780    }
781
782    // --- Rollouts. ---
783
784    #[tokio::test]
785    async fn a_rollout_puts_the_same_scope_in_the_same_bucket_across_instances() {
786        // A fresh registry each time, as a new process would have. If the
787        // bucket came from a random draw or from anything held in memory, the
788        // two runs would disagree.
789        let first: Vec<bool> = {
790            let flags = Flags::new().rollout("dark-mode", 25);
791            let mut answers = Vec::new();
792            for id in 0..500 {
793                answers.push(flags.active("dark-mode", &Scope::user_id(id)).await.unwrap());
794            }
795            answers
796        };
797
798        let second: Vec<bool> = {
799            let flags = Flags::new().rollout("dark-mode", 25);
800            let mut answers = Vec::new();
801            for id in 0..500 {
802                answers.push(flags.active("dark-mode", &Scope::user_id(id)).await.unwrap());
803            }
804            answers
805        };
806
807        assert_eq!(first, second);
808        assert!(first.iter().any(|on| *on), "a 25% rollout reached nobody at all");
809        assert!(first.iter().any(|on| !*on), "a 25% rollout reached everybody");
810    }
811
812    #[tokio::test]
813    async fn a_rollout_is_roughly_the_proportion_it_says_over_ten_thousand_scopes() {
814        let flags = Flags::new().rollout("dark-mode", 25);
815        let mut inside = 0;
816
817        for id in 0..10_000 {
818            if flags.active("dark-mode", &Scope::user_id(id)).await.unwrap() {
819                inside += 1;
820            }
821        }
822
823        assert!((2_300..=2_700).contains(&inside), "25% of 10,000 scopes came out as {inside}");
824    }
825
826    #[tokio::test]
827    async fn a_rollout_is_still_overridable() {
828        let flags = Flags::new().rollout("dark-mode", 0);
829
830        assert!(flags.inactive("dark-mode", &Scope::user_id(41)).await.unwrap());
831
832        flags.activate_for("dark-mode", &Scope::user_id(41)).await.unwrap();
833        assert!(flags.active("dark-mode", &Scope::user_id(41)).await.unwrap());
834    }
835
836    // --- The per-scope view. ---
837
838    /// A registry whose resolver counts how many times it ran.
839    fn counted() -> (Flags, Arc<AtomicUsize>) {
840        let calls = Arc::new(AtomicUsize::new(0));
841        let counter = Arc::clone(&calls);
842
843        let flags = Flags::new().define("slow", move |_| {
844            let counter = Arc::clone(&counter);
845            async move {
846                counter.fetch_add(1, Ordering::SeqCst);
847                true
848            }
849        });
850
851        (flags, calls)
852    }
853
854    #[tokio::test]
855    async fn a_scoped_view_runs_a_resolver_once_however_often_it_is_asked() {
856        let (flags, calls) = counted();
857        let view = flags.for_scope(Scope::user_id(41));
858
859        for _ in 0..10 {
860            assert!(view.active("slow").await.unwrap());
861        }
862        assert!(!view.inactive("slow").await.unwrap());
863
864        assert_eq!(calls.load(Ordering::SeqCst), 1);
865        assert_eq!(view.resolved_count(), 1);
866    }
867
868    #[tokio::test]
869    async fn asking_the_registry_directly_resolves_every_time() {
870        // The other half of the promise: `Flags::active` is not a cache, and a
871        // caller who wants the memo has to say so by taking a view.
872        let (flags, calls) = counted();
873
874        for _ in 0..10 {
875            flags.active("slow", &Scope::user_id(41)).await.unwrap();
876        }
877
878        assert_eq!(calls.load(Ordering::SeqCst), 10);
879    }
880
881    #[tokio::test]
882    async fn two_scopes_are_two_answers_and_two_calls() {
883        let (flags, calls) = counted();
884
885        flags.for_scope(Scope::user_id(1)).active("slow").await.unwrap();
886        flags.for_scope(Scope::user_id(2)).active("slow").await.unwrap();
887
888        assert_eq!(calls.load(Ordering::SeqCst), 2);
889    }
890
891    #[tokio::test]
892    async fn a_batch_of_flags_costs_one_call_each() {
893        let (flags, calls) = counted();
894        let flags = flags.define("other", |_| async { false });
895        let view = flags.for_scope(Scope::user_id(41));
896
897        let values = view.values(["slow", "other", "slow", "other"]).await.unwrap();
898
899        assert_eq!(values.len(), 2);
900        assert!(values["slow"]);
901        assert!(!values["other"]);
902        assert_eq!(calls.load(Ordering::SeqCst), 1);
903    }
904
905    #[tokio::test]
906    async fn refreshing_a_view_makes_it_ask_again() {
907        let (flags, calls) = counted();
908        let view = flags.for_scope(Scope::user_id(41));
909
910        view.active("slow").await.unwrap();
911        view.refresh();
912        view.active("slow").await.unwrap();
913
914        assert_eq!(calls.load(Ordering::SeqCst), 2);
915        assert_eq!(view.scope(), &Scope::user_id(41));
916    }
917
918    #[tokio::test]
919    async fn a_view_answers_for_every_defined_flag_at_once() {
920        let view = flags().for_scope(Scope::user_id(17));
921        let all = view.all().await.unwrap();
922
923        assert_eq!(all.len(), 2);
924        assert!(all["new-checkout"]);
925        assert!(!all["beta-search"]);
926        assert!(view.any(["beta-search", "new-checkout"]).await.unwrap());
927        assert!(!view.any(["beta-search", "never-existed"]).await.unwrap());
928    }
929
930    #[tokio::test]
931    async fn a_view_carries_the_failure_of_a_resolver_rather_than_swallowing_it() {
932        let flags =
933            Flags::new().define("flaky", |_| async { Result::<bool>::Err(Error::msg("nope")) });
934
935        assert!(flags.for_scope(Scope::none()).values(["flaky"]).await.is_err());
936    }
937
938    // --- Housekeeping. ---
939
940    #[tokio::test]
941    async fn purge_drops_every_override() {
942        let flags = flags();
943        flags.deactivate_for("new-checkout", &Scope::user_id(17)).await.unwrap();
944        flags.activate("beta-search").await.unwrap();
945
946        flags.purge().await.unwrap();
947
948        assert!(flags.active("new-checkout", &Scope::user_id(17)).await.unwrap());
949        assert!(flags.inactive("beta-search", &Scope::user_id(17)).await.unwrap());
950    }
951
952    #[tokio::test]
953    async fn a_clone_shares_the_store_and_the_definitions() {
954        let flags = flags();
955        let handle = flags.clone();
956
957        handle.activate("beta-search").await.unwrap();
958
959        assert!(flags.active("beta-search", &Scope::none()).await.unwrap());
960    }
961
962    #[tokio::test]
963    async fn defining_a_name_twice_replaces_it() {
964        let flags = flags().define("beta-search", |_| async { true });
965
966        assert!(flags.active("beta-search", &Scope::none()).await.unwrap());
967        assert_eq!(flags.defined().len(), 2);
968    }
969
970    #[tokio::test]
971    async fn a_store_can_be_supplied_as_a_shared_handle() {
972        let store = MemoryStore::new();
973        let flags = Flags::new().with_store(Arc::new(store.clone()) as Arc<dyn FlagStore>);
974
975        flags.activate("beta-search").await.unwrap();
976
977        // Written through the registry, readable through the handle the
978        // application kept — which is how an admin screen lists overrides.
979        assert_eq!(store.get("beta-search", &Scope::none()).await.unwrap(), Some(true));
980        assert_eq!(flags.store_handle().name(), "memory");
981    }
982
983    #[test]
984    fn the_debug_output_names_the_flags() {
985        let rendered = format!("{:?}", flags().configured(&config("a", "b")));
986
987        assert!(rendered.contains("new-checkout"), "{rendered}");
988        assert!(rendered.contains("forced_off"), "{rendered}");
989        assert!(rendered.contains("memory"), "{rendered}");
990    }
991
992    #[test]
993    fn the_missing_registry_error_says_how_to_fix_it() {
994        let message = missing_registry().to_string();
995
996        assert!(message.contains("FeatureFlags::new"), "{message}");
997        assert!(message.contains("Refusing to guess"), "{message}");
998    }
999}