#![cfg_attr(docsrs, doc = include_str!("../README.md"))]
use std::{fs, path::PathBuf};
use proc_macro;
use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream};
use quote::{ToTokens, TokenStreamExt};
use syn::{
self,
parse::{Parse, ParseStream},
LitStr, Token,
};
mod conv;
mod err;
mod gen;
mod sql;
#[proc_macro]
pub fn include_sql(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let inc_file_lit = syn::parse_macro_input!(input as syn::LitStr);
let inc_file_path = local_file(&inc_file_lit);
match read_and_parse_sql_file(&inc_file_path) {
Ok(included_sql) => {
let mut tokens = TokenStream::new();
output_include_bytes(&inc_file_path, &mut tokens);
if !included_sql.stmt_list.is_empty() {
included_sql.to_tokens(&mut tokens);
}
tokens.into()
}
Err(err) => syn::Error::new(inc_file_lit.span(), err.to_string())
.to_compile_error()
.into(),
}
}
fn local_file(inc_file_lit_path: &LitStr) -> PathBuf {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR");
let mut path = PathBuf::from(&manifest_dir);
let inc_file_path = inc_file_lit_path.value();
if inc_file_path.starts_with('/') {
path.push(&inc_file_path[1..]);
} else if inc_file_path.starts_with("./") {
let inc_mod = inc_file_lit_path.span().unwrap().file();
if !inc_mod.is_empty() {
path.push(inc_mod);
path.pop();
}
path.push(&inc_file_path[2..]);
} else {
path.push(inc_file_path);
}
path
}
fn read_and_parse_sql_file(file_path: &PathBuf) -> err::Result<sql::IncludedSql> {
let text = match fs::read_to_string(&file_path) {
Ok(text) => text,
Err(_) => return Err(err::Error::IO(std::io::Error::other(file_path.to_str().unwrap())))
};
let file_name = file_path
.file_stem()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
.replace('-', "_");
sql::parse(&text, &file_name)
}
fn output_include_bytes(file_path: &PathBuf, tokens: &mut TokenStream) {
tokens.append(Ident::new("const", Span::call_site()));
tokens.append(Ident::new("_", Span::call_site()));
tokens.append(Punct::new(':', Spacing::Alone));
tokens.append(Punct::new('&', Spacing::Alone));
let mut type_tokens = TokenStream::new();
type_tokens.append(Ident::new("u8", Span::call_site()));
tokens.append(Group::new(Delimiter::Bracket, type_tokens));
tokens.append(Punct::new('=', Spacing::Alone));
tokens.append(Ident::new("include_bytes", Span::call_site()));
tokens.append(Punct::new('!', Spacing::Alone));
let mut macro_tokens = TokenStream::new();
macro_tokens.append(Literal::string(file_path.to_str().unwrap()));
tokens.append(Group::new(Delimiter::Parenthesis, macro_tokens));
tokens.append(Punct::new(';', Spacing::Alone));
}
#[proc_macro]
pub fn index_of(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let IndexOfArgs {
param_name,
start_index,
stmt_params,
} = syn::parse_macro_input!(input as IndexOfArgs);
let param_lookup = stmt_params.iter().position(|param| param == ¶m_name);
if let Some(pos) = param_lookup {
let mut tokens = TokenStream::new();
tokens.append(Literal::usize_unsuffixed(start_index + pos));
tokens.into()
} else {
syn::Error::new(param_name.span(), "no such parameter")
.to_compile_error()
.into()
}
}
struct IndexOfArgs {
param_name: syn::Ident,
stmt_params: syn::punctuated::Punctuated<syn::Ident, Token![,]>,
start_index: usize,
}
impl Parse for IndexOfArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let param_name = input.parse()?;
input.parse::<Token![in]>()?;
let param_list;
syn::bracketed!(param_list in input);
input.parse::<Token![+]>()?;
let start_index: syn::LitInt = input.parse()?;
let start_index = start_index.base10_parse()?;
let stmt_params = param_list.parse_terminated(syn::Ident::parse, Token![,])?;
Ok(Self {
param_name,
stmt_params,
start_index,
})
}
}