Last active
June 16, 2022 07:42
-
-
Save justgage/0ad132409d0a91db88db5bc6f84666b3 to your computer and use it in GitHub Desktop.
This will automatically add new functions to a facade file
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 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