use hcl::expr::{Expression, TemplateExpr};
use super::escape::{quoted_inside, strings_escaped};
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Template {
parts: Vec<Part>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Piece<'a> {
Text(&'a str),
Expr(&'a Expression),
}
#[derive(Debug, Clone, PartialEq)]
enum Part {
Text(String),
Expr(Expression),
}
impl Template {
#[must_use]
pub fn new() -> Template {
Template::default()
}
#[must_use]
pub fn text(text: impl Into<String>) -> Template {
let mut t = Template::new();
t.push_text(text);
t
}
#[must_use]
pub fn with_text(mut self, text: impl Into<String>) -> Template {
self.push_text(text);
self
}
#[must_use]
pub fn with_expr(mut self, expr: Expression) -> Template {
self.push_expr(expr);
self
}
pub fn push_text(&mut self, text: impl Into<String>) {
let text = text.into();
match self.parts.last_mut() {
Some(Part::Text(t)) => t.push_str(&text),
_ if text.is_empty() => {}
_ => self.parts.push(Part::Text(text)),
}
}
pub fn push_expr(&mut self, expr: Expression) {
self.parts.push(Part::Expr(expr));
}
pub fn append(&mut self, other: Template) {
for p in other.parts {
match p {
Part::Text(t) => self.push_text(t),
Part::Expr(e) => self.push_expr(e),
}
}
}
pub fn pieces(&self) -> impl Iterator<Item = Piece<'_>> {
self.parts.iter().map(|p| match p {
Part::Text(t) => Piece::Text(t),
Part::Expr(e) => Piece::Expr(e),
})
}
#[must_use]
pub fn is_text(&self) -> bool {
self.parts.iter().all(|p| matches!(p, Part::Text(_)))
}
#[must_use]
pub fn into_expression(mut self) -> Expression {
if let [Part::Expr(_)] = self.parts.as_slice()
&& let Some(Part::Expr(e)) = self.parts.pop()
{
return e;
}
if self.is_text() {
let text: String = self
.parts
.into_iter()
.filter_map(|p| match p {
Part::Text(t) => Some(t),
Part::Expr(_) => None,
})
.collect();
return Expression::String(text);
}
let next = self.parts.iter().skip(1).map(Some).chain([None]);
let raw = self
.parts
.iter()
.zip(next)
.map(|(p, after)| match p {
Part::Text(t) => {
let text = quoted_inside(t);
if let Some(head) = text.strip_suffix('$').filter(|_| matches!(after, Some(Part::Expr(_)))) {
format!("{head}\\u0024")
} else {
text
}
}
Part::Expr(e) => format!("${{{}}}", strings_escaped(e)),
})
.collect::<Vec<_>>()
.concat();
Expression::TemplateExpr(Box::new(TemplateExpr::QuotedString(raw)))
}
}
impl From<&str> for Template {
fn from(text: &str) -> Template {
Template::text(text)
}
}
impl From<String> for Template {
fn from(text: String) -> Template {
Template::text(text)
}
}
impl From<Template> for Expression {
fn from(t: Template) -> Expression {
t.into_expression()
}
}
#[cfg(test)]
mod tests {
use super::{Piece, Template};
use crate::hcl_builder::reference;
#[test]
fn text_is_escaped_and_expressions_interpolated() {
let t = Template::text("echo \"${HOME}\" %{x}\n").with_expr(reference("var.slot")).with_text("!");
assert_eq!(t.into_expression().to_string(), r#""echo \"$${HOME}\" %%{x}\n${var.slot}!""#);
}
#[test]
fn adjacent_text_is_one_piece_and_empty_text_none() {
let t = Template::text("a").with_text("b").with_text("").with_expr(reference("var.x")).with_text("");
let pieces: Vec<Piece<'_>> = t.pieces().collect();
assert_eq!(pieces, [Piece::Text("ab"), Piece::Expr(&reference("var.x"))]);
assert_eq!(Template::new().with_text("").pieces().count(), 0);
assert!(!t.is_text() && Template::text("x").is_text());
}
#[test]
fn a_dollar_before_an_interpolation_stays_a_dollar() {
let t = Template::text("cost: $").with_expr(reference("var.price"));
assert_eq!(t.into_expression().to_string(), r#""cost: \u0024${var.price}""#);
}
#[test]
fn appending_keeps_text_and_expressions_in_order() {
let mut t = Template::text("a");
t.append(Template::text("b").with_expr(reference("var.y")).with_text("c"));
assert_eq!(t.into_expression().to_string(), r#""ab${var.y}c""#);
}
#[test]
fn text_converts_into_a_template_of_it() {
assert_eq!(Template::from("a").into_expression(), hcl::Expression::String("a".into()));
assert_eq!(Template::from(String::from("b")).into_expression(), hcl::Expression::String("b".into()));
assert_eq!(hcl::Expression::from(Template::text("c")), hcl::Expression::String("c".into()));
}
#[test]
fn a_lone_interpolation_is_its_expression_not_a_string_around_it() {
assert_eq!(Template::new().with_expr(reference("var.x")).into_expression(), reference("var.x"));
assert_eq!(
Template::new().with_expr(reference("var.x")).with_text("").into_expression(),
reference("var.x")
);
assert_eq!(
Template::text(" ").with_expr(reference("var.x")).into_expression().to_string(),
r#"" ${var.x}""#
);
}
#[test]
fn plain_text_is_a_string() {
assert_eq!(Template::text("a").with_text("b").into_expression(), hcl::Expression::String("ab".into()));
}
}