From 6f696a8487efa3c89f1a963ea97af2a106d0d8ca Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 30 Oct 2026 17:32:30 -0300 Subject: [PATCH 1/2] feat(Flag Dependency): Serve flags that depend on other flags Flags now honour segments that depend on other flags, and `/flags` ignores segments that need an identity, as Flagsmith does. Co-Authored-By: Claude Opus 5.5 (1M context) --- Cargo.lock | 170 ++++++++++++++++++++-------------- Cargo.toml | 2 +- src/services/environment.rs | 6 +- src/services/feature_utils.rs | 1 + tests/test_server.rs | 42 ++++++++- 5 files changed, 147 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b951b23..03c599c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -606,11 +606,12 @@ checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "flagsmith-flag-engine" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8506a1a6efe91540e5fa399f3d1ff1c1c756e431a2c8010885618b6a3d5da206" +version = "0.6.1" +source = "git+https://github.com/Flagsmith/flagsmith-rust-flag-engine?rev=0552139222db80ea1ec16ec5a6a7e4ae5912bcc5#0552139222db80ea1ec16ec5a6a7e4ae5912bcc5" dependencies = [ "chrono", + "indexmap", + "jsonpath-rust", "md-5", "num-bigint", "num-traits", @@ -618,7 +619,6 @@ dependencies = [ "semver", "serde", "serde_json", - "serde_json_path", "sha2", "uuid 0.8.2", ] @@ -1085,6 +1085,8 @@ checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown", + "serde", + "serde_core", ] [[package]] @@ -1160,6 +1162,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonpath-rust" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c4668c489390e66d586fe2238ee8568473f7ac1607b411342f787c5e1bedd7" +dependencies = [ + "pest", + "pest_derive", + "regex", + "serde_json", + "thiserror 2.0.17", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1236,12 +1251,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1289,16 +1298,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1438,6 +1437,59 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8db75153506001cb36ed924f08cadc862eb3f8c4d0d534139f29074769d7660" +dependencies = [ + "memchr", + "pest_stack_guard", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20aa17e11fb35664b67d5214a0afac57a360d231b6ec53d6eba0700b27d933b4" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626ec5afccf44ccbfeb2b232316510bb55b4bf34d891afcb0fb07d2b17d7fe9d" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "pest_meta" +version = "2.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3f45b6caa65c1b08186832745159c2529994146340a7bf44168095469720f5d" +dependencies = [ + "pest", +] + +[[package]] +name = "pest_stack_guard" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97e6cd2217057fa86b6eaa512917e58089d875b957a31e35e7ab729977e0a5f2" +dependencies = [ + "psm", + "stacker", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1550,6 +1602,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810" +dependencies = [ + "cc", +] + [[package]] name = "quinn" version = "0.11.9" @@ -1999,56 +2060,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "serde_json_path" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b992cea3194eea663ba99a042d61cea4bd1872da37021af56f6a37e0359b9d33" -dependencies = [ - "inventory", - "nom", - "regex", - "serde", - "serde_json", - "serde_json_path_core", - "serde_json_path_macros", - "thiserror 2.0.17", -] - -[[package]] -name = "serde_json_path_core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde67d8dfe7d4967b5a95e247d4148368ddd1e753e500adb34b3ffe40c6bc1bc" -dependencies = [ - "inventory", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "serde_json_path_macros" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "517acfa7f77ddaf5c43d5f119c44a683774e130b4247b7d3210f8924506cfac8" -dependencies = [ - "inventory", - "serde_json_path_core", - "serde_json_path_macros_internal", -] - -[[package]] -name = "serde_json_path_macros_internal" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafbefbe175fa9bf03ca83ef89beecff7d2a95aaacd5732325b90ac8c3bd7b90" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "serde_path_to_error" version = "0.1.20" @@ -2141,6 +2152,19 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.60.2", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2622,6 +2646,12 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.19" diff --git a/Cargo.toml b/Cargo.toml index 68cfbad..156d87a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ chrono = { version = "0.4", features = ["serde"] } async-trait = "0.1" http = "1" validator = { version = "0.20", features = ["derive"] } -flagsmith-flag-engine = "0.6" +flagsmith-flag-engine = { git = "https://github.com/Flagsmith/flagsmith-rust-flag-engine", rev = "0552139222db80ea1ec16ec5a6a7e4ae5912bcc5" } [dev-dependencies] axum-test = "18" diff --git a/src/services/environment.rs b/src/services/environment.rs index 5ef66dd..8467b66 100644 --- a/src/services/environment.rs +++ b/src/services/environment.rs @@ -8,7 +8,9 @@ use crate::models::{ use crate::services::feature_utils::filter_out_server_key_only_flag_results; use chrono::{DateTime, Utc}; use flagsmith_flag_engine::engine::get_evaluation_result; -use flagsmith_flag_engine::engine_eval::{FlagResult, add_identity_to_context}; +use flagsmith_flag_engine::engine_eval::{ + FlagResult, add_identity_to_context, remove_identity_from_context, +}; use flagsmith_flag_engine::identities::Trait as FlagsmithTrait; use reqwest::header::HeaderMap; use reqwest::{Client, Url}; @@ -306,7 +308,7 @@ impl EnvironmentService { EdgeProxyError::ServiceUnavailable("Environment not loaded".to_string()) })?; - let evaluation_result = get_evaluation_result(&context); + let evaluation_result = get_evaluation_result(&remove_identity_from_context(&context)); let mut flag_results: Vec = evaluation_result.flags.into_values().collect(); diff --git a/src/services/feature_utils.rs b/src/services/feature_utils.rs index 5e48618..8775543 100644 --- a/src/services/feature_utils.rs +++ b/src/services/feature_utils.rs @@ -25,6 +25,7 @@ mod tests { value: format!("value{}", id), }, reason: "DEFAULT".to_string(), + variant: None, metadata: FeatureMetadata { feature_id: id, feature_type: "STANDARD".to_string(), diff --git a/tests/test_server.rs b/tests/test_server.rs index 32a1b4e..49afe7a 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1,3 +1,5 @@ +#![allow(non_snake_case)] + mod fixtures; use axum_test::TestServer; @@ -8,6 +10,10 @@ use rstest::rstest; use serde_json::json; async fn setup_test_server() -> TestServer { + setup_test_server_with_environment(environment_1()).await +} + +async fn setup_test_server_with_environment(environment: serde_json::Value) -> TestServer { let settings = AppSettings { environment_key_pairs: vec![EnvironmentKeyPair { server_side_key: environment_1_server_key(), @@ -21,7 +27,7 @@ async fn setup_test_server() -> TestServer { service .cache - .put_environment(&environment_1_api_key(), environment_1()) + .put_environment(&environment_1_api_key(), environment) .await; TestServer::new(app).unwrap() @@ -128,6 +134,40 @@ async fn test_get_flags_unknown_key() { assert!(body["message"].as_str().unwrap().contains("unknown key")); } +#[tokio::test] +async fn test_get_flags__segment_needing_identity__returns_environment_default() { + // Given + let mut environment = environment_1(); + environment["project"]["segments"] = json!([{ + "id": 2, + "name": "without_plan", + "rules": [{ + "type": "ALL", + "rules": [], + "conditions": [{"operator": "IS_NOT_SET", "property_": "plan", "value": null}], + }], + "feature_states": [{ + "multivariate_feature_state_values": [], + "feature_state_value": "segment_override", + "feature": {"name": "feature_2", "type": "STANDARD", "id": 2}, + "enabled": true, + "featurestate_uuid": "uuid-segment-2", + }], + }]); + let server = setup_test_server_with_environment(environment).await; + + // When + let response = server + .get("/api/v1/flags?feature=feature_2") + .add_header("X-Environment-Key", &environment_1_api_key()) + .await; + + // Then + response.assert_status_ok(); + let body: serde_json::Value = response.json(); + assert_eq!(body["feature_state_value"], "2.3"); +} + #[rstest] #[case::client_key(environment_1_api_key(), &["feature_1", "feature_2"])] #[case::server_key(environment_1_server_key(), &["feature_1", "feature_2", "feature_3"])] From 1b7c615d43836a84fdfc278b5453eb3cc438f671 Mon Sep 17 00:00:00 2001 From: Evandro Myller Date: Fri, 30 Oct 2026 19:01:46 -0300 Subject: [PATCH 2/2] perf(Flag Dependency): Prepare the environment flags context once per environment update Co-Authored-By: Claude Opus 5.5 (1M context) --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/cache/environment.rs | 31 ++++++++++++++++++++++++++++++- src/services/environment.rs | 8 +++----- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03c599c..f31be8a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -607,7 +607,7 @@ checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "flagsmith-flag-engine" version = "0.6.1" -source = "git+https://github.com/Flagsmith/flagsmith-rust-flag-engine?rev=0552139222db80ea1ec16ec5a6a7e4ae5912bcc5#0552139222db80ea1ec16ec5a6a7e4ae5912bcc5" +source = "git+https://github.com/Flagsmith/flagsmith-rust-flag-engine?rev=92e19652b656f489769bdd62187a440f594a3c73#92e19652b656f489769bdd62187a440f594a3c73" dependencies = [ "chrono", "indexmap", diff --git a/Cargo.toml b/Cargo.toml index 156d87a..1ea3b3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ chrono = { version = "0.4", features = ["serde"] } async-trait = "0.1" http = "1" validator = { version = "0.20", features = ["derive"] } -flagsmith-flag-engine = { git = "https://github.com/Flagsmith/flagsmith-rust-flag-engine", rev = "0552139222db80ea1ec16ec5a6a7e4ae5912bcc5" } +flagsmith-flag-engine = { git = "https://github.com/Flagsmith/flagsmith-rust-flag-engine", rev = "92e19652b656f489769bdd62187a440f594a3c73" } [dev-dependencies] axum-test = "18" diff --git a/src/cache/environment.rs b/src/cache/environment.rs index 6274525..c11190f 100644 --- a/src/cache/environment.rs +++ b/src/cache/environment.rs @@ -1,5 +1,7 @@ use async_trait::async_trait; -use flagsmith_flag_engine::engine_eval::{EngineEvaluationContext, environment_to_context}; +use flagsmith_flag_engine::engine_eval::{ + EngineEvaluationContext, environment_to_context, remove_identity_from_context, +}; use flagsmith_flag_engine::environments::Environment as FlagsmithEnvironment; use serde_json::Value; use std::collections::HashMap; @@ -17,6 +19,13 @@ pub trait EnvironmentsCache: Send + Sync { /// Get the pre-computed evaluation context (for flag evaluation) async fn get_context(&self, environment_key: &str) -> Option; + /// Get the pre-computed evaluation context without segments that need an + /// identity (for environment flag evaluation) + async fn get_identity_free_context( + &self, + environment_key: &str, + ) -> Option; + /// Store environment document and compute context. Returns true if changed. async fn put_environment(&self, environment_key: &str, document: Value) -> bool; @@ -35,6 +44,9 @@ pub struct LocalMemEnvironmentsCache { environments: Arc>>>, /// Pre-computed evaluation contexts (for flag evaluation) contexts: Arc>>, + /// Pre-computed evaluation contexts without segments that need an identity + /// (for environment flag evaluation) + identity_free_contexts: Arc>>, /// Identity overrides extracted from environments identity_overrides: Arc>>>, } @@ -44,6 +56,7 @@ impl LocalMemEnvironmentsCache { Self { environments: Arc::new(RwLock::new(HashMap::new())), contexts: Arc::new(RwLock::new(HashMap::new())), + identity_free_contexts: Arc::new(RwLock::new(HashMap::new())), identity_overrides: Arc::new(RwLock::new(HashMap::new())), } } @@ -61,9 +74,18 @@ impl EnvironmentsCache for LocalMemEnvironmentsCache { contexts.get(environment_key).cloned() } + async fn get_identity_free_context( + &self, + environment_key: &str, + ) -> Option { + let identity_free_contexts = self.identity_free_contexts.read().await; + identity_free_contexts.get(environment_key).cloned() + } + async fn put_environment(&self, environment_key: &str, document: Value) -> bool { let mut environments = self.environments.write().await; let mut contexts = self.contexts.write().await; + let mut identity_free_contexts = self.identity_free_contexts.write().await; let mut identity_overrides = self.identity_overrides.write().await; // Check if document changed @@ -93,6 +115,10 @@ impl EnvironmentsCache for LocalMemEnvironmentsCache { if let Ok(environment) = serde_json::from_value::(document.clone()) { let flagsmith_env: FlagsmithEnvironment = environment.to_flagsmith_environment(); let context = environment_to_context(flagsmith_env); + identity_free_contexts.insert( + environment_key.to_string(), + remove_identity_from_context(&context), + ); contexts.insert(environment_key.to_string(), context); } @@ -106,10 +132,12 @@ impl EnvironmentsCache for LocalMemEnvironmentsCache { // Same guard order as put_environment so the two can't deadlock let mut environments = self.environments.write().await; let mut contexts = self.contexts.write().await; + let mut identity_free_contexts = self.identity_free_contexts.write().await; let mut identity_overrides = self.identity_overrides.write().await; environments.remove(environment_key); contexts.remove(environment_key); + identity_free_contexts.remove(environment_key); identity_overrides.remove(environment_key); } @@ -144,6 +172,7 @@ mod tests { // Then assert!(cache.get_environment("client_a").await.is_none()); assert!(cache.get_context("client_a").await.is_none()); + assert!(cache.get_identity_free_context("client_a").await.is_none()); assert!(cache.get_identity("client_a", "user_1").await.is_none()); } diff --git a/src/services/environment.rs b/src/services/environment.rs index 8467b66..119b1a8 100644 --- a/src/services/environment.rs +++ b/src/services/environment.rs @@ -8,9 +8,7 @@ use crate::models::{ use crate::services::feature_utils::filter_out_server_key_only_flag_results; use chrono::{DateTime, Utc}; use flagsmith_flag_engine::engine::get_evaluation_result; -use flagsmith_flag_engine::engine_eval::{ - FlagResult, add_identity_to_context, remove_identity_from_context, -}; +use flagsmith_flag_engine::engine_eval::{FlagResult, add_identity_to_context}; use flagsmith_flag_engine::identities::Trait as FlagsmithTrait; use reqwest::header::HeaderMap; use reqwest::{Client, Url}; @@ -302,13 +300,13 @@ impl EnvironmentService { let context = self .cache - .get_context(&keys.client_key) + .get_identity_free_context(&keys.client_key) .await .ok_or_else(|| { EdgeProxyError::ServiceUnavailable("Environment not loaded".to_string()) })?; - let evaluation_result = get_evaluation_result(&remove_identity_from_context(&context)); + let evaluation_result = get_evaluation_result(&context); let mut flag_results: Vec = evaluation_result.flags.into_values().collect();