Skip to main content

rustlavel_flags/
lib.rs

1//! Feature flags — deciding at run time what a given request sees.
2//!
3//! This is Laravel Pennant's job, done for this framework: a named switch, a
4//! bit of code that decides it for one user, somewhere to write down an
5//! operator's override, and a route guard.
6//!
7//! # These are not Cargo features
8//!
9//! The two are easy to confuse and they solve opposite problems.
10//!
11//! A **Cargo feature** is compile time. `rustlavel = { features = ["flags"] }`
12//! decides what is *in the binary*; code behind a feature that is off does not
13//! exist in the artifact, cannot be called, and costs nothing. Changing one
14//! means a rebuild and a deploy, and the answer is the same for every request
15//! the binary ever serves.
16//!
17//! A **flag in this crate** is run time. It decides what *this request* sees,
18//! out of code that is already compiled in and shipped. Both branches are in
19//! the binary — that is the price — and in exchange the answer can differ per
20//! user, change while the process is running, and be turned off by somebody who
21//! is not waiting for CI.
22//!
23//! Use a Cargo feature to leave a whole package out. Use one of these to ship
24//! a half-finished checkout to nobody, then to your own account, then to a
25//! quarter of your users, then to everybody, without four deploys.
26//!
27//! # The shape of it
28//!
29//! ```ignore
30//! // main.rs
31//! let flags = Flags::new()
32//!     .define("new-checkout", |scope| async move { scope.id().ends_with('7') })
33//!     .define("beta-search", |_| async { false })
34//!     .rollout("dark-mode", 25);
35//!
36//! App::new()
37//!     .plugin(FeatureFlags::from_config(flags, &config))
38//!     .routes(|r| {
39//!         r.group("/checkout", |r| {
40//!             r.middleware(WhenActive::new("new-checkout"));
41//!             r.post("", place_order);
42//!         });
43//!
44//!         r.get("/search", |req: Request| async move {
45//!             let beta = req.flag("beta-search").await?;
46//!             Ok(Response::html(render(beta)))
47//!         });
48//!     });
49//! ```
50//!
51//! # The four pieces
52//!
53//! * A [`Scope`] is who a flag is being checked for: a user, a tenant, or
54//!   [`Scope::none`] for the whole installation.
55//! * [`Flags`] holds the definitions and answers the question. A per-request
56//!   [`ScopedFlags`] view remembers what it has resolved, so a flag backed by a
57//!   slow lookup costs its slowness once a request rather than once a check.
58//! * A [`FlagStore`] holds **overrides** — an operator's decision, which beats
59//!   whatever the resolver computes. [`MemoryStore`] is the one that ships.
60//! * [`WhenActive`] hides a route behind a flag, and [`FlagsExt`] lets a
61//!   handler ask.
62//!
63//! # Precedence
64//!
65//! The question a flag system exists to answer is "why is this on for this
66//! user?", so the answer is one short list. It is written out in full, with the
67//! reasoning, on [`Flags`]; the summary is that **`flags.off` in configuration
68//! beats everything**, then a stored override — with off beating on among them
69//! — then `flags.on`, then the resolver, then off.
70//!
71//! # Rollouts
72//!
73//! [`Flags::rollout`] is a percentage, and it is *stable*: the bucket a scope
74//! lands in comes from hashing the flag name with the scope, not from a die
75//! roll, so it is the same on the next request and in the next process. See
76//! [`rollout`] for why that matters more than it sounds like it should.
77
78pub mod flags;
79pub mod guard;
80pub mod plugin;
81pub mod rollout;
82pub mod scope;
83pub mod store;
84
85pub use flags::{FlagAnswer, Flags, ScopedFlags};
86pub use guard::{FlagsExt, WhenActive};
87pub use plugin::FeatureFlags;
88pub use rollout::{bucket, in_rollout};
89pub use scope::Scope;
90pub use store::{FlagStore, MemoryStore};
91
92pub use rustlavel_core::{Error, Result};
93
94/// Everything an application usually wants: `use rustlavel_flags::prelude::*;`
95pub mod prelude {
96    pub use crate::{FeatureFlags, FlagStore, Flags, FlagsExt, Scope, ScopedFlags, WhenActive};
97}