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
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use camino::Utf8Path;
73
74 fn vars() -> YuiVars {
75 YuiVars {
76 os: "linux".into(),
77 arch: "x86_64".into(),
78 host: "test".into(),
79 user: "u".into(),
80 source: "/dotfiles".into(),
81 }
82 }
83
84 #[test]
85 fn renders_yui_vars() {
86 let mut e = Engine::new();
87 let ctx = config_context(&vars());
88 let out = e
89 .render("os={{ yui.os }}, arch={{ yui.arch }}", &ctx)
90 .unwrap();
91 assert_eq!(out, "os=linux, arch=x86_64");
92 }
93
94 #[test]
95 fn env_function_with_default() {
96 unsafe { std::env::remove_var("YUI_TEST_UNSET_VAR") };
98 let mut e = Engine::new();
99 let ctx = config_context(&vars());
100 let out = e
101 .render(
102 "{{ env(name='YUI_TEST_UNSET_VAR', default='fallback') }}",
103 &ctx,
104 )
105 .unwrap();
106 assert_eq!(out, "fallback");
107 }
108
109 #[test]
110 fn boolean_expression_renders_to_true_or_false() {
111 let mut e = Engine::new();
112 let ctx = config_context(&vars());
113 let out = e.render("{{ yui.os == 'linux' }}", &ctx).unwrap();
114 assert_eq!(out, "true");
115 let out = e.render("{{ yui.os == 'windows' }}", &ctx).unwrap();
116 assert_eq!(out, "false");
117 }
118
119 #[test]
120 fn template_context_exposes_user_vars() {
121 let mut e = Engine::new();
122 let mut user_vars = toml::Table::new();
123 user_vars.insert("greet".into(), toml::Value::String("hi".into()));
124 let ctx = template_context(&vars(), &user_vars);
125 let out = e.render("{{ vars.greet }} {{ yui.user }}", &ctx).unwrap();
126 assert_eq!(out, "hi u");
127 let _: &Utf8Path = Utf8Path::new(".");
129 }
130}