sailfish_compiler/
config.rs1use 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 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 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 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}