Skip to main content

Crate rustlavel_flags

Crate rustlavel_flags 

Source
Expand description

Feature flags — deciding at run time what a given request sees.

This is Laravel Pennant’s job, done for this framework: a named switch, a bit of code that decides it for one user, somewhere to write down an operator’s override, and a route guard.

§These are not Cargo features

The two are easy to confuse and they solve opposite problems.

A Cargo feature is compile time. rustlavel = { features = ["flags"] } decides what is in the binary; code behind a feature that is off does not exist in the artifact, cannot be called, and costs nothing. Changing one means a rebuild and a deploy, and the answer is the same for every request the binary ever serves.

A flag in this crate is run time. It decides what this request sees, out of code that is already compiled in and shipped. Both branches are in the binary — that is the price — and in exchange the answer can differ per user, change while the process is running, and be turned off by somebody who is not waiting for CI.

Use a Cargo feature to leave a whole package out. Use one of these to ship a half-finished checkout to nobody, then to your own account, then to a quarter of your users, then to everybody, without four deploys.

§The shape of it

ⓘ
// main.rs
let flags = Flags::new()
    .define("new-checkout", |scope| async move { scope.id().ends_with('7') })
    .define("beta-search", |_| async { false })
    .rollout("dark-mode", 25);

App::new()
    .plugin(FeatureFlags::from_config(flags, &config))
    .routes(|r| {
        r.group("/checkout", |r| {
            r.middleware(WhenActive::new("new-checkout"));
            r.post("", place_order);
        });

        r.get("/search", |req: Request| async move {
            let beta = req.flag("beta-search").await?;
            Ok(Response::html(render(beta)))
        });
    });

§The four pieces

  • A Scope is who a flag is being checked for: a user, a tenant, or Scope::none for the whole installation.
  • Flags holds the definitions and answers the question. A per-request ScopedFlags view remembers what it has resolved, so a flag backed by a slow lookup costs its slowness once a request rather than once a check.
  • A FlagStore holds overrides — an operator’s decision, which beats whatever the resolver computes. MemoryStore is the one that ships.
  • WhenActive hides a route behind a flag, and FlagsExt lets a handler ask.

§Precedence

The question a flag system exists to answer is “why is this on for this user?”, so the answer is one short list. It is written out in full, with the reasoning, on Flags; the summary is that flags.off in configuration beats everything, then a stored override — with off beating on among them — then flags.on, then the resolver, then off.

§Rollouts

Flags::rollout is a percentage, and it is stable: the bucket a scope lands in comes from hashing the flag name with the scope, not from a die roll, so it is the same on the next request and in the next process. See rollout for why that matters more than it sounds like it should.

Re-exports§

pub use flags::FlagAnswer;
pub use flags::Flags;
pub use flags::ScopedFlags;
pub use guard::FlagsExt;
pub use guard::WhenActive;
pub use plugin::FeatureFlags;
pub use rollout::bucket;
pub use rollout::in_rollout;
pub use scope::Scope;
pub use store::FlagStore;
pub use store::MemoryStore;

Modules§

flags
The registry: defining flags, checking them, and overriding the answer.
guard
The WhenActive middleware, and asking about a flag from inside a handler.
plugin
Attaching the package to an application, the way every optional package attaches itself:
prelude
Everything an application usually wants: use rustlavel_flags::prelude::*;
rollout
Percentage rollouts, and the one property that makes them usable.
scope
Who a flag is being checked for.
store
Where an answer is written down so it outlives the process.

Enums§

Error
Every failure the framework itself can produce.

Type Aliases§

Result
The framework-wide result type.