diff --git a/lib/igniter/project/application.ex b/lib/igniter/project/application.ex index 418ec75..61db2ac 100644 --- a/lib/igniter/project/application.ex +++ b/lib/igniter/project/application.ex @@ -143,17 +143,86 @@ defmodule Igniter.Project.Application do ## Options - - `after` - A list of other modules that this supervisor should appear after, + - `:after` - A list of other modules that this supervisor should appear after, or a function that takes a module and returns `true` if this module should be placed after it. - - `opts_updater` - A function that takes the current options (second element of the child tuple), + - `:opts_updater` - A function that takes the current options (second element of the child tuple), and returns a new value. If the existing value of the module is not a tuple, the value passed into your function will be `[]`. Your function *must* return `{:ok, zipper}` or `{:error | :warning, "error_or_warning"}`. + - `:force?` - If `true`, forces adding a new child, even if an existing child uses the + same child module. Defaults to `false`. ## Ordering We will put the new child as the earliest item in the list that we can, skipping any modules in the `after` option. + + ## Examples + + Given an application `start/2` that looks like this: + + def start(_type, _args) do + children = [ + ChildOne, + {ChildTwo, opt: 1} + ] + + Supervisor.start_link(children, strategy: :one_for_one) + end + + Add a new child that isn't currently present: + + Igniter.Project.Application.add_new_child(igniter, NewChild) + # => + children = [ + NewChild, + ChildOne, + {ChildTwo, opt: 1} + ] + + Add a new child after some existing ones: + + Igniter.Project.Application.add_new_child(igniter, NewChild, after: [ChildOne, ChildTwo]) + # => + children = [ + ChildOne, + {ChildTwo, opt: 1}, + NewChild + ] + + If the given child module is already present, `add_new_child/3` is a no-op by default: + + Igniter.Project.Application.add_new_child(igniter, {ChildOne, opt: 1}) + # => + children = [ + ChildOne, + {ChildTwo, opt: 1} + ] + + You can explicitly handle module conflicts by passing an `:opts_updater`: + + Igniter.Project.Application.add_new_child(igniter, {ChildOne, opt: 1}, + opts_updater: fn opts -> + {:ok, Sourceror.Zipper.replace(opts, [opt: 1])} + end + ) + # => + children = [ + {ChildOne, opt: 1}, + {ChildTwo, opt: 1} + ] + + Using `force?: true`, you can force a child to be added, even if the module + conflicts with an existing one: + + Igniter.Project.Application.add_new_child(igniter, {ChildOne, opt: 1}, force?: true) + # => + children = [ + {ChildOne, opt: 1}, + ChildOne, + {ChildTwo, opt: 1} + ] + """ @spec add_new_child( Igniter.t(), @@ -162,6 +231,14 @@ defmodule Igniter.Project.Application do ) :: Igniter.t() def add_new_child(igniter, to_supervise, opts \\ []) do + opts = + opts + |> Keyword.update(:after, fn _ -> false end, fn + fun when is_function(fun, 1) -> fun + list -> fn item -> item in List.wrap(list) end + end) + |> Keyword.put_new(:force?, false) + to_perform = case app_module(igniter) do nil -> {:create_an_app, Igniter.Project.Module.module_name(igniter, "Application")} @@ -169,15 +246,6 @@ defmodule Igniter.Project.Application do mod -> {:modify, mod} end - opts = - Keyword.update(opts, :after, fn _ -> false end, fn list -> - if is_list(list) do - fn item -> item in list end - else - list - end - end) - case to_perform do {:create_an_app, mod} -> igniter @@ -195,7 +263,7 @@ defmodule Igniter.Project.Application do |> create_application_file(application) end - def do_add_child(igniter, application, to_supervise, opts) do + defp do_add_child(igniter, application, to_supervise, opts) do path = Igniter.Project.Module.proper_location(igniter, application, :source_folder) to_supervise = @@ -229,56 +297,34 @@ defmodule Igniter.Project.Application do end ), {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1) do - case Igniter.Code.List.move_to_list_item(zipper, fn item -> - if Igniter.Code.Tuple.tuple?(item) do - with {:ok, item} <- Igniter.Code.Tuple.tuple_elem(item, 0), - item <- Igniter.Code.Common.expand_alias(item) do - Igniter.Code.Common.nodes_equal?(item, to_supervise_module) - else - _ -> false + if opts[:force?] do + insert_child(zipper, to_supervise, opts) + else + case Igniter.Code.List.move_to_list_item(zipper, fn item -> + case extract_child_module(item) do + {:ok, module} -> Igniter.Code.Common.nodes_equal?(module, to_supervise_module) + :error -> false end - else - item - |> Igniter.Code.Common.expand_alias() - |> Igniter.Code.Common.nodes_equal?(to_supervise_module) - end - end) do - {:ok, zipper} -> - if updater = opts[:opts_updater] do - zipper = - if Igniter.Code.Tuple.tuple?(zipper) do - zipper - else - Zipper.replace(zipper, {zipper.node, []}) - end - - {:ok, zipper} = Igniter.Code.Tuple.tuple_elem(zipper, 1) - - updater.(zipper) - else - {:ok, zipper} - end - - :error -> - zipper - |> Zipper.down() - |> then(fn zipper -> - case Zipper.down(zipper) do - nil -> - Zipper.insert_child(zipper, to_supervise) - - zipper -> - zipper - |> skip_after(opts) - |> case do - {:after, zipper} -> - Zipper.insert_right(zipper, to_supervise) - - {:before, zipper} -> - Zipper.insert_left(zipper, to_supervise) + end) do + {:ok, zipper} -> + if updater = opts[:opts_updater] do + zipper = + if Igniter.Code.Tuple.tuple?(zipper) do + zipper + else + Zipper.replace(zipper, {zipper.node, []}) end + + {:ok, zipper} = Igniter.Code.Tuple.tuple_elem(zipper, 1) + + updater.(zipper) + else + {:ok, zipper} end - end) + + :error -> + insert_child(zipper, to_supervise, opts) + end end else _ -> @@ -291,6 +337,38 @@ defmodule Igniter.Project.Application do end) end + defp insert_child(zipper, child, opts) do + zipper + |> Zipper.down() + |> then(fn zipper -> + case Zipper.down(zipper) do + nil -> + Zipper.insert_child(zipper, child) + + zipper -> + zipper + |> skip_after(opts) + |> case do + {:after, zipper} -> + Zipper.insert_right(zipper, child) + + {:before, zipper} -> + Zipper.insert_left(zipper, child) + end + end + end) + end + + defp extract_child_module(zipper) do + if Igniter.Code.Tuple.tuple?(zipper) do + with {:ok, elem} <- Igniter.Code.Tuple.tuple_elem(zipper, 0) do + {:ok, Igniter.Code.Common.expand_alias(elem)} + end + else + {:ok, Igniter.Code.Common.expand_alias(zipper)} + end + end + def skip_after(zipper, opts) do Igniter.Code.List.do_move_to_list_item(zipper, fn item -> with {:is_tuple, true} <- {:is_tuple, Igniter.Code.Tuple.tuple?(item)}, diff --git a/test/igniter/project/application_test.exs b/test/igniter/project/application_test.exs index 8b02750..493ea7e 100644 --- a/test/igniter/project/application_test.exs +++ b/test/igniter/project/application_test.exs @@ -160,16 +160,28 @@ defmodule Igniter.Project.ApplicationTest do """) end - test "supports expressing " do - :erlang.system_flag(:backtrace_depth, 1000) + test "adds a duplicate module with force?: true" do + test_project() + |> Igniter.Project.Application.add_new_child({Foo, name: Foo.One}) + |> apply_igniter!() + |> Igniter.Project.Application.add_new_child({Foo, name: Foo.Two}, force?: true) + |> assert_has_patch("lib/test/application.ex", """ + - | children = [{Foo, [name: Foo.One]}] + + | children = [{Foo, [name: Foo.Two]}, {Foo, [name: Foo.One]}] + """) + end + test "adds a duplicate module after an existing one with :after and force?: true" do test_project() - |> Igniter.Project.Application.add_new_child(Foo) + |> Igniter.Project.Application.add_new_child({Foo, name: Foo.One}) |> apply_igniter!() - |> Igniter.Project.Application.add_new_child(Bar) + |> Igniter.Project.Application.add_new_child({Foo, name: Foo.Two}, + after: Foo, + force?: true + ) |> assert_has_patch("lib/test/application.ex", """ - 8 - | children = [Foo] - 8 + | children = [Bar, Foo] + - | children = [{Foo, [name: Foo.One]}] + + | children = [{Foo, [name: Foo.One]}, {Foo, [name: Foo.Two]}] """) end end