Skip to main content

flagd_evaluation_engine/targeting/
mod.rs

1use crate::error::FlagdEvaluationError;
2use datalogic_rs::Engine;
3use open_feature::{EvaluationContext, EvaluationContextFieldValue};
4use serde_json::Value;
5use std::sync::Arc;
6
7mod fractional;
8mod semver;
9
10use fractional::FractionalOperator;
11use semver::SemVerOperator;
12
13/// JSONLogic-based targeting rule evaluator for flag evaluation
14///
15/// Supports custom operators for flagd-specific targeting:
16/// - `fractional`: Consistent hashing for percentage-based rollouts
17/// - `sem_ver`: Semantic version comparison
18pub struct Operator {
19    logic: Arc<Engine>,
20}
21
22impl Default for Operator {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28impl Operator {
29    pub fn new() -> Self {
30        let logic = Engine::builder()
31            .add_operator("fractional", FractionalOperator)
32            .add_operator("sem_ver", SemVerOperator)
33            .build();
34
35        Operator {
36            logic: Arc::new(logic),
37        }
38    }
39
40    pub fn apply(
41        &self,
42        flag_key: &str,
43        targeting_rule: &str,
44        ctx: &EvaluationContext,
45    ) -> Result<Option<String>, FlagdEvaluationError> {
46        // Compile the logic
47        let compiled = self.logic.compile(targeting_rule).map_err(|e| {
48            FlagdEvaluationError::Provider(format!("Failed to compile targeting rule: {:?}", e))
49        })?;
50
51        // Build context data as serde_json::Value
52        let context_data = self.build_context(flag_key, ctx);
53
54        // Evaluate using datalogic-rs
55        let mut session = self.logic.session();
56        match session.eval_str(&compiled, &context_data.to_string()) {
57            Ok(result) => {
58                // Convert result to Option<String>
59                match serde_json::from_str::<Value>(&result)? {
60                    Value::String(s) => Ok(Some(s)),
61                    Value::Null => Ok(None),
62                    _ => Ok(Some(result.to_string())),
63                }
64            }
65            Err(e) => {
66                // Log and return None on error
67                tracing::debug!("DataLogic evaluation error: {:?}", e);
68                Ok(None)
69            }
70        }
71    }
72
73    fn build_context(&self, flag_key: &str, ctx: &EvaluationContext) -> Value {
74        // Create a JSON object for our context
75        let mut root = serde_json::Map::new();
76
77        // Add targeting key if present
78        if let Some(targeting_key) = &ctx.targeting_key {
79            root.insert(
80                "targetingKey".to_string(),
81                Value::String(targeting_key.clone()),
82            );
83        }
84
85        // Add flagd metadata
86        let timestamp = std::time::SystemTime::now()
87            .duration_since(std::time::UNIX_EPOCH)
88            .unwrap()
89            .as_secs();
90
91        // Create flagd object
92        let mut flagd_props = serde_json::Map::new();
93        flagd_props.insert("flagKey".to_string(), Value::String(flag_key.to_string()));
94        flagd_props.insert(
95            "timestamp".to_string(),
96            Value::Number(serde_json::Number::from(timestamp)),
97        );
98
99        // Add flagd object to main object
100        root.insert("$flagd".to_string(), Value::Object(flagd_props));
101
102        // Add custom fields
103        for (key, value) in &ctx.custom_fields {
104            root.insert(key.clone(), self.evaluation_context_value_to_json(value));
105        }
106
107        // Return the JSON object
108        Value::Object(root)
109    }
110
111    /// Convert EvaluationContextFieldValue to serde_json::Value
112    fn evaluation_context_value_to_json(&self, value: &EvaluationContextFieldValue) -> Value {
113        match value {
114            EvaluationContextFieldValue::String(s) => Value::String(s.clone()),
115            EvaluationContextFieldValue::Bool(b) => Value::Bool(*b),
116            EvaluationContextFieldValue::Int(i) => Value::Number(serde_json::Number::from(*i)),
117            EvaluationContextFieldValue::Float(f) => {
118                if let Some(num) = serde_json::Number::from_f64(*f) {
119                    Value::Number(num)
120                } else {
121                    Value::Null
122                }
123            }
124            EvaluationContextFieldValue::DateTime(dt) => Value::String(dt.to_string()),
125            EvaluationContextFieldValue::Struct(s) => {
126                // Try to downcast to StructValue for proper serialization
127                if let Some(struct_value) = s.downcast_ref::<open_feature::StructValue>() {
128                    self.struct_value_to_json(struct_value)
129                } else {
130                    // Fallback for other types - serialize as string representation
131                    Value::Object(serde_json::Map::new())
132                }
133            }
134        }
135    }
136
137    /// Convert StructValue to serde_json::Value with proper nested serialization
138    fn struct_value_to_json(&self, struct_value: &open_feature::StructValue) -> Value {
139        let mut map = serde_json::Map::new();
140        for (key, value) in &struct_value.fields {
141            map.insert(key.clone(), self.open_feature_value_to_json(value));
142        }
143        Value::Object(map)
144    }
145
146    /// Convert OpenFeature Value to serde_json::Value
147    fn open_feature_value_to_json(&self, value: &open_feature::Value) -> Value {
148        match value {
149            open_feature::Value::String(s) => Value::String(s.clone()),
150            open_feature::Value::Bool(b) => Value::Bool(*b),
151            open_feature::Value::Int(i) => Value::Number(serde_json::Number::from(*i)),
152            open_feature::Value::Float(f) => {
153                if let Some(num) = serde_json::Number::from_f64(*f) {
154                    Value::Number(num)
155                } else {
156                    Value::Null
157                }
158            }
159            open_feature::Value::Struct(s) => self.struct_value_to_json(s),
160            open_feature::Value::Array(arr) => Value::Array(
161                arr.iter()
162                    .map(|v| self.open_feature_value_to_json(v))
163                    .collect(),
164            ),
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use open_feature::{EvaluationContext, StructValue, Value as OFValue};
173    use std::collections::HashMap;
174
175    #[test]
176    fn test_build_context_with_targeting_key() {
177        let operator = Operator::new();
178        let ctx = EvaluationContext::default().with_targeting_key("user-123");
179
180        let result = operator.build_context("test-flag", &ctx);
181
182        assert!(result.is_object());
183        let obj = result.as_object().unwrap();
184        assert_eq!(obj.get("targetingKey").unwrap(), "user-123");
185        assert!(obj.contains_key("$flagd"));
186
187        let flagd = obj.get("$flagd").unwrap().as_object().unwrap();
188        assert_eq!(flagd.get("flagKey").unwrap(), "test-flag");
189        assert!(flagd.contains_key("timestamp"));
190    }
191
192    #[test]
193    fn test_build_context_with_custom_fields() {
194        let operator = Operator::new();
195        let ctx = EvaluationContext::default()
196            .with_custom_field("string_field", "value")
197            .with_custom_field("int_field", 42i64)
198            .with_custom_field("bool_field", true)
199            .with_custom_field("float_field", 3.14f64);
200
201        let result = operator.build_context("test-flag", &ctx);
202        let obj = result.as_object().unwrap();
203
204        assert_eq!(obj.get("string_field").unwrap(), "value");
205        assert_eq!(obj.get("int_field").unwrap(), 42);
206        assert_eq!(obj.get("bool_field").unwrap(), true);
207        assert_eq!(obj.get("float_field").unwrap(), 3.14);
208    }
209
210    #[test]
211    fn test_open_feature_value_to_json_primitives() {
212        let operator = Operator::new();
213
214        assert_eq!(
215            operator.open_feature_value_to_json(&OFValue::String("test".to_string())),
216            Value::String("test".to_string())
217        );
218        assert_eq!(
219            operator.open_feature_value_to_json(&OFValue::Bool(true)),
220            Value::Bool(true)
221        );
222        assert_eq!(
223            operator.open_feature_value_to_json(&OFValue::Int(42)),
224            Value::Number(42.into())
225        );
226        assert_eq!(
227            operator.open_feature_value_to_json(&OFValue::Float(3.14)),
228            Value::Number(serde_json::Number::from_f64(3.14).unwrap())
229        );
230    }
231
232    #[test]
233    fn test_struct_value_to_json() {
234        let operator = Operator::new();
235
236        let mut fields = HashMap::new();
237        fields.insert("name".to_string(), OFValue::String("test".to_string()));
238        fields.insert("count".to_string(), OFValue::Int(5));
239        fields.insert("enabled".to_string(), OFValue::Bool(true));
240
241        let struct_value = StructValue { fields };
242        let result = operator.struct_value_to_json(&struct_value);
243
244        assert!(result.is_object());
245        let obj = result.as_object().unwrap();
246        assert_eq!(obj.get("name").unwrap(), "test");
247        assert_eq!(obj.get("count").unwrap(), 5);
248        assert_eq!(obj.get("enabled").unwrap(), true);
249    }
250
251    #[test]
252    fn test_nested_struct_value_to_json() {
253        let operator = Operator::new();
254
255        // Create nested struct
256        let mut inner_fields = HashMap::new();
257        inner_fields.insert(
258            "inner_key".to_string(),
259            OFValue::String("inner_value".to_string()),
260        );
261        let inner_struct = StructValue {
262            fields: inner_fields,
263        };
264
265        let mut outer_fields = HashMap::new();
266        outer_fields.insert(
267            "outer_key".to_string(),
268            OFValue::String("outer_value".to_string()),
269        );
270        outer_fields.insert("nested".to_string(), OFValue::Struct(inner_struct));
271
272        let outer_struct = StructValue {
273            fields: outer_fields,
274        };
275        let result = operator.struct_value_to_json(&outer_struct);
276
277        assert!(result.is_object());
278        let obj = result.as_object().unwrap();
279        assert_eq!(obj.get("outer_key").unwrap(), "outer_value");
280
281        let nested = obj.get("nested").unwrap().as_object().unwrap();
282        assert_eq!(nested.get("inner_key").unwrap(), "inner_value");
283    }
284
285    #[test]
286    fn test_array_value_to_json() {
287        let operator = Operator::new();
288
289        let array = vec![
290            OFValue::String("a".to_string()),
291            OFValue::Int(1),
292            OFValue::Bool(true),
293        ];
294
295        let result = operator.open_feature_value_to_json(&OFValue::Array(array));
296
297        assert!(result.is_array());
298        let arr = result.as_array().unwrap();
299        assert_eq!(arr.len(), 3);
300        assert_eq!(arr[0], "a");
301        assert_eq!(arr[1], 1);
302        assert_eq!(arr[2], true);
303    }
304
305    #[test]
306    fn test_apply_simple_targeting_rule() {
307        let operator = Operator::new();
308        let ctx = EvaluationContext::default().with_custom_field("tier", "premium");
309
310        // Simple if rule: if tier == "premium" then "gold" else "silver"
311        let rule = r#"{
312            "if": [
313                {"==": [{"var": "tier"}, "premium"]},
314                "gold",
315                "silver"
316            ]
317        }"#;
318
319        let result = operator.apply("test-flag", rule, &ctx).unwrap();
320        assert_eq!(result, Some("gold".to_string()));
321    }
322
323    #[test]
324    fn test_apply_targeting_rule_with_default() {
325        let operator = Operator::new();
326        let ctx = EvaluationContext::default().with_custom_field("tier", "basic");
327
328        let rule = r#"{
329            "if": [
330                {"==": [{"var": "tier"}, "premium"]},
331                "gold",
332                "silver"
333            ]
334        }"#;
335
336        let result = operator.apply("test-flag", rule, &ctx).unwrap();
337        assert_eq!(result, Some("silver".to_string()));
338    }
339
340    #[test]
341    fn test_apply_targeting_rule_with_string_operator() {
342        let operator = Operator::new();
343        let ctx = EvaluationContext::default().with_custom_field("email", "employee@company.com");
344
345        let rule = r#"{
346            "if": [
347                {"ends_with": [{"var": "email"}, "@company.com"]},
348                "internal",
349                "external"
350            ]
351        }"#;
352
353        let result = operator.apply("test-flag", rule, &ctx).unwrap();
354        assert_eq!(result, Some("internal".to_string()));
355    }
356
357    #[test]
358    fn test_apply_empty_targeting_returns_none() {
359        let operator = Operator::new();
360        let ctx = EvaluationContext::default();
361
362        let rule = "null";
363        let result = operator.apply("test-flag", rule, &ctx).unwrap();
364        assert_eq!(result, None);
365    }
366}