pub fn interpolate(template: &str, args: &[(&str, String)]) -> String {
let mut result = String::with_capacity(template.len());
let bytes = template.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%'
&& i + 1 < bytes.len()
&& bytes[i + 1] == b'{'
&& let Some(close_rel) = template[i + 2..].find('}')
{
let close = i + 2 + close_rel;
let name = &template[i + 2..close];
if !name.is_empty() {
match args.iter().find(|(key, _)| *key == name) {
Some((_, value)) => {
result.push_str(value);
i = close + 1;
continue;
}
None => {
result.push_str(&template[i..close + 1]);
i = close + 1;
continue;
}
}
}
}
let ch = template[i..].chars().next().expect("valid utf-8");
result.push(ch);
i += ch.len_utf8();
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_interpolation() {
let result = interpolate("Hello, %{name}!", &[("name", "world".into())]);
assert_eq!(result, "Hello, world!");
}
#[test]
fn test_multiple_interpolations() {
let result = interpolate(
"%{greeting}, %{name}! You have %{count} messages.",
&[
("greeting", "Hello".into()),
("name", "Alice".into()),
("count", "3".into()),
],
);
assert_eq!(result, "Hello, Alice! You have 3 messages.");
}
#[test]
fn test_missing_argument_left_as_is() {
let result = interpolate("Hello, %{name}!", &[]);
assert_eq!(result, "Hello, %{name}!");
}
#[test]
fn test_no_placeholders() {
let result = interpolate("No placeholders here.", &[("name", "test".into())]);
assert_eq!(result, "No placeholders here.");
}
#[test]
fn test_repeated_placeholder() {
let result = interpolate("%{x} and %{x}", &[("x", "1".into())]);
assert_eq!(result, "1 and 1");
}
#[test]
fn test_no_cascade_substitution() {
let result = interpolate("%{a}", &[("a", "%{b}".into()), ("b", "X".into())]);
assert_eq!(result, "%{b}");
}
}