Skip to main content

flagd_evaluation_engine/targeting/
mod.rs

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