Current section

Files

Jump to
temporal_sdk lib utils code.ex
Raw

lib/utils/code.ex

defmodule TemporalSdk.Utils.Code do
@moduledoc false
@spec delegate_moduledoc(Keyword.t()) :: Macro.t()
defmacro delegate_moduledoc(opts) do
{opts, _binding} = Code.eval_quoted(opts, [], __CALLER__)
from = opts |> Keyword.fetch!(:from) |> String.to_charlist()
{:ok, forms} = :epp.parse_file(from, [~c"include", ~c"."], [])
moduledoc_path = fetch_moduledoc_path(forms)
moduledoc_text = if moduledoc_path, do: exdoc!(moduledoc_path), else: nil
quote do
if unquote(moduledoc_path), do: @external_resource(unquote(moduledoc_path))
if unquote(moduledoc_text), do: @moduledoc(unquote(moduledoc_text))
end
end
@spec delegate_all(Keyword.t()) :: Macro.t()
defmacro delegate_all(opts) do
{opts, _binding} = Code.eval_quoted(opts, [], __CALLER__)
from = opts |> Keyword.fetch!(:from) |> String.to_charlist()
{:ok, forms} = :epp.parse_file(from, [~c"include", ~c"."], [])
exported_types = fetch_exported_types(forms, [])
exported_functions = fetch_exported_functions(forms, [])
{module_path, module_name} = fetch_module(forms)
moduledoc_path = fetch_moduledoc_path(forms)
moduledoc_text = if moduledoc_path, do: exdoc!(moduledoc_path), else: nil
functions = fetch_functions(forms, exported_functions, [])
module_ast =
quote do
if unquote(module_path), do: @external_resource(unquote(module_path))
if unquote(moduledoc_path), do: @external_resource(unquote(moduledoc_path))
if unquote(moduledoc_text), do: @moduledoc(unquote(moduledoc_text))
end
funtions_ast =
for {fun_name, args, spec, doc_path, group} <- functions do
doc = exdoc!(doc_path)
specs = translate_spec(fun_name, spec, exported_types, module_name)
quote do
if unquote(doc_path), do: @external_resource(unquote(doc_path))
if unquote(group), do: @doc(group: unquote(group))
if unquote(doc), do: @doc(unquote(doc))
unquote_splicing(for s <- specs, do: quote(do: @spec(unquote(s))))
def unquote(fun_name)(unquote_splicing(args)) do
unquote(module_name).unquote(fun_name)(unquote_splicing(args))
end
end
end
quote do
unquote(module_ast)
unquote(funtions_ast)
end
end
defp fetch_exported_functions([{:attribute, _, :export, exported} | tforms], acc),
do: fetch_exported_functions(tforms, Keyword.keys(exported) ++ acc)
defp fetch_exported_functions([_ | tforms], acc), do: fetch_exported_functions(tforms, acc)
defp fetch_exported_functions([], acc), do: acc
defp fetch_module([{:attribute, _, :file, {path, _}}, {:attribute, _, :module, mod} | _]),
do: {List.to_string(path), mod}
defp fetch_module(_), do: {nil, nil}
defp fetch_moduledoc_path([
_,
_,
{:attribute, _, :file, {path, _}},
{:attribute, _, :moduledoc, _} | _
]) do
do_file_path(path)
end
defp fetch_moduledoc_path(_), do: nil
defp fetch_exported_types(
[{:attribute, _, :type, _}, {:attribute, _, :export_type, [{type, 0}]} | tforms],
acc
),
do: fetch_exported_types(tforms, [type | acc])
defp fetch_exported_types([_ | tforms], acc), do: fetch_exported_types(tforms, acc)
defp fetch_exported_types([], acc), do: acc
defp fetch_functions(
[
{:attribute, _, :file, {doc_path, _}},
{:attribute, _, :doc, _},
{:attribute, _, :file, _},
{:attribute, _, :doc, %{group: group}},
{:attribute, _, :spec, spec},
{:function, _, fun, _arity, clauses} | tforms
],
exported_functions,
acc
) do
case Enum.member?(exported_functions, fun) do
true ->
fetch_functions(tforms, exported_functions, [
{fun, do_vars(clauses, spec), spec, do_file_path(doc_path), List.to_string(group)} | acc
])
false ->
fetch_functions(tforms, exported_functions, acc)
end
end
defp fetch_functions(
[
{:attribute, _, :doc, %{group: group}},
{:attribute, _, :file, {doc_path, _}},
{:attribute, _, :doc, _},
{:attribute, _, :file, _},
{:attribute, _, :spec, spec},
{:function, _, fun, _arity, clauses} | tforms
],
exported_functions,
acc
) do
case Enum.member?(exported_functions, fun) do
true ->
fetch_functions(tforms, exported_functions, [
{fun, do_vars(clauses, spec), spec, do_file_path(doc_path), List.to_string(group)} | acc
])
false ->
fetch_functions(tforms, exported_functions, acc)
end
end
defp fetch_functions(
[
{:attribute, _, :file, {doc_path, _}},
{:attribute, _, :doc, _},
{:attribute, _, :file, _},
{:attribute, _, :spec, spec},
{:function, _, fun, _arity, clauses} | tforms
],
exported_functions,
acc
) do
case Enum.member?(exported_functions, fun) do
true ->
fetch_functions(tforms, exported_functions, [
{fun, do_vars(clauses, spec), spec, do_file_path(doc_path), nil} | acc
])
false ->
fetch_functions(tforms, exported_functions, acc)
end
end
defp fetch_functions(
[
{:attribute, _, :doc, %{group: group}},
{:attribute, _, :spec, spec},
{:function, _, fun, _arity, clauses} | tforms
],
exported_functions,
acc
) do
case Enum.member?(exported_functions, fun) do
true ->
fetch_functions(tforms, exported_functions, [
{fun, do_vars(clauses, spec), spec, nil, List.to_string(group)} | acc
])
false ->
fetch_functions(tforms, exported_functions, acc)
end
end
defp fetch_functions(
[
{:attribute, _, :doc, false},
{:attribute, _, :spec, _spec},
{:function, _, _fun, _arity, _clauses} | tforms
],
exported_functions,
acc
) do
fetch_functions(tforms, exported_functions, acc)
end
defp fetch_functions(
[
{:attribute, _, :spec, spec},
{:function, _, fun, _arity, clauses} | tforms
],
exported_functions,
acc
) do
case Enum.member?(exported_functions, fun) do
true ->
fetch_functions(tforms, exported_functions, [
{fun, do_vars(clauses, spec), spec, nil, nil} | acc
])
false ->
fetch_functions(tforms, exported_functions, acc)
end
end
defp fetch_functions(
[
{:attribute, _, :doc, false},
{:function, _, _fun, _arity, _clauses} | tforms
],
exported_functions,
acc
) do
fetch_functions(tforms, exported_functions, acc)
end
defp fetch_functions(
[{:function, _, fun, _arity, clauses} | tforms],
exported_functions,
acc
) do
case Enum.member?(exported_functions, fun) do
true ->
fetch_functions(tforms, exported_functions, [
{fun, do_vars(clauses, nil), nil, nil, nil} | acc
])
false ->
fetch_functions(tforms, exported_functions, acc)
end
end
defp fetch_functions([_ | tforms], exported_functions, acc),
do: fetch_functions(tforms, exported_functions, acc)
defp fetch_functions([], _exported_functions, acc), do: Enum.sort(acc)
defp do_vars(clauses, spec) do
names_from_spec =
case spec do
{_name_arity, fun_types} ->
fun_types
|> Enum.map(&extract_names_from_fun_type/1)
|> Enum.reduce(nil, &merge_names/2)
_ ->
nil
end
names_from_clauses =
clauses
|> Enum.map(fn {:clause, _, vars, _, _} -> extract_names_from_vars(vars) end)
|> Enum.reduce(nil, &merge_names/2)
merged_names = merge_names(names_from_spec, names_from_clauses)
arity =
case clauses do
[{:clause, _, vars, _, _} | _] -> length(vars)
_ -> 0
end
if arity > 0 do
0..(arity - 1)
|> Enum.map(fn i ->
name = (merged_names && Enum.at(merged_names, i)) || "arg#{i + 1}"
{name |> Macro.underscore() |> String.to_atom(), [], nil}
end)
else
[]
end
end
defp extract_names_from_fun_type({:type, _, :fun, [{:type, _, :product, args}, _]}) do
Enum.map(args, &extract_name_from_type/1)
end
defp extract_names_from_fun_type({:type, _, :bounded_fun, [fun_type, constraints]}) do
names = extract_names_from_fun_type(fun_type)
Enum.reduce(constraints, names, fn
{:type, _, :constraint, [{:atom, _, :is_subtype}, [{:var, _, var}, _]]}, acc ->
case Enum.find_index(names, &(&1 == Atom.to_string(var))) do
nil -> acc
_index -> acc
end
_, acc ->
acc
end)
end
defp extract_names_from_fun_type(_), do: nil
defp extract_name_from_type({:ann_type, _, [{:var, _, var}, _]}), do: Atom.to_string(var)
defp extract_name_from_type({:var, _, var}) when var != :_, do: Atom.to_string(var)
defp extract_name_from_type(_), do: nil
defp extract_names_from_vars(vars) do
Enum.map(vars, &extract_name_from_var/1)
end
defp extract_name_from_var({:var, _, var}) when var != :_, do: Atom.to_string(var)
defp extract_name_from_var({:match, _, left, right}) do
extract_name_from_var(left) || extract_name_from_var(right)
end
defp extract_name_from_var(_), do: nil
defp merge_names(nil, names2), do: names2
defp merge_names(names1, nil), do: names1
defp merge_names(names1, names2) do
Enum.zip(names1, names2)
|> Enum.map(fn
{nil, n2} -> n2
{n1, _} -> n1
end)
end
defp translate_spec(_fun_name, {{name, _arity}, fun_types}, exported_types, module_name) do
fun_types
|> Enum.map(fn fun_type ->
{:type, _line, :fun, [{:type, _, :product, args_ast}, return_type_ast]} = fun_type
elixir_args = Enum.map(args_ast, fn t -> do_type(t, exported_types, module_name) end)
elixir_return = do_type(return_type_ast, exported_types, module_name)
{:"::", [], [{name, [], elixir_args}, elixir_return]}
end)
end
defp translate_spec(fun_name, nil, _exported_types, _module_name),
do: raise("Missing specification for exported function: `#{fun_name}`")
defp do_type({:ann_type, _line, [{:var, _, var_name}, type_ast]}, et, mn) do
elixir_var_name = var_name |> Atom.to_string() |> Macro.underscore() |> String.to_atom()
{:"::", [], [{elixir_var_name, [], nil}, do_type(type_ast, et, mn)]}
end
defp do_type({:type, _line, :union, types}, et, mn) do
types
|> Enum.map(&do_type(&1, et, mn))
|> Enum.reverse()
|> Enum.reduce(fn type, acc -> {:|, [], [type, acc]} end)
end
defp do_type({:user_type, _line, type_name, args}, exported_types, module_name) do
elixir_args = Enum.map(args, &do_type(&1, exported_types, module_name))
case Enum.member?(exported_types, type_name) do
false -> {type_name, [], elixir_args}
true -> {{:., [], [module_name, type_name]}, [], elixir_args}
end
end
defp do_type({:remote_type, _line, [{:atom, _, mod}, {:atom, _, type}, args]}, et, mn) do
elixir_args = Enum.map(args, &do_type(&1, et, mn))
{{:., [], [mod, type]}, [], elixir_args}
end
defp do_type({:type, _line, :tuple, elements}, et, mn) do
elixir_elements = Enum.map(elements, &do_type(&1, et, mn))
{:{}, [], elixir_elements}
end
defp do_type({:type, _line, :list, [type]}, et, mn), do: [do_type(type, et, mn)]
defp do_type({:type, _line, :list, [{:type, _, :tuple, [{:atom, _, key}, value_type]}]}, et, mn) do
[{key, do_type(value_type, et, mn)}]
end
defp do_type({:type, _line, :nonempty_list, [type]}, et, mn),
do: [do_type(type, et, mn), {:..., [], nil}]
defp do_type({:type, _line, :nonempty_list, []}, _et, _mn),
do: [{:term, [], []}, {:..., [], nil}]
defp do_type({:type, _line, :list, []}, _et, _mn), do: [{:any, [], []}]
defp do_type({:type, _line, :map, :any}, _et, _mn), do: {:map, [], []}
defp do_type({:type, _line, :map, pairs}, et, mn) do
elixir_pairs =
Enum.map(pairs, fn
{:type, _, :map_field_assoc, [k, v]} -> {do_type(k, et, mn), do_type(v, et, mn)}
{:type, _, :map_field_exact, [k, v]} -> {do_type(k, et, mn), do_type(v, et, mn)}
end)
{:%{}, [], elixir_pairs}
end
defp do_type({:type, _line, :string, []}, _et, _mn), do: {:charlist, [], []}
defp do_type({:type, _line, :nonempty_string, []}, _et, _mn), do: {:nonempty_charlist, [], []}
# Map explicit Erlang binary types to String.t() if they carry textual context
defp do_type({:remote_type, _line, [{:atom, _, :erlang}, {:atom, _, :binary}, []]}, _et, _mn),
do: {{:., [], [{:__aliases__, [], [:String]}, :t]}, [], []}
defp do_type({:type, _line, :binary, [{:integer, _, _each}, {:integer, _, _unit}]}, _et, _mn) do
{:bitstring, [], []}
end
defp do_type({:type, _line, :fun, []}, _et, _mn), do: {:fun, [], []}
defp do_type({:type, _line, nil, []}, _et, _mn), do: []
defp do_type({:type, _line, :any, []}, _et, _mn), do: {:term, [], []}
defp do_type({:type, _line, type, _}, _, _), do: {type, [], []}
defp do_type({tag, _line, value}, _, _) when tag in [:atom, :integer, :float], do: value
defp do_type(other, _, _), do: throw({:unsupported_erlang_type, other})
defp do_file_path(path),
do: path |> List.to_string() |> Path.expand() |> Path.relative_to_cwd()
@spec exdoc!(Path.t() | nil) :: String.t() | nil
def exdoc!(nil), do: nil
def exdoc!(erlang_docs_path), do: erlang_docs_path |> File.read!() |> translate_doc()
def translate_doc(doc_string) do
doc_string
|> then(fn doc ->
Regex.replace(~r/(?<!`)`([^`]+)`(?!`)/, doc, fn _, snippet -> "`#{ts(snippet)}`" end)
end)
|> then(fn doc ->
Regex.replace(
~r/\(https:\/\/hexdocs\.pm\/temporal_sdk_samples\/([a-z0-9_]+)\.html\)/,
doc,
fn _, segment ->
"(https://hexdocs.pm/temporal_sdk_samples/#{Macro.camelize(segment)}.html)"
end
)
end)
end
def ts(erl_code) do
cond do
String.starts_with?(erl_code, "c:") ->
tx = String.slice(erl_code, 2..-1//1) |> to_elixir(true)
"c:#{tx}"
String.starts_with?(erl_code, "t:") ->
tx = String.slice(erl_code, 2..-1//1) |> to_elixir(true)
"t:#{tx}"
String.starts_with?(erl_code, "e:") ->
[fmod | rest] = String.split(erl_code, "#", parts: 2, trim: true)
mod = String.slice(fmod, 2..-1//1)
tx_mod =
case String.contains?(mod, "/") do
false ->
[fmod1 | rest1] = String.split(mod, ":", parts: 2, trim: true)
case rest1 do
[] -> tx_mod(fmod1)
_ -> mod
end
true ->
to_elixir(mod, false)
end
case rest do
[] -> "e:#{tx_mod}"
_ -> "e:#{tx_mod}##{rest}"
end
String.starts_with?(erl_code, "m:") ->
[mod | rest] = String.split(erl_code, "#", parts: 2, trim: true)
tx_mod = String.slice(mod, 2..-1//1) |> tx_mod()
case rest do
[] -> "m:#{tx_mod}"
_ -> "m:#{tx_mod}##{rest}"
end
true ->
to_elixir(erl_code, false)
end
end
def to_elixir(code, is_erl) do
with {:ok, tokens, _} <- :erl_scan.string(to_charlist(code <> ".")),
{:ok, [ast]} <- :erl_parse.parse_exprs(tokens) do
do_to_elixir(ast, is_erl)
else
_ -> code
end
end
defp do_to_elixir(ast, is_erl) do
case tx(ast, is_erl) do
str when is_binary(str) -> str
other -> Macro.to_string(other)
end
end
# -------------------------------------------------------------------------------------------------
# AST translation logic
defp tx({:atom, _, value}, _), do: value
defp tx({:integer, _, value}, _), do: value
defp tx({:float, _, value}, _), do: value
defp tx({:string, _, chars}, _), do: "\"#{List.to_string(chars)}\""
# alternative:
# defp tx({:string, _, chars}, _), do: List.to_string(chars)
defp tx({:bin, 1, [{:bin_element, 1, {:string, 1, val}, :default, [:utf8]}]}, _),
do: List.to_string(val)
defp tx({:tuple, _, el}, is_erl), do: Enum.map(el, fn e -> tx(e, is_erl) end) |> List.to_tuple()
defp tx({:map, _, fields}, is_erl),
do:
{:%{}, [],
Enum.map(fields, fn {:map_field_assoc, _, k, v} -> {tx(k, is_erl), tx(v, is_erl)} end)}
defp tx({:cons, _, head, tail}, is_erl), do: [tx(head, is_erl) | tx(tail, is_erl)]
defp tx({nil, _}, _), do: []
defp tx(
{:op, _, :/, {:remote, _, {:atom, _, mod}, {:atom, _, fun}}, {:integer, _, arity}},
false
),
do: "#{tx_mod(mod)}.#{fun}/#{arity}"
defp tx(
{:op, _, :/, {:remote, _, {:atom, _, mod}, {:atom, _, fun}}, {:integer, _, arity}},
true
),
do: ":#{mod}.#{fun}/#{arity}"
defp tx({:op, _, :/, {:atom, _, fun}, {:integer, _, arity}}, _), do: "#{fun}/#{arity}"
defp tx({:call, _, {:atom, _, name}, args}, is_erl),
do: {name, [], Enum.map(args, fn a -> tx(a, is_erl) end)}
defp tx({:var, _, name}, _),
do:
name
|> Atom.to_string()
|> Macro.underscore()
|> String.to_atom()
|> then(fn snake_name -> {snake_name, [], nil} end)
defp tx(other, _), do: other
# -------------------------------------------------------------------------------------------------
# SDK modules translation logic
defp tx_mod(mod) when is_atom(mod), do: mod |> Atom.to_string() |> tx_mod()
defp tx_mod("temporal_sdk"), do: "TemporalSdk"
defp tx_mod("temporal_sdk_batch"), do: "TemporalSdk.Batch"
defp tx_mod("temporal_sdk_operator"), do: "TemporalSdk.Operator"
defp tx_mod("temporal_sdk_schedule"), do: "TemporalSdk.Schedule"
defp tx_mod("temporal_sdk_service"), do: "TemporalSdk.Service"
defp tx_mod("temporal_sdk_versioning"), do: "TemporalSdk.Versioning"
defp tx_mod("temporal_sdk_activity"), do: "TemporalSdk.Activity"
defp tx_mod("temporal_sdk_nexus"), do: "TemporalSdk.Nexus"
defp tx_mod("temporal_sdk_workflow"), do: "TemporalSdk.Workflow"
defp tx_mod("temporal_sdk_cluster"), do: "TemporalSdk.Cluster"
defp tx_mod("temporal_sdk_node"), do: "TemporalSdk.Node"
defp tx_mod("temporal_sdk_worker"), do: "TemporalSdk.Worker"
defp tx_mod(mod), do: ":#{mod}"
end