#![feature(proc_macro)]
#![recursion_limit = "128"]
#[macro_use] mod acquire;
mod new_mock;
mod given;
mod expect;
mod generate;
mod data;
extern crate proc_macro;
#[macro_use] extern crate lazy_static;
extern crate syn;
#[macro_use] extern crate synom;
#[macro_use] extern crate quote;
#[cfg(test)]#[macro_use]
extern crate galvanic_assert;
use proc_macro::TokenStream;
use new_mock::handle_new_mock;
use given::handle_given;
use expect::handle_expect_interactions;
use generate::handle_generate_mocks;
use data::*;
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::Path;
enum MockedTraitLocation {
TraitDef(syn::Path),
Referred(syn::Path)
}
named!(parse_trait_path -> MockedTraitLocation,
delimited!(
punct!("("),
do_parse!(
external: option!(alt!(keyword!("intern") | keyword!("extern"))) >> path: call!(syn::parse::path) >>
(match external {
Some(..) => MockedTraitLocation::Referred(path),
None => MockedTraitLocation::TraitDef(path)
})
),
punct!(")")
)
);
#[proc_macro_attribute]
pub fn mockable(args: TokenStream, input: TokenStream) -> TokenStream {
let s = input.to_string();
let trait_item = syn::parse_item(&s).expect("Expecting a trait definition.");
let args_str = &args.to_string();
match trait_item.node {
syn::ItemKind::Trait(safety, generics, bounds, items) => {
let mut mockable_traits = acquire!(MOCKABLE_TRAITS);
if args_str.is_empty() {
mockable_traits.insert(trait_item.ident.clone().into(), TraitInfo::new(safety, generics, bounds, items));
return input;
}
let trait_location = parse_trait_path(args_str)
.expect(concat!("#[mockable(..)] requires the absolute path of the trait's module.",
"It must be preceded with `extern`/`intern` if the trait is defined in another crate/module"));
match trait_location {
MockedTraitLocation::TraitDef(mut trait_path) => {
trait_path.segments.push(trait_item.ident.clone().into());
mockable_traits.insert(trait_path, TraitInfo::new(safety, generics, bounds, items));
input
},
MockedTraitLocation::Referred(mut trait_path) => {
trait_path.segments.push(trait_item.ident.clone().into());
mockable_traits.insert(trait_path, TraitInfo::new(safety, generics, bounds, items));
"".parse().unwrap()
}
}
},
_ => panic!("Expecting a trait definition.")
}
}
#[proc_macro_attribute]
pub fn use_mocks(_: TokenStream, input: TokenStream) -> TokenStream {
use MacroInvocationPos::*;
let mut reassembled = String::new();
let parsed = syn::parse_item(&input.to_string()).unwrap();
let mut remainder = quote!(#parsed).to_string();
let mut absolute_pos = 0;
while !remainder.is_empty() {
match find_next_mock_macro_invocation(&remainder) {
None => {
reassembled.push_str(&remainder);
remainder = String::new();
},
Some(invocation) => {
let (left, new_absolute_pos, right) = match invocation {
NewMock(pos) => handle_macro(&remainder, pos, absolute_pos, handle_new_mock),
Given(pos) => handle_macro(&remainder, pos, absolute_pos, handle_given),
ExpectInteractions(pos) => handle_macro(&remainder, pos, absolute_pos, handle_expect_interactions),
};
absolute_pos = new_absolute_pos;
reassembled.push_str(&left);
remainder = right;
}
}
}
let mut mock_using_item = syn::parse_item(&reassembled).expect("Reassembled function whi");
mock_using_item.vis = syn::Visibility::Public;
let item_ident = &mock_using_item.ident;
let item_vis = &mock_using_item.vis;
let mod_fn = syn::Ident::from(format!("mod_{}", item_ident));
if let syn::ItemKind::Mod(Some(ref mut mod_items)) = mock_using_item.node {
insert_use_generated_mocks_into_modules(mod_items);
}
let mocks = handle_generate_mocks();
let generated_mock = (quote! {
#[allow(unused_imports)]
#item_vis use self::#mod_fn::#item_ident;
mod #mod_fn {
#![allow(dead_code)]
#![allow(unused_imports)]
#![allow(unused_variables)]
use super::*;
#mock_using_item
pub(in self) mod mock {
use std;
use super::*;
#(#mocks)*
}
}
}).to_string();
debug(&item_ident, &generated_mock);
generated_mock.parse().unwrap()
}
fn insert_use_generated_mocks_into_modules(mod_items: &mut Vec<syn::Item>) {
for item in mod_items.iter_mut() {
if let syn::ItemKind::Mod(Some(ref mut sub_mod_items)) = item.node {
insert_use_generated_mocks_into_modules(sub_mod_items);
}
}
mod_items.push(syn::parse_item(quote!(pub use super::*;).as_str()).unwrap());
}
fn debug(item_ident: &syn::Ident, generated_mock: &str) {
if let Some((_, path)) = env::vars().find(|&(ref key, _)| key == "GA_WRITE_MOCK") {
if path.is_empty() {
println!("{}", generated_mock);
} else {
let success = File::create(Path::new(&path).join(&(item_ident.to_string())))
.and_then(|mut f| f.write_all(generated_mock.as_bytes()));
if let Err(err) = success {
eprintln!("Unable to write generated mock to file '{}' because: {}", path, err);
}
}
}
}
fn has_balanced_quotes(source: &str) -> bool {
let mut count = 0;
let mut skip = false;
for c in source.chars() {
if skip {
skip = false;
continue;
}
if c == '\\' {
skip = true;
} else if c == '\"' {
count += 1;
}
}
count % 2 == 0
}
enum MacroInvocationPos {
NewMock(usize),
Given(usize),
ExpectInteractions(usize),
}
fn find_next_mock_macro_invocation(source: &str) -> Option<MacroInvocationPos> {
use MacroInvocationPos::*;
let macro_names = ["new_mock !", "given !", "expect_interactions !"];
macro_names.into_iter()
.filter_map(|&mac| {
source.find(mac).and_then(|pos| {
if has_balanced_quotes(&source[.. pos]) {
Some((pos, mac))
} else { None }
})
})
.min_by_key(|&(pos, _)| pos)
.and_then(|(pos, mac)| Some(match mac {
"new_mock !" => NewMock(pos),
"given !" => Given(pos),
"expect_interactions !" => ExpectInteractions(pos),
_ => panic!("Unreachable. No variant for macro name: {}", mac)
}))
}
fn handle_macro<F>(source: &str, mac_pos_relative_to_source: usize, absolute_pos_of_source: usize, handler: F) -> (String, usize, String)
where F: Fn(&str, usize) -> (String, String) {
let absolute_pos_of_mac = absolute_pos_of_source + mac_pos_relative_to_source;
let (left_of_mac, right_with_mac) = source.split_at(mac_pos_relative_to_source);
let (mut generated_source, unhandled_source) = handler(right_with_mac, absolute_pos_of_mac);
generated_source.push_str(&unhandled_source);
(left_of_mac.to_string(), absolute_pos_of_mac, generated_source)
}
#[cfg(test)]
mod test_has_balanced_quotes {
use super::*;
#[test]
fn should_have_balanced_quotes_if_none_exist() {
let x = "df df df";
assert!(has_balanced_quotes(x));
}
#[test]
fn should_have_balanced_quotes_if_single_pair() {
let x = "df \"df\" df";
assert!(has_balanced_quotes(x));
}
#[test]
fn should_have_balanced_quotes_if_single_pair_with_escapes() {
let x = "df \"d\\\"f\" df";
assert!(has_balanced_quotes(x));
}
#[test]
fn should_have_balanced_quotes_if_multiple_pairs() {
let x = "df \"df\" \"df\" df";
assert!(has_balanced_quotes(x));
}
#[test]
fn should_not_have_balanced_quotes_if_single() {
let x = "df \"df df";
assert!(!has_balanced_quotes(x));
}
#[test]
fn should_not_have_balanced_quotes_if_escaped_pair() {
let x = "df \"d\\\" df";
assert!(!has_balanced_quotes(x));
}
}