dataflow_rs/engine/functions/
template.rs1use crate::engine::error::{DataflowError, Result};
10use crate::engine::task_context::TaskContext;
11use datalogic_rs::Logic;
12use datavalue::OwnedDataValue;
13use serde::{Deserialize, Deserializer};
14use serde_json::Value;
15use std::sync::Arc;
16
17#[derive(Debug, Clone)]
32pub struct Template {
33 raw: Value,
34 compiled: Option<Arc<Logic>>,
35}
36
37impl<'de> Deserialize<'de> for Template {
43 fn deserialize<D: Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
44 Ok(Self {
45 raw: Value::deserialize(d)?,
46 compiled: None,
47 })
48 }
49}
50
51impl Template {
52 pub fn compile(&mut self, c: &TemplateCompiler, label: &str) -> Result<()> {
62 let compiled = c
63 .engine
64 .compile_arc(&self.raw)
65 .map_err(|e| DataflowError::LogicEvaluation(format!("{label}: {e}")))?;
66 self.compiled = Some(compiled);
67 Ok(())
68 }
69
70 pub fn eval(&self, ctx: &TaskContext<'_>) -> Result<OwnedDataValue> {
79 let logic = self.compiled.as_deref().ok_or_else(|| {
80 DataflowError::LogicEvaluation(
81 "Template::eval called before Template::compile — the engine did not compile \
82 this field at construction time"
83 .to_string(),
84 )
85 })?;
86 ctx.eval(logic)
87 }
88
89 pub fn eval_into<T: serde::de::DeserializeOwned>(&self, ctx: &TaskContext<'_>) -> Result<T> {
101 let logic = self.compiled.as_deref().ok_or_else(|| {
102 DataflowError::LogicEvaluation(
103 "Template::eval_into called before Template::compile — the engine did not \
104 compile this field at construction time"
105 .to_string(),
106 )
107 })?;
108 let json = ctx.eval_json(logic)?;
109 serde_json::from_value(json).map_err(DataflowError::from_serde)
110 }
111
112 pub fn eval_to_plain_string(&self, ctx: &TaskContext<'_>) -> Result<String> {
122 let logic = self.compiled.as_deref().ok_or_else(|| {
123 DataflowError::LogicEvaluation(
124 "Template::eval_to_plain_string called before Template::compile — the engine \
125 did not compile this field at construction time"
126 .to_string(),
127 )
128 })?;
129 ctx.eval_to_plain_string(logic)
130 }
131
132 pub fn as_json(&self) -> &Value {
135 &self.raw
136 }
137
138 pub fn is_compiled(&self) -> bool {
141 self.compiled.is_some()
142 }
143}
144
145pub struct TemplateCompiler {
153 engine: Arc<datalogic_rs::Engine>,
154}
155
156impl TemplateCompiler {
157 pub(crate) fn new(engine: Arc<datalogic_rs::Engine>) -> Self {
158 Self { engine }
159 }
160
161 pub fn engine(&self) -> &datalogic_rs::Engine {
164 &self.engine
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::engine::message::Message;
172 use serde_json::json;
173
174 fn engine() -> Arc<datalogic_rs::Engine> {
175 Arc::new(
176 datalogic_rs::Engine::builder()
177 .with_templating(true)
178 .build(),
179 )
180 }
181
182 fn template_from(v: Value) -> Template {
183 serde_json::from_value(v).unwrap()
184 }
185
186 #[test]
187 fn deserializes_from_every_json_shape_and_as_json_is_verbatim() {
188 for v in [
189 json!({"a": 1}),
190 json!([1, 2, 3]),
191 json!("hello"),
192 json!(42),
193 json!(true),
194 json!(null),
195 json!({}),
196 ] {
197 let t = template_from(v.clone());
198 assert_eq!(t.as_json(), &v);
199 assert!(!t.is_compiled());
200 }
201 }
202
203 #[test]
204 fn eval_before_compile_errors_without_panicking() {
205 let dl = engine();
206 let mut m = Message::from_value(&json!({}));
207 let ctx = TaskContext::new(&mut m, &dl);
208 let t = template_from(json!({"var": "data.x"}));
209
210 match t.eval(&ctx) {
211 Err(DataflowError::LogicEvaluation(msg)) => {
212 assert!(
213 msg.contains("compile"),
214 "message should name the cause: {msg}"
215 );
216 }
217 other => panic!("expected LogicEvaluation, got {other:?}"),
218 }
219 }
220
221 #[test]
222 fn compile_on_a_malformed_expression_names_the_label() {
223 let c = TemplateCompiler::new(engine());
233 let mut too_deep = json!(1);
234 for _ in 0..300 {
235 too_deep = json!({"var": too_deep});
236 }
237 let mut t = template_from(too_deep);
238
239 match t.compile(&c, "my_field for task t in workflow w") {
240 Err(DataflowError::LogicEvaluation(msg)) => {
241 assert!(
242 msg.contains("my_field for task t in workflow w"),
243 "got: {msg}"
244 );
245 }
246 other => panic!("expected LogicEvaluation, got {other:?}"),
247 }
248 }
249
250 #[test]
251 fn a_literal_template_evaluates_to_that_literal() {
252 let dl = engine();
253 let c = TemplateCompiler::new(Arc::clone(&dl));
254 let mut m = Message::from_value(&json!({}));
255 let ctx = TaskContext::new(&mut m, &dl);
256
257 for v in [
258 json!("hello"),
259 json!(42),
260 json!({}),
261 json!({"a": 1, "b": 2}),
262 ] {
263 let mut t = template_from(v.clone());
264 t.compile(&c, "lbl").unwrap();
265 assert_eq!(t.eval_into::<Value>(&ctx).unwrap(), v);
266 }
267 }
268
269 #[test]
270 fn a_single_key_operator_name_evaluates_as_the_operator() {
271 let dl = engine();
275 let c = TemplateCompiler::new(Arc::clone(&dl));
276 let mut m = Message::from_value(&json!({}));
277 let ctx = TaskContext::new(&mut m, &dl);
278
279 let mut t = template_from(json!({"cat": ["a", "b"]}));
280 t.compile(&c, "lbl").unwrap();
281 assert_eq!(t.eval_into::<Value>(&ctx).unwrap(), json!("ab"));
282 }
283
284 #[test]
285 fn eval_to_plain_string_unquotes_and_coerces_non_strings() {
286 let dl = engine();
287 let c = TemplateCompiler::new(Arc::clone(&dl));
288 let mut m = Message::from_value(&json!({}));
289 let ctx = TaskContext::new(&mut m, &dl);
290
291 let mut string_t = template_from(json!("abc"));
292 string_t.compile(&c, "lbl").unwrap();
293 assert_eq!(string_t.eval_to_plain_string(&ctx).unwrap(), "abc");
294
295 let mut num_t = template_from(json!(7));
296 num_t.compile(&c, "lbl").unwrap();
297 assert_eq!(num_t.eval_to_plain_string(&ctx).unwrap(), "7");
298
299 let mut obj_t = template_from(json!({"a": 1}));
300 obj_t.compile(&c, "lbl").unwrap();
301 assert_eq!(obj_t.eval_to_plain_string(&ctx).unwrap(), "{\"a\":1}");
302 }
303
304 #[test]
305 fn eval_to_plain_string_before_compile_errors_without_panicking() {
306 let mut m = Message::from_value(&json!({}));
307 let dl = engine();
308 let ctx = TaskContext::new(&mut m, &dl);
309 let t = template_from(json!("abc"));
310
311 match t.eval_to_plain_string(&ctx) {
312 Err(DataflowError::LogicEvaluation(msg)) => {
313 assert!(
314 msg.contains("compile"),
315 "message should name the cause: {msg}"
316 );
317 }
318 other => panic!("expected LogicEvaluation, got {other:?}"),
319 }
320 }
321
322 #[test]
323 fn non_ascii_result_round_trips() {
324 let dl = engine();
325 let c = TemplateCompiler::new(Arc::clone(&dl));
326 let mut m = Message::from_value(&json!({}));
327 let ctx = TaskContext::new(&mut m, &dl);
328
329 let mut t = template_from(json!({"cat": ["über-", "größe"]}));
330 t.compile(&c, "lbl").unwrap();
331 assert_eq!(t.eval_into::<String>(&ctx).unwrap(), "über-größe");
332 }
333
334 #[test]
335 fn reading_an_absent_path_matches_the_engines_missing_path_result() {
336 let dl = engine();
337 let c = TemplateCompiler::new(Arc::clone(&dl));
338 let mut m = Message::from_value(&json!({}));
339 let ctx = TaskContext::new(&mut m, &dl);
340
341 let mut t = template_from(json!({"var": "data.nope"}));
342 t.compile(&c, "lbl").unwrap();
343 assert_eq!(t.eval(&ctx).unwrap(), OwnedDataValue::Null);
346 }
347}