1use serde_json::{Map, Value};
11use std::collections::HashMap;
12
13use crate::FormeError;
14
15pub struct EvalContext {
17 data: Value,
18 scope: HashMap<String, Value>,
19}
20
21impl EvalContext {
22 pub fn new(data: Value) -> Self {
23 EvalContext {
24 data,
25 scope: HashMap::new(),
26 }
27 }
28
29 fn resolve_ref(&self, path: &str) -> Option<&Value> {
31 let parts: Vec<&str> = path.split('.').collect();
32 if parts.is_empty() {
33 return None;
34 }
35
36 if let Some(scoped) = self.scope.get(parts[0]) {
38 return traverse(scoped, &parts[1..]);
39 }
40
41 traverse(&self.data, &parts)
43 }
44
45 fn with_binding(&self, key: &str, value: Value) -> EvalContext {
47 let mut scope = self.scope.clone();
48 scope.insert(key.to_string(), value);
49 EvalContext {
50 data: self.data.clone(),
51 scope,
52 }
53 }
54}
55
56fn traverse<'a>(value: &'a Value, parts: &[&str]) -> Option<&'a Value> {
58 let mut current = value;
59 for part in parts {
60 match current {
61 Value::Object(map) => {
62 current = map.get(*part)?;
63 }
64 Value::Array(arr) => {
65 let idx: usize = part.parse().ok()?;
66 current = arr.get(idx)?;
67 }
68 _ => return None,
69 }
70 }
71 Some(current)
72}
73
74pub fn evaluate_template(template: &Value, data: &Value) -> Result<Value, FormeError> {
76 let ctx = EvalContext::new(data.clone());
77 evaluate_node(template, &ctx).ok_or_else(|| {
78 FormeError::TemplateError("Template evaluation produced no output".to_string())
79 })
80}
81
82fn evaluate_node(node: &Value, ctx: &EvalContext) -> Option<Value> {
84 match node {
85 Value::Object(map) => {
86 if let Some(result) = evaluate_expr_object(map, ctx) {
88 return result;
89 }
90
91 let mut result = Map::new();
93 for (key, val) in map {
94 if let Some(evaluated) = evaluate_node(val, ctx) {
95 result.insert(key.clone(), evaluated);
96 }
97 }
98 Some(Value::Object(result))
99 }
100 Value::Array(arr) => {
101 let mut result = Vec::new();
102 for item in arr {
103 if let Some(evaluated) = evaluate_node(item, ctx) {
104 if is_flatten_marker(&evaluated) {
106 if let Value::Array(inner) =
107 evaluated.get("__flatten").unwrap_or(&Value::Null)
108 {
109 result.extend(inner.clone());
110 }
111 } else {
112 result.push(evaluated);
113 }
114 }
115 }
116 Some(Value::Array(result))
117 }
118 _ => Some(node.clone()),
120 }
121}
122
123fn is_flatten_marker(v: &Value) -> bool {
124 matches!(v, Value::Object(map) if map.contains_key("__flatten"))
125}
126
127fn evaluate_expr_object(map: &Map<String, Value>, ctx: &EvalContext) -> Option<Option<Value>> {
132 if let Some(path) = map.get("$ref") {
134 if let Value::String(path_str) = path {
135 return Some(ctx.resolve_ref(path_str).cloned());
136 }
137 return Some(None);
138 }
139
140 if let Some(source) = map.get("$each") {
142 return Some(evaluate_each(source, map, ctx));
143 }
144
145 if let Some(condition) = map.get("$if") {
147 return Some(evaluate_if(condition, map, ctx));
148 }
149
150 if let Some(args) = map.get("$cond") {
152 return Some(evaluate_cond(args, ctx));
153 }
154
155 if let Some(args) = map.get("$eq") {
157 return Some(evaluate_comparison(args, ctx, CompareOp::Eq));
158 }
159 if let Some(args) = map.get("$ne") {
160 return Some(evaluate_comparison(args, ctx, CompareOp::Ne));
161 }
162 if let Some(args) = map.get("$gt") {
163 return Some(evaluate_comparison(args, ctx, CompareOp::Gt));
164 }
165 if let Some(args) = map.get("$lt") {
166 return Some(evaluate_comparison(args, ctx, CompareOp::Lt));
167 }
168 if let Some(args) = map.get("$gte") {
169 return Some(evaluate_comparison(args, ctx, CompareOp::Gte));
170 }
171 if let Some(args) = map.get("$lte") {
172 return Some(evaluate_comparison(args, ctx, CompareOp::Lte));
173 }
174
175 if let Some(args) = map.get("$add") {
177 return Some(evaluate_arithmetic(args, ctx, |a, b| a + b));
178 }
179 if let Some(args) = map.get("$sub") {
180 return Some(evaluate_arithmetic(args, ctx, |a, b| a - b));
181 }
182 if let Some(args) = map.get("$mul") {
183 return Some(evaluate_arithmetic(args, ctx, |a, b| a * b));
184 }
185 if let Some(args) = map.get("$div") {
186 return Some(evaluate_arithmetic(args, ctx, |a, b| {
187 if b != 0.0 {
188 a / b
189 } else {
190 0.0
191 }
192 }));
193 }
194
195 if let Some(arg) = map.get("$upper") {
197 return Some(evaluate_string_transform(arg, ctx, |s| s.to_uppercase()));
198 }
199 if let Some(arg) = map.get("$lower") {
200 return Some(evaluate_string_transform(arg, ctx, |s| s.to_lowercase()));
201 }
202
203 if let Some(args) = map.get("$concat") {
205 return Some(evaluate_concat(args, ctx));
206 }
207
208 if let Some(args) = map.get("$format") {
210 return Some(evaluate_format(args, ctx));
211 }
212
213 if let Some(arg) = map.get("$count") {
215 return Some(evaluate_count(arg, ctx));
216 }
217
218 None
220}
221
222fn evaluate_each(source: &Value, map: &Map<String, Value>, ctx: &EvalContext) -> Option<Value> {
225 let resolved_source = evaluate_node(source, ctx)?;
226 let arr = match &resolved_source {
227 Value::Array(a) => a,
228 _ => return Some(Value::Array(vec![])),
229 };
230
231 if arr.is_empty() {
232 let mut marker = Map::new();
233 marker.insert("__flatten".to_string(), Value::Array(vec![]));
234 return Some(Value::Object(marker));
235 }
236
237 let binding_name = map.get("as").and_then(|v| v.as_str()).unwrap_or("$item");
238
239 let template = map.get("template")?;
240
241 let mut results = Vec::new();
242 for item in arr {
243 let child_ctx = ctx.with_binding(binding_name, item.clone());
244 if let Some(evaluated) = evaluate_node(template, &child_ctx) {
245 results.push(evaluated);
246 }
247 }
248
249 let mut marker = Map::new();
251 marker.insert("__flatten".to_string(), Value::Array(results));
252 Some(Value::Object(marker))
253}
254
255fn evaluate_if(condition: &Value, map: &Map<String, Value>, ctx: &EvalContext) -> Option<Value> {
256 let resolved_cond = evaluate_node(condition, ctx)?;
257 if is_truthy(&resolved_cond) {
258 map.get("then").and_then(|t| evaluate_node(t, ctx))
259 } else {
260 map.get("else").and_then(|e| evaluate_node(e, ctx))
261 }
262}
263
264fn evaluate_cond(args: &Value, ctx: &EvalContext) -> Option<Value> {
265 let arr = args.as_array()?;
266 if arr.len() != 3 {
267 return None;
268 }
269 let condition = evaluate_node(&arr[0], ctx)?;
270 if is_truthy(&condition) {
271 evaluate_node(&arr[1], ctx)
272 } else {
273 evaluate_node(&arr[2], ctx)
274 }
275}
276
277enum CompareOp {
278 Eq,
279 Ne,
280 Gt,
281 Lt,
282 Gte,
283 Lte,
284}
285
286fn evaluate_comparison(args: &Value, ctx: &EvalContext, op: CompareOp) -> Option<Value> {
287 let arr = args.as_array()?;
288 if arr.len() != 2 {
289 return None;
290 }
291 let a = evaluate_node(&arr[0], ctx)?;
292 let b = evaluate_node(&arr[1], ctx)?;
293 Some(Value::Bool(compare_values(&a, &b, &op)))
294}
295
296fn compare_values(a: &Value, b: &Value, op: &CompareOp) -> bool {
298 match (as_f64(a), as_f64(b)) {
299 (Some(na), Some(nb)) => match op {
300 CompareOp::Eq => na == nb,
301 CompareOp::Ne => na != nb,
302 CompareOp::Gt => na > nb,
303 CompareOp::Lt => na < nb,
304 CompareOp::Gte => na >= nb,
305 CompareOp::Lte => na <= nb,
306 },
307 _ => match op {
308 CompareOp::Eq => a == b,
309 CompareOp::Ne => a != b,
310 CompareOp::Gt | CompareOp::Lt | CompareOp::Gte | CompareOp::Lte => {
312 match (a.as_str(), b.as_str()) {
313 (Some(sa), Some(sb)) => match op {
314 CompareOp::Gt => sa > sb,
315 CompareOp::Lt => sa < sb,
316 CompareOp::Gte => sa >= sb,
317 CompareOp::Lte => sa <= sb,
318 _ => unreachable!(),
319 },
320 _ => false,
321 }
322 }
323 },
324 }
325}
326
327fn as_f64(v: &Value) -> Option<f64> {
328 match v {
329 Value::Number(n) => n.as_f64(),
330 _ => None,
331 }
332}
333
334fn evaluate_arithmetic(args: &Value, ctx: &EvalContext, op: fn(f64, f64) -> f64) -> Option<Value> {
335 let arr = args.as_array()?;
336 if arr.len() != 2 {
337 return None;
338 }
339 let a = evaluate_node(&arr[0], ctx).and_then(|v| as_f64(&v))?;
340 let b = evaluate_node(&arr[1], ctx).and_then(|v| as_f64(&v))?;
341 let result = op(a, b);
342 Some(serde_json::Number::from_f64(result).map_or(Value::Null, Value::Number))
343}
344
345fn evaluate_string_transform(
346 arg: &Value,
347 ctx: &EvalContext,
348 transform: fn(&str) -> String,
349) -> Option<Value> {
350 let resolved = evaluate_node(arg, ctx)?;
351 let s = value_to_string(&resolved)?;
352 Some(Value::String(transform(&s)))
353}
354
355fn evaluate_concat(args: &Value, ctx: &EvalContext) -> Option<Value> {
356 let arr = args.as_array()?;
357 let mut result = String::new();
358 for item in arr {
359 let resolved = evaluate_node(item, ctx)?;
360 result.push_str(&value_to_string(&resolved)?);
361 }
362 Some(Value::String(result))
363}
364
365fn evaluate_format(args: &Value, ctx: &EvalContext) -> Option<Value> {
366 let arr = args.as_array()?;
367 if arr.len() != 2 {
368 return None;
369 }
370 let value = evaluate_node(&arr[0], ctx).and_then(|v| as_f64(&v))?;
371 let format_str = evaluate_node(&arr[1], ctx)?;
372 let fmt = format_str.as_str()?;
373
374 let decimal_places = if let Some(dot_pos) = fmt.find('.') {
376 fmt.len() - dot_pos - 1
377 } else {
378 0
379 };
380
381 Some(Value::String(format!(
382 "{:.prec$}",
383 value,
384 prec = decimal_places
385 )))
386}
387
388fn evaluate_count(arg: &Value, ctx: &EvalContext) -> Option<Value> {
389 let resolved = evaluate_node(arg, ctx)?;
390 match &resolved {
391 Value::Array(arr) => Some(Value::Number(serde_json::Number::from(arr.len()))),
392 _ => Some(Value::Number(serde_json::Number::from(0))),
393 }
394}
395
396fn is_truthy(v: &Value) -> bool {
400 match v {
401 Value::Null => false,
402 Value::Bool(b) => *b,
403 Value::Number(n) => n.as_f64().is_some_and(|f| f != 0.0),
404 Value::String(s) => !s.is_empty(),
405 Value::Array(a) => !a.is_empty(),
406 Value::Object(_) => true,
407 }
408}
409
410fn value_to_string(v: &Value) -> Option<String> {
412 match v {
413 Value::String(s) => Some(s.clone()),
414 Value::Number(n) => Some(n.to_string()),
415 Value::Bool(b) => Some(b.to_string()),
416 Value::Null => Some("".to_string()),
417 _ => None,
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424 use serde_json::json;
425
426 #[test]
427 fn test_resolve_ref_simple() {
428 let ctx = EvalContext::new(json!({"name": "Alice"}));
429 assert_eq!(ctx.resolve_ref("name"), Some(&json!("Alice")));
430 }
431
432 #[test]
433 fn test_resolve_ref_nested() {
434 let ctx = EvalContext::new(json!({"user": {"name": "Bob"}}));
435 assert_eq!(ctx.resolve_ref("user.name"), Some(&json!("Bob")));
436 }
437
438 #[test]
439 fn test_resolve_ref_scope_first() {
440 let ctx = EvalContext::new(json!({"name": "root"}));
441 let child = ctx.with_binding("$item", json!({"name": "scoped"}));
442 assert_eq!(child.resolve_ref("$item.name"), Some(&json!("scoped")));
443 }
444
445 #[test]
446 fn test_resolve_ref_missing() {
447 let ctx = EvalContext::new(json!({"name": "Alice"}));
448 assert_eq!(ctx.resolve_ref("missing"), None);
449 }
450
451 #[test]
452 fn test_is_truthy() {
453 assert!(!is_truthy(&json!(null)));
454 assert!(!is_truthy(&json!(false)));
455 assert!(!is_truthy(&json!(0)));
456 assert!(!is_truthy(&json!("")));
457 assert!(!is_truthy(&json!([])));
458
459 assert!(is_truthy(&json!(true)));
460 assert!(is_truthy(&json!(1)));
461 assert!(is_truthy(&json!("hello")));
462 assert!(is_truthy(&json!([1])));
463 assert!(is_truthy(&json!({"a": 1})));
464 }
465
466 #[test]
467 fn test_evaluate_ref() {
468 let template = json!({"$ref": "name"});
469 let data = json!({"name": "Alice"});
470 let result = evaluate_template(&template, &data).unwrap();
471 assert_eq!(result, json!("Alice"));
472 }
473
474 #[test]
475 fn test_passthrough() {
476 let template = json!({"type": "Text", "content": "hello"});
477 let data = json!({});
478 let result = evaluate_template(&template, &data).unwrap();
479 assert_eq!(result, json!({"type": "Text", "content": "hello"}));
480 }
481}