Skip to content

Instantly share code, notes, and snippets.

@justgage
Last active June 16, 2022 07:42
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save justgage/0ad132409d0a91db88db5bc6f84666b3 to your computer and use it in GitHub Desktop.
Save justgage/0ad132409d0a91db88db5bc6f84666b3 to your computer and use it in GitHub Desktop.
This will automatically add new functions to a facade file
defmodule Mix.Tasks.Gen.Fn do
use Mix.Task
require Logger
@moduledoc """
A code generator to create a function on a facade. This will also create the facade if it doesn't exist.
You use it like this:
mix gen.fn "Widget.create(arg1, arg2 \\\\ 0)"
This will create an function file like this: `lib/your_app/widget/create.ex`
and help you modify `lib/your_app/widget.ex`
"""
@shortdoc "Generates a new function file for facades in YourApp."
@default_opts []
def run(args) do
{_merged_opts, words_after_command, _invalid} = parse_opts(args)
{function_signature, _fields} = parse_words(words_after_command)
case parse_signature(function_signature) do
{:ok, args} ->
write_files(args)
{:error, _} ->
Logger.error(
"Woops, input wasn't as expected. Should be a function call with double quotes around it, got: #{
function_signature
}"
)
exit(1)
end
end
defp write_files(args) do
function_file = EEx.eval_file("./lib/mix/templates/function_file.eex", Enum.into(args, []))
function_file_test =
EEx.eval_file("./lib/mix/templates/function_file_test.eex", Enum.into(args, []))
modified_facade = modify_or_create_facade(args)
write_file(args.function_file_path, function_file)
write_file(args.function_file_test_path, function_file_test)
update_file(args.facade_file_path, modified_facade)
System.cmd("mix", ["format"])
end
defp parse_signature(signature) do
with [facade_name, function_call] <- String.split(signature, "."),
[function_file_name, args_without_parenthesis] <-
function_call |> String.replace("?", "") |> String.split("(") do
arguments = "(" <> args_without_parenthesis
facade_file_name = Macro.underscore(facade_name)
function_name = Macro.camelize(function_file_name)
facade_folder_path = "./lib/your_app/#{facade_file_name}"
facade_test_folder_path = "./test/your_app/#{facade_file_name}"
{:ok,
%{
function_call: function_call,
facade_name: Macro.camelize(facade_name),
facade_file_name: facade_file_name,
facade_file_path: "#{facade_folder_path}/#{facade_file_name}.ex",
function_name: function_name,
function_file_name: function_file_name,
function_file_path: "#{facade_folder_path}/private/#{function_file_name}.ex",
facade_folder_path: facade_folder_path,
function_file_test_folder_path: facade_test_folder_path,
function_file_test_path: "#{facade_test_folder_path}/#{function_file_name}_test.exs",
arguments: arguments
}}
end
end
defp modify_or_create_facade(
args = %{
facade_file_path: facade_file_path,
facade_name: facade_name,
facade_folder_path: facade_folder_path,
function_file_test_folder_path: function_file_test_folder_path
}
) do
File.mkdir_p(facade_folder_path <> "/private")
File.mkdir_p(function_file_test_folder_path)
if File.exists?(facade_file_path) do
file_stream = facade_file_path |> File.read!() |> String.split("\n")
file_map = modify_facade(file_stream, args)
file_map.file
else
IO.puts("#{facade_file_path} doesn't exist, creating...")
contents =
EEx.eval_file(
"./lib/mix/templates/facade.eex",
facade_name: facade_name
)
write_file(facade_file_path, contents)
modify_or_create_facade(args)
end
end
defp modify_facade(lines, %{
function_call: function_call,
facade_name: facade_name,
function_name: function_name,
facade_file_path: facade_file_path
}) do
delegate =
EEx.eval_file(
"./lib/mix/templates/delegate.eex",
function_call: function_call,
function_name: function_name
)
Enum.reduce(
lines,
%{found_line?: false, file: nil, line_num: 1, lines_from_found_line: -1},
fn line, metadata ->
# Find beginning of imports
metadata =
if String.contains?(line, "YourApp.#{facade_name}.Private.{") do
%{metadata | found_line?: true, lines_from_found_line: 0}
else
metadata
end
metadata =
if metadata.found_line? && String.contains?(line, "}") do
if metadata.lines_from_found_line == 0 do
Logger.error(
"Can't modify #{facade_file_path}:#{metadata.line_num} The `{` and the `}` can't be on the same line in the alias"
)
exit(1)
end
maybe_comma = if metadata.lines_from_found_line > 1, do: ","
%{
metadata
| found_line?: false,
file: metadata.file <> "#{maybe_comma}\n " <> function_name
}
else
metadata
end
# Add delegate line
# This works because the module "end" doesn't have any indentation
metadata =
if line == "end" do
%{metadata | file: metadata.file <> "\n" <> delegate}
else
metadata
end
# modify this for future iterations
metadata =
if metadata.lines_from_found_line != -1 do
%{metadata | lines_from_found_line: metadata.lines_from_found_line + 1}
else
metadata
end
# increase line number
metadata = %{metadata | line_num: metadata.line_num + 1}
if metadata.file == nil do
%{metadata | file: line}
else
%{metadata | file: metadata.file <> "\n" <> line}
end
end
)
end
defp write_file(path, contents) do
if File.exists?(path) do
Logger.error("Looks like this file was already generated: #{path}")
exit(1)
end
File.write!(path, contents)
IO.puts("#{IO.ANSI.green()}File wrote to: #{IO.ANSI.faint()}#{path}#{IO.ANSI.reset()}")
end
defp update_file(path, contents) do
if File.exists?(path) do
File.write!(path, contents)
IO.puts("#{IO.ANSI.green()}File modified: #{IO.ANSI.faint()}#{path}#{IO.ANSI.reset()}")
else
Logger.error("You must create the file before you update it: #{path}")
exit(1)
end
end
defp parse_words([function_file_name | unparsed_fields]) do
Macro.camelize(function_file_name)
fields =
unparsed_fields
|> Enum.map(fn kv ->
[k, v] = String.split(kv, ":")
{k, v}
end)
|> Enum.into(%{})
{function_file_name, fields}
end
defp parse_words([]) do
Logger.error(
"You need to provide the event's file name as the first argument. EG: org_whitelisted_projector"
)
exit(1)
end
defp parse_opts(args) do
{opts, words_after_command, invalid} = OptionParser.parse(args, switches: [debug: :boolean])
merged_opts =
@default_opts
|> Keyword.merge(opts)
{merged_opts, words_after_command, invalid}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment