use std::collections::HashMap;
pub fn render_template(template: &str, args: &str, variables: &HashMap<String, String>) -> String {
let result = template.replace("$ARGUMENTS", args);
let positional: Vec<&str> = if args.is_empty() {
Vec::new()
} else {
args.split_whitespace().collect()
};
let mut output = String::with_capacity(result.len());
let mut chars = result.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '$' && chars.peek() == Some(&'{') {
chars.next();
let mut var_name = String::new();
let mut found_close = false;
for inner in chars.by_ref() {
if inner == '}' {
found_close = true;
break;
}
var_name.push(inner);
}
if !found_close {
output.push('$');
output.push('{');
output.push_str(&var_name);
} else if let Ok(idx) = var_name.parse::<usize>() {
if idx >= 1 && idx <= positional.len() {
output.push_str(positional[idx - 1]);
}
} else {
if let Some(val) = variables.get(&var_name) {
output.push_str(val);
}
}
} else {
output.push(ch);
}
}
output
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_vars() -> HashMap<String, String> {
HashMap::new()
}
fn sample_vars() -> HashMap<String, String> {
let mut m = HashMap::new();
m.insert("lang".into(), "Rust".into());
m.insert("framework".into(), "Axum".into());
m
}
#[test]
fn arguments_substitution() {
let result = render_template("Run with: $ARGUMENTS", "foo bar baz", &empty_vars());
assert_eq!(result, "Run with: foo bar baz");
}
#[test]
fn arguments_empty() {
let result = render_template("Args: $ARGUMENTS end", "", &empty_vars());
assert_eq!(result, "Args: end");
}
#[test]
fn positional_arg_1() {
let result = render_template("Hello ${1}!", "world", &empty_vars());
assert_eq!(result, "Hello world!");
}
#[test]
fn positional_args_multiple() {
let result = render_template("${1} and ${2}", "alpha beta gamma", &empty_vars());
assert_eq!(result, "alpha and beta");
}
#[test]
fn positional_arg_out_of_range() {
let result = render_template("Missing: ${5}", "only one", &empty_vars());
assert_eq!(result, "Missing: ");
}
#[test]
fn positional_arg_zero_is_empty() {
let result = render_template("Zero: ${0}", "first", &empty_vars());
assert_eq!(result, "Zero: ");
}
#[test]
fn named_variable_substitution() {
let result = render_template(
"Language: ${lang}, Framework: ${framework}",
"",
&sample_vars(),
);
assert_eq!(result, "Language: Rust, Framework: Axum");
}
#[test]
fn missing_variable_becomes_empty() {
let result = render_template("Missing: ${nonexistent}", "", &empty_vars());
assert_eq!(result, "Missing: ");
}
#[test]
fn mixed_substitutions() {
let result = render_template(
"Run ${1} with ${lang}: $ARGUMENTS",
"test_suite extra_arg",
&sample_vars(),
);
assert_eq!(result, "Run test_suite with Rust: test_suite extra_arg");
}
#[test]
fn no_substitutions_passthrough() {
let template = "No variables here, just plain text.";
let result = render_template(template, "args", &empty_vars());
assert_eq!(result, template);
}
#[test]
fn unclosed_brace_preserved() {
let result = render_template("Broken: ${unclosed", "", &empty_vars());
assert_eq!(result, "Broken: ${unclosed");
}
#[test]
fn dollar_without_brace_preserved() {
let result = render_template("Price: $100", "", &empty_vars());
assert_eq!(result, "Price: $100");
}
#[test]
fn empty_template() {
let result = render_template("", "args", &sample_vars());
assert_eq!(result, "");
}
#[test]
fn arguments_appears_multiple_times() {
let result = render_template("$ARGUMENTS and again $ARGUMENTS", "hello", &empty_vars());
assert_eq!(result, "hello and again hello");
}
#[test]
fn positional_with_extra_whitespace() {
let result = render_template("${1} ${2}", "alpha beta", &empty_vars());
assert_eq!(result, "alpha beta");
}
#[test]
fn consecutive_variables() {
let result = render_template("${lang}${framework}", "", &sample_vars());
assert_eq!(result, "RustAxum");
}
}