flagd_evaluation_engine/targeting/
mod.rs1use 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
16pub 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 let context_data = self.build_context(flag_key, ctx);
58
59 let mut session = self.logic.session();
61 match session.eval_str(&compiled, &context_data.to_string()) {
62 Ok(result) => {
63 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 let mut root = serde_json::Map::new();
88
89 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 let timestamp = std::time::SystemTime::now()
99 .duration_since(std::time::UNIX_EPOCH)
100 .unwrap()
101 .as_secs();
102
103 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 root.insert("$flagd".to_string(), Value::Object(flagd_props));
113
114 for (key, value) in &ctx.custom_fields {
116 root.insert(key.clone(), self.evaluation_context_value_to_json(value));
117 }
118
119 Value::Object(root)
121 }
122
123 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 if let Some(struct_value) = s.downcast_ref::<open_feature::StructValue>() {
140 self.struct_value_to_json(struct_value)
141 } else {
142 Value::Object(serde_json::Map::new())
144 }
145 }
146 }
147 }
148
149 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 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 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 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}