spin_templates/
writer.rs

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), // only have to worry about merging into root table for now
13    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}