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            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}