datafake_rs/
engine.rs

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