diff --git a/benches/comparison.rs b/benches/comparison.rs index 3bcac9e..7ba9237 100644 --- a/benches/comparison.rs +++ b/benches/comparison.rs @@ -71,7 +71,7 @@ fn comparison_simple_flag_evaluator(c: &mut Criterion) { let context = json!({}); c.bench_function("comparison_simple_flag_evaluator", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("simpleFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("simpleFlag"), black_box(context.clone()))) }); } @@ -116,7 +116,7 @@ fn comparison_complex_flag_evaluator(c: &mut Criterion) { let context = json!({"tier": "standard", "score": 75}); c.bench_function("comparison_complex_flag_evaluator", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(context.clone()))) }); } diff --git a/benches/concurrency.rs b/benches/concurrency.rs index fbe781e..442b3b0 100644 --- a/benches/concurrency.rs +++ b/benches/concurrency.rs @@ -102,7 +102,7 @@ fn concurrent_simple_1t(c: &mut Criterion) { let context = json!({}); c.bench_function("concurrent_simple_1t", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(context.clone()))) }); } @@ -122,7 +122,7 @@ fn concurrent_simple_4t(c: &mut Criterion) { thread::spawn(move || { let ctx = json!({}); let guard = eval.lock().unwrap(); - guard.evaluate_flag(black_box("boolFlag"), black_box(&ctx)) + guard.evaluate_flag(black_box("boolFlag"), black_box(ctx)) }) }) .collect(); @@ -149,7 +149,7 @@ fn concurrent_simple_8t(c: &mut Criterion) { thread::spawn(move || { let ctx = json!({}); let guard = eval.lock().unwrap(); - guard.evaluate_flag(black_box("boolFlag"), black_box(&ctx)) + guard.evaluate_flag(black_box("boolFlag"), black_box(ctx)) }) }) .collect(); @@ -177,7 +177,7 @@ fn concurrent_targeting_4t(c: &mut Criterion) { let role = if i % 2 == 0 { "admin" } else { "viewer" }; let ctx = json!({"role": role}); let guard = eval.lock().unwrap(); - guard.evaluate_flag(black_box("targetedFlag"), black_box(&ctx)) + guard.evaluate_flag(black_box("targetedFlag"), black_box(ctx)) }) }) .collect(); @@ -215,7 +215,7 @@ fn concurrent_mixed_4t(c: &mut Criterion) { let context = ctx.clone(); thread::spawn(move || { let guard = eval.lock().unwrap(); - guard.evaluate_flag(black_box(key), black_box(&context)) + guard.evaluate_flag(black_box(key), black_box(context)) }) }) .collect(); @@ -251,7 +251,7 @@ fn concurrent_read_write_4t(c: &mut Criterion) { thread::spawn(move || { let ctx = json!({}); let guard = eval.lock().unwrap(); - guard.evaluate_flag(black_box("boolFlag"), black_box(&ctx)) + guard.evaluate_flag(black_box("boolFlag"), black_box(ctx)) }) }) .collect(); @@ -283,7 +283,7 @@ fn concurrent_simple_16t(c: &mut Criterion) { thread::spawn(move || { let ctx = json!({}); let guard = eval.lock().unwrap(); - guard.evaluate_flag(black_box("boolFlag"), black_box(&ctx)) + guard.evaluate_flag(black_box("boolFlag"), black_box(ctx)) }) }) .collect(); @@ -314,7 +314,7 @@ fn concurrent_targeting_16t(c: &mut Criterion) { let role = if i % 2 == 0 { "admin" } else { "viewer" }; let ctx = json!({"role": role}); let guard = eval.lock().unwrap(); - guard.evaluate_flag(black_box("targetedFlag"), black_box(&ctx)) + guard.evaluate_flag(black_box("targetedFlag"), black_box(ctx)) }) }) .collect(); @@ -354,7 +354,7 @@ fn concurrent_mixed_16t(c: &mut Criterion) { let (key, ctx) = workload_defs[i % workload_defs.len()].clone(); thread::spawn(move || { let guard = eval.lock().unwrap(); - guard.evaluate_flag(black_box(key), black_box(&ctx)) + guard.evaluate_flag(black_box(key), black_box(ctx)) }) }) .collect(); @@ -391,7 +391,7 @@ fn concurrent_read_write_16t(c: &mut Criterion) { thread::spawn(move || { let ctx = json!({}); let guard = eval.lock().unwrap(); - guard.evaluate_flag(black_box("boolFlag"), black_box(&ctx)) + guard.evaluate_flag(black_box("boolFlag"), black_box(ctx)) }) }) .collect(); diff --git a/benches/evaluation.rs b/benches/evaluation.rs index f558071..f0fc22e 100644 --- a/benches/evaluation.rs +++ b/benches/evaluation.rs @@ -77,7 +77,7 @@ fn evaluate_flag_simple(c: &mut Criterion) { let context = json!({}); c.bench_function("evaluate_flag_simple", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(context.clone()))) }); } @@ -87,7 +87,7 @@ fn evaluate_flag_targeting_match(c: &mut Criterion) { let context = json!({"role": "admin"}); c.bench_function("evaluate_flag_targeting_match", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(context.clone()))) }); } @@ -97,7 +97,7 @@ fn evaluate_flag_targeting_no_match(c: &mut Criterion) { let context = json!({"role": "viewer"}); c.bench_function("evaluate_flag_targeting_no_match", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(context.clone()))) }); } @@ -107,7 +107,7 @@ fn evaluate_flag_complex_targeting(c: &mut Criterion) { let context = json!({"tier": "standard", "score": 75}); c.bench_function("evaluate_flag_complex_targeting", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(context.clone()))) }); } @@ -117,7 +117,7 @@ fn evaluate_flag_disabled(c: &mut Criterion) { let context = json!({}); c.bench_function("evaluate_flag_disabled", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("disabledFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("disabledFlag"), black_box(context.clone()))) }); } @@ -127,7 +127,7 @@ fn evaluate_flag_not_found(c: &mut Criterion) { let context = json!({}); c.bench_function("evaluate_flag_not_found", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("nonexistent"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("nonexistent"), black_box(context.clone()))) }); } @@ -205,7 +205,7 @@ fn evaluate_flag_simple_small_ctx(c: &mut Criterion) { let context = small_context(); c.bench_function("evaluate_flag_simple_small_ctx", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(context.clone()))) }); } @@ -216,7 +216,7 @@ fn evaluate_flag_simple_large_ctx(c: &mut Criterion) { let context = large_context(); c.bench_function("evaluate_flag_simple_large_ctx", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("boolFlag"), black_box(context.clone()))) }); } @@ -227,7 +227,7 @@ fn evaluate_flag_targeting_small_ctx(c: &mut Criterion) { let context = small_context(); // contains "role": "admin" which triggers the match c.bench_function("evaluate_flag_targeting_small_ctx", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(context.clone()))) }); } @@ -238,7 +238,7 @@ fn evaluate_flag_targeting_large_ctx(c: &mut Criterion) { let context = large_context(); // contains "role": "admin" which triggers the match c.bench_function("evaluate_flag_targeting_large_ctx", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("targetedFlag"), black_box(context.clone()))) }); } @@ -249,7 +249,7 @@ fn evaluate_flag_complex_targeting_small_ctx(c: &mut Criterion) { let context = small_context(); // contains "tier": "premium" and "score": 85 c.bench_function("evaluate_flag_complex_targeting_small_ctx", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(context.clone()))) }); } @@ -260,7 +260,7 @@ fn evaluate_flag_complex_targeting_large_ctx(c: &mut Criterion) { let context = large_context(); // contains "tier": "premium" and "score": 85 c.bench_function("evaluate_flag_complex_targeting_large_ctx", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("complexFlag"), black_box(context.clone()))) }); } diff --git a/benches/scale.rs b/benches/scale.rs index d669f08..a22252e 100644 --- a/benches/scale.rs +++ b/benches/scale.rs @@ -102,7 +102,7 @@ fn bench_evaluate_static_from_10k(c: &mut Criterion) { let context = json!({}); c.bench_function("S10_evaluate_static_from_10K_store", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("flag_0000"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("flag_0000"), black_box(context.clone()))) }); } @@ -117,7 +117,7 @@ fn bench_evaluate_targeting_from_10k(c: &mut Criterion) { let context = json!({"color": "blue", "targetingKey": "user-123"}); c.bench_function("S11_evaluate_targeting_from_10K_store", |b| { - b.iter(|| evaluator.evaluate_flag(black_box("flag_0014"), black_box(&context))) + b.iter(|| evaluator.evaluate_flag(black_box("flag_0014"), black_box(context.clone()))) }); } diff --git a/examples/evaluators_demo.rs b/examples/evaluators_demo.rs index c5d873a..9fda34c 100644 --- a/examples/evaluators_demo.rs +++ b/examples/evaluators_demo.rs @@ -67,7 +67,7 @@ fn main() { // Test 1: Admin user let context = json!({"email": "admin@company.com", "tier": "basic"}); - let result = evaluator.evaluate_flag("vipFeatures", &context); + let result = evaluator.evaluate_flag("vipFeatures", context); println!("1️⃣ Admin user (admin@company.com, tier=basic):"); println!( " → Result: {}, Variant: {}", @@ -77,7 +77,7 @@ fn main() { // Test 2: Premium user let context = json!({"email": "user@company.com", "tier": "premium"}); - let result = evaluator.evaluate_flag("vipFeatures", &context); + let result = evaluator.evaluate_flag("vipFeatures", context); println!("\n2️⃣ Premium user (user@company.com, tier=premium):"); println!( " → Result: {}, Variant: {}", @@ -87,7 +87,7 @@ fn main() { // Test 3: Regular user let context = json!({"email": "user@company.com", "tier": "basic"}); - let result = evaluator.evaluate_flag("vipFeatures", &context); + let result = evaluator.evaluate_flag("vipFeatures", context); println!("\n3️⃣ Regular user (user@company.com, tier=basic):"); println!( " → Result: {}, Variant: {}", diff --git a/python/src/lib.rs b/python/src/lib.rs index 4c30297..a0198a3 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -108,17 +108,17 @@ impl FlagEvaluator { // If we also have a flag index, use the index-based evaluation path if let Some(&index) = self.flag_indices.get(flag_key) { - return self.inner.evaluate_flag_by_index(index, &filtered_context); + return self.inner.evaluate_flag_by_index(index, filtered_context); } // Otherwise use pre-enriched evaluation (context already has $flagd) return self .inner - .evaluate_flag_pre_enriched(flag_key, &filtered_context); + .evaluate_flag_pre_enriched(flag_key, filtered_context); } // Full evaluation path (no optimization data available for this flag) - self.inner.evaluate_flag(flag_key, context) + self.inner.evaluate_flag(flag_key, context.clone()) } } diff --git a/src/evaluator.rs b/src/evaluator.rs index 8410bb1..9b63dc1 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -220,33 +220,33 @@ impl FlagEvaluator { /// /// # Returns /// An EvaluationResult containing the resolved value, variant, reason, and metadata - pub fn evaluate_flag(&self, flag_key: &str, context: &JsonValue) -> EvaluationResult { - self.evaluate_with_type_check(flag_key, context, None) + pub fn evaluate_flag(&self, flag_key: &str, context: Value) -> EvaluationResult { + self.evaluate_with_type_check(flag_key, context, None, true) } /// Evaluates a boolean flag with type checking. - pub fn evaluate_bool(&self, flag_key: &str, context: &JsonValue) -> EvaluationResult { - self.evaluate_with_type_check(flag_key, context, Some(ExpectedType::Boolean)) + pub fn evaluate_bool(&self, flag_key: &str, context: Value) -> EvaluationResult { + self.evaluate_with_type_check(flag_key, context, Some(ExpectedType::Boolean), true) } /// Evaluates a string flag with type checking. - pub fn evaluate_string(&self, flag_key: &str, context: &JsonValue) -> EvaluationResult { - self.evaluate_with_type_check(flag_key, context, Some(ExpectedType::String)) + pub fn evaluate_string(&self, flag_key: &str, context: Value) -> EvaluationResult { + self.evaluate_with_type_check(flag_key, context, Some(ExpectedType::String), true) } /// Evaluates an integer flag with type checking. - pub fn evaluate_int(&self, flag_key: &str, context: &JsonValue) -> EvaluationResult { - self.evaluate_with_type_check(flag_key, context, Some(ExpectedType::Integer)) + pub fn evaluate_int(&self, flag_key: &str, context: Value) -> EvaluationResult { + self.evaluate_with_type_check(flag_key, context, Some(ExpectedType::Integer), true) } /// Evaluates a float flag with type checking. - pub fn evaluate_float(&self, flag_key: &str, context: &JsonValue) -> EvaluationResult { - self.evaluate_with_type_check(flag_key, context, Some(ExpectedType::Float)) + pub fn evaluate_float(&self, flag_key: &str, context: Value) -> EvaluationResult { + self.evaluate_with_type_check(flag_key, context, Some(ExpectedType::Float), true) } /// Evaluates an object flag with type checking. - pub fn evaluate_object(&self, flag_key: &str, context: &JsonValue) -> EvaluationResult { - self.evaluate_with_type_check(flag_key, context, Some(ExpectedType::Object)) + pub fn evaluate_object(&self, flag_key: &str, context: Value) -> EvaluationResult { + self.evaluate_with_type_check(flag_key, context, Some(ExpectedType::Object), true) } // ========================================================================= @@ -257,8 +257,9 @@ impl FlagEvaluator { fn evaluate_with_type_check( &self, flag_key: &str, - context: &JsonValue, + context: Value, expected_type: Option, + needs_enrichment: bool, ) -> EvaluationResult { // Get flag and metadata from state - avoid cloning the flag! let state = match &self.state { @@ -274,7 +275,7 @@ impl FlagEvaluator { None => { // Flag not found - return flag-set metadata per spec (best effort) let flag_set_metadata = - Self::merge_metadata(&state.flag_set_metadata, &HashMap::new()); + Self::merge_metadata_flag_set_only(&state.flag_set_metadata); return EvaluationResult { value: JsonValue::Null, variant: None, @@ -286,8 +287,14 @@ impl FlagEvaluator { } }; - // Perform the evaluation with reference to flag - let result = self.evaluate_flag_internal(flag, flag_key, context, &state.flag_set_metadata); + // Perform the evaluation + let result = self.evaluate_flag_core( + flag, + flag_key, + context, + needs_enrichment, + &state.flag_set_metadata, + ); // Apply type checking if requested match expected_type { @@ -297,11 +304,12 @@ impl FlagEvaluator { } /// Core flag evaluation logic. - fn evaluate_flag_internal( + fn evaluate_flag_core( &self, flag: &FeatureFlag, flag_key: &str, - context: &JsonValue, + context: Value, + needs_enrichment: bool, flag_set_metadata: &HashMap, ) -> EvaluationResult { // Check if flag is disabled - still return metadata per spec @@ -346,18 +354,22 @@ impl FlagEvaluator { }; } - // Enrich the context with flagKey and targetingKey - let enriched_context = Self::enrich_context(flag_key, context); + // Conditionally enrich the context + let eval_context = if needs_enrichment { + Self::enrich_context(flag_key, context) + } else { + context + }; // Evaluate targeting using the instance's DataLogic engine let eval_result = if let Some(ref compiled) = flag.compiled_targeting { // Fast path: use pre-compiled targeting with evaluate_owned (no JSON serialization) - self.logic.evaluate_owned(compiled, enriched_context) + self.logic.evaluate_owned(compiled, eval_context) } else { // Fallback: compile at runtime (for flags created without pre-compilation) let targeting = flag.targeting.as_ref().unwrap(); let rule_str = targeting.to_string(); - let context_str = enriched_context.to_string(); + let context_str = eval_context.to_string(); self.logic.evaluate_json(&rule_str, &context_str) }; @@ -551,16 +563,16 @@ impl FlagEvaluator { &self, parsing_result: &ParsingResult, ) -> HashMap { - let empty_context = JsonValue::Object(Map::new()); let mut results = HashMap::new(); for (flag_key, flag) in &parsing_result.flags { // Pre-evaluate disabled flags if flag.state == "DISABLED" { - let result = self.evaluate_flag_internal( + let result = self.evaluate_flag_core( flag, flag_key, - &empty_context, + Value::Object(Map::new()), + false, &parsing_result.flag_set_metadata, ); results.insert(flag_key.clone(), result); @@ -575,10 +587,11 @@ impl FlagEvaluator { }; if is_static { - let result = self.evaluate_flag_internal( + let result = self.evaluate_flag_core( flag, flag_key, - &empty_context, + Value::Object(Map::new()), + false, &parsing_result.flag_set_metadata, ); results.insert(flag_key.clone(), result); @@ -629,11 +642,10 @@ impl FlagEvaluator { } /// Enriches the evaluation context with standard flagd fields. - fn enrich_context(flag_key: &str, context: &Value) -> Value { - let mut enriched = if let Some(obj) = context.as_object() { - obj.clone() - } else { - Map::new() + fn enrich_context(flag_key: &str, context: Value) -> Value { + let mut enriched = match context { + Value::Object(obj) => obj, + _ => Map::new(), }; // Get current Unix timestamp (seconds since epoch) @@ -683,6 +695,23 @@ impl FlagEvaluator { Some(merged) } + /// Merges only flag-set metadata (no flag-level metadata). + /// Used in flag-not-found paths to avoid creating an empty HashMap. + fn merge_metadata_flag_set_only( + flag_set_metadata: &HashMap, + ) -> Option> { + let filtered: HashMap = flag_set_metadata + .iter() + .filter(|(key, _)| !key.starts_with('$')) + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + if filtered.is_empty() { + None + } else { + Some(filtered) + } + } + /// Lazy metadata attachment - only merges metadata if there's actually metadata to merge. /// This avoids the cost of creating HashMaps when both sources are empty. fn with_lazy_metadata( @@ -706,9 +735,9 @@ impl FlagEvaluator { /// /// This is a fast path that avoids flag key string handling by using O(1) Vec lookup. /// The context is expected to be pre-enriched with `$flagd.*` and `targetingKey` by the host. - pub fn evaluate_flag_by_index(&self, index: u32, context: &JsonValue) -> EvaluationResult { + pub fn evaluate_flag_by_index(&self, index: u32, context: Value) -> EvaluationResult { let flag_key = match self.flag_index_map.get(index as usize) { - Some(key) => key, + Some(key) => key.clone(), None => { return EvaluationResult::error( ErrorCode::FlagNotFound, @@ -717,200 +746,21 @@ impl FlagEvaluator { } }; - self.evaluate_flag_pre_enriched(flag_key, context) + self.evaluate_flag_pre_enriched(&flag_key, context) } /// Evaluates a flag with a pre-enriched context (skips `enrich_context` if `$flagd` is present). /// /// The host is expected to have added `$flagd.flagKey`, `$flagd.timestamp`, and `targetingKey`. - pub fn evaluate_flag_pre_enriched( - &self, - flag_key: &str, - context: &JsonValue, - ) -> EvaluationResult { + pub fn evaluate_flag_pre_enriched(&self, flag_key: &str, context: Value) -> EvaluationResult { // If context already has $flagd, skip enrichment let is_pre_enriched = context .as_object() .map(|o| o.contains_key("$flagd")) .unwrap_or(false); - if is_pre_enriched { - self.evaluate_with_type_check_pre_enriched(flag_key, context, None) - } else { - self.evaluate_with_type_check(flag_key, context, None) - } - } - - /// Internal evaluation that skips context enrichment (context already has `$flagd`). - fn evaluate_with_type_check_pre_enriched( - &self, - flag_key: &str, - context: &JsonValue, - expected_type: Option, - ) -> EvaluationResult { - let state = match &self.state { - Some(s) => s, - None => { - return EvaluationResult::error(ErrorCode::General, "No flag configuration loaded"); - } - }; - - let flag = match state.flags.get(flag_key) { - Some(f) => f, - None => { - let flag_set_metadata = - Self::merge_metadata(&state.flag_set_metadata, &HashMap::new()); - return EvaluationResult { - value: JsonValue::Null, - variant: None, - reason: ResolutionReason::FlagNotFound, - error_code: Some(ErrorCode::FlagNotFound), - error_message: Some(format!("Flag '{}' not found in configuration", flag_key)), - flag_metadata: flag_set_metadata, - }; - } - }; - - let result = self.evaluate_flag_internal_pre_enriched( - flag, - flag_key, - context, - &state.flag_set_metadata, - ); - - match expected_type { - Some(expected) => self.apply_type_check(result, expected), - None => result, - } - } - - /// Core flag evaluation logic that skips context enrichment. - fn evaluate_flag_internal_pre_enriched( - &self, - flag: &FeatureFlag, - flag_key: &str, - context: &JsonValue, - flag_set_metadata: &HashMap, - ) -> EvaluationResult { - // Check if flag is disabled - if flag.state == "DISABLED" { - let merged_metadata = Self::merge_metadata(flag_set_metadata, &flag.metadata); - return EvaluationResult { - value: JsonValue::Null, - variant: None, - reason: ResolutionReason::Disabled, - error_code: Some(ErrorCode::FlagNotFound), - error_message: Some(format!("flag: {} is disabled", flag_key)), - flag_metadata: merged_metadata, - }; - } - - // Check for empty targeting - let is_empty_targeting = match &flag.targeting { - None => true, - Some(JsonValue::Object(map)) if map.is_empty() => true, - _ => false, - }; - - if is_empty_targeting { - return match flag.default_variant.as_ref() { - None => EvaluationResult::fallback(flag_key), - Some(value) if value.is_empty() => EvaluationResult::fallback(flag_key), - Some(default_variant) => match flag.variants.get(default_variant) { - Some(value) => { - let result = - EvaluationResult::static_result(value.clone(), default_variant.clone()); - Self::with_lazy_metadata(flag_set_metadata, &flag.metadata, result) - } - None => EvaluationResult::error( - ErrorCode::General, - format!( - "Default variant '{}' not found in flag variants", - default_variant - ), - ), - }, - }; - } - - // Use context directly (already enriched by host) - let eval_result = if let Some(ref compiled) = flag.compiled_targeting { - self.logic.evaluate_owned(compiled, context.clone()) - } else { - let targeting = flag.targeting.as_ref().unwrap(); - let rule_str = targeting.to_string(); - let context_str = context.to_string(); - self.logic.evaluate_json(&rule_str, &context_str) - }; - - // Same result processing as evaluate_flag_internal - match eval_result { - Ok(result) => { - if result.is_null() { - return match flag.default_variant.as_ref() { - None => EvaluationResult::fallback(flag_key), - Some(value) if value.is_empty() => EvaluationResult::fallback(flag_key), - Some(default_variant) => match flag.variants.get(default_variant) { - Some(value) => { - let result = EvaluationResult::default_result( - value.clone(), - default_variant.clone(), - ); - Self::with_lazy_metadata(flag_set_metadata, &flag.metadata, result) - } - None => EvaluationResult::error( - ErrorCode::General, - format!( - "Default variant '{}' not found in flag variants", - default_variant - ), - ), - }, - }; - } - - let variant_name = match result { - JsonValue::String(s) => s, - other => match other.as_str() { - Some(s) => s.to_string(), - None => other.to_string().trim_matches('"').to_string(), - }, - }; - - if variant_name.is_empty() { - return match flag.default_variant.as_ref() { - None => EvaluationResult::fallback(flag_key), - Some(default_variant) if default_variant.is_empty() => { - EvaluationResult::fallback(flag_key) - } - Some(_) => EvaluationResult::error( - ErrorCode::General, - format!( - "Targeting rule returned empty variant name for flag '{}'", - flag_key - ), - ), - }; - } - - match flag.variants.get(&variant_name) { - Some(value) => { - let result = EvaluationResult::targeting_match(value.clone(), variant_name); - Self::with_lazy_metadata(flag_set_metadata, &flag.metadata, result) - } - None => EvaluationResult::error( - ErrorCode::General, - format!( - "Targeting rule returned variant '{}' which is not defined in flag variants", - variant_name - ), - ), - } - } - Err(e) => { - EvaluationResult::error(ErrorCode::ParseError, format!("Evaluation error: {}", e)) - } - } + let needs_enrichment = !is_pre_enriched; + self.evaluate_with_type_check(flag_key, context, None, needs_enrichment) } /// Builds required_context_keys and flag_indices maps from parsed flag config. diff --git a/src/lib.rs b/src/lib.rs index 9217590..1167f9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -513,7 +513,7 @@ fn evaluate_by_index_internal( } }; - eval.evaluate_flag_by_index(flag_index, &context) + eval.evaluate_flag_by_index(flag_index, context) }) }); @@ -592,7 +592,7 @@ fn evaluate_internal( }; // Evaluate using the evaluator instance - eval.evaluate_flag(&flag_key, &context) + eval.evaluate_flag(&flag_key, context) }) }); @@ -637,7 +637,7 @@ mod tests { let response = evaluator.update_state(config).unwrap(); assert!(response.success); - let result = evaluator.evaluate_bool("boolFlag", &json!({})); + let result = evaluator.evaluate_bool("boolFlag", json!({})); assert_eq!(result.value, json!(false)); assert_eq!(result.variant, Some("off".to_string())); assert_eq!(result.reason, ResolutionReason::Static); @@ -662,7 +662,7 @@ mod tests { evaluator.update_state(config).unwrap(); - let result = evaluator.evaluate_int("intFlag", &json!({})); + let result = evaluator.evaluate_int("intFlag", json!({})); assert_eq!(result.value, json!(10)); assert_eq!(result.variant, Some("small".to_string())); } @@ -686,7 +686,7 @@ mod tests { evaluator.update_state(config).unwrap(); - let result = evaluator.evaluate_float("floatFlag", &json!({})); + let result = evaluator.evaluate_float("floatFlag", json!({})); assert_eq!(result.value, json!(3.14)); assert_eq!(result.variant, Some("pi".to_string())); } @@ -710,7 +710,7 @@ mod tests { evaluator.update_state(config).unwrap(); - let result = evaluator.evaluate_string("stringFlag", &json!({})); + let result = evaluator.evaluate_string("stringFlag", json!({})); assert_eq!(result.value, json!("Hello, World!")); assert_eq!(result.variant, Some("hello".to_string())); } @@ -734,7 +734,7 @@ mod tests { evaluator.update_state(config).unwrap(); - let result = evaluator.evaluate_flag("objectFlag", &json!({})); + let result = evaluator.evaluate_flag("objectFlag", json!({})); assert_eq!(result.value, json!({"key": "value1"})); assert_eq!(result.variant, Some("config1".to_string())); } @@ -766,13 +766,13 @@ mod tests { evaluator.update_state(config).unwrap(); // Test admin role - let result_admin = evaluator.evaluate_flag("targetedFlag", &json!({"role": "admin"})); + let result_admin = evaluator.evaluate_flag("targetedFlag", json!({"role": "admin"})); assert_eq!(result_admin.value, json!("admin-value")); assert_eq!(result_admin.variant, Some("admin".to_string())); assert_eq!(result_admin.reason, ResolutionReason::TargetingMatch); // Test user role - let result_user = evaluator.evaluate_flag("targetedFlag", &json!({"role": "user"})); + let result_user = evaluator.evaluate_flag("targetedFlag", json!({"role": "user"})); assert_eq!(result_user.value, json!("user-value")); assert_eq!(result_user.variant, Some("user".to_string())); assert_eq!(result_user.reason, ResolutionReason::TargetingMatch); @@ -794,7 +794,7 @@ mod tests { evaluator.update_state(config).unwrap(); - let result = evaluator.evaluate_bool("disabledFlag", &json!({})); + let result = evaluator.evaluate_bool("disabledFlag", json!({})); assert_eq!(result.value, Value::Null); assert_eq!(result.reason, ResolutionReason::Disabled); assert_eq!(result.error_code, Some(ErrorCode::FlagNotFound)); @@ -816,7 +816,7 @@ mod tests { evaluator.update_state(config).unwrap(); - let result = evaluator.evaluate_flag("nonexistentFlag", &json!({})); + let result = evaluator.evaluate_flag("nonexistentFlag", json!({})); assert_eq!(result.reason, ResolutionReason::FlagNotFound); assert_eq!(result.error_code, Some(ErrorCode::FlagNotFound)); } @@ -858,7 +858,7 @@ mod tests { evaluator.update_state(config).unwrap(); - let result = evaluator.evaluate_flag("abTestFlag", &json!({"targetingKey": "user-123"})); + let result = evaluator.evaluate_flag("abTestFlag", json!({"targetingKey": "user-123"})); assert!( result.value == json!("control-experience") || result.value == json!("treatment-experience") @@ -892,7 +892,7 @@ mod tests { evaluator.update_state(config).unwrap(); // Missing targetingKey - should use empty string - let result = evaluator.evaluate_flag("testFlag", &json!({})); + let result = evaluator.evaluate_flag("testFlag", json!({})); assert_eq!(result.reason, ResolutionReason::TargetingMatch); assert!(result.value == json!("variant-a") || result.value == json!("variant-b")); } @@ -922,7 +922,7 @@ mod tests { evaluator.update_state(config).unwrap(); - let result = evaluator.evaluate_flag("brokenFlag", &json!({})); + let result = evaluator.evaluate_flag("brokenFlag", json!({})); assert_eq!(result.reason, ResolutionReason::Error); assert_eq!(result.error_code, Some(ErrorCode::General)); } @@ -963,21 +963,21 @@ mod tests { // Admin email let result_admin = evaluator.evaluate_flag( "complexFlag", - &json!({"email": "admin@example.com", "appVersion": "1.0.0"}), + json!({"email": "admin@example.com", "appVersion": "1.0.0"}), ); assert_eq!(result_admin.value, json!("premium-tier")); // Non-admin with new version let result_standard = evaluator.evaluate_flag( "complexFlag", - &json!({"email": "user@example.com", "appVersion": "2.1.0"}), + json!({"email": "user@example.com", "appVersion": "2.1.0"}), ); assert_eq!(result_standard.value, json!("standard-tier")); // Non-admin with old version let result_basic = evaluator.evaluate_flag( "complexFlag", - &json!({"email": "user@example.com", "appVersion": "1.5.0"}), + json!({"email": "user@example.com", "appVersion": "1.5.0"}), ); assert_eq!(result_basic.value, json!("basic-tier")); } @@ -1013,7 +1013,7 @@ mod tests { evaluator.update_state(config).unwrap(); - let result = evaluator.evaluate_flag("enrichedFlag", &json!({})); + let result = evaluator.evaluate_flag("enrichedFlag", json!({})); assert_eq!(result.value, json!("properties-present")); assert_eq!(result.variant, Some("verified".to_string())); } @@ -1034,7 +1034,7 @@ mod tests { evaluator.update_state(config).unwrap(); - let result = evaluator.evaluate_bool("stringFlag", &json!({})); + let result = evaluator.evaluate_bool("stringFlag", json!({})); assert_eq!(result.reason, ResolutionReason::Error); assert_eq!(result.error_code, Some(ErrorCode::TypeMismatch)); } @@ -1055,7 +1055,7 @@ mod tests { evaluator.update_state(config).unwrap(); - let result = evaluator.evaluate_string("stringFlag", &json!({})); + let result = evaluator.evaluate_string("stringFlag", json!({})); assert_eq!(result.reason, ResolutionReason::FlagNotFound); } @@ -1076,7 +1076,7 @@ mod tests { evaluator.update_state(config).unwrap(); // Integer evaluation should accept floats via coercion - let result = evaluator.evaluate_int("floatFlag", &json!({})); + let result = evaluator.evaluate_int("floatFlag", json!({})); assert_eq!(result.value, json!(3)); } @@ -1097,7 +1097,7 @@ mod tests { evaluator.update_state(config).unwrap(); // Float evaluation should accept integers via coercion - let result = evaluator.evaluate_float("intFlag", &json!({})); + let result = evaluator.evaluate_float("intFlag", json!({})); assert_eq!(result.value, json!(42.0)); } @@ -1117,7 +1117,7 @@ mod tests { evaluator.update_state(config).unwrap(); - let result = evaluator.evaluate_object("stringFlag", &json!({})); + let result = evaluator.evaluate_object("stringFlag", json!({})); assert_eq!(result.reason, ResolutionReason::Error); assert_eq!(result.error_code, Some(ErrorCode::TypeMismatch)); } @@ -1138,19 +1138,19 @@ mod tests { evaluator.update_state(config).unwrap(); - let bool_result = evaluator.evaluate_bool("missingFlag", &json!({})); + let bool_result = evaluator.evaluate_bool("missingFlag", json!({})); assert_eq!(bool_result.reason, ResolutionReason::FlagNotFound); - let string_result = evaluator.evaluate_string("missingFlag", &json!({})); + let string_result = evaluator.evaluate_string("missingFlag", json!({})); assert_eq!(string_result.reason, ResolutionReason::FlagNotFound); - let int_result = evaluator.evaluate_int("missingFlag", &json!({})); + let int_result = evaluator.evaluate_int("missingFlag", json!({})); assert_eq!(int_result.reason, ResolutionReason::FlagNotFound); - let float_result = evaluator.evaluate_float("missingFlag", &json!({})); + let float_result = evaluator.evaluate_float("missingFlag", json!({})); assert_eq!(float_result.reason, ResolutionReason::FlagNotFound); - let object_result = evaluator.evaluate_object("missingFlag", &json!({})); + let object_result = evaluator.evaluate_object("missingFlag", json!({})); assert_eq!(object_result.reason, ResolutionReason::FlagNotFound); } @@ -1175,11 +1175,11 @@ mod tests { evaluator.update_state(config).unwrap(); - let bool_result = evaluator.evaluate_bool("disabledBool", &json!({})); + let bool_result = evaluator.evaluate_bool("disabledBool", json!({})); assert_eq!(bool_result.value, Value::Null); assert_eq!(bool_result.reason, ResolutionReason::Disabled); - let string_result = evaluator.evaluate_string("disabledString", &json!({})); + let string_result = evaluator.evaluate_string("disabledString", json!({})); assert_eq!(string_result.value, Value::Null); assert_eq!(string_result.reason, ResolutionReason::Disabled); } @@ -1656,8 +1656,8 @@ mod optimization_tests { } }); - let result_by_index = evaluator.evaluate_flag_by_index(index, &context); - let result_by_key = evaluator.evaluate_flag("targetedFlag", &json!({"role": "admin"})); + let result_by_index = evaluator.evaluate_flag_by_index(index, context); + let result_by_key = evaluator.evaluate_flag("targetedFlag", json!({"role": "admin"})); assert_eq!(result_by_index.value, result_by_key.value); assert_eq!(result_by_index.variant, result_by_key.variant); @@ -1696,7 +1696,7 @@ mod optimization_tests { } }); - let result = evaluator.evaluate_flag_pre_enriched("myFlag", &context); + let result = evaluator.evaluate_flag_pre_enriched("myFlag", context); assert_eq!(result.value, json!("found-key")); assert_eq!(result.reason, ResolutionReason::TargetingMatch); } @@ -1726,7 +1726,7 @@ mod optimization_tests { // Context without $flagd — should fall back to normal enrichment let context = json!({"targetingKey": "user-456"}); - let result = evaluator.evaluate_flag_pre_enriched("myFlag", &context); + let result = evaluator.evaluate_flag_pre_enriched("myFlag", context); assert_eq!(result.value, json!("found-key")); assert_eq!(result.reason, ResolutionReason::TargetingMatch); } diff --git a/tests/gherkin_tests.rs b/tests/gherkin_tests.rs index d8c62ce..69125ba 100644 --- a/tests/gherkin_tests.rs +++ b/tests/gherkin_tests.rs @@ -244,7 +244,9 @@ async fn given_option(_world: &mut FlagdWorld, _key: String, _type: String, _val async fn when_flag_evaluated(world: &mut FlagdWorld) { let flag_key = world.current_flag_key.as_ref().expect("No flag key set"); - let result = world.evaluator.evaluate_flag(flag_key, &world.context); + let result = world + .evaluator + .evaluate_flag(flag_key, world.context.clone()); // Apply mapping layer for backward compatibility with Gherkin tests // The WASM module now uses semantic reasons (Fallback, Disabled) with error codes diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index c2a1c5c..75b4cd5 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -322,13 +322,13 @@ fn test_evaluators_simple_ref_evaluation() { // Test with admin email - should return true let context = json!({"email": "admin@example.com"}); - let eval_result = evaluator.evaluate_flag("adminFeature", &context); + let eval_result = evaluator.evaluate_flag("adminFeature", context); assert_eq!(eval_result.value, json!(true)); assert_eq!(eval_result.variant, Some("on".to_string())); // Test with non-admin email - should return false let context = json!({"email": "user@example.com"}); - let eval_result = evaluator.evaluate_flag("adminFeature", &context); + let eval_result = evaluator.evaluate_flag("adminFeature", context); assert_eq!(eval_result.value, json!(false)); assert_eq!(eval_result.variant, Some("off".to_string())); } @@ -380,19 +380,19 @@ fn test_evaluators_nested_ref_evaluation() { // Test with active admin - should return premium let context = json!({"email": "admin@company.com", "status": "active"}); - let result = evaluator.evaluate_flag("premiumFeature", &context); + let result = evaluator.evaluate_flag("premiumFeature", context); assert_eq!(result.value, json!("premium")); assert_eq!(result.variant, Some("enabled".to_string())); // Test with non-admin - should return free let context = json!({"email": "user@company.com", "status": "active"}); - let result = evaluator.evaluate_flag("premiumFeature", &context); + let result = evaluator.evaluate_flag("premiumFeature", context); assert_eq!(result.value, json!("free")); assert_eq!(result.variant, Some("disabled".to_string())); // Test with admin but inactive - should return free let context = json!({"email": "admin@company.com", "status": "inactive"}); - let result = evaluator.evaluate_flag("premiumFeature", &context); + let result = evaluator.evaluate_flag("premiumFeature", context); assert_eq!(result.value, json!("free")); assert_eq!(result.variant, Some("disabled".to_string())); } @@ -436,8 +436,8 @@ fn test_evaluators_with_fractional_operator() { // Test with specific user ID - should consistently return same variant let context = json!({"userId": "user-123"}); - let result1 = evaluator.evaluate_flag("experimentFlag", &context); - let result2 = evaluator.evaluate_flag("experimentFlag", &context); + let result1 = evaluator.evaluate_flag("experimentFlag", context.clone()); + let result2 = evaluator.evaluate_flag("experimentFlag", context); assert_eq!(result1.value, result2.value); assert!( result1.value == json!("control-experience") @@ -498,22 +498,22 @@ fn test_evaluators_complex_targeting() { // Premium + active - should get VIP let context = json!({"tier": "premium", "lifetime_value": 500, "active": true}); - let result = evaluator.evaluate_flag("vipFeatures", &context); + let result = evaluator.evaluate_flag("vipFeatures", context); assert_eq!(result.variant, Some("vip".to_string())); // High value + active - should get VIP let context = json!({"tier": "basic", "lifetime_value": 1500, "active": true}); - let result = evaluator.evaluate_flag("vipFeatures", &context); + let result = evaluator.evaluate_flag("vipFeatures", context); assert_eq!(result.variant, Some("vip".to_string())); // Premium but inactive - should get standard let context = json!({"tier": "premium", "lifetime_value": 500, "active": false}); - let result = evaluator.evaluate_flag("vipFeatures", &context); + let result = evaluator.evaluate_flag("vipFeatures", context); assert_eq!(result.variant, Some("standard".to_string())); // Neither premium nor high value - should get standard let context = json!({"tier": "basic", "lifetime_value": 100, "active": true}); - let result = evaluator.evaluate_flag("vipFeatures", &context); + let result = evaluator.evaluate_flag("vipFeatures", context); assert_eq!(result.variant, Some("standard".to_string())); } @@ -599,17 +599,17 @@ fn test_evaluators_multiple_refs_in_single_flag() { // Admin gets full access let context = json!({"email": "admin@company.com"}); - let result = evaluator.evaluate_flag("accessFlag", &context); + let result = evaluator.evaluate_flag("accessFlag", context); assert_eq!(result.value, json!("full-access")); // Manager gets limited access let context = json!({"email": "manager@company.com"}); - let result = evaluator.evaluate_flag("accessFlag", &context); + let result = evaluator.evaluate_flag("accessFlag", context); assert_eq!(result.value, json!("limited-access")); // Regular user gets no access let context = json!({"email": "user@company.com"}); - let result = evaluator.evaluate_flag("accessFlag", &context); + let result = evaluator.evaluate_flag("accessFlag", context); assert_eq!(result.value, json!("no-access")); } @@ -905,7 +905,7 @@ fn test_fractional_single_bucket() { // Any context should get "on" variant for i in 0..10 { let context = json!({"targetingKey": format!("user-{}", i)}); - let result = evaluator.evaluate_flag("singleBucket", &context); + let result = evaluator.evaluate_flag("singleBucket", context); assert_eq!( result.variant, Some("on".to_string()), @@ -948,7 +948,7 @@ fn test_fractional_unequal_weights() { // Test with many users for i in 0..100 { let context = json!({"targetingKey": format!("test-user-{}", i)}); - let result = evaluator.evaluate_flag("heavyA", &context); + let result = evaluator.evaluate_flag("heavyA", context); match result.variant.as_deref() { Some("a") => a_count += 1, Some("b") => b_count += 1, @@ -985,7 +985,7 @@ fn test_unicode_flag_key() { evaluator .update_state(config) .expect("state should be updated"); - let result = evaluator.evaluate_bool("日本語フラグ", &json!({})); + let result = evaluator.evaluate_bool("日本語フラグ", json!({})); assert_eq!(result.value, json!(true)); assert_eq!(result.variant, Some("オン".to_string())); } @@ -1022,11 +1022,11 @@ fn test_unicode_in_context() { .expect("state should be updated"); let context = json!({"language": "中文"}); - let result = evaluator.evaluate_flag("greetingFlag", &context); + let result = evaluator.evaluate_flag("greetingFlag", context); assert_eq!(result.value, json!("你好")); let context = json!({"language": "日本語"}); - let result = evaluator.evaluate_flag("greetingFlag", &context); + let result = evaluator.evaluate_flag("greetingFlag", context); assert_eq!(result.value, json!("こんにちは")); } @@ -1049,7 +1049,7 @@ fn test_emoji_in_variant_values() { evaluator .update_state(config) .expect("state should be updated"); - let result = evaluator.evaluate_string("emojiFlag", &json!({})); + let result = evaluator.evaluate_string("emojiFlag", json!({})); assert_eq!(result.value, json!("😀")); } @@ -1092,7 +1092,7 @@ fn test_empty_variants_map() { evaluator .update_state(config) .expect("state should be updated"); - let result = evaluator.evaluate_flag("emptyVariants", &json!({})); + let result = evaluator.evaluate_flag("emptyVariants", json!({})); // Should return an error since variant doesn't exist assert!(result.error_code.is_some()); } @@ -1148,7 +1148,7 @@ fn test_deeply_nested_targeting() { for level in 0..=5 { let context = json!({"level": level}); - let result = evaluator.evaluate_flag("nestedFlag", &context); + let result = evaluator.evaluate_flag("nestedFlag", context); assert_eq!( result.value, json!(level), @@ -1234,7 +1234,7 @@ fn test_sem_ver_edge_cases() { for (version, expected) in test_cases { let context = json!({"appVersion": version}); - let result = evaluator.evaluate_flag("versionFlag", &context); + let result = evaluator.evaluate_flag("versionFlag", context); assert_eq!( result.variant, Some(expected.to_string()), diff --git a/tests/metadata_merging_tests.rs b/tests/metadata_merging_tests.rs index 520b2dd..d11b5c3 100644 --- a/tests/metadata_merging_tests.rs +++ b/tests/metadata_merging_tests.rs @@ -38,7 +38,7 @@ fn test_metadata_merging_flag_priority() { evaluator.update_state(config).unwrap(); let context = json!({}); - let result = evaluator.evaluate_flag("testFlag", &context); + let result = evaluator.evaluate_flag("testFlag", context); // Verify metadata is merged with flag metadata taking priority assert!(result.flag_metadata.is_some()); @@ -80,7 +80,7 @@ fn test_metadata_only_flag_set() { evaluator.update_state(config).unwrap(); let context = json!({}); - let result = evaluator.evaluate_flag("testFlag", &context); + let result = evaluator.evaluate_flag("testFlag", context); // Verify flag-set metadata is included assert!(result.flag_metadata.is_some()); @@ -114,7 +114,7 @@ fn test_metadata_only_flag_level() { evaluator.update_state(config).unwrap(); let context = json!({}); - let result = evaluator.evaluate_flag("testFlag", &context); + let result = evaluator.evaluate_flag("testFlag", context); // Verify flag-level metadata is included assert!(result.flag_metadata.is_some()); @@ -150,7 +150,7 @@ fn test_metadata_disabled_flag_returns_metadata() { evaluator.update_state(config).unwrap(); let context = json!({}); - let result = evaluator.evaluate_flag("testFlag", &context); + let result = evaluator.evaluate_flag("testFlag", context); // Verify metadata is returned even for disabled flag assert!(result.flag_metadata.is_some()); @@ -183,7 +183,7 @@ fn test_metadata_missing_flag_returns_flag_set_metadata() { evaluator.update_state(config).unwrap(); let context = json!({}); - let result = evaluator.evaluate_flag("missingFlag", &context); + let result = evaluator.evaluate_flag("missingFlag", context); // Verify flag-set metadata is returned on "best effort" basis assert!(result.flag_metadata.is_some()); @@ -213,7 +213,7 @@ fn test_metadata_empty_merging() { evaluator.update_state(config).unwrap(); let context = json!({}); - let result = evaluator.evaluate_flag("testFlag", &context); + let result = evaluator.evaluate_flag("testFlag", context); // Verify empty metadata is not included assert!(result.flag_metadata.is_none() || result.flag_metadata.as_ref().unwrap().is_empty());