greentic-flow-builder 0.1.0

AI-powered Adaptive Card flow builder with visual graph editor and demo runner
Documentation
//! Handlebars helper functions.

use handlebars::{Context, Handlebars, Helper, HelperResult, Output, RenderContext};
use serde_json::Value;

pub fn register_all(hbs: &mut Handlebars<'static>) {
    hbs.register_helper("default", Box::new(default_helper));
    hbs.register_helper("eq", Box::new(eq_helper));
    hbs.register_helper("json_str", Box::new(json_str_helper));
    hbs.register_helper("json_value", Box::new(json_value_helper));
    hbs.register_helper("has_any", Box::new(has_any_helper));
    hbs.register_helper("concat", Box::new(concat_helper));
    hbs.register_helper("format_number", Box::new(format_number_helper));
    hbs.register_helper("join", Box::new(join_helper));
    hbs.register_helper("theme", Box::new(theme_helper));
}

fn default_helper(
    h: &Helper,
    _: &Handlebars,
    _: &Context,
    _: &mut RenderContext,
    out: &mut dyn Output,
) -> HelperResult {
    let val = h.param(0).and_then(|p| p.value().as_str());
    let fallback = h.param(1).and_then(|p| p.value().as_str()).unwrap_or("");
    out.write(match val {
        Some(v) if !v.is_empty() => v,
        _ => fallback,
    })?;
    Ok(())
}

fn eq_helper(
    h: &Helper,
    _: &Handlebars,
    _: &Context,
    _: &mut RenderContext,
    out: &mut dyn Output,
) -> HelperResult {
    let a = h.param(0).map(|p| p.value());
    let b = h.param(1).map(|p| p.value());
    let result = matches!((a, b), (Some(a), Some(b)) if a == b);
    out.write(if result { "true" } else { "" })?;
    Ok(())
}

fn json_str_helper(
    h: &Helper,
    _: &Handlebars,
    _: &Context,
    _: &mut RenderContext,
    out: &mut dyn Output,
) -> HelperResult {
    let val = h.param(0).map(|p| p.value()).unwrap_or(&Value::Null);
    let s = match val {
        Value::String(s) => s.clone(),
        Value::Null => String::new(),
        other => other.to_string(),
    };
    let escaped = serde_json::to_string(&s).unwrap_or_else(|_| "\"\"".to_string());
    out.write(&escaped)?;
    Ok(())
}

fn json_value_helper(
    h: &Helper,
    _: &Handlebars,
    _: &Context,
    _: &mut RenderContext,
    out: &mut dyn Output,
) -> HelperResult {
    let val = h.param(0).map(|p| p.value().clone()).unwrap_or(Value::Null);
    let serialized = serde_json::to_string(&val).unwrap_or_else(|_| "null".to_string());
    out.write(&serialized)?;
    Ok(())
}

fn has_any_helper(
    h: &Helper,
    _: &Handlebars,
    _: &Context,
    _: &mut RenderContext,
    out: &mut dyn Output,
) -> HelperResult {
    let val = h.param(0).map(|p| p.value()).unwrap_or(&Value::Null);
    let has_any = match val {
        Value::Array(arr) => !arr.is_empty(),
        Value::Object(obj) => !obj.is_empty(),
        Value::String(s) => !s.is_empty(),
        Value::Null => false,
        _ => true,
    };
    out.write(if has_any { "true" } else { "" })?;
    Ok(())
}

fn concat_helper(
    h: &Helper,
    _: &Handlebars,
    _: &Context,
    _: &mut RenderContext,
    out: &mut dyn Output,
) -> HelperResult {
    let mut result = String::new();
    for p in h.params() {
        match p.value() {
            Value::String(s) => result.push_str(s),
            other => result.push_str(&other.to_string()),
        }
    }
    out.write(&result)?;
    Ok(())
}

fn format_number_helper(
    h: &Helper,
    _: &Handlebars,
    _: &Context,
    _: &mut RenderContext,
    out: &mut dyn Output,
) -> HelperResult {
    let value = h.param(0).and_then(|p| p.value().as_f64()).unwrap_or(0.0);
    let decimals = h.param(1).and_then(|p| p.value().as_u64()).unwrap_or(2) as usize;
    out.write(&format!("{:.*}", decimals, value))?;
    Ok(())
}

fn join_helper(
    h: &Helper,
    _: &Handlebars,
    _: &Context,
    _: &mut RenderContext,
    out: &mut dyn Output,
) -> HelperResult {
    let arr = h.param(0).and_then(|p| p.value().as_array());
    let sep = h.param(1).and_then(|p| p.value().as_str()).unwrap_or(", ");
    if let Some(arr) = arr {
        let parts: Vec<String> = arr
            .iter()
            .filter_map(|v| v.as_str().map(String::from))
            .collect();
        out.write(&parts.join(sep))?;
    }
    Ok(())
}

fn theme_helper(
    h: &Helper,
    _: &Handlebars,
    ctx: &Context,
    _: &mut RenderContext,
    out: &mut dyn Output,
) -> HelperResult {
    let key = h.param(0).and_then(|p| p.value().as_str()).unwrap_or("");
    let fallback = h.param(1).and_then(|p| p.value().as_str()).unwrap_or("");
    let data = ctx.data();
    let token_value = data
        .get("theme")
        .and_then(|t| t.get("tokens"))
        .and_then(|tokens| tokens.get(key));
    match token_value {
        Some(Value::String(s)) => out.write(s)?,
        Some(other) => out.write(&other.to_string())?,
        None => out.write(fallback)?,
    }
    Ok(())
}

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

    fn render(template: &str, data: Value) -> String {
        let mut hbs = Handlebars::new();
        register_all(&mut hbs);
        hbs.render_template(template, &data).unwrap()
    }

    #[test]
    fn test_default_with_value() {
        let result = render(
            r#"{{default name "Guest"}}"#,
            serde_json::json!({"name": "Alice"}),
        );
        assert_eq!(result, "Alice");
    }

    #[test]
    fn test_default_fallback() {
        let result = render(r#"{{default name "Guest"}}"#, serde_json::json!({}));
        assert_eq!(result, "Guest");
    }

    #[test]
    fn test_json_str_escapes_quotes() {
        let result = render(
            r#"{{json_str msg}}"#,
            serde_json::json!({"msg": "he said \"hi\""}),
        );
        assert_eq!(result, r#""he said \"hi\"""#);
    }

    #[test]
    fn test_json_str_unicode() {
        let result = render(r#"{{json_str emoji}}"#, serde_json::json!({"emoji": "✈️"}));
        assert!(result.starts_with('"') && result.ends_with('"'));
    }

    #[test]
    fn test_eq_true() {
        let result = render(
            r#"{{#if (eq status "active")}}YES{{/if}}"#,
            serde_json::json!({"status": "active"}),
        );
        assert_eq!(result, "YES");
    }

    #[test]
    fn test_has_any_empty_array() {
        let result = render(
            r#"{{#if (has_any items)}}HAS{{else}}EMPTY{{/if}}"#,
            serde_json::json!({"items": []}),
        );
        assert_eq!(result, "EMPTY");
    }

    #[test]
    fn test_has_any_with_items() {
        let result = render(
            r#"{{#if (has_any items)}}HAS{{/if}}"#,
            serde_json::json!({"items": [1, 2]}),
        );
        assert_eq!(result, "HAS");
    }

    #[test]
    fn test_concat() {
        let result = render(
            r#"{{concat "Hi " name "!"}}"#,
            serde_json::json!({"name": "Bob"}),
        );
        assert_eq!(result, "Hi Bob!");
    }

    #[test]
    fn test_format_number() {
        let result = render(
            r#"{{format_number price 2}}"#,
            serde_json::json!({"price": 12.3456}),
        );
        assert_eq!(result, "12.35");
    }

    #[test]
    fn test_join() {
        let result = render(
            r#"{{join tags ", "}}"#,
            serde_json::json!({"tags": ["a", "b", "c"]}),
        );
        assert_eq!(result, "a, b, c");
    }

    #[test]
    fn test_theme_token() {
        let result = render(
            r##"{{theme "color_primary" "#000"}}"##,
            serde_json::json!({"theme": {"tokens": {"color_primary": "#0d9488"}}}),
        );
        assert_eq!(result, "#0d9488");
    }

    #[test]
    fn test_theme_fallback() {
        let result = render(
            r##"{{theme "missing" "#000"}}"##,
            serde_json::json!({"theme": {"tokens": {}}}),
        );
        assert_eq!(result, "#000");
    }
}