use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplateError {
UnknownField(String),
MalformedDirective,
IncludeNotFound(String),
}
impl fmt::Display for TemplateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownField(name) => write!(f, "unknown template field: {name}"),
Self::MalformedDirective => write!(f, "malformed template directive"),
Self::IncludeNotFound(name) => write!(f, "included template not found: {name}"),
}
}
}
impl std::error::Error for TemplateError {}
#[derive(Debug, Default, Clone)]
pub struct TemplateContext {
pub scalars: HashMap<&'static str, String>,
pub lists: HashMap<&'static str, Vec<TemplateContext>>,
pub conditionals: HashMap<&'static str, bool>,
}
impl TemplateContext {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn with_scalar(mut self, key: &'static str, value: impl Into<String>) -> Self {
self.scalars.insert(key, value.into());
self
}
#[inline]
pub fn with_list(mut self, key: &'static str, value: Vec<TemplateContext>) -> Self {
self.lists.insert(key, value);
self
}
#[inline]
pub fn with_conditional(mut self, key: &'static str, value: bool) -> Self {
self.conditionals.insert(key, value);
self
}
}
#[inline]
pub fn render(template_src: &str, context: &TemplateContext) -> Result<String, TemplateError> {
let mut out = String::with_capacity(template_src.len() * 2);
render_inner(template_src, context, &mut out)?;
Ok(out)
}
#[inline]
pub fn rust_test_template(name: &str) -> Result<&'static str, TemplateError> {
match name {
"law" => Ok(include_str!("law.mustache")),
"spec_table" => Ok(include_str!("spec_table.mustache")),
"backend_equiv" => Ok(include_str!("backend_equiv.mustache")),
"point_parity" => Ok(include_str!("point_parity.mustache")),
"validation" => Ok(include_str!("validation.mustache")),
"mutation_kill" => Ok(include_str!("mutation_kill.mustache")),
_ => Err(TemplateError::IncludeNotFound(name.to_string())),
}
}
fn resolve_include(name: &str) -> Option<&'static str> {
match name {
"preamble" => Some(include_str!("preamble.tmpl")),
"op_correctness" => Some(include_str!("op_correctness.tmpl")),
"law" => Some(include_str!("law.tmpl")),
"validation" => Some(include_str!("validation.tmpl")),
"backend_equiv" => Some(include_str!("backend_equiv.tmpl")),
"archetype" => Some(include_str!("archetype.tmpl")),
"mutation_kill" => Some(include_str!("mutation_kill.tmpl")),
_ => None,
}
}
fn find_block_end(src: &str, open_prefix: &str, close_tag: &str) -> Result<usize, TemplateError> {
let mut depth = 1;
let mut i = 0;
while i < src.len() {
if src[i..].starts_with(open_prefix) {
depth += 1;
i += open_prefix.len();
} else if src[i..].starts_with(close_tag) {
depth -= 1;
if depth == 0 {
return Ok(i);
}
i += close_tag.len();
} else {
i += 1;
}
}
Err(TemplateError::MalformedDirective)
}
fn render_inner(src: &str, ctx: &TemplateContext, out: &mut String) -> Result<(), TemplateError> {
let mut i = 0;
while i < src.len() {
match src[i..].find('{') {
Some(pos) => {
out.push_str(&src[i..i + pos]);
i += pos;
let end = src[i..]
.find('}')
.ok_or(TemplateError::MalformedDirective)?;
let directive = &src[i + 1..i + end];
i += end + 1;
if let Some(name) = directive.strip_prefix("list:") {
let body_end = find_block_end(&src[i..], "{list:", "{/list}")?;
let body = &src[i..i + body_end];
i += body_end + "{/list}".len();
let items = ctx
.lists
.get(name)
.ok_or_else(|| TemplateError::UnknownField(name.to_string()))?;
for item in items {
render_inner(body, item, out)?;
}
} else if let Some(name) = directive.strip_prefix("if:") {
let body_end = find_block_end(&src[i..], "{if:", "{/if}")?;
let body = &src[i..i + body_end];
i += body_end + "{/if}".len();
if ctx.conditionals.get(name).copied().unwrap_or(false) {
render_inner(body, ctx, out)?;
}
} else if let Some(name) = directive.strip_prefix("include:") {
let included = resolve_include(name)
.ok_or_else(|| TemplateError::IncludeNotFound(name.to_string()))?;
render_inner(included, ctx, out)?;
} else {
let val = ctx
.scalars
.get(directive)
.ok_or_else(|| TemplateError::UnknownField(directive.to_string()))?;
out.push_str(val);
}
}
None => {
out.push_str(&src[i..]);
break;
}
}
}
Ok(())
}