1use std::path::PathBuf;
2
3use anyhow::Context;
4
5pub(crate) struct TemplateOutputs {
6 outputs: Vec<TemplateOutput>,
7}
8
9pub(crate) enum TemplateOutput {
10 WriteFile(PathBuf, Vec<u8>),
11 AppendToml(PathBuf, String),
12 MergeToml(PathBuf, &'static str, String), CreateDirectory(PathBuf),
14}
15
16impl TemplateOutputs {
17 pub fn new(outputs: Vec<TemplateOutput>) -> Self {
18 Self { outputs }
19 }
20
21 pub async fn write(&self) -> anyhow::Result<()> {
22 for output in &self.outputs {
23 output.write().await?;
24 }
25 Ok(())
26 }
27}
28
29impl TemplateOutput {
30 pub async fn write(&self) -> anyhow::Result<()> {
31 match &self {
32 TemplateOutput::WriteFile(path, contents) => {
33 let dir = path.parent().with_context(|| {
34 format!("Can't get directory containing {}", path.display())
35 })?;
36 tokio::fs::create_dir_all(&dir)
37 .await
38 .with_context(|| format!("Failed to create directory {}", dir.display()))?;
39 tokio::fs::write(&path, &contents)
40 .await
41 .with_context(|| format!("Failed to write file {}", path.display()))?;
42 }
43 TemplateOutput::AppendToml(path, text) => {
44 let existing_toml = tokio::fs::read_to_string(path)
45 .await
46 .with_context(|| format!("Can't open {} to append", path.display()))?;
47 let new_toml = format!("{}\n\n{}", existing_toml.trim_end(), text);
48 tokio::fs::write(path, new_toml)
49 .await
50 .with_context(|| format!("Can't save changes to {}", path.display()))?;
51 }
52 TemplateOutput::MergeToml(path, target, text) => {
53 let existing_toml = tokio::fs::read_to_string(path)
54 .await
55 .with_context(|| format!("Can't open {} to append", path.display()))?;
56 let new_toml = merge_toml(&existing_toml, target, text)?;
57 tokio::fs::write(path, new_toml)
58 .await
59 .with_context(|| format!("Can't save changes to {}", path.display()))?;
60 }
61 TemplateOutput::CreateDirectory(dir) => {
62 tokio::fs::create_dir_all(dir)
63 .await
64 .with_context(|| format!("Failed to create directory {}", dir.display()))?;
65 }
66 }
67 Ok(())
68 }
69}
70
71fn merge_toml(existing: &str, target: &str, text: &str) -> anyhow::Result<String> {
72 use toml_edit::{DocumentMut, Entry, Item};
73
74 let mut doc: DocumentMut = existing
75 .parse()
76 .context("Can't merge into the existing manifest - it's not valid TOML")?;
77 let merging: DocumentMut = text
78 .parse()
79 .context("Can't merge snippet - it's not valid TOML")?;
80 let merging = merging.as_table();
81 match doc.get_mut(target) {
82 Some(item) => {
83 let Some(table) = item.as_table_mut() else {
84 anyhow::bail!("Cannot merge template data into {target} as it is not a table");
85 };
86 for (key, value) in merging {
87 match table.entry(key) {
88 Entry::Occupied(mut e) => {
89 let existing_val = e.get_mut();
90 *existing_val = value.clone();
91 }
92 Entry::Vacant(e) => {
93 e.insert(value.clone());
94 }
95 }
96 }
97 }
98 None => {
99 let table = Item::Table(merging.clone());
100 doc.insert(target, table);
101 }
102 };
103 Ok(doc.to_string())
104}
105
106#[cfg(test)]
107mod test {
108 use super::*;
109
110 #[test]
111 fn can_insert_variables_in_manifest() {
112 let manifest = r#"spin_version = "1"
113
114[[component]]
115id = "dummy"
116"#;
117
118 let variables = r#"url = { required = true }"#;
119
120 let new = merge_toml(manifest, "variables", variables).unwrap();
121
122 assert_eq!(
123 r#"spin_version = "1"
124
125[[component]]
126id = "dummy"
127
128[variables]
129url = { required = true }
130"#,
131 new
132 );
133 }
134
135 #[test]
136 fn can_merge_variables_into_manifest() {
137 let manifest = r#"spin_version = "1"
138
139[variables]
140secret = { default = "1234 but don't tell anyone!" }
141
142[[component]]
143id = "dummy"
144"#;
145
146 let variables = r#"url = { required = true }"#;
147
148 let new = merge_toml(manifest, "variables", variables).unwrap();
149
150 assert_eq!(
151 r#"spin_version = "1"
152
153[variables]
154secret = { default = "1234 but don't tell anyone!" }
155url = { required = true }
156
157[[component]]
158id = "dummy"
159"#,
160 new
161 );
162 }
163}