1use crate::secrets::{SecretAccessResult, SecretAction, evaluate};
2use crate::spec::form::{FormSpec, SecretsPolicy};
3use handlebars::{
4 Context, Handlebars, Helper, HelperResult, Output, RenderContext, RenderError,
5 RenderErrorReason,
6};
7use serde_json::{Map, Value};
8use thiserror::Error;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ResolutionMode {
13 Strict,
15 Relaxed,
17}
18
19#[derive(Debug, Clone)]
21pub struct TemplateContext {
22 pub payload: Value,
23 pub state: Value,
24 pub config: Value,
25 pub answers: Value,
26 pub secrets: Option<SecretsContext>,
27}
28
29impl Default for TemplateContext {
30 fn default() -> Self {
31 let empty = Value::Object(Map::new());
32 Self {
33 payload: empty.clone(),
34 state: empty.clone(),
35 config: empty.clone(),
36 answers: empty,
37 secrets: None,
38 }
39 }
40}
41
42impl TemplateContext {
43 pub fn with_payload(mut self, payload: Value) -> Self {
45 self.payload = payload;
46 self
47 }
48
49 pub fn with_state(mut self, state: Value) -> Self {
51 self.state = state;
52 self
53 }
54
55 pub fn with_config(mut self, config: Value) -> Self {
57 self.config = config;
58 self
59 }
60
61 pub fn with_answers(mut self, answers: Value) -> Self {
63 self.answers = answers;
64 self
65 }
66
67 pub fn with_secrets(
69 mut self,
70 secrets: Value,
71 policy: Option<SecretsPolicy>,
72 host_available: bool,
73 ) -> Self {
74 self.secrets = Some(SecretsContext::new(secrets, policy, host_available));
75 self
76 }
77
78 fn to_value(&self) -> Value {
79 let mut map = Map::new();
80 map.insert("payload".into(), self.payload.clone());
81 map.insert("state".into(), self.state.clone());
82 map.insert("config".into(), self.config.clone());
83 map.insert("answers".into(), self.answers.clone());
84 if let Some(secrets) = &self.secrets {
85 map.insert("secrets".into(), secrets.value());
86 map.insert("__secrets_meta".into(), secrets.meta());
87 }
88 Value::Object(map)
89 }
90}
91
92fn render_error(message: impl Into<String>) -> RenderError {
93 RenderErrorReason::Other(message.into()).into()
94}
95
96#[derive(Debug, Clone)]
97pub struct SecretsContext {
98 values: Map<String, Value>,
99 denied: Map<String, Value>,
100 host_available: bool,
101}
102
103impl SecretsContext {
104 fn new(secrets: Value, policy: Option<SecretsPolicy>, host_available: bool) -> Self {
105 let mut values = Map::new();
106 let mut denied = Map::new();
107
108 if let Some(map) = secrets.as_object() {
109 for (key, value) in map {
110 match evaluate(policy.as_ref(), key, SecretAction::Read, host_available) {
111 SecretAccessResult::Allowed => {
112 values.insert(key.clone(), value.clone());
113 }
114 SecretAccessResult::Denied(code) => {
115 denied.insert(key.clone(), Value::String(code.into()));
116 }
117 SecretAccessResult::HostUnavailable => {
118 denied.insert(key.clone(), Value::String("secret_host_unavailable".into()));
119 }
120 }
121 }
122 }
123
124 Self {
125 values,
126 denied,
127 host_available,
128 }
129 }
130
131 fn value(&self) -> Value {
132 Value::Object(self.values.clone())
133 }
134
135 fn meta(&self) -> Value {
136 let mut meta = Map::new();
137 meta.insert("host_available".into(), Value::Bool(self.host_available));
138 meta.insert("denied".into(), Value::Object(self.denied.clone()));
139 Value::Object(meta)
140 }
141}
142
143#[derive(Debug, Error)]
145pub enum TemplateError {
146 #[error("template render error: {0}")]
147 Render(String),
148}
149
150pub struct TemplateEngine {
152 handlebars: Handlebars<'static>,
153 mode: ResolutionMode,
154}
155
156impl TemplateEngine {
157 pub fn new(mode: ResolutionMode) -> Self {
159 let mut handlebars = Handlebars::new();
160 register_default_helpers(&mut handlebars);
161 handlebars.set_strict_mode(true);
162 Self { handlebars, mode }
163 }
164
165 pub fn resolve_string(
167 &self,
168 template: &str,
169 ctx: &TemplateContext,
170 ) -> Result<String, TemplateError> {
171 match self.handlebars.render_template(template, &ctx.to_value()) {
172 Ok(result) => Ok(result),
173 Err(err) => match self.mode {
174 ResolutionMode::Relaxed => Ok(template.to_owned()),
175 ResolutionMode::Strict => Err(TemplateError::Render(err.to_string())),
176 },
177 }
178 }
179
180 pub fn resolve_form_spec(
182 &self,
183 spec: &FormSpec,
184 ctx: &TemplateContext,
185 ) -> Result<FormSpec, TemplateError> {
186 let mut resolved = spec.clone();
187 resolved.title = self.resolve_string(&spec.title, ctx)?;
188 resolved.description = spec
189 .description
190 .as_ref()
191 .map(|value| self.resolve_string(value, ctx))
192 .transpose()?;
193
194 resolved.presentation = if let Some(presentation) = &spec.presentation {
195 let mut next = presentation.clone();
196 next.intro = presentation
197 .intro
198 .as_ref()
199 .map(|value| self.resolve_string(value, ctx))
200 .transpose()?;
201 next.theme = presentation
202 .theme
203 .as_ref()
204 .map(|value| self.resolve_string(value, ctx))
205 .transpose()?;
206 Some(next)
207 } else {
208 None
209 };
210
211 resolved.questions = spec
212 .questions
213 .iter()
214 .map(|question| {
215 let mut updated = question.clone();
216 updated.title = self.resolve_string(&question.title, ctx)?;
217 updated.description = question
218 .description
219 .as_ref()
220 .map(|value| self.resolve_string(value, ctx))
221 .transpose()?;
222 updated.default_value = question
223 .default_value
224 .as_ref()
225 .map(|value| self.resolve_string(value, ctx))
226 .transpose()?;
227 Ok(updated)
228 })
229 .collect::<Result<Vec<_>, TemplateError>>()?;
230
231 Ok(resolved)
232 }
233}
234
235pub fn register_default_helpers(handlebars: &mut Handlebars<'static>) {
236 handlebars.register_helper("get", Box::new(helper_get));
237 handlebars.register_helper("default", Box::new(helper_default));
238 handlebars.register_helper("eq", Box::new(helper_eq));
239 handlebars.register_helper("and", Box::new(helper_and));
240 handlebars.register_helper("or", Box::new(helper_or));
241 handlebars.register_helper("not", Box::new(helper_not));
242 handlebars.register_helper("len", Box::new(helper_len));
243 handlebars.register_helper("json", Box::new(helper_json));
244 handlebars.register_helper("secret", Box::new(helper_secret));
245}
246
247fn helper_get(
248 h: &Helper,
249 _: &Handlebars,
250 ctx: &Context,
251 _: &mut RenderContext,
252 out: &mut dyn Output,
253) -> HelperResult {
254 let path = h
255 .param(0)
256 .and_then(|param| param.value().as_str())
257 .ok_or_else(|| render_error("get helper requires a path"))?;
258 let pointer = to_pointer(path);
259 let root = ctx.data();
260 let value = root
261 .pointer(&pointer)
262 .map(value_to_string)
263 .or_else(|| h.param(1).map(|param| value_to_string(param.value())))
264 .unwrap_or_default();
265 out.write(&value)?;
266 Ok(())
267}
268
269fn helper_default(
270 h: &Helper,
271 _: &Handlebars,
272 _: &Context,
273 _: &mut RenderContext,
274 out: &mut dyn Output,
275) -> HelperResult {
276 let first = h.param(0).map(|param| param.value());
277 let fallback = h.param(1).map(|param| param.value());
278 let chosen = if let Some(value) = first {
279 if is_truthy(value) {
280 value_to_string(value)
281 } else {
282 fallback.map(value_to_string).unwrap_or_default()
283 }
284 } else {
285 fallback.map(value_to_string).unwrap_or_default()
286 };
287 out.write(&chosen)?;
288 Ok(())
289}
290
291fn helper_eq(
292 h: &Helper,
293 _: &Handlebars,
294 _: &Context,
295 _: &mut RenderContext,
296 out: &mut dyn Output,
297) -> HelperResult {
298 let left = h
299 .param(0)
300 .map(|param| param.value())
301 .unwrap_or(&Value::Null);
302 let right = h
303 .param(1)
304 .map(|param| param.value())
305 .unwrap_or(&Value::Null);
306 let result = left == right;
307 out.write(&result.to_string())?;
308 Ok(())
309}
310
311fn helper_and(
312 h: &Helper,
313 _: &Handlebars,
314 _: &Context,
315 _: &mut RenderContext,
316 out: &mut dyn Output,
317) -> HelperResult {
318 let mut truthy = true;
319 for param in h.params() {
320 truthy &= is_truthy(param.value());
321 if !truthy {
322 break;
323 }
324 }
325 out.write(&truthy.to_string())?;
326 Ok(())
327}
328
329fn helper_or(
330 h: &Helper,
331 _: &Handlebars,
332 _: &Context,
333 _: &mut RenderContext,
334 out: &mut dyn Output,
335) -> HelperResult {
336 let mut truthy = false;
337 for param in h.params() {
338 if is_truthy(param.value()) {
339 truthy = true;
340 break;
341 }
342 }
343 out.write(&truthy.to_string())?;
344 Ok(())
345}
346
347fn helper_not(
348 h: &Helper,
349 _: &Handlebars,
350 _: &Context,
351 _: &mut RenderContext,
352 out: &mut dyn Output,
353) -> HelperResult {
354 let value = h
355 .param(0)
356 .map(|param| param.value())
357 .unwrap_or(&Value::Bool(false));
358 out.write(&(!is_truthy(value)).to_string())?;
359 Ok(())
360}
361
362fn helper_len(
363 h: &Helper,
364 _: &Handlebars,
365 _: &Context,
366 _: &mut RenderContext,
367 out: &mut dyn Output,
368) -> HelperResult {
369 let value = h
370 .param(0)
371 .map(|param| param.value())
372 .unwrap_or(&Value::Null);
373 let len = match value {
374 Value::String(s) => s.len(),
375 Value::Array(arr) => arr.len(),
376 Value::Object(obj) => obj.len(),
377 _ => 0,
378 };
379 out.write(&len.to_string())?;
380 Ok(())
381}
382
383fn helper_json(
384 h: &Helper,
385 _: &Handlebars,
386 _: &Context,
387 _: &mut RenderContext,
388 out: &mut dyn Output,
389) -> HelperResult {
390 let value = h
391 .param(0)
392 .map(|param| param.value())
393 .unwrap_or(&Value::Null);
394 let serialized = serde_json::to_string(value).unwrap_or_default();
395 out.write(&serialized)?;
396 Ok(())
397}
398
399fn helper_secret(
400 h: &Helper,
401 _: &Handlebars,
402 ctx: &Context,
403 _: &mut RenderContext,
404 out: &mut dyn Output,
405) -> HelperResult {
406 let key = h
407 .param(0)
408 .and_then(|param| param.value().as_str())
409 .ok_or_else(|| render_error("secret helper requires a key"))?;
410
411 let root = ctx.data();
412 let host_available = root
413 .get("__secrets_meta")
414 .and_then(Value::as_object)
415 .and_then(|meta| meta.get("host_available"))
416 .and_then(Value::as_bool)
417 .unwrap_or(false);
418
419 if !host_available {
420 return Err(render_error("secret_host_unavailable"));
421 }
422
423 if let Some(Value::Object(secrets)) = root.get("secrets")
424 && let Some(value) = secrets.get(key)
425 {
426 out.write(&value_to_string(value))?;
427 return Ok(());
428 }
429
430 if let Some(denied) = root
431 .get("__secrets_meta")
432 .and_then(Value::as_object)
433 .and_then(|meta| meta.get("denied"))
434 .and_then(Value::as_object)
435 && let Some(Value::String(code)) = denied.get(key)
436 {
437 return Err(render_error(code.clone()));
438 }
439
440 Err(render_error("secret_access_denied"))
441}
442
443fn to_pointer(path: &str) -> String {
444 let cleaned = path.replace('.', "/");
445 if cleaned.starts_with('/') {
446 cleaned
447 } else {
448 format!("/{}", cleaned)
449 }
450}
451
452fn is_truthy(value: &Value) -> bool {
453 match value {
454 Value::Null => false,
455 Value::Bool(flag) => *flag,
456 Value::String(text) => !text.is_empty(),
457 Value::Number(num) => num.as_f64().is_some_and(|n| n != 0.0),
458 Value::Array(arr) => !arr.is_empty(),
459 Value::Object(map) => !map.is_empty(),
460 }
461}
462
463fn value_to_string(value: &Value) -> String {
464 match value {
465 Value::String(text) => text.clone(),
466 Value::Bool(flag) => flag.to_string(),
467 Value::Number(num) => num.to_string(),
468 Value::Null => String::new(),
469 other => serde_json::to_string(other).unwrap_or_default(),
470 }
471}