feat: Global configuration file
This commit is contained in:
parent
5b4c13af3e
commit
2d994be4ff
|
@ -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);
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue