1- use :: flagd_evaluator:: ValidationMode ;
1+ use :: flagd_evaluator:: { EvaluationResult , ValidationMode } ;
22use pyo3:: prelude:: * ;
33use pyo3:: types:: PyDict ;
4- use serde_json:: Value ;
4+ use serde_json:: { Map , Value } ;
5+ use std:: collections:: { HashMap , HashSet } ;
56
67/// FlagEvaluator - Stateful feature flag evaluator
78///
@@ -26,6 +27,84 @@ use serde_json::Value;
2627struct FlagEvaluator {
2728 /// Wrap the Rust FlagEvaluator directly - no duplication!
2829 inner : :: flagd_evaluator:: FlagEvaluator ,
30+ /// Cache of pre-evaluated results for static and disabled flags
31+ pre_evaluated : HashMap < String , EvaluationResult > ,
32+ /// Required context keys per flag for context filtering
33+ required_context_keys : HashMap < String , HashSet < String > > ,
34+ }
35+
36+ /// Build a filtered, pre-enriched context containing only the required keys.
37+ ///
38+ /// Extracts only the keys in `required_keys` from the PyDict, adds `targetingKey`
39+ /// (defaulting to `""`) and `$flagd` enrichment with `flagKey` and `timestamp`.
40+ fn build_filtered_context (
41+ flag_key : & str ,
42+ context : & Bound < ' _ , PyDict > ,
43+ required_keys : & HashSet < String > ,
44+ ) -> PyResult < Value > {
45+ let mut map = Map :: new ( ) ;
46+
47+ // Extract only required keys from PyDict
48+ for key in required_keys {
49+ if let Some ( py_val) = context. get_item ( key) ? {
50+ let val: Value = pythonize:: depythonize ( & py_val) ?;
51+ map. insert ( key. clone ( ) , val) ;
52+ }
53+ }
54+
55+ // Ensure targetingKey is present
56+ if !map. contains_key ( "targetingKey" ) {
57+ if let Some ( py_val) = context. get_item ( "targetingKey" ) ? {
58+ let val: Value = pythonize:: depythonize ( & py_val) ?;
59+ map. insert ( "targetingKey" . to_string ( ) , val) ;
60+ } else {
61+ map. insert ( "targetingKey" . to_string ( ) , Value :: String ( String :: new ( ) ) ) ;
62+ }
63+ }
64+
65+ // Add $flagd enrichment
66+ let timestamp = std:: time:: SystemTime :: now ( )
67+ . duration_since ( std:: time:: UNIX_EPOCH )
68+ . unwrap_or_default ( )
69+ . as_secs ( ) ;
70+
71+ let mut flagd_props = Map :: new ( ) ;
72+ flagd_props. insert ( "flagKey" . to_string ( ) , Value :: String ( flag_key. to_string ( ) ) ) ;
73+ flagd_props. insert ( "timestamp" . to_string ( ) , Value :: Number ( timestamp. into ( ) ) ) ;
74+ map. insert ( "$flagd" . to_string ( ) , Value :: Object ( flagd_props) ) ;
75+
76+ Ok ( Value :: Object ( map) )
77+ }
78+
79+ /// Resolve the context for evaluation: use filtered context if required keys are
80+ /// known, otherwise fall back to full depythonize.
81+ fn resolve_context (
82+ flag_key : & str ,
83+ context : & Bound < ' _ , PyDict > ,
84+ required_context_keys : & HashMap < String , HashSet < String > > ,
85+ ) -> PyResult < ( Value , bool ) > {
86+ if let Some ( required_keys) = required_context_keys. get ( flag_key) {
87+ let filtered = build_filtered_context ( flag_key, context, required_keys) ?;
88+ Ok ( ( filtered, true ) )
89+ } else {
90+ let full: Value = pythonize:: depythonize ( context. as_any ( ) ) ?;
91+ Ok ( ( full, false ) )
92+ }
93+ }
94+
95+ /// Call the appropriate Rust evaluate method depending on whether context
96+ /// is pre-enriched.
97+ fn do_evaluate (
98+ inner : & :: flagd_evaluator:: FlagEvaluator ,
99+ flag_key : & str ,
100+ context : & Value ,
101+ pre_enriched : bool ,
102+ ) -> EvaluationResult {
103+ if pre_enriched {
104+ inner. evaluate_flag_pre_enriched ( flag_key, context)
105+ } else {
106+ inner. evaluate_flag ( flag_key, context)
107+ }
29108}
30109
31110#[ pymethods]
@@ -47,6 +126,8 @@ impl FlagEvaluator {
47126
48127 FlagEvaluator {
49128 inner : :: flagd_evaluator:: FlagEvaluator :: new ( mode) ,
129+ pre_evaluated : HashMap :: new ( ) ,
130+ required_context_keys : HashMap :: new ( ) ,
50131 }
51132 }
52133
@@ -77,6 +158,20 @@ impl FlagEvaluator {
77158 ) )
78159 } ) ?;
79160
161+ // Extract pre-evaluated cache
162+ self . pre_evaluated = response. pre_evaluated . clone ( ) . unwrap_or_default ( ) ;
163+
164+ // Extract required context keys (convert Vec<String> to HashSet<String>)
165+ self . required_context_keys = response
166+ . required_context_keys
167+ . as_ref ( )
168+ . map ( |m| {
169+ m. iter ( )
170+ . map ( |( k, v) | ( k. clone ( ) , v. iter ( ) . cloned ( ) . collect ( ) ) )
171+ . collect ( )
172+ } )
173+ . unwrap_or_default ( ) ;
174+
80175 // Convert response to Python dict
81176 pythonize:: pythonize ( py, & response)
82177 . map ( |bound| bound. unbind ( ) )
@@ -102,11 +197,23 @@ impl FlagEvaluator {
102197 flag_key : String ,
103198 context : & Bound < ' _ , PyDict > ,
104199 ) -> PyResult < PyObject > {
105- // Convert context to JSON Value
106- let context_value: Value = pythonize:: depythonize ( context. as_any ( ) ) ?;
200+ // Check pre-evaluation cache first
201+ if let Some ( cached) = self . pre_evaluated . get ( & flag_key) {
202+ return pythonize:: pythonize ( py, cached)
203+ . map ( |bound| bound. unbind ( ) )
204+ . map_err ( |e| {
205+ PyErr :: new :: < pyo3:: exceptions:: PyValueError , _ > ( format ! (
206+ "Failed to convert result: {}" ,
207+ e
208+ ) )
209+ } ) ;
210+ }
107211
108- // Delegate to the Rust FlagEvaluator
109- let result = self . inner . evaluate_flag ( & flag_key, & context_value) ;
212+ // Resolve context (filtered or full)
213+ let ( context_value, pre_enriched) =
214+ resolve_context ( & flag_key, context, & self . required_context_keys ) ?;
215+
216+ let result = do_evaluate ( & self . inner , & flag_key, & context_value, pre_enriched) ;
110217
111218 // Convert result to Python dict
112219 pythonize:: pythonize ( py, & result)
@@ -134,10 +241,21 @@ impl FlagEvaluator {
134241 context : & Bound < ' _ , PyDict > ,
135242 default_value : bool ,
136243 ) -> PyResult < bool > {
137- let context_value: Value = pythonize:: depythonize ( context. as_any ( ) ) ?;
138- let result = self . inner . evaluate_bool ( & flag_key, & context_value) ;
244+ // Check pre-evaluation cache first
245+ if let Some ( cached) = self . pre_evaluated . get ( & flag_key) {
246+ if cached. error_code . is_some ( ) {
247+ return Ok ( default_value) ;
248+ }
249+ return match & cached. value {
250+ Value :: Bool ( b) => Ok ( * b) ,
251+ _ => Ok ( default_value) ,
252+ } ;
253+ }
254+
255+ let ( context_value, pre_enriched) =
256+ resolve_context ( & flag_key, context, & self . required_context_keys ) ?;
257+ let result = do_evaluate ( & self . inner , & flag_key, & context_value, pre_enriched) ;
139258
140- // If there's an error, return the default value
141259 if result. error_code . is_some ( ) {
142260 return Ok ( default_value) ;
143261 }
@@ -163,10 +281,21 @@ impl FlagEvaluator {
163281 context : & Bound < ' _ , PyDict > ,
164282 default_value : String ,
165283 ) -> PyResult < String > {
166- let context_value: Value = pythonize:: depythonize ( context. as_any ( ) ) ?;
167- let result = self . inner . evaluate_string ( & flag_key, & context_value) ;
284+ // Check pre-evaluation cache first
285+ if let Some ( cached) = self . pre_evaluated . get ( & flag_key) {
286+ if cached. error_code . is_some ( ) {
287+ return Ok ( default_value) ;
288+ }
289+ return match & cached. value {
290+ Value :: String ( s) => Ok ( s. clone ( ) ) ,
291+ _ => Ok ( default_value) ,
292+ } ;
293+ }
294+
295+ let ( context_value, pre_enriched) =
296+ resolve_context ( & flag_key, context, & self . required_context_keys ) ?;
297+ let result = do_evaluate ( & self . inner , & flag_key, & context_value, pre_enriched) ;
168298
169- // If there's an error, return the default value
170299 if result. error_code . is_some ( ) {
171300 return Ok ( default_value) ;
172301 }
@@ -192,10 +321,21 @@ impl FlagEvaluator {
192321 context : & Bound < ' _ , PyDict > ,
193322 default_value : i64 ,
194323 ) -> PyResult < i64 > {
195- let context_value: Value = pythonize:: depythonize ( context. as_any ( ) ) ?;
196- let result = self . inner . evaluate_int ( & flag_key, & context_value) ;
324+ // Check pre-evaluation cache first
325+ if let Some ( cached) = self . pre_evaluated . get ( & flag_key) {
326+ if cached. error_code . is_some ( ) {
327+ return Ok ( default_value) ;
328+ }
329+ return match & cached. value {
330+ Value :: Number ( n) => Ok ( n. as_i64 ( ) . unwrap_or ( default_value) ) ,
331+ _ => Ok ( default_value) ,
332+ } ;
333+ }
334+
335+ let ( context_value, pre_enriched) =
336+ resolve_context ( & flag_key, context, & self . required_context_keys ) ?;
337+ let result = do_evaluate ( & self . inner , & flag_key, & context_value, pre_enriched) ;
197338
198- // If there's an error, return the default value
199339 if result. error_code . is_some ( ) {
200340 return Ok ( default_value) ;
201341 }
@@ -221,10 +361,21 @@ impl FlagEvaluator {
221361 context : & Bound < ' _ , PyDict > ,
222362 default_value : f64 ,
223363 ) -> PyResult < f64 > {
224- let context_value: Value = pythonize:: depythonize ( context. as_any ( ) ) ?;
225- let result = self . inner . evaluate_float ( & flag_key, & context_value) ;
364+ // Check pre-evaluation cache first
365+ if let Some ( cached) = self . pre_evaluated . get ( & flag_key) {
366+ if cached. error_code . is_some ( ) {
367+ return Ok ( default_value) ;
368+ }
369+ return match & cached. value {
370+ Value :: Number ( n) => Ok ( n. as_f64 ( ) . unwrap_or ( default_value) ) ,
371+ _ => Ok ( default_value) ,
372+ } ;
373+ }
374+
375+ let ( context_value, pre_enriched) =
376+ resolve_context ( & flag_key, context, & self . required_context_keys ) ?;
377+ let result = do_evaluate ( & self . inner , & flag_key, & context_value, pre_enriched) ;
226378
227- // If there's an error, return the default value
228379 if result. error_code . is_some ( ) {
229380 return Ok ( default_value) ;
230381 }
0 commit comments