From 5b4c13af3e9191e69d7e74c352b6da109616a9bd Mon Sep 17 00:00:00 2001 From: Kogia-sima Date: Sun, 14 Jun 2020 22:07:28 +0900 Subject: [PATCH 1/5] dep: Add dependency (yaml-rust) --- Cargo.lock | 10 ++++++++++ sailfish-compiler/Cargo.toml | 1 + 2 files changed, 11 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index bfde255..f0af403 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1064,6 +1064,7 @@ dependencies = [ "proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", "quote 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "syn 1.0.30 (registry+https://github.com/rust-lang/crates.io-index)", + "yaml-rust 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1410,6 +1411,14 @@ dependencies = [ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "yaml-rust" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "linked-hash-map 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [metadata] "checksum actix-codec 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "09e55f0a5c2ca15795035d90c46bd0e73a5123b72f68f12596d6ba5282051380" "checksum actix-connect 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c95cc9569221e9802bf4c377f6c18b90ef10227d787611decf79fd47d2a8e76c" @@ -1562,3 +1571,4 @@ dependencies = [ "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" "checksum winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +"checksum yaml-rust 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" diff --git a/sailfish-compiler/Cargo.toml b/sailfish-compiler/Cargo.toml index 31460a7..57c2f85 100644 --- a/sailfish-compiler/Cargo.toml +++ b/sailfish-compiler/Cargo.toml @@ -23,6 +23,7 @@ procmacro = [] [dependencies] memchr = "2.3.3" quote = { version = "1.0.6", default-features = false } +yaml-rust = "0.4.4" [dependencies.syn] version = "1.0.21" From 2d994be4fff48514ae28ca3cdf41874ff37a2e33 Mon Sep 17 00:00:00 2001 From: Kogia-sima Date: Mon, 15 Jun 2020 05:36:39 +0900 Subject: [PATCH 2/5] feat: Global configuration file --- sailfish-compiler/src/compiler.rs | 3 +- sailfish-compiler/src/config.rs | 206 ++++++++++++++++++++++++++++- sailfish-compiler/src/error.rs | 2 + sailfish-compiler/src/procmacro.rs | 124 +++++++++++------ sailfish-compiler/src/resolver.rs | 24 ++-- 5 files changed, 301 insertions(+), 58 deletions(-) diff --git a/sailfish-compiler/src/compiler.rs b/sailfish-compiler/src/compiler.rs index cb90a66..5af48ef 100644 --- a/sailfish-compiler/src/compiler.rs +++ b/sailfish-compiler/src/compiler.rs @@ -41,7 +41,6 @@ impl Compiler { pub fn compile_file( &self, - template_dir: &Path, input: &Path, output: &Path, ) -> Result { @@ -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); diff --git a/sailfish-compiler/src/config.rs b/sailfish-compiler/src/config.rs index 2d64415..13d5137 100644 --- a/sailfish-compiler/src/config.rs +++ b/sailfish-compiler/src/config.rs @@ -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, + #[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 { + // 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>, + delimiter: Option, + escape: Option, + rm_whitespace: Option, +} + +impl ConfigFile { + fn read_from_file(path: &Path) -> Result { + 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>(msg: T) -> Error { + make_error!(ErrorKind::ConfigError(msg.into())) + } +} diff --git a/sailfish-compiler/src/error.rs b/sailfish-compiler/src/error.rs index 8075e07..a698f17 100644 --- a/sailfish-compiler/src/error.rs +++ b/sailfish-compiler/src/error.rs @@ -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), diff --git a/sailfish-compiler/src/procmacro.rs b/sailfish-compiler/src/procmacro.rs index 9b0eaf2..ec2ea87 100644 --- a/sailfish-compiler/src/procmacro.rs +++ b/sailfish-compiler/src/procmacro.rs @@ -98,13 +98,7 @@ impl DeriveTemplateOptions { } } -fn compile( - template_dir: &Path, - input_file: &Path, - output_file: &Path, - options: &DeriveTemplateOptions, -) -> Result { - 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 { + 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 { 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 { @@ -130,13 +181,16 @@ fn derive_template_impl(tokens: TokenStream) -> Result } } - 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 .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(); diff --git a/sailfish-compiler/src/resolver.rs b/sailfish-compiler/src/resolver.rs index 27538fa..9a1b521 100644 --- a/sailfish-compiler/src/resolver.rs +++ b/sailfish-compiler/src/resolver.rs @@ -27,15 +27,14 @@ pub struct ResolveReport { pub deps: Vec, } -struct ResolverImpl<'s, 'h> { - template_dir: &'s Path, +struct ResolverImpl<'h> { path_stack: Vec, deps: Vec, error: Option, include_handler: Arc Result>, } -impl<'s, 'h> ResolverImpl<'s, 'h> { +impl<'h> ResolverImpl<'h> { fn resolve_include(&mut self, i: &ExprMacro) -> Result { let arg = match syn::parse2::(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 Result>, ) -> 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 { let mut child = ResolverImpl { - template_dir, path_stack: vec![input_file.to_owned()], deps: Vec::new(), error: None, From 76df7e6e1ba815f636adbcfc8a11afddcf8a23ec Mon Sep 17 00:00:00 2001 From: Kogia-sima Date: Mon, 15 Jun 2020 05:37:02 +0900 Subject: [PATCH 3/5] Update test for 2d994be --- integration-tests/templates/include-nest.stpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-tests/templates/include-nest.stpl b/integration-tests/templates/include-nest.stpl index b795cf1..b7b870c 100644 --- a/integration-tests/templates/include-nest.stpl +++ b/integration-tests/templates/include-nest.stpl @@ -1,5 +1,5 @@ 1 -<% include!("/includes/include-parent.stpl"); %> +<% include!("includes/include-parent.stpl"); %> 4 <% include!("./included.stpl"); %> 5 From 6865b8515950aa767c65063a53d4a6eb44144773 Mon Sep 17 00:00:00 2001 From: Kogia-sima Date: Mon, 15 Jun 2020 06:25:56 +0900 Subject: [PATCH 4/5] Update documents - Add `Configuration file` section --- docs/en/docs/options.md | 35 +++++++++++++++++++++++++++++++++-- docs/en/mkdocs.yml | 2 +- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/en/docs/options.md b/docs/en/docs/options.md index 72a3160..6bd7706 100644 --- a/docs/en/docs/options.md +++ b/docs/en/docs/options.md @@ -1,4 +1,6 @@ -# Derive Options +# Configuration + +## Derive options You can control the rendering behaviour via `template` attribute. @@ -13,7 +15,7 @@ struct TemplateStruct { `template` attribute accepts the following options. - `path`: path to template file. This options is always required. -- `escape`: Enable HTML escaping (default: `false`) +- `escape`: Enable HTML escaping (default: `true`) - `delimiter`: Replace the '%' character used for the tag delimiter (default: '%') - `rm_whitespace`: try to strip whitespaces as much as possible without collapsing HTML structure (default: `false`). This option might not work correctly if your templates have inline `script` tag. @@ -28,3 +30,32 @@ 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. +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` + +If a key is specified in multiple configuration files, the value in the deeper directory takes precedence over ancestor directories. + +If a key is specified in both configuration file and derive options, then the value specified in the derive options takes precedence over the configuration file. + +### Configuration file format + +Configuration files are written in the YAML 1.2 format. The default configuration is described below. + +``` +template_dir: "templates" +escape: true +delimiter: "%" + +optimization: + rm_whitespace: false +``` + +You can specify another template directory in `template_dir` option. Other options are same as derive options. diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml index f556c47..7dab418 100644 --- a/docs/en/mkdocs.yml +++ b/docs/en/mkdocs.yml @@ -44,6 +44,6 @@ nav: - 'Welcome': 'index.md' - 'Installation': 'installation.md' - 'Getting Started': 'getting-started.md' - - 'Options': 'options.md' + - 'Configuration': 'options.md' - 'Syntax': - 'Overview': 'syntax/overview.md' From d1b6d2559a0f26541be802bc51febaaed66826dc Mon Sep 17 00:00:00 2001 From: Kogia-sima Date: Mon, 15 Jun 2020 06:44:49 +0900 Subject: [PATCH 5/5] feat: New feature flag (config) --- sailfish-compiler/Cargo.toml | 5 +- sailfish-compiler/src/config.rs | 346 +++++++++++++++-------------- sailfish-compiler/src/procmacro.rs | 16 +- sailfish-macros/Cargo.toml | 11 +- 4 files changed, 200 insertions(+), 178 deletions(-) diff --git a/sailfish-compiler/Cargo.toml b/sailfish-compiler/Cargo.toml index 57c2f85..b72cf6b 100644 --- a/sailfish-compiler/Cargo.toml +++ b/sailfish-compiler/Cargo.toml @@ -17,13 +17,14 @@ name = "sailfish_compiler" doctest = false [features] -default = [] +default = ["config"] procmacro = [] +config = ["yaml-rust"] [dependencies] memchr = "2.3.3" quote = { version = "1.0.6", default-features = false } -yaml-rust = "0.4.4" +yaml-rust = { version = "0.4.4", optional = true } [dependencies.syn] version = "1.0.21" diff --git a/sailfish-compiler/src/config.rs b/sailfish-compiler/src/config.rs index 13d5137..12d8426 100644 --- a/sailfish-compiler/src/config.rs +++ b/sailfish-compiler/src/config.rs @@ -1,7 +1,4 @@ -use crate::error::*; -use std::fs; use std::path::{Path, PathBuf}; -use yaml_rust::yaml::{Yaml, YamlLoader}; #[derive(Clone, Debug)] pub struct Config { @@ -28,200 +25,211 @@ impl Default for Config { } } -impl Config { - pub fn search_file_and_read(base: &Path) -> Result { - // search config file - let mut path = PathBuf::new(); - let mut config = Config::default(); +#[cfg(feature = "config")] +mod config { + use std::fs; + use yaml_rust::yaml::{Yaml, YamlLoader}; - for component in base.iter() { - path.push(component); - path.push("sailfish.yml"); + use super::*; + use crate::error::*; - if path.is_file() { - let config_file = - ConfigFile::read_from_file(&*path).map_err(|mut e| { - e.source_file = Some(path.to_owned()); - e - })?; + impl Config { + pub fn search_file_and_read(base: &Path) -> Result { + // search config file + let mut path = PathBuf::new(); + let mut config = Config::default(); - 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)); + 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(delimiter) = config_file.delimiter { + config.delimiter = delimiter; + } - if let Some(escape) = config_file.escape { - config.escape = escape; - } + 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>, - delimiter: Option, - escape: Option, - rm_whitespace: Option, -} - -impl ConfigFile { - fn read_from_file(path: &Path) -> Result { - 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", - )); + if let Some(rm_whitespace) = config_file.rm_whitespace { + config.rm_whitespace = rm_whitespace; } } - self.template_dirs = Some(template_dirs); - } - _ => { - return Err(Self::error("Arguments of `template_dir` must be string")); - } - } - Ok(()) + path.pop(); + } + + Ok(config) + } } - fn visit_delimiter(&mut self, value: Yaml) -> Result<(), Error> { - if self.delimiter.is_some() { - return Err(Self::error("Duplicate key (delimiter)")); + #[derive(Default)] + struct ConfigFile { + template_dirs: Option>, + delimiter: Option, + escape: Option, + rm_whitespace: Option, + } + + impl ConfigFile { + fn read_from_file(path: &Path) -> Result { + 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) } - if let Yaml::String(s) = value { - if s.chars().count() == 1 { - self.delimiter = Some(s.chars().next().unwrap()); - Ok(()) + 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")) } - } 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_escape(&mut self, value: Yaml) -> Result<(), Error> { + if self.escape.is_some() { + return Err(Self::error("Duplicate key (escape)")); + } - 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")); - } + if let Yaml::Boolean(b) = value { + self.escape = Some(b); + Ok(()) + } else { + Err(Self::error("`escape` must be boolean")) } } - Ok(()) - } + fn visit_optimization(&mut self, entry: Yaml) -> Result<(), Error> { + let hash = entry.into_hash().ok_or_else(|| { + ErrorKind::ConfigError("Invalid configuration format".to_owned()) + })?; - fn visit_rm_whitespace(&mut self, value: Yaml) -> Result<(), Error> { - if self.rm_whitespace.is_some() { - return Err(Self::error("Duplicate key (rm_whitespace)")); - } + 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")); + } + } + } - if let Yaml::Boolean(b) = value { - self.rm_whitespace = Some(b); Ok(()) - } else { - Err(Self::error("`rm_whitespace` must be boolean")) } - } - fn error>(msg: T) -> Error { - make_error!(ErrorKind::ConfigError(msg.into())) + 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>(msg: T) -> Error { + make_error!(ErrorKind::ConfigError(msg.into())) + } } } diff --git a/sailfish-compiler/src/procmacro.rs b/sailfish-compiler/src/procmacro.rs index ec2ea87..4139f07 100644 --- a/sailfish-compiler/src/procmacro.rs +++ b/sailfish-compiler/src/procmacro.rs @@ -181,12 +181,16 @@ fn derive_template_impl(tokens: TokenStream) -> Result } } - 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))?; + let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect( + "Internal error: environmental variable `CARGO_MANIFEST_DIR` is not set.", + )); + + #[cfg(feature = "config")] + let mut config = Config::search_file_and_read(&*manifest_dir) + .map_err(|e| syn::Error::new(Span::call_site(), e))?; + + #[cfg(not(feature = "config"))] + let mut config = Config::default(); if env::var("SAILFISH_INTEGRATION_TESTS").map_or(false, |s| s == "1") { let template_dir = env::current_dir() diff --git a/sailfish-macros/Cargo.toml b/sailfish-macros/Cargo.toml index ce768f8..c688b42 100644 --- a/sailfish-macros/Cargo.toml +++ b/sailfish-macros/Cargo.toml @@ -20,6 +20,15 @@ proc-macro = true test = false doctest = false +[features] +default = ["config"] +config = ["sailfish-compiler/config"] + [dependencies] -sailfish-compiler = { path = "../sailfish-compiler", version = "0.0.5", features = ["procmacro"] } proc-macro2 = "1.0.17" + +[dependencies.sailfish-compiler] +path = "../sailfish-compiler" +version = "0.0.5" +default-features = false +features = ["procmacro"]