1use std::collections::HashMap;
10
11use serde::Serialize;
12use tera::{Context, Tera, Value};
13
14use crate::Result;
15use crate::vars::YuiVars;
16
17pub struct Engine {
18 tera: Tera,
19}
20
21impl Engine {
22 pub fn new() -> Self {
23 let mut tera = Tera::default();
24 tera.register_function("env", env_fn);
25 Self { tera }
26 }
27
28 pub fn render(&mut self, src: &str, ctx: &Context) -> Result<String> {
29 self.tera
30 .render_str(src, ctx)
31 .map_err(|e| crate::Error::Template(format!("{e:#}")))
32 }
33}
34
35impl Default for Engine {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41fn env_fn(args: &HashMap<String, Value>) -> tera::Result<Value> {
45 let name = args
46 .get("name")
47 .and_then(|v| v.as_str())
48 .ok_or_else(|| tera::Error::msg("env(name=…): missing or non-string 'name'"))?;
49 let default = args.get("default").cloned();
50 match std::env::var(name) {
51 Ok(v) => Ok(Value::String(v)),
52 Err(_) => Ok(default.unwrap_or_else(|| Value::String(String::new()))),
53 }
54}
55
56pub fn config_context(yui: &YuiVars) -> Context {
57 let mut ctx = Context::new();
58 ctx.insert("yui", yui);
59 ctx
60}
61
62pub fn template_context<V: Serialize>(yui: &YuiVars, vars: &V) -> Context {
63 let mut ctx = Context::new();
64 ctx.insert("yui", yui);
65 ctx.insert("vars", vars);
66 ctx
67}
68
69pub fn eval_truthy(expr: &str, engine: &mut Engine, ctx: &Context) -> Result<bool> {
74 let trimmed = expr.trim_start();
75 let to_render = if trimmed.starts_with("{{") || trimmed.starts_with("{%") {
76 expr.to_string()
77 } else {
78 format!("{{{{ {expr} }}}}")
79 };
80 let out = engine.render(&to_render, ctx)?;
81 let s = out.trim();
82 Ok(s.eq_ignore_ascii_case("true") || s == "1")
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use camino::Utf8Path;
89
90 fn vars() -> YuiVars {
91 YuiVars {
92 os: "linux".into(),
93 arch: "x86_64".into(),
94 host: "test".into(),
95 user: "u".into(),
96 source: "/dotfiles".into(),
97 }
98 }
99
100 #[test]
101 fn renders_yui_vars() {
102 let mut e = Engine::new();
103 let ctx = config_context(&vars());
104 let out = e
105 .render("os={{ yui.os }}, arch={{ yui.arch }}", &ctx)
106 .unwrap();
107 assert_eq!(out, "os=linux, arch=x86_64");
108 }
109
110 #[test]
111 fn env_function_with_default() {
112 unsafe { std::env::remove_var("YUI_TEST_UNSET_VAR") };
114 let mut e = Engine::new();
115 let ctx = config_context(&vars());
116 let out = e
117 .render(
118 "{{ env(name='YUI_TEST_UNSET_VAR', default='fallback') }}",
119 &ctx,
120 )
121 .unwrap();
122 assert_eq!(out, "fallback");
123 }
124
125 #[test]
126 fn boolean_expression_renders_to_true_or_false() {
127 let mut e = Engine::new();
128 let ctx = config_context(&vars());
129 let out = e.render("{{ yui.os == 'linux' }}", &ctx).unwrap();
130 assert_eq!(out, "true");
131 let out = e.render("{{ yui.os == 'windows' }}", &ctx).unwrap();
132 assert_eq!(out, "false");
133 }
134
135 #[test]
136 fn template_context_exposes_user_vars() {
137 let mut e = Engine::new();
138 let mut user_vars = toml::Table::new();
139 user_vars.insert("greet".into(), toml::Value::String("hi".into()));
140 let ctx = template_context(&vars(), &user_vars);
141 let out = e.render("{{ vars.greet }} {{ yui.user }}", &ctx).unwrap();
142 assert_eq!(out, "hi u");
143 let _: &Utf8Path = Utf8Path::new(".");
145 }
146}