Current section
Files
Jump to
Current section
Files
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 """
Returns the AST to get the current branch 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 ->
quote do
assigns = var!(assigns)
Routex.Utils.get_branch(assigns[:conn] || assigns[:socket])
end
:conn in vars ->
quote do
conn |> var!() |> Routex.Utils.get_branch()
end
:socket in vars ->
quote do
socket |> var!() |> Routex.Utils.get_branch()
end
ExUnit.Callbacks in caller.requires ->
0
true ->
require Logger
Logger.warning(
"#{caller.module}: No helper AST and no proces key `:rtx_branch` found. Fallback to `0`"
)
0
end
end
@spec get_branch(T.route() | map | Plug.Conn.t() | Phoenix.Socket.t()) :: integer()
def get_branch(%{private: %{routex: %{__branch__: branch}}}) do
List.last(branch)
end
def get_branch(_assigns_conn_socket) do
require Logger
Logger.warning("""
Using branching verified routes in `mount/3` is not supported.
Please move the code to the `handle_params/3`.
""")
{: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