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}