use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct VarjMap {
map: HashMap<String, String>,
}
impl VarjMap {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
map: HashMap::with_capacity(capacity),
}
}
pub fn insert<K, V>(&mut self, key: K, value: V)
where
K: Into<String>,
V: Into<String>,
{
self.map.insert(key.into(), value.into());
}
pub fn get<K: AsRef<str>>(&self, key: K) -> Option<&str> {
self.map.get(key.as_ref()).map(|s| s.as_str())
}
pub fn render(&self, template: &str) -> Result<String, Error> {
let blocks = parse_blocks(template);
let mut output = String::with_capacity(template.len() + 32);
let mut idx = 0;
for block in blocks {
output.push_str(&template[idx..block.start]);
idx = block.start;
if let Some(value) = block.value_from_map(self) {
output.push_str(value);
} else {
return Err(Error::from(block));
}
idx += block.len;
}
output.push_str(&template[idx..template.len()]);
Ok(output)
}
#[deprecated(since = "1.1.0", note = "please use `render` instead")]
pub fn parse(&self, template: &str) -> Result<String, Error> {
self.render(template)
}
}
impl From<HashMap<String, String>> for VarjMap {
fn from(map: HashMap<String, String>) -> Self {
VarjMap { map }
}
}
impl From<VarjMap> for HashMap<String, String> {
fn from(map: VarjMap) -> Self {
map.map
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error {
key: String,
line: usize,
col: usize,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}:{} unknown variable '{}'",
self.line, self.col, self.key
)
}
}
impl std::error::Error for Error {}
impl From<Block<'_>> for Error {
fn from(block: Block) -> Error {
Error {
key: block.variable_key.to_owned(),
line: block.line,
col: block.col,
}
}
}
#[derive(Debug, PartialEq)]
struct Block<'a> {
start: usize,
len: usize,
line: usize,
col: usize,
variable_key: &'a str,
}
impl<'a> Block<'a> {
fn value_from_map(&self, vars: &'a VarjMap) -> Option<&'a str> {
vars.get(self.variable_key)
}
}
fn parse_blocks(template: &str) -> Vec<Block> {
let mut blocks = Vec::new();
let mut in_block = false;
let mut idx_start = 0;
let mut line = 1;
let mut line_start = 1;
let mut col = 0;
let mut col_start = 0;
let mut chars = template.char_indices().peekable();
while let Some((idx, ch)) = chars.next() {
col += 1;
if ch == '\n' {
line += 1;
col = 0;
}
if in_block && ch == '}' {
match chars.peek() {
Some((next_idx, next_ch)) if *next_ch == '}' => {
blocks.push(Block {
start: idx_start,
len: next_idx - idx_start + 1,
line: line_start,
col: col_start,
variable_key: template[idx_start + 2..next_idx - 1].trim(),
});
in_block = false;
col += 1;
chars.next();
}
Some(_) => continue,
None => break,
};
} else if ch == '{' {
match chars.peek() {
Some((_, next_ch)) if *next_ch == '{' => {
idx_start = idx;
line_start = line;
col_start = col;
in_block = true;
col += 1;
chars.next();
}
Some(_) => continue,
None => break,
};
}
}
blocks
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_single_var() {
test_render_vars(
"testKey: testValue;",
"testKey: {{ testKey }};",
&[("testKey", "testValue")],
);
}
#[test]
fn render_multiple_vars() {
test_render_vars(
"testKey: testValue; testKey2: testValue2;",
"testKey: {{testKey}}; testKey2: {{ testKey2 }};",
&[("testKey", "testValue"), ("testKey2", "testValue2")],
);
}
#[test]
fn render_without_vars() {
test_render_vars(
"testKey: testValue; testKey2: testValue2;",
"testKey: testValue; testKey2: testValue2;",
&[],
);
}
#[test]
fn render_incorrect_vars() {
let input = "testKey: {{testKey}}; testValue2: {{ wrongKey }};";
let mut map = VarjMap::new();
map.insert("testKey", "testValue");
map.insert("testKey2", "testValue2");
let expected = Error {
line: 1,
col: 35,
key: "wrongKey".to_owned(),
};
let actual = map.render(input).expect_err("parsing should error");
assert_eq!(expected.line, actual.line);
assert_eq!(expected.col, actual.col);
assert_eq!(expected.key, actual.key);
let expected_error_msg = format!(
"{}:{} unknown variable '{}'",
expected.line, expected.col, expected.key
);
let actual_error_msg = format!("{}", actual);
assert_eq!(expected_error_msg, actual_error_msg);
}
#[test]
fn parse_single_block_with_whitespace() {
test_parsed_blocks(
"testKey: {{ testKey }};",
vec![Block {
start: 9,
len: 13,
line: 1,
col: 10,
variable_key: "testKey",
}],
);
}
#[test]
fn parse_single_block_without_whitespace() {
test_parsed_blocks(
"testKey: {{testKey}};",
vec![Block {
start: 9,
len: 11,
line: 1,
col: 10,
variable_key: "testKey",
}],
);
}
#[test]
fn parse_single_block_at_start() {
test_parsed_blocks(
"{{testKey}}: testKey",
vec![Block {
start: 0,
len: 11,
line: 1,
col: 1,
variable_key: "testKey",
}],
);
}
#[test]
fn parse_single_block_at_len() {
test_parsed_blocks(
"testKey: {{testKey}}",
vec![Block {
start: 9,
len: 11,
line: 1,
col: 10,
variable_key: "testKey",
}],
);
}
#[test]
fn parse_single_block_with_added_braces() {
test_parsed_blocks(
"test{Key: {{ test}Key }};",
vec![Block {
start: 10,
len: 14,
line: 1,
col: 11,
variable_key: "test}Key",
}],
);
}
#[test]
fn parse_multiple_blocks() {
test_parsed_blocks(
"testKey: {{testKey}}; testKey2: {{ testKey2 }};",
vec![
Block {
start: 9,
len: 11,
line: 1,
col: 10,
variable_key: "testKey",
},
Block {
start: 32,
len: 14,
line: 1,
col: 33,
variable_key: "testKey2",
},
],
);
}
#[test]
fn parse_multiple_blocks_on_multiple_lines() {
test_parsed_blocks(
"testKey: {{testKey}};\ntestKey2: {{ testKey2 }};",
vec![
Block {
start: 9,
len: 11,
line: 1,
col: 10,
variable_key: "testKey",
},
Block {
start: 32,
len: 14,
line: 2,
col: 11,
variable_key: "testKey2",
},
],
);
}
#[test]
fn from_hash_map() {
let (expected, hash_map) = matching_varj_and_hash_maps();
let actual = VarjMap::from(hash_map);
assert_eq!(expected, actual);
}
#[test]
fn into_hash_map() {
let (varj_map, expected) = matching_varj_and_hash_maps();
let actual = HashMap::from(varj_map);
assert_eq!(expected, actual);
}
fn test_render_vars(expected: &str, template: &str, vars: &[(&str, &str)]) {
let mut map = VarjMap::new();
for (k, v) in vars {
map.insert(*k, *v);
}
let actual = map.render(template).expect("rendering should succeed");
assert_eq!(expected, actual);
#[allow(deprecated)]
let actual = map.parse(template).expect("rendering should succeed");
assert_eq!(expected, actual);
}
fn test_parsed_blocks(input: &str, expected: Vec<Block>) {
let actual = parse_blocks(input);
for (idx, _block) in actual.iter().enumerate() {
assert_eq!(expected[idx], actual[idx]);
}
}
fn matching_varj_and_hash_maps() -> (VarjMap, HashMap<String, String>) {
let key1 = "testKey1";
let value1 = "testValue1";
let key2 = "testKey2";
let value2 = "testValue2";
let mut hash_map = HashMap::with_capacity(2);
hash_map.insert(key1.to_string(), value1.to_string());
hash_map.insert(key2.to_string(), value2.to_string());
let mut varj_map = VarjMap::with_capacity(2);
varj_map.insert(key1, value1);
varj_map.insert(key2, value2);
(varj_map, hash_map)
}
}