sailfish_compiler/
config.rs

1use std::path::{Path, PathBuf};
2
3#[derive(Clone, Debug, Hash)]
4pub struct Config {
5    pub delimiter: char,
6    pub escape: bool,
7    pub rm_whitespace: bool,
8    pub rm_newline: bool,
9    pub template_dirs: Vec<PathBuf>,
10    #[doc(hidden)]
11    pub cache_dir: PathBuf,
12    #[doc(hidden)]
13    pub _non_exhaustive: (),
14}
15
16impl Default for Config {
17    fn default() -> Self {
18        Self {
19            template_dirs: Vec::new(),
20            delimiter: '%',
21            escape: true,
22            cache_dir: Path::new(env!("OUT_DIR")).join("cache"),
23            rm_whitespace: false,
24            rm_newline: false,
25            _non_exhaustive: (),
26        }
27    }
28}
29
30#[cfg(feature = "config")]
31mod imp {
32    use serde::Deserialize;
33    use std::fs;
34
35    use super::*;
36    use crate::error::*;
37
38    impl Config {
39        pub fn search_file_and_read(base: &Path) -> Result<Config, Error> {
40            // search config file
41            let mut path = PathBuf::new();
42            let mut config = Config::default();
43
44            for component in base.iter() {
45                path.push(component);
46                path.push("sailfish.toml");
47
48                if path.is_file() {
49                    let config_file =
50                        ConfigFile::read_from_file(&path).map_err(|mut e| {
51                            e.source_file = Some(path.to_owned());
52                            e
53                        })?;
54
55                    if let Some(template_dirs) = config_file.template_dirs {
56                        for template_dir in template_dirs.into_iter().rev() {
57                            let expanded =
58                                expand_env_vars(template_dir).map_err(|mut e| {
59                                    e.source_file = Some(path.to_owned());
60                                    e
61                                })?;
62
63                            let template_dir = PathBuf::from(expanded);
64
65                            if template_dir.is_absolute() {
66                                config.template_dirs.push(template_dir);
67                            } else {
68                                config
69                                    .template_dirs
70                                    .push(path.parent().unwrap().join(template_dir));
71                            }
72                        }
73                    }
74
75                    if let Some(delimiter) = config_file.delimiter {
76                        config.delimiter = delimiter;
77                    }
78
79                    if let Some(escape) = config_file.escape {
80                        config.escape = escape;
81                    }
82
83                    if let Some(optimizations) = config_file.optimizations {
84                        if let Some(rm_whitespace) = optimizations.rm_whitespace {
85                            config.rm_whitespace = rm_whitespace;
86                        }
87
88                        if let Some(rm_newline) = optimizations.rm_newline {
89                            config.rm_newline = rm_newline;
90                        }
91                    }
92                }
93
94                path.pop();
95            }
96
97            Ok(config)
98        }
99    }
100
101    #[derive(Deserialize, Debug)]
102    #[serde(deny_unknown_fields)]
103    struct Optimizations {
104        rm_whitespace: Option<bool>,
105        rm_newline: Option<bool>,
106    }
107
108    #[derive(Deserialize, Debug)]
109    #[serde(deny_unknown_fields)]
110    struct ConfigFile {
111        template_dirs: Option<Vec<String>>,
112        delimiter: Option<char>,
113        escape: Option<bool>,
114        optimizations: Option<Optimizations>,
115    }
116
117    impl ConfigFile {
118        fn read_from_file(path: &Path) -> Result<Self, Error> {
119            let content = fs::read_to_string(path)
120                .chain_err(|| format!("Failed to read configuration file {:?}", path))?;
121            Self::from_string(&content)
122        }
123
124        fn from_string(content: &str) -> Result<Self, Error> {
125            toml::from_str::<Self>(content).map_err(|e| error(e.to_string()))
126        }
127    }
128
129    fn expand_env_vars<S: AsRef<str>>(input: S) -> Result<String, Error> {
130        use std::env;
131
132        let input = input.as_ref();
133        let len = input.len();
134        let mut iter = input.chars().enumerate();
135        let mut result = String::new();
136
137        let mut found = false;
138        let mut env_var = String::new();
139
140        while let Some((i, c)) = iter.next() {
141            match c {
142                '$' if !found => {
143                    if let Some((_, cc)) = iter.next() {
144                        if cc == '{' {
145                            found = true;
146                        } else {
147                            // We didn't find a trailing { after the $
148                            // so we push the chars read onto the result
149                            result.push(c);
150                            result.push(cc);
151                        }
152                    }
153                }
154                '}' if found => {
155                    let val = env::var(&env_var).map_err(|e| match e {
156                        env::VarError::NotPresent => {
157                            error(format!("Environment variable ({}) not set", env_var))
158                        }
159                        env::VarError::NotUnicode(_) => error(format!(
160                            "Environment variable ({}) contents not valid unicode",
161                            env_var
162                        )),
163                    })?;
164                    result.push_str(&val);
165
166                    env_var.clear();
167                    found = false;
168                }
169                _ => {
170                    if found {
171                        env_var.push(c);
172
173                        // Check if we're at the end with an unclosed environment variable:
174                        // ${MYVAR instead of ${MYVAR}
175                        // If so, push it back onto the string as some systems allows the $ { characters in paths.
176                        if i == len - 1 {
177                            result.push_str("${");
178                            result.push_str(&env_var);
179                        }
180                    } else {
181                        result.push(c);
182                    }
183                }
184            }
185        }
186
187        Ok(result)
188    }
189
190    fn error<T: Into<String>>(msg: T) -> Error {
191        make_error!(ErrorKind::ConfigError(msg.into()))
192    }
193
194    #[cfg(test)]
195    mod tests {
196
197        use crate::config::imp::expand_env_vars;
198        use std::env;
199
200        #[test]
201        #[allow(unsafe_code)]
202        fn expands_env_vars() {
203            unsafe { env::set_var("TESTVAR", "/a/path") };
204            let input = "/path/to/${TESTVAR}Templates";
205            let output = expand_env_vars(input).unwrap();
206            assert_eq!(output, "/path/to//a/pathTemplates");
207        }
208
209        #[test]
210        #[allow(unsafe_code)]
211        fn retains_case_sensitivity() {
212            unsafe { env::set_var("tEstVar", "/a/path") };
213            let input = "/path/${tEstVar}";
214            let output = expand_env_vars(input).unwrap();
215            assert_eq!(output, "/path//a/path");
216        }
217
218        #[test]
219        fn retains_unclosed_env_var() {
220            let input = "/path/to/${UNCLOSED";
221            let output = expand_env_vars(input).unwrap();
222            assert_eq!(output, input);
223        }
224
225        #[test]
226        fn ingores_markers() {
227            let input = "path/{$/$}/${/to/{";
228            let output = expand_env_vars(input).unwrap();
229            assert_eq!(output, input);
230        }
231
232        #[test]
233        fn errors_on_unset_env_var() {
234            let input = "/path/to/${UNSET}";
235            let output = expand_env_vars(input);
236            assert!(output.is_err());
237        }
238    }
239}