spin_variables_static/
source.rs1use 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 Literal(String, String),
9 FileContent(String, PathBuf),
11 JsonFile(PathBuf),
13 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}