use crate::engine::error::{DataflowError, ErrorInfo, Result};
use crate::engine::message::{Change, Message};
use crate::engine::utils::{get_nested_value, set_nested_value};
use datalogic_rs::{Engine as DatalogicEngine, Logic};
use datavalue::OwnedDataValue;
use serde_json::Value as JsonValue;
use std::sync::Arc;
pub struct TaskContext<'a> {
message: &'a mut Message,
datalogic: &'a Arc<DatalogicEngine>,
changes: Vec<Change>,
}
impl<'a> TaskContext<'a> {
pub fn new(message: &'a mut Message, datalogic: &'a Arc<DatalogicEngine>) -> Self {
Self {
message,
datalogic,
changes: Vec::new(),
}
}
#[inline]
pub fn message(&self) -> &Message {
self.message
}
#[inline]
pub fn message_mut(&mut self) -> &mut Message {
self.message
}
#[inline]
pub fn datalogic(&self) -> &Arc<DatalogicEngine> {
self.datalogic
}
#[inline]
pub fn data(&self) -> &OwnedDataValue {
self.message.data()
}
#[inline]
pub fn metadata(&self) -> &OwnedDataValue {
self.message.metadata()
}
#[inline]
pub fn temp_data(&self) -> &OwnedDataValue {
self.message.temp_data()
}
#[inline]
pub fn context(&self) -> &OwnedDataValue {
&self.message.context
}
pub fn eval(&self, logic: &Logic) -> Result<OwnedDataValue> {
crate::engine::executor::eval_to_owned(self.datalogic, logic, &self.message.context)
.map_err(|e| DataflowError::LogicEvaluation(e.to_string()))
}
pub fn eval_json(&self, logic: &Logic) -> Result<JsonValue> {
crate::engine::executor::eval_to_json(self.datalogic, logic, &self.message.context)
.map_err(|e| DataflowError::LogicEvaluation(e.to_string()))
}
pub fn eval_to_plain_string(&self, logic: &Logic) -> Result<String> {
crate::engine::executor::eval_to_plain_string(self.datalogic, logic, &self.message.context)
.map_err(|e| DataflowError::LogicEvaluation(e.to_string()))
}
#[inline]
pub fn get(&self, path: &str) -> Option<&OwnedDataValue> {
get_nested_value(&self.message.context, path)
}
pub fn set(&mut self, path: &str, value: OwnedDataValue) {
if self.message.capture_changes {
let old_value = get_nested_value(&self.message.context, path)
.cloned()
.unwrap_or(OwnedDataValue::Null);
let new_value = value.clone();
self.changes.push(Change {
path: Arc::from(path),
old_value,
new_value,
});
}
set_nested_value(&mut self.message.context, path, value);
}
#[inline]
pub fn set_json(&mut self, path: &str, value: &JsonValue) {
self.set(path, OwnedDataValue::from(value));
}
#[inline]
pub fn add_error(&mut self, error: ErrorInfo) {
self.message.add_error(error);
}
#[inline]
pub fn into_changes(self) -> Vec<Change> {
self.changes
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::executor::with_arena;
use crate::engine::utils::set_nested_value;
use serde_json::json;
fn dv(v: serde_json::Value) -> OwnedDataValue {
OwnedDataValue::from(&v)
}
fn engine() -> Arc<DatalogicEngine> {
Arc::new(DatalogicEngine::builder().with_templating(true).build())
}
fn populated() -> Message {
let mut m = Message::from_value(&json!({"payload_key": "payload_value"}));
set_nested_value(&mut m.context, "data.x", dv(json!("dx")));
set_nested_value(&mut m.context, "metadata.x", dv(json!("mx")));
set_nested_value(&mut m.context, "temp_data.x", dv(json!("tx")));
m
}
#[test]
fn context_matches_the_three_slot_accessors() {
let mut m = populated();
let dl = engine();
let ctx = TaskContext::new(&mut m, &dl);
let whole = ctx.context();
assert_eq!(&whole["data"], ctx.data());
assert_eq!(&whole["metadata"], ctx.metadata());
assert_eq!(&whole["temp_data"], ctx.temp_data());
assert_eq!(whole, &ctx.message().context);
}
#[test]
fn eval_roots_at_the_unified_context_not_data_alone() {
let mut m = populated();
let dl = engine();
let ctx = TaskContext::new(&mut m, &dl);
for (path, expected) in [
("data.x", "dx"),
("metadata.x", "mx"),
("temp_data.x", "tx"),
] {
let logic = dl.compile_arc(&json!({"var": path})).unwrap();
assert_eq!(ctx.eval(&logic).unwrap(), dv(json!(expected)));
assert_eq!(ctx.eval_json(&logic).unwrap(), json!(expected));
assert_eq!(ctx.eval_to_plain_string(&logic).unwrap(), expected);
}
}
#[test]
fn payload_is_not_in_the_eval_context() {
let mut m = populated();
let dl = engine();
let ctx = TaskContext::new(&mut m, &dl);
let logic = dl
.compile_arc(&json!({"var": "payload.payload_key"}))
.unwrap();
assert_eq!(ctx.eval(&logic).unwrap(), OwnedDataValue::Null);
assert_eq!(ctx.eval_json(&logic).unwrap(), serde_json::Value::Null);
assert_eq!(ctx.eval_to_plain_string(&logic).unwrap(), "null");
}
#[test]
fn eval_json_covers_every_result_kind() {
let mut m = Message::from_value(&json!({}));
let dl = engine();
let ctx = TaskContext::new(&mut m, &dl);
for expected in [
json!(null),
json!(true),
json!(42),
json!(1.5),
json!("abc"),
json!([1, 2]),
json!({"a": 1}),
json!({"a": [1, {"b": "c"}], "d": {"e": [true, null]}}),
] {
let logic = dl.compile_arc(&expected).unwrap();
assert_eq!(
ctx.eval_json(&logic).unwrap(),
expected,
"round-trip for {expected}"
);
assert_eq!(
serde_json::Value::from(&ctx.eval(&logic).unwrap()),
expected
);
}
}
#[test]
fn eval_to_plain_string_unquotes_strings_and_compacts_the_rest() {
let mut m = Message::from_value(&json!({}));
let dl = engine();
let ctx = TaskContext::new(&mut m, &dl);
let cases = [
(json!("abc"), "abc"),
(json!(""), ""),
(json!(null), "null"),
(json!(true), "true"),
(json!(42), "42"),
(json!({"a": 1}), "{\"a\":1}"),
(json!([1, 2]), "[1,2]"),
];
for (input, expected) in cases {
let logic = dl.compile_arc(&input).unwrap();
assert_eq!(
ctx.eval_to_plain_string(&logic).unwrap(),
expected,
"for {input}"
);
}
}
#[test]
fn eval_to_plain_string_diverges_from_datalogics_own_string_projection() {
let mut m = Message::from_value(&json!({}));
let dl = engine();
let ctx = TaskContext::new(&mut m, &dl);
let raw = "héllo \"world\" 世界";
let logic = dl.compile_arc(&json!(raw)).unwrap();
assert_eq!(ctx.eval_to_plain_string(&logic).unwrap(), raw);
let via_session = dl.session().eval_str(&logic, &m.context).unwrap();
assert_ne!(
via_session, raw,
"if these agree, the divergence this method exists for is gone"
);
assert!(
via_session.starts_with('"') && via_session.contains("\\\""),
"datalogic keeps the quoting and escaping, got: {via_session}"
);
}
#[test]
fn eval_surfaces_an_error_where_evaluate_condition_returns_false() {
let mut m = Message::from_value(&json!({}));
let dl = engine();
let bad = dl.compile_arc(&json!({"+": ["abc", 1]})).unwrap();
let condition_result =
crate::engine::executor::evaluate_condition(&dl, Some(&bad), &m.context);
let ctx = TaskContext::new(&mut m, &dl);
let eval_result = ctx.eval(&bad);
match (&condition_result, &eval_result) {
(Ok(false), Err(DataflowError::LogicEvaluation(msg))) => {
assert!(!msg.is_empty(), "the error message must not be empty");
}
other => panic!(
"expected evaluate_condition Ok(false) alongside eval Err(LogicEvaluation), got {other:?}"
),
}
}
#[test]
fn consecutive_evals_and_interleaved_sets_both_work() {
let mut m = populated();
let dl = engine();
let first = dl.compile_arc(&json!({"var": "data.x"})).unwrap();
let second = dl.compile_arc(&json!({"var": "metadata.x"})).unwrap();
let mut ctx = TaskContext::new(&mut m, &dl);
assert_eq!(ctx.eval_json(&first).unwrap(), json!("dx"));
assert_eq!(ctx.eval_json(&second).unwrap(), json!("mx"));
assert_eq!(ctx.eval_json(&first).unwrap(), json!("dx"));
ctx.set("data.written", dv(json!(1)));
assert_eq!(ctx.eval_json(&first).unwrap(), json!("dx"));
ctx.set("data.written2", dv(json!(2)));
let changes = ctx.into_changes();
let paths: Vec<&str> = changes.iter().map(|c| &*c.path).collect();
assert_eq!(paths, vec!["data.written", "data.written2"]);
}
#[test]
fn eval_inside_a_with_arena_scope_falls_back_instead_of_panicking() {
let mut m = populated();
let dl = engine();
let logic = dl.compile_arc(&json!({"var": "data.x"})).unwrap();
let got = with_arena(|_| {
let ctx = TaskContext::new(&mut m, &dl);
ctx.eval_json(&logic)
});
assert_eq!(got.unwrap(), json!("dx"));
}
}