gha-expression-proof 1.0.0

GitHub Actions expression evaluator and receipt generator for offline CI compatibility testing
Documentation
use crate::ast::Expr;
use crate::eval::{EvaluationOptions, Evaluator};
use crate::parser::parse_expression;
use crate::value::string_for_render;
use anyhow::{Result, bail};

pub fn render_template(
    template: &str,
    options: &EvaluationOptions,
    functions: &mut Vec<String>,
    references: &mut Vec<String>,
) -> Result<String> {
    let mut out = String::new();
    let mut pos = 0usize;

    while let Some(start) = find_open(template, pos) {
        out.push_str(&template[pos..start]);
        let expr_start = start + 3;
        let close = find_close(template, expr_start)?;
        let raw_expr = template[expr_start..close].trim();
        let expr = parse_expression(raw_expr)?;
        expr.collect_functions(functions);
        expr.collect_roots(references);
        let mut evaluator = Evaluator::new(options);
        let value = evaluator.eval_for_template(&expr)?;
        out.push_str(&string_for_render(&value.json));
        pos = close + 2;
    }

    out.push_str(&template[pos..]);
    Ok(out)
}

fn find_open(template: &str, start: usize) -> Option<usize> {
    template[start..].find("${{").map(|offset| start + offset)
}

fn find_close(template: &str, start: usize) -> Result<usize> {
    let chars = template.char_indices().collect::<Vec<_>>();
    let mut i = chars.partition_point(|(offset, _)| *offset < start);
    let mut in_string = false;

    while i < chars.len() {
        let (offset, ch) = chars[i];
        if ch == '\'' {
            if in_string && chars.get(i + 1).map(|(_, ch)| *ch) == Some('\'') {
                i += 2;
                continue;
            }
            in_string = !in_string;
        }
        if !in_string && ch == '}' && chars.get(i + 1).map(|(_, ch)| *ch) == Some('}') {
            return Ok(offset);
        }
        i += 1;
    }

    bail!("template expression is missing closing `}}`")
}

#[allow(dead_code)]
fn _assert_expr_send_sync(_: &Expr) {}