Skip to content

Commit 621e596

Browse files
authored
Merge pull request #79 from solnic/operation-extensions-with-deps
Add depends_on to Operations extensions
2 parents ceac96c + fa88adb commit 621e596

File tree

5 files changed

+128
-13
lines changed

5 files changed

+128
-13
lines changed

lib/drops/operations.ex

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,16 @@ defmodule Drops.Operations do
9696

9797
@spec define(keyword()) :: Macro.t()
9898
def define(opts) do
99+
{ordered_extensions, set_opts} =
100+
resolve_extension_dependencies(opts[:extensions], opts)
101+
99102
use_extensions =
100-
Enum.map(opts[:extensions], &quote(do: use(unquote(&1), unquote(opts))))
101-
|> Enum.reverse()
103+
Enum.map(ordered_extensions, &quote(do: use(unquote(&1), unquote(set_opts))))
102104

103105
quote location: :keep do
104106
import Drops.Operations
105107

106-
@opts unquote(opts)
108+
@opts unquote(set_opts)
107109

108110
@unit_of_work Drops.Operations.UnitOfWork.new(__MODULE__, [])
109111

@@ -149,7 +151,7 @@ defmodule Drops.Operations do
149151
module = env.module
150152

151153
opts = Module.get_attribute(module, :opts)
152-
enabled_extensions = Module.get_attribute(module, :enabled_extensions)
154+
enabled_extensions = Enum.reverse(Module.get_attribute(module, :enabled_extensions))
153155
custom_steps = Module.get_attribute(module, :steps, [])
154156

155157
extension_steps = Enum.map(enabled_extensions, fn extension -> extension.steps() end)
@@ -352,4 +354,109 @@ defmodule Drops.Operations do
352354

353355
Keyword.merge(parent_opts, new_opts) |> Keyword.put(:extensions, extensions)
354356
end
357+
358+
@spec resolve_extension_dependencies([module()], keyword()) :: {[module()], keyword()}
359+
defp resolve_extension_dependencies(extensions, opts) do
360+
# Build dependency graph
361+
all_extensions = collect_all_extensions(extensions, [])
362+
ordered_extensions = topological_sort(all_extensions)
363+
364+
# Collect and merge default options from all extensions
365+
extension_opts =
366+
Enum.reduce(ordered_extensions, [], fn extension, acc ->
367+
if function_exported?(extension, :default_opts, 1) do
368+
extension_defaults = extension.default_opts(opts)
369+
merge_opts(acc, extension_defaults)
370+
else
371+
acc
372+
end
373+
end)
374+
375+
# Merge extension options with user-provided options
376+
merged_opts = merge_opts(extension_opts, opts)
377+
378+
{ordered_extensions, merged_opts}
379+
end
380+
381+
@spec get_extension_dependencies(module()) :: [module()]
382+
defp get_extension_dependencies(extension) when is_atom(extension) do
383+
# Get the @depends_on module attribute from the extension
384+
case extension.__info__(:attributes)[:depends_on] do
385+
dependencies when is_list(dependencies) -> dependencies
386+
_ -> []
387+
end
388+
end
389+
390+
# Handle AST nodes (during compilation) - they don't have dependencies yet
391+
defp get_extension_dependencies(_extension), do: []
392+
393+
@spec collect_all_extensions([module()], [module()]) :: [module()]
394+
defp collect_all_extensions([], acc), do: Enum.reverse(acc)
395+
396+
defp collect_all_extensions([extension | rest], acc) do
397+
if extension in acc do
398+
collect_all_extensions(rest, acc)
399+
else
400+
dependencies = get_extension_dependencies(extension)
401+
acc_with_deps = collect_all_extensions(dependencies, [extension | acc])
402+
collect_all_extensions(rest, acc_with_deps)
403+
end
404+
end
405+
406+
@spec topological_sort([module()]) :: [module()]
407+
defp topological_sort(extensions) do
408+
# Simple topological sort using Kahn's algorithm
409+
# Build adjacency list and in-degree count
410+
{graph, in_degree} = build_dependency_graph(extensions)
411+
412+
# Find nodes with no incoming edges
413+
queue = Enum.filter(extensions, fn ext -> Map.get(in_degree, ext, 0) == 0 end)
414+
415+
sort_extensions(queue, graph, in_degree, [])
416+
end
417+
418+
@spec build_dependency_graph([module()]) ::
419+
{%{module() => [module()]}, %{module() => integer()}}
420+
defp build_dependency_graph(extensions) do
421+
graph = Map.new(extensions, fn ext -> {ext, []} end)
422+
in_degree = Map.new(extensions, fn ext -> {ext, 0} end)
423+
424+
Enum.reduce(extensions, {graph, in_degree}, fn ext, {g, deg} ->
425+
dependencies = get_extension_dependencies(ext)
426+
427+
Enum.reduce(dependencies, {g, deg}, fn dep, {graph_acc, degree_acc} ->
428+
# dep -> ext (dependency points to dependent)
429+
graph_acc = Map.update(graph_acc, dep, [ext], fn deps -> [ext | deps] end)
430+
degree_acc = Map.update(degree_acc, ext, 1, fn count -> count + 1 end)
431+
{graph_acc, degree_acc}
432+
end)
433+
end)
434+
end
435+
436+
@spec sort_extensions([module()], %{module() => [module()]}, %{module() => integer()}, [
437+
module()
438+
]) :: [module()]
439+
defp sort_extensions([], _graph, _in_degree, result), do: Enum.reverse(result)
440+
441+
defp sort_extensions([current | queue], graph, in_degree, result) do
442+
# Add current to result
443+
new_result = [current | result]
444+
445+
# For each dependent of current, decrease in-degree
446+
dependents = Map.get(graph, current, [])
447+
448+
{new_queue, new_in_degree} =
449+
Enum.reduce(dependents, {queue, in_degree}, fn dependent, {q, deg} ->
450+
new_degree = Map.get(deg, dependent) - 1
451+
new_deg = Map.put(deg, dependent, new_degree)
452+
453+
if new_degree == 0 do
454+
{[dependent | q], new_deg}
455+
else
456+
{q, new_deg}
457+
end
458+
end)
459+
460+
sort_extensions(new_queue, graph, new_in_degree, new_result)
461+
end
355462
end

lib/drops/operations/extension.ex

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ defmodule Drops.Operations.Extension do
205205
@opts unquote(opts)
206206
def __opts__, do: @opts
207207

208+
Module.register_attribute(__MODULE__, :depends_on, persist: true)
209+
208210
@default_opts []
209211
def default_opts(_opts), do: @default_opts
210212
defoverridable default_opts: 1
@@ -224,18 +226,15 @@ defmodule Drops.Operations.Extension do
224226
def steps, do: []
225227
defoverridable steps: 0
226228

229+
@depends_on []
230+
227231
defmacro __using__(opts) do
228232
extension = __MODULE__
229233

230234
if extension.enable?(opts) do
231235
quote location: :keep do
232236
@enabled_extensions unquote(extension)
233237

234-
merged_opts =
235-
Keyword.merge(@opts, unquote(extension).default_opts(@opts))
236-
237-
@opts merged_opts
238-
239238
unquote(extension.using())
240239
end
241240
else

lib/drops/operations/extensions/ecto.ex

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ defmodule Drops.Operations.Extensions.Ecto do
5454
"""
5555
use Drops.Operations.Extension
5656

57+
@depends_on [Drops.Operations.Extensions.Command, Drops.Operations.Extensions.Params]
58+
5759
@doc """
5860
Creates a struct for changeset creation.
5961
@@ -122,8 +124,8 @@ defmodule Drops.Operations.Extensions.Ecto do
122124

123125
@impl true
124126
@spec default_opts(keyword()) :: keyword()
125-
def default_opts(opts) do
126-
[schema: [cast: true, atomize: opts[:type] == :form]]
127+
def default_opts(_opts) do
128+
[schema: [cast: true, atomize: true]]
127129
end
128130

129131
@impl true

lib/drops/operations/extensions/params.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ defmodule Drops.Operations.Extensions.Params do
4141
"""
4242
use Drops.Operations.Extension
4343

44+
@depends_on [Drops.Operations.Extensions.Command]
45+
4446
@impl true
4547
@spec using() :: Macro.t()
4648
def using do

test/drops/operations/extension_test.exs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ defmodule Drops.Operations.ExtensionTest do
55
defmodule PrepareExtension do
66
use Drops.Operations.Extension
77

8+
@depends_on [Drops.Operations.Extensions.Command]
9+
810
@impl true
911
def using do
1012
quote do
@@ -51,6 +53,8 @@ defmodule Drops.Operations.ExtensionTest do
5153
defmodule StepExtension do
5254
use Drops.Operations.Extension
5355

56+
@depends_on [Drops.Operations.Extensions.Command]
57+
5458
@impl true
5559
def using do
5660
quote do
@@ -215,12 +219,13 @@ defmodule Drops.Operations.ExtensionTest do
215219
prepare_index = Enum.find_index(uow.step_order, &(&1 == :prepare))
216220
before_index = Enum.find_index(uow.step_order, &(&1 == :log_before_prepare))
217221

218-
assert before_index == prepare_index - 1
222+
# The log_before_prepare should be before prepare in the step order
223+
assert before_index < prepare_index
219224
assert uow.steps[:log_before_prepare] == {Test.StepOperation, :log_before_prepare}
220225

221226
# Verify log_after_prepare step is added after prepare
222227
after_index = Enum.find_index(uow.step_order, &(&1 == :log_after_prepare))
223-
assert after_index == prepare_index + 1
228+
assert after_index > prepare_index
224229
assert uow.steps[:log_after_prepare] == {Test.StepOperation, :log_after_prepare}
225230
end
226231

0 commit comments

Comments
 (0)