Last active
March 13, 2023 08:16
-
-
Save mayel/e82a73d09ae608fbea3308359bc9c3fe to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Module.Extend do | |
@doc """ | |
Extend a module (i.e. define `defdelegate` and `defoverridable` for all functions from the source module in the current module. | |
Usage: | |
import Module.Extend | |
extend_module Common.Text | |
""" | |
defmacro extend_module(module) do | |
require Logger | |
module = Macro.expand(module, __CALLER__) | |
Logger.info("[Module.Extend] Extending module #{inspect(module)}") | |
functions = module.__info__(:functions) | |
signatures = | |
Enum.map(functions, fn {name, arity} -> | |
args = | |
if arity == 0 do | |
[] | |
else | |
Enum.map(1..arity, fn i -> | |
{String.to_atom(<<?x, ?A + i - 1>>), [], nil} | |
end) | |
end | |
{name, [], args} | |
end) | |
zipped = List.zip([signatures, functions]) | |
for sig_func <- zipped do | |
quote do | |
Module.register_attribute(__MODULE__, :extend_module, persist: true, accumulate: false) | |
@extend_module unquote(module) | |
defdelegate unquote(elem(sig_func, 0)), to: unquote(module) | |
defoverridable unquote([elem(sig_func, 1)]) | |
end | |
end | |
end | |
@doc """ | |
Copy the code defining a function from its original module to one that extends it (or a manually specified module). | |
Usage: `Module.Extend.inject_function(Common.TextExtended, :blank?)` | |
""" | |
def inject_function(module, fun, target_module \\ nil) do | |
with {:ok, module_file} <- module_file(module), | |
orig_module when not is_nil(orig_module) <- target_module || List.first(module.__info__(:attributes)[:extend_module]) do | |
code = function_code(orig_module, fun) | |
IO.inspect(code, label: "Injecting the code from `#{orig_module}.#{fun}` into #{module_file}") | |
inject_before_final_end(module_file, code) | |
end | |
end | |
@doc "Return the path of code file for a module (in dev only)" | |
def module_file(module) when is_atom(module) do | |
{:ok, to_string(module.__info__(:compile)[:source])} | |
end | |
@doc "Return the code of a module (in dev only)" | |
def module_code(module) when is_atom(module) do | |
with {:ok, code_file} <- module_file(module) do | |
File.read(code_file) | |
end | |
end | |
def module_code(code_file) when is_binary(code_file) do | |
File.read(code_file) | |
end | |
def function_code(module, fun) do | |
with {:ok, code} <- module_code(module), | |
{first_line, last_line} <- function_line_numbers(module, fun) do | |
code | |
|> Common.Text.split_lines() | |
|> Enum.slice((first_line-1)..(last_line-1)) | |
|> Enum.join("\n") | |
end | |
end | |
@doc "Return the numbers (as a tuple) of the first and last lines of a function's definition in a module" | |
def function_line_numbers(module, fun) when is_atom(module) do | |
with {:ok, code} <- module_file(module) do | |
function_line_numbers(code, fun) | |
end | |
end | |
def function_line_numbers(module_code, fun) when is_binary(module_code) do | |
module_code | |
|> Code.string_to_quoted!() | |
|> function_line_numbers(fun) | |
end | |
def function_line_numbers(module_ast, fun) do | |
module_ast | |
|> Macro.prewalk(nil, fn | |
result = {:def, [line: number], [{^fun, _, _} | _]}, acc -> {result, acc || number} | |
result = {:defp, [line: number], [{^fun, _, _} | _]}, acc -> {result, acc || number} | |
result = {:def, [line: number], [{:when, _, [{^fun, _, _} | _]} | _]}, acc -> {result, acc || number} | |
result = {:defp, [line: number], [{:when, _, [{^fun, _, _} | _]} | _]}, acc -> {result, acc || number} | |
other = {prefix, [line: number], _}, acc when prefix in [:def, :defp] -> | |
if acc do | |
throw {acc, number-1} | |
else | |
{other, acc} | |
end | |
other, acc -> | |
{other, acc} | |
end) | |
catch | |
numbers -> | |
numbers | |
end | |
defp inject_before_final_end(file_path, content_to_inject) do | |
file = File.read!(file_path) | |
if String.contains?(file, content_to_inject) do | |
:ok | |
else | |
Mix.shell().info([:green, "* injecting ", :reset, Path.relative_to_cwd(file_path)]) | |
content = | |
file | |
|> String.trim_trailing() | |
|> String.trim_trailing("end") | |
|> Kernel.<>("\n" <> content_to_inject) | |
|> Kernel.<>("\nend\n") | |
formatted_content = Code.format_string!(content) |> IO.iodata_to_binary() | |
File.write!(file_path, formatted_content) | |
end | |
end | |
end | |
defmodule Common.Text do | |
def blank?(str_or_nil), do: "" == str_or_nil |> to_string() |> String.trim() | |
def split_lines(string) when is_binary(string), | |
do: :binary.split(string, ["\r", "\n", "\r\n"], [:global]) | |
end | |
defmodule Common.TextExtended do | |
import Module.Extend | |
extend_module Common.Text | |
def blank?(str_or_nil \\ 1) do | |
require Logger | |
Logger.info("Check if #{str_or_nil} is considered blank") | |
# call function from original module: | |
super(str_or_nil) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment