devist 0.17.2

Project bootstrap CLI for AI-assisted development. Spin up new projects from templates, manage backends, and keep your codebase comprehensible.
use anyhow::{anyhow, Result};
use std::collections::HashMap;

/// Substitute `{{ key }}` (with optional whitespace) using values from `vars`.
/// Also supports conditional blocks:
///
/// ```text
/// {{#if some_var}}included when truthy{{/if}}
/// {{#unless some_var}}included when falsy{{/unless}}
/// ```
///
/// Truthy = the variable exists and its value is non-empty. Conditionals
/// can be nested. Inside a *skipped* block, unknown variable references
/// are tolerated (they would be unreachable anyway). Unknown placeholders
/// in *active* regions still error — fail-loud beats silent corruption.
pub fn render(template: &str, vars: &HashMap<String, String>) -> Result<String> {
    let mut out = String::with_capacity(template.len());
    let mut rest = template;
    // Each entry = "render this branch?". Active iff every level is true.
    let mut keep_stack: Vec<bool> = Vec::new();

    while let Some(start) = rest.find("{{") {
        let active = keep_stack.iter().all(|b| *b);
        if active {
            out.push_str(&rest[..start]);
        }
        let after_open = &rest[start + 2..];
        let end = after_open
            .find("}}")
            .ok_or_else(|| anyhow!("Unclosed placeholder in template"))?;
        let token = after_open[..end].trim();
        rest = &after_open[end + 2..];

        if let Some(var) = token.strip_prefix("#if ") {
            let truthy = is_truthy(vars, var.trim());
            keep_stack.push(truthy);
        } else if let Some(var) = token.strip_prefix("#unless ") {
            let truthy = is_truthy(vars, var.trim());
            keep_stack.push(!truthy);
        } else if token == "/if" || token == "/unless" {
            keep_stack
                .pop()
                .ok_or_else(|| anyhow!("Unmatched {{{{/if}}}} or {{{{/unless}}}}"))?;
        } else if active {
            let value = vars
                .get(token)
                .ok_or_else(|| anyhow!("Unknown template variable: {}", token))?;
            out.push_str(value);
        }
    }

    if !keep_stack.is_empty() {
        return Err(anyhow!("Unclosed {{{{#if}}}} or {{{{#unless}}}} block"));
    }
    out.push_str(rest);
    Ok(out)
}

fn is_truthy(vars: &HashMap<String, String>, key: &str) -> bool {
    vars.get(key).map(|v| !v.is_empty()).unwrap_or(false)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn vars(pairs: &[(&str, &str)]) -> HashMap<String, String> {
        pairs
            .iter()
            .map(|(k, v)| (k.to_string(), v.to_string()))
            .collect()
    }

    #[test]
    fn renders_simple() {
        let v = vars(&[("name", "alice")]);
        assert_eq!(render("hello {{ name }}", &v).unwrap(), "hello alice");
    }

    #[test]
    fn renders_no_whitespace() {
        let v = vars(&[("name", "bob")]);
        assert_eq!(render("hi {{name}}!", &v).unwrap(), "hi bob!");
    }

    #[test]
    fn fails_on_unknown_var() {
        let v = vars(&[]);
        assert!(render("{{ missing }}", &v).is_err());
    }

    #[test]
    fn if_truthy_includes_block() {
        let v = vars(&[("flag", "1")]);
        assert_eq!(render("a {{#if flag}}YES{{/if}} b", &v).unwrap(), "a YES b");
    }

    #[test]
    fn if_falsy_skips_block() {
        let v = vars(&[("flag", "")]);
        assert_eq!(render("a {{#if flag}}YES{{/if}} b", &v).unwrap(), "a  b");
    }

    #[test]
    fn if_missing_var_is_falsy() {
        let v = vars(&[]);
        assert_eq!(render("a {{#if flag}}YES{{/if}} b", &v).unwrap(), "a  b");
    }

    #[test]
    fn unless_negates() {
        let v = vars(&[("flag", "")]);
        assert_eq!(
            render("a {{#unless flag}}NO{{/unless}} b", &v).unwrap(),
            "a NO b"
        );
        let v2 = vars(&[("flag", "1")]);
        assert_eq!(
            render("a {{#unless flag}}NO{{/unless}} b", &v2).unwrap(),
            "a  b"
        );
    }

    #[test]
    fn nested_blocks() {
        let v = vars(&[("a", "1"), ("b", "1")]);
        assert_eq!(
            render("{{#if a}}A{{#if b}}B{{/if}}C{{/if}}", &v).unwrap(),
            "ABC"
        );
        let v2 = vars(&[("a", "1"), ("b", "")]);
        assert_eq!(
            render("{{#if a}}A{{#if b}}B{{/if}}C{{/if}}", &v2).unwrap(),
            "AC"
        );
    }

    #[test]
    fn skipped_block_tolerates_unknown_var() {
        // Inside a falsy block, references to unknown vars must NOT error.
        let v = vars(&[("flag", "")]);
        assert_eq!(
            render("{{#if flag}}{{ never_resolved }}{{/if}}", &v).unwrap(),
            ""
        );
    }

    #[test]
    fn active_block_still_errors_on_unknown_var() {
        let v = vars(&[("flag", "1")]);
        assert!(render("{{#if flag}}{{ missing }}{{/if}}", &v).is_err());
    }

    #[test]
    fn unmatched_close_errors() {
        let v = vars(&[]);
        assert!(render("hello {{/if}}", &v).is_err());
    }

    #[test]
    fn unclosed_block_errors() {
        let v = vars(&[("flag", "1")]);
        assert!(render("{{#if flag}}oops", &v).is_err());
    }
}