diff --git a/.gitignore b/.gitignore index 023d19a..a52ec8a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ erl_crash.dump *.ez *.swp *.swo +rename-*.tar diff --git a/config/config.exs b/config/config.exs index d2d855e..68457de 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1 +1,3 @@ -use Mix.Config +import Config + +import_config("#{Mix.env()}.exs") diff --git a/config/dev.exs b/config/dev.exs new file mode 100644 index 0000000..e69de29 diff --git a/config/prod.exs b/config/prod.exs new file mode 100644 index 0000000..e69de29 diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 0000000..57aaa2b --- /dev/null +++ b/config/test.exs @@ -0,0 +1,3 @@ +import Config + +config :rename, Rename.Repo, foo: :bar diff --git a/lib/mix/tasks/rename.ex b/lib/mix/tasks/rename.ex index 8315524..d24cd7b 100644 --- a/lib/mix/tasks/rename.ex +++ b/lib/mix/tasks/rename.ex @@ -2,42 +2,46 @@ defmodule Mix.Tasks.Rename do use Mix.Task def run(args \\ []) - def run([old_name, new_name, old_otp, new_otp | extra_options]) do + + def run([old_name, new_name, old_otp, new_otp | options]) do + {options, _argv, _errors} = + options + |> OptionParser.parse( + strict: [ + ignore_directories: :keep, + ignore_files: :keep, + starting_directory: :string, + include_extensions: :keep, + include_files: :keep + ] + ) + + options = + options + |> Enum.group_by(fn {k, _v} -> k end, fn {_k, v} -> v end) + |> Map.to_list() + |> maybe_put_starting_directory(Keyword.get(options, :starting_directory)) + Rename.run( - {old_name, new_name}, - {old_otp, new_otp}, - run_options(extra_options) + {old_name, new_name}, + {old_otp, new_otp}, + options ) end + def run(_bad_args) do - IO.puts """ + IO.puts(""" Did not provide required app and otp names Call should look like: mix rename OldName NewName old_name NewName - """ + """) + {:error, :bad_params} end - def run_options(extra_options, options \\ []) - def run_options([], options) do - ignore_files = options[:ignore_files] || [] - options - |> Enum.reject(fn {key, _val} -> key == :ignore_files end) - |> Enum.concat([ignore_files: ignore_files]) - end - def run_options([key, val | rest], options) do - rest - |> run_options( - options - |> Enum.concat([{parsed_key(key), val}]) - ) - end + defp maybe_put_starting_directory(options, nil), do: options - def parsed_key(key) do - key - |> String.slice(2..-1) - |> String.replace("-", "_") - |> String.to_atom + defp maybe_put_starting_directory(options, starting_directory) do + Keyword.put(options, :starting_directory, starting_directory) end - end diff --git a/lib/rename.ex b/lib/rename.ex index 9d5d2e9..448d8ad 100644 --- a/lib/rename.ex +++ b/lib/rename.ex @@ -1,5 +1,4 @@ defmodule Rename do - @moduledoc """ The single module that does all the renaming Talk about pressure @@ -9,91 +8,148 @@ defmodule Rename do So it's fine If you're mad about it, submit a PR. """ + @default_extensions ~w( + .ex + .exs + .eex + .md + ) - @default_extensions [ - "ex", - "exs", - "eex", - "md", - ] - - @default_ignore_directories [ - "_build", - "deps", - "assets", - ] + @default_ignore_directories ~w( + .elixir_ls + _build + deps + assets + ) @default_starting_directory "." @default_ignore_files [] + @default_include_files ~w( + mix.exs + ) + @doc """ The public function you use to rename your app. Call looks like: run({"OldName", "NewName"}, {"old_otp", "new_otp"}, options) """ - def run(names, opts, options \\ []) - def run(names = {_old_name, _new_name}, otps = {_old_otp, _new_otp}, options) do + def run(names, otps, options \\ []) + + def run({_old_name, _new_name} = names, {_old_otp, _new_otp} = otps, options) do + options = + options + |> Enum.reduce(defaults(), fn + {key, val}, acc when is_list(val) -> + Keyword.put(acc, key, merge_with_default({key, val}, acc)) + + {k, v}, acc -> + Keyword.put(acc, k, v) + end) + names |> rename_in_directory( otps, - options[:starting_directory] || @default_starting_directory, + options[:starting_directory], options ) end def run(_names, _otp, _options), do: {:error, "bad params"} + defp defaults do + [ + ignore_directories: @default_ignore_directories, + ignore_files: @default_ignore_files, + include_extensions: @default_extensions, + starting_directory: @default_starting_directory, + include_files: @default_include_files + ] + end + defp rename_in_directory(names = {old_name, new_name}, otps = {old_otp, new_otp}, cwd, options) do cwd - |> File.ls! - |> Enum.each(fn path -> - file_or_dir = cwd <> "/" <> path + |> File.ls!() + |> Enum.reject(&ignored_directory?(&1, options)) + |> Enum.each(fn file_or_dir -> + path = Path.join([cwd, file_or_dir]) + cond do - is_valid_directory?(file_or_dir, options) -> - rename_in_directory(names, otps, file_or_dir, options) + File.dir?(path) -> + rename_in_directory(names, otps, path, options) true - is_valid_file?(file_or_dir, options) -> - file_or_dir - |> File.read + + is_valid_file?(path, options) -> + path + |> File.read() |> case do - {:ok, file} -> - updated_file = file - |> String.replace(old_name, new_name) - |> String.replace(old_otp, new_otp) - File.write(file_or_dir, updated_file) + {:ok, file} -> + updated_file = + file + |> String.replace(old_name, new_name) + |> String.replace(old_otp, new_otp) + |> String.replace(dasherised(old_otp), dasherised(new_otp)) + + File.write(path, updated_file) true + _ -> false end - true -> + + true -> false end |> case do - true -> - file_or_dir - |> File.rename(String.replace(file_or_dir, old_otp, new_otp)) - _ -> :nothing + true -> + rename_file(path, old_otp, new_otp) + + _ -> + :ok end end) end - defp is_valid_directory?(dir, options) do - File.dir?(dir) and - dir in (options[:ignore_directories] || @default_ignore_directories) == false + defp rename_file(path, old_otp, new_otp) do + File.rename(path, String.replace(path, old_otp, new_otp)) + end + + defp ignored_directory?(dir, options) do + File.dir?(dir) and dir in options[:ignore_directories] end defp is_valid_file?(file, options) do - File.exists?(file) and - file in (options[:ignore_files] || @default_ignore_files) == false and - has_valid_extension?(file, options) + File.exists?(file) && + has_valid_extension?(file, options) && + (include?(file, options) || !ignore?(file, options)) + end + + def include?(file, options) do + Path.basename(file) in options[:include_files] + end + + def ignore?(file, options) do + Path.basename(file) in options[:ignore_files] end defp has_valid_extension?(file, options) do - extension = file - |> String.split(".") - |> List.last - extension in (options[:valid_extensions] || @default_extensions) + file + |> Path.extname() + |> case do + "" -> + true + + ext -> + ext in options[:include_extensions] + end end + defp dasherised(name), do: String.replace(name, "_", "-") + + defp merge_with_default({key, val}, acc) do + acc + |> Keyword.get(key) + |> then(&(&1 ++ val)) + end end diff --git a/lib/rename/.keep b/lib/rename/.keep new file mode 100644 index 0000000..e69de29 diff --git a/mix.exs b/mix.exs index 1d49e1c..1bbe038 100644 --- a/mix.exs +++ b/mix.exs @@ -6,17 +6,18 @@ defmodule Rename.Mixfile do app: :rename, version: "0.1.0", elixir: "~> 1.4", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, description: description(), + elixirc_paths: elixirc_paths(Mix.env()), package: package(), deps: deps(), test_coverage: [tool: ExCoveralls], preferred_cli_env: [ - "coveralls": :test, + coveralls: :test, "coveralls.detail": :test, "coveralls.post": :test, - "coveralls.html": :test, + "coveralls.html": :test ] ] end @@ -38,8 +39,10 @@ defmodule Rename.Mixfile do defp deps do [ {:excoveralls, "~> 0.6", only: :test}, - {:ex_doc, ">= 0.0.0", only: :dev}, + {:ex_doc, ">= 0.0.0", only: :dev} ] end + defp elixirc_paths(:test), do: ["lib", "test/support"] + defp elixirc_paths(_), do: ["lib"] end diff --git a/mix.lock b/mix.lock index 4e8d949..bb5b5ff 100644 --- a/mix.lock +++ b/mix.lock @@ -1,11 +1,13 @@ -%{"certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [:rebar3], []}, - "earmark": {:hex, :earmark, "1.2.0", "bf1ce17aea43ab62f6943b97bd6e3dc032ce45d4f787504e3adf738e54b42f3a", [:mix], []}, - "ex_doc": {:hex, :ex_doc, "0.15.0", "e73333785eef3488cf9144a6e847d3d647e67d02bd6fdac500687854dd5c599f", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]}, - "excoveralls": {:hex, :excoveralls, "0.6.3", "894bf9254890a4aac1d1165da08145a72700ff42d8cb6ce8195a584cb2a4b374", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]}, - "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]}, - "hackney": {:hex, :hackney, "1.7.1", "e238c52c5df3c3b16ce613d3a51c7220a784d734879b1e231c9babd433ac1cb4", [:rebar3], [{:certifi, "1.0.0", [hex: :certifi, optional: false]}, {:idna, "4.0.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, - "idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], []}, - "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], []}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, - "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}} +%{ + "certifi": {:hex, :certifi, "1.0.0", "1c787a85b1855ba354f0b8920392c19aa1d06b0ee1362f9141279620a5be2039", [:rebar3], [], "hexpm", "44a5aa4261490a7d7fa6909ab4bcf14bff928a4fef49e80fc1e7a8fdb7b45f79"}, + "earmark": {:hex, :earmark, "1.2.0", "bf1ce17aea43ab62f6943b97bd6e3dc032ce45d4f787504e3adf738e54b42f3a", [:mix], [], "hexpm", "6709251dd10e70cca0d50be8a25adc38c701b39eac2da3b1c166e3e7e4d358ed"}, + "ex_doc": {:hex, :ex_doc, "0.15.0", "e73333785eef3488cf9144a6e847d3d647e67d02bd6fdac500687854dd5c599f", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm", "e5ea59f50ecdfe4cc755808450dafe35221d5a0f4a31c42e80a7188eca570e4c"}, + "excoveralls": {:hex, :excoveralls, "0.6.3", "894bf9254890a4aac1d1165da08145a72700ff42d8cb6ce8195a584cb2a4b374", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "33b8e869a0afe422e2a05794ac67bed6570498173a02a80f0c88bc0fe3580287"}, + "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "b55727b206dab96feb025267e5c122ddb448f55b6648f9156b8d481215d80290"}, + "hackney": {:hex, :hackney, "1.7.1", "e238c52c5df3c3b16ce613d3a51c7220a784d734879b1e231c9babd433ac1cb4", [:rebar3], [{:certifi, "1.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "4.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "ec8309cb6d42251513492ef683d212c614d78b20594e5f4d89a05d8411dd0dea"}, + "idna": {:hex, :idna, "4.0.0", "10aaa9f79d0b12cf0def53038547855b91144f1bfcc0ec73494f38bb7b9c4961", [:rebar3], [], "hexpm", "f1b699f7275728538da7b5e35679f9e0f41ad8e0a49896e6a27b61867ed344eb"}, + "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], [], "hexpm", "b4c5d3230b397c8d95579e4a3d72826bb6463160130ccf4182f5be8579b5f44c"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm", "7a4c8e1115a2732a67d7624e28cf6c9f30c66711a9e92928e745c255887ba465"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm", "4f8805eb5c8a939cf2359367cb651a3180b27dfb48444846be2613d79355d65e"}, +} diff --git a/test/mix/tasks/rename_test.exs b/test/mix/tasks/rename_test.exs new file mode 100644 index 0000000..3d6da38 --- /dev/null +++ b/test/mix/tasks/rename_test.exs @@ -0,0 +1,50 @@ +defmodule Mix.Tasks.RenameTest do + use Rename.UnitCase + + @test_copy_dir "test_copy" + @old_app_name "Rename" + @old_app_otp "rename" + @new_app_name "ToDoTwitterClone" + @new_app_otp "to_do_twitter_clone" + + describe "run/1" do + test "rename mix task works" do + create_copy_of_app(@test_copy_dir) + + Mix.Tasks.Rename.run([ + @old_app_name, + @new_app_name, + @old_app_otp, + @new_app_otp, + "--starting-directory", + @test_copy_dir, + "--ignore-directories", + "foo" + ]) + + mix_file = File.read!(@test_copy_dir <> "/mix.exs") + assert mix_file |> String.contains?(@new_app_name) + assert mix_file |> String.contains?(@new_app_otp) + assert mix_file |> String.contains?(@old_app_name) == false + main_module = File.read!(@test_copy_dir <> "/lib/" <> @new_app_otp <> ".ex") + assert main_module |> String.contains?(@new_app_name) + assert main_module |> String.contains?(@old_app_name) == false + readme = File.read!(@test_copy_dir <> "/README.md") + assert readme |> String.contains?(@new_app_name) + assert readme |> String.contains?(@new_app_otp) + assert readme |> String.contains?(@old_app_name) == false + + delete_copy_of_app(@test_copy_dir) + end + end + + test "rename mix task should give proper error for bad params" do + {stdout, 0} = System.cmd("mix", ["rename", "not", "enought", "params"]) + + assert stdout =~ """ + Did not provide required app and otp names + Call should look like: + mix rename OldName NewName old_name NewName + """ + end +end diff --git a/test/rename_test.exs b/test/rename_test.exs index fa461ec..84766ba 100644 --- a/test/rename_test.exs +++ b/test/rename_test.exs @@ -1,94 +1,81 @@ defmodule RenameTest do - use ExUnit.Case + use Rename.UnitCase @test_copy_dir "test_copy" @old_app_name "Rename" @old_app_otp "rename" @new_app_name "ToDoTwitterClone" @new_app_otp "to_do_twitter_clone" - - test "should properly rename app with default options" do - create_copy_of_app() + + setup_all do + create_copy_of_app(@test_copy_dir) + Rename.run( - {@old_app_name, @new_app_name}, - {@old_app_otp, @new_app_otp}, + {@old_app_name, @new_app_name}, + {@old_app_otp, @new_app_otp}, starting_directory: @test_copy_dir ) - mix_file = File.read!(@test_copy_dir <> "/mix.exs") - assert mix_file |> String.contains?(@new_app_name) - assert mix_file |> String.contains?(@new_app_otp) - assert mix_file |> String.contains?(@old_app_name) == false - main_module = File.read!(@test_copy_dir <> "/lib/" <> @new_app_otp <> ".ex") - assert main_module |> String.contains?(@new_app_name) - assert main_module |> String.contains?(@old_app_name) == false - readme = File.read!(@test_copy_dir <> "/README.md") - assert readme |> String.contains?(@new_app_name) - assert readme |> String.contains?(@new_app_otp) - assert readme |> String.contains?(@old_app_name) == false - delete_copy_of_app() - end - test "should give proper error for invalid params" do - assert Rename.run( - {@old_app_name, @new_app_name}, - starting_directory: @test_copy_dir - ) == {:error, "bad params"} + on_exit(fn -> delete_copy_of_app(@test_copy_dir) end) end - test "rename mix task works" do - create_copy_of_app() - Mix.Tasks.Rename.run([ - @old_app_name, - @new_app_name, - @old_app_otp, - @new_app_otp, - "--starting-directory", - @test_copy_dir, - ]) - mix_file = File.read!(@test_copy_dir <> "/mix.exs") - assert mix_file |> String.contains?(@new_app_name) - assert mix_file |> String.contains?(@new_app_otp) - assert mix_file |> String.contains?(@old_app_name) == false - main_module = File.read!(@test_copy_dir <> "/lib/" <> @new_app_otp <> ".ex") - assert main_module |> String.contains?(@new_app_name) - assert main_module |> String.contains?(@old_app_name) == false - readme = File.read!(@test_copy_dir <> "/README.md") - assert readme |> String.contains?(@new_app_name) - assert readme |> String.contains?(@new_app_otp) - assert readme |> String.contains?(@old_app_name) == false - delete_copy_of_app() - end + describe "run/3" do + test "should rename mix file" do + file = File.read!(@test_copy_dir <> "/mix.exs") - test "rename mix task should give proper error for bad params" do - {stdout, 0} = System.cmd("mix", ["rename", "not", "enought", "params"]) - assert stdout =~ """ - Did not provide required app and otp names - Call should look like: - mix rename OldName NewName old_name NewName - """ - end + assert String.contains?(file, @new_app_name) + assert String.contains?(file, @old_app_name) == false + assert String.contains?(file, @new_app_otp) + assert String.contains?(file, @old_app_otp) == false + end - defp create_copy_of_app do - File.mkdir(@test_copy_dir) - File.ls! - |> Enum.each(fn path -> - if not_ignored_path(path) do - System.cmd("cp", ["-r", path, @test_copy_dir]) - end - end) - end + test "should rename main module" do + file = File.read!(@test_copy_dir <> "/lib/" <> @new_app_otp <> ".ex") - defp delete_copy_of_app() do - System.cmd("rm", ["-rf", @test_copy_dir]) - end + assert String.contains?(file, @new_app_name) + assert String.contains?(file, @old_app_name) == false + assert String.contains?(file, @new_app_otp) + assert String.contains?(file, @old_app_otp) == false + end + + test "should rename README" do + file = File.read!(@test_copy_dir <> "/README.md") + + assert String.contains?(file, @new_app_name) + assert String.contains?(file, @old_app_name) == false + assert String.contains?(file, @new_app_otp) + assert String.contains?(file, @old_app_otp) == false + end + + test "should rename gitignore" do + file = File.read!(@test_copy_dir <> "/.gitignore") + + assert String.contains?(file, @new_app_otp) + assert String.contains?(file, @old_app_otp) == false + end + + test "should rename config" do + file = File.read!(@test_copy_dir <> "/config/test.exs") + + assert String.contains?(file, @new_app_name) + assert String.contains?(file, @old_app_name) == false + assert String.contains?(file, @new_app_otp) + assert String.contains?(file, @old_app_otp) == false + end + + test "should rename dir" do + assert File.dir?("#{@test_copy_dir}/lib/#{@new_app_otp}") + end + + test "should rename tests" do + assert File.exists?("#{@test_copy_dir}/test/#{@new_app_otp}_test.exs") + end - defp not_ignored_path(path) do - [ - "_build", - "deps", - @test_copy_dir, - ".git", - ] - |> Enum.find(&(&1 == path)) == nil + test "should give proper error for invalid params" do + assert Rename.run( + {@old_app_name, @new_app_name}, + starting_directory: @test_copy_dir + ) == {:error, "bad params"} + end end end diff --git a/test/support/unit_case.ex b/test/support/unit_case.ex new file mode 100644 index 0000000..979a86c --- /dev/null +++ b/test/support/unit_case.ex @@ -0,0 +1,39 @@ +defmodule Rename.UnitCase do + @moduledoc false + use ExUnit.CaseTemplate + + using do + quote do + import Rename.UnitCase + end + end + + def create_copy_of_app(test_copy_dir) do + File.mkdir(test_copy_dir) + + File.ls!() + |> Enum.filter(fn path -> + File.dir?(path) || + Path.extname(path) in ~w(.ex .exs .md) || + Path.basename(path) == ".gitignore" + end) + |> Enum.reject(&(&1 in ignored_paths(test_copy_dir))) + |> Enum.each(fn path -> + System.cmd("cp", ["-r", path, test_copy_dir]) + end) + end + + def delete_copy_of_app(test_copy_dir) do + System.cmd("rm", ["-rf", test_copy_dir]) + end + + defp ignored_paths(test_copy_dir) do + [ + "_build", + "deps", + test_copy_dir, + ".git", + ".elixir_ls" + ] + end +end