1pub mod error;
35pub mod extract;
36
37use serde::{Deserialize, Serialize};
38use serde_json::{json, Value};
39
40use error::{RuleEngineError, RuleEngineResult};
41use extract::extract_f64;
42
43#[cfg(not(target_arch = "wasm32"))]
44use rayon::prelude::*;
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47pub struct EvaluationResult {
49 pub result: Option<Value>,
50 pub error: Option<String>,
51}
52
53impl EvaluationResult {
54 fn ok(result: Value) -> Self {
55 Self {
56 result: Some(result),
57 error: None,
58 }
59 }
60
61 fn err(error: impl Into<String>) -> Self {
62 Self {
63 result: None,
64 error: Some(error.into()),
65 }
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
70pub struct NumericEvaluationResult {
72 pub result: f64,
73 pub error: Option<String>,
74}
75
76fn parse_rule(rule_json: &str) -> RuleEngineResult<Value> {
77 serde_json::from_str(rule_json).map_err(|e| RuleEngineError::InvalidRule(e.to_string()))
78}
79
80fn parse_context(context_json: &str) -> RuleEngineResult<Value> {
81 serde_json::from_str(context_json).map_err(|e| RuleEngineError::InvalidContext(e.to_string()))
82}
83
84fn apply_rule(rule: &Value, context: &Value) -> RuleEngineResult<Value> {
85 jsonlogic::apply(rule, context).map_err(|e| RuleEngineError::Evaluation(e.to_string()))
86}
87
88fn evaluate_context(rule: &Value, context_json: &str) -> EvaluationResult {
89 let context = match parse_context(context_json) {
90 Ok(context) => context,
91 Err(error) => return EvaluationResult::err(error.to_string()),
92 };
93
94 match apply_rule(rule, &context) {
95 Ok(result) => EvaluationResult::ok(result),
96 Err(error) => EvaluationResult::err(error.to_string()),
97 }
98}
99
100fn default_validation_context() -> Value {
101 json!({
102 "amount": 100.0,
103 "country": "MX",
104 "method": "CREDIT_CARD",
105 "active": true,
106 "count": 3,
107 "score": 720,
108 "tags": ["vip", "beta"],
109 "user": {
110 "tier": "gold",
111 "region": "north"
112 },
113 "metrics": {
114 "total_volume": 1000.0,
115 "chargebacks": 1
116 },
117 "null_value": null
118 })
119}
120
121#[cfg(not(target_arch = "wasm32"))]
122fn map_contexts<T, F>(contexts_json: &[String], evaluator: F) -> Vec<T>
123where
124 T: Send,
125 F: Fn(&str) -> T + Sync + Send,
126{
127 contexts_json
128 .par_iter()
129 .map(|context_json| evaluator(context_json))
130 .collect()
131}
132
133#[cfg(target_arch = "wasm32")]
134fn map_contexts<T, F>(contexts_json: &[String], evaluator: F) -> Vec<T>
135where
136 F: Fn(&str) -> T,
137{
138 contexts_json
139 .iter()
140 .map(|context_json| evaluator(context_json))
141 .collect()
142}
143
144#[cfg(not(target_arch = "wasm32"))]
145fn available_threads() -> usize {
146 rayon::current_num_threads()
147}
148
149#[cfg(target_arch = "wasm32")]
150fn available_threads() -> usize {
151 1
152}
153
154pub fn evaluate(rule_json: &str, context_json: &str) -> RuleEngineResult<Value> {
156 let rule = parse_rule(rule_json)?;
157 let context = parse_context(context_json)?;
158 apply_rule(&rule, &context)
159}
160
161pub fn evaluate_rule(rule_json: &str, context_json: &str) -> RuleEngineResult<Value> {
163 evaluate(rule_json, context_json)
164}
165
166pub fn evaluate_numeric(rule_json: &str, context_json: &str) -> RuleEngineResult<f64> {
168 extract_f64(evaluate(rule_json, context_json)?)
169}
170
171pub fn evaluate_batch(rule_json: &str, contexts_json: &[String]) -> RuleEngineResult<Vec<Value>> {
173 let rule = parse_rule(rule_json)?;
174 Ok(map_contexts(contexts_json, |context_json| {
175 evaluate_context(&rule, context_json)
176 .result
177 .unwrap_or(Value::Null)
178 }))
179}
180
181pub fn evaluate_batch_detailed(
183 rule_json: &str,
184 contexts_json: &[String],
185) -> RuleEngineResult<Vec<EvaluationResult>> {
186 let rule = parse_rule(rule_json)?;
187 Ok(map_contexts(contexts_json, |context_json| {
188 evaluate_context(&rule, context_json)
189 }))
190}
191
192pub fn evaluate_batch_numeric(
194 rule_json: &str,
195 contexts_json: &[String],
196) -> RuleEngineResult<Vec<f64>> {
197 let results = evaluate_batch_detailed(rule_json, contexts_json)?;
198 Ok(results
199 .into_iter()
200 .map(|item| match item.result {
201 Some(result) => extract_f64(result).unwrap_or(0.0),
202 None => 0.0,
203 })
204 .collect())
205}
206
207pub fn evaluate_batch_numeric_detailed(
209 rule_json: &str,
210 contexts_json: &[String],
211) -> RuleEngineResult<Vec<NumericEvaluationResult>> {
212 let results = evaluate_batch_detailed(rule_json, contexts_json)?;
213 Ok(results
214 .into_iter()
215 .map(|item| match (item.result, item.error) {
216 (Some(result), None) => match extract_f64(result) {
217 Ok(value) => NumericEvaluationResult {
218 result: value,
219 error: None,
220 },
221 Err(error) => NumericEvaluationResult {
222 result: 0.0,
223 error: Some(error.to_string()),
224 },
225 },
226 (_, Some(error)) => NumericEvaluationResult {
227 result: 0.0,
228 error: Some(error),
229 },
230 (None, None) => NumericEvaluationResult {
231 result: 0.0,
232 error: Some("Unknown evaluation failure".to_string()),
233 },
234 })
235 .collect())
236}
237
238pub fn validate_rule(rule_json: &str) -> RuleEngineResult<bool> {
240 let rule = parse_rule(rule_json)?;
241 let context = default_validation_context();
242
243 apply_rule(&rule, &context)
244 .map(|_| true)
245 .map_err(|error| RuleEngineError::Evaluation(format!("Rule validation failed: {error}")))
246}
247
248pub fn serialize_value(value: &Value) -> RuleEngineResult<String> {
250 serde_json::to_string(value).map_err(|e| RuleEngineError::Serialization(e.to_string()))
251}
252
253pub fn serialize<T: Serialize>(value: &T) -> RuleEngineResult<String> {
255 serde_json::to_string(value).map_err(|e| RuleEngineError::Serialization(e.to_string()))
256}
257
258pub fn get_core_info() -> Value {
260 json!({
261 "engine": "jsonlogic-fast",
262 "version": env!("CARGO_PKG_VERSION"),
263 "parallelism": if cfg!(target_arch = "wasm32") { "sequential" } else { "rayon" },
264 "available_threads": available_threads(),
265 "evaluator": "jsonlogic-rs",
266 "result_model": "serde_json::Value"
267 })
268}
269
270#[cfg(test)]
271mod tests {
272 use serde_json::json;
273
274 use super::*;
275
276 #[test]
277 fn evaluate_preserves_string_results() {
278 let rule = r#"{"if":[{"==":[{"var":"country"},"MX"]},"domestic","intl"]}"#;
279 let context = r#"{"country":"MX"}"#;
280
281 assert_eq!(evaluate(rule, context).unwrap(), json!("domestic"));
282 }
283
284 #[test]
285 fn evaluate_numeric_coerces_boolean_results() {
286 let rule = r#"{"==":[{"var":"country"},"MX"]}"#;
287 let context = r#"{"country":"MX"}"#;
288
289 assert_eq!(evaluate_numeric(rule, context).unwrap(), 1.0);
290 }
291
292 #[test]
293 fn evaluate_batch_returns_null_for_invalid_contexts() {
294 let rule = r#"{"var":"amount"}"#;
295 let contexts = vec![r#"{"amount":10}"#.to_string(), "{bad json}".to_string()];
296
297 assert_eq!(
298 evaluate_batch(rule, &contexts).unwrap(),
299 vec![json!(10), Value::Null]
300 );
301 }
302
303 #[test]
304 fn evaluate_batch_detailed_reports_errors() {
305 let rule = r#"{"var":"amount"}"#;
306 let contexts = vec!["{}".to_string(), "{bad json}".to_string()];
307 let results = evaluate_batch_detailed(rule, &contexts).unwrap();
308
309 assert_eq!(results[0].result, Some(Value::Null));
310 assert!(results[1]
311 .error
312 .as_ref()
313 .is_some_and(|message| message.contains("Error parsing context")));
314 }
315
316 #[test]
317 fn evaluate_batch_numeric_keeps_fail_safe_zeroes() {
318 let rule = r#"{"var":"amount"}"#;
319 let contexts = vec!["{}".to_string(), "{bad json}".to_string()];
320
321 assert_eq!(
322 evaluate_batch_numeric(rule, &contexts).unwrap(),
323 vec![0.0, 0.0]
324 );
325 }
326
327 #[test]
328 fn validate_rule_accepts_generic_contexts() {
329 let rule = r#"{"cat":[{"var":"user.tier"},"-",{"var":"country"}]}"#;
330 assert!(validate_rule(rule).unwrap());
331 }
332
333 #[test]
338 fn var_dot_notation_resolves_nested_objects() {
339 let rule = r#"{"var":"user.address.city"}"#;
340 let context = r#"{"user":{"address":{"city":"Monterrey"}}}"#;
341 assert_eq!(evaluate(rule, context).unwrap(), json!("Monterrey"));
342 }
343
344 #[test]
345 fn var_with_default_value_on_missing_path() {
346 let rule = r#"{"var":["user.phone","N/A"]}"#;
347 let context = r#"{"user":{"name":"Ana"}}"#;
348 assert_eq!(evaluate(rule, context).unwrap(), json!("N/A"));
349 }
350
351 #[test]
352 fn var_array_index_access() {
353 let rule = r#"{"var":"items.1"}"#;
354 let context = r#"{"items":["a","b","c"]}"#;
355 assert_eq!(evaluate(rule, context).unwrap(), json!("b"));
356 }
357
358 #[test]
363 fn nested_if_evaluates_correctly() {
364 let rule = r#"{"if":[true,{"if":[false,1,2]},3]}"#;
365 let context = r#"{}"#;
366 assert_eq!(evaluate(rule, context).unwrap(), json!(2));
367 }
368
369 #[test]
370 fn deeply_nested_conditionals() {
371 let rule =
372 r#"{"if":[{">":[{"var":"x"},10]},{"if":[{">":[{"var":"x"},20]},"high","mid"]},"low"]}"#;
373 let context = r#"{"x":25}"#;
374 assert_eq!(evaluate(rule, context).unwrap(), json!("high"));
375 }
376
377 #[test]
382 fn evaluate_null_literal() {
383 let rule = r#"{"var":"missing_key"}"#;
384 let context = r#"{"other":1}"#;
385 assert_eq!(evaluate(rule, context).unwrap(), Value::Null);
386 }
387
388 #[test]
389 fn evaluate_boolean_literal() {
390 let rule = r#"{"==":[true,true]}"#;
391 assert_eq!(evaluate(rule, r#"{}"#).unwrap(), json!(true));
392 }
393
394 #[test]
395 fn evaluate_number_result() {
396 let rule = r#"{"+":[1,2,3]}"#;
397 assert_eq!(evaluate(rule, r#"{}"#).unwrap(), json!(6.0));
398 }
399
400 #[test]
401 fn evaluate_string_cat() {
402 let rule = r#"{"cat":["hello"," ","world"]}"#;
403 assert_eq!(evaluate(rule, r#"{}"#).unwrap(), json!("hello world"));
404 }
405
406 #[test]
411 fn arithmetic_addition_and_subtraction() {
412 assert_eq!(evaluate(r#"{"+":[10,5]}"#, "{}").unwrap(), json!(15.0));
413 assert_eq!(evaluate(r#"{"-":[10,5]}"#, "{}").unwrap(), json!(5.0));
414 }
415
416 #[test]
417 fn arithmetic_multiply_and_divide() {
418 assert_eq!(evaluate(r#"{"*":[4,3]}"#, "{}").unwrap(), json!(12.0));
419 assert_eq!(evaluate(r#"{"/":[10,4]}"#, "{}").unwrap(), json!(2.5));
420 }
421
422 #[test]
423 fn modulo_operator() {
424 assert_eq!(evaluate(r#"{"%":[10,3]}"#, "{}").unwrap(), json!(1.0));
425 }
426
427 #[test]
432 fn comparison_operators() {
433 assert_eq!(evaluate(r#"{">":[5,3]}"#, "{}").unwrap(), json!(true));
434 assert_eq!(evaluate(r#"{"<":[5,3]}"#, "{}").unwrap(), json!(false));
435 assert_eq!(evaluate(r#"{">=":[5,5]}"#, "{}").unwrap(), json!(true));
436 assert_eq!(evaluate(r#"{"<=":[4,5]}"#, "{}").unwrap(), json!(true));
437 assert_eq!(evaluate(r#"{"==":[5,5]}"#, "{}").unwrap(), json!(true));
438 assert_eq!(evaluate(r#"{"!=":[5,3]}"#, "{}").unwrap(), json!(true));
439 }
440
441 #[test]
446 fn logical_and_short_circuits() {
447 let rule = r#"{"and":[false,{"var":"missing"}]}"#;
448 assert_eq!(evaluate(rule, r#"{}"#).unwrap(), json!(false));
449 }
450
451 #[test]
452 fn logical_or_short_circuits() {
453 let rule = r#"{"or":[true,{"var":"missing"}]}"#;
454 assert_eq!(evaluate(rule, r#"{}"#).unwrap(), json!(true));
455 }
456
457 #[test]
458 fn logical_not() {
459 assert_eq!(evaluate(r#"{"!":[true]}"#, "{}").unwrap(), json!(false));
460 assert_eq!(evaluate(r#"{"!":[false]}"#, "{}").unwrap(), json!(true));
461 }
462
463 #[test]
464 fn double_negation() {
465 assert_eq!(evaluate(r#"{"!!":[1]}"#, "{}").unwrap(), json!(true));
466 assert_eq!(evaluate(r#"{"!!":[0]}"#, "{}").unwrap(), json!(false));
467 }
468
469 #[test]
474 fn map_operator() {
475 let rule = r#"{"map":[{"var":"items"},{"*":[{"var":""},2]}]}"#;
476 let context = r#"{"items":[1,2,3]}"#;
477 assert_eq!(evaluate(rule, context).unwrap(), json!([2.0, 4.0, 6.0]));
478 }
479
480 #[test]
481 fn filter_operator() {
482 let rule = r#"{"filter":[{"var":"items"},{">":[{"var":""},2]}]}"#;
483 let context = r#"{"items":[1,2,3,4,5]}"#;
484 assert_eq!(evaluate(rule, context).unwrap(), json!([3, 4, 5]));
485 }
486
487 #[test]
488 fn reduce_operator() {
489 let rule =
490 r#"{"reduce":[{"var":"items"},{"+":[{"var":"current"},{"var":"accumulator"}]},0]}"#;
491 let context = r#"{"items":[1,2,3,4]}"#;
492 let result =
493 evaluate(rule, context).unwrap_or_else(|e| panic!("reduce evaluation failed: {e}"));
494 let n = result
495 .as_f64()
496 .unwrap_or_else(|| panic!("expected numeric result, got: {result}"));
497 assert!((n - 10.0).abs() < 1e-6, "expected 10.0, got {n}");
498 }
499
500 #[test]
501 fn in_operator_string() {
502 assert_eq!(
503 evaluate(r#"{"in":["Spring","Springfield"]}"#, "{}").unwrap(),
504 json!(true)
505 );
506 }
507
508 #[test]
509 fn in_operator_array() {
510 assert_eq!(
511 evaluate(r#"{"in":["banana",["apple","banana","cherry"]]}"#, "{}").unwrap(),
512 json!(true)
513 );
514 }
515
516 #[test]
517 fn merge_operator() {
518 assert_eq!(
519 evaluate(r#"{"merge":[[1,2],[3,4]]}"#, "{}").unwrap(),
520 json!([1, 2, 3, 4])
521 );
522 }
523
524 #[test]
525 fn missing_operator() {
526 let rule = r#"{"missing":["a","b","c"]}"#;
527 let context = r#"{"a":1,"c":3}"#;
528 assert_eq!(evaluate(rule, context).unwrap(), json!(["b"]));
529 }
530
531 #[test]
532 fn missing_some_operator() {
533 let rule = r#"{"missing_some":[1,["a","b","c"]]}"#;
534 let context = r#"{"a":1}"#;
535 assert_eq!(evaluate(rule, context).unwrap(), json!([]));
536 }
537
538 #[test]
543 fn between_operator() {
544 assert_eq!(
545 evaluate(r#"{"<":[1,{"var":"x"},10]}"#, r#"{"x":5}"#).unwrap(),
546 json!(true)
547 );
548 assert_eq!(
549 evaluate(r#"{"<=":[1,{"var":"x"},10]}"#, r#"{"x":10}"#).unwrap(),
550 json!(true)
551 );
552 }
553
554 #[test]
559 fn complex_rule_with_many_operators() {
560 let rule = r#"{
561 "if":[
562 {"and":[
563 {">":[{"var":"score"},700]},
564 {"==":[{"var":"country"},"MX"]},
565 {"in":[{"var":"tier"},["gold","platinum"]]}
566 ]},
567 {"*":[{"var":"amount"},0.025]},
568 {"*":[{"var":"amount"},0.035]}
569 ]
570 }"#;
571 let context = r#"{"score":750,"country":"MX","tier":"gold","amount":1000}"#;
572 assert_eq!(evaluate_numeric(rule, context).unwrap(), 25.0);
573 }
574}