1use crate::YamlLocationData;
4use crate::errors::ExpandError;
5use crate::errors::ExpandErrorKind;
6use jsonschema::paths::LazyLocation;
7use jsonschema::paths::Location;
8use miette::SourceSpan;
9use std::borrow::Cow;
10use std::collections::HashMap;
11use std::env::VarError;
12use std::sync::Arc;
13
14pub trait VariableProvider {
16 fn get<'a>(&'a self, name: &str) -> Result<Cow<'a, str>, VarError>;
18}
19
20impl VariableProvider for () {
21 fn get<'a>(&'a self, _name: &str) -> Result<Cow<'a, str>, VarError> {
22 Err(VarError::NotPresent)
23 }
24}
25
26#[derive(Debug, Clone, Default)]
28pub struct EnvVariables;
29impl VariableProvider for EnvVariables {
30 fn get<'a>(&'a self, name: &str) -> Result<Cow<'a, str>, VarError> {
31 std::env::var(name).map(Cow::Owned)
32 }
33}
34
35#[derive(Debug, Clone, Default)]
37pub struct MapVariables(pub HashMap<String, String>);
38impl VariableProvider for MapVariables {
39 fn get<'a>(&'a self, name: &str) -> Result<Cow<'a, str>, VarError> {
40 self.0
41 .get(name)
42 .map(|value| Cow::Borrowed(value.as_str()))
43 .ok_or(VarError::NotPresent)
44 }
45}
46
47fn expand_variable<'v>(
48 variable: &str,
49 span: SourceSpan,
50 vars: &'v dyn VariableProvider,
51) -> Result<Cow<'v, str>, ExpandErrorKind> {
52 let Some((kind, argument)) = variable.split_once('.') else {
53 return Err(ExpandErrorKind::UnprefixedExpansion {
54 span,
55 name: variable.to_string(),
56 });
57 };
58 match kind {
62 "env" => match vars.get(argument) {
63 Ok(value) => Ok(value),
64 Err(VarError::NotPresent) => Err(ExpandErrorKind::NotPresent { span }),
65 Err(VarError::NotUnicode(_)) => Err(ExpandErrorKind::NotUnicode { span }),
66 },
67 _ => Err(ExpandErrorKind::InvalidExpansion { span }),
68 }
69}
70
71fn expand_inner(
72 value: &mut serde_json::Value,
73 vars: &dyn VariableProvider,
74 location_data: &YamlLocationData,
75 path: LazyLocation,
76 errors: &mut Vec<ExpandErrorKind>,
77) {
78 match value {
79 serde_json::Value::Object(map) => {
80 for (key, value) in map.iter_mut() {
81 expand_inner(value, vars, location_data, path.push(key.as_str()), errors);
82 }
83 }
84 serde_json::Value::Array(vec) => {
85 for (key, value) in vec.iter_mut().enumerate() {
86 expand_inner(value, vars, location_data, path.push(key), errors);
87 }
88 }
89 serde_json::Value::String(string) => {
90 let location = Location::from(&path);
91 let span = location_data
94 .resolve_instance_span(&location)
95 .unwrap_or_else(|| SourceSpan::new(0.into(), 0));
96 match shellexpand::env_with_context(string, |name| {
97 expand_variable(name, span, vars).map(Some)
100 }) {
101 Ok(Cow::Borrowed(_)) => {
102 }
104 Ok(Cow::Owned(expanded)) => {
105 *string = expanded;
106 }
107 Err(err) => errors.push(err.cause),
108 }
109 }
110 _ => {}
111 }
112}
113
114pub(crate) fn expand(
115 value: &mut serde_json::Value,
116 vars: &dyn VariableProvider,
117 location_data: Option<&YamlLocationData>,
118) -> Result<(), ExpandError> {
119 let no_yaml = YamlLocationData::empty();
120 let mut errors = vec![];
121 expand_inner(
122 value,
123 vars,
124 location_data.unwrap_or(&no_yaml),
125 LazyLocation::new(),
126 &mut errors,
127 );
128
129 if errors.is_empty() {
130 Ok(())
131 } else {
132 Err(ExpandError {
133 source_code: location_data
134 .map(|data| data.source_code.clone())
135 .unwrap_or_else(|| Arc::from("")),
136 errors,
137 })
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::MapVariables;
144 use super::VariableProvider;
145 use super::expand;
146 use miette::EyreContext as _;
147 use serde_json::json;
148 use std::collections::HashMap;
149 use std::ffi::OsString;
150
151 #[test]
152 fn expand_env_variable() {
153 let mut json = json!("${env.REPLACE}");
154 let vars = MapVariables(HashMap::from([(
155 "REPLACE".to_string(),
156 "value".to_string(),
157 )]));
158 expand(&mut json, &vars, None).expect("should succeed");
159 assert_eq!(json.as_str(), Some("value"));
160 }
161
162 #[test]
163 fn expand_env_variable_inside_string() {
164 let mut json = json!("this will be ${env.REPLACE}");
165 let vars = MapVariables(HashMap::from([(
166 "REPLACE".to_string(),
167 "value".to_string(),
168 )]));
169 expand(&mut json, &vars, None).expect("should succeed");
170 assert_eq!(json.as_str(), Some("this will be value"));
171 }
172
173 #[test]
174 fn expand_env_variable_inside_nesting() {
175 let mut json = json!({
176 "object": {
177 "array": ["${env.REPLACE}"],
178 }
179 });
180 let vars = MapVariables(HashMap::from([(
181 "REPLACE".to_string(),
182 "value".to_string(),
183 )]));
184 expand(&mut json, &vars, None).expect("should succeed");
185 assert_eq!(json["object"]["array"][0].as_str(), Some("value"));
186 }
187
188 #[test]
189 fn missing_variable_error() {
190 let mut json = json!({
191 "object": {
192 "array": ["${env.REPLACE}"],
193 }
194 });
195 let vars = MapVariables(HashMap::new());
196 let err = expand(&mut json, &vars, None).expect_err("should fail");
197 println!("{:?}", err);
198 }
199
200 #[test]
201 fn missing_defaulted_variable() {
202 let mut json = json!({
203 "object": {
204 "array": ["${env.REPLACE:-default}"],
205 }
206 });
207 let vars = MapVariables(HashMap::new());
208 expand(&mut json, &vars, None).expect("should succeed");
209 assert_eq!(json["object"]["array"][0].as_str(), Some("default"));
210 }
211
212 #[test]
213 fn unprefixed_replacement() {
214 let mut json = json!({
215 "object": {
216 "array": ["$REPLACE"],
217 }
218 });
219 let vars = MapVariables(HashMap::new());
220 let err = expand(&mut json, &vars, None).expect_err("should fail");
221 println!("{:?}", miette::Report::new(err));
222 }
223
224 #[test]
225 fn unsupported_prefix() {
226 let mut json = json!({
227 "object": {
228 "array": ["${other.REPLACE}"],
229 }
230 });
231 let vars = MapVariables(HashMap::new());
232 let err = expand(&mut json, &vars, None).expect_err("should fail");
233 println!("{:?}", miette::Report::new(err));
234 }
235
236 #[test]
237 fn collects_all_errors() {
238 let mut json = json!([
239 "$REPLACE ${other.REPLACE}",
240 { "works": "${env.REPLACE}" },
241 { "nested": "${env.DOESNOTEXIST}" },
242 ]);
243 let vars = MapVariables(HashMap::from([(
244 "REPLACE".to_string(),
245 "value".to_string(),
246 )]));
247 let err = expand(&mut json, &vars, None).expect_err("should fail");
248 println!("{:?}", miette::Report::new(err));
249 }
250
251 #[test]
252 fn diagnostics() {
253 let mut json = json!([
254 "$REPLACE ${other.REPLACE}",
255 { "works": "${env.REPLACE}" },
256 { "nested": "${env.DOESNOTEXIST}" },
257 ]);
258 let source_code = r#"
259 - '$REPLACE ${other.REPLACE}'
260 - works: ${env.REPLACE}
261 - { nested: "${env.DOESNOTEXIST}" }
262 "#;
263 let marked_yaml = crate::YamlLocationData::parse(source_code).expect("valid yaml");
264 let vars = MapVariables(HashMap::from([(
265 "REPLACE".to_string(),
266 "value".to_string(),
267 )]));
268 let err = expand(&mut json, &vars, Some(&marked_yaml)).expect_err("should fail");
269 insta::assert_snapshot!(print_miette(&err), @r#"
270 apollo::configuration::expansion
271
272 × could not expand configuration values
273
274 Error: apollo::configuration::env_invalid
275
276 × invalid bare expansion
277 ╭─[2:13]
278 1 │
279 2 │ - '$REPLACE ${other.REPLACE}'
280 · ─────────────┬─────────────
281 · ╰── change this to `${env.REPLACE}`
282 3 │ - works: ${env.REPLACE}
283 ╰────
284
285 Error: apollo::configuration::env_missing
286
287 × environment variable not present
288 ╭─[4:23]
289 3 │ - works: ${env.REPLACE}
290 4 │ - { nested: "${env.DOESNOTEXIST}" }
291 · ───────────┬──────────
292 · ╰── environment variable not present
293 5 │
294 ╰────
295 "#);
296 }
297
298 #[test]
299 fn not_unicode() {
300 struct MockNonUtf8Variables;
302 impl VariableProvider for MockNonUtf8Variables {
303 fn get<'a>(
304 &'a self,
305 name: &str,
306 ) -> Result<std::borrow::Cow<'a, str>, std::env::VarError> {
307 match name {
308 "NON_UTF8" => Err(std::env::VarError::NotUnicode(OsString::new())),
309 _ => Ok(std::borrow::Cow::Owned(name.to_lowercase())),
310 }
311 }
312 }
313
314 let mut json = json!({
315 "UTF8": "${env.UTF8}",
316 "NON_UTF8": "${env.NON_UTF8}",
317 });
318 let vars = MockNonUtf8Variables;
319 let err = expand(&mut json, &vars, None).expect_err("should fail");
320 assert!(matches!(
321 err.errors.as_slice(),
322 [super::ExpandErrorKind::NotUnicode { .. }]
323 ));
324 }
325
326 fn print_miette(diagnostic: &dyn miette::Diagnostic) -> String {
327 struct F<'a>(&'a dyn miette::Diagnostic);
328 impl std::fmt::Display for F<'_> {
329 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330 miette::MietteHandlerOpts::new()
331 .color(false)
332 .build()
333 .debug(self.0, f)
334 }
335 }
336
337 F(diagnostic).to_string()
338 }
339}