Skip to main content

jsonlogic_fast/
lib.rs

1pub mod error;
2pub mod extract;
3
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6
7use error::{RuleEngineError, RuleEngineResult};
8use extract::extract_f64;
9
10#[cfg(not(target_arch = "wasm32"))]
11use rayon::prelude::*;
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14pub struct EvaluationResult {
15    pub result: Option<Value>,
16    pub error: Option<String>,
17}
18
19impl EvaluationResult {
20    fn ok(result: Value) -> Self {
21        Self {
22            result: Some(result),
23            error: None,
24        }
25    }
26
27    fn err(error: impl Into<String>) -> Self {
28        Self {
29            result: None,
30            error: Some(error.into()),
31        }
32    }
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
36pub struct NumericEvaluationResult {
37    pub result: f64,
38    pub error: Option<String>,
39}
40
41fn parse_rule(rule_json: &str) -> RuleEngineResult<Value> {
42    serde_json::from_str(rule_json).map_err(|e| RuleEngineError::InvalidRule(e.to_string()))
43}
44
45fn parse_context(context_json: &str) -> RuleEngineResult<Value> {
46    serde_json::from_str(context_json).map_err(|e| RuleEngineError::InvalidContext(e.to_string()))
47}
48
49fn apply_rule(rule: &Value, context: &Value) -> RuleEngineResult<Value> {
50    jsonlogic::apply(rule, context).map_err(|e| RuleEngineError::Evaluation(e.to_string()))
51}
52
53fn evaluate_context(rule: &Value, context_json: &str) -> EvaluationResult {
54    let context = match parse_context(context_json) {
55        Ok(context) => context,
56        Err(error) => return EvaluationResult::err(error.to_string()),
57    };
58
59    match apply_rule(rule, &context) {
60        Ok(result) => EvaluationResult::ok(result),
61        Err(error) => EvaluationResult::err(error.to_string()),
62    }
63}
64
65fn default_validation_context() -> Value {
66    json!({
67        "amount": 100.0,
68        "country": "MX",
69        "method": "CREDIT_CARD",
70        "active": true,
71        "count": 3,
72        "score": 720,
73        "tags": ["vip", "beta"],
74        "user": {
75            "tier": "gold",
76            "region": "north"
77        },
78        "metrics": {
79            "total_volume": 1000.0,
80            "chargebacks": 1
81        },
82        "null_value": null
83    })
84}
85
86#[cfg(not(target_arch = "wasm32"))]
87fn map_contexts<T, F>(contexts_json: &[String], evaluator: F) -> Vec<T>
88where
89    T: Send,
90    F: Fn(&str) -> T + Sync + Send,
91{
92    contexts_json
93        .par_iter()
94        .map(|context_json| evaluator(context_json))
95        .collect()
96}
97
98#[cfg(target_arch = "wasm32")]
99fn map_contexts<T, F>(contexts_json: &[String], evaluator: F) -> Vec<T>
100where
101    F: Fn(&str) -> T,
102{
103    contexts_json
104        .iter()
105        .map(|context_json| evaluator(context_json))
106        .collect()
107}
108
109#[cfg(not(target_arch = "wasm32"))]
110fn available_threads() -> usize {
111    rayon::current_num_threads()
112}
113
114#[cfg(target_arch = "wasm32")]
115fn available_threads() -> usize {
116    1
117}
118
119pub fn evaluate(rule_json: &str, context_json: &str) -> RuleEngineResult<Value> {
120    let rule = parse_rule(rule_json)?;
121    let context = parse_context(context_json)?;
122    apply_rule(&rule, &context)
123}
124
125pub fn evaluate_rule(rule_json: &str, context_json: &str) -> RuleEngineResult<Value> {
126    evaluate(rule_json, context_json)
127}
128
129pub fn evaluate_numeric(rule_json: &str, context_json: &str) -> RuleEngineResult<f64> {
130    extract_f64(evaluate(rule_json, context_json)?)
131}
132
133pub fn evaluate_batch(rule_json: &str, contexts_json: &[String]) -> RuleEngineResult<Vec<Value>> {
134    let rule = parse_rule(rule_json)?;
135    Ok(map_contexts(contexts_json, |context_json| {
136        evaluate_context(&rule, context_json)
137            .result
138            .unwrap_or(Value::Null)
139    }))
140}
141
142pub fn evaluate_batch_detailed(
143    rule_json: &str,
144    contexts_json: &[String],
145) -> RuleEngineResult<Vec<EvaluationResult>> {
146    let rule = parse_rule(rule_json)?;
147    Ok(map_contexts(contexts_json, |context_json| {
148        evaluate_context(&rule, context_json)
149    }))
150}
151
152pub fn evaluate_batch_numeric(
153    rule_json: &str,
154    contexts_json: &[String],
155) -> RuleEngineResult<Vec<f64>> {
156    let results = evaluate_batch_detailed(rule_json, contexts_json)?;
157    Ok(results
158        .into_iter()
159        .map(|item| match item.result {
160            Some(result) => extract_f64(result).unwrap_or(0.0),
161            None => 0.0,
162        })
163        .collect())
164}
165
166pub fn evaluate_batch_numeric_detailed(
167    rule_json: &str,
168    contexts_json: &[String],
169) -> RuleEngineResult<Vec<NumericEvaluationResult>> {
170    let results = evaluate_batch_detailed(rule_json, contexts_json)?;
171    Ok(results
172        .into_iter()
173        .map(|item| match (item.result, item.error) {
174            (Some(result), None) => match extract_f64(result) {
175                Ok(value) => NumericEvaluationResult {
176                    result: value,
177                    error: None,
178                },
179                Err(error) => NumericEvaluationResult {
180                    result: 0.0,
181                    error: Some(error.to_string()),
182                },
183            },
184            (_, Some(error)) => NumericEvaluationResult {
185                result: 0.0,
186                error: Some(error),
187            },
188            (None, None) => NumericEvaluationResult {
189                result: 0.0,
190                error: Some("Unknown evaluation failure".to_string()),
191            },
192        })
193        .collect())
194}
195
196pub fn validate_rule(rule_json: &str) -> RuleEngineResult<bool> {
197    let rule = parse_rule(rule_json)?;
198    let context = default_validation_context();
199
200    apply_rule(&rule, &context)
201        .map(|_| true)
202        .map_err(|error| RuleEngineError::Evaluation(format!("Rule validation failed: {error}")))
203}
204
205pub fn serialize_value(value: &Value) -> RuleEngineResult<String> {
206    serde_json::to_string(value).map_err(|e| RuleEngineError::Serialization(e.to_string()))
207}
208
209pub fn serialize<T: Serialize>(value: &T) -> RuleEngineResult<String> {
210    serde_json::to_string(value).map_err(|e| RuleEngineError::Serialization(e.to_string()))
211}
212
213pub fn get_core_info() -> Value {
214    json!({
215        "engine": "jsonlogic-fast",
216        "version": env!("CARGO_PKG_VERSION"),
217        "parallelism": if cfg!(target_arch = "wasm32") { "sequential" } else { "rayon" },
218        "available_threads": available_threads(),
219        "evaluator": "jsonlogic-rs",
220        "result_model": "serde_json::Value"
221    })
222}
223
224#[cfg(test)]
225mod tests {
226    use serde_json::json;
227
228    use super::*;
229
230    #[test]
231    fn evaluate_preserves_string_results() {
232        let rule = r#"{"if":[{"==":[{"var":"country"},"MX"]},"domestic","intl"]}"#;
233        let context = r#"{"country":"MX"}"#;
234
235        assert_eq!(evaluate(rule, context).unwrap(), json!("domestic"));
236    }
237
238    #[test]
239    fn evaluate_numeric_coerces_boolean_results() {
240        let rule = r#"{"==":[{"var":"country"},"MX"]}"#;
241        let context = r#"{"country":"MX"}"#;
242
243        assert_eq!(evaluate_numeric(rule, context).unwrap(), 1.0);
244    }
245
246    #[test]
247    fn evaluate_batch_returns_null_for_invalid_contexts() {
248        let rule = r#"{"var":"amount"}"#;
249        let contexts = vec![r#"{"amount":10}"#.to_string(), "{bad json}".to_string()];
250
251        assert_eq!(
252            evaluate_batch(rule, &contexts).unwrap(),
253            vec![json!(10), Value::Null]
254        );
255    }
256
257    #[test]
258    fn evaluate_batch_detailed_reports_errors() {
259        let rule = r#"{"var":"amount"}"#;
260        let contexts = vec!["{}".to_string(), "{bad json}".to_string()];
261        let results = evaluate_batch_detailed(rule, &contexts).unwrap();
262
263        assert_eq!(results[0].result, Some(Value::Null));
264        assert!(results[1]
265            .error
266            .as_ref()
267            .is_some_and(|message| message.contains("Error parsing context")));
268    }
269
270    #[test]
271    fn evaluate_batch_numeric_keeps_fail_safe_zeroes() {
272        let rule = r#"{"var":"amount"}"#;
273        let contexts = vec!["{}".to_string(), "{bad json}".to_string()];
274
275        assert_eq!(
276            evaluate_batch_numeric(rule, &contexts).unwrap(),
277            vec![0.0, 0.0]
278        );
279    }
280
281    #[test]
282    fn validate_rule_accepts_generic_contexts() {
283        let rule = r#"{"cat":[{"var":"user.tier"},"-",{"var":"country"}]}"#;
284        assert!(validate_rule(rule).unwrap());
285    }
286}