feat: Global configuration file

This commit is contained in:
Kogia-sima 2020-06-15 05:36:39 +09:00
parent 5b4c13af3e
commit 2d994be4ff
5 changed files with 301 additions and 58 deletions

View File

@ -41,7 +41,6 @@ impl Compiler {
pub fn compile_file(
&self,
template_dir: &Path,
input: &Path,
output: &Path,
) -> Result<CompilationReport, Error> {
@ -64,7 +63,7 @@ impl Compiler {
let mut tsource = self.translate_file_contents(input)?;
let mut report = CompilationReport { deps: Vec::new() };
let r = resolver.resolve(template_dir, &*input, &mut tsource.ast)?;
let r = resolver.resolve(&*input, &mut tsource.ast)?;
report.deps = r.deps;
optimizer.optimize(&mut tsource.ast);

View File

@ -1,11 +1,16 @@
use crate::error::*;
use std::fs;
use std::path::{Path, PathBuf};
use yaml_rust::yaml::{Yaml, YamlLoader};
#[derive(Clone, Debug)]
pub struct Config {
pub delimiter: char,
pub escape: bool,
pub cache_dir: PathBuf,
pub rm_whitespace: bool,
pub template_dirs: Vec<PathBuf>,
#[doc(hidden)]
pub cache_dir: PathBuf,
#[doc(hidden)]
pub _non_exhaustive: (),
}
@ -13,6 +18,7 @@ pub struct Config {
impl Default for Config {
fn default() -> Self {
Self {
template_dirs: Vec::new(),
delimiter: '%',
escape: true,
cache_dir: Path::new(env!("OUT_DIR")).join("cache"),
@ -22,4 +28,200 @@ impl Default for Config {
}
}
// TODO: Global configration file
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.yml");
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() {
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(rm_whitespace) = config_file.rm_whitespace {
config.rm_whitespace = rm_whitespace;
}
}
path.pop();
}
Ok(config)
}
}
#[derive(Default)]
struct ConfigFile {
template_dirs: Option<Vec<PathBuf>>,
delimiter: Option<char>,
escape: Option<bool>,
rm_whitespace: Option<bool>,
}
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)
}
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 visit_template_dir(&mut self, value: Yaml) -> Result<(), Error> {
if self.template_dirs.is_some() {
return Err(Self::error("Duplicate key (template_dir)"));
}
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));
} else {
return Err(Self::error(
"Arguments of `template_dir` must be string",
));
}
}
self.template_dirs = Some(template_dirs);
}
_ => {
return Err(Self::error("Arguments of `template_dir` must be string"));
}
}
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
)));
}
},
_ => {
return Err(Self::error("Invalid configuration format"));
}
}
}
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)"));
}
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()))
}
}

View File

@ -10,6 +10,7 @@ pub enum ErrorKind {
FmtError(fmt::Error),
IoError(io::Error),
RustSyntaxError(syn::Error),
ConfigError(String),
ParseError(String),
AnalyzeError(String),
Unimplemented(String),
@ -22,6 +23,7 @@ impl fmt::Display for ErrorKind {
ErrorKind::FmtError(ref e) => e.fmt(f),
ErrorKind::IoError(ref e) => e.fmt(f),
ErrorKind::RustSyntaxError(ref e) => write!(f, "Rust Syntax Error: {}", e),
ErrorKind::ConfigError(ref e) => write!(f, "Invalid configuration: {}", e),
ErrorKind::ParseError(ref msg) => write!(f, "Parse error: {}", msg),
ErrorKind::AnalyzeError(ref msg) => write!(f, "Analyzation error: {}", msg),
ErrorKind::Unimplemented(ref msg) => f.write_str(&**msg),

View File

@ -98,13 +98,7 @@ impl DeriveTemplateOptions {
}
}
fn compile(
template_dir: &Path,
input_file: &Path,
output_file: &Path,
options: &DeriveTemplateOptions,
) -> Result<CompilationReport, Error> {
let mut config = Config::default();
fn merge_config_options(config: &mut Config, options: &DeriveTemplateOptions) {
if let Some(ref delimiter) = options.delimiter {
config.delimiter = delimiter.value();
}
@ -114,9 +108,66 @@ fn compile(
if let Some(ref rm_whitespace) = options.rm_whitespace {
config.rm_whitespace = rm_whitespace.value;
}
}
fn resolve_template_file(path: &str, template_dirs: &[PathBuf]) -> Option<PathBuf> {
for template_dir in template_dirs.iter().rev() {
let p = template_dir.join(path);
if p.is_file() {
return Some(p);
}
}
let mut fallback = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect(
"Internal error: environmental variable `CARGO_MANIFEST_DIR` is not set.",
));
fallback.push("templates");
fallback.push(path);
if fallback.is_file() {
return Some(fallback);
}
None
}
fn filename_hash(path: &Path) -> String {
use std::fmt::Write;
const FNV_PRIME: u64 = 1_099_511_628_211;
const FNV_OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
let mut hash = String::with_capacity(16);
if let Some(n) = path.file_name() {
let mut filename = &*n.to_string_lossy();
if let Some(p) = filename.find('.') {
filename = &filename[..p];
}
hash.push_str(filename);
hash.push('-');
}
// calculate 64bit hash
let mut h = FNV_OFFSET_BASIS;
for b in (&*path.to_string_lossy()).bytes() {
h = h.wrapping_mul(FNV_PRIME);
h ^= b as u64;
}
// convert 64bit hash into ascii
let _ = write!(hash, "{:016x}", h);
hash
}
fn compile(
input_file: &Path,
output_file: &Path,
config: Config,
) -> Result<CompilationReport, Error> {
let compiler = Compiler::with_config(config);
compiler.compile_file(template_dir, input_file, &*output_file)
compiler.compile_file(input_file, &*output_file)
}
fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error> {
@ -130,13 +181,16 @@ fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error>
}
}
let mut template_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect(
"Internal error: environmental variable `CARGO_MANIFEST_DIR` is not set.",
));
template_dir.push("templates");
let mut config = Config::search_file_and_read(&*PathBuf::from(
std::env::var("CARGO_MANIFEST_DIR").expect(
"Internal error: environmental variable `CARGO_MANIFEST_DIR` is not set.",
),
))
.map_err(|e| syn::Error::new(Span::call_site(), e))?;
if env::var("SAILFISH_INTEGRATION_TESTS").map_or(false, |s| s == "1") {
template_dir = template_dir
let template_dir = env::current_dir()
.unwrap()
.ancestors()
.find(|p| p.join("LICENSE").exists())
.unwrap()
@ -144,40 +198,30 @@ fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error>
.join("tests")
.join("fails")
.join("templates");
config.template_dirs.push(template_dir);
}
let input_file = match all_options.path {
Some(ref path) => template_dir.join(path.value()),
None => {
return Err(syn::Error::new(
Span::call_site(),
"`path` option must be specified.",
)
.into())
}
let input_file = {
let path = all_options.path.as_ref().ok_or_else(|| {
syn::Error::new(Span::call_site(), "`path` option must be specified.")
})?;
resolve_template_file(&*path.value(), &*config.template_dirs).ok_or_else(
|| {
syn::Error::new(
path.span(),
format!("Template file {:?} not found", path.value()),
)
},
)?
};
let filename = match input_file.file_name() {
Some(f) => f,
None => {
return Err(syn::Error::new(
Span::call_site(),
format!("Invalid file name: {:?}", input_file),
))
}
};
let out_dir =
if std::env::var("SAILFISH_INTEGRATION_TESTS").map_or(false, |s| s == "1") {
template_dir.parent().unwrap().join("out")
} else {
PathBuf::from(env!("OUT_DIR"))
};
let out_dir = PathBuf::from(env!("OUT_DIR"));
let mut output_file = out_dir.clone();
output_file.push("templates");
output_file.push(filename);
output_file.push(filename_hash(&*input_file));
let report = compile(&*template_dir, &*input_file, &*output_file, &all_options)
merge_config_options(&mut config, &all_options);
let report = compile(&*input_file, &*output_file, config)
.map_err(|e| syn::Error::new(Span::call_site(), e))?;
let input_file_string = input_file.to_string_lossy();

View File

@ -27,15 +27,14 @@ pub struct ResolveReport {
pub deps: Vec<PathBuf>,
}
struct ResolverImpl<'s, 'h> {
template_dir: &'s Path,
struct ResolverImpl<'h> {
path_stack: Vec<PathBuf>,
deps: Vec<PathBuf>,
error: Option<Error>,
include_handler: Arc<dyn 'h + Fn(&Path) -> Result<Block, Error>>,
}
impl<'s, 'h> ResolverImpl<'s, 'h> {
impl<'h> ResolverImpl<'h> {
fn resolve_include(&mut self, i: &ExprMacro) -> Result<Expr, Error> {
let arg = match syn::parse2::<LitStr>(i.mac.tokens.clone()) {
Ok(l) => l.value(),
@ -50,8 +49,8 @@ impl<'s, 'h> ResolverImpl<'s, 'h> {
// resolve include! for rust file
if arg.ends_with(".rs") {
let absolute_path = if arg.starts_with('/') {
self.template_dir.join(&arg[1..])
let absolute_path = if Path::new(&*arg).is_absolute() {
PathBuf::from(&arg[1..])
} else {
self.path_stack.last().unwrap().parent().unwrap().join(arg)
};
@ -61,9 +60,9 @@ impl<'s, 'h> ResolverImpl<'s, 'h> {
// resolve the template file path
// TODO: How should arguments be interpreted on Windows?
let child_template_file = if arg.starts_with('/') {
let child_template_file = if Path::new(&*arg).is_absolute() {
// absolute imclude
self.template_dir.join(&arg[1..])
PathBuf::from(&arg[1..])
} else {
// relative include
self.path_stack
@ -95,7 +94,7 @@ impl<'s, 'h> ResolverImpl<'s, 'h> {
}
}
impl<'s, 'h> VisitMut for ResolverImpl<'s, 'h> {
impl<'h> VisitMut for ResolverImpl<'h> {
fn visit_expr_mut(&mut self, i: &mut Expr) {
return_if_some!(self.error);
let em = matches_or_else!(*i, Expr::Macro(ref mut em), em, {
@ -134,23 +133,20 @@ impl<'h> Resolver<'h> {
#[inline]
pub fn include_handler(
self,
mut self,
new: Arc<dyn 'h + Fn(&Path) -> Result<Block, Error>>,
) -> Resolver<'h> {
Self {
include_handler: new,
}
self.include_handler = new;
self
}
#[inline]
pub fn resolve(
&self,
template_dir: &Path,
input_file: &Path,
ast: &mut Block,
) -> Result<ResolveReport, Error> {
let mut child = ResolverImpl {
template_dir,
path_stack: vec![input_file.to_owned()],
deps: Vec::new(),
error: None,