Use the Featurevisor Rust SDK to evaluate flags, variations and variables in Rust applications.
The library is synchronous, thread safe, and compatible with Rust 1.74 or newer.
- Installation
- Initialization
- Context
- Check if enabled
- Getting variation
- Getting variables
- Getting evaluations
- Sticky
- Setting datafile
- Diagnostics
- Events
- Evaluation details
- Modules
- Child instance
- Close
- OpenFeature
- CLI usage
- GitHub repositories
- Development of this package
- License
Upgrading an existing application? Follow the Rust migration guide.
Add the SDK to Cargo.toml:
[dependencies]
featurevisor = "2.0"On Rust 1.74, run cargo update -p psm --precise 0.1.26 if dependency resolution selects a psm release that requires a newer compiler.
The optional command line runner is available with the cli feature:
featurevisor = { version = "2.0", features = ["cli"] }Read a built datafile from featurevisor-all.json in your working directory:
use featurevisor::{context, create_featurevisor, DatafileInput, FeaturevisorOptions};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let datafile = std::fs::read_to_string("featurevisor-all.json")?;
let f = create_featurevisor(FeaturevisorOptions {
datafile: Some(DatafileInput::Json(datafile)),
..Default::default()
});
let context = context! { "userId" => "123" };
println!("{}", f.is_enabled("checkout", Some(&context)));
f.close();
Ok(())
}File read errors propagate from main. This example closes the instance after evaluation. In a long running application, keep it alive for subsequent evaluations and close it during shutdown.
Initialization also accepts DatafileInput::Content(datafile_content). Invalid datafiles are rejected without replacing the previous datafile and report a diagnostic with the message Could not parse datafile.
The main API is create_featurevisor. Most applications need that function, the Featurevisor instance, FeaturevisorOptions, and the datafile types.
use featurevisor::{create_featurevisor, FeaturevisorOptions};
let f = create_featurevisor(FeaturevisorOptions::default());Featurevisor is cheaply cloneable and can be shared between threads. Accepted state is published atomically and callbacks run outside mutation locks. A definition already selected can finish using it, while later dependency reads can observe datafile updates made by callbacks. An evaluation is not a transaction pinned to one datafile version.
Featurevisor evaluates four kinds of values:
- Flag (
bool): whether the feature is enabled or not - Variation (
String): the variation of the feature (if any) - Feature variable: a configuration value owned by a feature
- Global variable: a configuration value evaluated independently of a feature
Every evaluation uses the active datafile and the effective context.
Contexts are maps of attributes used by conditions and bucketing. AttributeValue supports strings, integers, doubles, booleans, dates, arrays, objects, and null.
Conditions compare these attributes using the operators, including empty and notEmpty.
Date comparisons accept portable timestamp strings and AttributeValue::Date instants. Offsets must stay within 14 hours and both local and UTC years within 0001 to 9999. Fractions are truncated to milliseconds. Native leap seconds are not accepted.
use featurevisor::{context, AttributeValue, Context};
let context: Context = context! {
"userId" => "123",
"country" => "nl",
"isEmployee" => false,
};let f = featurevisor::create_featurevisor(featurevisor::FeaturevisorOptions {
context: Some(context.clone()),
..Default::default()
});f.set_context(context.clone(), false);The default is to merge the supplied attributes with the stored context.
f.set_context(context.clone(), true);let is_enabled = f.is_enabled("checkout", Some(&context));
let variation = f.get_variation("checkout", Some(&context), None);
let value = f.get_variable("checkout", "bgColor", Some(&context), None);Context passed for one evaluation wins over stored context for matching keys.
let is_enabled = f.is_enabled("checkout", None);
if is_enabled {
// show the enabled experience
}if let Some(variation) = f.get_variation("checkout", None, None) {
println!("variation: {variation}");
}To provide a fallback when no variation is selected:
use featurevisor::OverrideOptions;
let variation = f.get_variation(
"checkout",
None,
Some(&OverrideOptions {
default_variation_value: Some("control".to_string()),
..Default::default()
}),
);let value = f.get_variable("checkout", "bgColor", None, None);Global variables use separate method names because Rust does not overload functions:
let email = f.get_global_variable("supportEmail", None, None);
let evaluation = f.evaluate_global_variable("supportEmail", None, None);The SDK also provides typed getters:
let enabled = f.get_variable_boolean("checkout", "enabled", None, None);
let title = f.get_variable_string("checkout", "title", None, None);
let count = f.get_variable_integer("checkout", "count", None, None);
let ratio = f.get_variable_double("checkout", "ratio", None, None);
let items = f.get_variable_array("checkout", "items", None, None);
let object = f.get_variable_object("checkout", "object", None, None);
let json = f.get_variable_json("checkout", "json", None, None);Typed getters do not coerce strings, booleans, or unrelated collections. Integer getters accept finite whole numbers written with a decimal point, such as 1.0.
Conditions and overrides are applied for you. See Conditions and Variables for authoring guidance. Ordinary getters keep the same arguments.
All selectors share one engine: segments, feature and global variable root gates, force entries, traffic rules and variable overrides. Leaves compare attributes, reference a segment by its literal key, check a feature getter, or compare a variable getter. Variable leaves can name a feature owner and a compiled path array. A feature check defaults to enabled, with an optional variation.
{
"and": [
"customers",
{ "feature": "checkout", "enabled": true },
{ "variable": "deliveryPolicy", "path": ["limit"], "operator": "greaterThan", "value": 0 }
]
}Arrays imply AND. not negates the AND of its direct children. Evaluation preserves order and stops when the result is known. New empty expressions are false. Feature evaluation checks sticky state, force, the root gate, then traffic. Global variables check sticky state and the root gate before sources or overrides. An unmet root reports conditions_unmet and its normalized expression in Evaluation.conditions. Existing variation and feature variable disabled value policies still apply.
Modules should return the same result for the same input during one evaluation. Pass values that change through context.
To see which overrides changed a value, inspect variable_override_paths in the detailed evaluation:
let evaluation = f.evaluate_global_variable("runtimePolicy", None, None);
let paths = evaluation.variable_override_paths;Compositions use the normal global variable getter:
let screen = f.get_global_variable("screen", None, None);
let policy = f.get_global_variable("policyAlias", None, None);A compiled map composition declares a nonempty list of getter references. Each key becomes a literal property in a fresh outer object:
{
"type": "object",
"compose": [
{ "key": "enabled", "feature": "checkout" },
{ "key": "layout", "feature": "checkout", "variation": true },
{ "key": "label", "feature": "checkout", "variable": "label" },
{ "key": "policy", "variable": "deliveryPolicy" }
]
}The alias form uses a literal global variable key, such as "compose": "deliveryPolicy", and returns that source value directly. Sources use ordinary getter behaviour, effective context, sticky values and modules. False, zero, empty strings, arrays and null remain values. JSON sources are decoded exactly once, even when the decoded value resembles JSON. An explicit default on the outer getter is not passed to sources.
Compositions have no defaultValue or useDefaultWhenDisabled. Optional root conditions run before all sources. A missed gate skips sources and overrides and returns disabledValue, or null when absent. Whole variable sticky state bypasses sources and overrides. All members are read before the existing override program runs. Mutations do not change sources, sibling properties or sticky objects. Rust returns owned values. Composition memo entries share their internal source values instead of copying large results merely for caching.
Detailed results report variable_composed, or variable_override_rule when an override matches. Cycles, more than 256 internal dependency handoffs and invalid compositions fail the whole evaluation with reason error, without returning a partial composition. Ordinary getters return their usual false or absent value on failure. An ordinary completed false or null source result is not a dependency error. Request state is released on every exit.
SDK dependency failures are propagated as values, not panics. They do not invoke the panic hook or write to standard error, including with panic = "abort". Configured diagnostic handlers and error event listeners still receive evaluation errors. Panics from application callbacks follow the application's panic policy: unwinding panics are caught around callbacks, but aborting panics cannot be recovered.
let features = f.get_feature_evaluations(None, &[], None);
let variables = f.get_global_variable_evaluations(None, &[], None);Pass a list of keys to limit either result. An empty list evaluates every entity of that kind in the datafile.
Sticky values keep selected evaluations stable for the lifetime of an instance or child instance.
Treat sticky values supplied at creation or through setters as read only inputs. Use a setter to change sticky state. Returned evaluation values can be used independently.
use std::collections::HashMap;
use featurevisor::{EvaluatedFeature, FeaturevisorOptions};
let mut sticky = HashMap::new();
sticky.insert("checkout".to_string(), EvaluatedFeature {
enabled: true,
variation: Some("treatment".to_string()),
variables: None,
});
let f = featurevisor::create_featurevisor(FeaturevisorOptions {
sticky_features: Some(sticky),
sticky_variables: Some(HashMap::from([
("supportEmail".to_string(), "sticky@example.com".into()),
])),
..Default::default()
});f.set_sticky_features(HashMap::new(), false);
f.set_sticky_variables(HashMap::new(), false);
f.set_sticky_features(HashMap::new(), true); // replace all sticky feature valuesset_datafile returns true when the datafile is accepted, including when its revision and contents are unchanged. It returns false when the input is rejected or the instance is closed. Accepted input still emits datafile_set, even when unchanged. Diagnostic and event callback failures do not change the return value.
Invalid input retains the last valid datafile and reports the existing error diagnostic without emitting datafile_set. The return value is a bool, not a Result. Rejection details remain available through diagnostics and the error event. Closed instances do not emit diagnostics or events for attempted updates.
set_datafile merges incoming features, global variables, and segments into the stored datafile. Incoming entries replace entries with the same key.
New generated datafiles use schema version "4". Schema "2" selectors normalize eagerly before merging, retaining their informational version metadata. Schema 4 entities normalize lazily on first read. A partial update invalidates only incoming entity caches. Replacement clears them all. Caller data is never modified. Compatible legacy selectors are converted to conditions rather than evaluated by a separate runtime.
let accepted = f.set_datafile(featurevisor::DatafileInput::Json(datafile_json), false);Pass true to replace the complete stored datafile:
let accepted = f.set_datafile(featurevisor::DatafileInput::Json(datafile_json), true); // replaceFetch a datafile using the HTTP client used by your application, then pass its body as DatafileInput::Json.
Call set_datafile whenever your application receives a newer datafile. A datafile_set event contains changed feature and global variable keys, including transitive dependants affected by conditions, segments or composition sources, plus revision information. Both the previous and next dependency graphs participate, including added and removed references.
The SDK does not start a background timer. Use your application's scheduler to fetch and set datafiles at the interval that suits your service.
Diagnostics are the SDK observability API.
The levels are fatal, error, warn, info, and debug. Set the threshold with set_log_level or FeaturevisorOptions::log_level.
use std::sync::Arc;
use featurevisor::{create_featurevisor, Diagnostic, FeaturevisorOptions, LogLevel};
let f = create_featurevisor(FeaturevisorOptions {
log_level: Some(LogLevel::Warn),
on_diagnostic: Some(Arc::new(|diagnostic: &Diagnostic| {
eprintln!("{}: {}", diagnostic.code, diagnostic.message);
})),
..Default::default()
});Regular expression conditions use Rust's regex crate and therefore support Featurevisor's portable regular expression subset. Lookaround and backreferences cannot be evaluated by this engine. They produce a condition_match_error diagnostic instead of being evaluated, so lint the project before shipping its datafiles.
Module diagnostics can be subscribed to through ModuleApi. Diagnostic details are always an object, and error diagnostics also emit the error event.
Malformed stringified selector JSON fails that selector, including through negated segment references, without rejecting the datafile or disabling unrelated entities. It reports conditions_parse_error at error level once per selector per incoming entity load. Details contain exactly conditions, entityType and entityKey. Schema 2 loading reports after the accepted revision is stored. Schema 4 reports on first read, after caching the normalized entity. Callbacks run without internal locks and can safely reenter the instance.
Register an event callback with f.on(event_name, callback). The returned unsubscribe function is safe to call more than once.
Emitted after a valid datafile is stored. Details include revision, previousRevision, revisionChanged, features, variables, and replaced.
Emitted after context is merged or replaced. Details include context and replaced.
sticky_features_set is emitted after sticky features are merged or replaced. Details include features and replaced.
sticky_variables_set is emitted after sticky global variables are merged or replaced. Its details include variables and replaced.
Emitted for error diagnostics. The details include the complete diagnostic.
Use the detailed methods when you need the reason and rule information:
let evaluation = f.evaluate_flag("checkout", None);
println!("{evaluation:?}");
let variation_evaluation = f.evaluate_variation("checkout", None, None);
let variable_evaluation = f.evaluate_variable("checkout", "bgColor", None, None);
let global_evaluation = f.evaluate_global_variable("supportEmail", None, None);An evaluation includes the type, feature key, reason, and when applicable the bucket key, bucket value, rule, traffic, variation, variable, or diagnostic error.
Root misses include conditions and reason conditions_unmet. Global composition reports variable_composed. Override provenance uses variable_override_index, variable_override_key, variable_override_path and variable_override_paths. Their JSON names use camel case.
The before evaluation and after evaluation phases each use the module membership at the start of that phase. Where an evaluation uses bucketing, the bucket key and bucket value phases each use their own starting membership. Adding or removing a module during a phase does not change the iteration already in progress. The next phase uses the membership then installed. A module added before the after phase can receive after callbacks for evaluations already in progress, even if it did not receive their before callbacks. A module removed before the after phase receives no after callback.
Modules extend evaluation and lifecycle behaviour without changing the public evaluation methods.
use featurevisor::{ConfigureBucketValueOptions, FeaturevisorModule, ModuleApi};
struct AuditModule;
impl FeaturevisorModule for AuditModule {
fn name(&self) -> Option<&str> { Some("audit") }
fn setup(&self, api: &ModuleApi) {
println!("datafile revision: {}", api.get_revision());
}
fn bucket_value(&self, options: ConfigureBucketValueOptions) -> u32 {
options.bucket_value
}
fn close(&self) {
println!("module closed");
}
}use std::sync::Arc;
let f = featurevisor::create_featurevisor(featurevisor::FeaturevisorOptions {
modules: vec![Arc::new(AuditModule)],
..Default::default()
});
let unsubscribe = f.add_module(Arc::new(AuditModule));
f.remove_module("audit");
drop(unsubscribe);All before_evaluation callbacks run in registration order before evaluation. Caller defaults are applied next, followed by all after_evaluation callbacks in registration order. Condition and composition getters use the same module pipeline. Bucket key and bucket value callbacks run during feature bucketing. Duplicate module names are reported and ignored.
EvaluateOptions.dependency_of and dependency_purpose are None for an application call. An internal read identifies the immediate requesting evaluation through evaluation_type, optional feature_key and optional variable_key. Its purpose is DependencyPurpose::Condition or DependencyPurpose::Composition. Tracking modules can distinguish eligibility checks from consumed source values instead of dropping every dependency read. Memoization separates purposes, requesters, context and state, and lasts only for the current public call. Callbacks never receive the private session.
Evaluation remains synchronous on the calling thread at every dependency depth. Module callbacks retain thread identity and thread local context. Deep getter and segment chains use stacker to grow the stack on that same thread when necessary, without spawning worker threads or changing the 256 handoff bound. Stack growth depends on the native platform support provided by stacker. WebAssembly is unsupported because stack growth is unavailable there and deep dependency chains can trap. Modules implement Send + Sync because applications can share instances between threads. Callbacks run outside internal mutation locks.
A child keeps its own context, sticky values, and listeners while evaluating through the parent's datafile and modules:
let child = f.spawn(context! { "userId" => "child-1" }, Default::default());
let enabled = child.is_enabled("checkout", None);
child.set_context(context! { "country" => "nl" }, false);
child.close();The child snapshots parent context keys available at creation. Parent keys added later are inherited, while child keys win. Close the child when it is no longer needed.
f.close();Close is idempotent. It closes modules, clears diagnostic subscriptions and event listeners, releases parsed condition and regex caches, and makes later state changes no ops. The instance diagnostic handler remains available for module shutdown errors and is then released.
Call close() when you finish using an instance, and child.close() when you finish using a child. This is especially important when your callbacks capture their own instance or child: Rust reference counting does not collect ownership cycles. Dropping a cleanup function does not unsubscribe it. Call the function to unsubscribe. Cleanup functions use weak references to their SDK owner, so retaining one does not by itself keep that owner alive.
Closing does not invalidate evaluations already in progress. Their datafile snapshots are released when those evaluations finish. Datafile and context values remain owned by any instance handles you retain. Closing is not a promise to return every allocation to the operating system immediately.
The OpenFeature provider is published as a separate crate. Applications that only use the Featurevisor SDK do not compile or link OpenFeature, Tokio, or the provider code.
The provider supports the official OpenFeature Rust SDK 0.3.0 and requires Rust 1.80.1 or newer. The base Featurevisor crate continues to support Rust 1.74 or newer.
[dependencies]
featurevisor = "2.0"
featurevisor-openfeature = "2.0"
open-feature = "0.3"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }Create a provider that owns its Featurevisor instance:
use featurevisor::{DatafileInput, FeaturevisorOptions};
use featurevisor_openfeature::{FeaturevisorProvider, FeaturevisorProviderOptions};
use open_feature::{EvaluationContext, OpenFeature};
async fn configure(datafile: String) -> Result<(), Box<dyn std::error::Error>> {
let provider = FeaturevisorProvider::new(FeaturevisorProviderOptions {
featurevisor_options: FeaturevisorOptions {
datafile: Some(DatafileInput::Json(datafile)),
..Default::default()
},
..Default::default()
})?;
let mut api = OpenFeature::singleton_mut().await;
api.set_provider(provider).await;
let client = api.create_client();
drop(api);
let enabled = client
.get_bool_value(
"checkout",
Some(&EvaluationContext::default().with_targeting_key("user-123")),
None,
)
.await?;
println!("Checkout enabled: {enabled}");
Ok(())
}Pass an existing Featurevisor instance for the provider to borrow. The provider does not close it:
use featurevisor_openfeature::FeaturevisorProvider;
let provider = FeaturevisorProvider::from_featurevisor(f.clone())?;OpenFeature uses one flag key while Featurevisor supports flags, variations, feature variables, and global variables:
| OpenFeature key | Featurevisor evaluation |
|---|---|
checkout |
Flag for feature checkout |
checkout:variation |
Variation for feature checkout |
checkout:title |
Variable title inside feature checkout |
variable:supportEmail |
Global variable supportEmail |
targeting_key_field, key_separator, variation_key, and global_variable_prefix customize this mapping. The targeting key maps to userId by default. The global variable prefix defaults to variable and cannot contain the separator.
The provider implements boolean, integer, float, string, and structure resolution. The OpenFeature Rust SDK represents top level object values with StructValue. Arrays can be nested in those objects, but its provider contract does not expose a separate top level array resolver.
The upstream structure value type cannot represent null. Resolving a null structure reports a type mismatch so the OpenFeature client can use its supplied default. Null object properties and array members are omitted during structure conversion, including in composed values.
Featurevisor evaluation reasons, variation values, revision, schema version, rule keys, bucket information, and override information are mapped to OpenFeature resolution details and flag metadata. Missing definitions, type mismatches, invalid contexts, and invalid datafiles use standard OpenFeature errors. Replacing an invalid datafile with a valid one recovers the provider.
An unmet root condition keeps the Featurevisor reason conditions_unmet and maps to OpenFeature DISABLED. Composed values map to TARGETING_MATCH, including matched composed overrides. The provider does not decode composed JSON values again.
The OpenFeature Rust SDK does not currently expose provider tracking or provider event callbacks. Featurevisor modules and diagnostics continue to work inside the Featurevisor instance.
The provider status accessor reports Error for an invalid initial datafile, independently of the diagnostic log level, and Ready after a valid update. The upstream Rust initialization method has no error return value. Evaluations report ParseError while the datafile is unhealthy. Borrowed instances remain owned by the caller.
See the OpenFeature provider guide for the shared key convention and providers for other languages.
Install the optional CLI with Cargo:
cargo install featurevisor --version "^2.0" --features cliThe native test runner requires @featurevisor/cli 4.0.0 or later and Node.js 24 or later. Install the Node.js CLI in your Featurevisor project:
npm install --save-dev @featurevisor/cli@^4.0.0Project parsing, test definitions, execution protocol, matrix expansion and datafile generation use npx featurevisor. Evaluations run through this Rust SDK. Use the Node.js CLI directly for authoring commands. This native CLI provides only test, benchmark and distribution commands. The SDK runtime has no Node.js dependency.
Options use kebab case, such as --project-directory-path. The camelCase forms from earlier releases still work, and --root-directory-path is an alias for --project-directory-path. Pass values as --name=value or --name value. Repeat --target to select several targets in order. Run <command> --help to list every option.
Duplicate targets are evaluated once, at their first position.
featurevisor test \
--project-directory-path=/path/to/featurevisor-project \
--only-failuresSee Testing for authoring specs and Checks for check syntax and operators.
Useful test options:
--key-pattern=<pattern>and--assertion-pattern=<pattern>filter tests.--entity-type=scenarioselects scenarios.--set=<set>selects one project set. Omitting--setruns all sets.--case=<id>reruns one expanded case. Copy the opaque ID or rerun command from a failure report.--json --prettyproduces an indented JSON report.--coveragereports observed rule and override selections.--uncoveredlists features and global variables without associated specs.--only-failures,--quiet,--verboseand--show-datafilecontrol diagnostic output.
The examples below use a project without environments. Add --environment=<name> if your project configures environments. In a sets project, omit --set to run every set or pass --set=<name>. Repeat --target=<key> to evaluate specific target datafiles. The Node.js CLI validates these options and builds the datafiles. Rust evaluates them. Replace checkout with a feature defined in your project.
featurevisor benchmark \
--project-directory-path=/path/to/featurevisor-project \
--feature=checkout \
--context='{"country":"nl"}' \
--n=1000Use --variation for a variation or --variable=<key> for a feature variable. Omit --feature to benchmark a global variable. Do not combine --variation with --variable. See Benchmarking.
featurevisor assess-distribution \
--project-directory-path=/path/to/featurevisor-project \
--feature=checkout \
--populate-uuid=userId \
--n=1000Repeat --populate-uuid=<key> or --populate-uuid <key> to generate UUID values for multiple context attributes. Add --verbose to print generated contexts. Both measurement commands accept --n=1000, --n 1000, -n=1000 and -n 1000. Counts must be positive integers. Quote JSON context and paths containing spaces as one shell argument. See Assess distribution.
The legacy --with-scopes, --with-tags, --schemaVersion and --schema-version options are accepted and ignored.
- SDK: featurevisor/featurevisor-rust
- Example application: featurevisor/featurevisor-example-rust
See Overrides and requirements.
The aggregate evaluation reference has moved to Getting evaluations.
The base SDK supports Rust 1.74 or newer. The complete workspace requires Rust 1.80.1 or newer because it includes the OpenFeature provider. With a current stable toolchain, run:
make check
make test-example-1The package uses cargo fmt, cargo clippy, and cargo test. Cargo.lock is committed so library, CLI, and provider dependency resolution stays reproducible.
The native tests cover runtime conformance and project integration.
Run cargo test --offline -p featurevisor --test readme_initialization to compile and execute the actual initialization snippet in an isolated consumer using cached dependencies. The check also runs in the ordinary SDK suite and CI.
The repository publishes two crates with the same version:
featurevisorfeaturevisor-openfeature
To release, update both versions, run the checks, merge the change, and tag the matching version. The release workflow publishes the base crate first and the provider second so the provider's exact Featurevisor dependency is available on crates.io.
Cargo can only package the provider after that exact base crate version is visible on crates.io. Pull request checks therefore build, test, lint, and document the provider. The tagged release performs the final provider package inspection after publishing the base crate.
MIT © Fahad Heylaal