1use std::{collections::HashMap, sync::LazyLock};
2
3use regex::{Captures, Regex};
4
5pub static ENVIRONMENT_VARIABLE_INTERPOLATION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
13 Regex::new(
14 r"(?x)
15 \$\$|
16 \$([[:word:].]+)|
17 \$\{([[:word:].]+)(?:(:?-|:?\?)([^}]*))?\}",
18 )
19 .unwrap()
20});
21
22pub fn interpolate(input: &str, vars: &HashMap<String, String>) -> Result<String, Vec<String>> {
24 let mut errors = Vec::new();
25
26 let interpolated = ENVIRONMENT_VARIABLE_INTERPOLATION_REGEX
27 .replace_all(input, |caps: &Captures<'_>| {
28 let flags = caps.get(3).map(|m| m.as_str()).unwrap_or_default();
29 let def_or_err = caps.get(4).map(|m| m.as_str()).unwrap_or_default();
30 caps.get(1)
31 .or_else(|| caps.get(2))
32 .map(|m| m.as_str())
33 .map(|name| {
34 let val = vars.get(name).map(|v| v.as_str());
35 match flags {
36 ":-" => match val {
37 Some(v) if !v.is_empty() => v,
38 _ => def_or_err,
39 },
40 "-" => val.unwrap_or(def_or_err),
41 ":?" => match val {
42 Some(v) if !v.is_empty() => v,
43 _ => {
44 errors.push(format!(
45 "Non-empty environment variable required in config. name = {name:?}, error = {def_or_err:?}",
46 ));
47 ""
48 },
49 }
50 "?" => val.unwrap_or_else(|| {
51 errors.push(format!(
52 "Missing environment variable required in config. name = {name:?}, error = {def_or_err:?}",
53 ));
54 ""
55 }),
56 _ => val.unwrap_or_else(|| {
57 errors.push(format!(
58 "Missing environment variable in config. name = {name:?}",
59 ));
60 ""
61 }),
62 }
63 })
64 .unwrap_or("$")
65 .to_string()
66 })
67 .into_owned();
68
69 if errors.is_empty() {
70 Ok(interpolated)
71 } else {
72 Err(errors)
73 }
74}
75
76#[cfg(test)]
77mod test {
78 use super::interpolate;
79 #[test]
80 fn interpolation() {
81 let vars = vec![
82 ("FOO".into(), "dogs".into()),
83 ("FOOBAR".into(), "cats".into()),
84 ("FOO.BAR".into(), "turtles".into()),
86 ("EMPTY".into(), "".into()),
87 ]
88 .into_iter()
89 .collect();
90
91 assert_eq!("dogs", interpolate("$FOO", &vars).unwrap());
92 assert_eq!("dogs", interpolate("${FOO}", &vars).unwrap());
93 assert_eq!("cats", interpolate("${FOOBAR}", &vars).unwrap());
94 assert_eq!("xcatsy", interpolate("x${FOOBAR}y", &vars).unwrap());
95 assert!(interpolate("x$FOOBARy", &vars).is_err());
96 assert_eq!("$ x", interpolate("$ x", &vars).unwrap());
97 assert_eq!("$FOO", interpolate("$$FOO", &vars).unwrap());
98 assert_eq!("dogs=bar", interpolate("$FOO=bar", &vars).unwrap());
99 assert!(interpolate("$NOT_FOO", &vars).is_err());
100 assert!(interpolate("$NOT-FOO", &vars).is_err());
101 assert_eq!("turtles", interpolate("$FOO.BAR", &vars).unwrap());
102 assert_eq!("${FOO x", interpolate("${FOO x", &vars).unwrap());
103 assert_eq!("${}", interpolate("${}", &vars).unwrap());
104 assert_eq!("dogs", interpolate("${FOO:-cats}", &vars).unwrap());
105 assert_eq!("dogcats", interpolate("${NOT:-dogcats}", &vars).unwrap());
106 assert_eq!(
107 "dogs and cats",
108 interpolate("${NOT:-dogs and cats}", &vars).unwrap()
109 );
110 assert_eq!("${:-cats}", interpolate("${:-cats}", &vars).unwrap());
111 assert_eq!("", interpolate("${NOT:-}", &vars).unwrap());
112 assert_eq!("cats", interpolate("${NOT-cats}", &vars).unwrap());
113 assert_eq!("", interpolate("${EMPTY-cats}", &vars).unwrap());
114 assert_eq!("dogs", interpolate("${FOO:?error cats}", &vars).unwrap());
115 assert_eq!("dogs", interpolate("${FOO?error cats}", &vars).unwrap());
116 assert_eq!("", interpolate("${EMPTY?error cats}", &vars).unwrap());
117 assert!(interpolate("${NOT:?error cats}", &vars).is_err());
118 assert!(interpolate("${NOT?error cats}", &vars).is_err());
119 assert!(interpolate("${EMPTY:?error cats}", &vars).is_err());
120 }
121}