Current section
Files
Jump to
Current section
Files
lib/snakebridge/helper_generator.ex
defmodule SnakeBridge.HelperGenerator do
@moduledoc """
Generates Elixir helper wrappers from registry data.
"""
@spec render_library(String.t(), list(), keyword()) :: String.t()
def render_library(library, helpers, opts \\ []) when is_binary(library) do
version = Keyword.get(opts, :version, Application.spec(:snakebridge, :vsn) |> to_string())
header = """
# Generated by SnakeBridge v#{version} - DO NOT EDIT MANUALLY
# Regenerate with: mix compile
# Helper library: #{library}
"""
modules = render_modules(library, helpers)
"""
#{header}
#{modules}
"""
|> Code.format_string!()
|> IO.iodata_to_binary()
|> SnakeBridge.Generator.ensure_final_newline()
end
@spec generate_helpers(list(), SnakeBridge.Config.t()) :: :ok
def generate_helpers(helpers, config) do
grouped = group_by_library(helpers)
if map_size(grouped) == 0 do
:ok
else
dir = Path.join(config.generated_dir, "helpers")
File.mkdir_p!(dir)
Enum.each(grouped, fn {library, entries} ->
source = render_library(library, entries, version: Application.spec(:snakebridge, :vsn))
path = Path.join(dir, "#{library}.ex")
SnakeBridge.Generator.write_if_changed(path, source)
end)
:ok
end
end
defp group_by_library(helpers) do
helpers
|> Enum.map(&helper_name/1)
|> Enum.zip(helpers)
|> Enum.reduce(%{}, fn
{nil, _}, acc ->
acc
{name, helper}, acc ->
case String.split(name, ".", parts: 2) do
[library, _rest] ->
Map.update(acc, library, [helper], fn existing -> [helper | existing] end)
_ ->
acc
end
end)
|> Map.new(fn {library, entries} ->
{library, Enum.reverse(entries)}
end)
end
defp helper_name(%{"name" => name}) when is_binary(name), do: name
defp helper_name(%{name: name}) when is_binary(name), do: name
defp helper_name(_), do: nil
defp render_modules(library, helpers) do
root = root_module(library)
helpers
|> Enum.map(&normalize_helper(library, &1))
|> Enum.group_by(& &1.module)
|> Enum.sort_by(fn {module, _} -> module end)
|> Enum.map_join("\n\n", fn {module, entries} ->
moduledoc = if module == root, do: root_moduledoc(library), else: " @moduledoc false"
functions =
entries |> Enum.sort_by(& &1.function) |> Enum.map_join("\n\n", &render_function/1)
"""
defmodule #{module} do
#{moduledoc}
#{indent(functions, 2)}
end
"""
end)
end
defp root_module(library) do
[Macro.camelize(library), "Helpers"] |> Enum.join(".")
end
defp root_moduledoc(library) do
" @moduledoc #{inspect("Helper wrappers for `#{library}`.")}"
end
defp normalize_helper(library, helper) do
helper_name = helper_name(helper) || ""
segments = String.split(helper_name, ".")
module_segments =
segments
|> Enum.drop(1)
|> Enum.drop(-1)
|> Enum.map(&Macro.camelize/1)
module = ([Macro.camelize(library), "Helpers"] ++ module_segments) |> Enum.join(".")
%{
helper: helper_name,
module: module,
function: helper_function_name(segments),
parameters: helper_parameters(helper),
docstring: helper_docstring(helper)
}
end
defp helper_function_name(segments) do
segments
|> List.last()
|> sanitize_name()
end
defp helper_parameters(%{"parameters" => params}) when is_list(params), do: params
defp helper_parameters(%{parameters: params}) when is_list(params), do: params
defp helper_parameters(_), do: []
defp helper_docstring(%{"docstring" => doc}) when is_binary(doc), do: doc
defp helper_docstring(%{docstring: doc}) when is_binary(doc), do: doc
defp helper_docstring(_), do: ""
defp render_function(%{helper: helper_name, function: name, parameters: params, docstring: doc}) do
plan = SnakeBridge.Generator.build_params(params)
param_names = Enum.map(plan.required, & &1.name)
args_name = extra_args_name(param_names)
args = args_expr(param_names, plan.has_args, args_name)
call = helper_call(helper_name, args)
spec = function_spec(name, plan.required, plan.has_args)
normalize = normalize_args_line(plan.has_args, args_name, 8)
docstring = String.trim(doc)
"""
@doc #{inspect(docstring, limit: :infinity, printable_limit: :infinity)}
#{spec}
def #{name}(#{param_list(param_names, plan.has_args, plan.has_opts, args_name)}) do
#{normalize} #{call}
end
"""
end
defp helper_call(name, args) do
"SnakeBridge.Runtime.call_helper(\"#{name}\", #{args}, opts)"
end
defp function_spec(name, param_entries, has_args) do
args = Enum.map(param_entries, fn _ -> "term()" end)
args = if has_args, do: args ++ ["list(term())"], else: args
args = args ++ ["keyword()"]
"@spec #{name}(#{Enum.join(args, ", ")}) :: {:ok, term()} | {:error, term()}"
end
defp param_list(param_names, has_args, has_opts, args_name) do
param_names
|> maybe_add_args(has_args, args_name)
|> maybe_add_opts(has_opts)
|> Enum.join(", ")
end
defp args_expr(param_names, true, args_name) do
base = "[" <> Enum.join(param_names, ", ") <> "]"
base <> " ++ List.wrap(" <> args_name <> ")"
end
defp args_expr(param_names, false, _args_name) do
"[" <> Enum.join(param_names, ", ") <> "]"
end
defp maybe_add_args(items, true, args_name), do: items ++ ["#{args_name} \\\\ []"]
defp maybe_add_args(items, false, _args_name), do: items
defp maybe_add_opts(items, _has_opts), do: items ++ ["opts \\\\ []"]
defp normalize_args_line(true, args_name, indent) do
String.duplicate(" ", indent) <>
"{#{args_name}, opts} = SnakeBridge.Runtime.normalize_args_opts(#{args_name}, opts)\n"
end
defp normalize_args_line(false, _args_name, _indent), do: ""
defp extra_args_name(param_names) do
if "args" in param_names do
"extra_args"
else
"args"
end
end
defp sanitize_name(%{"name" => name}), do: sanitize_name(name)
defp sanitize_name(name) when is_binary(name) do
name
|> Macro.underscore()
|> String.replace(~r/[^a-z0-9_]/, "_")
|> ensure_identifier()
end
defp ensure_identifier(<<first::utf8, _rest::binary>> = name)
when first in ?a..?z or first == ?_ do
name
end
defp ensure_identifier(<<first::utf8, _rest::binary>> = name) when first in ?0..?9 do
"_" <> name
end
defp ensure_identifier(""), do: "_unnamed"
defp ensure_identifier(name), do: "_" <> name
defp indent(text, spaces) do
prefix = String.duplicate(" ", spaces)
text
|> String.split("\n")
|> Enum.map_join("\n", fn line -> prefix <> line end)
end
end