1use std::collections::HashMap;
2
3use super::{VarsBuildError, VarsParseError};
4use crate::fs::home_dir;
5
6#[derive(Debug)]
7pub struct VarMap {
8 map: HashMap<String, String>,
9}
10
11impl VarMap {
12 fn default_vars() -> HashMap<String, String> {
13 HashMap::from([("HOME".into(), home_dir().to_string_lossy().into())])
14 }
15
16 pub fn try_new(vars: &[(String, String)]) -> Result<Self, VarsBuildError> {
17 let mut ret = Self::default();
18 for (var, value) in vars {
19 let value = ret.parse(value).map_err(|kind| VarsBuildError {
20 var: var.clone(),
21 kind,
22 })?;
23 ret.map.insert(var.clone(), value);
24 }
25 Ok(ret)
26 }
27
28 pub fn map(&self) -> &HashMap<String, String> {
29 &self.map
30 }
31
32 pub fn parse(&self, input: &str) -> Result<String, VarsParseError> {
33 let mut result = String::with_capacity(input.len());
34 let mut rest = input;
35 let mut cursor = 0;
36
37 while let Some(i) = rest.find("${") {
38 result.push_str(&rest[..i]);
39
40 let after = i + 2;
41 let Some(end_rel) = rest[after..].find('}') else {
42 return Err(VarsParseError::UnclosedBrace(cursor + i));
43 };
44
45 let name = &rest[after..after + end_rel];
46 if name.is_empty() {
47 return Err(VarsParseError::EmptyVarName(cursor + i));
48 }
49
50 match self.map().get(name) {
51 Some(value) => result.push_str(value),
52 None => return Err(VarsParseError::UnknowndVar(name.to_string(), cursor + i)),
53 }
54
55 rest = &rest[after + end_rel + 1..];
56 cursor += after + end_rel + 1;
57 }
58 result.push_str(rest);
59
60 Ok(result)
61 }
62}
63
64impl Default for VarMap {
65 fn default() -> Self {
66 Self {
67 map: Self::default_vars(),
68 }
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use crate::test_utils::prelude::*;
76
77 fn setup() -> Result<VarMap> {
78 let custom = vec![
79 ("CONFIG_DIR".into(), "${HOME}/.config".into()),
80 ("MY_VAR1".into(), "hello".into()),
81 ("MY_VAR2".into(), "${MY_VAR1}_world".into()),
82 ];
83 Ok(VarMap::try_new(&custom)?)
84 }
85
86 #[gtest]
87 fn default_vars() -> Result<()> {
88 let var_map = VarMap::try_new(&[])?;
89 expect_eq!(var_map.map()["HOME"], home_dir().to_str().unwrap());
90 Ok(())
91 }
92
93 #[gtest]
94 fn custom_vars() -> Result<()> {
95 let var_map = setup()?;
96 expect_eq!(var_map.map()["HOME"], home_dir().to_str().unwrap());
97 expect_eq!(
98 var_map.map()["CONFIG_DIR"],
99 home_dir().join(".config").to_str().unwrap()
100 );
101 expect_eq!(var_map.map()["MY_VAR1"], "hello");
102 expect_eq!(var_map.map()["MY_VAR2"], "hello_world");
103
104 Ok(())
105 }
106
107 #[gtest]
108 fn custom_vars_with_wrong_order() -> Result<()> {
109 let custom = vec![
110 ("MY_VAR2".into(), "${MY_VAR1}_world".into()),
111 ("MY_VAR1".into(), "hello".into()),
112 ];
113
114 let err = VarMap::try_new(&custom).unwrap_err();
115 expect_that!(
116 err.kind,
117 pat!(VarsParseError::UnknowndVar("MY_VAR1", &0_usize))
118 );
119 expect_eq!(err.var, "MY_VAR2");
120
121 Ok(())
122 }
123
124 mod parse {
125 use super::*;
126
127 #[gtest]
128 fn common_parse() -> Result<()> {
129 let var_map = setup()?;
130 expect_eq!(
131 var_map.parse("${CONFIG_DIR}/test")?,
132 home_dir().join(".config/test").to_str().unwrap()
133 );
134 expect_eq!(
135 var_map.parse("/tmp/${MY_VAR1}/${MY_VAR2}/${MY_VAR1}")?,
136 "/tmp/hello/hello_world/hello"
137 );
138
139 Ok(())
140 }
141
142 #[gtest]
143 fn unclosed_brace() -> Result<()> {
144 let var_map = setup()?;
145 let err = var_map
146 .parse("/tmp/${MY_VAR1}/${MY_VAR2/hello")
147 .unwrap_err();
148 expect_that!(err, pat!(VarsParseError::UnclosedBrace(&16_usize)));
149 Ok(())
150 }
151
152 #[gtest]
153 fn empty_var() -> Result<()> {
154 let var_map = setup()?;
155 let err = var_map.parse("/tmp/${MY_VAR1}/${MY_VAR2}/${}").unwrap_err();
156 expect_that!(err, pat!(VarsParseError::EmptyVarName(&27_usize)));
157 Ok(())
158 }
159
160 #[gtest]
161 fn unknowd_var() -> Result<()> {
162 let var_map = setup()?;
163 let err = var_map
164 .parse("/tmp/${MY_VAR1}/${MY_VAR2}/${MY_VAR3}")
165 .unwrap_err();
166 expect_that!(err, pat!(VarsParseError::UnknowndVar("MY_VAR3", &27_usize)));
167 Ok(())
168 }
169 }
170}