lithos_gotmpl_engine/
runtime.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2use std::collections::HashMap;
3use std::sync::Arc;
4
5use serde_json::{Number, Value};
6
7use crate::ast::{BindingKind, Command, Expression, Pipeline};
8use crate::error::Error;
9
10/// Signature implemented by helper functions invoked from templates.
11pub type Function = dyn Fn(&mut EvalContext, &[Value]) -> Result<Value, Error> + Send + Sync;
12
13/// Registry that maps helper names to callable functions.
14#[derive(Clone, Default)]
15pub struct FunctionRegistry {
16    map: Arc<HashMap<String, Arc<Function>>>,
17}
18
19impl FunctionRegistry {
20    /// Creates an empty registry.
21    pub fn empty() -> Self {
22        Self {
23            map: Arc::new(HashMap::new()),
24        }
25    }
26
27    /// Returns a new builder for constructing registries.
28    pub fn builder() -> FunctionRegistryBuilder {
29        FunctionRegistryBuilder::new()
30    }
31
32    /// Builds a registry from a previously configured builder.
33    pub fn from_builder(builder: FunctionRegistryBuilder) -> Self {
34        builder.build()
35    }
36
37    /// Fetches a helper function by name.
38    pub fn get(&self, name: &str) -> Option<Arc<Function>> {
39        self.map.get(name).cloned()
40    }
41
42    /// Reports whether the registry contains no helper functions.
43    pub fn is_empty(&self) -> bool {
44        self.map.is_empty()
45    }
46}
47
48/// Helper for constructing registries before freezing them into an immutable map.
49#[derive(Default)]
50pub struct FunctionRegistryBuilder {
51    map: HashMap<String, Arc<Function>>,
52}
53
54impl FunctionRegistryBuilder {
55    /// Creates a new, empty builder.
56    pub fn new() -> Self {
57        Self {
58            map: HashMap::new(),
59        }
60    }
61
62    /// Registers a helper function under the provided name.
63    pub fn register<F>(&mut self, name: impl Into<String>, func: F) -> &mut Self
64    where
65        F: Fn(&mut EvalContext, &[Value]) -> Result<Value, Error> + Send + Sync + 'static,
66    {
67        self.map.insert(name.into(), Arc::new(func));
68        self
69    }
70
71    /// Extends the builder with all helpers from another registry.
72    pub fn extend(&mut self, other: &FunctionRegistry) -> &mut Self {
73        for (key, value) in other.map.iter() {
74            self.map.insert(key.clone(), value.clone());
75        }
76        self
77    }
78
79    /// Finalises the builder into an immutable registry.
80    pub fn build(self) -> FunctionRegistry {
81        FunctionRegistry {
82            map: Arc::new(self.map),
83        }
84    }
85}
86
87/// Execution context threaded through template evaluation.
88pub struct EvalContext {
89    stack: Vec<Value>,
90    root: Value,
91    variables: Vec<HashMap<String, Value>>,
92    functions: FunctionRegistry,
93}
94
95impl EvalContext {
96    /// Creates a new evaluation context seeded with the input data and helper registry.
97    pub fn new(data: Value, functions: FunctionRegistry) -> Self {
98        let mut variables = Vec::new();
99        let mut scope = HashMap::new();
100        scope.insert("$".to_string(), data.clone());
101        variables.push(scope);
102
103        Self {
104            stack: vec![data.clone()],
105            root: data,
106            variables,
107            functions,
108        }
109    }
110
111    /// Retrieves a helper function by name, if registered.
112    pub fn function(&self, name: &str) -> Option<Arc<Function>> {
113        self.functions.get(name)
114    }
115
116    /// Pushes a new scope with the provided value at the top of the stack.
117    pub fn push_scope(&mut self, value: Value) {
118        self.stack.push(value);
119        self.variables.push(self.new_scope());
120    }
121
122    /// Pops the current scope, restoring the previous context.
123    pub fn pop_scope(&mut self) {
124        if self.stack.len() > 1 {
125            self.stack.pop();
126        }
127        if self.variables.len() > 1 {
128            self.variables.pop();
129        }
130    }
131
132    fn new_scope(&self) -> HashMap<String, Value> {
133        let mut scope = HashMap::new();
134        scope.insert("$".to_string(), self.root.clone());
135        scope
136    }
137
138    /// Evaluates a pipeline in the context and returns the resulting value.
139    pub fn eval_pipeline(&mut self, pipeline: &Pipeline) -> Result<Value, Error> {
140        let mut iter = pipeline.commands.iter();
141        let first = iter
142            .next()
143            .ok_or_else(|| Error::render("empty pipeline", None))?;
144        let mut value = self.eval_command(first, None)?;
145
146        for command in iter {
147            value = self.eval_command(command, Some(value))?;
148        }
149
150        Ok(value)
151    }
152
153    fn eval_command(&mut self, command: &Command, input: Option<Value>) -> Result<Value, Error> {
154        if let Expression::Identifier(name) = &command.target {
155            if let Some(func) = self.functions.get(name.as_str()) {
156                let mut args = Vec::new();
157                for expr in &command.args {
158                    args.push(self.eval_expression(expr)?);
159                }
160                if let Some(prev) = input {
161                    args.push(prev);
162                }
163                return func(self, &args);
164            } else if !command.args.is_empty() || input.is_some() {
165                return Err(Error::render(format!("unknown function \"{name}\""), None));
166            }
167        }
168
169        if !command.args.is_empty() {
170            return Err(Error::render(
171                "arguments supplied to non-function expression",
172                None,
173            ));
174        }
175
176        if input.is_some() {
177            return Err(Error::render(
178                "cannot pipe value into non-function expression",
179                None,
180            ));
181        }
182
183        self.eval_expression(&command.target)
184    }
185
186    fn eval_expression(&mut self, expr: &Expression) -> Result<Value, Error> {
187        match expr {
188            Expression::Identifier(name) => Ok(self.resolve_identifier(name)),
189            Expression::Field(parts) => self.resolve_field(parts),
190            Expression::Variable(name) => Ok(self.resolve_variable(name)),
191            Expression::PipelineExpr(pipeline) => {
192                if pipeline.declarations.is_some() {
193                    return Err(Error::render(
194                        "pipeline declarations not allowed in expression",
195                        None,
196                    ));
197                }
198                self.eval_pipeline(pipeline)
199            }
200            Expression::StringLiteral(value) => Ok(Value::String(value.clone())),
201            Expression::NumberLiteral(text) => parse_number(text)
202                .map(Value::Number)
203                .ok_or_else(|| Error::render(format!("invalid number literal {text}"), None)),
204            Expression::BoolLiteral(flag) => Ok(Value::Bool(*flag)),
205            Expression::Nil => Ok(Value::Null),
206        }
207    }
208
209    fn resolve_identifier(&self, name: &str) -> Value {
210        for value in self.stack.iter().rev() {
211            if let Value::Object(map) = value {
212                if let Some(found) = map.get(name) {
213                    return found.clone();
214                }
215            }
216        }
217        Value::Null
218    }
219
220    fn resolve_field(&self, parts: &[String]) -> Result<Value, Error> {
221        if parts.is_empty() {
222            return self
223                .stack
224                .last()
225                .cloned()
226                .ok_or_else(|| Error::render("dot resolution failed", None));
227        }
228        if let Some(first) = parts.first() {
229            if first.starts_with('$') {
230                let mut value = self.resolve_variable(first);
231                for part in parts.iter().skip(1) {
232                    value = Self::project_field_segment(value, part)?;
233                }
234                return Ok(value);
235            }
236        }
237
238        let mut value = self
239            .stack
240            .last()
241            .cloned()
242            .ok_or_else(|| Error::render("dot resolution failed", None))?;
243
244        for part in parts {
245            value = Self::project_field_segment(value, part)?;
246        }
247
248        Ok(value)
249    }
250
251    fn resolve_variable(&self, name: &str) -> Value {
252        if name == "$" {
253            return self.root.clone();
254        }
255
256        for scope in self.variables.iter().rev() {
257            if let Some(value) = scope.get(name) {
258                return value.clone();
259            }
260        }
261
262        Value::Null
263    }
264
265    fn set_variable(&mut self, name: &str, kind: BindingKind, value: Value) -> Result<(), Error> {
266        if name == "$" {
267            return Err(Error::render("cannot assign to root variable", None));
268        }
269
270        match kind {
271            BindingKind::Declare => {
272                self.variables
273                    .last_mut()
274                    .expect("scope stack is non-empty")
275                    .insert(name.to_string(), value);
276                Ok(())
277            }
278            BindingKind::Assign => {
279                for scope in self.variables.iter_mut().rev() {
280                    if scope.contains_key(name) {
281                        scope.insert(name.to_string(), value);
282                        return Ok(());
283                    }
284                }
285                Err(Error::render(format!("variable {name} not defined"), None))
286            }
287        }
288    }
289
290    fn project_field_segment(value: Value, part: &str) -> Result<Value, Error> {
291        match value {
292            Value::Object(map) => Ok(map.get(part).cloned().unwrap_or(Value::Null)),
293            Value::Array(list) => {
294                let index = part.parse::<usize>().map_err(|_| {
295                    Error::render(format!("array index must be integer, got {part}"), None)
296                })?;
297                Ok(list.get(index).cloned().unwrap_or(Value::Null))
298            }
299            _ => Err(Error::render(
300                format!("cannot access field {part} on non-container value"),
301                None,
302            )),
303        }
304    }
305
306    pub(crate) fn apply_bindings(
307        &mut self,
308        pipeline: &Pipeline,
309        value: &Value,
310    ) -> Result<(), Error> {
311        if let Some(decls) = &pipeline.declarations {
312            if decls.variables.is_empty() {
313                return Ok(());
314            }
315
316            if decls.variables.len() == 1 {
317                self.set_variable(&decls.variables[0], decls.kind, value.clone())?;
318            } else if let Value::Array(items) = value {
319                for (idx, name) in decls.variables.iter().enumerate() {
320                    let assigned = items.get(idx).cloned().unwrap_or(Value::Null);
321                    self.set_variable(name, decls.kind, assigned)?;
322                }
323            } else {
324                for name in &decls.variables {
325                    self.set_variable(name, decls.kind, value.clone())?;
326                }
327            }
328        }
329        Ok(())
330    }
331
332    pub(crate) fn predeclare_bindings(&mut self, pipeline: &Pipeline) {
333        if let Some(decls) = &pipeline.declarations {
334            if decls.kind == BindingKind::Declare {
335                for name in &decls.variables {
336                    self.variables
337                        .last_mut()
338                        .expect("scope stack is non-empty")
339                        .entry(name.clone())
340                        .or_insert(Value::Null);
341                }
342            }
343        }
344    }
345
346    pub(crate) fn assign_range_bindings(
347        &mut self,
348        pipeline: &Pipeline,
349        key: Option<Value>,
350        value: Value,
351    ) -> Result<(), Error> {
352        if let Some(decls) = &pipeline.declarations {
353            match decls.variables.len() {
354                0 => {}
355                1 => {
356                    self.set_variable(&decls.variables[0], decls.kind, value)?;
357                }
358                _ => {
359                    let key_value = key.unwrap_or(Value::Null);
360                    self.set_variable(&decls.variables[0], decls.kind, key_value)?;
361                    if let Some(second) = decls.variables.get(1) {
362                        self.set_variable(second, decls.kind, value)?;
363                    }
364                }
365            }
366        }
367        Ok(())
368    }
369}
370
371pub fn value_to_string(value: &Value) -> String {
372    match value {
373        Value::Null => String::new(),
374        Value::Bool(b) => b.to_string(),
375        Value::Number(n) => {
376            if let Some(i) = n.as_i64() {
377                i.to_string()
378            } else if let Some(u) = n.as_u64() {
379                u.to_string()
380            } else {
381                let mut s = n.to_string();
382                if s.contains('.') {
383                    while s.ends_with('0') {
384                        s.pop();
385                    }
386                    if s.ends_with('.') {
387                        s.pop();
388                    }
389                }
390                s
391            }
392        }
393        Value::String(s) => s.clone(),
394        Value::Array(_) | Value::Object(_) => serde_json::to_string(value).unwrap_or_default(),
395    }
396}
397
398pub fn parse_number(text: &str) -> Option<Number> {
399    if !text.contains(['.', 'e', 'E']) {
400        if let Ok(value) = text.parse::<i64>() {
401            return Some(Number::from(value));
402        }
403        if let Ok(value) = text.parse::<u64>() {
404            return Some(Number::from(value));
405        }
406    }
407
408    text.parse::<f64>().ok().and_then(Number::from_f64)
409}
410
411pub fn is_empty(value: &Value) -> bool {
412    match value {
413        Value::Null => true,
414        Value::Bool(b) => !*b,
415        Value::Number(n) => {
416            if let Some(i) = n.as_i64() {
417                i == 0
418            } else if let Some(u) = n.as_u64() {
419                u == 0
420            } else {
421                n.as_f64().map(|f| f == 0.0).unwrap_or(false)
422            }
423        }
424        Value::String(s) => s.is_empty(),
425        Value::Array(arr) => arr.iter().all(is_empty),
426        Value::Object(map) => map.is_empty(),
427    }
428}
429
430pub fn is_truthy(value: &Value) -> bool {
431    match value {
432        Value::Null => false,
433        Value::Bool(b) => *b,
434        Value::Number(n) => {
435            if let Some(i) = n.as_i64() {
436                i != 0
437            } else if let Some(u) = n.as_u64() {
438                u != 0
439            } else {
440                n.as_f64().map(|f| f != 0.0).unwrap_or(false)
441            }
442        }
443        Value::String(s) => !s.is_empty(),
444        Value::Array(arr) => !arr.is_empty(),
445        Value::Object(map) => !map.is_empty(),
446    }
447}
448
449pub fn coerce_number(value: &Value) -> Result<f64, Error> {
450    if let Some(i) = value.as_i64() {
451        Ok(i as f64)
452    } else if let Some(u) = value.as_u64() {
453        Ok(u as f64)
454    } else if let Some(f) = value.as_f64() {
455        Ok(f)
456    } else if let Some(s) = value.as_str() {
457        s.parse::<f64>()
458            .map_err(|_| Error::render("cannot convert string to number", None))
459    } else {
460        Err(Error::render("expected numeric value for comparison", None))
461    }
462}