Skip to content

open-feature-forking/flagd-evaluator

Repository files navigation

flagd-evaluator

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.

CI License

Features

  • Full JSON Logic Support: Evaluate complex JSON Logic rules with all standard operators via datalogic-rs
  • Custom Operators: Feature-flag specific operators like fractional for 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 FlagEvaluator for 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

Quick Start

Building from Source

# 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 --release

The WASM file will be at: target/wasm32-unknown-unknown/release/flagd_evaluator.wasm

Running Tests

cargo test

Installation

From Release

Download the latest WASM file from the Releases page.

From Source

cargo build --target wasm32-unknown-unknown --release

Java Library (Recommended for Java Projects)

For 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.

Usage Examples

Java Library (Simple)

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());

Java with Chicory (Advanced)

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.

Python (Native Bindings) - Recommended

Native Python bindings provide the best performance and most Pythonic API using PyO3:

pip install flagd-evaluator

Quick 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)  # True

Benefits:

  • ⚡ 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/ -v

See python/README.md for complete documentation.

Python with Wasmtime (Alternative)

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 wasmtime

Basic 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/JavaScript with WASM

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.

Rust

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 flags
  • evaluate_string(flag_key, context) -> EvaluationResult - For string flags
  • evaluate_int(flag_key, context) -> EvaluationResult - For integer flags
  • evaluate_float(flag_key, context) -> EvaluationResult - For float flags
  • evaluate_object(flag_key, context) -> EvaluationResult - For object flags
  • evaluate_flag(flag_key, context) -> EvaluationResult - Generic evaluation

Each evaluator instance maintains its own flag configuration state and validation mode.

API Reference

Exported Functions

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

update_state

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 memory
  • config_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()
Loading

evaluate

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 memory
  • flag_key_len (u32): Length of the flag key string
  • context_ptr (u32): Pointer to the evaluation context JSON string in WASM memory
  • context_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 match
  • TARGETING_MATCH: The resolved value is the result of targeting rule evaluation
  • DISABLED: The flag is disabled, returning the default variant
  • ERROR: An error occurred during evaluation
  • FLAG_NOT_FOUND: The flag was not found in the configuration

Error Codes:

  • FLAG_NOT_FOUND: The flag key was not found in the configuration
  • PARSE_ERROR: Error parsing or evaluating the targeting rule
  • TYPE_MISMATCH: The evaluated type does not match the expected type
  • GENERAL: 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])
Loading

Context Enrichment

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"}).

Host Functions (Required for WASM)

The WASM module requires the host environment to provide the current timestamp for context enrichment.

Required: get_current_time_unix_seconds

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).

Why is this needed?

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.

Java Implementation Example (Chicory)

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.

Behavior Without Host Function

If the host function is not provided:

  • $flagd.timestamp defaults to 0
  • Evaluation continues without errors
  • Time-based targeting rules will not work correctly

JSON Schema Validation

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.

Validation Modes

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)

Validation Error Format

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"
    }
  ]
}

Common Validation Errors

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"
    }
  ]
}

Custom Operators

This library implements all flagd custom operators for feature flag evaluation. See the flagd Custom Operations Specification for the full specification.

fractional

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 bucketing
  • buckets: 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

starts_with

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 check
  • prefix: 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

ends_with

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 check
  • suffix: 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

sem_ver

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"} reference
  • operator: 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

Memory Model & Safety

Memory Management

The library uses a simple linear memory allocation model:

  1. Allocation: Call alloc(len) to allocate len bytes. Returns a pointer or null on failure.
  2. Usage: Write data to the allocated memory region.
  3. 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
Loading

Pointer Packing

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);

Safety Considerations

  • All memory operations go through the exported alloc/dealloc functions
  • 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

Performance Considerations

Targets

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

Optimization Tips

  1. Reuse the WASM instance - Instantiation is expensive; reuse the instance for multiple evaluations
  2. Batch evaluations - If evaluating many rules, consider batching
  3. Keep data small - Only include necessary data in the context
  4. Use wasm-opt - The release workflow uses wasm-opt for additional optimization

Building from Source

Requirements

  • Rust 1.70+ (for 2021 edition support)
  • wasm32-unknown-unknown target

Development Build

cargo build
cargo test

Release Build

cargo build --target wasm32-unknown-unknown --release

Linting

cargo clippy -- -D warnings
cargo fmt -- --check

Contributing

See CONTRIBUTING.md for detailed guidelines.

Quick summary:

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes with tests
  4. Ensure cargo test and cargo clippy pass
  5. Submit a pull request

License

This project is licensed under the Apache License, Version 2.0 - see the LICENSE file for details.

Acknowledgments

  • 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

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •