Fix movement commands retrying on estop

Fix movement commands retrying on estop

defmodule FarmbotOS.SysCalls do
  require FarmbotCore.Logger
  require Logger

  alias FarmbotCeleryScript.AST
  alias FarmbotFirmware

  alias FarmbotCore.Asset.{
    BoxLed,
    Private
  }

  alias FarmbotOS.SysCalls.{
    ChangeOwnership,
    CheckUpdate,
    DumpInfo,
    Farmware,
    FactoryReset,
    FlashFirmware,
    SendMessage,
    SetPinIOMode,
    PinControl
  }

  alias FarmbotOS.Lua

  alias FarmbotCore.{Asset, Asset.Repo, Asset.Private, Asset.Sync, BotState, Leds}
  alias FarmbotExt.{API, API.Reconciler, API.SyncGroup}

  @behaviour FarmbotCeleryScript.SysCalls

  @impl true
  defdelegate send_message(level, message, channels), to: SendMessage

  @impl true
  defdelegate execute_script(name, env), to: Farmware

  @impl true
  defdelegate update_farmware(name), to: Farmware

  @impl true
  defdelegate flash_firmware(package), to: FlashFirmware

  @impl true
  defdelegate change_ownership(email, secret, server), to: ChangeOwnership

  @impl true
  defdelegate dump_info(), to: DumpInfo

  @impl true
  defdelegate check_update(), to: CheckUpdate

  @impl true
  defdelegate read_status(), to: FarmbotExt.AMQP.BotStateChannel

  @impl true
  defdelegate factory_reset(package), to: FactoryReset

  @impl true
  defdelegate set_pin_io_mode(pin, mode), to: SetPinIOMode

  @impl true
  defdelegate eval_assertion(comment, expression), to: Lua
  defdelegate log_assertion(passed?, type, message), to: Lua

  @impl true
  defdelegate read_pin(number, mode), to: PinControl

  @impl true
  defdelegate write_pin(number, mode, value), to: PinControl

  @impl true
  defdelegate toggle_pin(number), to: PinControl

  @impl true
  def log(message, force?) do
    if force? || FarmbotCore.Asset.fbos_config(:sequence_body_log) do
      FarmbotCore.Logger.info(2, message)
      :ok
    else
      :ok
    end
  end

  @impl true
  def sequence_init_log(message) do
    if FarmbotCore.Asset.fbos_config(:sequence_init_log) do
      FarmbotCore.Logger.info(2, message)
      :ok
    else
      :ok
    end
  end

  @impl true
  def sequence_complete_log(message) do
    if FarmbotCore.Asset.fbos_config(:sequence_complete_log) do
      FarmbotCore.Logger.info(2, message)
      :ok
    else
      :ok
    end
  end

  @impl true
  def reboot do
    FarmbotOS.System.reboot("Reboot requested by Sequence or frontend")
    :ok
  end

  @impl true
  def power_off do
    FarmbotOS.System.shutdown("Shut down requested by Sequence or frontend")
    :ok
  end

  @impl true
  def firmware_reboot do
    GenServer.stop(FarmbotFirmware, :reboot)
  end

  @impl true
  def resource_update(kind, id, params) do
    module = Module.concat(Asset, kind)

    with true <- Code.ensure_loaded?(module),
         %{} = orig <- Repo.get_by(module, [id: id], preload: [:local_meta]),
         %{valid?: true} = change <- module.changeset(orig, params),
         {:ok, new} <- Repo.update(change),
         new <- Repo.preload(new, [:local_meta]) do
      Private.mark_dirty!(new, %{})
      :ok
    else
      false ->
        {:error, "unknown asset kind: #{kind}"}

      nil ->
        {:error, "Could not find asset by kind: #{kind} and id: #{id}"}

      %{valid?: false} = changeset ->
        {:error, "failed to update #{kind}: #{inspect(changeset.errors)}"}
    end
  end

  @impl true
  def set_user_env(key, value) do
    with {:ok, fwe} <- Asset.new_farmware_env(%{key: key, value: value}),
         _ <- Private.mark_dirty!(fwe) do
      :ok
    else
      {:error, reason} ->
        {:error, inspect(reason)}

      error ->
        {:error, inspect(error)}
    end
  end

  @impl true
  def get_current_x do
    get_position(:x)
  end

  @impl true
  def get_current_y do
    get_position(:y)
  end

  @impl true
  def get_current_z do
    get_position(:z)
  end

  @impl true
  def zero(axis) do
    axis = assert_axis!(axis)

    case FarmbotFirmware.command({:position_write_zero, [axis]}) do
      :ok ->
        :ok

      {:error, reason} ->
        {:error, "Firmware error: #{inspect(reason)}"}
    end
  end

  @impl true
  def point(kind, id) do
    case Asset.get_point(id: id) do
      nil -> {:error, "#{kind} not found"}
      %{x: x, y: y, z: z} -> %{x: x, y: y, z: z}
    end
  end

  def get_position() do
    case FarmbotFirmware.request({nil, {:position_read, []}}) do
      {:ok, {_, {:report_position, params}}} ->
        params

      {:error, reason} ->
        {:error, "Firmware error: #{inspect(reason)}"}
    end
  end

  def get_position(axis) do
    axis = assert_axis!(axis)

    case get_position() do
      {:error, _} = error -> error
      position -> Keyword.fetch!(position, axis)
    end
  end

  @impl true
  def move_absolute(x, y, z, speed) do
    do_move_absolute(x, y, z, speed, max_retries())
  end

  defp do_move_absolute(x, y, z, speed, retries, errors \\ [])

  defp do_move_absolute(x, y, z, speed, 0, errors) do
    params = [x: x / 1.0, y: y / 1.0, z: z / 1.0, s: speed / 1.0]

    case FarmbotFirmware.command({nil, {:command_movement, params}}) do
      :ok ->
        :ok

      {:error, reason} ->
        errors =
          [reason | errors]
          |> Enum.reverse()
          |> Enum.map(&inspect/1)
          |> Enum.join(", ")

        {:error, "movement error(s): #{errors}"}
    end
  end

  defp do_move_absolute(x, y, z, speed, retries, errors) do
    params = [x: x / 1.0, y: y / 1.0, z: z / 1.0, s: speed / 1.0]

    case FarmbotFirmware.command({nil, {:command_movement, params}}) do
      :ok ->
        :ok

      {:error, :emergency_lock} ->
        {:error, "emergency_lock"}

      {:error, reason} ->
        FarmbotCore.Logger.error(1, "Movement failed. Retrying up to #{retries} more time(s)")
        do_move_absolute(x, y, z, speed, retries - 1, [reason | errors])
    end
  end

  defp max_retries do
    case FarmbotFirmware.request({:parameter_read, [:param_mov_nr_retry]}) do
      {:ok, {_, {:report_parameter_value, [param_mov_nr_retry: nr]}}} -> floor(nr)
      _ -> 3
    end
  end

  @impl true
  def calibrate(axis) do
    axis = assert_axis!(axis)

    case FarmbotFirmware.command({:command_movement_calibrate, [axis]}) do
      :ok ->
        :ok

      {:error, reason} ->
        {:error, "Firmware error: #{inspect(reason)}"}
    end
  end

  @impl true
  def find_home(axis) do
    axis = assert_axis!(axis)

    case FarmbotFirmware.command({:command_movement_find_home, [axis]}) do
      :ok ->
        :ok

      {:error, reason} ->
        {:error, "Firmware error: #{inspect(reason)}"}
    end
  end

  @impl true
  def home(axis, _speed) do
    # TODO(Connor) fix speed
    axis = assert_axis!(axis)

    case FarmbotFirmware.command({:command_movement_home, [axis]}) do
      :ok ->
        :ok

      {:error, reason} ->
        {:error, "Firmware error: #{inspect(reason)}"}
    end
  end

  @impl true
  def emergency_lock do
    _ = FarmbotFirmware.command({:command_emergency_lock, []})
    :ok
  end

  @impl true
  def emergency_unlock do
    _ = FarmbotFirmware.command({:command_emergency_unlock, []})
    :ok
  end

  defp assert_axis!(axis) when is_atom(axis),
    do: axis

  defp assert_axis!(axis) when axis in ~w(x y z),
    do: String.to_existing_atom(axis)

  defp assert_axis!(axis) do
    # {:error, "unknown axis #{axis}"}
    raise("unknown axis #{axis}")
  end

  @impl true
  def wait(ms) do
    Process.sleep(ms)
    :ok
  end

  @impl true
  def named_pin("Peripheral", id) do
    case Asset.get_peripheral(id: id) do
      %{} = peripheral -> peripheral
      nil -> {:error, "Could not find peripheral by id: #{id}"}
    end
  end

  def named_pin("Sensor", id) do
    case Asset.get_sensor(id) do
      %{} = sensor -> sensor
      nil -> {:error, "Could not find peripheral by id: #{id}"}
    end
  end

  def named_pin("BoxLed" <> id, _) do
    %BoxLed{id: String.to_integer(id)}
  end

  def named_pin(kind, id) do
    {:error, "unknown pin kind: #{kind} of id: #{id}"}
  end

  @impl true
  def get_sequence(id) do
    case Asset.get_sequence(id) do
      nil ->
        {:error, "sequence not found"}

      %{} = sequence ->
        ast = AST.decode(sequence)
        args = Map.put(ast.args, :sequence_name, sequence.name)
        %{%{ast | args: args} | meta: %{sequence_name: sequence.name}}
    end
  end

  @impl true
  def get_toolslot_for_tool(id) do
    with %{id: ^id} <- Asset.get_tool(id: id),
         %{x: x, y: y, z: z} <- Asset.get_point(tool_id: id) do
      %{x: x, y: y, z: z}
    else
      nil -> {:error, "Could not find point for tool by id: #{id}"}
    end
  end

  @impl true
  def sync() do
    FarmbotCore.Logger.busy(3, "Syncing")

    with {:ok, sync_changeset} <- API.get_changeset(Sync),
         :ok <- BotState.set_sync_status("syncing"),
         _ <- Leds.green(:fast_blink),
         sync_changeset <- Reconciler.sync_group(sync_changeset, SyncGroup.group_0()),
         sync_changeset <- Reconciler.sync_group(sync_changeset, SyncGroup.group_1()),
         sync_changeset <- Reconciler.sync_group(sync_changeset, SyncGroup.group_2()),
         sync_changeset <- Reconciler.sync_group(sync_changeset, SyncGroup.group_3()),
         _sync_changeset <- Reconciler.sync_group(sync_changeset, SyncGroup.group_4()) do
      FarmbotCore.Logger.success(3, "Synced")
      :ok = BotState.set_sync_status("synced")
      _ = Leds.green(:solid)
      :ok
    else
      error ->
        :ok = BotState.set_sync_status("sync_error")
        _ = Leds.green(:slow_blink)
        {:error, inspect(error)}
    end
  end

  @impl true
  def coordinate(x, y, z) do
    %{x: x, y: y, z: z}
  end

  @impl true
  def set_servo_angle(_pin, _angle) do
    {:error, "set_servo_angle not yet supported"}
  end

  @impl true
  def install_first_party_farmware() do
    {:error, "install_first_party_farmware not yet supported"}
  end

  @impl true
  def nothing(), do: nil
end

defmodule FarmbotOS.SysCalls do
  require FarmbotCore.Logger
  require Logger

  alias FarmbotCeleryScript.AST
  alias FarmbotFirmware

  alias FarmbotCore.Asset.{
    BoxLed,
    Private
  }

  alias FarmbotOS.SysCalls.{
    ChangeOwnership,
    CheckUpdate,
    DumpInfo,
    Farmware,
    FactoryReset,
    FlashFirmware,
    SendMessage,
    SetPinIOMode,
    PinControl
  }

  alias FarmbotOS.Lua

  alias FarmbotCore.{Asset, Asset.Repo, Asset.Private, Asset.Sync, BotState, Leds}
  alias FarmbotExt.{API, API.Reconciler, API.SyncGroup}

  @behaviour FarmbotCeleryScript.SysCalls

  @impl true
  defdelegate send_message(level, message, channels), to: SendMessage

  @impl true
  defdelegate execute_script(name, env), to: Farmware

  @impl true
  defdelegate update_farmware(name), to: Farmware

  @impl true
  defdelegate flash_firmware(package), to: FlashFirmware

  @impl true
  defdelegate change_ownership(email, secret, server), to: ChangeOwnership

  @impl true
  defdelegate dump_info(), to: DumpInfo

  @impl true
  defdelegate check_update(), to: CheckUpdate

  @impl true
  defdelegate read_status(), to: FarmbotExt.AMQP.BotStateChannel

  @impl true
  defdelegate factory_reset(package), to: FactoryReset

  @impl true
  defdelegate set_pin_io_mode(pin, mode), to: SetPinIOMode

  @impl true
  defdelegate eval_assertion(comment, expression), to: Lua
  defdelegate log_assertion(passed?, type, message), to: Lua

  @impl true
  defdelegate read_pin(number, mode), to: PinControl

  @impl true
  defdelegate write_pin(number, mode, value), to: PinControl

  @impl true
  defdelegate toggle_pin(number), to: PinControl

  @impl true
  def log(message, force?) do
    if force? || FarmbotCore.Asset.fbos_config(:sequence_body_log) do
      FarmbotCore.Logger.info(2, message)
      :ok
    else
      :ok
    end
  end

  @impl true
  def sequence_init_log(message) do
    if FarmbotCore.Asset.fbos_config(:sequence_init_log) do
      FarmbotCore.Logger.info(2, message)
      :ok
    else
      :ok
    end
  end

  @impl true
  def sequence_complete_log(message) do
    if FarmbotCore.Asset.fbos_config(:sequence_complete_log) do
      FarmbotCore.Logger.info(2, message)
      :ok
    else
      :ok
    end
  end

  @impl true
  def reboot do
    FarmbotOS.System.reboot("Reboot requested by Sequence or frontend")
    :ok
  end

  @impl true
  def power_off do
    FarmbotOS.System.shutdown("Shut down requested by Sequence or frontend")
    :ok
  end

  @impl true
  def firmware_reboot do
    GenServer.stop(FarmbotFirmware, :reboot)
  end

  @impl true
  def resource_update(kind, id, params) do
    module = Module.concat(Asset, kind)

    with true <- Code.ensure_loaded?(module),
         %{} = orig <- Repo.get_by(module, [id: id], preload: [:local_meta]),
         %{valid?: true} = change <- module.changeset(orig, params),
         {:ok, new} <- Repo.update(change),
         new <- Repo.preload(new, [:local_meta]) do
      Private.mark_dirty!(new, %{})
      :ok
    else
      false ->
        {:error, "unknown asset kind: #{kind}"}

      nil ->
        {:error, "Could not find asset by kind: #{kind} and id: #{id}"}

      %{valid?: false} = changeset ->
        {:error, "failed to update #{kind}: #{inspect(changeset.errors)}"}
    end
  end

  @impl true
  def set_user_env(key, value) do
    with {:ok, fwe} <- Asset.new_farmware_env(%{key: key, value: value}),
         _ <- Private.mark_dirty!(fwe) do
      :ok
    else
      {:error, reason} ->
        {:error, inspect(reason)}

      error ->
        {:error, inspect(error)}
    end
  end

  @impl true
  def get_current_x do
    get_position(:x)
  end

  @impl true
  def get_current_y do
    get_position(:y)
  end

  @impl true
  def get_current_z do
    get_position(:z)
  end

  @impl true
  def zero(axis) do
    axis = assert_axis!(axis)

    case FarmbotFirmware.command({:position_write_zero, [axis]}) do
      :ok ->
        :ok

      {:error, reason} ->
        {:error, "Firmware error: #{inspect(reason)}"}
    end
  end

  @impl true
  def point(kind, id) do
    case Asset.get_point(id: id) do
      nil -> {:error, "#{kind} not found"}
      %{x: x, y: y, z: z} -> %{x: x, y: y, z: z}
    end
  end

  def get_position() do
    case FarmbotFirmware.request({nil, {:position_read, []}}) do
      {:ok, {_, {:report_position, params}}} ->
        params

      {:error, reason} ->
        {:error, "Firmware error: #{inspect(reason)}"}
    end
  end

  def get_position(axis) do
    axis = assert_axis!(axis)

    case get_position() do
      {:error, _} = error -> error
      position -> Keyword.fetch!(position, axis)
    end
  end

  @impl true
  def move_absolute(x, y, z, speed) do
    do_move_absolute(x, y, z, speed, max_retries())
  end

  defp do_move_absolute(x, y, z, speed, retries, errors \\ [])

  defp do_move_absolute(x, y, z, speed, 0, errors) do
    params = [x: x / 1.0, y: y / 1.0, z: z / 1.0, s: speed / 1.0]

    case FarmbotFirmware.command({nil, {:command_movement, params}}) do
      :ok ->
        :ok

      {:error, reason} ->
        errors =
          [reason | errors]
          |> Enum.reverse()
          |> Enum.map(&inspect/1)
          |> Enum.join(", ")

        {:error, "movement error(s): #{errors}"}
    end
  end

  defp do_move_absolute(x, y, z, speed, retries, errors) do
    params = [x: x / 1.0, y: y / 1.0, z: z / 1.0, s: speed / 1.0]

    case FarmbotFirmware.command({nil, {:command_movement, params}}) do
      :ok ->
        :ok

      {:error, reason} ->
        FarmbotCore.Logger.error(1, "Movement failed. Retrying up to #{retries} more time(s)")
        do_move_absolute(x, y, z, speed, retries - 1, [reason | errors])
    end
  end

  defp max_retries do
    case FarmbotFirmware.request({:parameter_read, [:param_mov_nr_retry]}) do
      {:ok, {_, {:report_parameter_value, [param_mov_nr_retry: nr]}}} -> floor(nr)
      _ -> 3
    end
  end

  @impl true
  def calibrate(axis) do
    axis = assert_axis!(axis)

    case FarmbotFirmware.command({:command_movement_calibrate, [axis]}) do
      :ok ->
        :ok

      {:error, reason} ->
        {:error, "Firmware error: #{inspect(reason)}"}
    end
  end

  @impl true
  def find_home(axis) do
    axis = assert_axis!(axis)

    case FarmbotFirmware.command({:command_movement_find_home, [axis]}) do
      :ok ->
        :ok

      {:error, reason} ->
        {:error, "Firmware error: #{inspect(reason)}"}
    end
  end

  @impl true
  def home(axis, _speed) do
    # TODO(Connor) fix speed
    axis = assert_axis!(axis)

    case FarmbotFirmware.command({:command_movement_home, [axis]}) do
      :ok ->
        :ok

      {:error, reason} ->
        {:error, "Firmware error: #{inspect(reason)}"}
    end
  end

  @impl true
  def emergency_lock do
    _ = FarmbotFirmware.command({:command_emergency_lock, []})
    :ok
  end

  @impl true
  def emergency_unlock do
    _ = FarmbotFirmware.command({:command_emergency_unlock, []})
    :ok
  end

  defp assert_axis!(axis) when is_atom(axis),
    do: axis

  defp assert_axis!(axis) when axis in ~w(x y z),
    do: String.to_existing_atom(axis)

  defp assert_axis!(axis) do
    # {:error, "unknown axis #{axis}"}
    raise("unknown axis #{axis}")
  end

  @impl true
  def wait(ms) do
    Process.sleep(ms)
    :ok
  end

  @impl true
  def named_pin("Peripheral", id) do
    case Asset.get_peripheral(id: id) do
      %{} = peripheral -> peripheral
      nil -> {:error, "Could not find peripheral by id: #{id}"}
    end
  end

  def named_pin("Sensor", id) do
    case Asset.get_sensor(id) do
      %{} = sensor -> sensor
      nil -> {:error, "Could not find peripheral by id: #{id}"}
    end
  end

  def named_pin("BoxLed" <> id, _) do
    %BoxLed{id: String.to_integer(id)}
  end

  def named_pin(kind, id) do
    {:error, "unknown pin kind: #{kind} of id: #{id}"}
  end

  @impl true
  def get_sequence(id) do
    case Asset.get_sequence(id) do
      nil ->
        {:error, "sequence not found"}

      %{} = sequence ->
        ast = AST.decode(sequence)
        args = Map.put(ast.args, :sequence_name, sequence.name)
        %{%{ast | args: args} | meta: %{sequence_name: sequence.name}}
    end
  end

  @impl true
  def get_toolslot_for_tool(id) do
    with %{id: ^id} <- Asset.get_tool(id: id),
         %{x: x, y: y, z: z} <- Asset.get_point(tool_id: id) do
      %{x: x, y: y, z: z}
    else
      nil -> {:error, "Could not find point for tool by id: #{id}"}
    end
  end

  @impl true
  def sync() do
    FarmbotCore.Logger.busy(3, "Syncing")

    with {:ok, sync_changeset} <- API.get_changeset(Sync),
         :ok <- BotState.set_sync_status("syncing"),
         _ <- Leds.green(:fast_blink),
         sync_changeset <- Reconciler.sync_group(sync_changeset, SyncGroup.group_0()),
         sync_changeset <- Reconciler.sync_group(sync_changeset, SyncGroup.group_1()),
         sync_changeset <- Reconciler.sync_group(sync_changeset, SyncGroup.group_2()),
         sync_changeset <- Reconciler.sync_group(sync_changeset, SyncGroup.group_3()),
         _sync_changeset <- Reconciler.sync_group(sync_changeset, SyncGroup.group_4()) do
      FarmbotCore.Logger.success(3, "Synced")
      :ok = BotState.set_sync_status("synced")
      _ = Leds.green(:solid)
      :ok
    else
      error ->
        :ok = BotState.set_sync_status("sync_error")
        _ = Leds.green(:slow_blink)
        {:error, inspect(error)}
    end
  end

  @impl true
  def coordinate(x, y, z) do
    %{x: x, y: y, z: z}
  end

  @impl true
  def set_servo_angle(_pin, _angle) do
    {:error, "set_servo_angle not yet supported"}
  end

  @impl true
  def install_first_party_farmware() do
    {:error, "install_first_party_farmware not yet supported"}
  end

  @impl true
  def nothing(), do: nil
end


Fix bug in ApplicationConfigInModuleAttribute

defmodule Credo.Check.Warning.ApplicationConfigInModuleAttribute do
  use Credo.Check,
    base_priority: :high,
    tags: [:controversial],
    category: :warning,
    explanations: [
      check: """
      Module attributes are evaluated at compile time and not at run time. As
      a result, certain configuration read calls made in your module attributes
      may work as expected during local development, but may break once in a
      deployed context.

      This check analyzes all of the module attributes present within a module,
      and validates that there are no unsafe calls. Unsafe calls include:
        - `Application.fetch_env/2`
        - `Application.fetch_env!/2`
        - `Application.get_all_env/1`
        - `Application.get_env/3`
        - `Application.get_env/2`

      As of Elixir 1.10 you can leverage `Application.compile_env/3` and
      `Application.compile_env!/2` if you wish to set configuration at
      compile time using module attributes.
      """
    ]

  @doc false
  @impl true
  def run(%SourceFile{} = source_file, params \\ []) do
    issue_meta = IssueMeta.for(source_file, params)

    Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta))
  end

  defp traverse({:@, meta, [attribute_definition]} = ast, issues, issue_meta) do
    case traverse_attribute(attribute_definition) do
      nil ->
        {ast, issues}

      {attribute, call} ->
        {ast, issues_for_call(attribute, call, meta, issue_meta, issues)}
    end
  end

  defp traverse(ast, issues, _issue_meta) do
    {ast, issues}
  end

  defp traverse_attribute({attribute, _, _} = ast) do
    case Macro.prewalk(ast, nil, &get_forbidden_call/2) do
      {_ast, nil} ->
        nil

      {_ast, call} ->
        {attribute, call}
    end
  end

  defp traverse_attribute(_ast) do
    nil
  end

  defp get_forbidden_call(
         {{:., _, [{:__aliases__, _, [:Application]}, :fetch_env]}, _meta, _args} = ast,
         _acc
       ) do
    {ast, "Application.fetch_env/2"}
  end

  defp get_forbidden_call(
         {{:., _, [{:__aliases__, _, [:Application]}, :fetch_env!]}, _meta, _args} = ast,
         _acc
       ) do
    {ast, "Application.fetch_env!/2"}
  end

  defp get_forbidden_call(
         {{:., _, [{:__aliases__, _, [:Application]}, :get_all_env]}, _meta, _args} = ast,
         _acc
       ) do
    {ast, "Application.get_all_env/1"}
  end

  defp get_forbidden_call(
         {{:., _, [{:__aliases__, _, [:Application]}, :get_env]}, _meta, args} = ast,
         _acc
       ) do
    {ast, "Application.get_env/#{length(args)}"}
  end

  defp get_forbidden_call(ast, acc) do
    {ast, acc}
  end

  defp issues_for_call(attribute, call, meta, issue_meta, issues) do
    options = [
      message:
        "Module attribute @#{Atom.to_string(attribute)} makes use of unsafe Application configuration call #{
          call
        }",
      trigger: call,
      line_no: meta[:line]
    ]

    [format_issue(issue_meta, options) | issues]
  end
end

defmodule Credo.Check.Warning.ApplicationConfigInModuleAttribute do
  use Credo.Check,
    base_priority: :high,
    tags: [:controversial],
    category: :warning,
    explanations: [
      check: """
      Module attributes are evaluated at compile time and not at run time. As
      a result, certain configuration read calls made in your module attributes
      may work as expected during local development, but may break once in a
      deployed context.

      This check analyzes all of the module attributes present within a module,
      and validates that there are no unsafe calls. Unsafe calls include:
        - `Application.fetch_env/2`
        - `Application.fetch_env!/2`
        - `Application.get_all_env/1`
        - `Application.get_env/3`
        - `Application.get_env/2`

      As of Elixir 1.10 you can leverage `Application.compile_env/3` and
      `Application.compile_env!/2` if you wish to set configuration at
      compile time using module attributes.
      """
    ]

  @doc false
  @impl true
  def run(%SourceFile{} = source_file, params \\ []) do
    issue_meta = IssueMeta.for(source_file, params)

    Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta))
  end

  defp traverse({:@, meta, [attribute_definition]} = ast, issues, issue_meta) do
    case traverse_attribute(attribute_definition) do
      nil ->
        {ast, issues}

      {attribute, call} ->
        {ast, issues_for_call(attribute, call, meta, issue_meta, issues)}
    end
  end

  defp traverse(ast, issues, _issue_meta) do
    {ast, issues}
  end

  defp traverse_attribute({attribute, _, _} = ast) do
    case Macro.prewalk(ast, nil, &get_forbidden_call/2) do
      {_ast, nil} ->
        nil

      {_ast, call} ->
        {attribute, call}
    end
  end

  defp get_forbidden_call(
         {{:., _, [{:__aliases__, _, [:Application]}, :fetch_env]}, _meta, _args} = ast,
         _acc
       ) do
    {ast, "Application.fetch_env/2"}
  end

  defp get_forbidden_call(
         {{:., _, [{:__aliases__, _, [:Application]}, :fetch_env!]}, _meta, _args} = ast,
         _acc
       ) do
    {ast, "Application.fetch_env!/2"}
  end

  defp get_forbidden_call(
         {{:., _, [{:__aliases__, _, [:Application]}, :get_all_env]}, _meta, _args} = ast,
         _acc
       ) do
    {ast, "Application.get_all_env/1"}
  end

  defp get_forbidden_call(
         {{:., _, [{:__aliases__, _, [:Application]}, :get_env]}, _meta, args} = ast,
         _acc
       ) do
    {ast, "Application.get_env/#{length(args)}"}
  end

  defp get_forbidden_call(ast, acc) do
    {ast, acc}
  end

  defp issues_for_call(attribute, call, meta, issue_meta, issues) do
    options = [
      message:
        "Module attribute @#{Atom.to_string(attribute)} makes use of unsafe Application configuration call #{
          call
        }",
      trigger: call,
      line_no: meta[:line]
    ]

    [format_issue(issue_meta, options) | issues]
  end
end


Fix relative redirect handling in API

Fix relative redirect handling in API

The Location header can be a relative path from the requested URL, and
so should be resolved to a full URL.

Poison helpfully just crashes the process with a `badarg` if you don't do
this, so Phoenix gives you no stacktrace. It's great.

defmodule FourOhFourFinderApp.PageAnalyzer do
  use HTTPoison.Base

  def analyze(url) do
    case get_with_redirects(url) do
      {:ok, %HTTPoison.Response{status_code: code, body: body}} ->
        if good_resp?(code) do
          %{
            url: url,
            http_status: code,
            success: true,
            outgoing_hrefs: tree_hrefs(body, url),
            ids: tree_ids(body)
          }
        else
          %{ url: url, http_status: code, success: false }
        end

      {:error, %HTTPoison.Error{reason: reason}} ->
        %{
          url: url,
          error: reason,
          success: false
        }
    end
  end

  def get_with_redirects(url) do
    case get(url) do
      {:ok, response} ->
        if redirect?(response.status_code) do
          get_with_redirects(resolve_url(redirect_location(response.headers), url))
        else
          {:ok, response}
        end

      {:error, response} ->
        {:error, response}
    end
  end

  def redirect_location(headers) do
    elem(Enum.find(headers, fn({header, _}) -> "Location" == header end), 1)
  end

  def good_resp?(code) do
    code >= 200 && code < 300
  end

  def redirect?(code) do
    code >= 300 && code < 400
  end

  def tree_hrefs(html, url) do
    html |> Floki.find("a[href]")
      |> Enum.reject(fn(a) -> is_nil(Enum.at(Floki.attribute(a, "href"), 0)) end)
      |> Enum.filter(fn(a) -> String.length(Enum.at(Floki.attribute(a, "href"), 0)) > 0 end)
      |> Enum.map(fn(a) ->
        %{
           href: resolve_url(Enum.at(Floki.attribute(a, "href"), 0), url),
           text: Floki.text(a),
        }
      end)
  end

  def tree_ids(html) do
    html |> Floki.find("[id]") |> Floki.attribute("id")
      |> Enum.reject(&is_nil/1)
  end

  def resolve_url(target, base) do
    case URI.parse(target) do
      %{ scheme: nil } -> resolve_path_url(target, base)
      _ -> target
    end
  end

  def resolve_path_url(target, base) do
    base_uri = URI.parse(base)
    case target do
      "/" <> _ -> # absolute path on same domain
        new_uri = %{ base_uri | path: target }
        URI.to_string(new_uri)
      _ -> # relative path
        new_uri = %{ base_uri | path: String.rstrip(base_uri.path, ?/) <> "/" <> target }
        URI.to_string(base_uri)
    end
  end
end

defmodule FourOhFourFinderApp.PageAnalyzer do
  use HTTPoison.Base

  def analyze(url) do
    case get_with_redirects(url) do
      {:ok, %HTTPoison.Response{status_code: code, body: body}} ->
        if good_resp?(code) do
          %{
            url: url,
            http_status: code,
            success: true,
            outgoing_hrefs: tree_hrefs(body, url),
            ids: tree_ids(body)
          }
        else
          %{ url: url, http_status: code, success: false }
        end

      {:error, %HTTPoison.Error{reason: reason}} ->
        %{
          url: url,
          error: reason,
          success: false
        }
    end
  end

  def get_with_redirects(url) do
    case get(url) do
      {:ok, response} ->
        if redirect?(response.status_code) do
          get_with_redirects(redirect_location(response.headers))
        else
          {:ok, response}
        end

      {:error, response} ->
        {:error, response}
    end
  end

  def redirect_location(headers) do
    elem(Enum.find(headers, fn({header, _}) -> "Location" == header end), 1)
  end

  def good_resp?(code) do
    code >= 200 && code < 300
  end

  def redirect?(code) do
    code >= 300 && code < 400
  end

  def tree_hrefs(html, url) do
    html |> Floki.find("a[href]")
      |> Enum.reject(fn(a) -> is_nil(Enum.at(Floki.attribute(a, "href"), 0)) end)
      |> Enum.filter(fn(a) -> String.length(Enum.at(Floki.attribute(a, "href"), 0)) > 0 end)
      |> Enum.map(fn(a) ->
        %{
           href: resolve_url(Enum.at(Floki.attribute(a, "href"), 0), url),
           text: Floki.text(a),
        }
      end)
  end

  def tree_ids(html) do
    html |> Floki.find("[id]") |> Floki.attribute("id")
      |> Enum.reject(&is_nil/1)
  end

  def resolve_url(target, base) do
    case URI.parse(target) do
      %{ scheme: nil } -> resolve_path_url(target, base)
      _ -> target
    end
  end

  def resolve_path_url(target, base) do
    base_uri = URI.parse(base)
    case target do
      "/" <> _ -> # absolute path on same domain
        new_uri = %{ base_uri | path: target }
        URI.to_string(new_uri)
      _ -> # relative path
        new_uri = %{ base_uri | path: String.rstrip(base_uri.path, ?/) <> "/" <> target }
        URI.to_string(base_uri)
    end
  end
end


Added test cases for status ok and elapsed time


get_block bug

get_block bug

defmodule Neoscan.Blocks do
  @moduledoc false

  @moduledoc """
  The boundary for the Blocks system.
  """

  import Ecto.Query, warn: true
  alias Neoscan.Repo
  alias Neoscan.Blocks.Block
  alias Neoscan.Transactions.Transaction
  alias Neoscan.Addresses
  alias NeoscanMonitor.Api

  @doc """
  Returns the list of blocks.

  ## Examples

      iex> list_blocks()
      [%Block{}, ...]

  """
  def list_blocks do
    Repo.all(Block)
  end

  @doc """
  Returns the list of blocks in the home page.

  ## Examples

      iex> home_blocks()
      [%Block{}, ...]

  """
  def home_blocks do
    block_query = from e in Block,
      where: e.index > 1200000,
      order_by: [desc: e.index],
      select: %{:index => e.index, :time => e.time, :tx_count => e.tx_count, :hash => e.hash},
      limit: 15

    Repo.all(block_query)
  end

  @doc """
  Gets a single block.

  Raises `Ecto.NoResultsError` if the Block does not exist.

  ## Examples

      iex> get_block!(123)
      %Block{}

      iex> get_block!(456)
      ** (Ecto.NoResultsError)

  """
  def get_block!(id), do: Repo.get!(Block, id)

  @doc """
  Gets a single block by its hash value

  ## Examples

      iex> get_block_by_hash(123)
      %Block{}

      iex> get_block_by_hash(456)
      nil

  """
  def get_block_by_hash(hash) do
   query = from e in Block,
     where: e.hash == ^hash,
     select: e
   Repo.all(query)
   |> List.first
  end

  @doc """
  Gets a single block by its hash value for blocks page

  ## Examples

      iex> get_block_by_hash_for_view(hash)
      %Block{}

      iex> get_block_by_hash_for_view(hash)
      nil

  """
  def get_block_by_hash_for_view(hash) do
    trans_query = from t in Transaction,
      select: %{
        type: t.type,
        txid: t.txid
      }
   query = from e in Block,
     where: e.hash == ^hash,
     preload: [transactions: ^trans_query],
     select: e
   Repo.all(query)
   |> List.first
  end

  @doc """
  Gets a single block by its heigh value

  ## Examples

      iex> get_block_by_height(123)
      %Block{}

      iex> get_block_by_height(456)
      nill

  """
  def get_block_by_height(height) do
   query = from e in Block,
     where: e.index == ^height,
     select: e
   Repo.all(query)
   |> List.first
  end

  @doc """
  Creates a block.

  ## Examples

      iex> create_block(%{field: value})
      {:ok, %Block{}}

      iex> create_block(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_block(attrs \\ %{}) do
    %Block{}
    |> Block.changeset(attrs)
    |> Repo.insert!()
    |> update_blocks_state
  end
  def update_blocks_state(block) do
    Api.add_block(block)
    block
  end

  @doc """
  Updates a block.

  ## Examples

      iex> update_block(block, %{field: new_value})
      {:ok, %Block{}}

      iex> update_block(block, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_block(%Block{} = block, attrs) do
    block
    |> Block.changeset(attrs)
    |> Repo.update!()
  end

  @doc """
  Deletes a Block.

  ## Examples

      iex> delete_block(block)
      {:ok, %Block{}}

      iex> delete_block(block)
      {:error, %Ecto.Changeset{}}

  """
  def delete_block(%Block{:updated_at => time} = block) do
    Addresses.rollback_addresses(time)
    Repo.delete!(block)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking block changes.

  ## Examples

      iex> change_block(block)
      %Ecto.Changeset{source: %Block{}}

  """
  def change_block(%Block{} = block) do
    Block.changeset(block, %{})
  end

  @doc """
  Returns the heighest block in the database

  ## Examples

      iex> get_highest_block_in_db()
      {:ok, %Block{}}

  """
  def get_highest_block_in_db() do
    query = from e in Block,
      select: e.index,
      where: e.index > 1200000,  #force postgres to use index
      order_by: [desc: e.index],
      limit: 1

    case Repo.all(query) do
      [index] ->
        {:ok , index}
      [] ->
        {:ok , -1 }
    end
  end

  @doc """
  get all blocks heigher than `height`

  ## Examples

      iex> get_higher_than(height)
      [%Block{}, ...]

  """
  def get_higher_than(index) do
    query = from e in Block,
      where: e.index > ^index,
      select: e
    Repo.all(query)
  end

  @doc """
  delete all blocks in list

  ## Examples

      iex> delete_blocks([%Block{}, ...])
      { :ok, "deleted"}

  """
  def delete_blocks([ block | tail ]), do: [ delete_block(block) | delete_blocks(tail)]
  def delete_blocks([]), do: {:ok , "Deleted" }

  @doc """
  delete all blocks heigher than `height`

  ## Examples

      iex> get_higher_than(height)
      [%Block{}, ...]

  """
  def delete_higher_than(height) do
    get_higher_than(height)
    |> delete_blocks
  end

  def check_if_transaction_blocks_are_missing(transactions) do
    Enum.map(transactions, fn %{"blockhash" => block_hash} = transaction -> check_if_block_exists(block_hash, transaction) end)
  end

  def check_if_block_exists(hash, transaction) do
    query = from e in Block,
      where: e.hash == ^hash,
      select: e
    case Repo.all(query) do
      [] ->
        {:block_missing , transaction}
      [block] ->
        {:transaction_missing, {block, transaction}}
    end
  end

  def get_missing_blocks(transaction_tuples) do
    Enum.filter(transaction_tuples, fn { key, _transaction} -> key == :block_missing end)
    |> Enum.group_by( fn { _key, transaction} -> transaction["blockhash"] end)
    |> Map.keys
    |> Enum.each(fn hash -> get_missing_block(hash) end)
  end

  def get_missing_block(hash) do
    NeoscanSync.Blockchain.get_block_by_hash(NeoscanSync.HttpCalls.url(1), hash)
    |> NeoscanSync.Consumer.add_block
  end

end

defmodule Neoscan.Blocks do
  @moduledoc false

  @moduledoc """
  The boundary for the Blocks system.
  """

  import Ecto.Query, warn: true
  alias Neoscan.Repo
  alias Neoscan.Blocks.Block
  alias Neoscan.Transactions.Transaction
  alias Neoscan.Addresses
  alias NeoscanMonitor.Api

  @doc """
  Returns the list of blocks.

  ## Examples

      iex> list_blocks()
      [%Block{}, ...]

  """
  def list_blocks do
    Repo.all(Block)
  end

  @doc """
  Returns the list of blocks in the home page.

  ## Examples

      iex> home_blocks()
      [%Block{}, ...]

  """
  def home_blocks do
    block_query = from e in Block,
      where: e.index > 1200000,
      order_by: [desc: e.index],
      select: %{:index => e.index, :time => e.time, :tx_count => e.tx_count, :hash => e.hash},
      limit: 15

    Repo.all(block_query)
  end

  @doc """
  Gets a single block.

  Raises `Ecto.NoResultsError` if the Block does not exist.

  ## Examples

      iex> get_block!(123)
      %Block{}

      iex> get_block!(456)
      ** (Ecto.NoResultsError)

  """
  def get_block!(id), do: Repo.get!(Block, id)

  @doc """
  Gets a single block by its hash value

  ## Examples

      iex> get_block_by_hash(123)
      %Block{}

      iex> get_block_by_hash(456)
      nil

  """
  def get_block_by_hash(hash) do
   query = from e in Block,
     where: e.hash == ^hash,
     select: e
   Repo.all(query)
   |> List.first
  end

  @doc """
  Gets a single block by its hash value for blocks page

  ## Examples

      iex> get_block_by_hash_for_view(hash)
      %Block{}

      iex> get_block_by_hash_for_view(hash)
      nil

  """
  def get_block_by_hash_for_view(hash) do
    trans_query = from t in Transaction,
      select: %{
        type: t.type,
        txid: t.txid
      }
   query = from e in Block,
     where: e.hash == ^hash,
     preload: [transactions: ^trans_query],
     select: e
   Repo.all(query)
   |> List.first
  end

  @doc """
  Gets a single block by its heigh value

  ## Examples

      iex> get_block_by_height(123)
      %Block{}

      iex> get_block_by_height(456)
      nill

  """
  def get_block_by_height(height) do
   query = from e in Block,
     where: e.index == ^height,
     select: e
   Repo.all(query)
   |> List.first
  end

  @doc """
  Creates a block.

  ## Examples

      iex> create_block(%{field: value})
      {:ok, %Block{}}

      iex> create_block(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_block(attrs \\ %{}) do
    %Block{}
    |> Block.changeset(attrs)
    |> Repo.insert!()
    |> update_blocks_state
  end
  def update_blocks_state(block) do
    Api.add_block(block)
    block
  end

  @doc """
  Updates a block.

  ## Examples

      iex> update_block(block, %{field: new_value})
      {:ok, %Block{}}

      iex> update_block(block, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_block(%Block{} = block, attrs) do
    block
    |> Block.changeset(attrs)
    |> Repo.update!()
  end

  @doc """
  Deletes a Block.

  ## Examples

      iex> delete_block(block)
      {:ok, %Block{}}

      iex> delete_block(block)
      {:error, %Ecto.Changeset{}}

  """
  def delete_block(%Block{:updated_at => time} = block) do
    Addresses.rollback_addresses(time)
    Repo.delete!(block)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking block changes.

  ## Examples

      iex> change_block(block)
      %Ecto.Changeset{source: %Block{}}

  """
  def change_block(%Block{} = block) do
    Block.changeset(block, %{})
  end

  @doc """
  Returns the heighest block in the database

  ## Examples

      iex> get_highest_block_in_db()
      {:ok, %Block{}}

  """
  def get_highest_block_in_db() do
    query = from e in Block,
      select: e.index,
      where: e.index > 1200000,  #force postgres to use index
      order_by: [desc: e.index],
      limit: 1

    case Repo.all(query) do
      [index] ->
        {:ok , index}
      [] ->
        {:ok , -1 }
    end
  end

  @doc """
  get all blocks heigher than `height`

  ## Examples

      iex> get_higher_than(height)
      [%Block{}, ...]

  """
  def get_higher_than(index) do
    query = from e in Block,
      where: e.index > ^index,
      select: e
    Repo.all(query)
  end

  @doc """
  delete all blocks in list

  ## Examples

      iex> delete_blocks([%Block{}, ...])
      { :ok, "deleted"}

  """
  def delete_blocks([ block | tail ]), do: [ delete_block(block) | delete_blocks(tail)]
  def delete_blocks([]), do: {:ok , "Deleted" }

  @doc """
  delete all blocks heigher than `height`

  ## Examples

      iex> get_higher_than(height)
      [%Block{}, ...]

  """
  def delete_higher_than(height) do
    get_higher_than(height)
    |> delete_blocks
  end

  def check_if_transaction_blocks_are_missing(transactions) do
    Enum.map(transactions, fn %{"blockhash" => block_hash} = transaction -> check_if_block_exists(block_hash, transaction) end)
  end

  def check_if_block_exists(hash, transaction) do
    query = from e in Block,
      where: e.hash == ^hash,
      select: e
    case Repo.all(query) do
      [] ->
        {:block_missing , transaction}
      [block] ->
        {:transaction_missing, {block, transaction}}
    end
  end

  def get_missing_blocks(transaction_tuples) do
    Enum.filter(transaction_tuples, fn { key, _transaction} -> key == :block_missing end)
    |> Enum.group_by( fn { _key, transaction} -> transaction["blockhash"] end)
    |> Map.keys
    |> Enum.each(fn hash -> get_missing_block(hash) end)
  end

  def get_missing_block(hash) do
    NeoscanSync.Blockchain.get_block_by_hash(hash)
    |> NeoscanSync.Consumer.add_block
  end

end


Bump version number

Bump version number


Change structure of deleted objects a bit

defmodule Enricher.Relations do
  require Logger

  @result_document_properties [:shortDescription, :id, :type, :category, :identifier]

  def expand(change = %{ doc: %{ resource: %{ relations: relations }}}, get_for_id) do
    put_in(change.doc.resource.relations, Enum.map(relations, &(expand_relation(&1, get_for_id))) |> Enum.into(%{}))
  end

  defp expand_relation({ name, targets }, get_for_id) do
    { name, Enum.map(targets, &(expand_target(&1, get_for_id))) }
  end

  defp expand_target(target_id, get_for_id) do
    doc = get_for_id.(target_id)
    case doc do
      %{ resource: resource } -> map_resource(resource)
      nil -> %{ resource: %{ id: target_id }, deleted: true }
    end
  end

  defp map_resource(resource) do
    result = %{ resource: Map.take(resource, @result_document_properties) }
    result = if Map.has_key?(result.resource, :type) do
      {category, result} = pop_in(result.resource[:type])
      put_in(result, [:resource, :category], category)
    end
    result
  end
end

defmodule Enricher.Relations do
  require Logger

  @result_document_properties [:shortDescription, :id, :type, :category, :identifier]

  def expand(change = %{ doc: %{ resource: %{ relations: relations }}}, get_for_id) do
    put_in(change.doc.resource.relations, Enum.map(relations, &(expand_relation(&1, get_for_id))) |> Enum.into(%{}))
  end

  defp expand_relation({ name, targets }, get_for_id) do
    { name, Enum.map(targets, &(expand_target(&1, get_for_id))) }
  end

  defp expand_target(target_id, get_for_id) do
    doc = get_for_id.(target_id)
    case doc do
      %{ resource: resource } -> map_resource(resource)
      nil -> %{ resource: %{ id: target_id, deleted: true }}
    end
  end

  defp map_resource(resource) do
    result = %{ resource: Map.take(resource, @result_document_properties) }
    result = if Map.has_key?(result.resource, :type) do
      {category, result} = pop_in(result.resource[:type])
      put_in(result, [:resource, :category], category)
    end
    result
  end
end


Add missing new line to warning

Add missing new line to warning

defmodule Mix.Compilers.ApplicationTracer do
  @moduledoc false
  @manifest "compile.app_tracer"
  @table __MODULE__
  @warnings_key 0

  def init(opts) do
    config = Mix.Project.config()
    manifest = manifest(config)

    if Mix.Utils.stale?([Mix.Project.config_mtime()], [manifest]) do
      build_manifest(config, manifest)
    else
      read_manifest(manifest) || build_manifest(config, manifest)
    end

    setup_warnings_table(config)
    Keyword.update(opts, :tracers, [__MODULE__], &[__MODULE__ | &1])
  end

  # :erlang and other preloaded apps are not part of any module.
  # :code.which/1 will rule them out but, since :erlang is very
  # common, we rule it out upfront.
  def trace({_, _, :erlang, _, _}, _env) do
    :ok
  end

  def trace({type, meta, module, function, arity}, env)
      when type in [:remote_function, :remote_macro, :imported_function, :imported_macro] do
    # Unknown modules need to be looked up and filtered later
    unless :ets.member(@table, module) do
      :ets.insert(
        warnings_table(),
        {module, function, arity, env.module, env.function, env.file, meta[:line] || env.line}
      )
    end

    :ok
  end

  def trace(_, _) do
    :ok
  end

  def warnings(modules_entries) do
    [{_, table, app, excludes}] = :ets.lookup(@table, @warnings_key)

    for module_entry <- modules_entries do
      :ets.delete(table, elem(module_entry, 1))
    end

    warnings =
      :ets.foldl(
        fn {module, function, arity, env_module, env_function, env_file, env_line}, acc ->
          # If the module is either preloaded, it is always available.
          # If the module is non existing, the compiler will warn, so we don't.
          # If the module belongs to this application (from another compiler), we skip it.
          # If the module is excluded, we skip it too.
          with path when is_list(path) <- :code.which(module),
               [module_app, "ebin", _beam] <- path |> Path.split() |> Enum.take(-3),
               true <- module_app != app,
               false <- module in excludes or {module, function, arity} in excludes do
            env_mfa =
              if env_function do
                {env_module, elem(env_function, 0), elem(env_function, 1)}
              else
                env_module
              end

            warning = {:undefined_app, module_app, module, function, arity}
            [{__MODULE__, warning, {env_file, env_line, env_mfa}} | acc]
          else
            _ -> acc
          end
        end,
        [],
        table
      )

    warnings
    |> Module.Checker.group_warnings()
    |> Module.Checker.emit_warnings()
  end

  def stop() do
    try do
      :ets.delete(warnings_table())
    rescue
      _ -> false
    end

    try do
      :ets.delete(@table)
    rescue
      _ -> false
    end

    :ok
  end

  def format_warning({:undefined_app, app, module, function, arity}) do
    """
    #{Exception.format_mfa(module, function, arity)} defined in application :#{app} \
    is used by the current application but the current application does not directly \
    depend on :#{app}. To fix this, you must do one of:

      1. If :#{app} is part of Erlang/Elixir, you must include it under \
    :extra_applications inside "def application" in your mix.exs

      2. If :#{app} is a dependency, make sure it is listed under "def deps" \
    in your mix.exs

      3. In case you don't want to add a requirement to :#{app}, you may \
    optionally skip this warning by adding [xref: [exclude: #{inspect(module)}] \
    to your "def project" in mix.exs
    """
  end

  ## Helpers

  def setup_warnings_table(config) do
    table = :ets.new(:app_tracer_warnings, [:public, :duplicate_bag, write_concurrency: true])
    excludes = List.wrap(config[:xref][:exclude])
    :ets.insert(@table, [{@warnings_key, table, Atom.to_string(config[:app]), excludes}])
  end

  defp warnings_table() do
    :ets.lookup_element(@table, @warnings_key, 2)
  end

  defp manifest(config) do
    Path.join(Mix.Project.manifest_path(config), @manifest)
  end

  defp read_manifest(manifest) do
    try do
      {:ok, table} = :ets.file2tab(String.to_charlist(manifest))
      table
    rescue
      _ -> nil
    end
  end

  defp build_manifest(config, manifest) do
    table = :ets.new(@table, [:public, :named_table, :set, read_concurrency: true])

    %{}
    |> store_apps(table, language_apps(config))
    |> store_apps(table, app_file_applications())
    |> store_deps(table, config)

    File.mkdir_p!(Path.dirname(manifest))
    :ok = :ets.tab2file(table, String.to_charlist(manifest), sync: true)
    table
  end

  defp language_apps(config) do
    case Keyword.get(config, :language, :elixir) do
      :elixir -> [:elixir, :ex_unit, :mix]
      :erlang -> [:stdlib, :kernel]
    end
  end

  defp app_file_applications() do
    project = Mix.Project.get!()

    if function_exported?(project, :application, 0) do
      properties = project.application()

      Keyword.get(properties, :applications, []) ++
        Keyword.get(properties, :extra_applications, []) ++
        Keyword.get(properties, :included_applications, [])
    else
      []
    end
  end

  defp store_deps(seen, table, config) do
    config
    |> Mix.Dep.compile_or_runtime_deps_mapping()
    |> Enum.reduce(seen, fn {app, _}, acc -> store_app(table, app, acc) end)
  end

  defp store_apps(seen, table, apps) do
    Enum.reduce(apps, seen, &store_app(table, &1, &2))
  end

  defp store_app(table, app, seen) do
    if Map.has_key?(seen, app) do
      seen
    else
      seen = Map.put(seen, app, true)
      result = Application.load(app)

      if result == :ok or result == {:error, {:already_loaded, app}} do
        modules = Application.spec(app, :modules)
        :ets.insert(table, Enum.map(modules, &{&1}))

        seen
        |> store_apps(table, Application.spec(app, :applications))
        |> store_apps(table, Application.spec(app, :included_applications))
      else
        seen
      end
    end
  end
end

defmodule Mix.Compilers.ApplicationTracer do
  @moduledoc false
  @manifest "compile.app_tracer"
  @table __MODULE__
  @warnings_key 0

  def init(opts) do
    config = Mix.Project.config()
    manifest = manifest(config)

    if Mix.Utils.stale?([Mix.Project.config_mtime()], [manifest]) do
      build_manifest(config, manifest)
    else
      read_manifest(manifest) || build_manifest(config, manifest)
    end

    setup_warnings_table(config)
    Keyword.update(opts, :tracers, [__MODULE__], &[__MODULE__ | &1])
  end

  # :erlang and other preloaded apps are not part of any module.
  # :code.which/1 will rule them out but, since :erlang is very
  # common, we rule it out upfront.
  def trace({_, _, :erlang, _, _}, _env) do
    :ok
  end

  def trace({type, meta, module, function, arity}, env)
      when type in [:remote_function, :remote_macro, :imported_function, :imported_macro] do
    # Unknown modules need to be looked up and filtered later
    unless :ets.member(@table, module) do
      :ets.insert(
        warnings_table(),
        {module, function, arity, env.module, env.function, env.file, meta[:line] || env.line}
      )
    end

    :ok
  end

  def trace(_, _) do
    :ok
  end

  def warnings(modules_entries) do
    [{_, table, app, excludes}] = :ets.lookup(@table, @warnings_key)

    for module_entry <- modules_entries do
      :ets.delete(table, elem(module_entry, 1))
    end

    warnings =
      :ets.foldl(
        fn {module, function, arity, env_module, env_function, env_file, env_line}, acc ->
          # If the module is either preloaded, it is always available.
          # If the module is non existing, the compiler will warn, so we don't.
          # If the module belongs to this application (from another compiler), we skip it.
          # If the module is excluded, we skip it too.
          with path when is_list(path) <- :code.which(module),
               [module_app, "ebin", _beam] <- path |> Path.split() |> Enum.take(-3),
               true <- module_app != app,
               false <- module in excludes or {module, function, arity} in excludes do
            env_mfa =
              if env_function do
                {env_module, elem(env_function, 0), elem(env_function, 1)}
              else
                env_module
              end

            warning = {:undefined_app, module_app, module, function, arity}
            [{__MODULE__, warning, {env_file, env_line, env_mfa}} | acc]
          else
            _ -> acc
          end
        end,
        [],
        table
      )

    warnings
    |> Module.Checker.group_warnings()
    |> Module.Checker.emit_warnings()
  end

  def stop() do
    try do
      :ets.delete(warnings_table())
    rescue
      _ -> false
    end

    try do
      :ets.delete(@table)
    rescue
      _ -> false
    end

    :ok
  end

  def format_warning({:undefined_app, app, module, function, arity}) do
    """
    #{Exception.format_mfa(module, function, arity)} defined in application :#{app} \
    is used by the current application but the current application does not directly \
    depend on :#{app}. To fix this, you must do one of:

      1. If :#{app} is part of Erlang/Elixir, you must include it under \
    :extra_applications inside "def application" in your mix.exs

      2. If :#{app} is a dependency, make sure it is listed under "def deps" \
    in your mix.exs

      3. In case you don't want to add a requirement to :#{app}, you may \
    optionally skip this warning by adding [xref: [exclude: #{inspect(module)}]
    to your "def project" in mix.exs
    """
  end

  ## Helpers

  def setup_warnings_table(config) do
    table = :ets.new(:app_tracer_warnings, [:public, :duplicate_bag, write_concurrency: true])
    excludes = List.wrap(config[:xref][:exclude])
    :ets.insert(@table, [{@warnings_key, table, Atom.to_string(config[:app]), excludes}])
  end

  defp warnings_table() do
    :ets.lookup_element(@table, @warnings_key, 2)
  end

  defp manifest(config) do
    Path.join(Mix.Project.manifest_path(config), @manifest)
  end

  defp read_manifest(manifest) do
    try do
      {:ok, table} = :ets.file2tab(String.to_charlist(manifest))
      table
    rescue
      _ -> nil
    end
  end

  defp build_manifest(config, manifest) do
    table = :ets.new(@table, [:public, :named_table, :set, read_concurrency: true])

    %{}
    |> store_apps(table, language_apps(config))
    |> store_apps(table, app_file_applications())
    |> store_deps(table, config)

    File.mkdir_p!(Path.dirname(manifest))
    :ok = :ets.tab2file(table, String.to_charlist(manifest), sync: true)
    table
  end

  defp language_apps(config) do
    case Keyword.get(config, :language, :elixir) do
      :elixir -> [:elixir, :ex_unit, :mix]
      :erlang -> [:stdlib, :kernel]
    end
  end

  defp app_file_applications() do
    project = Mix.Project.get!()

    if function_exported?(project, :application, 0) do
      properties = project.application()

      Keyword.get(properties, :applications, []) ++
        Keyword.get(properties, :extra_applications, []) ++
        Keyword.get(properties, :included_applications, [])
    else
      []
    end
  end

  defp store_deps(seen, table, config) do
    config
    |> Mix.Dep.compile_or_runtime_deps_mapping()
    |> Enum.reduce(seen, fn {app, _}, acc -> store_app(table, app, acc) end)
  end

  defp store_apps(seen, table, apps) do
    Enum.reduce(apps, seen, &store_app(table, &1, &2))
  end

  defp store_app(table, app, seen) do
    if Map.has_key?(seen, app) do
      seen
    else
      seen = Map.put(seen, app, true)
      result = Application.load(app)

      if result == :ok or result == {:error, {:already_loaded, app}} do
        modules = Application.spec(app, :modules)
        :ets.insert(table, Enum.map(modules, &{&1}))

        seen
        |> store_apps(table, Application.spec(app, :applications))
        |> store_apps(table, Application.spec(app, :included_applications))
      else
        seen
      end
    end
  end
end


Don't call pub sub directly

Don't call pub sub directly

defmodule BsWeb.SpectatorChannel do
  alias Bs.Game

  use BsWeb, :channel

  @type join_payload :: %{optional(binary) => binary}
  @spec join(binary, join_payload, Phoenix.Socket.t) :: {:ok, Phoenix.Socket} | {:error, any}
  def join("spectator:" <> game_id, payload, socket) do
    do_join(game_id, payload, socket)
  end

  defp do_join(game_id, payload, socket) do
    if authorized?(payload) do
      Game.subscribe(game_id)
      send(self(), :after_join)
      socket = assign(socket, :game_id, game_id)
      {:ok, socket}
    else
      {:error, %{reason: "unauthorized"}}
    end
  end

  ####################
  # Game State Event #
  ####################

  def handle_info({:tick, state}, socket) do
    broadcast(socket, "tick", %{content: render(state.world)})

    {:noreply, socket}
  end

  ##############
  # After Join #
  ##############

  def handle_info(:after_join, socket) do
    state = socket.assigns.game_id
    |> Game.find!
    |> Game.get_game_state

    push(socket, "tick", %{content: render(state.world)})

    {:noreply, socket}
  end

  ###################
  # Private Methods #
  ###################

  # Add authorization logic here as required.
  defp authorized?(_payload) do
    true
  end

  defp render(board) do
    Phoenix.View.render(
      BsWeb.BoardView,
      "show.json",
      board: board
    )
  end
end

defmodule BsWeb.SpectatorChannel do
  alias Bs.Game

  use BsWeb, :channel

  @type join_payload :: %{optional(binary) => binary}
  @spec join(binary, join_payload, Phoenix.Socket.t) :: {:ok, Phoenix.Socket} | {:error, any}
  def join("spectator:" <> game_id, payload, socket) do
    do_join(game_id, payload, socket)
  end

  defp do_join(game_id, payload, socket) do
    if authorized?(payload) do
      Game.PubSub.subscribe(game_id)
      send(self(), :after_join)
      socket = assign(socket, :game_id, game_id)
      {:ok, socket}
    else
      {:error, %{reason: "unauthorized"}}
    end
  end

  ####################
  # Game State Event #
  ####################

  def handle_info({:tick, state}, socket) do
    broadcast(socket, "tick", %{content: render(state.world)})

    {:noreply, socket}
  end

  ##############
  # After Join #
  ##############

  def handle_info(:after_join, socket) do
    state = socket.assigns.game_id
    |> Game.find!
    |> Game.get_game_state

    push(socket, "tick", %{content: render(state.world)})

    {:noreply, socket}
  end

  ###################
  # Private Methods #
  ###################

  # Add authorization logic here as required.
  defp authorized?(_payload) do
    true
  end

  defp render(board) do
    Phoenix.View.render(
      BsWeb.BoardView,
      "show.json",
      board: board
    )
  end
end


Hexdocs -> HexDocs (#4917)

Hexdocs -> HexDocs (#4917)

defmodule Mix.Tasks.New do
  use Mix.Task

  import Mix.Generator
  import Mix.Utils, only: [camelize: 1]

  @shortdoc "Creates a new Elixir project"

  @moduledoc """
  Creates a new Elixir project.
  It expects the path of the project as argument.

      mix new PATH [--sup] [--module MODULE] [--app APP] [--umbrella]

  A project at the given PATH  will be created. The
  application name and module name will be retrieved
  from the path, unless `--module` or `--app` is given.

  A `--sup` option can be given to generate an OTP application
  skeleton including a supervision tree. Normally an app is
  generated without a supervisor and without the app callback.

  An `--umbrella` option can be given to generate an
  umbrella project.

  An `--app` option can be given in order to
  name the OTP application for the project.

  A `--module` option can be given in order
  to name the modules in the generated code skeleton.

  ## Examples

      mix new hello_world

  Is equivalent to:

      mix new hello_world --module HelloWorld

  To generate an app with supervisor and application callback:

      mix new hello_world --sup

  """

  @switches [sup: :boolean, umbrella: :boolean, app: :string, module: :string]

  @spec run(OptionParser.argv) :: :ok
  def run(argv) do
    {opts, argv} = OptionParser.parse!(argv, strict: @switches)

    case argv do
      [] ->
        Mix.raise "Expected PATH to be given, please use \"mix new PATH\""
      [path | _] ->
        app = opts[:app] || Path.basename(Path.expand(path))
        check_application_name!(app, !!opts[:app])
        mod = opts[:module] || camelize(app)
        check_mod_name_validity!(mod)
        check_mod_name_availability!(mod)
        File.mkdir_p!(path)

        File.cd! path, fn ->
          if opts[:umbrella] do
            generate_umbrella(app, mod, path, opts)
          else
            generate(app, mod, path, opts)
          end
        end
    end
  end

  defp generate(app, mod, path, opts) do
    assigns = [app: app, mod: mod, otp_app: otp_app(mod, !!opts[:sup]),
               version: get_version(System.version)]

    create_file "README.md",  readme_template(assigns)
    create_file ".gitignore", gitignore_text()

    if in_umbrella?() do
      create_file "mix.exs", mixfile_apps_template(assigns)
    else
      create_file "mix.exs", mixfile_template(assigns)
    end

    create_directory "config"
    create_file "config/config.exs", config_template(assigns)

    create_directory "lib"

    if opts[:sup] do
      create_file "lib/#{app}.ex", lib_sup_template(assigns)
    else
      create_file "lib/#{app}.ex", lib_template(assigns)
    end

    create_directory "test"
    create_file "test/test_helper.exs", test_helper_template(assigns)
    create_file "test/#{app}_test.exs", test_template(assigns)

    """

    Your Mix project was created successfully.
    You can use "mix" to compile it, test it, and more:

        cd #{path}
        mix test

    Run "mix help" for more commands.
    """
    |> String.trim_trailing
    |> Mix.shell.info
  end

  defp otp_app(_mod, false) do
    "    [applications: [:logger]]"
  end

  defp otp_app(mod, true) do
    "    [applications: [:logger],\n     mod: {#{mod}, []}]"
  end

  defp generate_umbrella(_app, mod, path, _opts) do
    assigns = [app: nil, mod: mod]

    create_file ".gitignore", gitignore_text()
    create_file "README.md", readme_template(assigns)
    create_file "mix.exs", mixfile_umbrella_template(assigns)

    create_directory "apps"

    create_directory "config"
    create_file "config/config.exs", config_umbrella_template(assigns)

    """

    Your umbrella project was created successfully.
    Inside your project, you will find an apps/ directory
    where you can create and host many apps:

        cd #{path}
        cd apps
        mix new my_app

    Commands like "mix compile" and "mix test" when executed
    in the umbrella project root will automatically run
    for each application in the apps/ directory.
    """
    |> String.trim_trailing
    |> Mix.shell.info
  end

  defp check_application_name!(name, from_app_flag) do
    unless name =~ ~r/^[a-z][\w_]*$/ do
      Mix.raise "Application name must start with a letter and have only lowercase " <>
                "letters, numbers and underscore, got: #{inspect name}" <>
                (if !from_app_flag do
                  ". The application name is inferred from the path, if you'd like to " <>
                  "explicitly name the application then use the \"--app APP\" option."
                else
                  ""
                end)
    end
  end

  defp check_mod_name_validity!(name) do
    unless name =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/ do
      Mix.raise "Module name must be a valid Elixir alias (for example: Foo.Bar), got: #{inspect name}"
    end
  end

  defp check_mod_name_availability!(name) do
    name = Module.concat(Elixir, name)
    if Code.ensure_loaded?(name) do
      Mix.raise "Module name #{inspect name} is already taken, please choose another name"
    end
  end

  defp get_version(version) do
    {:ok, version} = Version.parse(version)
    "#{version.major}.#{version.minor}" <>
      case version.pre do
        [h | _] -> "-#{h}"
        []      -> ""
      end
  end

  defp in_umbrella? do
    apps = Path.dirname(File.cwd!)

    try do
      Mix.Project.in_project(:umbrella_check, "../..", fn _ ->
        path = Mix.Project.config[:apps_path]
        path && Path.expand(path) == apps
      end)
    catch
      _, _ -> false
    end
  end

  embed_template :readme, """
  # <%= @mod %>

  **TODO: Add description**
  <%= if @app do %>
  ## Installation

  If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:

    1. Add `<%= @app %>` to your list of dependencies in `mix.exs`:

      ```elixir
      def deps do
        [{:<%= @app %>, "~> 0.1.0"}]
      end
      ```

    2. Ensure `<%= @app %>` is started before your application:

      ```elixir
      def application do
        [applications: [:<%= @app %>]]
      end
      ```

  If [published on HexDocs](https://hex.pm/docs/tasks#hex_docs), the docs can
  be found at [https://hexdocs.pm/<%= @app %>](https://hexdocs.pm/<%= @app %>)
  <% end %>
  """

  embed_text :gitignore, """
  # The directory Mix will write compiled artifacts to.
  /_build

  # If you run "mix test --cover", coverage assets end up here.
  /cover

  # The directory Mix downloads your dependencies sources to.
  /deps

  # Where 3rd-party dependencies like ExDoc output generated docs.
  /doc

  # If the VM crashes, it generates a dump, let's ignore it too.
  erl_crash.dump

  # Also ignore archive artifacts (built via "mix archive.build").
  *.ez
  """

  embed_template :mixfile, """
  defmodule <%= @mod %>.Mixfile do
    use Mix.Project

    def project do
      [app: :<%= @app %>,
       version: "0.1.0",
       elixir: "~> <%= @version %>",
       build_embedded: Mix.env == :prod,
       start_permanent: Mix.env == :prod,
       deps: deps()]
    end

    # Configuration for the OTP application
    #
    # Type "mix help compile.app" for more information
    def application do
  <%= @otp_app %>
    end

    # Dependencies can be Hex packages:
    #
    #   {:mydep, "~> 0.3.0"}
    #
    # Or git/path repositories:
    #
    #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
    #
    # Type "mix help deps" for more examples and options
    defp deps do
      []
    end
  end
  """

  embed_template :mixfile_apps, """
  defmodule <%= @mod %>.Mixfile do
    use Mix.Project

    def project do
      [app: :<%= @app %>,
       version: "0.1.0",
       build_path: "../../_build",
       config_path: "../../config/config.exs",
       deps_path: "../../deps",
       lockfile: "../../mix.lock",
       elixir: "~> <%= @version %>",
       build_embedded: Mix.env == :prod,
       start_permanent: Mix.env == :prod,
       deps: deps()]
    end

    # Configuration for the OTP application
    #
    # Type "mix help compile.app" for more information
    def application do
  <%= @otp_app %>
    end

    # Dependencies can be Hex packages:
    #
    #   {:mydep, "~> 0.3.0"}
    #
    # Or git/path repositories:
    #
    #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
    #
    # To depend on another app inside the umbrella:
    #
    #   {:myapp, in_umbrella: true}
    #
    # Type "mix help deps" for more examples and options
    defp deps do
      []
    end
  end
  """

  embed_template :mixfile_umbrella, """
  defmodule <%= @mod %>.Mixfile do
    use Mix.Project

    def project do
      [apps_path: "apps",
       build_embedded: Mix.env == :prod,
       start_permanent: Mix.env == :prod,
       deps: deps()]
    end

    # Dependencies can be Hex packages:
    #
    #   {:mydep, "~> 0.3.0"}
    #
    # Or git/path repositories:
    #
    #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
    #
    # Type "mix help deps" for more examples and options.
    #
    # Dependencies listed here are available only for this project
    # and cannot be accessed from applications inside the apps folder
    defp deps do
      []
    end
  end
  """

  embed_template :config, ~S"""
  # This file is responsible for configuring your application
  # and its dependencies with the aid of the Mix.Config module.
  use Mix.Config

  # This configuration is loaded before any dependency and is restricted
  # to this project. If another project depends on this project, this
  # file won't be loaded nor affect the parent project. For this reason,
  # if you want to provide default values for your application for
  # 3rd-party users, it should be done in your "mix.exs" file.

  # You can configure for your application as:
  #
  #     config :<%= @app %>, key: :value
  #
  # And access this configuration in your application as:
  #
  #     Application.get_env(:<%= @app %>, :key)
  #
  # Or configure a 3rd-party app:
  #
  #     config :logger, level: :info
  #

  # It is also possible to import configuration files, relative to this
  # directory. For example, you can emulate configuration per environment
  # by uncommenting the line below and defining dev.exs, test.exs and such.
  # Configuration from the imported file will override the ones defined
  # here (which is why it is important to import them last).
  #
  #     import_config "#{Mix.env}.exs"
  """

  embed_template :config_umbrella, ~S"""
  # This file is responsible for configuring your application
  # and its dependencies with the aid of the Mix.Config module.
  use Mix.Config

  # By default, the umbrella project as well as each child
  # application will require this configuration file, ensuring
  # they all use the same configuration. While one could
  # configure all applications here, we prefer to delegate
  # back to each application for organization purposes.
  import_config "../apps/*/config/config.exs"

  # Sample configuration (overrides the imported configuration above):
  #
  #     config :logger, :console,
  #       level: :info,
  #       format: "$date $time [$level] $metadata$message\n",
  #       metadata: [:user_id]
  """

  embed_template :lib, """
  defmodule <%= @mod %> do
  end
  """

  embed_template :lib_sup, """
  defmodule <%= @mod %> do
    use Application

    # See http://elixir-lang.org/docs/stable/elixir/Application.html
    # for more information on OTP Applications
    def start(_type, _args) do
      import Supervisor.Spec, warn: false

      # Define workers and child supervisors to be supervised
      children = [
        # Starts a worker by calling: <%= @mod %>.Worker.start_link(arg1, arg2, arg3)
        # worker(<%= @mod %>.Worker, [arg1, arg2, arg3]),
      ]

      # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
      # for other strategies and supported options
      opts = [strategy: :one_for_one, name: <%= @mod %>.Supervisor]
      Supervisor.start_link(children, opts)
    end
  end
  """

  embed_template :test, """
  defmodule <%= @mod %>Test do
    use ExUnit.Case
    doctest <%= @mod %>

    test "the truth" do
      assert 1 + 1 == 2
    end
  end
  """

  embed_template :test_helper, """
  ExUnit.start()
  """
end

defmodule Mix.Tasks.New do
  use Mix.Task

  import Mix.Generator
  import Mix.Utils, only: [camelize: 1]

  @shortdoc "Creates a new Elixir project"

  @moduledoc """
  Creates a new Elixir project.
  It expects the path of the project as argument.

      mix new PATH [--sup] [--module MODULE] [--app APP] [--umbrella]

  A project at the given PATH  will be created. The
  application name and module name will be retrieved
  from the path, unless `--module` or `--app` is given.

  A `--sup` option can be given to generate an OTP application
  skeleton including a supervision tree. Normally an app is
  generated without a supervisor and without the app callback.

  An `--umbrella` option can be given to generate an
  umbrella project.

  An `--app` option can be given in order to
  name the OTP application for the project.

  A `--module` option can be given in order
  to name the modules in the generated code skeleton.

  ## Examples

      mix new hello_world

  Is equivalent to:

      mix new hello_world --module HelloWorld

  To generate an app with supervisor and application callback:

      mix new hello_world --sup

  """

  @switches [sup: :boolean, umbrella: :boolean, app: :string, module: :string]

  @spec run(OptionParser.argv) :: :ok
  def run(argv) do
    {opts, argv} = OptionParser.parse!(argv, strict: @switches)

    case argv do
      [] ->
        Mix.raise "Expected PATH to be given, please use \"mix new PATH\""
      [path | _] ->
        app = opts[:app] || Path.basename(Path.expand(path))
        check_application_name!(app, !!opts[:app])
        mod = opts[:module] || camelize(app)
        check_mod_name_validity!(mod)
        check_mod_name_availability!(mod)
        File.mkdir_p!(path)

        File.cd! path, fn ->
          if opts[:umbrella] do
            generate_umbrella(app, mod, path, opts)
          else
            generate(app, mod, path, opts)
          end
        end
    end
  end

  defp generate(app, mod, path, opts) do
    assigns = [app: app, mod: mod, otp_app: otp_app(mod, !!opts[:sup]),
               version: get_version(System.version)]

    create_file "README.md",  readme_template(assigns)
    create_file ".gitignore", gitignore_text()

    if in_umbrella?() do
      create_file "mix.exs", mixfile_apps_template(assigns)
    else
      create_file "mix.exs", mixfile_template(assigns)
    end

    create_directory "config"
    create_file "config/config.exs", config_template(assigns)

    create_directory "lib"

    if opts[:sup] do
      create_file "lib/#{app}.ex", lib_sup_template(assigns)
    else
      create_file "lib/#{app}.ex", lib_template(assigns)
    end

    create_directory "test"
    create_file "test/test_helper.exs", test_helper_template(assigns)
    create_file "test/#{app}_test.exs", test_template(assigns)

    """

    Your Mix project was created successfully.
    You can use "mix" to compile it, test it, and more:

        cd #{path}
        mix test

    Run "mix help" for more commands.
    """
    |> String.trim_trailing
    |> Mix.shell.info
  end

  defp otp_app(_mod, false) do
    "    [applications: [:logger]]"
  end

  defp otp_app(mod, true) do
    "    [applications: [:logger],\n     mod: {#{mod}, []}]"
  end

  defp generate_umbrella(_app, mod, path, _opts) do
    assigns = [app: nil, mod: mod]

    create_file ".gitignore", gitignore_text()
    create_file "README.md", readme_template(assigns)
    create_file "mix.exs", mixfile_umbrella_template(assigns)

    create_directory "apps"

    create_directory "config"
    create_file "config/config.exs", config_umbrella_template(assigns)

    """

    Your umbrella project was created successfully.
    Inside your project, you will find an apps/ directory
    where you can create and host many apps:

        cd #{path}
        cd apps
        mix new my_app

    Commands like "mix compile" and "mix test" when executed
    in the umbrella project root will automatically run
    for each application in the apps/ directory.
    """
    |> String.trim_trailing
    |> Mix.shell.info
  end

  defp check_application_name!(name, from_app_flag) do
    unless name =~ ~r/^[a-z][\w_]*$/ do
      Mix.raise "Application name must start with a letter and have only lowercase " <>
                "letters, numbers and underscore, got: #{inspect name}" <>
                (if !from_app_flag do
                  ". The application name is inferred from the path, if you'd like to " <>
                  "explicitly name the application then use the \"--app APP\" option."
                else
                  ""
                end)
    end
  end

  defp check_mod_name_validity!(name) do
    unless name =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/ do
      Mix.raise "Module name must be a valid Elixir alias (for example: Foo.Bar), got: #{inspect name}"
    end
  end

  defp check_mod_name_availability!(name) do
    name = Module.concat(Elixir, name)
    if Code.ensure_loaded?(name) do
      Mix.raise "Module name #{inspect name} is already taken, please choose another name"
    end
  end

  defp get_version(version) do
    {:ok, version} = Version.parse(version)
    "#{version.major}.#{version.minor}" <>
      case version.pre do
        [h | _] -> "-#{h}"
        []      -> ""
      end
  end

  defp in_umbrella? do
    apps = Path.dirname(File.cwd!)

    try do
      Mix.Project.in_project(:umbrella_check, "../..", fn _ ->
        path = Mix.Project.config[:apps_path]
        path && Path.expand(path) == apps
      end)
    catch
      _, _ -> false
    end
  end

  embed_template :readme, """
  # <%= @mod %>

  **TODO: Add description**
  <%= if @app do %>
  ## Installation

  If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:

    1. Add `<%= @app %>` to your list of dependencies in `mix.exs`:

      ```elixir
      def deps do
        [{:<%= @app %>, "~> 0.1.0"}]
      end
      ```

    2. Ensure `<%= @app %>` is started before your application:

      ```elixir
      def application do
        [applications: [:<%= @app %>]]
      end
      ```

  If [published on Hexdocs](https://hex.pm/docs/tasks#hex_docs), the docs can
  be found at [https://hexdocs.pm/<%= @app %>](https://hexdocs.pm/<%= @app %>)
  <% end %>
  """

  embed_text :gitignore, """
  # The directory Mix will write compiled artifacts to.
  /_build

  # If you run "mix test --cover", coverage assets end up here.
  /cover

  # The directory Mix downloads your dependencies sources to.
  /deps

  # Where 3rd-party dependencies like ExDoc output generated docs.
  /doc

  # If the VM crashes, it generates a dump, let's ignore it too.
  erl_crash.dump

  # Also ignore archive artifacts (built via "mix archive.build").
  *.ez
  """

  embed_template :mixfile, """
  defmodule <%= @mod %>.Mixfile do
    use Mix.Project

    def project do
      [app: :<%= @app %>,
       version: "0.1.0",
       elixir: "~> <%= @version %>",
       build_embedded: Mix.env == :prod,
       start_permanent: Mix.env == :prod,
       deps: deps()]
    end

    # Configuration for the OTP application
    #
    # Type "mix help compile.app" for more information
    def application do
  <%= @otp_app %>
    end

    # Dependencies can be Hex packages:
    #
    #   {:mydep, "~> 0.3.0"}
    #
    # Or git/path repositories:
    #
    #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
    #
    # Type "mix help deps" for more examples and options
    defp deps do
      []
    end
  end
  """

  embed_template :mixfile_apps, """
  defmodule <%= @mod %>.Mixfile do
    use Mix.Project

    def project do
      [app: :<%= @app %>,
       version: "0.1.0",
       build_path: "../../_build",
       config_path: "../../config/config.exs",
       deps_path: "../../deps",
       lockfile: "../../mix.lock",
       elixir: "~> <%= @version %>",
       build_embedded: Mix.env == :prod,
       start_permanent: Mix.env == :prod,
       deps: deps()]
    end

    # Configuration for the OTP application
    #
    # Type "mix help compile.app" for more information
    def application do
  <%= @otp_app %>
    end

    # Dependencies can be Hex packages:
    #
    #   {:mydep, "~> 0.3.0"}
    #
    # Or git/path repositories:
    #
    #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
    #
    # To depend on another app inside the umbrella:
    #
    #   {:myapp, in_umbrella: true}
    #
    # Type "mix help deps" for more examples and options
    defp deps do
      []
    end
  end
  """

  embed_template :mixfile_umbrella, """
  defmodule <%= @mod %>.Mixfile do
    use Mix.Project

    def project do
      [apps_path: "apps",
       build_embedded: Mix.env == :prod,
       start_permanent: Mix.env == :prod,
       deps: deps()]
    end

    # Dependencies can be Hex packages:
    #
    #   {:mydep, "~> 0.3.0"}
    #
    # Or git/path repositories:
    #
    #   {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
    #
    # Type "mix help deps" for more examples and options.
    #
    # Dependencies listed here are available only for this project
    # and cannot be accessed from applications inside the apps folder
    defp deps do
      []
    end
  end
  """

  embed_template :config, ~S"""
  # This file is responsible for configuring your application
  # and its dependencies with the aid of the Mix.Config module.
  use Mix.Config

  # This configuration is loaded before any dependency and is restricted
  # to this project. If another project depends on this project, this
  # file won't be loaded nor affect the parent project. For this reason,
  # if you want to provide default values for your application for
  # 3rd-party users, it should be done in your "mix.exs" file.

  # You can configure for your application as:
  #
  #     config :<%= @app %>, key: :value
  #
  # And access this configuration in your application as:
  #
  #     Application.get_env(:<%= @app %>, :key)
  #
  # Or configure a 3rd-party app:
  #
  #     config :logger, level: :info
  #

  # It is also possible to import configuration files, relative to this
  # directory. For example, you can emulate configuration per environment
  # by uncommenting the line below and defining dev.exs, test.exs and such.
  # Configuration from the imported file will override the ones defined
  # here (which is why it is important to import them last).
  #
  #     import_config "#{Mix.env}.exs"
  """

  embed_template :config_umbrella, ~S"""
  # This file is responsible for configuring your application
  # and its dependencies with the aid of the Mix.Config module.
  use Mix.Config

  # By default, the umbrella project as well as each child
  # application will require this configuration file, ensuring
  # they all use the same configuration. While one could
  # configure all applications here, we prefer to delegate
  # back to each application for organization purposes.
  import_config "../apps/*/config/config.exs"

  # Sample configuration (overrides the imported configuration above):
  #
  #     config :logger, :console,
  #       level: :info,
  #       format: "$date $time [$level] $metadata$message\n",
  #       metadata: [:user_id]
  """

  embed_template :lib, """
  defmodule <%= @mod %> do
  end
  """

  embed_template :lib_sup, """
  defmodule <%= @mod %> do
    use Application

    # See http://elixir-lang.org/docs/stable/elixir/Application.html
    # for more information on OTP Applications
    def start(_type, _args) do
      import Supervisor.Spec, warn: false

      # Define workers and child supervisors to be supervised
      children = [
        # Starts a worker by calling: <%= @mod %>.Worker.start_link(arg1, arg2, arg3)
        # worker(<%= @mod %>.Worker, [arg1, arg2, arg3]),
      ]

      # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
      # for other strategies and supported options
      opts = [strategy: :one_for_one, name: <%= @mod %>.Supervisor]
      Supervisor.start_link(children, opts)
    end
  end
  """

  embed_template :test, """
  defmodule <%= @mod %>Test do
    use ExUnit.Case
    doctest <%= @mod %>

    test "the truth" do
      assert 1 + 1 == 2
    end
  end
  """

  embed_template :test_helper, """
  ExUnit.start()
  """
end


expect correct return tuple from mnesia

expect correct return tuple from mnesia

defmodule Yodado.FeaturePersistence.Mnesia do

  @table_record Yodado.Feature.Feature

  @all_features_matcher Yodado.Feature.Feature.__record__(:fields) |>
    Enum.map(fn({key, _default}) -> {key, :_} end) |>
    Yodado.Feature.Feature.new

  def all() do
    features = :mnesia.dirty_match_object(@all_features_matcher)
    {:ok, features}
  end

  def find_feature({:key, feature_id}) do
    features = :mnesia.dirty_read(@table_record, feature_id)
    case features do
      [feature] ->
        {:ok, feature}
      [] ->
        :not_found
    end
  end

  def find_feature(feature_id) do
    find_feature({:key, key(feature_id)})
  end

  def save_feature(feature) do
    {:atomic, :ok} = :mnesia.transaction(fn() ->
      :mnesia.write(feature)
    end)
    {:ok, true}
  end

  def delete_feature({:key, feature_id}) do
    :ok = :mnesia.transaction(fn() ->
      :mnesia.delete({@table_record, feature_id})
    end)
    {:ok, true}
  end

  def delete_feature(feature_id) do
    delete_feature({:key, feature_id})
  end

  #TODO this must die. only used by old JS client
  def sync(features) do
    {:atomic, :ok} =  :mnesia.transaction(fn() ->
      #TODO slow, and hacky...
      keys = :mnesia.all_keys(@table_record)
      Enum.each(keys, fn(k) ->
        :ok = :mnesia.delete({@table_record, k})
      end)

      Enum.each(features, fn(f) ->
        :ok = :mnesia.write(f)
      end)
    end)
    {:ok, true}
  end

  defp key(feature) when is_record(feature, @table_record) do
    feature.title
  end

  defp key(feature_id) do
    feature_id
  end
end

defmodule Yodado.FeaturePersistence.Mnesia do

  @table_record Yodado.Feature.Feature

  @all_features_matcher Yodado.Feature.Feature.__record__(:fields) |>
    Enum.map(fn({key, _default}) -> {key, :_} end) |>
    Yodado.Feature.Feature.new

  def all() do
    features = :mnesia.dirty_match_object(@all_features_matcher)
    {:ok, features}
  end

  def find_feature({:key, feature_id}) do
    features = :mnesia.dirty_read(@table_record, feature_id)
    case features do
      [feature] ->
        {:ok, feature}
      [] ->
        :not_found
    end
  end

  def find_feature(feature_id) do
    find_feature({:key, key(feature_id)})
  end

  def save_feature(feature) do
    :ok = :mnesia.transaction(fn() ->
      :mnesia.write(feature)
    end)
    {:ok, true}
  end

  def delete_feature({:key, feature_id}) do
    :ok = :mnesia.transaction(fn() ->
      :mnesia.delete({@table_record, feature_id})
    end)
    {:ok, true}
  end

  def delete_feature(feature_id) do
    delete_feature({:key, feature_id})
  end

  #TODO this must die. only used by old JS client
  def sync(features) do
    {:atomic, :ok} =  :mnesia.transaction(fn() ->
      #TODO slow, and hacky...
      keys = :mnesia.all_keys(@table_record)
      Enum.each(keys, fn(k) ->
        :ok = :mnesia.delete({@table_record, k})
      end)

      Enum.each(features, fn(f) ->
        :ok = :mnesia.write(f)
      end)
    end)
    {:ok, true}
  end

  defp key(feature) when is_record(feature, @table_record) do
    feature.title
  end

  defp key(feature_id) do
    feature_id
  end
end


More tests for RuleProcessor

More tests for RuleProcessor

defmodule ExMoney.RuleProcessorTest do
  use ExUnit.Case

  alias ExMoney.{RuleProcessor, Repo, Transaction}

  import ExMoney.Factory

  setup do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(ExMoney.Repo)
    Ecto.Adapters.SQL.Sandbox.mode(ExMoney.Repo, {:shared, self()})

    :ok
  end

  describe "process callback" do
    setup do
      account = insert(:account)
      transaction = insert(:transaction,
        description: "bar foo baz",
        account: account,
        saltedge_account_id: account.saltedge_account_id
      )

      {:ok, transaction: transaction}
    end

    test "applies rules according to priority", %{transaction: transaction} do
      category_1 = insert(:category, name: "groceries")
      category_2 = insert(:category, name: "cafe")

      insert(:rule,
        account: transaction.account,
        type: "assign_category",
        pattern: "foo",
        priority: 1,
        target_id: category_1.id
      )

      insert(:rule,
        account: transaction.account,
        type: "assign_category",
        pattern: "foo",
        priority: 2,
        target_id: category_2.id
      )

      RuleProcessor.handle_cast({:process, transaction.id}, %{})

      updated_transaction = Repo.get(Transaction, transaction.id)

      assert updated_transaction.category_id == category_2.id
      assert updated_transaction.rule_applied
    end

    test "does not apply rule for different account", %{transaction: transaction} do
      category = insert(:category, name: "groceries")

      insert(:rule,
        type: "assign_category",
        pattern: "foo",
        priority: 1,
        target_id: category.id
      )

      RuleProcessor.handle_cast({:process, transaction.id}, %{})

      updated_transaction = Repo.get(Transaction, transaction.id)

      refute updated_transaction.category_id == category.id
      refute updated_transaction.rule_applied
    end

    test "does not apply rule when pattern does not match", %{transaction: transaction} do
      category = insert(:category, name: "groceries")

      insert(:rule,
        account: transaction.account,
        type: "assign_category",
        pattern: "something else",
        priority: 1,
        target_id: category.id
      )

      RuleProcessor.handle_cast({:process, transaction.id}, %{})

      updated_transaction = Repo.get(Transaction, transaction.id)

      refute updated_transaction.category_id == category.id
      refute updated_transaction.rule_applied
    end

    test "withdraw to cash", %{transaction: transaction} do
      cash_account = insert(:account)
      transfer_transaction = insert(:transaction,
        description: "bar foo baz",
        account: transaction.account,
        saltedge_account_id: transaction.account.saltedge_account_id,
        amount: Decimal.new(-10)
      )
      category = insert(:category, name: "withdraw")

      insert(:rule,
        account: transaction.account,
        type: "withdraw_to_cash",
        pattern: "foo",
        priority: 1,
        target_id: cash_account.id
      )

      RuleProcessor.handle_cast({:process, transfer_transaction.id}, %{})

      updated_transaction = Repo.get(Transaction, transfer_transaction.id)
      cash_transaction = Repo.get_by(Transaction, category_id: category.id)

      assert updated_transaction.rule_applied
      assert cash_transaction.account_id == cash_account.id
      assert cash_transaction.amount == Decimal.mult(updated_transaction.amount, Decimal.new(-1))
    end
  end

  describe "process_all callback" do
    setup do
      rule = insert(:rule, pattern: "foo", type: "assign_category", target_id: insert(:category).id)

      {:ok, rule: rule}
    end

    test "applies rule for all matched transactions", %{rule: rule} do
      tr_1 = insert(:transaction,
        description: "qwe foo something",
        account: rule.account,
        saltedge_account_id: rule.account.saltedge_account_id
      )
      tr_2 = insert(:transaction,
        transaction_info: build(:transaction_info, payee: "foo"),
        account: rule.account,
        saltedge_account_id: rule.account.saltedge_account_id
      )
      tr_3 = insert(:transaction,
        description: "baz",
        account: rule.account,
        saltedge_account_id: rule.account.saltedge_account_id
      )
      tr_4 = insert(:transaction,
        description: "foo",
        rule_applied: true,
        account: rule.account,
        saltedge_account_id: rule.account.saltedge_account_id
      )

      RuleProcessor.handle_cast({:process_all, rule.id}, %{})

      :timer.sleep(500)

      tr_1 = Repo.get(Transaction, tr_1.id)
      tr_2 = Repo.get(Transaction, tr_2.id)
      tr_3 = Repo.get(Transaction, tr_3.id)
      tr_4 = Repo.get(Transaction, tr_4.id)

      assert tr_1.rule_applied
      assert tr_1.category_id == rule.target_id

      assert tr_2.rule_applied
      assert tr_2.category_id == rule.target_id

      refute tr_3.rule_applied
      refute tr_3.category_id == rule.target_id

      assert tr_4.rule_applied
      refute tr_4.category_id == rule.target_id
    end
  end
end

defmodule ExMoney.RuleProcessorTest do
  use ExUnit.Case

  alias ExMoney.{RuleProcessor, Repo, Transaction}

  import ExMoney.Factory

  setup do
    :ok = Ecto.Adapters.SQL.Sandbox.checkout(ExMoney.Repo)
    Ecto.Adapters.SQL.Sandbox.mode(ExMoney.Repo, {:shared, self()})

    :ok
  end

  describe "process callback" do
    setup do
      account = insert(:account)
      transaction = insert(:transaction,
        description: "bar foo baz",
        account: account,
        saltedge_account_id: account.saltedge_account_id
      )

      {:ok, transaction: transaction}
    end

    test "applies rules according to priority", %{transaction: transaction} do
      category_1 = insert(:category, name: "groceries")
      category_2 = insert(:category, name: "cafe")

      insert(:rule,
        account: transaction.account,
        type: "assign_category",
        pattern: "foo",
        priority: 1,
        target_id: category_1.id
      )

      insert(:rule,
        account: transaction.account,
        type: "assign_category",
        pattern: "foo",
        priority: 2,
        target_id: category_2.id
      )

      RuleProcessor.handle_cast({:process, transaction.id}, %{})

      updated_transaction = Repo.get(Transaction, transaction.id)

      assert updated_transaction.category_id == category_2.id
      assert updated_transaction.rule_applied
    end

    test "does not apply rule for different account", %{transaction: transaction} do
      category = insert(:category, name: "groceries")

      insert(:rule,
        type: "assign_category",
        pattern: "foo",
        priority: 1,
        target_id: category.id
      )

      RuleProcessor.handle_cast({:process, transaction.id}, %{})

      updated_transaction = Repo.get(Transaction, transaction.id)

      refute updated_transaction.category_id == category.id
      refute updated_transaction.rule_applied
    end

    test "does not apply rule when pattern does not match", %{transaction: transaction} do
      category = insert(:category, name: "groceries")

      insert(:rule,
        account: transaction.account,
        type: "assign_category",
        pattern: "something else",
        priority: 1,
        target_id: category.id
      )

      RuleProcessor.handle_cast({:process, transaction.id}, %{})

      updated_transaction = Repo.get(Transaction, transaction.id)

      refute updated_transaction.category_id == category.id
      refute updated_transaction.rule_applied
    end

    test "withdraw to cash", %{transaction: transaction} do
      cash_account = insert(:account)
      transfer_transaction = insert(:transaction,
        description: "bar foo baz",
        account: transaction.account,
        saltedge_account_id: transaction.account.saltedge_account_id,
        amount: Decimal.new(-10)
      )
      category = insert(:category, name: "withdraw")

      insert(:rule,
        account: transaction.account,
        type: "withdraw_to_cash",
        pattern: "foo",
        priority: 1,
        target_id: cash_account.id
      )

      RuleProcessor.handle_cast({:process, transfer_transaction.id}, %{})

      updated_transaction = Repo.get(Transaction, transfer_transaction.id)
      cash_transaction = Repo.get_by(Transaction, category_id: category.id)

      assert updated_transaction.rule_applied
      assert cash_transaction.account_id == cash_account.id
      assert cash_transaction.amount == Decimal.mult(updated_transaction.amount, Decimal.new(-1))
    end
  end
end


Remove reference to Plug.Conn struct in Phoenix.EventHandler

Instead of requiring a Plug.Conn, this patch changes the match
in Phoenix.EventHandler.handle_event/4 to accept any map with a :conn
that has a :private key.

    %{conn: %{private: %{appsignal_transaction: transaction}}},

This removes the hard dependency on Plug, allowing this module to be
compiled in applications that don't use Phoenix or Plug without having
to add a hard dependency on the latter.

defmodule Appsignal.Phoenix.EventHandler do
  import Appsignal.Utils

  @transaction Application.get_env(:appsignal, :appsignal_transaction, Appsignal.Transaction)

  @spec attach() :: :ok | {:error, :already_exists}
  def attach do
    :application.ensure_all_started(:telemetry)

    :telemetry.attach_many(
      "appsignal_phoenix_event_handler",
      [
        [:phoenix, :router_dispatch, :start],
        [:phoenix, :endpoint, :start],
        [:phoenix, :endpoint, :stop]
      ],
      &Appsignal.Phoenix.EventHandler.handle_event/4,
      nil
    )
  end

  @spec handle_event(list(atom()), map(), map(), any()) :: Appsignal.Transaction.t() | nil
  def handle_event(
        [:phoenix, :router_dispatch, :start],
        _measurements,
        %{plug: controller, plug_opts: action},
        _config
      ) do
    @transaction.set_action("#{module_name(controller)}##{action}")
  end

  def handle_event(
        [:phoenix, :endpoint, :start],
        _measurements,
        %{conn: %{private: %{appsignal_transaction: transaction}}},
        _config
      ) do
    @transaction.start_event(transaction)
  end

  def handle_event([:phoenix, :endpoint, :start], _measurements, _metadata, _config) do
    @transaction.start_event()
  end

  def handle_event(
        [:phoenix, :endpoint, :stop],
        _measurements,
        %{conn: %{private: %{appsignal_transaction: transaction}}},
        _config
      ) do
    @transaction.finish_event(
      transaction,
      "call.phoenix_endpoint",
      "call.phoenix_endpoint",
      %{},
      0
    )
  end

  def handle_event([:phoenix, :endpoint, :stop], _measurements, _metadata, _config) do
    @transaction.finish_event("call.phoenix_endpoint", "call.phoenix_endpoint", %{}, 0)
  end
end

defmodule Appsignal.Phoenix.EventHandler do
  import Appsignal.Utils

  @transaction Application.get_env(:appsignal, :appsignal_transaction, Appsignal.Transaction)

  @spec attach() :: :ok | {:error, :already_exists}
  def attach do
    :application.ensure_all_started(:telemetry)

    :telemetry.attach_many(
      "appsignal_phoenix_event_handler",
      [
        [:phoenix, :router_dispatch, :start],
        [:phoenix, :endpoint, :start],
        [:phoenix, :endpoint, :stop]
      ],
      &Appsignal.Phoenix.EventHandler.handle_event/4,
      nil
    )
  end

  @spec handle_event(list(atom()), map(), map(), any()) :: Appsignal.Transaction.t() | nil
  def handle_event(
        [:phoenix, :router_dispatch, :start],
        _measurements,
        %{plug: controller, plug_opts: action},
        _config
      ) do
    @transaction.set_action("#{module_name(controller)}##{action}")
  end

  def handle_event(
        [:phoenix, :endpoint, :start],
        _measurements,
        %{conn: %Plug.Conn{private: %{appsignal_transaction: transaction}}},
        _config
      ) do
    @transaction.start_event(transaction)
  end

  def handle_event([:phoenix, :endpoint, :start], _measurements, _metadata, _config) do
    @transaction.start_event()
  end

  def handle_event(
        [:phoenix, :endpoint, :stop],
        _measurements,
        %{conn: %Plug.Conn{private: %{appsignal_transaction: transaction}}},
        _config
      ) do
    @transaction.finish_event(
      transaction,
      "call.phoenix_endpoint",
      "call.phoenix_endpoint",
      %{},
      0
    )
  end

  def handle_event([:phoenix, :endpoint, :stop], _measurements, _metadata, _config) do
    @transaction.finish_event("call.phoenix_endpoint", "call.phoenix_endpoint", %{}, 0)
  end
end