use crate::engine::error::{DataflowError, Result};
use crate::engine::task_context::TaskContext;
use datalogic_rs::Logic;
use datavalue::OwnedDataValue;
use serde::{Deserialize, Deserializer};
use serde_json::Value;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct Template {
raw: Value,
compiled: Option<Arc<Logic>>,
}
impl<'de> Deserialize<'de> for Template {
fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
Ok(Self {
raw: Value::deserialize(d)?,
compiled: None,
})
}
}
impl Template {
pub fn compile(&mut self, c: &TemplateCompiler, label: &str) -> Result<()> {
let compiled = c
.engine
.compile_arc(&self.raw)
.map_err(|e| DataflowError::LogicEvaluation(format!("{label}: {e}")))?;
self.compiled = Some(compiled);
Ok(())
}
pub fn eval(&self, ctx: &TaskContext<'_>) -> Result<OwnedDataValue> {
let logic = self.compiled.as_deref().ok_or_else(|| {
DataflowError::LogicEvaluation(
"Template::eval called before Template::compile — the engine did not compile \
this field at construction time"
.to_string(),
)
})?;
ctx.eval(logic)
}
pub fn eval_into<T: serde::de::DeserializeOwned>(&self, ctx: &TaskContext<'_>) -> Result<T> {
let logic = self.compiled.as_deref().ok_or_else(|| {
DataflowError::LogicEvaluation(
"Template::eval_into called before Template::compile — the engine did not \
compile this field at construction time"
.to_string(),
)
})?;
let json = ctx.eval_json(logic)?;
serde_json::from_value(json).map_err(DataflowError::from_serde)
}
pub fn eval_to_plain_string(&self, ctx: &TaskContext<'_>) -> Result<String> {
let logic = self.compiled.as_deref().ok_or_else(|| {
DataflowError::LogicEvaluation(
"Template::eval_to_plain_string called before Template::compile — the engine \
did not compile this field at construction time"
.to_string(),
)
})?;
ctx.eval_to_plain_string(logic)
}
pub fn as_json(&self) -> &Value {
&self.raw
}
pub fn is_compiled(&self) -> bool {
self.compiled.is_some()
}
}
pub struct TemplateCompiler {
engine: Arc<datalogic_rs::Engine>,
}
impl TemplateCompiler {
pub(crate) fn new(engine: Arc<datalogic_rs::Engine>) -> Self {
Self { engine }
}
pub fn engine(&self) -> &datalogic_rs::Engine {
&self.engine
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::message::Message;
use serde_json::json;
fn engine() -> Arc<datalogic_rs::Engine> {
Arc::new(
datalogic_rs::Engine::builder()
.with_templating(true)
.build(),
)
}
fn template_from(v: Value) -> Template {
serde_json::from_value(v).unwrap()
}
#[test]
fn deserializes_from_every_json_shape_and_as_json_is_verbatim() {
for v in [
json!({"a": 1}),
json!([1, 2, 3]),
json!("hello"),
json!(42),
json!(true),
json!(null),
json!({}),
] {
let t = template_from(v.clone());
assert_eq!(t.as_json(), &v);
assert!(!t.is_compiled());
}
}
#[test]
fn eval_before_compile_errors_without_panicking() {
let dl = engine();
let mut m = Message::from_value(&json!({}));
let ctx = TaskContext::new(&mut m, &dl);
let t = template_from(json!({"var": "data.x"}));
match t.eval(&ctx) {
Err(DataflowError::LogicEvaluation(msg)) => {
assert!(
msg.contains("compile"),
"message should name the cause: {msg}"
);
}
other => panic!("expected LogicEvaluation, got {other:?}"),
}
}
#[test]
fn compile_on_a_malformed_expression_names_the_label() {
let c = TemplateCompiler::new(engine());
let mut too_deep = json!(1);
for _ in 0..300 {
too_deep = json!({"var": too_deep});
}
let mut t = template_from(too_deep);
match t.compile(&c, "my_field for task t in workflow w") {
Err(DataflowError::LogicEvaluation(msg)) => {
assert!(
msg.contains("my_field for task t in workflow w"),
"got: {msg}"
);
}
other => panic!("expected LogicEvaluation, got {other:?}"),
}
}
#[test]
fn a_literal_template_evaluates_to_that_literal() {
let dl = engine();
let c = TemplateCompiler::new(Arc::clone(&dl));
let mut m = Message::from_value(&json!({}));
let ctx = TaskContext::new(&mut m, &dl);
for v in [
json!("hello"),
json!(42),
json!({}),
json!({"a": 1, "b": 2}),
] {
let mut t = template_from(v.clone());
t.compile(&c, "lbl").unwrap();
assert_eq!(t.eval_into::<Value>(&ctx).unwrap(), v);
}
}
#[test]
fn a_single_key_operator_name_evaluates_as_the_operator() {
let dl = engine();
let c = TemplateCompiler::new(Arc::clone(&dl));
let mut m = Message::from_value(&json!({}));
let ctx = TaskContext::new(&mut m, &dl);
let mut t = template_from(json!({"cat": ["a", "b"]}));
t.compile(&c, "lbl").unwrap();
assert_eq!(t.eval_into::<Value>(&ctx).unwrap(), json!("ab"));
}
#[test]
fn eval_to_plain_string_unquotes_and_coerces_non_strings() {
let dl = engine();
let c = TemplateCompiler::new(Arc::clone(&dl));
let mut m = Message::from_value(&json!({}));
let ctx = TaskContext::new(&mut m, &dl);
let mut string_t = template_from(json!("abc"));
string_t.compile(&c, "lbl").unwrap();
assert_eq!(string_t.eval_to_plain_string(&ctx).unwrap(), "abc");
let mut num_t = template_from(json!(7));
num_t.compile(&c, "lbl").unwrap();
assert_eq!(num_t.eval_to_plain_string(&ctx).unwrap(), "7");
let mut obj_t = template_from(json!({"a": 1}));
obj_t.compile(&c, "lbl").unwrap();
assert_eq!(obj_t.eval_to_plain_string(&ctx).unwrap(), "{\"a\":1}");
}
#[test]
fn eval_to_plain_string_before_compile_errors_without_panicking() {
let mut m = Message::from_value(&json!({}));
let dl = engine();
let ctx = TaskContext::new(&mut m, &dl);
let t = template_from(json!("abc"));
match t.eval_to_plain_string(&ctx) {
Err(DataflowError::LogicEvaluation(msg)) => {
assert!(
msg.contains("compile"),
"message should name the cause: {msg}"
);
}
other => panic!("expected LogicEvaluation, got {other:?}"),
}
}
#[test]
fn non_ascii_result_round_trips() {
let dl = engine();
let c = TemplateCompiler::new(Arc::clone(&dl));
let mut m = Message::from_value(&json!({}));
let ctx = TaskContext::new(&mut m, &dl);
let mut t = template_from(json!({"cat": ["über-", "größe"]}));
t.compile(&c, "lbl").unwrap();
assert_eq!(t.eval_into::<String>(&ctx).unwrap(), "über-größe");
}
#[test]
fn reading_an_absent_path_matches_the_engines_missing_path_result() {
let dl = engine();
let c = TemplateCompiler::new(Arc::clone(&dl));
let mut m = Message::from_value(&json!({}));
let ctx = TaskContext::new(&mut m, &dl);
let mut t = template_from(json!({"var": "data.nope"}));
t.compile(&c, "lbl").unwrap();
assert_eq!(t.eval(&ctx).unwrap(), OwnedDataValue::Null);
}
}