spin_variables_static/
source.rs

1use spin_common::ui::quoted_path;
2use spin_factors::anyhow::{self, bail, Context as _};
3use std::{collections::HashMap, path::PathBuf, str::FromStr};
4
5#[derive(Clone, Debug)]
6pub enum VariableSource {
7    /// The value of the given variable name is the given string
8    Literal(String, String),
9    /// The value of the given variable name is the content of the given file (as a string)
10    FileContent(String, PathBuf),
11    /// The file contains a map of variable names to (string) values
12    JsonFile(PathBuf),
13    /// The file contains a map of variable names to (string) values
14    TomlFile(PathBuf),
15}
16
17impl VariableSource {
18    pub fn get_variables(&self) -> anyhow::Result<HashMap<String, String>> {
19        match self {
20            VariableSource::Literal(key, val) => Ok([(key.to_string(), val.to_string())].into()),
21            VariableSource::FileContent(key, path) => {
22                let val = std::fs::read_to_string(path)
23                    .with_context(|| format!("Failed to read {}.", quoted_path(path)))?;
24                Ok([(key.to_string(), val)].into())
25            }
26            VariableSource::JsonFile(path) => {
27                let json_bytes = std::fs::read(path)
28                    .with_context(|| format!("Failed to read {}.", quoted_path(path)))?;
29                let json_vars: HashMap<String, String> = serde_json::from_slice(&json_bytes)
30                    .with_context(|| format!("Failed to parse JSON from {}.", quoted_path(path)))?;
31                Ok(json_vars)
32            }
33            VariableSource::TomlFile(path) => {
34                let toml_str = std::fs::read_to_string(path)
35                    .with_context(|| format!("Failed to read {}.", quoted_path(path)))?;
36                let toml_vars: HashMap<String, String> = toml::from_str(&toml_str)
37                    .with_context(|| format!("Failed to parse TOML from {}.", quoted_path(path)))?;
38                Ok(toml_vars)
39            }
40        }
41    }
42}
43
44impl FromStr for VariableSource {
45    type Err = anyhow::Error;
46
47    fn from_str(s: &str) -> Result<Self, Self::Err> {
48        if let Some(path) = s.strip_prefix('@') {
49            let path = PathBuf::from(path);
50            match path.extension().and_then(|s| s.to_str()) {
51                Some("json") => Ok(VariableSource::JsonFile(path)),
52                Some("toml") => Ok(VariableSource::TomlFile(path)),
53                _ => bail!("variable files must end in .json or .toml"),
54            }
55        } else if let Some((key, val)) = s.split_once('=') {
56            if let Some(path) = val.strip_prefix('@') {
57                Ok(VariableSource::FileContent(
58                    key.to_string(),
59                    PathBuf::from(path),
60                ))
61            } else {
62                Ok(VariableSource::Literal(key.to_string(), val.to_string()))
63            }
64        } else {
65            bail!("variables must be in the form 'key=value' or '@file'")
66        }
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use std::io::Write;
73
74    use super::*;
75
76    #[test]
77    fn source_from_str() {
78        match "k=v".parse() {
79            Ok(VariableSource::Literal(key, val)) => {
80                assert_eq!(key, "k");
81                assert_eq!(val, "v");
82            }
83            Ok(other) => panic!("wrong variant {other:?}"),
84            Err(err) => panic!("{err:?}"),
85        }
86        match "k=@v.txt".parse() {
87            Ok(VariableSource::FileContent(key, path)) => {
88                assert_eq!(key, "k");
89                assert_eq!(path, PathBuf::from("v.txt"));
90            }
91            Ok(other) => panic!("wrong variant {other:?}"),
92            Err(err) => panic!("{err:?}"),
93        }
94        match "@file.json".parse() {
95            Ok(VariableSource::JsonFile(_)) => {}
96            Ok(other) => panic!("wrong variant {other:?}"),
97            Err(err) => panic!("{err:?}"),
98        }
99        match "@file.toml".parse() {
100            Ok(VariableSource::TomlFile(_)) => {}
101            Ok(other) => panic!("wrong variant {other:?}"),
102            Err(err) => panic!("{err:?}"),
103        }
104    }
105
106    #[test]
107    fn source_from_str_errors() {
108        assert!(VariableSource::from_str("nope").is_err());
109        assert!(VariableSource::from_str("@whatami").is_err());
110        assert!(VariableSource::from_str("@wrong.kind").is_err());
111    }
112
113    #[test]
114    fn literal_get_variables() {
115        let vars = VariableSource::Literal("k".to_string(), "v".to_string())
116            .get_variables()
117            .unwrap();
118        assert_eq!(vars["k"], "v");
119    }
120
121    #[test]
122    fn file_content_get_variables() {
123        let mut file = tempfile::NamedTempFile::with_suffix(".txt").unwrap();
124        file.write_all(br#"sausage time!"#).unwrap();
125        let path = file.into_temp_path();
126        let vars = VariableSource::FileContent("k".to_string(), path.to_path_buf())
127            .get_variables()
128            .unwrap();
129        assert_eq!(vars["k"], "sausage time!");
130    }
131
132    #[test]
133    fn json_get_variables() {
134        let mut json_file = tempfile::NamedTempFile::with_suffix(".json").unwrap();
135        json_file.write_all(br#"{"k": "v"}"#).unwrap();
136        let json_path = json_file.into_temp_path();
137        let vars = VariableSource::JsonFile(json_path.to_path_buf())
138            .get_variables()
139            .unwrap();
140        assert_eq!(vars["k"], "v");
141    }
142
143    #[test]
144    fn toml() {
145        let mut toml_file = tempfile::NamedTempFile::with_suffix(".toml").unwrap();
146        toml_file.write_all(br#"k = "v""#).unwrap();
147        let toml_path = toml_file.into_temp_path();
148        let vars = VariableSource::TomlFile(toml_path.to_path_buf())
149            .get_variables()
150            .unwrap();
151        assert_eq!(vars["k"], "v");
152    }
153}