pkgs/config/
var.rs

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            ret.add_var(var, value)?;
20        }
21        Ok(ret)
22    }
23
24    pub fn extends(&mut self, vars: &[(String, String)]) -> Result<(), VarsBuildError> {
25        for (var, value) in vars {
26            self.add_var(var, value)?;
27        }
28        Ok(())
29    }
30
31    fn add_var(&mut self, var: &str, value: &str) -> Result<(), VarsBuildError> {
32        let value = self.parse(value).map_err(|kind| VarsBuildError {
33            var: var.to_string(),
34            kind,
35        })?;
36        self.map.insert(var.to_string(), value);
37        Ok(())
38    }
39
40    pub fn map(&self) -> &HashMap<String, String> {
41        &self.map
42    }
43
44    pub fn parse(&self, input: &str) -> Result<String, VarsParseError> {
45        let mut result = String::with_capacity(input.len());
46        let mut rest = input;
47        let mut cursor = 0;
48
49        while let Some(i) = rest.find("${") {
50            result.push_str(&rest[..i]);
51
52            let after = i + 2;
53            let Some(end_rel) = rest[after..].find('}') else {
54                return Err(VarsParseError::UnclosedBrace(cursor + i));
55            };
56
57            let name = &rest[after..after + end_rel];
58            if name.is_empty() {
59                return Err(VarsParseError::EmptyVarName(cursor + i));
60            }
61
62            match self.map().get(name) {
63                Some(value) => result.push_str(value),
64                None => return Err(VarsParseError::UnknowndVar(name.to_string(), cursor + i)),
65            }
66
67            rest = &rest[after + end_rel + 1..];
68            cursor += after + end_rel + 1;
69        }
70        result.push_str(rest);
71
72        Ok(result)
73    }
74}
75
76impl Default for VarMap {
77    fn default() -> Self {
78        Self {
79            map: Self::default_vars(),
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use crate::test_utils::prelude::*;
88
89    fn setup() -> Result<VarMap> {
90        let custom = vec![
91            ("CONFIG_DIR".into(), "${HOME}/.config".into()),
92            ("MY_VAR1".into(), "hello".into()),
93            ("MY_VAR2".into(), "${MY_VAR1}_world".into()),
94        ];
95        Ok(VarMap::try_new(&custom)?)
96    }
97
98    #[gtest]
99    fn default_vars() -> Result<()> {
100        let var_map = VarMap::try_new(&[])?;
101        expect_eq!(var_map.map()["HOME"], home_dir().to_str().unwrap());
102        Ok(())
103    }
104
105    mod extend_vars {
106        use super::*;
107
108        #[gtest]
109        fn add_new() -> Result<()> {
110            let mut var_map = setup()?;
111            let len = var_map.map().len();
112
113            var_map.extends(&[("NEW_VAR".into(), "new_value".into())])?;
114            expect_eq!(var_map.map().len(), len + 1);
115            expect_eq!(var_map.map()["NEW_VAR"], "new_value");
116
117            Ok(())
118        }
119
120        #[gtest]
121        fn override_existing() -> Result<()> {
122            let mut var_map = setup()?;
123            let len = var_map.map().len();
124
125            var_map.extends(&[("MY_VAR1".into(), "${MY_VAR1}_new".into())])?;
126            expect_eq!(var_map.map().len(), len);
127            expect_eq!(var_map.map()["MY_VAR1"], "hello_new");
128            expect_eq!(var_map.map()["MY_VAR2"], "hello_world");
129
130            Ok(())
131        }
132    }
133
134    #[gtest]
135    fn custom_vars() -> Result<()> {
136        let var_map = setup()?;
137        expect_eq!(var_map.map()["HOME"], home_dir().to_str().unwrap());
138        expect_eq!(
139            var_map.map()["CONFIG_DIR"],
140            home_dir().join(".config").to_str().unwrap()
141        );
142        expect_eq!(var_map.map()["MY_VAR1"], "hello");
143        expect_eq!(var_map.map()["MY_VAR2"], "hello_world");
144
145        Ok(())
146    }
147
148    #[gtest]
149    fn custom_vars_with_wrong_order() -> Result<()> {
150        let custom = vec![
151            ("MY_VAR2".into(), "${MY_VAR1}_world".into()),
152            ("MY_VAR1".into(), "hello".into()),
153        ];
154
155        let err = VarMap::try_new(&custom).unwrap_err();
156        expect_that!(
157            err.kind,
158            pat!(VarsParseError::UnknowndVar("MY_VAR1", &0_usize))
159        );
160        expect_eq!(err.var, "MY_VAR2");
161
162        Ok(())
163    }
164
165    mod parse {
166        use super::*;
167
168        #[gtest]
169        fn common_parse() -> Result<()> {
170            let var_map = setup()?;
171            expect_eq!(
172                var_map.parse("${CONFIG_DIR}/test")?,
173                home_dir().join(".config/test").to_str().unwrap()
174            );
175            expect_eq!(
176                var_map.parse("/tmp/${MY_VAR1}/${MY_VAR2}/${MY_VAR1}")?,
177                "/tmp/hello/hello_world/hello"
178            );
179
180            Ok(())
181        }
182
183        #[gtest]
184        fn unclosed_brace() -> Result<()> {
185            let var_map = setup()?;
186            let err = var_map
187                .parse("/tmp/${MY_VAR1}/${MY_VAR2/hello")
188                .unwrap_err();
189            expect_that!(err, pat!(VarsParseError::UnclosedBrace(&16_usize)));
190            Ok(())
191        }
192
193        #[gtest]
194        fn empty_var() -> Result<()> {
195            let var_map = setup()?;
196            let err = var_map.parse("/tmp/${MY_VAR1}/${MY_VAR2}/${}").unwrap_err();
197            expect_that!(err, pat!(VarsParseError::EmptyVarName(&27_usize)));
198            Ok(())
199        }
200
201        #[gtest]
202        fn unknowd_var() -> Result<()> {
203            let var_map = setup()?;
204            let err = var_map
205                .parse("/tmp/${MY_VAR1}/${MY_VAR2}/${MY_VAR3}")
206                .unwrap_err();
207            expect_that!(err, pat!(VarsParseError::UnknowndVar("MY_VAR3", &27_usize)));
208            Ok(())
209        }
210    }
211}