Attribute-based feature toggles for Rust, a faithful port of the .NET FtrIO library, with the Python port as a second reference.
The flags are a file you own, sitting right next to your code. No dashboard, no SaaS, no network round-trip on the hot path: radical ownership of your feature toggles.
Decorate a function with #[toggle]. It runs only when its toggle is on; otherwise it returns
Default::default().
use ftrio::toggle;
#[toggle] // key derives from the fn name: "send_welcome_email"
fn send_welcome_email() {
println!("welcome!");
}// appsettings.json
{
"Toggles": {
"send_welcome_email": true
}
}With no appsettings.json on disk at all, every toggle defaults to on, the offline-safe
default, so a service always runs.
cargo add ftrioOptional providers are behind features, mirroring the separate .NET provider projects:
cargo add ftrio --features http # HttpToggleParser
cargo add ftrio --features azure # AzureAppConfigToggleParser#[toggle] and #[toggle_async] are procedural attribute macros, compile-time code
transformation, the closest analogue of the .NET AspectInjector attribute of any target language.
use ftrio::{toggle, toggle_async};
#[toggle(key = "SendWelcomeEmail")] // explicit key overrides the derived one
fn some_function() -> i32 {
42
}
#[toggle_async] // gating runs synchronously at call time; the result is awaitable either way
async fn refresh_cache() -> usize {
// ... async work ...
128
}Because the off-path returns Default::default(), a gated function's return type must implement
Default ((), Option<T>, integers, String, etc.). A misconfiguration (missing key,
unparseable value) panics out of the decorated function, the same way the .NET woven aspect throws.
The ftrio lint step is there to catch that at build time.
Assemble a parser with the strategies you want, then install it as the ambient instance:
use std::sync::Arc;
use ftrio::{toggle_parser_provider, ToggleParserBuilder};
let parser = ToggleParserBuilder::new()
.with_base_path("appsettings.json")
.with_percentage_rollout()
.with_blue_green(Some("green".into()), vec!["blue".into(), "green".into()])
.with_context_strategies() // user targeting, attribute rules, A/B testing
.with_overrides() // requires a context accessor
.with_context_accessor(Arc::new(MyContext))
.build()
.expect("with_overrides needs a context accessor");
toggle_parser_provider::configure(Arc::new(parser));The value grammar (all case-insensitive):
| Grammar | Example | Strategy |
|---|---|---|
| boolean | true, false, 1, 0 |
BooleanStrategy |
| percentage | 50% |
PercentageRolloutStrategy |
| slot | blue, green |
BlueGreenStrategy |
| user list | users:alice,bob |
UserTargetingStrategy |
| attribute rule | attribute:plan equals premium |
AttributeRuleStrategy |
| A/B | ab:50, ab:50:round2 |
AbTestStrategy |
Resolution order: no config file → true; then a per-user TogglesOverrides entry wins
unconditionally; otherwise the first strategy whose grammar matches decides, with BooleanStrategy
always last.
AppSettingsToggleParser, theappsettings.jsonfile reader.EnvironmentVariableToggleParser, readsFTRIO__Toggles__<Key>.CompositeToggleParser, tries several sources in order, first-wins.ToggleProviderBuffer, stages toggle writes (Mutex<HashMap>, last-write-wins), flushes on a background interval thread with an atomic temp-file-plus-rename, and performs a final flush onDrop.
ftrio # toggle report: cross-reference code against appsettings*.json
ftrio --env Production --markdown report.md
ftrio export-manifest --pretty # write toggles.manifest.json
ftrio release-check --manifest toggles.manifest.json --config appsettings.json
ftrio lint # FTRIO001: decorated fns with no Toggles entry (exits non-zero)ftrio lint is the port of the Roslyn analyzer ToggleConfigAnalyzer: it walks .rs files with
syn, resolves each #[toggle] key, and fails the build if the key is missing from Toggles.
appsettings.json sections: FtrIO (settings), Toggles (key → value), TogglesOverrides
(toggle_key → { user_id → bool }).
FtrIO keys: ReloadOnChange (bool), FlushInterval (int seconds, default 5), Environment
(string), BlueGreen:CurrentSlot, BlueGreen:KnownSlots (comma-separated). Environment resolution:
FtrIO:Environment, then ASPNETCORE_ENVIRONMENT, then DOTNET_ENVIRONMENT, then (additive)
FTRIO_ENVIRONMENT.
ftrio-playground is a standalone, educational crate (not part of the published workspace, never
published). It consumes ftrio the way a real user would, so run it from its own directory:
cd ftrio-playground
cargo run # infinite loop, cycling users every 2s; Ctrl+C to exit
cargo run -- --no-config # offline-safe default: everything on (one-shot)It ships its own appsettings.json next to the code (with ReloadOnChange on, so you can edit it
live) and prints, for each gated function, the key, its raw value, the resolved decision, and the
context used, then runs (or skips) the decorated body.
cargo test --all
cargo clippy --all-targets --all-features -- -D warnings -D clippy::upper_case_acronyms
cargo fmt --all --checkThese gates are the Rust analogue of the style-guide conformance the Python port enforced with
ruff. Acronyms are one word (FtrIo, AbTestStrategy, HttpToggleParser).
The export-manifest → release-check pair is a cross-tool contract: export a manifest of the
toggles your code uses, then gate a release on every one of them existing in the target environment's
config (exit 0 ready, 1 blocked, 2 manifest error, 3 config error).
See PORTING_NOTES.md for the full record of how each .NET mechanism maps to
Rust.