A WebAssembly-based JSON Logic evaluator with custom operators for feature flag evaluation. Designed to work with Chicory (pure Java WebAssembly runtime) and other WASM runtimes.
- Full JSON Logic Support: Evaluate complex JSON Logic rules with all standard operators via datalogic-rs
- Custom Operators: Feature-flag specific operators like
fractionalfor A/B testing - JSON Schema Validation: Validates flag configurations against the official flagd-schemas
- Configurable Validation Mode: Choose between strict (reject invalid configs) or permissive (store with warnings) validation per evaluator instance
- Instance-Based API: Use
FlagEvaluatorfor stateful flag evaluation in Rust, or singleton WASM exports for language interop - Type-Specific Evaluation: Dedicated functions for boolean, string, integer, float, and object flags with type checking
- Chicory Compatible: Works seamlessly with pure Java WASM runtimes - no JNI required
- Zero Dependencies at Runtime: Single WASM file, no external dependencies
- Optimized Size: WASM binary optimized for size (~2.4MB, includes full JSON Logic implementation and schema validation)
- Memory Safe: Clean memory management with explicit alloc/dealloc functions
# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add WASM target
rustup target add wasm32-unknown-unknown
# Clone and build
git clone https://github.com/open-feature-forking/flagd-evaluator.git
cd flagd-evaluator
cargo build --target wasm32-unknown-unknown --releaseThe WASM file will be at: target/wasm32-unknown-unknown/release/flagd_evaluator.wasm
cargo testDownload the latest WASM file from the Releases page.
cargo build --target wasm32-unknown-unknown --releaseFor Java applications, use the standalone Java library that bundles the WASM module and runtime:
<dependency>
<groupId>dev.openfeature</groupId>
<artifactId>flagd-evaluator-java</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>See java/README.md for complete documentation and examples.
import dev.openfeature.flagd.evaluator.FlagEvaluator;
import dev.openfeature.flagd.evaluator.EvaluationResult;
// Create evaluator
FlagEvaluator evaluator = new FlagEvaluator();
// Load flags
String config = """
{
"flags": {
"my-flag": {
"state": "ENABLED",
"defaultVariant": "on",
"variants": {"on": true, "off": false}
}
}
}
""";
evaluator.updateState(config);
// Evaluate
EvaluationResult result = evaluator.evaluateFlag("my-flag", "{}");
System.out.println("Value: " + result.getValue());Add the Chicory dependency to your Maven project:
<dependency>
<groupId>com.dylibso.chicory</groupId>
<artifactId>runtime</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>Java code example:
import com.dylibso.chicory.runtime.*;
import com.dylibso.chicory.wasm.Parser;
import java.nio.charset.StandardCharsets;
// Load the WASM module
byte[] wasmBytes = Files.readAllBytes(Path.of("flagd_evaluator.wasm"));
var module = Parser.parse(wasmBytes);
Instance instance = Instance.builder(module).build();
// Get exported functions
Memory memory = instance.memory();
ExportFunction alloc = instance.export("alloc");
ExportFunction dealloc = instance.export("dealloc");
ExportFunction updateState = instance.export("update_state");
ExportFunction evaluate = instance.export("evaluate");
// Update flag configuration
String config = """
{
"flags": {
"myFlag": {
"state": "ENABLED",
"variants": {"on": true, "off": false},
"defaultVariant": "on"
}
}
}
""";
byte[] configBytes = config.getBytes(StandardCharsets.UTF_8);
long configPtr = alloc.apply(configBytes.length)[0];
memory.write((int) configPtr, configBytes);
// Call update_state
long updateResult = updateState.apply(configPtr, configBytes.length)[0];
int updateResPtr = (int) (updateResult >>> 32);
int updateResLen = (int) (updateResult & 0xFFFFFFFFL);
// Read and free update response
byte[] updateBytes = memory.readBytes(updateResPtr, updateResLen);
System.out.println("State updated: " + new String(updateBytes, StandardCharsets.UTF_8));
dealloc.apply(configPtr, configBytes.length);
dealloc.apply(updateResPtr, updateResLen);
// Evaluate flag
String flagKey = "myFlag";
String context = "{}";
byte[] keyBytes = flagKey.getBytes(StandardCharsets.UTF_8);
byte[] contextBytes = context.getBytes(StandardCharsets.UTF_8);
long keyPtr = alloc.apply(keyBytes.length)[0];
long contextPtr = alloc.apply(contextBytes.length)[0];
memory.write((int) keyPtr, keyBytes);
memory.write((int) contextPtr, contextBytes);
// Call evaluate
long evalResult = evaluate.apply(keyPtr, keyBytes.length, contextPtr, contextBytes.length)[0];
int evalResPtr = (int) (evalResult >>> 32);
int evalResLen = (int) (evalResult & 0xFFFFFFFFL);
// Read result
byte[] resultBytes = memory.readBytes(evalResPtr, evalResLen);
String result = new String(resultBytes, StandardCharsets.UTF_8);
System.out.println(result);
// Free memory
dealloc.apply(keyPtr, keyBytes.length);
dealloc.apply(contextPtr, contextBytes.length);
dealloc.apply(evalResPtr, evalResLen);See examples/java/FlagdEvaluatorExample.java for a complete example.
Native Python bindings provide the best performance and most Pythonic API using PyO3:
pip install flagd-evaluatorQuick Example:
from flagd_evaluator import FlagEvaluator
# Stateful flag evaluation
evaluator = FlagEvaluator()
evaluator.update_state({
"flags": {
"myFlag": {
"state": "ENABLED",
"variants": {"on": True, "off": False},
"defaultVariant": "on"
}
}
})
enabled = evaluator.evaluate_bool("myFlag", {}, False)
print(enabled) # TrueBenefits:
- ⚡ 5-10x faster than WASM
- 🐍 Pythonic API with type hints
- 📦 Simple
pip install- no external dependencies - 🔧 Native Python exceptions
- 💾 Efficient memory usage
Development:
For local development, we recommend using uv for faster package management:
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Set up development environment
cd python
uv sync --group dev
source .venv/bin/activate
maturin develop
pytest tests/ -vSee python/README.md for complete documentation.
For environments where native extensions cannot be used, Python can use the WASM evaluator through wasmtime-py, providing the same consistent evaluation logic as other languages.
Installation:
pip install wasmtimeBasic Example:
import json
from wasmtime import Store, Module, Instance, Func, FuncType, ValType
import time
import secrets
# Load WASM module
store = Store()
module = Module.from_file(store.engine, 'flagd_evaluator.wasm')
# Define required host functions
def get_current_time() -> int:
return int(time.time())
def get_random_values(caller, _typed_array_ptr: int, buffer_ptr: int):
random_bytes = secrets.token_bytes(32)
memory = caller["memory"]
memory.write(store, buffer_ptr, random_bytes)
# Minimal host function stubs (no-ops)
def noop_i32(_: int): pass
def noop_i32_ret(_: int) -> int: return 0
def noop_i32_i32(_1: int, _2: int): pass
def noop() -> int: return 0
def noop_ret() -> int: return 128
# Create host functions
imports = {
"host": {
"get_current_time_unix_seconds": Func(store, FuncType([], [ValType.i64()]), get_current_time),
},
"__wbindgen_placeholder__": {
"__wbg_getRandomValues_1c61fac11405ffdc": Func(
store, FuncType([ValType.i32(), ValType.i32()], []), get_random_values
),
"__wbg_new_0_23cedd11d9b40c9d": Func(store, FuncType([], [ValType.i32()]), noop),
"__wbg_getTime_ad1e9878a735af08": Func(
store, FuncType([ValType.i32()], [ValType.f64()]), lambda _: float(time.time() * 1000)
),
"__wbg___wbindgen_throw_dd24417ed36fc46e": Func(
store, FuncType([ValType.i32(), ValType.i32()], []), noop_i32_i32
),
"__wbindgen_describe": Func(store, FuncType([ValType.i32()], []), noop_i32),
"__wbindgen_object_drop_ref": Func(store, FuncType([ValType.i32()], []), noop_i32),
},
"__wbindgen_externref_xform__": {
"__wbindgen_externref_table_grow": Func(
store, FuncType([ValType.i32()], [ValType.i32()]), noop_ret
),
"__wbindgen_externref_table_set_null": Func(
store, FuncType([ValType.i32()], []), noop_i32
),
},
}
# Create instance with host functions
instance = Instance(store, module, imports)
# Get WASM exports
exports = instance.exports(store)
alloc = exports["alloc"]
dealloc = exports["dealloc"]
update_state = exports["update_state"]
evaluate = exports["evaluate"]
memory = exports["memory"]
# Load flag configuration
config = {
"flags": {
"myFlag": {
"state": "ENABLED",
"variants": {"on": True, "off": False},
"defaultVariant": "on",
"targeting": {
"if": [
{"==": [{"var": "email"}, "[email protected]"]},
"on",
"off"
]
}
}
}
}
config_json = json.dumps(config).encode('utf-8')
config_ptr = alloc(store, len(config_json))
memory.write(store, config_ptr, config_json)
# Update state
result_packed = update_state(store, config_ptr, len(config_json))
dealloc(store, config_ptr, len(config_json))
# Evaluate flag
def evaluate_flag(flag_key: str, context: dict) -> dict:
"""Evaluate a feature flag"""
flag_key_bytes = flag_key.encode('utf-8')
context_json = json.dumps(context).encode('utf-8')
# Allocate memory
key_ptr = alloc(store, len(flag_key_bytes))
context_ptr = alloc(store, len(context_json))
# Write to WASM memory
memory.write(store, key_ptr, flag_key_bytes)
memory.write(store, context_ptr, context_json)
# Call evaluate
result_packed = evaluate(store, key_ptr, len(flag_key_bytes), context_ptr, len(context_json))
# Unpack result
result_ptr = result_packed >> 32
result_len = result_packed & 0xFFFFFFFF
# Read result
result_bytes = memory.read(store, result_ptr, result_len)
result = json.loads(result_bytes.decode('utf-8'))
# Free memory
dealloc(store, key_ptr, len(flag_key_bytes))
dealloc(store, context_ptr, len(context_json))
dealloc(store, result_ptr, result_len)
return result
# Example usage
result = evaluate_flag("myFlag", {})
print(result["value"]) # False (default variant, no context match)
# With matching context
result = evaluate_flag("myFlag", {"email": "[email protected]"})
print(result["value"]) # True (targeting matched)Recommended: Native Python Bindings with PyO3
For better performance and a more Pythonic API, use the native Python bindings built with PyO3 (see section above). Native bindings:
- Eliminate WASM overhead (5-10x faster)
- Provide direct Rust-to-Python compilation
- Enable a simpler, more idiomatic Python API
- Simple
pip install flagd-evaluator- no WASM runtime needed
See python/README.md for complete documentation.
Node.js can use the WASM evaluator using the built-in WebAssembly API:
const fs = require('fs');
const crypto = require('crypto');
// Load WASM module
const wasmBuffer = fs.readFileSync('flagd_evaluator.wasm');
// Define host functions
const imports = {
host: {
get_current_time_unix_seconds: () => BigInt(Math.floor(Date.now() / 1000))
},
__wbindgen_placeholder__: {
__wbg_getRandomValues_1c61fac11405ffdc: (typedArrayPtr, bufferPtr) => {
const randomBytes = crypto.randomBytes(32);
const memory = instance.exports.memory;
new Uint8Array(memory.buffer, bufferPtr, 32).set(randomBytes);
},
__wbindgen_describe: () => {},
__wbindgen_throw: (ptr, len) => {
const memory = instance.exports.memory;
const message = new TextDecoder().decode(
new Uint8Array(memory.buffer, ptr, len)
);
throw new Error(message);
},
__wbindgen_object_drop_ref: () => {},
__wbindgen_externref_table_grow: () => 0,
__wbindgen_externref_table_set_null: () => {},
__wbg_new0_1: () => Date.now(),
__wbg_getTime_1: (datePtr) => Date.now()
}
};
// Instantiate WASM
let instance;
WebAssembly.instantiate(wasmBuffer, imports).then(result => {
instance = result.instance;
// Helper functions
const alloc = instance.exports.alloc;
const dealloc = instance.exports.dealloc;
const updateState = instance.exports.update_state;
const evaluate = instance.exports.evaluate;
const memory = instance.exports.memory;
function writeString(str) {
const bytes = Buffer.from(str, 'utf8');
const ptr = alloc(bytes.length);
new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes);
return { ptr, len: bytes.length };
}
function readString(ptr, len) {
const bytes = new Uint8Array(memory.buffer, ptr, len);
return Buffer.from(bytes).toString('utf8');
}
// Load flag configuration
const config = writeString(JSON.stringify({
flags: {
myFlag: {
state: "ENABLED",
variants: { on: true, off: false },
defaultVariant: "on"
}
}
}));
const updateResult = updateState(config.ptr, config.len);
dealloc(config.ptr, config.len);
// Evaluate flag
const flagKey = writeString('myFlag');
const context = writeString('{}');
const resultPacked = evaluate(flagKey.ptr, flagKey.len, context.ptr, context.len);
const resultPtr = Number(resultPacked >> 32n);
const resultLen = Number(resultPacked & 0xFFFFFFFFn);
const resultJson = readString(resultPtr, resultLen);
console.log(JSON.parse(resultJson).value);
// Output: true
// Clean up
dealloc(flagKey.ptr, flagKey.len);
dealloc(context.ptr, context.len);
dealloc(resultPtr, resultLen);
});Alternative: Native Node.js Bindings with napi-rs
For better performance and a more idiomatic JavaScript/TypeScript API, native Node.js bindings could be created using napi-rs. This would:
- Eliminate WASM overhead
- Provide native npm package installation
- Enable simpler, more JavaScript-idiomatic API
- Auto-generate TypeScript definitions
See GitHub issue #48 for discussion on adding native Node.js bindings.
The Rust library provides an instance-based FlagEvaluator API for stateful flag evaluation:
use flagd_evaluator::{FlagEvaluator, ValidationMode};
use serde_json::json;
// Create evaluator with strict validation (default)
let mut evaluator = FlagEvaluator::new(ValidationMode::Strict);
// Update flag configuration
let config = json!({
"flags": {
"myFlag": {
"state": "ENABLED",
"variants": {"on": true, "off": false},
"defaultVariant": "on",
"targeting": {
"if": [
{"==": [{"var": "email"}, "[email protected]"]},
"on",
"off"
]
}
}
}
}).to_string();
evaluator.update_state(&config).unwrap();
// Evaluate with type-specific methods
let result = evaluator.evaluate_bool("myFlag", &json!({}));
println!("{:?}", result.value); // false (default variant)
let result = evaluator.evaluate_bool("myFlag", &json!({"email": "[email protected]"}));
println!("{:?}", result.value); // true (targeting matched)
// Or use the generic evaluate method for full result details
let result = evaluator.evaluate_flag("myFlag", &json!({}));
println!("Variant: {:?}, Reason: {:?}", result.variant, result.reason);Type-Specific Evaluation Methods:
evaluate_bool(flag_key, context) -> EvaluationResult- For boolean flagsevaluate_string(flag_key, context) -> EvaluationResult- For string flagsevaluate_int(flag_key, context) -> EvaluationResult- For integer flagsevaluate_float(flag_key, context) -> EvaluationResult- For float flagsevaluate_object(flag_key, context) -> EvaluationResult- For object flagsevaluate_flag(flag_key, context) -> EvaluationResult- Generic evaluation
Each evaluator instance maintains its own flag configuration state and validation mode.
| Function | Signature | Description |
|---|---|---|
update_state |
(config_ptr, config_len) -> u64 |
Updates the feature flag configuration state |
evaluate |
(flag_key_ptr, flag_key_len, context_ptr, context_len) -> u64 |
Evaluates a feature flag against context (generic) |
evaluate_boolean |
(flag_key_ptr, flag_key_len, context_ptr, context_len) -> u64 |
Evaluates a boolean flag with type checking |
evaluate_string |
(flag_key_ptr, flag_key_len, context_ptr, context_len) -> u64 |
Evaluates a string flag with type checking |
evaluate_integer |
(flag_key_ptr, flag_key_len, context_ptr, context_len) -> u64 |
Evaluates an integer flag with type checking |
evaluate_float |
(flag_key_ptr, flag_key_len, context_ptr, context_len) -> u64 |
Evaluates a float flag with type checking |
evaluate_object |
(flag_key_ptr, flag_key_len, context_ptr, context_len) -> u64 |
Evaluates an object flag with type checking |
set_validation_mode |
(mode: u32) -> u64 |
Sets validation mode (0=Strict, 1=Permissive) |
alloc |
(len: u32) -> *mut u8 |
Allocates memory in WASM linear memory |
dealloc |
(ptr: *mut u8, len: u32) |
Frees previously allocated memory |
Updates the internal feature flag configuration state. This function should be called before evaluating flags using the evaluate function.
Parameters:
config_ptr(u32): Pointer to the flagd configuration JSON string in WASM memoryconfig_len(u32): Length of the configuration JSON string
Returns:
u64: Packed pointer where upper 32 bits = result pointer, lower 32 bits = result length
Configuration Format: The configuration should follow the flagd flag definition schema:
{
"flags": {
"myFlag": {
"state": "ENABLED",
"variants": {
"on": true,
"off": false
},
"defaultVariant": "off",
"targeting": {
"if": [
{"==": [{"var": "email"}, "[email protected]"]},
"on",
"off"
]
}
}
}
}Response Format:
// Success
{
"success": true,
"error": null
}
// Error
{
"success": false,
"error": "error message"
}State Update Flow:
sequenceDiagram
participant Host as Host Application
participant WASM as WASM Module
participant Storage as Flag Storage
Host->>Host: Allocate memory using alloc()
Host->>Host: Write config JSON to memory
Host->>WASM: Call update_state(config_ptr, config_len)
WASM->>WASM: Read JSON from memory
alt Invalid UTF-8
WASM-->>Host: Return error response
end
WASM->>WASM: Validate against JSON Schema
alt Strict Mode
alt Validation Failed
WASM-->>Host: Return validation error
end
else Permissive Mode
alt Validation Failed
WASM->>WASM: Log warning, continue
end
end
WASM->>WASM: Parse JSON to FeatureFlag objects
alt Parse Error
WASM-->>Host: Return parse error
end
WASM->>Storage: Store parsed flags (replace existing)
WASM->>WASM: Allocate memory for success response
WASM-->>Host: Return packed pointer (success)
Host->>Host: Read response from memory
Host->>Host: Deallocate memory using dealloc()
Evaluates a feature flag from the previously stored configuration (set via update_state) against the provided context.
Parameters:
flag_key_ptr(u32): Pointer to the flag key string in WASM memoryflag_key_len(u32): Length of the flag key stringcontext_ptr(u32): Pointer to the evaluation context JSON string in WASM memorycontext_len(u32): Length of the context JSON string
Returns:
u64: Packed pointer where upper 32 bits = result pointer, lower 32 bits = result length
Response Format: The response follows the flagd provider specification:
{
"value": <resolved_value>,
"variant": "variant_name",
"reason": "DEFAULT" | "TARGETING_MATCH" | "DISABLED" | "ERROR",
"errorCode": "FLAG_NOT_FOUND" | "PARSE_ERROR" | "TYPE_MISMATCH" | "GENERAL",
"errorMessage": "error description"
}Reasons:
STATIC: The resolved value is statically configured (no targeting rules exist)DEFAULT: The resolved value uses the default variant because targeting didn't matchTARGETING_MATCH: The resolved value is the result of targeting rule evaluationDISABLED: The flag is disabled, returning the default variantERROR: An error occurred during evaluationFLAG_NOT_FOUND: The flag was not found in the configuration
Error Codes:
FLAG_NOT_FOUND: The flag key was not found in the configurationPARSE_ERROR: Error parsing or evaluating the targeting ruleTYPE_MISMATCH: The evaluated type does not match the expected typeGENERAL: Generic evaluation error
Example Usage:
// 1. Update state with flag configuration
String config = "{\"flags\": {...}}";
byte[] configBytes = config.getBytes(StandardCharsets.UTF_8);
long configPtr = alloc.apply(configBytes.length)[0];
memory.write((int) configPtr, configBytes);
long updateResult = updateState.apply(configPtr, configBytes.length)[0];
// ... read and parse update result ...
dealloc.apply(configPtr, configBytes.length);
// 2. Evaluate a flag
String flagKey = "myFlag";
String context = "{\"email\": \"[email protected]\"}";
byte[] keyBytes = flagKey.getBytes(StandardCharsets.UTF_8);
byte[] contextBytes = context.getBytes(StandardCharsets.UTF_8);
long keyPtr = alloc.apply(keyBytes.length)[0];
long contextPtr = alloc.apply(contextBytes.length)[0];
memory.write((int) keyPtr, keyBytes);
memory.write((int) contextPtr, contextBytes);
long packedResult = evaluate.apply(keyPtr, keyBytes.length, contextPtr, contextBytes.length)[0];
int resultPtr = (int) (packedResult >>> 32);
int resultLen = (int) (packedResult & 0xFFFFFFFFL);
byte[] resultBytes = memory.readBytes(resultPtr, resultLen);
String result = new String(resultBytes, StandardCharsets.UTF_8);
// Parse JSON result...
// Cleanup
dealloc.apply(keyPtr, keyBytes.length);
dealloc.apply(contextPtr, contextBytes.length);
dealloc.apply(resultPtr, resultLen);Flag Evaluation Flow:
flowchart TD
Start([Host calls evaluate]) --> AllocMem[Host allocates memory for flag key and context]
AllocMem --> WriteData[Host writes data to WASM memory]
WriteData --> CallEval[Call evaluate with pointers]
CallEval --> ReadMem[WASM reads flag key and context from memory]
ReadMem --> ParseCtx{Parse context JSON}
ParseCtx -->|Error| ReturnError[Return PARSE_ERROR]
ParseCtx -->|Success| GetFlag{Retrieve flag from storage}
GetFlag -->|Not Found| ReturnNotFound[Return FLAG_NOT_FOUND]
GetFlag -->|Found| CheckState{Check flag state}
CheckState -->|DISABLED| ReturnDisabled[Return default variant with DISABLED reason]
CheckState -->|ENABLED| CheckTargeting{Has targeting rules?}
CheckTargeting -->|No| ReturnStatic[Return default variant with STATIC reason]
CheckTargeting -->|Yes| EnrichContext[Enrich context with:<br/>- $flagd.flagKey<br/>- $flagd.timestamp<br/>- targetingKey default]
EnrichContext --> EvalRule[Evaluate targeting rule using JSON Logic]
EvalRule --> EvalResult{Evaluation result}
EvalResult -->|Error| ReturnParseError[Return PARSE_ERROR]
EvalResult -->|Success| GetVariant{Variant exists?}
GetVariant -->|Yes| ReturnMatch[Return variant value with TARGETING_MATCH]
GetVariant -->|No| ReturnDefault[Return default variant with DEFAULT reason]
ReturnError --> PackResult[Pack result into memory]
ReturnNotFound --> PackResult
ReturnDisabled --> PackResult
ReturnStatic --> PackResult
ReturnParseError --> PackResult
ReturnMatch --> PackResult
ReturnDefault --> PackResult
PackResult --> ReturnPacked[Return packed pointer to host]
ReturnPacked --> HostRead[Host reads result from memory]
HostRead --> HostDealloc[Host deallocates all memory]
HostDealloc --> End([Evaluation complete])
The evaluator automatically enriches the evaluation context with standard $flagd properties according to the flagd provider specification. These properties are available in targeting rules via JSON Logic's var operator.
Injected Properties:
| Property | Type | Description | Example Access |
|---|---|---|---|
$flagd.flagKey |
string | The key of the flag being evaluated | {"var": "$flagd.flagKey"} |
$flagd.timestamp |
number | Unix timestamp in seconds at evaluation time | {"var": "$flagd.timestamp"} |
targetingKey |
string | Key for consistent hashing (from context or empty string) | {"var": "targetingKey"} |
Example - Time-based Feature Flag:
{
"flags": {
"limitedTimeOffer": {
"state": "ENABLED",
"variants": {
"active": true,
"expired": false
},
"defaultVariant": "expired",
"targeting": {
"if": [
{
"and": [
{">=": [{"var": "$flagd.timestamp"}, 1704067200]},
{"<": [{"var": "$flagd.timestamp"}, 1735689600]}
]
},
"active",
"expired"
]
}
}
}
}Example - Flag-specific Logic:
{
"targeting": {
"if": [
{"==": [{"var": "$flagd.flagKey"}, "debugMode"]},
"enabled",
"disabled"
]
}
}Note: The $flagd properties are stored as a nested object in the evaluation context: {"$flagd": {"flagKey": "...", "timestamp": ...}}. This allows JSON Logic to access them using dot notation (e.g., {"var": "$flagd.timestamp"}).
The WASM module requires the host environment to provide the current timestamp for context enrichment.
Module: host
Function: get_current_time_unix_seconds() -> u64
Returns the current Unix timestamp in seconds since epoch (1970-01-01 00:00:00 UTC).
The WASM sandbox cannot access system time without WASI support. Since Chicory and other pure WASM runtimes don't provide WASI, the host must supply the current time for the $flagd.timestamp property used in targeting rules.
import com.dylibso.chicory.runtime.HostFunction;
import com.dylibso.chicory.wasm.types.Value;
import com.dylibso.chicory.wasm.types.ValueType;
HostFunction getCurrentTime = new HostFunction(
"host", // Module name
"get_current_time_unix_seconds", // Function name
List.of(), // No parameters
List.of(ValueType.I64), // Returns i64
(Instance instance, Value... args) -> {
long currentTimeSeconds = System.currentTimeMillis() / 1000;
return new Value[] { Value.i64(currentTimeSeconds) };
}
);
// Add to module when loading WASM
Module module = Module.builder(wasmBytes)
.withHostFunction(getCurrentTime)
.build();📝 See HOST_FUNCTIONS.md for complete implementation examples in Java, JavaScript, and Go.
If the host function is not provided:
$flagd.timestampdefaults to0- Evaluation continues without errors
- Time-based targeting rules will not work correctly
The evaluator automatically validates flag configurations against the official flagd-schemas before storing them. This ensures that your flag configurations match the expected structure and catches errors early.
You can configure how validation errors are handled:
- Strict Mode (default): Rejects flag configurations that fail validation
- Permissive Mode: Stores flag configurations even if validation fails (useful for legacy configurations)
From Rust:
Each FlagEvaluator instance can have its own validation mode:
use flagd_evaluator::{FlagEvaluator, ValidationMode};
// Create evaluator with strict validation (default)
let strict_evaluator = FlagEvaluator::new(ValidationMode::Strict);
// Create evaluator with permissive validation
let permissive_evaluator = FlagEvaluator::new(ValidationMode::Permissive);
// Change validation mode on an existing evaluator
let mut evaluator = FlagEvaluator::new(ValidationMode::Strict);
evaluator.set_validation_mode(ValidationMode::Permissive);From WASM (e.g., Java via Chicory):
The WASM module uses a singleton evaluator, and you can set its validation mode globally:
// Get the set_validation_mode function
WasmFunction setValidationMode = instance.export("set_validation_mode");
// Set to permissive mode (1)
long resultPtr = setValidationMode.apply(1L)[0];
// Parse the response
int ptr = (int) (resultPtr >>> 32);
int len = (int) (resultPtr & 0xFFFFFFFFL);
byte[] responseBytes = memory.readBytes(ptr, len);
String response = new String(responseBytes, StandardCharsets.UTF_8);
// {"success":true,"error":null}
// Don't forget to free the memory
dealloc.apply(ptr, len);
// To set back to strict mode (0) - this is the default
setValidationMode.apply(0L);Validation Mode Values:
0= Strict mode (reject invalid configurations)1= Permissive mode (accept with warnings)
When validation fails in strict mode, the update_state function returns a JSON error object:
{
"valid": false,
"errors": [
{
"path": "/flags/myFlag/state",
"message": "'INVALID' is not one of ['ENABLED', 'DISABLED']"
},
{
"path": "/flags/myFlag",
"message": "'variants' is a required property"
}
]
}Missing Required Fields:
{
"valid": false,
"errors": [
{
"path": "/flags/myFlag",
"message": "'state' is a required property"
}
]
}Invalid State Value:
{
"valid": false,
"errors": [
{
"path": "/flags/myFlag/state",
"message": "'INVALID_STATE' is not one of ['ENABLED', 'DISABLED']"
}
]
}Mixed Variant Types (e.g., boolean flag with string variant):
{
"valid": false,
"errors": [
{
"path": "/flags/boolFlag/variants/on",
"message": "'string value' is not of type 'boolean'"
}
]
}Invalid JSON:
{
"valid": false,
"errors": [
{
"path": "",
"message": "Invalid JSON: expected value at line 1 column 5"
}
]
}This library implements all flagd custom operators for feature flag evaluation. See the flagd Custom Operations Specification for the full specification.
The fractional operator provides consistent hashing for A/B testing and feature flag rollouts. It uses MurmurHash3 to consistently assign the same key to the same bucket.
Syntax:
{"fractional": [<bucket_key>, [<name1>, <weight1>, <name2>, <weight2>, ...]]}Parameters:
bucket_key: A string, number, or{"var": "path"}reference used for bucketingbuckets: Array of alternating bucket names and weights
Examples:
// 50/50 A/B test
{"fractional": ["user-123", ["control", 50, "treatment", 50]]}
// Using a variable reference
{"fractional": [{"var": "user.id"}, ["A", 33, "B", 33, "C", 34]]}
// 10% rollout to beta
{"fractional": [{"var": "userId"}, ["beta", 10, "stable", 90]]}Properties:
- Consistent: Same bucket key always returns the same bucket
- Deterministic: Results are reproducible across different invocations
- Uniform Distribution: Keys are evenly distributed across buckets according to weights
The starts_with operator checks if a string starts with a specific prefix. The comparison is case-sensitive.
Syntax:
{"starts_with": [<string_value>, <prefix>]}Parameters:
string_value: A string or{"var": "path"}reference to the value to checkprefix: A string or{"var": "path"}reference to the prefix to search for
Examples:
// Check if email starts with "admin@"
{"starts_with": [{"var": "email"}, "admin@"]}
// Check if path starts with "/api/"
{"starts_with": [{"var": "path"}, "/api/"]}Properties:
- Case-sensitive: "Hello" does not start with "hello"
- Empty prefix: An empty prefix always returns true
The ends_with operator checks if a string ends with a specific suffix. The comparison is case-sensitive.
Syntax:
{"ends_with": [<string_value>, <suffix>]}Parameters:
string_value: A string or{"var": "path"}reference to the value to checksuffix: A string or{"var": "path"}reference to the suffix to search for
Examples:
// Check if filename ends with ".pdf"
{"ends_with": [{"var": "filename"}, ".pdf"]}
// Check if URL ends with ".com"
{"ends_with": [{"var": "url"}, ".com"]}Properties:
- Case-sensitive: "Hello.PDF" does not end with ".pdf"
- Empty suffix: An empty suffix always returns true
The sem_ver operator compares semantic versions according to the semver.org specification. It supports all standard comparison operators plus caret (^) and tilde (~) ranges.
Syntax:
{"sem_ver": [<version>, <operator>, <target_version>]}Parameters:
version: A version string or{"var": "path"}referenceoperator: One of"=","!=","<","<=",">",">=","^","~"target_version: The version to compare against
Operators:
| Operator | Description |
|---|---|
"=" |
Equal to |
"!=" |
Not equal to |
"<" |
Less than |
"<=" |
Less than or equal to |
">" |
Greater than |
">=" |
Greater than or equal to |
"^" |
Caret range (allows minor and patch updates) |
"~" |
Tilde range (allows patch updates only) |
Examples:
// Check if version is greater than or equal to 2.0.0
{"sem_ver": [{"var": "app.version"}, ">=", "2.0.0"]}
// Caret range: ^1.2.3 means >=1.2.3 <2.0.0
{"sem_ver": [{"var": "version"}, "^", "1.2.3"]}
// Tilde range: ~1.2.3 means >=1.2.3 <1.3.0
{"sem_ver": [{"var": "version"}, "~", "1.2.3"]}Version Handling:
- Supports versions with prerelease tags (e.g., "1.0.0-alpha.1")
- Supports versions with build metadata (e.g., "1.0.0+build.123")
- Missing minor/patch versions are treated as 0 (e.g., "1.2" = "1.2.0")
- Prerelease versions have lower precedence than release versions
The library uses a simple linear memory allocation model:
- Allocation: Call
alloc(len)to allocatelenbytes. Returns a pointer or null on failure. - Usage: Write data to the allocated memory region.
- Deallocation: Call
dealloc(ptr, len)to free the memory.
Important: The caller is responsible for:
- Freeing input memory after WASM functions return
- Freeing the result memory after reading it
Memory Management Flow:
sequenceDiagram
participant Host as Host Application
participant WASM as WASM Module
participant Heap as WASM Heap
Note over Host,Heap: Input Phase
Host->>WASM: Call alloc(input_len)
WASM->>Heap: Allocate input_len bytes
Heap-->>WASM: Return pointer or null
WASM-->>Host: Return input_ptr
Host->>WASM: Write input data to memory[input_ptr]
Note over Host,Heap: Processing Phase
Host->>WASM: Call function(input_ptr, input_len, ...)
WASM->>WASM: Read data from memory[input_ptr]
WASM->>WASM: Process data
WASM->>WASM: Call alloc(result_len) internally
WASM->>Heap: Allocate result_len bytes
Heap-->>WASM: Return result_ptr
WASM->>WASM: Write result to memory[result_ptr]
WASM->>WASM: Pack pointer and length:<br/>packed = (result_ptr << 32) | result_len
WASM-->>Host: Return packed u64
Note over Host,Heap: Cleanup Phase
Host->>Host: Unpack u64:<br/>result_ptr = packed >> 32<br/>result_len = packed & 0xFFFFFFFF
Host->>WASM: Read result from memory[result_ptr]
Host->>WASM: Call dealloc(input_ptr, input_len)
WASM->>Heap: Free input memory
Host->>WASM: Call dealloc(result_ptr, result_len)
WASM->>Heap: Free result memory
Note over Host,Heap: Memory lifecycle complete
Results are returned as a packed 64-bit integer:
- Upper 32 bits: Memory pointer to result string
- Lower 32 bits: Length of result string in bytes
// Java example to unpack
long packedResult = evaluateLogic.apply(...)[0];
int ptr = (int) (packedResult >>> 32);
int len = (int) (packedResult & 0xFFFFFFFFL);- All memory operations go through the exported
alloc/deallocfunctions - The library never accesses memory outside allocated regions
- Invalid UTF-8 input is detected and reported as an error
- All errors are caught and returned as JSON, never as panics
| Metric | Target | Notes |
|---|---|---|
| WASM Size | ~1.5MB | Full JSON Logic implementation with 50+ operators |
| Evaluation Time | < 1ms | For simple rules with small data |
| Memory Overhead | Minimal | Only allocates what's needed for inputs and outputs |
- Reuse the WASM instance - Instantiation is expensive; reuse the instance for multiple evaluations
- Batch evaluations - If evaluating many rules, consider batching
- Keep data small - Only include necessary data in the context
- Use wasm-opt - The release workflow uses wasm-opt for additional optimization
- Rust 1.70+ (for 2021 edition support)
- wasm32-unknown-unknown target
cargo build
cargo testcargo build --target wasm32-unknown-unknown --releasecargo clippy -- -D warnings
cargo fmt -- --checkSee CONTRIBUTING.md for detailed guidelines.
Quick summary:
- Fork the repository
- Create a feature branch
- Make your changes with tests
- Ensure
cargo testandcargo clippypass - Submit a pull request
This project is licensed under the Apache License, Version 2.0 - see the LICENSE file for details.
- datalogic-rs - The JSON Logic implementation this library is built on
- Chicory - Pure Java WebAssembly runtime
- serde - Serialization framework for Rust
- OpenFeature - The open standard for feature flag management