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
- Runtime-configurable output โ templates loaded from files or databases, not hardcoded `format!` calls.
- Separation of concerns โ designers edit templates; engineers supply data. No recompilation for copy changes.
- Crate literacy โ once you've written a minimal version, Tera and MiniJinja are easy to evaluate and adopt.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Template engine | `Printf`-style or `Format` | Manual find/replace or `tera`/`minijinja` crates |
| Placeholder syntax | `%s`, `%d` positional | Custom `{{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)