use std::error::Error;
use std::fmt::{self, Display};
#[derive(Debug)]
pub enum TemplateError {
MissingValueForPlaceholder(String),
NoPlaceholder(String),
}
impl Display for TemplateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TemplateError::MissingValueForPlaceholder(placeh) => write!(f, "template is missing value for placeholder {:?}", placeh),
TemplateError::NoPlaceholder(placeh) => write!(f, "template has no placeholder {:?}", placeh),
}
}
}
impl Error for TemplateError {}
pub trait PlaceholderValuePair<'k, 'v> {
fn get_placeholder(&self) -> &'k str;
fn get_value(&self) -> &'v str;
}
impl<'k, 'v> PlaceholderValuePair<'k, 'v> for (&'k str, &'v str) {
fn get_placeholder(&self) -> &'k str {
self.0
}
fn get_value(&self) -> &'v str {
self.1
}
}
impl<'k, 'v> PlaceholderValuePair<'k, 'v> for &(&'k str, &'v str) {
fn get_placeholder(&self) -> &'k str {
self.0
}
fn get_value(&self) -> &'v str {
self.1
}
}
impl<'t> PlaceholderValuePair<'t, 't> for [&'t str; 2] {
fn get_placeholder(&self) -> &'t str {
self[0]
}
fn get_value(&self) -> &'t str {
self[1]
}
}
impl<'t> PlaceholderValuePair<'t, 't> for &[&'t str; 2] {
fn get_placeholder(&self) -> &'t str {
self[0]
}
fn get_value(&self) -> &'t str {
self[1]
}
}
pub fn template<'k, 'v, T: PlaceholderValuePair<'k, 'v>, I: IntoIterator<Item = T>>(template: &str, values: I) -> Result<String, TemplateError> {
let mut vars = Vec::new();
let mut slices = Vec::new();
let mut from = 0;
let mut candidate = None;
for (i, c) in template.char_indices() {
match c {
'{' => candidate = Some(i),
'}' => if let Some(placeh) = candidate.take() {
slices.push(&template[from .. placeh]);
vars.push((&template[placeh + 1 .. i], None));
from = i + 1;
}
c => match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => (),
_ => candidate = None,
}
}
}
slices.push(&template[from .. template.len()]);
for t in values {
let (tplaceh, tvalue) = (t.get_placeholder(), t.get_value());
let mut found = false;
for &mut (placeh, ref mut value) in vars.iter_mut() {
if placeh == tplaceh {
*value = Some(tvalue);
found = true;
}
}
if !found {
return Err(TemplateError::NoPlaceholder(tplaceh.to_owned()))
}
}
let mut out = String::new();
let len = slices.len();
for (i, s) in slices.into_iter().enumerate() {
out.push_str(s);
if i < len - 1 {
let slot = &vars[i];
let value = slot.1
.as_ref()
.ok_or_else(|| TemplateError::MissingValueForPlaceholder(slot.0.to_owned()))?;
out.push_str(value);
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_template_empty() {
assert_eq!(&template::<&(&str, &str), _>("", None).unwrap(), "");
}
#[test]
fn test_template_none() {
assert_eq!(&template::<&(&str, &str), _>("hello world", None).unwrap(), "hello world");
}
#[test]
fn test_template_single() {
assert_eq!(&template("hello {foo} world", Some(("foo", "bar"))).unwrap(), "hello bar world");
}
#[test]
fn test_template_single_variable_multi() {
assert_eq!(&template("{foo} hello {foo} world {foo}", Some(("foo", "bar"))).unwrap(), "bar hello bar world bar");
}
#[test]
fn test_template_many_vars_slice() {
assert_eq!(&template("{foo} hello {bar} world {baz}", &[("foo", "1"), ("bar", "2"), ("baz" ,"3")]).unwrap(), "1 hello 2 world 3");
}
#[test]
fn test_template_many_vars_slice_array() {
assert_eq!(&template("{foo} hello {bar} world {baz}", &[["foo", "1"], ["bar", "2"], ["baz" ,"3"]]).unwrap(), "1 hello 2 world 3");
}
#[test]
fn test_template_many_vars() {
assert_eq!(&template("{foo} hello {bar} world {baz}", vec![("foo", "1"), ("bar", "2"), ("baz" ,"3")]).unwrap(), "1 hello 2 world 3");
}
#[test]
fn test_template_nested_reverse() {
assert_eq!(&template("}}}{foo}{{ hello }}}{bar}{{{ world }}{baz}{{{", vec![("foo", "1"), ("bar", "2"), ("baz" ,"3")]).unwrap(), "}}}1{{ hello }}}2{{{ world }}3{{{");
}
#[test]
fn test_template_nested() {
assert_eq!(&template("{{{{foo}}} hello {{{{bar}}}} world {{{baz}}}}", vec![("foo", "1"), ("bar", "2"), ("baz" ,"3")]).unwrap(), "{{{1}} hello {{{2}}} world {{3}}}");
}
#[test]
fn test_template_json() {
let tpl = r#"{"menu": {
"id": "file",
"value": {File},
"{Popup}": {
"menuitem": [
{"value": "{New}", "onclick": "CreateNewDoc()"},
{"value": "{Open}", "onclick": "OpenDoc()"},
{"value": "{Close}", "onclick": "CloseDoc()"}
]
}
}}"#;
let out = r#"{"menu": {
"id": "file",
"value": "File",
"popup": {
"menuitem": [
{"value": "New", "onclick": "CreateNewDoc()"},
{"value": "Open", "onclick": "OpenDoc()"},
{"value": "Close", "onclick": "CloseDoc()"}
]
}
}}"#;
assert_eq!(&template(tpl, vec![("File", "\"File\""), ("Popup", "popup"), ("New", "New"), ("Open" ,"Open"), ("Close", "Close")]).unwrap(), out);
}
#[test]
fn test_template_many_vars_space() {
assert_eq!(&template("{foo} hello {foo bar} world {baz}", vec![("foo", "1"), ("baz" ,"3")]).unwrap(), "1 hello {foo bar} world 3");
assert_eq!(&template("{foo.bar} hello {foo+bar} world {foo_baz}", vec![("foo.bar", "1"), ("foo_baz" ,"3")]).unwrap(), "1 hello {foo+bar} world 3");
}
#[test]
#[should_panic(expected = "template is missing value for placeholder \\\"foo\\\"")]
fn test_template_missing_placeholder() {
template::<&(&str, &str), _>("{foo} hello {foo} world {foo}", &[]).map_err(|e| e.to_string()).unwrap();
}
#[test]
#[should_panic(expected = "template has no placeholder \\\"bar\\\"")]
fn test_template_no_placeholder() {
template("{foo} hello {foo} world {foo}", vec![("foo", "1"), ("bar", "2")]).map_err(|e| e.to_string()).unwrap();
}
}