1use std::collections::BTreeMap;
2
3#[derive(Debug, Clone, Default)]
12pub struct RenderContext<'a> {
13 pub branch_name: &'a str,
14 pub branch_slug: &'a str,
15 pub workspace: &'a str,
16 pub scripts: &'a str,
20 pub ports: Option<&'a BTreeMap<String, u16>>,
21 pub exports: Option<&'a BTreeMap<String, String>>,
22 pub snapshot_path: Option<&'a str>,
23 pub state_ref: Option<&'a str>,
24}
25
26pub fn render(template: &str, context: &RenderContext) -> String {
27 let mut rendered = template
28 .replace("{{branch.name}}", context.branch_name)
29 .replace("{{branch.slug}}", context.branch_slug)
30 .replace("{{workspace}}", context.workspace)
31 .replace("{{scripts}}", context.scripts);
32 for (name, port) in context.ports.into_iter().flatten() {
33 rendered = rendered.replace(&format!("{{{{ports.{name}}}}}"), &port.to_string());
34 }
35 for (name, value) in context.exports.into_iter().flatten() {
36 rendered = rendered.replace(&format!("{{{{exports.{name}}}}}"), value);
37 }
38 if let Some(path) = context.snapshot_path {
39 rendered = rendered.replace("{{snapshot.path}}", path);
40 }
41 if let Some(state_ref) = context.state_ref {
42 rendered = rendered.replace("{{state_ref}}", state_ref);
43 }
44 rendered
45}
46
47pub fn unresolved_placeholder(rendered: &str) -> Option<&str> {
55 let start = rendered.find("{{")?;
56 let rest = &rendered[start..];
57 let end = rest.find("}}")? + 2;
58 Some(&rest[..end])
59}
60
61#[cfg(test)]
62mod tests {
63 use std::collections::BTreeMap;
64
65 use super::{RenderContext, render, unresolved_placeholder};
66
67 #[test]
68 fn renders_ports_and_branch_vars() {
69 let ports = BTreeMap::from([("app".to_owned(), 3107)]);
70 let context = RenderContext {
71 branch_name: "feature/a",
72 branch_slug: "feature-a",
73 workspace: "/ws",
74 ports: Some(&ports),
75 ..RenderContext::default()
76 };
77 assert_eq!(
78 render("http://127.0.0.1:{{ports.app}}/{{branch.slug}}", &context),
79 "http://127.0.0.1:3107/feature-a"
80 );
81 }
82
83 #[test]
84 fn renders_checkpoint_vars_only_when_provided() {
85 let exports = BTreeMap::from([("PREVIEW_ID".to_owned(), "pv_9".to_owned())]);
86 let context = RenderContext {
87 branch_name: "a",
88 branch_slug: "a",
89 workspace: "/ws",
90 exports: Some(&exports),
91 snapshot_path: Some("/stage"),
92 state_ref: Some("/snap/db.sql"),
93 ..RenderContext::default()
94 };
95 assert_eq!(
96 render(
97 "{{exports.PREVIEW_ID}} {{snapshot.path}}/db.sql < {{state_ref}}",
98 &context
99 ),
100 "pv_9 /stage/db.sql < /snap/db.sql"
101 );
102 assert_eq!(
105 render("{{state_ref}}", &RenderContext::default()),
106 "{{state_ref}}"
107 );
108 }
109
110 #[test]
111 fn unresolved_placeholders_are_reported_for_refusal() {
112 assert_eq!(
113 unresolved_placeholder("delete {{state_ref}} --force"),
114 Some("{{state_ref}}")
115 );
116 assert_eq!(unresolved_placeholder("delete pv_9"), None);
117 assert_eq!(unresolved_placeholder("echo {{oops"), None);
119 }
120}