1pub mod error;
2pub mod explain;
3pub mod metadata;
4pub mod store;
5
6pub use jsonlogic_fast::error::{RuleEngineError, RuleEngineResult};
10pub use jsonlogic_fast::extract::{
11 extract_array, extract_bool, extract_f64, extract_object, extract_string,
12};
13pub use jsonlogic_fast::{
14 evaluate, evaluate_batch, evaluate_batch_detailed, evaluate_batch_numeric,
15 evaluate_batch_numeric_detailed, evaluate_numeric, evaluate_rule, serialize, serialize_value,
16 validate_rule, EvaluationResult, NumericEvaluationResult,
17};
18
19pub use error::ValidationError;
20pub use explain::ExplainResult;
21pub use store::RuleStore;
22
23use metadata::RuleDefinition;
24use serde_json::Value;
25
26pub fn execute(rule_def: &RuleDefinition, context_json: &str) -> RuleEngineResult<Value> {
31 let rule_json = serde_json::to_string(&rule_def.logic)
32 .map_err(|e| RuleEngineError::InvalidRule(e.to_string()))?;
33 evaluate(&rule_json, context_json)
34}
35
36pub fn execute_numeric(rule_def: &RuleDefinition, context_json: &str) -> RuleEngineResult<f64> {
38 let rule_json = serde_json::to_string(&rule_def.logic)
39 .map_err(|e| RuleEngineError::InvalidRule(e.to_string()))?;
40 evaluate_numeric(&rule_json, context_json)
41}
42
43pub fn execute_batch(
45 rule_def: &RuleDefinition,
46 contexts_json: &[String],
47) -> RuleEngineResult<Vec<Value>> {
48 let rule_json = serde_json::to_string(&rule_def.logic)
49 .map_err(|e| RuleEngineError::InvalidRule(e.to_string()))?;
50 evaluate_batch(&rule_json, contexts_json)
51}
52
53pub fn execute_batch_detailed(
55 rule_def: &RuleDefinition,
56 contexts_json: &[String],
57) -> RuleEngineResult<Vec<EvaluationResult>> {
58 let rule_json = serde_json::to_string(&rule_def.logic)
59 .map_err(|e| RuleEngineError::InvalidRule(e.to_string()))?;
60 evaluate_batch_detailed(&rule_json, contexts_json)
61}
62
63pub fn execute_chain(
75 rules: &[RuleDefinition],
76 initial_context_json: &str,
77) -> RuleEngineResult<(Value, Value)> {
78 if rules.is_empty() {
79 return Err(RuleEngineError::InvalidRule(
80 "rule chain must contain at least one rule".to_owned(),
81 ));
82 }
83 let mut context: Value = serde_json::from_str(initial_context_json)
84 .map_err(|e| RuleEngineError::InvalidRule(format!("invalid context JSON: {e}")))?;
85
86 let mut last_result = Value::Null;
87 for rule in rules {
88 let ctx_str = serde_json::to_string(&context)
89 .map_err(|e| RuleEngineError::InvalidRule(e.to_string()))?;
90 last_result = execute(rule, &ctx_str)?;
91 if let Some(obj) = context.as_object_mut() {
93 obj.insert("decision".to_owned(), last_result.clone());
94 }
95 }
96 Ok((last_result, context))
97}
98
99pub fn execute_explain(
105 rule_def: &RuleDefinition,
106 context_json: &str,
107) -> RuleEngineResult<ExplainResult> {
108 let context_snapshot: Value = serde_json::from_str(context_json)
109 .map_err(|e| RuleEngineError::InvalidRule(format!("invalid context JSON: {e}")))?;
110 let result = execute(rule_def, context_json)?;
111 Ok(ExplainResult::new(
112 rule_def.name.clone(),
113 rule_def.version.clone(),
114 rule_def.tags.clone(),
115 rule_def.logic.clone(),
116 context_snapshot,
117 result,
118 ))
119}
120
121pub fn get_engine_info() -> Value {
123 let core_info = jsonlogic_fast::get_core_info();
124 serde_json::json!({
125 "engine": "tempus-engine",
126 "version": env!("CARGO_PKG_VERSION"),
127 "evaluator": core_info,
128 })
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use serde_json::json;
135
136 fn sample_rule() -> RuleDefinition {
137 RuleDefinition::new(
138 "score-check",
139 json!({"if": [{">": [{"var": "score"}, 700]}, "approve", "review"]}),
140 )
141 }
142
143 #[test]
144 fn execute_returns_correct_result() {
145 let rule = sample_rule();
146 let result = execute(&rule, r#"{"score": 800}"#).unwrap();
147 assert_eq!(result, json!("approve"));
148 }
149
150 #[test]
151 fn execute_returns_review_for_low_score() {
152 let rule = sample_rule();
153 let result = execute(&rule, r#"{"score": 500}"#).unwrap();
154 assert_eq!(result, json!("review"));
155 }
156
157 #[test]
158 fn execute_numeric_works() {
159 let rule = RuleDefinition::new("fee-calc", json!({"*": [{"var": "amount"}, 0.029]}));
160 let result = execute_numeric(&rule, r#"{"amount": 1000}"#).unwrap();
161 assert!((result - 29.0).abs() < 1e-6);
162 }
163
164 #[test]
165 fn execute_batch_processes_multiple_contexts() {
166 let rule = sample_rule();
167 let contexts = vec![
168 r#"{"score": 800}"#.to_string(),
169 r#"{"score": 500}"#.to_string(),
170 r#"{"score": 742}"#.to_string(),
171 ];
172 let results = execute_batch(&rule, &contexts).unwrap();
173 assert_eq!(
174 results,
175 vec![json!("approve"), json!("review"), json!("approve")]
176 );
177 }
178
179 #[test]
180 fn execute_batch_detailed_reports_errors() {
181 let rule = sample_rule();
182 let contexts = vec![r#"{"score": 800}"#.to_string(), "{bad json}".to_string()];
183 let results = execute_batch_detailed(&rule, &contexts).unwrap();
184 assert_eq!(results[0].result, Some(json!("approve")));
185 assert!(results[1].error.is_some());
186 }
187
188 #[test]
189 fn get_engine_info_returns_version() {
190 let info = get_engine_info();
191 assert_eq!(info["engine"], "tempus-engine");
192 assert!(info["version"].as_str().is_some());
193 assert!(info["evaluator"]["engine"].as_str().is_some());
194 }
195
196 #[test]
197 fn rule_metadata_is_preserved() {
198 let mut rule = sample_rule();
199 rule.version = Some("1.2.0".to_string());
200 rule.tags = vec!["credit".to_string(), "prod".to_string()];
201
202 assert_eq!(rule.name, "score-check");
203 assert_eq!(rule.version, Some("1.2.0".to_string()));
204 assert_eq!(rule.tags.len(), 2);
205
206 let result = execute(&rule, r#"{"score": 800}"#).unwrap();
208 assert_eq!(result, json!("approve"));
209 }
210
211 #[test]
212 fn re_exported_evaluate_works_directly() {
213 let result = evaluate(r#"{">":[{"var":"x"},1]}"#, r#"{"x":5}"#).unwrap();
214 assert_eq!(result, json!(true));
215 }
216
217 #[test]
218 fn re_exported_validate_rule_works() {
219 assert!(validate_rule(r#"{">":[{"var":"x"},1]}"#).unwrap());
220 }
221
222 #[test]
225 fn execute_chain_single_rule() {
226 let rules = vec![sample_rule()];
227 let (result, _ctx) = execute_chain(&rules, r#"{"score": 800}"#).unwrap();
228 assert_eq!(result, json!("approve"));
229 }
230
231 #[test]
232 fn execute_chain_pipes_decision_to_next_rule() {
233 let rule1 = RuleDefinition::new(
235 "score-gate",
236 json!({"if": [{">": [{"var": "score"}, 700]}, "approve", "review"]}),
237 );
238 let rule2 = RuleDefinition::new(
240 "decision-logger",
241 json!({"var": "decision"}),
242 );
243 let (result, ctx) = execute_chain(&[rule1, rule2], r#"{"score": 800}"#).unwrap();
244 assert_eq!(result, json!("approve"));
245 assert_eq!(ctx["decision"], json!("approve"));
246 }
247
248 #[test]
249 fn execute_chain_empty_returns_error() {
250 assert!(execute_chain(&[], r#"{}"#).is_err());
251 }
252
253 #[test]
256 fn execute_explain_captures_metadata() {
257 let rule = sample_rule()
258 .with_version("1.0.0")
259 .with_tags(vec!["credit".into()]);
260 let trace = execute_explain(&rule, r#"{"score": 800}"#).unwrap();
261 assert_eq!(trace.rule_name, "score-check");
262 assert_eq!(trace.rule_version.as_deref(), Some("1.0.0"));
263 assert_eq!(trace.result, json!("approve"));
264 assert_eq!(trace.context_snapshot["score"], json!(800));
265 assert!(!trace.evaluated_at.is_empty());
266 }
267}
268