use std::path::{Path, PathBuf}; #[derive(Clone, Debug, Hash)] pub struct Config { pub delimiter: char, pub escape: bool, pub rm_whitespace: bool, pub template_dirs: Vec, #[doc(hidden)] pub cache_dir: PathBuf, #[doc(hidden)] pub _non_exhaustive: (), } impl Default for Config { fn default() -> Self { Self { template_dirs: Vec::new(), delimiter: '%', escape: true, cache_dir: Path::new(env!("OUT_DIR")).join("cache"), rm_whitespace: false, _non_exhaustive: (), } } } #[cfg(feature = "config")] mod imp { use serde::Deserialize; use std::fs; use super::*; use crate::error::*; impl Config { pub fn search_file_and_read(base: &Path) -> Result { // search config file let mut path = PathBuf::new(); let mut config = Config::default(); for component in base.iter() { path.push(component); path.push("sailfish.toml"); if path.is_file() { let config_file = ConfigFile::read_from_file(&path).map_err(|mut e| { e.source_file = Some(path.to_owned()); e })?; if let Some(template_dirs) = config_file.template_dirs { for template_dir in template_dirs.into_iter().rev() { let expanded = expand_env_vars(template_dir).map_err(|mut e| { e.source_file = Some(path.to_owned()); e })?; let template_dir = PathBuf::from(expanded); if template_dir.is_absolute() { config.template_dirs.push(template_dir); } else { config .template_dirs .push(path.parent().unwrap().join(template_dir)); } } } if let Some(delimiter) = config_file.delimiter { config.delimiter = delimiter; } if let Some(escape) = config_file.escape { config.escape = escape; } if let Some(optimizations) = config_file.optimizations { if let Some(rm_whitespace) = optimizations.rm_whitespace { config.rm_whitespace = rm_whitespace; } } } path.pop(); } Ok(config) } } #[derive(Deserialize, Debug)] #[serde(deny_unknown_fields)] struct Optimizations { rm_whitespace: Option, } #[derive(Deserialize, Debug)] #[serde(deny_unknown_fields)] struct ConfigFile { template_dirs: Option>, delimiter: Option, escape: Option, optimizations: Option, } impl ConfigFile { fn read_from_file(path: &Path) -> Result { let content = fs::read_to_string(path) .chain_err(|| format!("Failed to read configuration file {:?}", path))?; Self::from_string(&content) } fn from_string(content: &str) -> Result { toml::from_str::(content).map_err(|e| error(e.to_string())) } } fn expand_env_vars>(input: S) -> Result { use std::env; let input = input.as_ref(); let len = input.len(); let mut iter = input.chars().enumerate(); let mut result = String::new(); let mut found = false; let mut env_var = String::new(); while let Some((i, c)) = iter.next() { match c { '$' if !found => { if let Some((_, cc)) = iter.next() { if cc == '{' { found = true; } else { // We didn't find a trailing { after the $ // so we push the chars read onto the result result.push(c); result.push(cc); } } } '}' if found => { let val = env::var(&env_var).map_err(|e| match e { env::VarError::NotPresent => { error(format!("Environment variable ({}) not set", env_var)) } env::VarError::NotUnicode(_) => error(format!( "Environment variable ({}) contents not valid unicode", env_var )), })?; result.push_str(&val); env_var.clear(); found = false; } _ => { if found { env_var.push(c); // Check if we're at the end with an unclosed environment variable: // ${MYVAR instead of ${MYVAR} // If so, push it back onto the string as some systems allows the $ { characters in paths. if i == len - 1 { result.push_str("${"); result.push_str(&env_var); } } else { result.push(c); } } } } Ok(result) } fn error>(msg: T) -> Error { make_error!(ErrorKind::ConfigError(msg.into())) } #[cfg(test)] mod tests { use crate::config::imp::expand_env_vars; use std::env; #[test] fn expands_env_vars() { env::set_var("TESTVAR", "/a/path"); let input = "/path/to/${TESTVAR}Templates"; let output = expand_env_vars(input).unwrap(); assert_eq!(output, "/path/to//a/pathTemplates"); } #[test] fn retains_case_sensitivity() { env::set_var("tEstVar", "/a/path"); let input = "/path/${tEstVar}"; let output = expand_env_vars(input).unwrap(); assert_eq!(output, "/path//a/path"); } #[test] fn retains_unclosed_env_var() { let input = "/path/to/${UNCLOSED"; let output = expand_env_vars(input).unwrap(); assert_eq!(output, input); } #[test] fn ingores_markers() { let input = "path/{$/$}/${/to/{"; let output = expand_env_vars(input).unwrap(); assert_eq!(output, input); } #[test] fn errors_on_unset_env_var() { let input = "/path/to/${UNSET}"; let output = expand_env_vars(input); assert!(output.is_err()); } } }