Skip to main content

datafake_rs/
engine.rs

1//! JSONLogic evaluation engine for processing schemas.
2//!
3//! This module provides the core engine that evaluates JSONLogic expressions
4//! with the custom `fake` operator for generating fake data.
5
6use crate::error::{DataFakeError, Result};
7use crate::operators::FakeOperator;
8use crate::types::GenerationContext;
9use datalogic_rs::DataLogic;
10use serde_json::{Map, Value};
11use std::cell::RefCell;
12
13// Thread-local storage for DataLogic instances.
14// Each thread gets its own DataLogic instance to avoid contention.
15thread_local! {
16    static THREAD_LOCAL_DATA_LOGIC: RefCell<Option<DataLogic>> = const { RefCell::new(None) };
17}
18
19fn get_or_init_datalogic() -> &'static RefCell<Option<DataLogic>> {
20    THREAD_LOCAL_DATA_LOGIC.with(|dl_cell| {
21        let mut dl_opt = dl_cell.borrow_mut();
22        if dl_opt.is_none() {
23            // Note: Cannot use preserve_structure mode with custom operators in v4
24            // This is a limitation in datalogic-rs v4 where custom operators are not
25            // recognized in preserve_structure mode
26            let mut dl = DataLogic::new();
27            // Register the fake operator
28            dl.add_operator("fake".to_string(), Box::new(FakeOperator));
29
30            *dl_opt = Some(dl);
31        }
32        // SAFETY: This is safe because:
33        // 1. We're returning a reference to a thread_local static, which has 'static lifetime
34        // 2. The reference is only used within the same thread (thread_local guarantees this)
35        // 3. The RefCell provides interior mutability with runtime borrow checking
36        // 4. The pointer cast is valid since we're just extending the lifetime of the borrow
37        //    to match the thread_local's actual 'static lifetime
38        unsafe { &*(dl_cell as *const RefCell<Option<DataLogic>>) }
39    })
40}
41
42/// The core evaluation engine for JSONLogic expressions with fake data support.
43///
44/// `Engine` provides static methods for evaluating JSONLogic expressions,
45/// processing schemas, and generating variables. It uses thread-local
46/// `DataLogic` instances for safe concurrent usage.
47pub struct Engine;
48
49impl Engine {
50    /// Evaluates a single JSONLogic expression with the given context.
51    ///
52    /// This method compiles and evaluates the expression using datalogic-rs,
53    /// with the custom `fake` operator available for generating fake data.
54    pub fn evaluate(expression: &Value, context: &GenerationContext) -> Result<Value> {
55        // Evaluate the expression directly with JSONLogic (fake operator is registered)
56        let dl_cell = get_or_init_datalogic();
57        let dl_opt = dl_cell.borrow();
58        let data_logic = dl_opt
59            .as_ref()
60            .expect("DataLogic should be initialized by get_or_init_datalogic");
61
62        // Convert context to JSON value for datalogic
63        let context_json =
64            serde_json::to_value(&context.variables).map_err(DataFakeError::JsonError)?;
65
66        // Compile and evaluate the expression
67        let compiled = data_logic.compile(expression).map_err(|e| {
68            DataFakeError::FakeOperatorError(format!("JSONLogic compilation error: {e}"))
69        })?;
70
71        data_logic
72            .evaluate_owned(&compiled, context_json)
73            .map_err(|e| {
74                DataFakeError::FakeOperatorError(format!("JSONLogic evaluation error: {e}"))
75            })
76    }
77
78    /// Processes a schema, evaluating all JSONLogic expressions within it.
79    ///
80    /// This method recursively walks the schema structure, evaluating
81    /// JSONLogic operators and preserving the overall structure of objects and arrays.
82    pub fn process_schema(schema: &Value, context: &GenerationContext) -> Result<Value> {
83        // Since we can't use preserve_structure with custom operators in v4,
84        // we need to manually handle object structure preservation
85        match schema {
86            Value::Object(obj) if obj.len() == 1 => {
87                // Single-key objects might be JSONLogic operators
88                if let Some((key, _value)) = obj.iter().next() {
89                    // Check if this looks like a JSONLogic operator
90                    // Known operators or custom operators should be evaluated
91                    if Self::is_jsonlogic_operator(key) {
92                        return Self::evaluate(schema, context);
93                    }
94                }
95                // Not an operator, process as regular object
96                let mut result = serde_json::Map::new();
97                for (key, value) in obj {
98                    result.insert(key.clone(), Self::process_schema(value, context)?);
99                }
100                Ok(Value::Object(result))
101            }
102            Value::Object(obj) => {
103                // Multi-key objects are treated as templates
104                let mut result = serde_json::Map::new();
105                for (key, value) in obj {
106                    result.insert(key.clone(), Self::process_schema(value, context)?);
107                }
108                Ok(Value::Object(result))
109            }
110            Value::Array(arr) => {
111                let mut result = Vec::new();
112                for item in arr {
113                    result.push(Self::process_schema(item, context)?);
114                }
115                Ok(Value::Array(result))
116            }
117            _ => {
118                // Primitive values are returned as-is
119                Ok(schema.clone())
120            }
121        }
122    }
123
124    fn is_jsonlogic_operator(key: &str) -> bool {
125        // Check if this is a known JSONLogic operator or our custom operator
126        matches!(
127            key,
128            "var" | "==" | "!=" | "===" | "!==" | "!" | "!!" | "or" | "and" | "?:" | "if" |
129            ">" | ">=" | "<" | "<=" | "max" | "min" | "+" | "-" | "*" | "/" | "%" |
130            "map" | "filter" | "reduce" | "all" | "none" | "some" | "merge" | "in" |
131            "cat" | "substr" | "log" | "method" | "preserve" | "missing" | "missing_some" |
132            // Our custom operator
133            "fake"
134        )
135    }
136
137    /// Generates values for all defined variables.
138    ///
139    /// Each variable expression is evaluated and the results are returned
140    /// as a map that can be used as context for schema processing.
141    pub fn generate_variables(variables: &Map<String, Value>) -> Result<Map<String, Value>> {
142        if variables.is_empty() {
143            return Ok(Map::new());
144        }
145
146        // Since we can't use preserve_structure with custom operators,
147        // we process each variable individually
148        let temp_context = GenerationContext::new();
149        let variables_as_value = Value::Object(variables.clone());
150
151        match Self::process_schema(&variables_as_value, &temp_context)? {
152            Value::Object(map) => Ok(map),
153            _ => Err(DataFakeError::FakeOperatorError(
154                "Variables evaluation did not return an object".to_string(),
155            )),
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use serde_json::json;
164
165    #[test]
166    fn test_evaluate_simple_fake() {
167        let expression = json!({"fake": ["uuid"]});
168        let context = GenerationContext::new();
169        let result = Engine::evaluate(&expression, &context).unwrap();
170        assert!(result.is_string());
171        assert_eq!(result.as_str().unwrap().len(), 36);
172    }
173
174    #[test]
175    fn test_evaluate_var_reference() {
176        let expression = json!({"var": "userId"});
177        let mut context = GenerationContext::new();
178        context.set_variable("userId".to_string(), json!("test-id-123"));
179
180        let result = Engine::evaluate(&expression, &context).unwrap();
181        assert_eq!(result, json!("test-id-123"));
182    }
183
184    #[test]
185    fn test_process_schema_nested() {
186        let schema = json!({
187            "id": {"fake": ["uuid"]},
188            "user": {
189                "name": {"fake": ["name"]},
190                "email": {"fake": ["email"]}
191            }
192        });
193
194        let context = GenerationContext::new();
195        let result = Engine::process_schema(&schema, &context).unwrap();
196
197        assert!(result["id"].is_string());
198        assert!(result["user"]["name"].is_string());
199        assert!(result["user"]["email"].as_str().unwrap().contains('@'));
200    }
201
202    #[test]
203    fn test_process_schema_with_array() {
204        let schema = json!({
205            "tags": [
206                {"fake": ["word"]},
207                {"fake": ["word"]},
208                {"fake": ["word"]}
209            ]
210        });
211
212        let context = GenerationContext::new();
213        let result = Engine::process_schema(&schema, &context).unwrap();
214
215        assert!(result["tags"].is_array());
216        assert_eq!(result["tags"].as_array().unwrap().len(), 3);
217    }
218
219    #[test]
220    fn test_generate_variables() {
221        let variables = json!({
222            "userId": {"fake": ["uuid"]},
223            "timestamp": {"fake": ["u64", 1000000, 9999999]}
224        })
225        .as_object()
226        .unwrap()
227        .clone();
228
229        let result = Engine::generate_variables(&variables).unwrap();
230
231        assert!(result.contains_key("userId"));
232        assert!(result.contains_key("timestamp"));
233        assert!(result["userId"].is_string());
234        assert!(result["timestamp"].is_number());
235    }
236
237    #[test]
238    fn test_process_schema_with_cat_operator() {
239        let schema = json!({
240            "terminal": {"cat": ["ABCD", "XXXX"]},
241            "code": {"cat": [{"var": "prefix"}, "-", {"var": "suffix"}]}
242        });
243
244        let mut context = GenerationContext::new();
245        context.set_variable("prefix".to_string(), json!("PRE"));
246        context.set_variable("suffix".to_string(), json!("SUF"));
247
248        let result = Engine::process_schema(&schema, &context).unwrap();
249
250        assert_eq!(result["terminal"], "ABCDXXXX");
251        assert_eq!(result["code"], "PRE-SUF");
252    }
253
254    #[test]
255    fn test_jsonlogic_operators_in_schema() {
256        let schema = json!({
257            "isActive": {"==": [{"var": "status"}, "active"]},
258            "fullName": {"cat": [{"var": "firstName"}, " ", {"var": "lastName"}]},
259            "age": {"+": [{"var": "baseAge"}, 10]},
260            "hasDiscount": {">": [{"var": "purchases"}, 5]}
261        });
262
263        let mut context = GenerationContext::new();
264        context.set_variable("status".to_string(), json!("active"));
265        context.set_variable("firstName".to_string(), json!("John"));
266        context.set_variable("lastName".to_string(), json!("Doe"));
267        context.set_variable("baseAge".to_string(), json!(20));
268        context.set_variable("purchases".to_string(), json!(10));
269
270        let result = Engine::process_schema(&schema, &context).unwrap();
271
272        assert_eq!(result["isActive"], true);
273        assert_eq!(result["fullName"], "John Doe");
274        assert_eq!(result["age"], 30);
275        assert_eq!(result["hasDiscount"], true);
276    }
277
278    #[test]
279    fn test_preserve_structure_with_custom_operators() {
280        // Test that custom operators work with preserve_structure enabled
281        let schema = json!({
282            "user": {
283                "id": {"fake": ["uuid"]},
284                "profile": {
285                    "name": {"fake": ["name"]},
286                    "age": {"fake": ["u8", 18, 65]},
287                    "nested": {
288                        "email": {"fake": ["email"]},
289                        "active": true,
290                        "count": 42
291                    }
292                }
293            },
294            "metadata": {
295                "version": "1.0",
296                "generated": {"fake": ["bool"]}
297            }
298        });
299
300        let context = GenerationContext::new();
301        let result = Engine::process_schema(&schema, &context).unwrap();
302
303        // Check structure is preserved
304        assert!(result["user"]["id"].is_string());
305        assert_eq!(result["user"]["id"].as_str().unwrap().len(), 36); // UUID length
306        assert!(result["user"]["profile"]["name"].is_string());
307        assert!(result["user"]["profile"]["age"].is_number());
308        let age = result["user"]["profile"]["age"].as_u64().unwrap();
309        assert!((18..=65).contains(&age));
310        assert!(
311            result["user"]["profile"]["nested"]["email"]
312                .as_str()
313                .unwrap()
314                .contains('@')
315        );
316        assert_eq!(result["user"]["profile"]["nested"]["active"], true);
317        assert_eq!(result["user"]["profile"]["nested"]["count"], 42);
318        assert_eq!(result["metadata"]["version"], "1.0");
319        assert!(result["metadata"]["generated"].is_boolean());
320    }
321}