diff --git a/lib/drops/operations.ex b/lib/drops/operations.ex index d9ddb67..b0fa1e3 100644 --- a/lib/drops/operations.ex +++ b/lib/drops/operations.ex @@ -96,14 +96,16 @@ defmodule Drops.Operations do @spec define(keyword()) :: Macro.t() def define(opts) do + {ordered_extensions, set_opts} = + resolve_extension_dependencies(opts[:extensions], opts) + use_extensions = - Enum.map(opts[:extensions], "e(do: use(unquote(&1), unquote(opts)))) - |> Enum.reverse() + Enum.map(ordered_extensions, "e(do: use(unquote(&1), unquote(set_opts)))) quote location: :keep do import Drops.Operations - @opts unquote(opts) + @opts unquote(set_opts) @unit_of_work Drops.Operations.UnitOfWork.new(__MODULE__, []) @@ -149,7 +151,7 @@ defmodule Drops.Operations do module = env.module opts = Module.get_attribute(module, :opts) - enabled_extensions = Module.get_attribute(module, :enabled_extensions) + enabled_extensions = Enum.reverse(Module.get_attribute(module, :enabled_extensions)) custom_steps = Module.get_attribute(module, :steps, []) extension_steps = Enum.map(enabled_extensions, fn extension -> extension.steps() end) @@ -352,4 +354,109 @@ defmodule Drops.Operations do Keyword.merge(parent_opts, new_opts) |> Keyword.put(:extensions, extensions) end + + @spec resolve_extension_dependencies([module()], keyword()) :: {[module()], keyword()} + defp resolve_extension_dependencies(extensions, opts) do + # Build dependency graph + all_extensions = collect_all_extensions(extensions, []) + ordered_extensions = topological_sort(all_extensions) + + # Collect and merge default options from all extensions + extension_opts = + Enum.reduce(ordered_extensions, [], fn extension, acc -> + if function_exported?(extension, :default_opts, 1) do + extension_defaults = extension.default_opts(opts) + merge_opts(acc, extension_defaults) + else + acc + end + end) + + # Merge extension options with user-provided options + merged_opts = merge_opts(extension_opts, opts) + + {ordered_extensions, merged_opts} + end + + @spec get_extension_dependencies(module()) :: [module()] + defp get_extension_dependencies(extension) when is_atom(extension) do + # Get the @depends_on module attribute from the extension + case extension.__info__(:attributes)[:depends_on] do + dependencies when is_list(dependencies) -> dependencies + _ -> [] + end + end + + # Handle AST nodes (during compilation) - they don't have dependencies yet + defp get_extension_dependencies(_extension), do: [] + + @spec collect_all_extensions([module()], [module()]) :: [module()] + defp collect_all_extensions([], acc), do: Enum.reverse(acc) + + defp collect_all_extensions([extension | rest], acc) do + if extension in acc do + collect_all_extensions(rest, acc) + else + dependencies = get_extension_dependencies(extension) + acc_with_deps = collect_all_extensions(dependencies, [extension | acc]) + collect_all_extensions(rest, acc_with_deps) + end + end + + @spec topological_sort([module()]) :: [module()] + defp topological_sort(extensions) do + # Simple topological sort using Kahn's algorithm + # Build adjacency list and in-degree count + {graph, in_degree} = build_dependency_graph(extensions) + + # Find nodes with no incoming edges + queue = Enum.filter(extensions, fn ext -> Map.get(in_degree, ext, 0) == 0 end) + + sort_extensions(queue, graph, in_degree, []) + end + + @spec build_dependency_graph([module()]) :: + {%{module() => [module()]}, %{module() => integer()}} + defp build_dependency_graph(extensions) do + graph = Map.new(extensions, fn ext -> {ext, []} end) + in_degree = Map.new(extensions, fn ext -> {ext, 0} end) + + Enum.reduce(extensions, {graph, in_degree}, fn ext, {g, deg} -> + dependencies = get_extension_dependencies(ext) + + Enum.reduce(dependencies, {g, deg}, fn dep, {graph_acc, degree_acc} -> + # dep -> ext (dependency points to dependent) + graph_acc = Map.update(graph_acc, dep, [ext], fn deps -> [ext | deps] end) + degree_acc = Map.update(degree_acc, ext, 1, fn count -> count + 1 end) + {graph_acc, degree_acc} + end) + end) + end + + @spec sort_extensions([module()], %{module() => [module()]}, %{module() => integer()}, [ + module() + ]) :: [module()] + defp sort_extensions([], _graph, _in_degree, result), do: Enum.reverse(result) + + defp sort_extensions([current | queue], graph, in_degree, result) do + # Add current to result + new_result = [current | result] + + # For each dependent of current, decrease in-degree + dependents = Map.get(graph, current, []) + + {new_queue, new_in_degree} = + Enum.reduce(dependents, {queue, in_degree}, fn dependent, {q, deg} -> + new_degree = Map.get(deg, dependent) - 1 + new_deg = Map.put(deg, dependent, new_degree) + + if new_degree == 0 do + {[dependent | q], new_deg} + else + {q, new_deg} + end + end) + + sort_extensions(new_queue, graph, new_in_degree, new_result) + end end diff --git a/lib/drops/operations/extension.ex b/lib/drops/operations/extension.ex index 27a08d4..5179112 100644 --- a/lib/drops/operations/extension.ex +++ b/lib/drops/operations/extension.ex @@ -205,6 +205,8 @@ defmodule Drops.Operations.Extension do @opts unquote(opts) def __opts__, do: @opts + Module.register_attribute(__MODULE__, :depends_on, persist: true) + @default_opts [] def default_opts(_opts), do: @default_opts defoverridable default_opts: 1 @@ -224,6 +226,8 @@ defmodule Drops.Operations.Extension do def steps, do: [] defoverridable steps: 0 + @depends_on [] + defmacro __using__(opts) do extension = __MODULE__ @@ -231,11 +235,6 @@ defmodule Drops.Operations.Extension do quote location: :keep do @enabled_extensions unquote(extension) - merged_opts = - Keyword.merge(@opts, unquote(extension).default_opts(@opts)) - - @opts merged_opts - unquote(extension.using()) end else diff --git a/lib/drops/operations/extensions/ecto.ex b/lib/drops/operations/extensions/ecto.ex index fd511fd..603608a 100644 --- a/lib/drops/operations/extensions/ecto.ex +++ b/lib/drops/operations/extensions/ecto.ex @@ -54,6 +54,8 @@ defmodule Drops.Operations.Extensions.Ecto do """ use Drops.Operations.Extension + @depends_on [Drops.Operations.Extensions.Command, Drops.Operations.Extensions.Params] + @doc """ Creates a struct for changeset creation. @@ -122,8 +124,8 @@ defmodule Drops.Operations.Extensions.Ecto do @impl true @spec default_opts(keyword()) :: keyword() - def default_opts(opts) do - [schema: [cast: true, atomize: opts[:type] == :form]] + def default_opts(_opts) do + [schema: [cast: true, atomize: true]] end @impl true diff --git a/lib/drops/operations/extensions/params.ex b/lib/drops/operations/extensions/params.ex index 5ae1307..a300a1d 100644 --- a/lib/drops/operations/extensions/params.ex +++ b/lib/drops/operations/extensions/params.ex @@ -41,6 +41,8 @@ defmodule Drops.Operations.Extensions.Params do """ use Drops.Operations.Extension + @depends_on [Drops.Operations.Extensions.Command] + @impl true @spec using() :: Macro.t() def using do diff --git a/test/drops/operations/extension_test.exs b/test/drops/operations/extension_test.exs index c3f7d0f..dd3b487 100644 --- a/test/drops/operations/extension_test.exs +++ b/test/drops/operations/extension_test.exs @@ -5,6 +5,8 @@ defmodule Drops.Operations.ExtensionTest do defmodule PrepareExtension do use Drops.Operations.Extension + @depends_on [Drops.Operations.Extensions.Command] + @impl true def using do quote do @@ -51,6 +53,8 @@ defmodule Drops.Operations.ExtensionTest do defmodule StepExtension do use Drops.Operations.Extension + @depends_on [Drops.Operations.Extensions.Command] + @impl true def using do quote do @@ -215,12 +219,13 @@ defmodule Drops.Operations.ExtensionTest do prepare_index = Enum.find_index(uow.step_order, &(&1 == :prepare)) before_index = Enum.find_index(uow.step_order, &(&1 == :log_before_prepare)) - assert before_index == prepare_index - 1 + # The log_before_prepare should be before prepare in the step order + assert before_index < prepare_index assert uow.steps[:log_before_prepare] == {Test.StepOperation, :log_before_prepare} # Verify log_after_prepare step is added after prepare after_index = Enum.find_index(uow.step_order, &(&1 == :log_after_prepare)) - assert after_index == prepare_index + 1 + assert after_index > prepare_index assert uow.steps[:log_after_prepare] == {Test.StepOperation, :log_after_prepare} end