Merge pull request #57 from Svenskunganka/toml

Change config file format from YAML to TOML
This commit is contained in:
Vince Pike 2022-03-09 11:01:11 -05:00 committed by GitHub
commit ef48042264
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 145 additions and 144 deletions

View File

@ -33,13 +33,13 @@ struct TemplateStruct {
## Configuration file
Sailfish allows global and local configuration in a file named `sailfish.yml`. Sailfish looks for this file in same directory as `Cargo.toml` and all parent directories.
Sailfish allows global and local configuration in a file named `sailfish.toml`. Sailfish looks for this file in same directory as `Cargo.toml` and all parent directories.
If, for example, `Cargo.toml` exists in `/foo/bar/baz` directory, then the following configuration files would be scanned in this order.
- `/foo/bar/baz/sailfish.yml`
- `/foo/bar/sailfish.yml`
- `/foo/sailfish.yml`
- `/sailfish.yml`
- `/foo/bar/baz/sailfish.toml`
- `/foo/bar/sailfish.toml`
- `/foo/sailfish.toml`
- `/sailfish.toml`
If a key is specified in multiple configuration files, the value in the deeper directory takes precedence over ancestor directories.
@ -47,15 +47,21 @@ If a key is specified in both configuration file and derive options, then the va
### Configuration file format
Configuration files are written in the YAML 1.2 format. Here is the default configuration.
Configuration files are written in the TOML 0.5 format. Here is the default configuration:
``` yaml
template_dir: "templates"
escape: true
delimiter: "%"
``` toml
template_dirs = ["templates"]
escape = true
delimiter = "%"
optimization:
rm_whitespace: false
[optimizations]
rm_whitespace = false
```
You can specify another template directory in `template_dir` option. Other options are same as derive options.
You can specify another template directory in `template_dirs` option. Other options are same as derive options.
You can also embed environment variables in `template_dirs` paths by wrapping the variable name with `${` and `}` like `${MY_ENV_VAR}`:
```toml
template_dirs = ["${CI}/path/to/project/${MYVAR}/templates"]
```

View File

@ -19,12 +19,13 @@ doctest = false
[features]
default = ["config"]
procmacro = []
config = ["yaml-rust"]
config = ["serde", "toml"]
[dependencies]
memchr = "2.3.3"
quote = { version = "1.0.6", default-features = false }
yaml-rust = { version = "0.4.4", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
toml = { version = "0.5", optional = true }
home = "0.5.3"
filetime = "0.2.14"

View File

@ -27,8 +27,8 @@ impl Default for Config {
#[cfg(feature = "config")]
mod imp {
use serde::Deserialize;
use std::fs;
use yaml_rust::yaml::{Yaml, YamlLoader};
use super::*;
use crate::error::*;
@ -41,7 +41,7 @@ mod imp {
for component in base.iter() {
path.push(component);
path.push("sailfish.yml");
path.push("sailfish.toml");
if path.is_file() {
let config_file =
@ -52,6 +52,14 @@ mod imp {
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 {
@ -70,8 +78,10 @@ mod imp {
config.escape = escape;
}
if let Some(rm_whitespace) = config_file.rm_whitespace {
config.rm_whitespace = rm_whitespace;
if let Some(optimizations) = config_file.optimizations {
if let Some(rm_whitespace) = optimizations.rm_whitespace {
config.rm_whitespace = rm_whitespace;
}
}
}
@ -82,154 +92,138 @@ mod imp {
}
}
#[derive(Default)]
#[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<PathBuf>>,
template_dirs: Option<Vec<String>>,
delimiter: Option<char>,
escape: Option<bool>,
rm_whitespace: Option<bool>,
optimizations: Option<Optimizations>,
}
impl ConfigFile {
fn read_from_file(path: &Path) -> Result<Self, Error> {
let mut config = Self::default();
let content = fs::read_to_string(path)
.chain_err(|| format!("Failed to read configuration file {:?}", path))?;
let entries = YamlLoader::load_from_str(&*content)
.map_err(|e| ErrorKind::ConfigError(e.to_string()))?;
drop(content);
for entry in entries {
config.visit_global(entry)?
}
Ok(config)
Self::from_string(&content)
}
fn visit_global(&mut self, entry: Yaml) -> Result<(), Error> {
let hash = entry.into_hash().ok_or_else(|| {
ErrorKind::ConfigError("Invalid configuration format".to_owned())
})?;
for (k, v) in hash {
match k {
Yaml::String(ref s) => match &**s {
"template_dir" => self.visit_template_dir(v)?,
"delimiter" => self.visit_delimiter(v)?,
"escape" => self.visit_escape(v)?,
"optimization" => self.visit_optimization(v)?,
_ => return Err(Self::error(format!("Unknown key ({})", s))),
},
_ => {
return Err(Self::error("Invalid configuration format"));
}
}
}
Ok(())
fn from_string(content: &str) -> Result<Self, Error> {
toml::from_str::<Self>(content).map_err(|e| error(e.to_string()))
}
}
fn visit_template_dir(&mut self, value: Yaml) -> Result<(), Error> {
if self.template_dirs.is_some() {
return Err(Self::error("Duplicate key (template_dir)"));
}
fn expand_env_vars<S: AsRef<str>>(input: S) -> Result<String, Error> {
use std::env;
match value {
Yaml::String(s) => self.template_dirs = Some(vec![PathBuf::from(s)]),
Yaml::Array(v) => {
let mut template_dirs = Vec::new();
for e in v {
if let Yaml::String(s) = e {
template_dirs.push(PathBuf::from(s));
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 {
return Err(Self::error(
"Arguments of `template_dir` must be string",
));
// We didn't find a trailing { after the $
// so we push the chars read onto the result
result.push(c);
result.push(cc);
}
}
self.template_dirs = Some(template_dirs);
}
'}' 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;
}
_ => {
return Err(Self::error(
"Arguments of `template_dir` must be string",
));
}
}
if found {
env_var.push(c);
Ok(())
}
fn visit_delimiter(&mut self, value: Yaml) -> Result<(), Error> {
if self.delimiter.is_some() {
return Err(Self::error("Duplicate key (delimiter)"));
}
if let Yaml::String(s) = value {
if s.chars().count() == 1 {
self.delimiter = Some(s.chars().next().unwrap());
Ok(())
} else {
Err(Self::error("`escape` must be single character"))
}
} else {
Err(Self::error("`escape` must be single character"))
}
}
fn visit_escape(&mut self, value: Yaml) -> Result<(), Error> {
if self.escape.is_some() {
return Err(Self::error("Duplicate key (escape)"));
}
if let Yaml::Boolean(b) = value {
self.escape = Some(b);
Ok(())
} else {
Err(Self::error("`escape` must be boolean"))
}
}
fn visit_optimization(&mut self, entry: Yaml) -> Result<(), Error> {
let hash = entry.into_hash().ok_or_else(|| {
ErrorKind::ConfigError("Invalid configuration format".to_owned())
})?;
for (k, v) in hash {
match k {
Yaml::String(ref s) => match &**s {
"rm_whitespace" => self.visit_rm_whitespace(v)?,
_ => {
return Err(Self::error(format!(
"Unknown key (optimization.{})",
s
)));
// 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);
}
},
_ => {
return Err(Self::error("Invalid configuration format"));
} else {
result.push(c);
}
}
}
Ok(())
}
fn visit_rm_whitespace(&mut self, value: Yaml) -> Result<(), Error> {
if self.rm_whitespace.is_some() {
return Err(Self::error("Duplicate key (rm_whitespace)"));
}
Ok(result)
}
if let Yaml::Boolean(b) = value {
self.rm_whitespace = Some(b);
Ok(())
} else {
Err(Self::error("`rm_whitespace` must be boolean"))
}
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");
}
fn error<T: Into<String>>(msg: T) -> Error {
make_error!(ErrorKind::ConfigError(msg.into()))
#[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());
}
}
}

View File

@ -0,0 +1,6 @@
template_dirs = ["../templates"]
escape = true
delimiter = "%"
[optimizations]
rm_whitespace = false

View File

@ -1,6 +0,0 @@
template_dir: "../templates"
escape: true
delimiter: "%"
optimization:
rm_whitespace: false