flagd_evaluation_engine/targeting/
mod.rs1use 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
13pub 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 let compiled = self.logic.compile(targeting_rule).map_err(|e| {
48 FlagdEvaluationError::Provider(format!("Failed to compile targeting rule: {:?}", e))
49 })?;
50
51 let context_data = self.build_context(flag_key, ctx);
53
54 let mut session = self.logic.session();
56 match session.eval_str(&compiled, &context_data.to_string()) {
57 Ok(result) => {
58 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 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 let mut root = serde_json::Map::new();
76
77 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 let timestamp = std::time::SystemTime::now()
87 .duration_since(std::time::UNIX_EPOCH)
88 .unwrap()
89 .as_secs();
90
91 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 root.insert("$flagd".to_string(), Value::Object(flagd_props));
101
102 for (key, value) in &ctx.custom_fields {
104 root.insert(key.clone(), self.evaluation_context_value_to_json(value));
105 }
106
107 Value::Object(root)
109 }
110
111 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 if let Some(struct_value) = s.downcast_ref::<open_feature::StructValue>() {
128 self.struct_value_to_json(struct_value)
129 } else {
130 Value::Object(serde_json::Map::new())
132 }
133 }
134 }
135 }
136
137 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 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 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 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}