use minijinja::{Environment, Value};
use std::collections::HashMap;
use std::sync::Mutex;
use crate::context::Context;
use crate::error::Result;
pub struct TemplateEngine {
env: Mutex<Environment<'static>>,
template_names: Mutex<HashMap<String, String>>,
env_snapshot: HashMap<String, Value>,
}
impl TemplateEngine {
pub fn new() -> Self {
let mut env = Environment::new();
env.set_undefined_behavior(minijinja::UndefinedBehavior::Strict);
let env_snapshot = std::env::vars()
.map(|(key, val)| (key, Value::from(val)))
.collect();
Self {
env: Mutex::new(env),
template_names: Mutex::new(HashMap::new()),
env_snapshot,
}
}
pub fn render(&self, template: &str, ctx: &Context) -> Result<String> {
let vars = Value::from(self.build_template_vars(ctx));
let template_name = self.get_or_add_template_name(template)?;
let env = self
.env
.lock()
.expect("template environment mutex poisoned");
let tmpl = env
.get_template(&template_name)
.map_err(crate::error::Error::Template)?;
let rendered = tmpl.render(&vars).map_err(crate::error::Error::Template)?;
Ok(rendered)
}
fn build_template_vars(&self, ctx: &Context) -> HashMap<String, Value> {
let mut vars = HashMap::new();
let mut job_map = HashMap::new();
job_map.insert("id".to_string(), Value::from(ctx.job.id.clone()));
job_map.insert("source".to_string(), Value::from(ctx.job.source.clone()));
let payload = serde_json_to_value(&ctx.job.payload);
job_map.insert("payload".to_string(), payload);
let mut labels_map = HashMap::new();
for (k, v) in &ctx.job.labels {
labels_map.insert(k.clone(), Value::from(v.clone()));
}
job_map.insert("labels".to_string(), Value::from(labels_map));
vars.insert("job".to_string(), Value::from(job_map));
let mut steps_map = HashMap::new();
for (step_id, result) in &ctx.steps {
let mut step_map = HashMap::new();
step_map.insert(
"stdout".to_string(),
Value::from(result.stdout.clone().unwrap_or_default()),
);
step_map.insert(
"stderr".to_string(),
Value::from(result.stderr.clone().unwrap_or_default()),
);
step_map.insert(
"exit_code".to_string(),
Value::from(i64::from(result.exit_code)),
);
let mut outputs_map = HashMap::new();
for (name, val) in &result.outputs {
outputs_map.insert(name.clone(), serde_json_to_value(val));
}
step_map.insert("outputs".to_string(), Value::from(outputs_map));
steps_map.insert(step_id.clone(), Value::from(step_map));
}
vars.insert("steps".to_string(), Value::from(steps_map));
vars.insert("env".to_string(), Value::from(self.env_snapshot.clone()));
let now = chrono::Utc::now();
vars.insert("now".to_string(), Value::from(now.to_rfc3339()));
vars.insert("now_unix".to_string(), Value::from(now.timestamp()));
vars
}
pub fn render_result(
&self,
template: &str,
ctx: &Context,
pipeline_id: &str,
result_status: &str,
duration_ms: u64,
occurred_at: &str,
) -> Result<String> {
let mut vars = self.build_template_vars(ctx);
let mut pipeline_map = HashMap::new();
pipeline_map.insert("id".to_string(), Value::from(pipeline_id.to_string()));
vars.insert("pipeline".to_string(), Value::from(pipeline_map));
let mut result_map = HashMap::new();
result_map.insert("status".to_string(), Value::from(result_status.to_string()));
result_map.insert("duration_ms".to_string(), Value::from(duration_ms));
vars.insert("result".to_string(), Value::from(result_map));
vars.insert(
"occurred_at".to_string(),
Value::from(occurred_at.to_string()),
);
let vars = Value::from(vars);
let template_name = self.get_or_add_template_name(template)?;
let env = self
.env
.lock()
.expect("template environment mutex poisoned");
let tmpl = env
.get_template(&template_name)
.map_err(crate::error::Error::Template)?;
let rendered = tmpl.render(&vars).map_err(crate::error::Error::Template)?;
Ok(rendered)
}
fn get_or_add_template_name(&self, template: &str) -> Result<String> {
if let Some(name) = self
.template_names
.lock()
.expect("template-name cache mutex poisoned")
.get(template)
.cloned()
{
return Ok(name);
}
let mut names = self
.template_names
.lock()
.expect("template-name cache mutex poisoned");
if let Some(name) = names.get(template).cloned() {
return Ok(name);
}
let name = format!("inline:{}", names.len());
self.env
.lock()
.expect("template environment mutex poisoned")
.add_template_owned(name.clone(), template.to_string())
.map_err(crate::error::Error::Template)?;
names.insert(template.to_string(), name.clone());
Ok(name)
}
}
impl Default for TemplateEngine {
fn default() -> Self {
Self::new()
}
}
fn serde_json_to_value(json: &serde_json::Value) -> Value {
match json {
serde_json::Value::Null => Value::UNDEFINED,
serde_json::Value::Bool(b) => Value::from(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Value::from(i)
} else if let Some(f) = n.as_f64() {
Value::from(f)
} else {
Value::from(n.to_string())
}
}
serde_json::Value::String(s) => Value::from(s.clone()),
serde_json::Value::Array(arr) => {
let vals: Vec<Value> = arr.iter().map(serde_json_to_value).collect();
Value::from(vals)
}
serde_json::Value::Object(obj) => {
let mut map = HashMap::new();
for (k, v) in obj {
map.insert(k.clone(), serde_json_to_value(v));
}
Value::from(map)
}
}
}