Current section
Files
Jump to
Current section
Files
lib/interceptor.ex
defmodule Interceptor do
@moduledoc """
Documentation for Interceptor.
"""
def on_success_default_callback(_mfa, _result, _started_at), do: :noop
def on_error_default_callback(_mfa, _error, _started_at), do: :noop
@doc """
TODO: Docs for intercept
"""
defmacro intercept([do: do_block_body]) do
updated_do_block = _intercept(__CALLER__.module, do_block_body)
[do: updated_do_block]
end
defp _intercept(caller, {:def, _metadata, function_hdr_body} = function_def) do
_intercept(caller, {:__block__, [], [function_def]})
end
defp _intercept(caller, {:__block__, _metadata, definitions} = do_block) do
definitions
|> Enum.map(&(add_calls(&1, caller)))
|> update_block_definitions(do_block)
end
defp _intercept(_caller, something_else), do: something_else
defp get_mfa(current_module, function_header) do
{function, _context, args} = function_header
{current_module, function, length(args)}
end
defp add_calls({:def, _metadata, [function_hdr | [[do: function_body]]]} = function, current_module) do
mfa = get_mfa(current_module, function_hdr)
function_body = function_body
|> set_on_before_callback(mfa)
|> set_on_after_callback(mfa)
|> set_on_success_error_callback(mfa)
|> set_lambda_wrapper(mfa)
new_function = function
|> put_elem(2, [function_hdr | [[do: function_body]]])
IO.puts("##################### RESULT")
IO.inspect(new_function)
end
defp add_calls({:defp, _metadata, [function_hdr | [[do: function_body]]]} = function, _current_module) do
function
end
defp add_calls(something_else, _current_module) do
something_else
end
defp set_lambda_wrapper(function_body, mfa) do
wrapper_function = get_interceptor_module_function_for(mfa, :wrapper)
wrapper_only_callback? = [
:on_before,
:on_after,
:on_success,
:on_failure
] |> Enum.all?(&is_nil(get_interceptor_module_function_for(mfa, &1)))
set_lambda_wrapper_in_place(function_body, mfa, wrapper_function, wrapper_only_callback?)
end
defp set_lambda_wrapper_in_place(function_body, _mfa, nil, _wrapper_only_callback?), do: function_body
defp set_lambda_wrapper_in_place(function_body, mfa, _wrapper_function, false),
do: raise "Wrapper needs to be the only callback configured. You configured another callback besides `wrapper` for the following function: #{inspect(mfa)}."
defp set_lambda_wrapper_in_place(function_body, mfa, wrapper_function, true),
do: wrap_block_in_lambda(function_body, mfa, wrapper_function)
defp set_on_success_error_callback(function_body, mfa) do
success_callback = get_interceptor_module_function_for(mfa, :on_success)
error_callback = get_interceptor_module_function_for(mfa, :on_error)
set_on_success_error_callback_in_place(function_body, mfa, success_callback, error_callback)
end
defp set_on_success_error_callback_in_place(function_body, _mfa, nil, nil), do: function_body
defp set_on_success_error_callback_in_place(function_body, mfa, success_callback, nil),
do: wrap_do_in_try_catch(function_body, mfa, success_callback, {__MODULE__, :on_error_default_callback})
defp set_on_success_error_callback_in_place(function_body, mfa, nil, error_callback),
do: wrap_do_in_try_catch(function_body, mfa, {__MODULE__, :on_success_default_callback}, error_callback)
defp set_on_success_error_callback_in_place(function_body, mfa, success_callback, error_callback),
do: wrap_do_in_try_catch(function_body, mfa, success_callback, error_callback)
defp set_on_after_callback(function_body, mfa) do
interceptor_callback = get_interceptor_module_function_for(mfa, :on_after)
set_on_after_callback_in_place(
function_body, mfa, interceptor_callback)
end
defp set_on_after_callback_in_place(
function_body, mfa, nil = _interceptor_callback), do: function_body
defp set_on_after_callback_in_place(
function_body, mfa,
{interceptor_module, interceptor_function}) do
new_var_name = :qwertyqwerty
new_var_not_hygienic = quote do
var!(unquote(Macro.var(new_var_name, nil)))
end
after_quoted_call = quote bind_quoted: [
interceptor_module: interceptor_module,
interceptor_function: interceptor_function,
mfa: Macro.escape(mfa),
result_var: new_var_not_hygienic
] do
Kernel.apply(interceptor_module, interceptor_function, [mfa, result_var])
end
append_to_function_body(function_body, after_quoted_call, new_var_name)
end
defp set_on_before_callback(function_body, mfa) do
interceptor_callback = get_interceptor_module_function_for(mfa, :on_before)
set_on_before_callback_in_place(
function_body, mfa, interceptor_callback)
end
defp set_on_before_callback_in_place(
function_body, mfa, nil = _interceptor_callback), do: function_body
defp set_on_before_callback_in_place(
function_body, mfa,
{interceptor_module, interceptor_function}) do
before_quoted_call = quote bind_quoted: [
interceptor_module: interceptor_module,
interceptor_function: interceptor_function,
mfa: Macro.escape(mfa)
] do
Kernel.apply(interceptor_module, interceptor_function, [mfa])
end
prepend_to_function_body(function_body, before_quoted_call)
end
defp get_interceptor_module_function_for({_module, _function, _arity} = to_intercept, interception_type) do
interception_configuration = Application.get_env(:interceptor, :configuration)
configuration = interception_configuration[to_intercept]
configuration && Keyword.get(configuration, interception_type)
end
def wrap_block_in_lambda(function_body, {_module, _func, _arity} = mfa, {wrapper_module, wrapper_func}) do
escaped_mfa = Macro.escape(mfa)
lambda_wrapped = quote do
fn ->
unquote(function_body)
end
end
quote do
Kernel.apply(unquote(wrapper_module), unquote(wrapper_func), [unquote(escaped_mfa), unquote(lambda_wrapped)])
end
end
def wrap_do_in_try_catch(function_body, {module, _func, _arity} = mfa, {success_module, success_func}, {error_module, error_func}) do
start_time_var_name = :blhargblharg # TODO: Generate randomly
# start_time_assignment = quote do
# var!(unquote(Macro.var(start_time_var_name, module))) = :os.system_time(:microsecond)
# end
# function_body = prepend_to_function_body(function_body, start_time_assignment)
result_var_name = :abcdefghi # TODO: Generate randomly
# TODO: Horrible hack, try to use the suggested way
# by Valim https://groups.google.com/forum/#!topic/elixir-lang-talk/maki_LbLLVI
new_var_not_hygienic = quote do
var!(unquote(Macro.var(result_var_name, module)))
end
time_var_not_hygienic = quote do
var!(unquote(Macro.var(start_time_var_name, module)))
end
# append the success call to end of the function body
quoted_success_call = quote bind_quoted: [
success_module: success_module,
success_func: success_func,
mfa: Macro.escape(mfa),
result_var: new_var_not_hygienic,
time_var: time_var_not_hygienic
] do
Kernel.apply(success_module, success_func, [mfa, result_var, time_var])
end
new_function_body = append_to_function_body(function_body, quoted_success_call, result_var_name)
escaped_mfa = Macro.escape(mfa)
try_catch_block = quote do
unquote(time_var_not_hygienic) = :os.system_time(:microsecond)
try do
unquote(new_function_body)
rescue
error ->
Kernel.apply(unquote(error_module), unquote(error_func), [unquote(escaped_mfa), error, unquote(time_var_not_hygienic)])
reraise(error, __STACKTRACE__)
end
end
end
defp prepend_to_function_body({:__block__, _metadata, statements} = body, quoted_call) do
body
|> put_elem(2, [quoted_call | statements])
end
defp prepend_to_function_body(single_statement, quoted_call) do
{:__block__, [], [quoted_call, single_statement]}
end
defp append_to_function_body(body, quoted_call, new_var_name \\ nil)
defp append_to_function_body({:__block__, _metadata, statements} = body, quoted_call, new_var_name) do
last_statement = Enum.at(statements, -1)
{_result_var, new_last_statements} = return_statement_result_after_quoted_call(last_statement, quoted_call, new_var_name)
new_statements = Enum.take(statements, length(statements) - 1) ++ new_last_statements
{:__block__, [], new_statements}
end
defp append_to_function_body(single_statement, quoted_call, new_var_name) do
{_result_var, new_last_statements} = return_statement_result_after_quoted_call(single_statement, quoted_call, new_var_name)
{:__block__, [], new_last_statements}
end
defp return_statement_result_after_quoted_call(statement, quoted_call, new_var_name \\ nil) do
new_result_var = case new_var_name do
nil -> :qwerty # TODO: Randomly generate this
_ -> new_var_name
end
{
new_result_var,
[ # first we store the statement result
{:=, [],
[
{new_result_var, [], nil},
statement,
]},
# then we call the interceptor function
quoted_call,
# finally we return the result
{new_result_var, [], nil}
]
}
end
defp update_block_definitions(new_definitions, {_block, _metadata, _definitions} = do_block) do
do_block
|> put_elem(2, new_definitions)
end
end