Packages
igniter
0.6.1
0.8.2
0.8.1
0.8.0
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.30
0.6.29
0.6.28
0.6.27
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.52
0.5.51
0.5.50
0.5.49
0.5.48
0.5.47
0.5.46
0.5.45
0.5.44
0.5.43
0.5.42
0.5.41
0.5.40
0.5.39
0.5.38
0.5.37
0.5.36
0.5.35
0.5.34
0.5.33
0.5.32
0.5.31
0.5.30
0.5.29
0.5.28
0.5.27
0.5.26
0.5.25
0.5.24
0.5.23
0.5.22
0.5.21
0.5.20
0.5.19
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.78
0.3.77
0.3.76
0.3.75
0.3.74
0.3.73
0.3.72
0.3.71
0.3.70
0.3.69
0.3.68
0.3.67
0.3.66
0.3.65
0.3.64
0.3.63
0.3.62
0.3.61
0.3.60
0.3.59
0.3.58
0.3.57
0.3.56
0.3.55
0.3.54
0.3.53
0.3.52
0.3.51
0.3.50
0.3.49
0.3.48
0.3.47
0.3.46
0.3.45
0.3.44
0.3.43
0.3.42
0.3.41
0.3.40
0.3.39
0.3.38
0.3.37
0.3.36
0.3.35
0.3.34
0.3.33
0.3.32
0.3.31
0.3.30
0.3.29
0.3.28
0.3.27
0.3.26
0.3.25
0.3.24
0.3.23
0.3.22
0.3.21
0.3.20
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
A code generation and project patching framework
Current section
Files
Jump to
Current section
Files
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