2020-06-04 16:39:33 -04:00
|
|
|
use proc_macro2::{Span, TokenStream};
|
|
|
|
use quote::{quote, ToTokens};
|
2020-06-11 02:31:55 -04:00
|
|
|
use std::env;
|
2020-06-04 16:39:33 -04:00
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use syn::parse::{Parse, ParseStream, Result as ParseResult};
|
|
|
|
use syn::punctuated::Punctuated;
|
2020-06-07 10:23:45 -04:00
|
|
|
use syn::{Fields, Ident, ItemStruct, LitBool, LitChar, LitStr, Token};
|
2020-06-04 16:39:33 -04:00
|
|
|
|
|
|
|
use crate::compiler::Compiler;
|
2020-06-07 04:58:52 -04:00
|
|
|
use crate::config::Config;
|
2020-06-04 16:39:33 -04:00
|
|
|
use crate::error::*;
|
|
|
|
|
|
|
|
// arguments for include_template* macros
|
|
|
|
#[derive(Default)]
|
|
|
|
struct DeriveTemplateOptions {
|
|
|
|
path: Option<LitStr>,
|
|
|
|
delimiter: Option<LitChar>,
|
|
|
|
escape: Option<LitBool>,
|
2020-06-06 18:01:15 -04:00
|
|
|
rm_whitespace: Option<LitBool>,
|
2020-06-04 16:39:33 -04:00
|
|
|
type_: Option<LitStr>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Parse for DeriveTemplateOptions {
|
|
|
|
fn parse(outer: ParseStream) -> ParseResult<Self> {
|
|
|
|
let s;
|
|
|
|
syn::parenthesized!(s in outer);
|
|
|
|
|
|
|
|
let mut options = Self::default();
|
|
|
|
let mut found_keys = Vec::new();
|
|
|
|
|
|
|
|
while !s.is_empty() {
|
|
|
|
let key = s.parse::<Ident>()?;
|
|
|
|
s.parse::<Token![=]>()?;
|
|
|
|
|
|
|
|
// check if argument is repeated
|
|
|
|
if found_keys.iter().any(|e| *e == key) {
|
|
|
|
return Err(syn::Error::new(
|
|
|
|
key.span(),
|
|
|
|
format!("Argument `{}` was repeated.", key),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
if key == "path" {
|
|
|
|
options.path = Some(s.parse::<LitStr>()?);
|
|
|
|
} else if key == "delimiter" {
|
|
|
|
options.delimiter = Some(s.parse::<LitChar>()?);
|
|
|
|
} else if key == "escape" {
|
|
|
|
options.escape = Some(s.parse::<LitBool>()?);
|
2020-06-06 18:01:15 -04:00
|
|
|
} else if key == "rm_whitespace" {
|
|
|
|
options.rm_whitespace = Some(s.parse::<LitBool>()?);
|
2020-06-04 16:39:33 -04:00
|
|
|
} else if key == "type" {
|
|
|
|
options.type_ = Some(s.parse::<LitStr>()?);
|
|
|
|
} else {
|
|
|
|
return Err(syn::Error::new(
|
|
|
|
key.span(),
|
|
|
|
format!("Unknown option: `{}`", key),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
found_keys.push(key);
|
|
|
|
|
|
|
|
// consume comma token
|
|
|
|
if s.is_empty() {
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
s.parse::<Token![,]>()?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(options)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl DeriveTemplateOptions {
|
|
|
|
fn merge(&mut self, other: DeriveTemplateOptions) -> Result<(), syn::Error> {
|
|
|
|
fn merge_single<T: ToTokens>(
|
|
|
|
lhs: &mut Option<T>,
|
|
|
|
rhs: Option<T>,
|
|
|
|
) -> Result<(), syn::Error> {
|
|
|
|
if lhs.is_some() {
|
|
|
|
if let Some(rhs) = rhs {
|
|
|
|
Err(syn::Error::new_spanned(rhs, "keyword argument repeated."))
|
|
|
|
} else {
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
*lhs = rhs;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
merge_single(&mut self.path, other.path)?;
|
|
|
|
merge_single(&mut self.delimiter, other.delimiter)?;
|
|
|
|
merge_single(&mut self.escape, other.escape)?;
|
2020-06-06 18:01:15 -04:00
|
|
|
merge_single(&mut self.rm_whitespace, other.rm_whitespace)?;
|
2020-06-04 16:39:33 -04:00
|
|
|
merge_single(&mut self.type_, other.type_)?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn compile(
|
2020-06-06 09:49:01 -04:00
|
|
|
template_dir: &Path,
|
2020-06-04 16:39:33 -04:00
|
|
|
input_file: &Path,
|
|
|
|
output_file: &Path,
|
|
|
|
options: &DeriveTemplateOptions,
|
|
|
|
) -> Result<(), Error> {
|
2020-06-05 22:58:14 -04:00
|
|
|
let mut config = Config::default();
|
2020-06-04 16:39:33 -04:00
|
|
|
if let Some(ref delimiter) = options.delimiter {
|
2020-06-05 22:58:14 -04:00
|
|
|
config.delimiter = delimiter.value();
|
2020-06-04 16:39:33 -04:00
|
|
|
}
|
|
|
|
if let Some(ref escape) = options.escape {
|
2020-06-05 22:58:14 -04:00
|
|
|
config.escape = escape.value;
|
2020-06-04 16:39:33 -04:00
|
|
|
}
|
2020-06-06 18:01:15 -04:00
|
|
|
if let Some(ref rm_whitespace) = options.rm_whitespace {
|
|
|
|
config.rm_whitespace = rm_whitespace.value;
|
|
|
|
}
|
2020-06-04 16:39:33 -04:00
|
|
|
|
2020-06-05 22:58:14 -04:00
|
|
|
let compiler = Compiler::with_config(config);
|
2020-06-06 09:49:01 -04:00
|
|
|
compiler.compile_file(template_dir, input_file, &*output_file)
|
2020-06-04 16:39:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error> {
|
|
|
|
let strct = syn::parse2::<ItemStruct>(tokens)?;
|
|
|
|
|
|
|
|
let mut all_options = DeriveTemplateOptions::default();
|
|
|
|
for attr in strct.attrs {
|
2020-06-06 14:44:30 -04:00
|
|
|
if attr.path.is_ident("template") {
|
|
|
|
let opt = syn::parse2::<DeriveTemplateOptions>(attr.tokens)?;
|
|
|
|
all_options.merge(opt)?;
|
|
|
|
}
|
2020-06-04 16:39:33 -04:00
|
|
|
}
|
|
|
|
|
2020-06-06 09:49:01 -04:00
|
|
|
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");
|
|
|
|
|
2020-06-11 02:31:55 -04:00
|
|
|
if env::var("SAILFISH_INTEGRATION_TESTS").map_or(false, |s| s == "1") {
|
|
|
|
template_dir = template_dir
|
|
|
|
.ancestors()
|
|
|
|
.find(|p| p.join("LICENSE").exists())
|
|
|
|
.unwrap()
|
|
|
|
.join("integration-tests")
|
|
|
|
.join("tests")
|
|
|
|
.join("fails")
|
|
|
|
.join("templates");
|
|
|
|
}
|
|
|
|
|
2020-06-04 16:39:33 -04:00
|
|
|
let input_file = match all_options.path {
|
2020-06-06 09:49:01 -04:00
|
|
|
Some(ref path) => template_dir.join(path.value()),
|
2020-06-04 16:39:33 -04:00
|
|
|
None => {
|
|
|
|
return Err(syn::Error::new(
|
|
|
|
Span::call_site(),
|
|
|
|
"`path` option must be specified.",
|
|
|
|
)
|
|
|
|
.into())
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
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),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-06-11 02:31:55 -04:00
|
|
|
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"))
|
2020-06-04 16:39:33 -04:00
|
|
|
};
|
|
|
|
let mut output_file = out_dir.clone();
|
|
|
|
output_file.push("templates");
|
|
|
|
output_file.push(filename);
|
|
|
|
|
2020-06-06 09:49:01 -04:00
|
|
|
compile(&*template_dir, &*input_file, &*output_file, &all_options)
|
2020-06-04 16:39:33 -04:00
|
|
|
.map_err(|e| syn::Error::new(Span::call_site(), e))?;
|
|
|
|
|
|
|
|
let input_file_string = input_file.to_string_lossy();
|
|
|
|
let output_file_string = output_file.to_string_lossy();
|
|
|
|
|
|
|
|
// Generate tokens
|
|
|
|
|
|
|
|
let name = strct.ident;
|
|
|
|
|
|
|
|
let field_names: Punctuated<Ident, Token![,]> = match strct.fields {
|
|
|
|
Fields::Named(fields) => fields
|
|
|
|
.named
|
|
|
|
.into_iter()
|
|
|
|
.map(|f| {
|
|
|
|
f.ident.expect(
|
|
|
|
"Internal error: Failed to get field name (error code: 73621)",
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.collect(),
|
|
|
|
Fields::Unit => Punctuated::new(),
|
|
|
|
_ => {
|
|
|
|
return Err(syn::Error::new(
|
|
|
|
Span::call_site(),
|
|
|
|
"You cannot derive `Template` or `TemplateOnce` for tuple struct",
|
|
|
|
));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let (impl_generics, ty_generics, where_clause) = strct.generics.split_for_impl();
|
|
|
|
|
|
|
|
let tokens = quote! {
|
|
|
|
impl #impl_generics sailfish::TemplateOnce for #name #ty_generics #where_clause {
|
|
|
|
fn render_once(self) -> sailfish::runtime::RenderResult {
|
|
|
|
include_bytes!(#input_file_string);
|
|
|
|
|
|
|
|
use sailfish::runtime as sfrt;
|
|
|
|
use sfrt::Render as _;
|
|
|
|
|
|
|
|
static SIZE_HINT: sfrt::SizeHint = sfrt::SizeHint::new();
|
|
|
|
let _size_hint = SIZE_HINT.get();
|
|
|
|
let mut _ctx = sfrt::Context {
|
|
|
|
buf: sfrt::Buffer::with_capacity(_size_hint)
|
|
|
|
};
|
|
|
|
|
|
|
|
let #name { #field_names } = self;
|
|
|
|
include!(#output_file_string);
|
|
|
|
|
|
|
|
SIZE_HINT.update(_ctx.buf.len());
|
|
|
|
_ctx.into_result()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(tokens)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn derive_template(tokens: TokenStream) -> TokenStream {
|
|
|
|
derive_template_impl(tokens).unwrap_or_else(|e| e.to_compile_error())
|
|
|
|
}
|