223 lines
7.0 KiB
Rust
223 lines
7.0 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
#[derive(Clone, Debug, Hash)]
|
|
pub struct Config {
|
|
pub delimiter: char,
|
|
pub rm_whitespace: bool,
|
|
pub template_dirs: Vec<PathBuf>,
|
|
#[doc(hidden)]
|
|
pub cache_dir: PathBuf,
|
|
#[doc(hidden)]
|
|
pub _non_exhaustive: (),
|
|
}
|
|
|
|
impl Default for Config {
|
|
fn default() -> Self {
|
|
Self {
|
|
template_dirs: Vec::new(),
|
|
delimiter: '%',
|
|
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<Config, Error> {
|
|
// 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(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<bool>,
|
|
}
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
#[serde(deny_unknown_fields)]
|
|
struct ConfigFile {
|
|
template_dirs: Option<Vec<String>>,
|
|
delimiter: Option<char>,
|
|
optimizations: Option<Optimizations>,
|
|
}
|
|
|
|
impl ConfigFile {
|
|
fn read_from_file(path: &Path) -> Result<Self, Error> {
|
|
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<Self, Error> {
|
|
toml::from_str::<Self>(content).map_err(|e| error(e.to_string()))
|
|
}
|
|
}
|
|
|
|
fn expand_env_vars<S: AsRef<str>>(input: S) -> Result<String, Error> {
|
|
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 == false => {
|
|
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<T: Into<String>>(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());
|
|
}
|
|
}
|
|
}
|