Repository: phoenixframework/phoenix
Pull request #4121 (2020-12-31 06:32:26)

Add flag to skip prompt when generator find an existing context
This adds new `--merge-with-existing-context` and `--no-merge-with-existing-context` options to the `phx.gen.context` family of generators. This allows us to have an integration test that merges`phx.gen.auth` generated code with an existing context (a prerequisite for me to clean up some duplicate fixture generation code). This test also found a formatting error where we were missing a blank newline between the existing context's functions/tests and `phx.gen.auth`'s functions/tests. I've also added unit tests and documentation for these new options. 

I was unable to add unit tests for `--no-merge-with-existing-context` because `System.halt/0` is called in the generator and kills the tests.


Patch:
diff --git a/installer/lib/mix/tasks/local.phx.ex b/installer/lib/mix/tasks/local.phx.ex
index ee9e52deb..63de0fe16 100644
--- a/installer/lib/mix/tasks/local.phx.ex
+++ b/installer/lib/mix/tasks/local.phx.ex
@@ -10,6 +10,8 @@ defmodule Mix.Tasks.Local.Phx do
 
   Accepts the same command line options as `archive.install hex phx_new`.
   """
+
+  @impl true
   def run(args) do
     Mix.Task.run("archive.install", ["hex", "phx_new" | args])
   end
diff --git a/installer/lib/mix/tasks/phx.new.ecto.ex b/installer/lib/mix/tasks/phx.new.ecto.ex
index 86ae6a26e..f6c1b1f42 100644
--- a/installer/lib/mix/tasks/phx.new.ecto.ex
+++ b/installer/lib/mix/tasks/phx.new.ecto.ex
@@ -49,9 +49,11 @@ defmodule Mix.Tasks.Phx.New.Ecto do
   use Mix.Task
   import Phx.New.Generator
 
+  @impl true
   def run([]) do
     Mix.Tasks.Help.run(["phx.new.ecto"])
   end
+
   def run([path | _] = args) do
     unless in_umbrella?(path) do
       Mix.raise "The ecto task can only be run within an umbrella's apps directory"
diff --git a/installer/lib/mix/tasks/phx.new.ex b/installer/lib/mix/tasks/phx.new.ex
index 7df5c58d9..509f18140 100644
--- a/installer/lib/mix/tasks/phx.new.ex
+++ b/installer/lib/mix/tasks/phx.new.ex
@@ -110,6 +110,7 @@ defmodule Mix.Tasks.Phx.New do
              gettext: :boolean, umbrella: :boolean, verbose: :boolean,
              live: :boolean, dashboard: :boolean, install: :boolean]
 
+  @impl true
   def run([version]) when version in ~w(-v --version) do
     Mix.shell().info("Phoenix v#{@version}")
   end
@@ -126,6 +127,7 @@ defmodule Mix.Tasks.Phx.New do
     end
   end
 
+  @doc false
   def run(argv, generator, path) do
     elixir_version_check!()
     case parse_opts(argv) do
@@ -134,7 +136,7 @@ defmodule Mix.Tasks.Phx.New do
     end
   end
 
-  def generate(base_path, generator, path, opts) do
+  defp generate(base_path, generator, path, opts) do
     base_path
     |> Project.new(opts)
     |> generator.prepare_project()
diff --git a/installer/lib/mix/tasks/phx.new.web.ex b/installer/lib/mix/tasks/phx.new.web.ex
index 052c4766d..20d8fc5be 100644
--- a/installer/lib/mix/tasks/phx.new.web.ex
+++ b/installer/lib/mix/tasks/phx.new.web.ex
@@ -29,9 +29,11 @@ defmodule Mix.Tasks.Phx.New.Web do
 
   use Mix.Task
 
+  @impl true
   def run([]) do
     Mix.Tasks.Help.run(["phx.new.web"])
   end
+
   def run([path | _] = args) do
     unless Phx.New.Generator.in_umbrella?(path) do
       Mix.raise "The web task can only be run within an umbrella's apps directory"
diff --git a/lib/mix/tasks/phx.gen.auth.ex b/lib/mix/tasks/phx.gen.auth.ex
index 4b99ba8cd..4ca8e5d23 100644
--- a/lib/mix/tasks/phx.gen.auth.ex
+++ b/lib/mix/tasks/phx.gen.auth.ex
@@ -81,7 +81,8 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
   alias Mix.Tasks.Phx.Gen
   alias Mix.Tasks.Phx.Gen.Auth.{HashingLibrary, Injector, Migration}
 
-  @switches [web: :string, binary_id: :boolean, hashing_lib: :string, table: :string]
+  @switches [web: :string, binary_id: :boolean, hashing_lib: :string,
+             table: :string, merge_with_existing_context: :boolean]
 
   @doc false
   def run(args) do
@@ -254,6 +255,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
 
     paths
     |> Mix.Phoenix.eval_from("priv/templates/phx.gen.auth/context_functions.ex", binding)
+    |> prepend_newline()
     |> inject_before_final_end(file)
   end
 
@@ -264,6 +266,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
 
     paths
     |> Mix.Phoenix.eval_from("priv/templates/phx.gen.auth/test_cases.exs", binding)
+    |> prepend_newline()
     |> inject_before_final_end(test_file)
   end
 
@@ -600,6 +603,8 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
   defp pad(i) when i < 10, do: <<?0, ?0 + i>>
   defp pad(i), do: to_string(i)
 
+  defp prepend_newline(string) when is_binary(string), do: "\n" <> string
+
   defp get_ecto_adapter!(%Schema{repo: repo}) do
     if Code.ensure_loaded?(repo) do
       repo.__adapter__()
diff --git a/lib/mix/tasks/phx.gen.context.ex b/lib/mix/tasks/phx.gen.context.ex
index 6b6bdeb3d..e41bb7590 100644
--- a/lib/mix/tasks/phx.gen.context.ex
+++ b/lib/mix/tasks/phx.gen.context.ex
@@ -63,6 +63,14 @@ defmodule Mix.Tasks.Phx.Gen.Context do
 
   Read the documentation for `phx.gen.schema` for more information on
   attributes.
+
+  ## Skipping prompts
+
+  This generator will prompt you if there is an existing context with the same
+  name, in order to provide more instructions on how to correctly use phoenix contexts.
+  You can skip this prompt and automatically merge the new schema access functions and tests into the
+  existing context using `--merge-with-existing-context`. To prevent changes to
+  the existing context and exit the generator, use `--no-merge-with-existing-context`.
   """
 
   use Mix.Task
@@ -71,7 +79,8 @@ defmodule Mix.Tasks.Phx.Gen.Context do
   alias Mix.Tasks.Phx.Gen
 
   @switches [binary_id: :boolean, table: :string, web: :string,
-             schema: :boolean, context: :boolean, context_app: :string]
+             schema: :boolean, context: :boolean, context_app: :string,
+             merge_with_existing_context: :boolean]
 
   @default_opts [schema: true, context: true]
 
@@ -256,11 +265,17 @@ defmodule Mix.Tasks.Phx.Gen.Context do
 
   def prompt_for_code_injection(%Context{generate?: false}), do: :ok
   def prompt_for_code_injection(%Context{} = context) do
-    if Context.pre_existing?(context) do
+    if Context.pre_existing?(context) && !merge_with_existing_context?(context) do
+      System.halt()
+    end
+  end
+
+  defp merge_with_existing_context?(%Context{} = context) do
+    Keyword.get_lazy(context.opts, :merge_with_existing_context, fn ->
       function_count = Context.function_count(context)
       file_count = Context.file_count(context)
 
-      Mix.shell().info """
+      Mix.shell().info("""
       You are generating into an existing context.
 
       The #{inspect context.module} context currently has #{function_count} functions and \
@@ -276,10 +291,9 @@ defmodule Mix.Tasks.Phx.Gen.Context do
       to the same context.
 
       If you are not sure, prefer creating a new context over adding to the existing one.
-      """
-      unless Mix.shell().yes?("Would you like to proceed?") do
-        System.halt()
-      end
-    end
+      """)
+
+      Mix.shell().yes?("Would you like to proceed?")
+    end)
   end
 end


Test patch:
diff --git a/integration_test/test/code_generation/app_with_mssql_adapter_test.exs b/integration_test/test/code_generation/app_with_mssql_adapter_test.exs
index 7c65cfda2..ab084fe67 100644
--- a/integration_test/test/code_generation/app_with_mssql_adapter_test.exs
+++ b/integration_test/test/code_generation/app_with_mssql_adapter_test.exs
@@ -84,12 +84,25 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithMSSQLAdapterTest do
     end
   end
 
-  describe "phx.gen.auth + pbkdf2" do
+  describe "phx.gen.auth + pbkdf2 + existing context" do
     test "has no compilation or formatter warnings" do
       with_installer_tmp("new with defaults", fn tmp_dir ->
         {app_root_path, _} = generate_phoenix_app(tmp_dir, "phx_blog", ["--database", "mssql", "--live"])
 
-        mix_run!(~w(phx.gen.auth Accounts User users --hashing-lib pbkdf2), app_root_path)
+        mix_run!(~w(phx.gen.html Accounts Group groups name), app_root_path)
+
+        modify_file(Path.join(app_root_path, "lib/phx_blog_web/router.ex"), fn file ->
+          inject_before_final_end(file, """
+
+            scope "/", PhxBlogWeb do
+              pipe_through [:browser]
+
+              resources "/groups", GroupController
+            end
+          """)
+        end)
+
+        mix_run!(~w(phx.gen.auth Accounts User users --hashing-lib pbkdf2 --merge-with-existing-context), app_root_path)
 
         assert_no_compilation_warnings(app_root_path)
         assert_passes_formatter_check(app_root_path)
@@ -99,9 +112,22 @@ defmodule Phoenix.Integration.CodeGeneration.AppWithMSSQLAdapterTest do
     @tag database: :mssql
     test "has a passing test suite" do
       with_installer_tmp("app_with_defaults", fn tmp_dir ->
-        {app_root_path, _} = generate_phoenix_app(tmp_dir, "default_app", ["--database", "mssql", "--live"])
+        {app_root_path, _} = generate_phoenix_app(tmp_dir, "phx_blog", ["--database", "mssql", "--live"])
+
+        mix_run!(~w(phx.gen.html Accounts Group groups name), app_root_path)
+
+        modify_file(Path.join(app_root_path, "lib/phx_blog_web/router.ex"), fn file ->
+          inject_before_final_end(file, """
+
+            scope "/", PhxBlogWeb do
+              pipe_through [:browser]
+
+              resources "/groups", GroupController
+            end
+          """)
+        end)
 
-        mix_run!(~w(phx.gen.auth Accounts User users --hashing-lib pbkdf2), app_root_path)
+        mix_run!(~w(phx.gen.auth Accounts User users --hashing-lib pbkdf2 --merge-with-existing-context), app_root_path)
 
         drop_test_database(app_root_path)
         assert_tests_pass(app_root_path)
diff --git a/test/mix/tasks/phx.gen.context_test.exs b/test/mix/tasks/phx.gen.context_test.exs
index 6452b408c..a8bc0cf29 100644
--- a/test/mix/tasks/phx.gen.context_test.exs
+++ b/test/mix/tasks/phx.gen.context_test.exs
@@ -211,6 +211,34 @@ defmodule Mix.Tasks.Phx.Gen.ContextTest do
     end
   end
 
+  test "generates into existing context without prompt with --merge-with-existing-context", config do
+    in_tmp_project config.test, fn ->
+      Gen.Context.run(~w(Blog Post posts title))
+
+      assert_file "lib/phoenix/blog.ex", fn file ->
+        assert file =~ "def get_post!"
+        assert file =~ "def list_posts"
+        assert file =~ "def create_post"
+        assert file =~ "def update_post"
+        assert file =~ "def delete_post"
+        assert file =~ "def change_post"
+      end
+
+      Gen.Context.run(~w(Blog Comment comments message:string --merge-with-existing-context))
+
+      refute_received {:mix_shell, :info, ["You are generating into an existing context" <> _notice]}
+
+      assert_file "lib/phoenix/blog.ex", fn file ->
+        assert file =~ "def get_comment!"
+        assert file =~ "def list_comments"
+        assert file =~ "def create_comment"
+        assert file =~ "def update_comment"
+        assert file =~ "def delete_comment"
+        assert file =~ "def change_comment"
+      end
+    end
+  end
+
   test "when more than 50 attributes are given", config do
     in_tmp_project config.test, fn ->
       long_attribute_list = 0..55 |> Enum.map(&("attribute#{&1}:string")) |> Enum.join(" ")
diff --git a/test/mix/tasks/phx.gen.html_test.exs b/test/mix/tasks/phx.gen.html_test.exs
index 360fcb963..86a8b734f 100644
--- a/test/mix/tasks/phx.gen.html_test.exs
+++ b/test/mix/tasks/phx.gen.html_test.exs
@@ -199,6 +199,34 @@ defmodule Mix.Tasks.Phx.Gen.HtmlTest do
     end
   end
 
+  test "generates into existing context without prompt with --merge-with-existing-context", config do
+    in_tmp_project config.test, fn ->
+      Gen.Html.run(~w(Blog Post posts title))
+
+      assert_file "lib/phoenix/blog.ex", fn file ->
+        assert file =~ "def get_post!"
+        assert file =~ "def list_posts"
+        assert file =~ "def create_post"
+        assert file =~ "def update_post"
+        assert file =~ "def delete_post"
+        assert file =~ "def change_post"
+      end
+
+      Gen.Html.run(~w(Blog Comment comments message:string --merge-with-existing-context))
+
+      refute_received {:mix_shell, :info, ["You are generating into an existing context" <> _notice]}
+
+      assert_file "lib/phoenix/blog.ex", fn file ->
+        assert file =~ "def get_comment!"
+        assert file =~ "def list_comments"
+        assert file =~ "def create_comment"
+        assert file =~ "def update_comment"
+        assert file =~ "def delete_comment"
+        assert file =~ "def change_comment"
+      end
+    end
+  end
+
   test "with --web namespace generates namedspaced web modules and directories", config do
     in_tmp_project config.test, fn ->
       Gen.Html.run(~w(Blog Post posts title:string --web Blog))
diff --git a/test/mix/tasks/phx.gen.json_test.exs b/test/mix/tasks/phx.gen.json_test.exs
index 5cd660615..73028d4fd 100644
--- a/test/mix/tasks/phx.gen.json_test.exs
+++ b/test/mix/tasks/phx.gen.json_test.exs
@@ -117,6 +117,34 @@ defmodule Mix.Tasks.Phx.Gen.JsonTest do
     end
   end
 
+  test "generates into existing context without prompt with --merge-with-existing-context", config do
+    in_tmp_project config.test, fn ->
+      Gen.Json.run(~w(Blog Post posts title))
+
+      assert_file "lib/phoenix/blog.ex", fn file ->
+        assert file =~ "def get_post!"
+        assert file =~ "def list_posts"
+        assert file =~ "def create_post"
+        assert file =~ "def update_post"
+        assert file =~ "def delete_post"
+        assert file =~ "def change_post"
+      end
+
+      Gen.Json.run(~w(Blog Comment comments message:string --merge-with-existing-context))
+
+      refute_received {:mix_shell, :info, ["You are generating into an existing context" <> _notice]}
+
+      assert_file "lib/phoenix/blog.ex", fn file ->
+        assert file =~ "def get_comment!"
+        assert file =~ "def list_comments"
+        assert file =~ "def create_comment"
+        assert file =~ "def update_comment"
+        assert file =~ "def delete_comment"
+        assert file =~ "def change_comment"
+      end
+    end
+  end
+
   test "when more than 50 arguments are given", config do
     in_tmp_project config.test, fn ->
       long_attribute_list = 0..55 |> Enum.map(&("attribute#{&1}:string")) |> Enum.join(" ")
diff --git a/test/mix/tasks/phx.gen.live_test.exs b/test/mix/tasks/phx.gen.live_test.exs
index 8a4d81e3d..061d2b184 100644
--- a/test/mix/tasks/phx.gen.live_test.exs
+++ b/test/mix/tasks/phx.gen.live_test.exs
@@ -181,6 +181,34 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do
     end
   end
 
+  test "generates into existing context without prompt with --merge-with-existing-context", config do
+    in_tmp_live_project config.test, fn ->
+      Gen.Live.run(~w(Blog Post posts title))
+
+      assert_file "lib/phoenix/blog.ex", fn file ->
+        assert file =~ "def get_post!"
+        assert file =~ "def list_posts"
+        assert file =~ "def create_post"
+        assert file =~ "def update_post"
+        assert file =~ "def delete_post"
+        assert file =~ "def change_post"
+      end
+
+      Gen.Live.run(~w(Blog Comment comments message:string --merge-with-existing-context))
+
+      refute_received {:mix_shell, :info, ["You are generating into an existing context" <> _notice]}
+
+      assert_file "lib/phoenix/blog.ex", fn file ->
+        assert file =~ "def get_comment!"
+        assert file =~ "def list_comments"
+        assert file =~ "def create_comment"
+        assert file =~ "def update_comment"
+        assert file =~ "def delete_comment"
+        assert file =~ "def change_comment"
+      end
+    end
+  end
+
   test "with --web namespace generates namespaced web modules and directories", config do
     in_tmp_live_project config.test, fn ->
       Gen.Live.run(~w(Blog Post posts title:string --web Blog))