open_feature_flagd/resolver/in_process/targeting/
mod.rs

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