Current section

Files

Jump to
igniter lib igniter libs phoenix.ex
Raw

lib/igniter/libs/phoenix.ex

defmodule Igniter.Libs.Phoenix do
@moduledoc "Codemods & utilities for working with Phoenix"
@doc """
Returns the web module name for the current app
"""
@spec web_module_name() :: module()
@deprecated "Use `web_module/0` instead."
def web_module_name do
web_module(Igniter.new())
end
@doc """
Returns the web module name for the current app
"""
@spec web_module(Igniter.t()) :: module()
def web_module(igniter) do
prefix =
igniter
|> Igniter.Project.Module.module_name_prefix()
|> inspect()
if String.ends_with?(prefix, "Web") do
Module.concat([prefix])
else
Module.concat([prefix <> "Web"])
end
end
@doc "Returns `true` if the module is a Phoenix HTML module"
@spec html?(Igniter.t(), module()) :: boolean()
def html?(igniter, module) do
zipper = elem(Igniter.Project.Module.find_module!(igniter, module), 2)
case Igniter.Code.Common.move_to(zipper, fn zipper ->
if Igniter.Code.Function.function_call?(zipper, :use, 2) do
using_a_webbish_module?(zipper) &&
Igniter.Code.Function.argument_equals?(zipper, 1, :html)
else
false
end
end) do
{:ok, _} ->
true
_ ->
false
end
end
@doc "Returns `true` if the module is a Phoenix controller"
@spec controller?(Igniter.t(), module()) :: boolean()
def controller?(igniter, module) do
zipper = elem(Igniter.Project.Module.find_module!(igniter, module), 2)
case Igniter.Code.Common.move_to(zipper, fn zipper ->
if Igniter.Code.Function.function_call?(zipper, :use, 2) do
using_a_webbish_module?(zipper) &&
Igniter.Code.Function.argument_equals?(zipper, 1, :controller)
else
false
end
end) do
{:ok, _} ->
true
_ ->
false
end
end
@doc """
Generates a module name that lives in the Web directory of the current app.
"""
@spec web_module_name(String.t()) :: module()
@deprecated "Use `web_module_name/2` instead."
def web_module_name(suffix) do
Module.concat(web_module(Igniter.new()), suffix)
end
@doc """
Generates a module name that lives in the Web directory of the current app.
"""
@spec web_module_name(Igniter.t(), String.t()) :: module()
def web_module_name(igniter, suffix) do
Module.concat(web_module(igniter), suffix)
end
@doc "Gets the list of endpoints that use a given router"
@spec endpoints_for_router(igniter :: Igniter.t(), router :: module()) ::
{Igniter.t(), list(module())}
def endpoints_for_router(igniter, router) do
Igniter.Project.Module.find_all_matching_modules(igniter, fn _module, zipper ->
with {:ok, _} <- Igniter.Code.Module.move_to_use(zipper, Phoenix.Endpoint),
{:ok, _} <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:plug,
[1, 2],
&Igniter.Code.Function.argument_equals?(&1, 0, router)
) do
true
else
_ ->
false
end
end)
end
@doc """
Selects an endpoint to be used in a later step. If only one endpoint is found, it will be selected automatically.
If no endpoints exist, `{igniter, nil}` is returned.
If multiple endpoints are found, the user is prompted to select one of them.
"""
@spec select_endpoint(Igniter.t(), module() | nil, String.t()) :: {Igniter.t(), module() | nil}
def select_endpoint(igniter, router \\ nil, label \\ "Which endpoint should be used") do
if router do
case endpoints_for_router(igniter, router) do
{igniter, []} ->
{igniter, nil}
{igniter, [endpoint]} ->
{igniter, endpoint}
{igniter, endpoints} ->
{igniter, Igniter.Util.IO.select(label, endpoints, display: &inspect/1)}
end
else
{igniter, endpoints} =
Igniter.Project.Module.find_all_matching_modules(igniter, fn _mod, zipper ->
Igniter.Code.Module.move_to_use(zipper, Phoenix.Endpoint) != :error
end)
case endpoints do
[] ->
{igniter, nil}
[endpoint] ->
{igniter, endpoint}
endpoints ->
{igniter, Igniter.Util.IO.select(label, endpoints, display: &inspect/1)}
end
end
end
@doc """
Adds a scope to a Phoenix router.
## Options
- `:router` - The router module to append to. Will be looked up if not provided.
- `:arg2` - The second argument to the scope macro. Must be a value (typically a module).
- `:placement` - `:before` | `:after`. Determines where the `contents` will be placed:
- `:before` - place before the first scope in the module if one is found, otherwise tries to place
after the last pipeline or after the `use MyAppWeb, :router` call.
- `:after` - place at the end (bottom) of the module, after all scopes and pipelines.
"""
@spec add_scope(Igniter.t(), String.t(), String.t(), Keyword.t()) :: Igniter.t()
def add_scope(igniter, route, contents, opts \\ []) do
{igniter, router} =
case Keyword.fetch(opts, :router) do
{:ok, router} ->
{igniter, router}
:error ->
select_router(igniter)
end
scope_code =
case Keyword.fetch(opts, :arg2) do
{:ok, arg2} ->
"""
scope "#{route}", #{inspect(arg2)} do
#{contents}
end
"""
_ ->
"""
scope #{inspect(route)} do
#{contents}
end
"""
end
if router do
Igniter.Project.Module.find_and_update_module!(igniter, router, fn zipper ->
case move_to_scope_location(igniter, zipper, placement: opts[:placement] || :before) do
{:ok, zipper, append_or_prepend} ->
{:ok, Igniter.Code.Common.add_code(zipper, scope_code, placement: append_or_prepend)}
:error ->
{:warning,
Igniter.Util.Warning.formatted_warning(
"Could not add a scope for #{inspect(route)} to your router. Please add it manually.",
scope_code
)}
end
end)
else
Igniter.add_warning(
igniter,
Igniter.Util.Warning.formatted_warning(
"Could not add a scope for #{inspect(route)} to your router. Please add it manually.",
scope_code
)
)
end
end
@doc """
Appends to a phoenix router scope.
Relatively limited currently only exact matches of a top level route, second argument, and pipelines.
## Options
* `:router` - The router module to append to. Will be looked up if not provided.
* `:arg2` - The second argument to the scope macro. Must be a value (typically a module).
* `:with_pipelines` - A list of pipelines that the pipeline must be using to be considered a match.
- `:placement` - `:before` | `:after`. Determines where the `contents` will be placed. Note that it first tries
to find a matching scope and place the contents into that scope, otherwise `:placement` is used to determine
where to place the contents:
* `:before` - place before the first scope in the module if one is found, otherwise tries to place
after the last pipeline or after the `use MyAppWeb, :router` call.
* `:after` - place at the end (bottom) of the module, after all scopes and pipelines.
"""
@spec append_to_scope(Igniter.t(), String.t(), String.t(), Keyword.t()) :: Igniter.t()
def append_to_scope(igniter, route, contents, opts \\ []) do
{igniter, router} =
case Keyword.fetch(opts, :router) do
{:ok, router} ->
{igniter, router}
:error ->
select_router(igniter)
end
scope_code =
case Keyword.fetch(opts, :arg2) do
{:ok, arg2} ->
if opts[:with_pipelines] do
"""
scope "#{route}", #{inspect(arg2)} do
pipe_through #{inspect(opts[:with_pipelines])}
#{contents}
end
"""
else
"""
scope "#{route}", #{inspect(arg2)} do
#{contents}
end
"""
end
_ ->
if opts[:with_pipelines] do
"""
scope #{inspect(route)} do
pipe_through #{inspect(opts[:with_pipelines])}
#{contents}
end
"""
else
"""
scope #{inspect(route)} do
#{contents}
end
"""
end
end
if router do
Igniter.Project.Module.find_and_update_module!(igniter, router, fn zipper ->
case move_to_matching_scope(zipper, route, opts) do
{:ok, zipper} ->
{:ok, Igniter.Code.Common.add_code(zipper, contents)}
:error ->
case move_to_scope_location(igniter, zipper, placement: opts[:placement] || :before) do
{:ok, zipper, append_or_prepend} ->
{:ok,
Igniter.Code.Common.add_code(zipper, scope_code, placement: append_or_prepend)}
:error ->
{:warning,
Igniter.Util.Warning.formatted_warning(
"Could not add a scope for #{inspect(route)} to your router. Please add it manually.",
scope_code
)}
end
end
end)
else
Igniter.add_warning(
igniter,
Igniter.Util.Warning.formatted_warning(
"Could not add a scope for #{inspect(route)} to your router. Please add it manually.",
scope_code
)
)
end
end
@doc """
Appends code to a Phoenix router pipeline.
## Options
* `:router` - The router module to append to. Will be looked up if not provided.
"""
@spec append_to_pipeline(Igniter.t(), atom, String.t(), Keyword.t()) :: Igniter.t()
def append_to_pipeline(igniter, name, contents, opts \\ []) do
{igniter, router} =
case Keyword.fetch(opts, :router) do
{:ok, router} ->
{igniter, router}
:error ->
select_router(igniter)
end
pipeline_code = """
pipeline #{inspect(name)} do
#{contents}
end
"""
if router do
Igniter.Project.Module.find_and_update_module!(igniter, router, fn zipper ->
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:pipeline,
2,
fn zipper ->
Igniter.Code.Function.argument_equals?(
zipper,
0,
name
)
end
)
|> case do
{:ok, zipper} ->
case Igniter.Code.Common.move_to_do_block(zipper) do
{:ok, zipper} ->
{:ok, Igniter.Code.Common.add_code(zipper, contents)}
:error ->
{:warning,
Igniter.Util.Warning.formatted_warning(
"Could not add the #{name} pipeline to your router. Please add it manually.",
pipeline_code
)}
end
_ ->
case move_to_pipeline_location(igniter, zipper) do
{:ok, zipper, append_or_prepend} ->
{:ok,
Igniter.Code.Common.add_code(zipper, pipeline_code, placement: append_or_prepend)}
:error ->
{:warning,
Igniter.Util.Warning.formatted_warning(
"Could not add the #{name} pipeline to your router. Please add it manually.",
pipeline_code
)}
end
end
end)
else
Igniter.add_warning(
igniter,
Igniter.Util.Warning.formatted_warning(
"Could not append the following contents to the #{name} pipeline to your router. Please add it manually.",
contents
)
)
end
end
@doc """
Prepends code to a Phoenix router pipeline.
## Options
* `:router` - The router module to append to. Will be looked up if not provided.
"""
@spec prepend_to_pipeline(Igniter.t(), atom, String.t(), Keyword.t()) :: Igniter.t()
def prepend_to_pipeline(igniter, name, contents, opts \\ []) do
{igniter, router} =
case Keyword.fetch(opts, :router) do
{:ok, router} ->
{igniter, router}
:error ->
select_router(igniter)
end
pipeline_code = """
pipeline #{inspect(name)} do
#{contents}
end
"""
if router do
Igniter.Project.Module.find_and_update_module!(igniter, router, fn zipper ->
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:pipeline,
2,
fn zipper ->
Igniter.Code.Function.argument_equals?(
zipper,
0,
name
)
end
)
|> case do
{:ok, zipper} ->
case Igniter.Code.Common.move_to_do_block(zipper) do
{:ok, zipper} ->
{:ok, Igniter.Code.Common.add_code(zipper, contents, placement: :before)}
:error ->
{:warning,
Igniter.Util.Warning.formatted_warning(
"Could not add the #{name} pipeline to your router. Please add it manually.",
pipeline_code
)}
end
_ ->
case move_to_pipeline_location(igniter, zipper) do
{:ok, zipper, append_or_prepend} ->
{:ok,
Igniter.Code.Common.add_code(zipper, pipeline_code, placement: append_or_prepend)}
:error ->
{:warning,
Igniter.Util.Warning.formatted_warning(
"Could not add the #{name} pipeline to your router. Please add it manually.",
pipeline_code
)}
end
end
end)
else
Igniter.add_warning(
igniter,
Igniter.Util.Warning.formatted_warning(
"Could not append the following contents to the #{name} pipeline to your router. Please add it manually.",
contents
)
)
end
end
@doc """
Adds a pipeline to a Phoenix router.
## Options
* `:router` - The router module to append to. Will be looked up if not provided.
* `:arg2` - The second argument to the scope macro. Must be a value (typically a module).
"""
@spec add_pipeline(Igniter.t(), atom(), String.t(), Keyword.t()) :: Igniter.t()
def add_pipeline(igniter, name, contents, opts \\ []) do
{igniter, router} =
case Keyword.fetch(opts, :router) do
{:ok, router} ->
{igniter, router}
:error ->
select_router(igniter)
end
pipeline_code = """
pipeline #{inspect(name)} do
#{contents}
end
"""
if router do
Igniter.Project.Module.find_and_update_module!(igniter, router, fn zipper ->
Igniter.Code.Function.move_to_function_call(zipper, :pipeline, 2, fn zipper ->
Igniter.Code.Function.argument_equals?(
zipper,
0,
name
)
end)
|> case do
{:ok, _} ->
if Keyword.get(opts, :warn_on_present?, true) do
{:warning,
Igniter.Util.Warning.formatted_warning(
"The #{name} pipeline already exists in the router. Attempting to add pipeline: ",
pipeline_code
)}
else
{:ok, zipper}
end
_ ->
case move_to_pipeline_location(igniter, zipper) do
{:ok, zipper, append_or_prepend} ->
{:ok,
Igniter.Code.Common.add_code(zipper, pipeline_code, placement: append_or_prepend)}
:error ->
{:warning,
Igniter.Util.Warning.formatted_warning(
"Could not add the #{name} pipeline to your router. Please add it manually.",
pipeline_code
)}
end
end
end)
else
Igniter.add_warning(
igniter,
Igniter.Util.Warning.formatted_warning(
"Could not add the #{name} pipeline to your router. Please add it manually.",
pipeline_code
)
)
end
end
@doc """
Returns `{igniter, true}` if a pipeline exists in a Phoenix router, and `{igniter, false}` otherwise.
## Options
* `:router` - The router module to append to. Will be looked up if not provided.
* `:arg2` - The second argument to the scope macro. Must be a value (typically a module).
"""
@spec has_pipeline(Igniter.t(), router :: module(), name :: atom()) ::
{Igniter.t(), boolean()}
def has_pipeline(igniter, router, name) do
{_igniter, _source, zipper} = Igniter.Project.Module.find_module!(igniter, router)
Igniter.Code.Function.move_to_function_call(zipper, :pipeline, 2, fn zipper ->
Igniter.Code.Function.argument_equals?(
zipper,
0,
name
)
end)
|> case do
{:ok, _} -> {igniter, true}
_ -> {igniter, false}
end
end
@doc """
Selects a router to be used in a later step. If only one router is found, it will be selected automatically.
If no routers exist, `{igniter, nil}` is returned.
If multiple routes are found, the user is prompted to select one of them.
"""
@spec select_router(Igniter.t(), String.t()) :: {Igniter.t(), module() | nil}
def select_router(igniter, label \\ "Which router should be modified?") do
case list_routers(igniter) do
{igniter, []} ->
{igniter, nil}
{igniter, [router]} ->
{igniter, router}
{igniter, routers} ->
{igniter, Igniter.Util.IO.select(label, routers, display: &inspect/1)}
end
end
@doc "Lists all routers found in the project."
@spec list_routers(Igniter.t()) :: {Igniter.t(), [module()]}
def list_routers(igniter) do
Igniter.Project.Module.find_all_matching_modules(igniter, fn _mod, zipper ->
move_to_router_use(igniter, zipper) != :error
end)
end
@doc "Moves to the use statement in a module that matches `use TheirAppWeb, :router`"
@spec move_to_router_use(Igniter.t(), Sourceror.Zipper.t()) ::
:error | {:ok, Sourceror.Zipper.t()}
def move_to_router_use(igniter, zipper) do
with :error <-
Igniter.Code.Function.move_to_function_call(zipper, :use, 2, fn zipper ->
Igniter.Code.Function.argument_equals?(
zipper,
0,
web_module(igniter)
) &&
Igniter.Code.Function.argument_equals?(
zipper,
1,
:router
)
end) do
Igniter.Code.Module.move_to_use(zipper, Phoenix.Router)
end
end
defp move_to_pipeline_location(igniter, zipper) do
with {:pipeline, :error} <-
{:pipeline,
Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, :pipeline, 2)},
:error <-
Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, :scope, [2, 3, 4]) do
case move_to_router_use(igniter, zipper) do
{:ok, zipper} -> {:ok, zipper, :after}
:error -> :error
end
else
{:pipeline, {:ok, zipper}} ->
{:ok, zipper, :before}
{:ok, zipper} ->
{:ok, zipper, :before}
end
end
defp move_to_scope_location(igniter, zipper, opts) do
with {:placement, :before} <- {:placement, opts[:placement]},
:error <-
Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, :scope, [2, 3, 4]),
{:pipeline, :error} <- {:pipeline, last_pipeline(zipper)} do
case move_to_router_use(igniter, zipper) do
{:ok, zipper} -> {:ok, zipper, :after}
:error -> :error
end
else
{:ok, zipper} ->
{:ok, zipper, :before}
{:placement, :after} ->
{:ok, zipper, :after}
{:pipeline, {:ok, zipper}} ->
{:ok, zipper, :after}
end
end
defp last_pipeline(zipper) do
case Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, :pipeline, 2) do
{:ok, zipper} ->
with zipper when not is_nil(zipper) <- Sourceror.Zipper.right(zipper),
{:ok, zipper} <- last_pipeline(zipper) do
{:ok, zipper}
else
_ ->
{:ok, zipper}
end
:error ->
:error
end
end
defp using_a_webbish_module?(zipper) do
case Igniter.Code.Function.move_to_nth_argument(zipper, 0) do
{:ok, zipper} ->
Igniter.Code.Module.module_matching?(zipper, &String.ends_with?(to_string(&1), "Web"))
:error ->
false
end
end
# We can do all kinds of things better here
# for example, we can handle nested scopes, etc.
defp move_to_matching_scope(zipper, route, opts) do
call =
if is_nil(opts[:arg2]) do
zipper
|> Igniter.Code.Function.move_to_function_call_in_current_scope(:scope, [2], fn call ->
Igniter.Code.Function.argument_equals?(call, 0, route)
end)
else
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:scope,
3,
fn call ->
Igniter.Code.Function.argument_equals?(call, 0, route) and
Igniter.Code.Function.argument_equals?(call, 1, opts[:arg2])
end
)
end
case call do
{:ok, zipper} ->
with {:ok, zipper} <- Igniter.Code.Common.move_to_do_block(zipper),
true <- contains_pipe_through?(zipper, opts) do
{:ok, zipper}
else
_ ->
:error
end
:error ->
:error
end
end
defp contains_pipe_through?(zipper, opts) do
case List.wrap(opts[:with_pipelines]) do
[] ->
true
pipelines ->
Enum.all?(pipelines, fn pipeline ->
zipper
|> Igniter.Code.Function.move_to_function_call_in_current_scope(
:pipe_through,
1,
fn zipper ->
with {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 0),
{:is_list?, _zipper, true} <-
{:is_list?, zipper, Igniter.Code.List.list?(zipper)},
{:ok, _} <-
Igniter.Code.List.move_to_list_item(
zipper,
&Igniter.Code.Common.nodes_equal?(&1, pipeline)
) do
true
else
{:is_list?, zipper, false} ->
Igniter.Code.Common.nodes_equal?(zipper, pipeline)
_ ->
false
end
end
)
|> case do
{:ok, _} -> true
:error -> false
end
end)
end
end
end