Current section

Files

Jump to
routex lib routex utils.ex
Raw

lib/routex/utils.ex

defmodule Routex.Utils do
@moduledoc """
Provides an interface to functions which can be used in extensions.
"""
@doc """
Prints an indented text. Should be used when printing messages in
the terminal during compile time.
"""
alias Routex.Types, as: T
@line_indicator ":: "
# credo:disable-for-lines:2
@spec print(module(), input :: iodata) :: :ok
def print(module \\ nil, input)
def print(nil, nil), do: :noop
def print(module, input) do
prefix = (module && [@line_indicator, inspect(module), " "]) || ""
IO.write([prefix, @line_indicator, sanitize(input), IO.ANSI.reset(), "\n"])
end
defp sanitize(input) when is_atom(input), do: to_string(input)
defp sanitize(input) when is_list(input),
do: input |> Enum.reject(&is_nil/1) |> Enum.map(&sanitize/1)
defp sanitize(input) when is_binary(input),
do: String.replace(input, "\n", "\n#{@line_indicator}")
@doc """
Prints an alert. Should be used when printing critical alerts in
the terminal during compile time.
"""
# credo:disable-for-lines:11
@spec alert(input :: iodata) :: :ok
def alert(title \\ "Critical", input),
do:
IO.write([
IO.ANSI.blink_slow(),
IO.ANSI.light_red_background(),
sanitize(title),
IO.ANSI.reset(),
" ",
sanitize(input),
"\n"
])
@doc """
Helps setting the branch in the process dictionary.
**Example: As dispatch target**
```elixir
dispatch_targets: [{Routex.Utils, :process_put_branch, [[:attrs, :__branch__]]}]
```
"""
@spec process_put_branch(branch :: [integer, ...]) :: integer
@spec process_put_branch(branch :: integer) :: integer
def process_put_branch(branch) when is_list(branch) do
leaf = get_branch_leaf(branch)
Process.put(:rtx_branch, leaf)
end
def process_put_branch(leaf) when is_integer(leaf) do
Process.put(:rtx_branch, leaf)
end
@doc """
Returns the AST to get the current branch leaf from process dict or from assigns, conn or socket
based on the available variables in the `caller` module.
"""
@spec get_helper_ast(caller :: T.env()) :: T.ast()
def get_helper_ast(caller) do
quote do
if branch = Process.get(:rtx_branch) do
branch
else
unquote(get_derived_ast(caller))
end
end
end
defp get_derived_ast(caller) do
vars = list_available_module_vars(caller)
cond do
:assigns in vars ->
mod =
try do
Macro.expand_once({:__aliases__, [], [:Routes]}, caller)
rescue
e in FunctionClauseError ->
if caller.module == Routex.UtilsTest,
do: Routex.UtilsTest.Helpers,
else: reraise(e, __STACKTRACE__)
end
quote do
assigns
|> var!()
|> Routex.Utils.get_branch_leaf_from_assigns(unquote(mod), unquote(caller.module))
end
:conn in vars ->
quote do
conn |> var!() |> Routex.Utils.get_branch_leaf()
end
:socket in vars ->
quote do
socket |> var!() |> Routex.Utils.get_branch_leaf()
end
ExUnit.Callbacks in caller.requires ->
0
true ->
require Logger
Logger.warning(
"#{caller.module}: No helper AST and no process key `:rtx_branch` found. Fallback to `0`"
)
0
end
end
@doc """
Returns the branch leaf from assigns.
"""
@spec get_branch_leaf_from_assigns(map, module, module) :: integer()
def get_branch_leaf_from_assigns(%{conn: conn}, _helper_mod, _caller),
do: Routex.Utils.get_branch_leaf(conn)
def get_branch_leaf_from_assigns(%{socket: socket}, _helper_mod, _caller),
do: Routex.Utils.get_branch_leaf(socket)
def get_branch_leaf_from_assigns(%{url: url}, helper_mod, _caller),
do: helper_mod.attrs(url).__branch__ |> Routex.Utils.get_branch_leaf()
def get_branch_leaf_from_assigns(assigns, _mod, call_mod) do
require Logger
Logger.warning("""
Routex detected a problem in module #{call_mod |> to_string()}:
Expected one of :socket, :conn, or :url in assigns, or process dictonary key
:rtx_branch being set, but none was found.
Assigns do not propagate automatically into child components; you must provide
them explicitly, for example:
<Layout.app url={@url}> ... </Layout.app>
When using a component, make sure :url is a required attribute.
attr :url, :string,
required: true,
doc: "Routex pass through: url={@url}"
Alternatively, you can set the process key. See the Verified Routes extension
documentation for more information: https://hexdocs.pm/routex/Routex.Extension.VerifiedRoutes.html
Falling back to default route.
Received assigns:
#{inspect(assigns, pretty: true, limit: :infinity, structs: false)}
""")
{:current_stacktrace, st} = Process.info(self(), :current_stacktrace)
st |> tl() |> Exception.format_stacktrace() |> Logger.warning()
0
end
@spec get_branch_leaf(T.route() | map | Plug.Conn.t() | Phoenix.Socket.t() | [integer, ...]) ::
integer()
def get_branch_leaf(%{private: %{routex: %{__branch__: branch}}}) do
List.last(branch)
end
def get_branch_leaf(branch) when is_list(branch) do
List.last(branch)
end
def get_branch_leaf(assigns_conn_socket) do
require Logger
Logger.warning("""
Routex detected a problem: could not detect a branch.
Tips:
- Using branching verified routes in `mount/3` is not supported.
Please move the code to the `handle_params/3`.
- Otherwise please report the issue including the information below.
Assigns, conn or socket:
#{inspect(assigns_conn_socket, pretty: true, limit: :infinity, structs: false)}
""")
{:current_stacktrace, st} = Process.info(self(), :current_stacktrace)
st |> tl() |> Exception.format_stacktrace() |> Logger.warning()
0
end
@doc """
Test env aware variant of Module.get_attribute. Delegates to
`Module.get_attribute/3` in non-test environments. In test environment it
returns the result of `Module.get_attribute/3` or an empty list when the
module is already compiled.
"""
if Mix.env() == :test do
def get_attribute(module, key, default \\ nil) do
Module.get_attribute(module, key, default)
rescue
ArgumentError ->
alert(
"TEST MODE",
"Used fallback for `get_attributes(#{module}, #{key})`, returning an empty list"
)
[]
end
else
defdelegate get_attribute(module, key, default \\ nil), to: Module
end
defp list_available_module_vars(caller) do
caller.versioned_vars
|> Enum.filter(fn
{{var, _module}, _version} when var in [:socket, :conn, :assigns] ->
true
_other ->
false
end)
|> Enum.map(fn {{var, _module}, _version} -> var end)
end
# =====================
# Compatilility Helpers
# =====================
# credo:disable-for-next-line
# TODO: Extract to own module for discoverability?
# credo:disable-for-next-line
# TODO: remove when we depend on Elixir 1.12+
@doc """
Backward compatible version of `Code.ensure_compiled!/1`
"""
@spec ensure_compiled!(module()) :: module()
if function_exported?(Code, :ensure_compiled!, 1) do
def ensure_compiled!(mod), do: Code.ensure_compiled!(mod)
else
def ensure_compiled!(mod) do
case Code.ensure_compiled(mod) do
{:module, ^mod} ->
mod
{:error, reason} ->
raise ArgumentError,
"could not load module #{inspect(mod)} due to reason #{inspect(reason)}"
end
end
end
@doc """
Returns the module to use for LiveView assignments
"""
@spec assign_module :: module()
{:ok, phx_version} = :application.get_key(:phoenix, :vsn)
if phx_version |> to_string() |> Version.match?("< 1.7.0-dev") do
def assign_module, do: Phoenix.LiveView
else
def assign_module, do: Phoenix.Component
end
end