runjucks_core 0.1.7

Pure Rust Nunjucks-compatible template engine core (Runjucks)
Documentation
//! Parse cache correctness: signature invalidation and named/inline reuse.

use runjucks_core::extension::CustomExtensionHandler;
use runjucks_core::loader::map_loader;
use runjucks_core::Environment;
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;

fn echo_handler() -> CustomExtensionHandler {
    Arc::new(|_ctx, args, body| {
        Ok(format!(
            "[{}]{}",
            args.trim(),
            body.as_deref().unwrap_or("")
        ))
    })
}

fn env_with_map(templates: HashMap<String, String>) -> Environment {
    let mut env = Environment::default();
    env.loader = Some(map_loader(templates));
    env
}

#[test]
fn inline_cache_hit_same_source_twice() {
    let env = Environment::default();
    let tpl = "Hello {{ name }}".to_string();
    let ctx = json!({ "name": "Ada" });
    let a = env.render_string(tpl.clone(), ctx.clone()).unwrap();
    let b = env.render_string(tpl, ctx).unwrap();
    assert_eq!(a, b);
    assert_eq!(a, "Hello Ada");
}

#[test]
fn signature_invalidation_trim_blocks() {
    let mut env = Environment::default();
    let tpl = "{% if true %}\nX{% endif %}".to_string();
    env.trim_blocks = false;
    let out_loose = env.render_string(tpl.clone(), json!({})).unwrap();
    env.trim_blocks = true;
    let out_trim = env.render_string(tpl, json!({})).unwrap();
    assert_ne!(out_loose, out_trim);
}

#[test]
fn signature_invalidation_custom_delimiters() {
    let mut env = Environment::default();
    let tpl = "<$ x $>".to_string();
    let as_text = env
        .render_string(tpl.clone(), json!({ "x": "hi" }))
        .unwrap();
    assert_eq!(as_text, "<$ x $>");
    env.tags = Some(runjucks_core::Tags {
        variable_start: "<$".into(),
        variable_end: "$>".into(),
        ..Default::default()
    });
    let as_var = env.render_string(tpl, json!({ "x": "hi" })).unwrap();
    assert_eq!(as_var, "hi");
}

#[test]
fn signature_invalidation_extension_tags() {
    let mut env = Environment::default();
    env.autoescape = false;
    let src = "{% echo x %}";
    assert!(env.render_string(src.into(), json!({})).is_err());
    env.register_extension("e", vec![("echo".into(), None)], echo_handler())
        .unwrap();
    let out = env.render_string(src.into(), json!({})).unwrap();
    assert_eq!(out, "[x]");
}

#[test]
fn signature_invalidation_remove_extension_after_cache() {
    let mut env = Environment::default();
    env.autoescape = false;
    env.register_extension("e", vec![("echo".into(), None)], echo_handler())
        .unwrap();
    let src = "{% echo %}";
    let ok = env.render_string(src.into(), json!({})).unwrap();
    assert_eq!(ok, "[]");
    assert!(env.remove_extension("e"));
    assert!(env.render_string(src.into(), json!({})).is_err());
}

#[test]
fn named_cache_hit_same_name_twice() {
    let mut m = HashMap::new();
    m.insert("a.njk".into(), "{{ x }}".into());
    let env = env_with_map(m);
    let ctx = json!({ "x": 1 });
    let u = env.render_template("a.njk", ctx.clone()).unwrap();
    let v = env.render_template("a.njk", ctx).unwrap();
    assert_eq!(u, v);
    assert_eq!(u, "1");
}

#[test]
fn named_cache_reflects_loader_source_change() {
    let mut m1 = HashMap::new();
    m1.insert("a.njk".into(), "one".into());
    let mut env = env_with_map(m1);
    assert_eq!(env.render_template("a.njk", json!({})).unwrap(), "one");

    let mut m2 = HashMap::new();
    m2.insert("a.njk".into(), "two".into());
    env.loader = Some(map_loader(m2));
    assert_eq!(env.render_template("a.njk", json!({})).unwrap(), "two");
}

#[test]
fn nested_include_renders_twice() {
    let mut m = HashMap::new();
    m.insert("p.njk".into(), "{{ n }}".into());
    m.insert(
        "main.njk".into(),
        r#"{% include "p.njk" %}{% include "p.njk" %}"#.into(),
    );
    let env = env_with_map(m);
    let out = env.render_template("main.njk", json!({ "n": 3 })).unwrap();
    assert_eq!(out, "33");
    let out2 = env.render_template("main.njk", json!({ "n": 4 })).unwrap();
    assert_eq!(out2, "44");
}

#[test]
fn invalidate_cache_smoke_named_and_inline() {
    let tpl = "Hello {{ x }}".to_string();
    let mut env = Environment::default();
    env.render_string(tpl.clone(), json!({ "x": 1 })).unwrap();

    let mut m = HashMap::new();
    m.insert("a.njk".into(), "{{ y }}".into());
    env.loader = Some(map_loader(m));
    env.render_template("a.njk", json!({ "y": 2 })).unwrap();

    env.invalidate_cache();

    assert_eq!(
        env.render_string(tpl, json!({ "x": 3 })).unwrap(),
        "Hello 3"
    );
    assert_eq!(
        env.render_template("a.njk", json!({ "y": 4 })).unwrap(),
        "4"
    );
}