Skip to content

Commit

Permalink
Fix: support for multiple functions with same name and different arity
Browse files Browse the repository at this point in the history
Problem: 
- current implementation only keeps the most recent function spec, if we have multiple functions specs with same name, but different arity

Solution:
- change the way we collect callbacks and store multiple function specs, if needed
- adjust implementations
  • Loading branch information
mindreframer committed Feb 1, 2024
1 parent 8e33158 commit 098a599
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 29 deletions.
16 changes: 9 additions & 7 deletions lib/maxo_adapt/impl/compile.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ defmodule MaxoAdapt.Impl.Compile do

def generate(code, callbacks, config) do
%{default: default, error: error, log: log, random: random, validate: validate} = config
simple_callbacks = Enum.map(callbacks, fn {k, %{args: args}} -> {k, Enum.count(args)} end)

simple_callbacks =
Enum.map(callbacks, fn {_, %{args: args, name: name}} -> {name, Enum.count(args)} end)

stubs =
if default do
Expand Down Expand Up @@ -104,27 +106,27 @@ defmodule MaxoAdapt.Impl.Compile do

@spec generate_compiled_delegates(MaxoAdapt.Utility.behaviour(), module) :: term
defp generate_compiled_delegates(callbacks, target) do
Enum.map(callbacks, fn {key, %{spec: spec, doc: doc, args: args}} ->
Enum.map(callbacks, fn {_key, %{spec: spec, doc: doc, args: args, name: name}} ->
vars = Enum.map(args, &Macro.var(&1, nil))

quote do
unquote(doc)
unquote(spec)
def unquote(key)(unquote_splicing(vars))
defdelegate unquote(key)(unquote_splicing(vars)), to: unquote(target)
def unquote(name)(unquote_splicing(vars))
defdelegate unquote(name)(unquote_splicing(vars)), to: unquote(target)
end
end)
end

@spec generate_stubs(MaxoAdapt.Utility.behaviour(), term) :: term
defp generate_stubs(callbacks, result) do
Enum.map(callbacks, fn {key, %{spec: spec, doc: docs, args: args}} ->
Enum.map(callbacks, fn {_key, %{spec: spec, doc: docs, args: args, name: name}} ->
quote do
unquote(docs)
unquote(spec)
def unquote(key)(unquote_splicing(Enum.map(args, &Macro.var(&1, nil))))
def unquote(name)(unquote_splicing(Enum.map(args, &Macro.var(&1, nil))))

def unquote(key)(unquote_splicing(Enum.map(args, &Macro.var(:"_#{&1}", nil)))),
def unquote(name)(unquote_splicing(Enum.map(args, &Macro.var(:"_#{&1}", nil)))),
do: unquote(result)
end
end)
Expand Down
14 changes: 7 additions & 7 deletions lib/maxo_adapt/impl/get_compiled.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,32 +58,32 @@ defmodule MaxoAdapt.Impl.GetCompiled do

@spec generate_errors(MaxoAdapt.Utility.behaviour(), term) :: term
defp generate_errors(callbacks, error) do
Enum.map(callbacks, fn {key, %{spec: spec, doc: doc, args: args}} ->
Enum.map(callbacks, fn {_key, %{spec: spec, doc: doc, args: args, name: name}} ->
vars = Enum.map(args, &Macro.var(&1, nil))
u_vars = Enum.map(args, &Macro.var(:"_#{&1}", nil))

quote do
unquote(doc)
unquote(spec)
def unquote(key)(unquote_splicing(vars))
def unquote(key)(unquote_splicing(u_vars)), do: unquote(error)
def unquote(name)(unquote_splicing(vars))
def unquote(name)(unquote_splicing(u_vars)), do: unquote(error)
end
end)
end

@spec generate_implementation(MaxoAdapt.Utility.behaviour()) :: term
defp generate_implementation(callbacks) do
Enum.map(callbacks, fn {key, %{spec: spec, doc: doc, args: args}} ->
Enum.map(callbacks, fn {_key, %{spec: spec, doc: doc, args: args, name: name}} ->
vars = Enum.map(args, &Macro.var(&1, nil))

quote do
unquote(doc)
unquote(spec)

def unquote(key)(unquote_splicing(vars))
def unquote(name)(unquote_splicing(vars))

def unquote(key)(unquote_splicing(vars)) do
@adapter.unquote(key)(unquote_splicing(vars))
def unquote(name)(unquote_splicing(vars)) do
@adapter.unquote(name)(unquote_splicing(vars))
end
end
end)
Expand Down
10 changes: 5 additions & 5 deletions lib/maxo_adapt/impl/get_dict.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,17 @@ defmodule MaxoAdapt.Impl.GetDict do

@spec generate_implementation(MaxoAdapt.Utility.behaviour(), term) :: term
defp generate_implementation(callbacks, error) do
Enum.map(callbacks, fn {key, %{spec: spec, doc: doc, args: a}} ->
vars = Enum.map(a, &Macro.var(&1, nil))
Enum.map(callbacks, fn {key, %{spec: spec, doc: doc, args: args, name: name}} ->
vars = Enum.map(args, &Macro.var(&1, nil))

quote do
unquote(doc)
unquote(spec)
def unquote(key)(unquote_splicing(vars))
def unquote(name)(unquote_splicing(vars))

def unquote(key)(unquote_splicing(vars)) do
def unquote(name)(unquote_splicing(vars)) do
if adapter = __maxo_adapt__(),
do: adapter.unquote(key)(unquote_splicing(vars)),
do: adapter.unquote(name)(unquote_splicing(vars)),
else: unquote(error)
end
end
Expand Down
10 changes: 5 additions & 5 deletions lib/maxo_adapt/impl/get_env.ex
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,17 @@ defmodule MaxoAdapt.Impl.GetEnv do

@spec generate_implementation(MaxoAdapt.Utility.behaviour(), term) :: term
defp generate_implementation(callbacks, error) do
Enum.map(callbacks, fn {key, %{spec: spec, doc: doc, args: a}} ->
vars = Enum.map(a, &Macro.var(&1, nil))
Enum.map(callbacks, fn {key, %{spec: spec, doc: doc, args: args, name: name}} ->
vars = Enum.map(args, &Macro.var(&1, nil))

quote do
unquote(doc)
unquote(spec)
def unquote(key)(unquote_splicing(vars))
def unquote(name)(unquote_splicing(vars))

def unquote(key)(unquote_splicing(vars)) do
def unquote(name)(unquote_splicing(vars)) do
if adapter = __maxo_adapt__(),
do: adapter.unquote(key)(unquote_splicing(vars)),
do: adapter.unquote(name)(unquote_splicing(vars)),
else: unquote(error)
end
end
Expand Down
25 changes: 22 additions & 3 deletions lib/maxo_adapt/utility.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule MaxoAdapt.Utility do
def generate_validation(false, _callbacks, _var), do: quote(do: :ok)

def generate_validation(true, callbacks, var) do
spec = Enum.map(callbacks, fn {k, %{args: a}} -> {k, Enum.count(a)} end)
spec = Enum.map(callbacks, fn {_k, %{args: args, name: name}} -> {name, Enum.count(args)} end)

quote do
require Logger
Expand Down Expand Up @@ -119,9 +119,19 @@ defmodule MaxoAdapt.Utility do
{acc, doc, _, _}
) do
argv = process_prewalk_args(args)
### useful in local dev
# IO.inspect(key, label: "key")
# IO.inspect(argv, label: "argv")
# IO.inspect(ast, label: "ast")
# IO.inspect(acc, label: "acc")

{ast,
{Map.put(acc, key, %{spec: {:@, a, [{:spec, b, c}]}, doc: doc, args: argv}), nil, key, false}}
{Map.put(acc, to_map_key(key, argv), %{
spec: {:@, a, [{:spec, b, c}]},
doc: doc,
name: key,
args: argv
}), nil, key, false}}
end

# Callback with `when`
Expand All @@ -135,14 +145,23 @@ defmodule MaxoAdapt.Utility do
argv = process_prewalk_args(args)

{ast,
{Map.put(acc, key, %{spec: {:@, a, [{:spec, b, c}]}, doc: doc, args: argv}), nil, key, false}}
{Map.put(acc, to_map_key(key, argv), %{
spec: {:@, a, [{:spec, b, c}]},
doc: doc,
name: key,
args: argv
}), nil, key, false}}
end

defp pre_walk(ast = {:"::", _, [{key, _, _} | _]}, {acc, nil, key, false}),
do: {ast, {acc, nil, key, true}}

defp pre_walk(ast, acc), do: {ast, acc}

defp to_map_key(name, argv) do
{name, argv}
end

defp process_prewalk_args(args) when is_list(args) do
args
|> Enum.with_index()
Expand Down
114 changes: 112 additions & 2 deletions lib/maxo_adapt/utility_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,114 @@ defmodule MaxoAdapt.UtilityTest do
alias MaxoAdapt.Utility

describe "analyze" do
test "callbacks with multiple function heads" do
block =
quote do
@callback get(path :: binary) :: any()
@callback get(path :: binary, query :: map()) :: any()
end

auto_assert(
"""
@callback get(path :: binary) :: any()
@callback get(path :: binary, query :: map()) :: any()\
""" <- Macro.to_string(block)
)

{code, callbacks} = Utility.analyze(block)

auto_assert(
%{
{:get, [:path]} => %{
args: [:path],
doc: nil,
name: :get,
spec:
{:@, [context: MaxoAdapt.UtilityTest, imports: [{1, Kernel}]],
[
{:spec, [context: MaxoAdapt.UtilityTest],
[
{:"::", [],
[
{:get, [],
[
{:"::", [],
[
{:path, [], MaxoAdapt.UtilityTest},
{:binary, [], MaxoAdapt.UtilityTest}
]}
]},
{:any, [], []}
]}
]}
]}
},
{:get, [:path, :query]} => %{
args: [:path, :query],
doc: nil,
name: :get,
spec:
{:@, [context: MaxoAdapt.UtilityTest, imports: [{1, Kernel}]],
[
{:spec, [context: MaxoAdapt.UtilityTest],
[
{:"::", [],
[
{:get, [],
[
{:"::", [],
[
{:path, [], MaxoAdapt.UtilityTest},
{:binary, [], MaxoAdapt.UtilityTest}
]},
{:"::", [], [{:query, [], MaxoAdapt.UtilityTest}, {:map, [], []}]}
]},
{:any, [], []}
]}
]}
]}
}
} <- callbacks
)

auto_assert(
{:__block__, [],
[
{:@, [context: MaxoAdapt.UtilityTest, imports: [{1, Kernel}]],
[
{:callback, [context: MaxoAdapt.UtilityTest],
[
{:"::", [],
[
{:get, [],
[
{:"::", [],
[{:path, [], MaxoAdapt.UtilityTest}, {:binary, [], MaxoAdapt.UtilityTest}]}
]},
{:any, [], []}
]}
]}
]},
{:@, [context: MaxoAdapt.UtilityTest, imports: [{1, Kernel}]],
[
{:callback, [context: MaxoAdapt.UtilityTest],
[
{:"::", [],
[
{:get, [],
[
{:"::", [],
[{:path, [], MaxoAdapt.UtilityTest}, {:binary, [], MaxoAdapt.UtilityTest}]},
{:"::", [], [{:query, [], MaxoAdapt.UtilityTest}, {:map, [], []}]}
]},
{:any, [], []}
]}
]}
]}
]} <- code
)
end

test "callbacks with when condition" do
block =
quote do
Expand Down Expand Up @@ -34,11 +142,12 @@ defmodule MaxoAdapt.UtilityTest do

auto_assert(
%{
insert_all: %{
{:insert_all, [:schema_or_source, :entries_or_query, :opts]} => %{
args: [:schema_or_source, :entries_or_query, :opts],
doc:
{:@, [context: MaxoAdapt.UtilityTest, imports: [{1, Kernel}]],
[{:doc, [context: MaxoAdapt.UtilityTest], [[group: "Schema API"]]}]},
name: :insert_all,
spec:
{:@, [context: MaxoAdapt.UtilityTest, imports: [{1, Kernel}]],
[
Expand Down Expand Up @@ -125,7 +234,7 @@ defmodule MaxoAdapt.UtilityTest do

auto_assert(
%{
rgb: %{
{:rgb, []} => %{
args: [],
doc:
{:@, [context: MaxoAdapt.UtilityTest, imports: [{1, Kernel}]],
Expand All @@ -137,6 +246,7 @@ defmodule MaxoAdapt.UtilityTest do
[{:<<>>, [], ["A color's RGB value."]}, []]}
]}
]},
name: :rgb,
spec:
{:@, [context: MaxoAdapt.UtilityTest, imports: [{1, Kernel}]],
[
Expand Down

0 comments on commit 098a599

Please sign in to comment.