use anyhow::{anyhow, bail};
use serde_with::DeserializeFromStr;
use crate::config::property_key::PropertyKey;
#[derive(Debug, DeserializeFromStr)]
#[cfg_attr(test, derive(PartialEq))]
pub struct NameTemplate {
parts: Vec<Part>,
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
enum Part {
Literal(String),
Tag(PropertyKey),
}
impl std::str::FromStr for NameTemplate {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse_string(s)
}
}
impl NameTemplate {
fn parse_string(s: &str) -> Result<Self, anyhow::Error> {
let mut parts = Vec::new();
let mut chars = s.chars().peekable();
let mut current_part = String::new();
while let Some(ch) = chars.next() {
match ch {
'{' => {
if chars.peek() == Some(&'{') {
current_part.push('{');
chars.next(); continue;
} else {
if !current_part.is_empty() {
parts.push(Part::Literal(current_part));
current_part = String::new();
}
let tag_content = Self::parse_tag(&mut chars)?;
let property_key = tag_content
.parse::<PropertyKey>()
.map_err(|_| {
anyhow!("\"{}\" is not implemented", tag_content)
})?;
parts.push(Part::Tag(property_key));
}
}
'}' => {
if chars.peek() == Some(&'}') {
current_part.push('}');
chars.next(); } else {
bail!("'}}' without '{{'");
}
}
_ => current_part.push(ch),
}
}
if !current_part.is_empty() {
parts.push(Part::Literal(current_part));
}
Ok(NameTemplate { parts })
}
fn parse_tag(
chars: &mut std::iter::Peekable<std::str::Chars>,
) -> Result<String, anyhow::Error> {
let mut content = String::new();
for ch in chars.by_ref() {
match ch {
'}' => {
return Ok(content);
}
'{' => bail!("'{{' without '}}'"),
_ => content.push(ch),
}
}
Err(anyhow!("'{{' without '}}'"))
}
pub fn render<T: AsRef<str>>(
&self,
lookup: impl Fn(&PropertyKey) -> Option<T>,
) -> Option<String> {
let mut result = String::new();
for part in &self.parts {
match part {
Part::Literal(literal) => result.push_str(literal),
Part::Tag(property_key) => {
result.push_str(lookup(property_key)?.as_ref())
}
}
}
Some(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_tags() {
let s = String::from("Hello");
let template: Result<NameTemplate, _> = s.parse();
assert!(template.is_ok());
assert_eq!(
template.unwrap(),
NameTemplate {
parts: vec![Part::Literal(s.clone())],
}
);
}
#[test]
fn good_tag() {
let s = String::from("Hello {node:node.name}");
let template: Result<NameTemplate, _> = s.parse();
assert!(template.is_ok());
assert_eq!(
template.unwrap(),
NameTemplate {
parts: vec![
Part::Literal(String::from("Hello ")),
Part::Tag(PropertyKey::Node(String::from("node.name"))),
],
}
);
}
#[test]
fn escapes() {
let s = String::from("Hello }} {{ {{ {node:node.name} }}");
let template: Result<NameTemplate, _> = s.parse();
assert!(template.is_ok());
assert_eq!(
template.unwrap(),
NameTemplate {
parts: vec![
Part::Literal(String::from("Hello } { { ")),
Part::Tag(PropertyKey::Node(String::from("node.name"))),
Part::Literal(String::from(" }")),
],
}
);
}
#[test]
fn extra_opening() {
let s = String::from("Hello { {node:node.name}}");
let template: Result<NameTemplate, _> = s.parse();
assert!(template.is_err());
}
#[test]
fn extra_closing() {
let s = String::from("Hello {node:node.name}}");
let template: Result<NameTemplate, _> = s.parse();
assert!(template.is_err());
}
#[test]
fn empty_tag() {
let s = String::from("Hello {}");
let template: Result<NameTemplate, _> = s.parse();
assert!(template.is_err());
}
#[test]
fn nested_escapes() {
let s = String::from("Hello {{{{}}}}");
let template: Result<NameTemplate, _> = s.parse();
assert!(template.is_ok());
assert_eq!(
template.unwrap(),
NameTemplate {
parts: vec![Part::Literal(String::from("Hello {{}}")),],
}
);
}
#[test]
fn render_empty() {
let s = String::from("");
let template: Result<NameTemplate, _> = s.parse();
assert!(template.is_ok());
let rendered = template.unwrap().render(|_| None::<&str>);
assert_eq!(rendered, Some(s));
}
#[test]
fn render_tags() {
let s = String::from("{node:node.name}{device:device.name}");
let template: Result<NameTemplate, _> = s.parse();
assert!(template.is_ok());
let rendered = template.unwrap().render(|tag| match tag {
PropertyKey::Node(ref s) if s == "node.name" => {
Some(String::from("foo"))
}
PropertyKey::Device(ref s) if s == "device.name" => {
Some(String::from("bar"))
}
_ => None,
});
assert_eq!(rendered, Some(String::from("foobar")));
}
#[test]
fn render_missing_tag() {
let s = String::from("{node:node.name}{device:device.name}");
let template: Result<NameTemplate, _> = s.parse();
assert!(template.is_ok());
let rendered = template.unwrap().render(|tag| match tag {
PropertyKey::Node(ref s) if s == "node.name" => {
Some(String::from("foo"))
}
_ => None,
});
assert_eq!(rendered, None)
}
#[test]
fn render_mixed() {
let s = String::from("let {node:node.name} = {device:device.name};");
let template: Result<NameTemplate, _> = s.parse();
assert!(template.is_ok());
let rendered = template.unwrap().render(|tag| match tag {
PropertyKey::Node(ref s) if s == "node.name" => {
Some(String::from("foo"))
}
PropertyKey::Device(ref s) if s == "device.name" => {
Some(String::from("bar"))
}
_ => None,
});
assert_eq!(rendered, Some(String::from("let foo = bar;")));
}
}