๐Ÿฆ€ Functional Rust

495: String Template Pattern

Difficulty: 2 Level: Intermediate Replace `{{variable}}` placeholders with values from a map โ€” a lightweight template engine in pure Rust.

The Problem This Solves

Code generation, email bodies, SQL fragment builders, configuration file generation โ€” all share a pattern: a fixed skeleton with slots that get filled in at runtime. You could use `format!` with positional arguments, but that requires knowing all variables at compile time and gets unwieldy with many fields. A template engine decouples the structure (written as a string with named placeholders) from the data (a map of variable names to values). The template is a first-class value you can load from a file, pass around, and render multiple times with different data. Building a minimal version teaches you `str` manipulation patterns, iterator chaining, and how professional template engines (Tera, Handlebars-rs, MiniJinja) work under the hood.

The Intuition

Mail merge. You have a letter template: "Dear {{name}}, your order {{order_id}} has shipped." You run it through the merge engine with a spreadsheet of customer data. Each row fills in the placeholders; the structure stays the same. The template is separate from the data.

How It Works in Rust

1. Find and replace with a `HashMap`:
fn render(template: &str, vars: &HashMap<&str, &str>) -> String {
    let mut result = template.to_string();
    for (key, val) in vars {
        result = result.replace(&format!("{{{{{}}}}}", key), val);
    }
    result
}
2. Single-pass with `find` โ€” more efficient for large templates:
fn render(template: &str, vars: &HashMap<&str, &str>) -> String {
    let mut out = String::with_capacity(template.len());
    let mut rest = template;
    while let Some(start) = rest.find("{{") {
        out.push_str(&rest[..start]);
        let end = rest[start..].find("}}").map(|i| start + i + 2).unwrap_or(rest.len());
        let key = &rest[start+2..end-2];
        out.push_str(vars.get(key).copied().unwrap_or(""));
        rest = &rest[end..];
    }
    out.push_str(rest);
    out
}
3. Usage:
let mut vars = HashMap::new();
vars.insert("name", "Alice");
vars.insert("lang", "Rust");
let result = render("Hello {{name}}, welcome to {{lang}}!", &vars);

What This Unlocks

Key Differences

ConceptOCamlRust
Template engine`Printf`-style or `Format`Manual find/replace or `tera`/`minijinja` crates
Placeholder syntax`%s`, `%d` positionalCustom `{{key}}` named
Variable map`Hashtbl``HashMap<&str, &str>`
Crate option`jingoo``tera`, `handlebars`, `minijinja`
// 495. Template string pattern
use std::collections::HashMap;

fn render(template: &str, vars: &HashMap<&str, &str>) -> String {
    let mut result = template.to_string();
    for (key, value) in vars {
        let placeholder = format!("{{{{{}}}}}", key);
        result = result.replace(&placeholder, value);
    }
    result
}

fn render_fn<F: Fn(&str) -> Option<String>>(template: &str, lookup: F) -> String {
    let mut out = String::with_capacity(template.len());
    let mut rest = template;
    while let Some(start) = rest.find("{{") {
        out.push_str(&rest[..start]);
        rest = &rest[start+2..];
        if let Some(end) = rest.find("}}") {
            let key = &rest[..end];
            out.push_str(&lookup(key).unwrap_or_else(|| format!("{{{{{}}}}}", key)));
            rest = &rest[end+2..];
        } else {
            out.push_str("{{"); // unclosed โ€” keep as-is
        }
    }
    out.push_str(rest);
    out
}

fn main() {
    let tmpl = "Hello, {{name}}! You have {{count}} messages from {{sender}}.";
    let mut vars = HashMap::new();
    vars.insert("name",  "Alice");
    vars.insert("count", "5");
    vars.insert("sender","Bob");

    println!("{}", render(tmpl, &vars));

    // Dynamic lookup
    let result = render_fn(tmpl, |key| vars.get(key).map(|&v| v.to_string()));
    println!("{}", result);

    // Missing key
    let mut v2 = HashMap::new(); v2.insert("name","Eve");
    println!("{}", render("Hi {{name}}, count={{count}}", &v2));
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test] fn test_render() {
        let mut v=HashMap::new(); v.insert("x","10"); v.insert("y","20");
        assert_eq!(render("{{x}}+{{y}}",&v),"10+20");
    }
    #[test] fn test_missing() {
        let v:HashMap<&str,&str>=HashMap::new();
        assert_eq!(render("{{x}}",&v),"{{x}}"); // placeholder kept
    }
}
(* 495. Template strings โ€“ OCaml *)
let render template vars =
  List.fold_left (fun s (k,v) ->
    let placeholder = "{{" ^ k ^ "}}" in
    let buf = Buffer.create (String.length s) in
    let ls = String.length s and lp = String.length placeholder in
    let i = ref 0 in
    while !i <= ls - lp do
      if String.sub s !i lp = placeholder
      then (Buffer.add_string buf v; i := !i + lp)
      else (Buffer.add_char buf s.[!i]; incr i)
    done;
    while !i < ls do Buffer.add_char buf s.[!i]; incr i done;
    Buffer.contents buf
  ) template vars

let () =
  let tmpl = "Hello, {{name}}! You have {{count}} messages." in
  let vars = [("name","Alice");("count","5")] in
  Printf.printf "%s\n" (render tmpl vars)