Implement closing/reopening of Transports in farmbot_firmware

defmodule FarmbotFirmware do
  @moduledoc """
  Firmware wrapper for interacting with Farmbot-Arduino-Firmware.
  This GenServer is expected to be a pretty simple state machine
  with no side effects to anything in the rest of the Farmbot application.
  Side effects should be implemented using a callback/pubsub system. This
  allows for indpendent testing.

  Functionality that is needed to boot the firmware:
    * paramaters - Keyword list of {param_atom, float}

  Side affects that should be handled
    * position reports
    * end stop reports
    * calibration reports
    * busy reports

  # State machine
  The firmware starts in a `:transport_boot` state, moving to `:boot`. It then
  loads all paramaters writes all paramaters, and goes to idle if all params
  were loaded successfully.

  State machine flows go as follows:
  ## Boot
      :transport_boot
      |> :boot
      |> :no_config
      |> :configuration
      |> :idle

  ## Idle
      :idle
      |> :begin
      |> :busy
      |> :error | :invalid | :success

  # Constraints and Exceptions
  Commands will be queued as they received with some exceptions:
  * if a command is currently executing (state is not `:idle`),
    proceding commands will be queued in the order they are received.
  * the `:emergency_lock` and `:emergency_unlock` commands go to the front
    of the command queue and are started immediately.
  * if a `report_emergency_lock` message is received at any point during a
    commands execution, that command is considered an error.
    (this does not apply to `:boot` state, since `:paramater_write`
     is accepted while the firmware is locked.)
  * all reports outside of control flow reports (:begin, :error, :invalid,
    :success) will be discarded while in `:boot` state. This means while
    boot, position updates, end stop updates etc are ignored.

  # Transports
  GCODES should be exchanged in the following format:
      {tag, {command, args}}
  * `tag` - binary integer. This is translated to the `Q` paramater.
  * `command` - either a `RXX`, `FXX`, or `GXX` code.
  * `args` - a list of arguments to be processed.

  For example a report might look like:
      {"123", {:report_some_information, [h: 10.00, u: 90.10]}}
  and a command might look like:
      {"555", {:fire_laser, [w: 100.00]}}
  Numbers should be floats when possible. An Exeption to this is `:report_end_stops`
  where there is only two values: `1` or `0`.

  See the `GCODE` module for more information on available implemented GCODES.
  a `Transport` should be a process that implements standard `GenServer`
  behaviour.

  Upon `init/1` the args passed in should be a Keyword list required to configure
  the transport such as a serial device, etc. `args` will also contain a
  `:handle_gcode` function that should be called everytime a GCODE is received.

      Keyword.fetch!(args, :handle_gcode).({"999", {:report_software_version, ["Just a test!"]}})

  a transport should also implement a `handle_call` clause like:

      def handle_call({"166", {:paramater_write, [some_param: 100.00]}}, _from, state)

  and reply with `:ok | {:error, term()}`
  """
  use GenServer
  require Logger

  alias FarmbotFirmware, as: State
  alias FarmbotFirmware.{GCODE, Command, Request}

  @transport_init_error_retry_ms 5_000

  @type status ::
          :transport_boot
          | :boot
          | :no_config
          | :configuration
          | :idle
          | :emergency_lock

  defstruct [
    :transport,
    :transport_pid,
    :transport_ref,
    :transport_args,
    :side_effects,
    :status,
    :tag,
    :configuration_queue,
    :command_queue,
    :caller_pid,
    :current
  ]

  @type state :: %State{
          transport: module(),
          transport_pid: nil | pid(),
          transport_ref: nil | reference(),
          transport_args: Keyword.t(),
          side_effects: nil | module(),
          status: status(),
          tag: GCODE.tag(),
          configuration_queue: [{GCODE.kind(), GCODE.args()}],
          command_queue: [{pid(), GCODE.t()}],
          caller_pid: nil | pid,
          current: nil | GCODE.t()
        }

  @doc """
  Command the firmware to do something. Takes a `{tag, {command, args}}`
  GCODE. This command will be queued if there is already a command
  executing. (this does not apply to `:emergency_lock` and `:emergency_unlock`)

  ## Response/Control Flow
  When executed, `command` will block until one of the following respones
  are received:
    * `{:report_success, []}` -> `:ok`
    * `{:report_invalid, []}` -> `{:error, :invalid_command}`
    * `{:report_error, []}` -> `{:error, :firmware_error}`
    * `{:report_emergency_lock, []}` -> {:error, :emergency_lock}`

  If the firmware is in any of the following states:
    * `:boot`
    * `:no_config`
    * `:configuration`
  `command` will fail with `{:error, state}`
  """
  defdelegate command(server \\ __MODULE__, code), to: Command

  @doc """
  Request data from the firmware.
  Valid requests are of kind:

      :paramater_read
      :status_read
      :pin_read
      :end_stops_read
      :position_read
      :software_version_read

  Will return `{:ok, {tag, {:report_*, args}}}` on success
  or `{:error, term()}` on error.
  """
  defdelegate request(server \\ __MODULE__, code), to: Request

  @doc """
  Close the transport, putting the Firmware State Machine back into
  the `:transport_boot` state.
  """
  def close_transport(server \\ __MODULE__) do
    _ = command(server, {nil, {:command_emergency_lock, []}})
    GenServer.call(server, :close_transport)
  end

  @doc """
  Opens the transport,
  """
  def open_transport(server \\ __MODULE__) do
    GenServer.call(server, :open_transport)
  end

  @doc """
  Starting the Firmware server requires at least:
  * `:transport` - a module implementing the Transport GenServer behaviour.
    See the `Transports` section of moduledoc.

  Every other arg passed in will be passed directly to the `:transport` module's
  `init/1` function.
  """
  def start_link(args, opts \\ [name: __MODULE__]) do
    GenServer.start_link(__MODULE__, args, opts)
  end

  def init(args) do
    transport = Keyword.fetch!(args, :transport)
    side_effects = Keyword.get(args, :side_effects)

    # Add an anon function that transport implementations should call.
    fw = self()
    fun = fn {_, _} = code -> GenServer.cast(fw, code) end
    transport_args = Keyword.put(args, :handle_gcode, fun)

    state = %State{
      transport_pid: nil,
      transport_ref: nil,
      transport: transport,
      transport_args: transport_args,
      side_effects: side_effects,
      status: :transport_boot,
      command_queue: [],
      configuration_queue: []
    }

    send(self(), :timeout)
    {:ok, state}
  end

  def terminate(reason, state) do
    for {pid, _code} <- state.command_queue, do: send(pid, reason)
  end

  # This will be the first message received right after `init/1`
  # It should try to open a transport every `transport_init_error_retry_ms`
  # until success.
  # TODO(Connor) maybe make this timer back off over time.
  def handle_info(:timeout, %{status: :transport_boot} = state) do
    case GenServer.start_link(state.transport, state.transport_args) do
      {:ok, pid} ->
        ref = Process.monitor(pid)

        Logger.debug(
          "Firmware Transport #{state.transport} started. #{inspect(state.transport_args)}"
        )

        state = goto(%{state | transport_pid: pid, transport_ref: ref}, :boot)
        {:noreply, state}

      error ->
        Logger.error("Error starting Firmware: #{inspect(error)}")
        Process.send_after(self(), :timeout, @transport_init_error_retry_ms)
        {:noreply, state}
    end
  end

  # @spec handle_info(:timeout, state) :: {:noreply, state}
  def handle_info(:timeout, %{configuration_queue: [code | rest]} = state) do
    Logger.debug("Starting next configuration code: #{inspect(code)}")

    case GenServer.call(state.transport_pid, {state.tag, code}) do
      :ok ->
        new_state = %{state | current: code, configuration_queue: rest}
        side_effects(new_state, :handle_output_gcode, [{state.tag, code}])
        {:noreply, new_state}

      error ->
        {:stop, error, state}
    end
  end

  def handle_info(:timeout, %{command_queue: [{pid, {tag, code}} | rest]} = state) do
    case GenServer.call(state.transport_pid, {tag, code}) do
      :ok ->
        new_state = %{state | tag: tag, current: code, command_queue: rest, caller_pid: pid}
        side_effects(new_state, :handle_output_gcode, [{state.tag, code}])
        {:noreply, new_state}

      error ->
        {:stop, error, state}
    end
  end

  def handle_info(:timeout, %{configuration_queue: []} = state) do
    {:noreply, state}
  end

  # Closing the transport will purge the buffer of queued commands in both
  # the `configuration_queue` and in the `command_queue`.
  def handle_call(:close_transport, _from, %{status: s} = state) when s != :transport_boot do
    true = Process.demonitor(state.transport_ref)
    :ok = GenServer.stop(state.transport_pid, :normal)

    state =
      goto(
        %{
          state
          | transport_pid: nil,
            transport_ref: nil,
            status: :transport_boot,
            command_queue: [],
            configuration_queue: []
        },
        :transport_boot
      )

    {:reply, :ok, state}
  end

  def handle_call(:close_transport, _, %{status: s} = state) do
    {:reply, {:error, s}, state}
  end

  def handle_call(:open_transport, _from, %{status: s} = state) when s == :transport_boot do
    send(self(), :timeout)
    {:reply, :ok, state}
  end

  def handle_call(:open_transport, _from, %{status: s} = state) do
    {:reply, {:error, s}, state}
  end

  def handle_call({_tag, _code} = gcode, from, state) do
    handle_command(gcode, from, state)
  end

  @doc false
  @spec handle_command(GCODE.t(), GenServer.from(), state()) :: {:reply, term(), state()}

  # If not in an acceptable state, return an error immediately.
  def handle_command(_, _, %{status: s} = state)
      when s in [:transport_boot, :boot, :no_config, :configuration] do
    {:reply, {:error, s}, state}
  end

  # EmergencyLock should be ran immediately
  def handle_command({tag, {:command_emergency_lock, []}} = code, {pid, _ref}, state) do
    {:reply, {:ok, tag}, %{state | command_queue: [{pid, code} | state.command_queue]}, 0}
  end

  # EmergencyUnLock should be ran immediately
  def handle_command({tag, {:command_emergency_unlock, []}} = code, {pid, _ref}, state) do
    {:reply, {:ok, tag}, %{state | command_queue: [{pid, code} | state.command_queue]}, 0}
  end

  def handle_command({tag, {_, _}} = code, {pid, _ref}, state) do
    new_state = %{state | command_queue: state.command_queue ++ [{pid, code}]}

    case new_state.status do
      :idle ->
        {:reply, {:ok, tag}, new_state, 0}

      # Don't do any flow control if state is emergency_lock.
      # This allows a transport to decide
      # if a command should be blocked or not.
      :emergency_lock ->
        {:reply, {:ok, tag}, new_state, 0}

      _ ->
        {:reply, {:ok, tag}, new_state}
    end
  end

  # Extracts tag
  def handle_cast({tag, {_, _} = code}, state) do
    side_effects(state, :handle_input_gcode, [{tag, code}])
    handle_report(code, %{state | tag: tag})
  end

  @doc false
  @spec handle_report({GCODE.report_kind(), GCODE.args()}, state) ::
          {:noreply, state(), 0} | {:noreply, state()}
  def handle_report({:report_emergency_lock, []} = code, state) do
    Logger.info("Emergency lock")
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    {:noreply, goto(%{state | current: nil, caller_pid: nil}, :emergency_lock), 0}
  end

  # "ARDUINO STARTUP COMPLETE" => goto(:boot, :no_config)
  def handle_report({:report_debug_message, ["ARDUINO STARTUP COMPLETE"]}, state) do
    Logger.info("ARDUINO STARTUP COMPLETE")
    {:noreply, goto(state, :no_config)}
  end

  def handle_report({:report_no_config, []}, %{status: :boot} = state) do
    Logger.info("ARDUINO STARTUP COMPLETE")
    {:noreply, goto(state, :no_config)}
  end

  def handle_report({:report_debug_message, msg}, state) do
    side_effects(state, :handle_debug_message, [msg])
    {:noreply, state}
  end

  def handle_report(report, %{status: :boot} = state) do
    Logger.debug(["still in state: :boot ", inspect(report)])
    {:noreply, state}
  end

  # report_idle => goto(_, :idle)
  def handle_report({:report_idle, []}, %{status: _} = state) do
    side_effects(state, :handle_busy, [false])
    {:noreply, goto(%{state | caller_pid: nil, current: nil}, :idle), 0}
  end

  def handle_report({:report_begin, []} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    {:noreply, state}
  end

  def handle_report({:report_success, []} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    new_state = %{state | current: nil, caller_pid: nil}
    side_effects(state, :handle_busy, [false])

    if new_state.status == :emergency_lock do
      {:noreply, goto(new_state, :idle), 0}
    else
      {:noreply, new_state, 0}
    end
  end

  def handle_report({:report_busy, []} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_busy, [true])
    {:noreply, state}
  end

  def handle_report({:report_error, []} = code, %{status: :configuration} = state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_busy, [false])
    {:stop, {:error, state.current}, state}
  end

  def handle_report({:report_error, []} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_busy, [false])
    {:noreply, %{state | caller_pid: nil, current: nil}, 0}
  end

  def handle_report({:report_invalid, []} = code, %{status: :configuration} = state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    {:stop, {:error, state.current}, state}
  end

  def handle_report({:report_invalid, []} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    {:noreply, %{state | caller_pid: nil, current: nil}, 0}
  end

  def handle_report({:report_retry, []} = code, %{status: :configuration} = state) do
    Logger.warn("Retrying configuration command: #{inspect(code)}")
    {:noreply, state}
  end

  def handle_report({:report_retry, []} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    {:noreply, state}
  end

  def handle_report({:report_paramater_value, param} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_paramater_value, [param])
    {:noreply, state}
  end

  def handle_report({:report_calibration_paramater_value, args} = _code, state) do
    to_process = [{:paramater_write, args}]

    {:noreply, goto(%{state | tag: state.tag, configuration_queue: to_process}, :configuration),
     0}
  end

  # report_no_config => goto(_, :no_config)
  def handle_report({:report_no_config, []}, %{status: _} = state) do
    Logger.debug("REPORT_NO_CONFIG")
    tag = state.tag || "0"
    loaded_params = side_effects(state, :load_params, []) || []

    param_commands =
      Enum.reduce(loaded_params, [], fn {param, val}, acc ->
        if val, do: acc ++ [{:paramater_write, [{param, val}]}], else: acc
      end)

    to_process =
      param_commands ++
        [
          {:paramater_write, [{:param_config_ok, 1.0}]},
          {:paramater_read_all, []}
        ]

    to_process =
      if loaded_params[:movement_home_at_boot_x] == 1,
        do: to_process ++ [{:command_movement_find_home, [:x]}],
        else: to_process

    to_process =
      if loaded_params[:movement_home_at_boot_y] == 1,
        do: to_process ++ [{:command_movement_find_home, [:y]}],
        else: to_process

    to_process =
      if loaded_params[:movement_home_at_boot_z] == 1,
        do: to_process ++ [{:command_movement_find_home, [:z]}],
        else: to_process

    {:noreply, goto(%{state | tag: tag, configuration_queue: to_process}, :configuration), 0}
  end

  # report_paramaters_complete => goto(:configuration, :idle)
  def handle_report({:report_paramaters_complete, []}, %{status: :configuration} = state) do
    {:noreply, goto(state, :idle)}
  end

  def handle_report(_, %{status: :no_config} = state) do
    {:noreply, state}
  end

  def handle_report({:report_position, position} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_position, [position])
    {:noreply, state}
  end

  def handle_report({:report_axis_state, axis_state} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_axis_state, [axis_state])
    {:noreply, state}
  end

  def handle_report({:report_calibration_state, calibration_state} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_calibration_state, [calibration_state])
    {:noreply, state}
  end

  def handle_report({:report_position_change, position} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_position_change, [position])
    {:noreply, state}
  end

  def handle_report({:report_encoders_scaled, encoders} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_encoders_scaled, [encoders])
    {:noreply, state}
  end

  def handle_report({:report_encoders_raw, encoders} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_encoders_raw, [encoders])
    {:noreply, state}
  end

  def handle_report({:report_end_stops, end_stops} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_end_stops, [end_stops])
    {:noreply, state}
  end

  def handle_report({:report_pin_value, value} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_pin_value, [value])
    {:noreply, state}
  end

  def handle_report({:report_software_version, version} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_software_version, [version])
    {:noreply, state}
  end

  # NOOP
  def handle_report({:report_echo, _}, state), do: {:noreply, state}

  def handle_report({_kind, _args} = code, state) do
    IO.inspect(code, label: "unknown code for #{state.status}")
    {:noreply, state}
  end

  @spec goto(state(), status()) :: state()
  defp goto(%{status: old} = state, new) do
    new_state = %{state | status: new}

    cond do
      old != new && new == :emergency_lock ->
        side_effects(new_state, :handle_emergency_lock, [])

      old != new && old == :emergency_lock ->
        side_effects(new_state, :handle_emergency_unlock, [])

      old == new ->
        :ok

      true ->
        Logger.debug("unhandled state change: #{old} => #{new}")
    end

    new_state
  end

  @spec side_effects(state, atom, GCODE.args()) :: any()
  defp side_effects(%{side_effects: nil}, _function, _args), do: nil
  defp side_effects(%{side_effects: m}, function, args), do: apply(m, function, args)
end

defmodule FarmbotFirmware do
  @moduledoc """
  Firmware wrapper for interacting with Farmbot-Arduino-Firmware.
  This GenServer is expected to be a pretty simple state machine
  with no side effects to anything in the rest of the Farmbot application.
  Side effects should be implemented using a callback/pubsub system. This
  allows for indpendent testing.

  Functionality that is needed to boot the firmware:
    * paramaters - Keyword list of {param_atom, float}

  Side affects that should be handled
    * position reports
    * end stop reports
    * calibration reports
    * busy reports

  # State machine
  The firmware starts in a `:boot` state. It then loads all paramaters
  writes all paramaters, and goes to idle if all params were loaded successfully.

  State machine flows go as follows:
  ## Boot
      :boot
      |> :no_config
      |> :configuration
      |> :idle

  ## Idle
      :idle
      |> :begin
      |> :busy
      |> :error | :invalid | :success

  # Constraints and Exceptions
  Commands will be queued as they received with some exceptions:
  * if a command is currently executing (state is not `:idle`),
    proceding commands will be queued in the order they are received.
  * the `:emergency_lock` and `:emergency_unlock` commands go to the front
    of the command queue and are started immediately.
  * if a `report_emergency_lock` message is received at any point during a
    commands execution, that command is considered an error.
    (this does not apply to `:boot` state, since `:paramater_write`
     is accepted while the firmware is locked.)
  * all reports outside of control flow reports (:begin, :error, :invalid,
    :success) will be discarded while in `:boot` state. This means while
    boot, position updates, end stop updates etc are ignored.

  # Transports
  GCODES should be exchanged in the following format:
      {tag, {command, args}}
  * `tag` - binary integer. This is translated to the `Q` paramater.
  * `command` - either a `RXX`, `FXX`, or `GXX` code.
  * `args` - a list of arguments to be processed.

  For example a report might look like:
      {"123", {:report_some_information, [h: 10.00, u: 90.10]}}
  and a command might look like:
      {"555", {:fire_laser, [w: 100.00]}}
  Numbers should be floats when possible. An Exeption to this is `:report_end_stops`
  where there is only two values: `1` or `0`.

  See the `GCODE` module for more information on available implemented GCODES.
  a `Transport` should be a process that implements standard `GenServer`
  behaviour.

  Upon `init/1` the args passed in should be a Keyword list required to configure
  the transport such as a serial device, etc. `args` will also contain a
  `:handle_gcode` function that should be called everytime a GCODE is received.

      Keyword.fetch!(args, :handle_gcode).({"999", {:report_software_version, ["Just a test!"]}})

  a transport should also implement a `handle_call` clause like:

      def handle_call({"166", {:paramater_write, [some_param: 100.00]}}, _from, state)

  and reply with `:ok | {:error, term()}`
  """
  use GenServer
  require Logger

  alias FarmbotFirmware, as: State
  alias FarmbotFirmware.{GCODE, Command, Request}
  @type status :: :boot | :no_config | :configuration | :idle | :emergency_lock

  defstruct [
    :transport,
    :transport_pid,
    :side_effects,
    :status,
    :tag,
    :configuration_queue,
    :command_queue,
    :caller_pid,
    :current
  ]

  @type state :: %State{
          transport: module(),
          transport_pid: pid(),
          side_effects: nil | module(),
          status: status(),
          tag: GCODE.tag(),
          configuration_queue: [{GCODE.kind(), GCODE.args()}],
          command_queue: [{pid(), GCODE.t()}],
          caller_pid: nil | pid,
          current: nil | GCODE.t()
        }

  @doc """
  Command the firmware to do something. Takes a `{tag, {command, args}}`
  GCODE. This command will be queued if there is already a command
  executing. (this does not apply to `:emergency_lock` and `:emergency_unlock`)

  ## Response/Control Flow
  When executed, `command` will block until one of the following respones
  are received:
    * `{:report_success, []}` -> `:ok`
    * `{:report_invalid, []}` -> `{:error, :invalid_command}`
    * `{:report_error, []}` -> `{:error, :firmware_error}`
    * `{:report_emergency_lock, []}` -> {:error, :emergency_lock}`

  If the firmware is in any of the following states:
    * `:boot`
    * `:no_config`
    * `:configuration`
  `command` will fail with `{:error, state}`
  """
  defdelegate command(server \\ __MODULE__, code), to: Command

  @doc """
  Request data from the firmware.
  Valid requests are of kind:

      :paramater_read
      :status_read
      :pin_read
      :end_stops_read
      :position_read
      :software_version_read

  Will return `{:ok, {tag, {:report_*, args}}}` on success
  or `{:error, term()}` on error.
  """
  defdelegate request(server \\ __MODULE__, code), to: Request

  def close_transport(server \\ __MODULE__) do
    _ = command(server, {nil, {:emergency_lock, []}})
    GenServer.call(server, :close_transport)
  end

  def open_transport(server \\ __MODULE__) do
  end

  @doc """
  Starting the Firmware server requires at least:
  * `:transport` - a module implementing the Transport GenServer behaviour.
    See the `Transports` section of moduledoc.

  Every other arg passed in will be passed directly to the `:transport` module's
  `init/1` function.
  """
  def start_link(args, opts \\ [name: __MODULE__]) do
    GenServer.start_link(__MODULE__, args, opts)
  end

  def init(args) do
    transport = Keyword.fetch!(args, :transport)
    side_effects = Keyword.get(args, :side_effects)
    fw = self()
    fun = fn {_, _} = code -> GenServer.cast(fw, code) end
    args = Keyword.put(args, :handle_gcode, fun)

    with {:ok, pid} <- GenServer.start_link(transport, args) do
      Process.monitor(pid)
      Logger.debug("Starting Firmware: #{inspect(args)}")

      state = %State{
        transport_pid: pid,
        transport: transport,
        side_effects: side_effects,
        status: :boot,
        command_queue: [],
        configuration_queue: []
      }

      {:ok, state}
    end
  end

  def terminate(reason, state) do
    for {pid, _code} <- state.command_queue, do: send(pid, reason)
  end

  def handle_info({:DOWN, _ref, :process, pid, reason}, %{transport_pid: pid} = state) do
    {:stop, reason, state}
  end

  # @spec handle_info(:timeout, state) :: {:noreply, state}
  def handle_info(:timeout, %{configuration_queue: [code | rest]} = state) do
    Logger.debug("Starting next configuration code: #{inspect(code)}")

    case GenServer.call(state.transport_pid, {state.tag, code}) do
      :ok ->
        new_state = %{state | current: code, configuration_queue: rest}
        side_effects(new_state, :handle_output_gcode, [{state.tag, code}])
        {:noreply, new_state}

      error ->
        {:stop, error, state}
    end
  end

  def handle_info(:timeout, %{command_queue: [{pid, {tag, code}} | rest]} = state) do
    case GenServer.call(state.transport_pid, {tag, code}) do
      :ok ->
        new_state = %{state | tag: tag, current: code, command_queue: rest, caller_pid: pid}
        side_effects(new_state, :handle_output_gcode, [{state.tag, code}])
        {:noreply, new_state}

      error ->
        {:stop, error, state}
    end
  end

  def handle_info(:timeout, %{configuration_queue: []} = state) do
    {:noreply, state}
  end

  def handle_call(:close_transport, _from, state) do
    emergency_lock()
    {:reply, :ok, state}
  end

  def handle_call({_tag, _code} = gcode, from, state) do
    handle_command(gcode, from, state)
  end

  @doc false
  @spec handle_command(GCODE.t(), GenServer.from(), state()) :: {:reply, term(), state()}
  def handle_command(_, _, %{status: s} = state) when s in [:boot, :no_config, :configuration] do
    {:reply, {:error, s}, state}
  end

  def handle_command({tag, {:command_emergency_lock, []}} = code, {pid, _ref}, state) do
    {:reply, {:ok, tag}, %{state | command_queue: [{pid, code} | state.command_queue]}, 0}
  end

  def handle_command({tag, {:command_emergency_unlock, []}} = code, {pid, _ref}, state) do
    {:reply, {:ok, tag}, %{state | command_queue: [{pid, code} | state.command_queue]}, 0}
  end

  def handle_command({tag, {_, _}} = code, {pid, _ref}, state) do
    new_state = %{state | command_queue: state.command_queue ++ [{pid, code}]}

    case new_state.status do
      :idle ->
        {:reply, {:ok, tag}, new_state, 0}

      # Don't do any flow control if state is emergency_lock.
      # This allows a transport to decide
      # if a command should be blocked or not.
      :emergency_lock ->
        {:reply, {:ok, tag}, new_state, 0}

      _ ->
        {:reply, {:ok, tag}, new_state}
    end
  end

  # Extracts tag
  def handle_cast({tag, {_, _} = code}, state) do
    side_effects(state, :handle_input_gcode, [{tag, code}])
    handle_report(code, %{state | tag: tag})
  end

  @doc false
  @spec handle_report({GCODE.report_kind(), GCODE.args()}, state) ::
          {:noreply, state(), 0} | {:noreply, state()}
  def handle_report({:report_emergency_lock, []} = code, state) do
    Logger.info("Emergency lock")
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    {:noreply, goto(%{state | current: nil, caller_pid: nil}, :emergency_lock), 0}
  end

  # "ARDUINO STARTUP COMPLETE" => goto(:boot, :no_config)
  def handle_report({:report_debug_message, ["ARDUINO STARTUP COMPLETE"]}, state) do
    Logger.info("ARDUINO STARTUP COMPLETE")
    {:noreply, goto(state, :no_config)}
  end

  def handle_report({:report_no_config, []}, %{status: :boot} = state) do
    Logger.info("ARDUINO STARTUP COMPLETE")
    {:noreply, goto(state, :no_config)}
  end

  def handle_report({:report_debug_message, msg}, state) do
    side_effects(state, :handle_debug_message, [msg])
    {:noreply, state}
  end

  def handle_report(report, %{status: :boot} = state) do
    Logger.debug(["still in state: :boot ", inspect(report)])
    {:noreply, state}
  end

  # report_idle => goto(_, :idle)
  def handle_report({:report_idle, []}, %{status: _} = state) do
    side_effects(state, :handle_busy, [false])
    {:noreply, goto(%{state | caller_pid: nil, current: nil}, :idle), 0}
  end

  def handle_report({:report_begin, []} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    {:noreply, state}
  end

  def handle_report({:report_success, []} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    new_state = %{state | current: nil, caller_pid: nil}
    side_effects(state, :handle_busy, [false])

    if new_state.status == :emergency_lock do
      {:noreply, goto(new_state, :idle), 0}
    else
      {:noreply, new_state, 0}
    end
  end

  def handle_report({:report_busy, []} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_busy, [true])
    {:noreply, state}
  end

  def handle_report({:report_error, []} = code, %{status: :configuration} = state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_busy, [false])
    {:stop, {:error, state.current}, state}
  end

  def handle_report({:report_error, []} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_busy, [false])
    {:noreply, %{state | caller_pid: nil, current: nil}, 0}
  end

  def handle_report({:report_invalid, []} = code, %{status: :configuration} = state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    {:stop, {:error, state.current}, state}
  end

  def handle_report({:report_invalid, []} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    {:noreply, %{state | caller_pid: nil, current: nil}, 0}
  end

  def handle_report({:report_retry, []} = code, %{status: :configuration} = state) do
    Logger.warn("Retrying configuration command: #{inspect(code)}")
    {:noreply, state}
  end

  def handle_report({:report_retry, []} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    {:noreply, state}
  end

  def handle_report({:report_paramater_value, param} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_paramater_value, [param])
    {:noreply, state}
  end

  def handle_report({:report_calibration_paramater_value, args} = _code, state) do
    to_process = [{:paramater_write, args}]

    {:noreply, goto(%{state | tag: state.tag, configuration_queue: to_process}, :configuration),
     0}
  end

  # report_no_config => goto(_, :no_config)
  def handle_report({:report_no_config, []}, %{status: _} = state) do
    Logger.debug("REPORT_NO_CONFIG")
    tag = state.tag || "0"
    loaded_params = side_effects(state, :load_params, []) || []

    param_commands =
      Enum.reduce(loaded_params, [], fn {param, val}, acc ->
        if val, do: acc ++ [{:paramater_write, [{param, val}]}], else: acc
      end)

    to_process =
      param_commands ++
        [
          {:paramater_write, [{:param_config_ok, 1.0}]},
          {:paramater_read_all, []}
        ]

    to_process =
      if loaded_params[:movement_home_at_boot_x] == 1,
        do: to_process ++ [{:command_movement_find_home, [:x]}],
        else: to_process

    to_process =
      if loaded_params[:movement_home_at_boot_y] == 1,
        do: to_process ++ [{:command_movement_find_home, [:y]}],
        else: to_process

    to_process =
      if loaded_params[:movement_home_at_boot_z] == 1,
        do: to_process ++ [{:command_movement_find_home, [:z]}],
        else: to_process

    {:noreply, goto(%{state | tag: tag, configuration_queue: to_process}, :configuration), 0}
  end

  # report_paramaters_complete => goto(:configuration, :idle)
  def handle_report({:report_paramaters_complete, []}, %{status: :configuration} = state) do
    {:noreply, goto(state, :idle)}
  end

  def handle_report(_, %{status: :no_config} = state) do
    {:noreply, state}
  end

  def handle_report({:report_position, position} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_position, [position])
    {:noreply, state}
  end

  def handle_report({:report_axis_state, axis_state} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_axis_state, [axis_state])
    {:noreply, state}
  end

  def handle_report({:report_calibration_state, calibration_state} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_calibration_state, [calibration_state])
    {:noreply, state}
  end

  def handle_report({:report_position_change, position} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_position_change, [position])
    {:noreply, state}
  end

  def handle_report({:report_encoders_scaled, encoders} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_encoders_scaled, [encoders])
    {:noreply, state}
  end

  def handle_report({:report_encoders_raw, encoders} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_encoders_raw, [encoders])
    {:noreply, state}
  end

  def handle_report({:report_end_stops, end_stops} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_end_stops, [end_stops])
    {:noreply, state}
  end

  def handle_report({:report_pin_value, value} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_pin_value, [value])
    {:noreply, state}
  end

  def handle_report({:report_software_version, version} = code, state) do
    if state.caller_pid, do: send(state.caller_pid, {state.tag, code})
    side_effects(state, :handle_software_version, [version])
    {:noreply, state}
  end

  # NOOP
  def handle_report({:report_echo, _}, state), do: {:noreply, state}

  def handle_report({_kind, _args} = code, state) do
    IO.inspect(code, label: "unknown code for #{state.status}")
    {:noreply, state}
  end

  @spec goto(state(), status()) :: state()
  defp goto(%{status: old} = state, new) do
    new_state = %{state | status: new}

    cond do
      old != new && new == :emergency_lock ->
        side_effects(new_state, :handle_emergency_lock, [])

      old != new && old == :emergency_lock ->
        side_effects(new_state, :handle_emergency_unlock, [])

      old == new ->
        :ok

      true ->
        Logger.debug("unhandled state change: #{old} => #{new}")
    end

    new_state
  end

  @spec side_effects(state, atom, GCODE.args()) :: any()
  defp side_effects(%{side_effects: nil}, _function, _args), do: nil
  defp side_effects(%{side_effects: m}, function, args), do: apply(m, function, args)
end


Lazy logging in ErrorHandler

Lazy logging in ErrorHandler

defmodule Appsignal.ErrorHandler do
  @moduledoc """
  Error handler which sends all crash reports to the AppSignal backend.

  When you add `:appsignal` to your application's dependencies, this
  error logger will automatically be installed. All processes that are
  supervised, like GenServers, Tasks, Agents, Supervisored will be
  monitored for crashes. In the case of a crash, the AppSignal error
  handler collects error information and sends it to the backend.

  """

  require Logger

  alias Appsignal.{Transaction, Backtrace}

  @doc """
  Retrieve the last Appsignal.Transaction.t that the error logger picked up
  """
  @spec get_last_transaction :: Transaction.t | nil
  def get_last_transaction do
    :gen_event.call(:error_logger, Appsignal.ErrorHandler, :get_last_transaction)
  end

  def init(_) do
    # state of the error handler holds the last matched transaction
    {:ok, nil}
  end

  def handle_event(event, state) do
    state =
     case match_event(event) do
        {origin, reason, message, stack, conn} ->
          transaction = Transaction.lookup_or_create_transaction(origin)
          if transaction != nil do
            submit_transaction(transaction, normalize_reason(reason), message, stack, %{}, conn)
          end
        :nomatch ->
          state
      end
    {:ok, state}
  end

  def submit_transaction(transaction, reason, message, stack, metadata, conn \\ nil)
  def submit_transaction(transaction, reason, message, stack, metadata, nil) do
    Transaction.set_error(transaction, reason, message, stack)
    Transaction.set_meta_data(metadata)
    Transaction.finish(transaction)
    Transaction.complete(transaction)
    Logger.debug fn ->
      "Submitting #{inspect transaction}: #{message}"
    end
    transaction
  end
  if Appsignal.plug? do
    def submit_transaction(transaction, reason, message, stack, metadata, conn) do
      if conn do
        Transaction.set_request_metadata(transaction, conn)
      end
      submit_transaction(transaction, reason, message, stack, metadata)
    end
  end

  def handle_call(:get_last_transaction, state) do
    {:ok, state, state}
  end

  @doc false
  @spec match_event(term) :: {pid, term, String.t, list, Map.t} | :nomatch
  def match_event({:error_report, _gleader, {origin, :crash_report, report}}) do
    match_error_report(origin, report)
  end
  def match_event(_event) do
    :nomatch
  end

  defp match_error_report(origin, [[{:initial_call, _},
                                    {:pid, pid},
                                    {:registered_name, name},
                                    {:error_info, {_kind, exception, stack}} | _], _linked]) do
    msg = "Process #{crash_name(pid, name)} terminating"
    stacktrace = extract_stacktrace(exception) || stack
    {reason, message} = extract_reason_and_message(exception, msg)
    {origin, reason, message, Backtrace.from_stacktrace(stacktrace), nil}
  end

  defp extract_stacktrace({_, stacktrace}) do
    case stacktrace?(stacktrace) do
      true -> stacktrace
      false -> nil
    end
  end
  defp extract_stacktrace(_), do: nil

  defp stacktrace?(stacktrace) when is_list(stacktrace) do
    Enum.all?(stacktrace, &stacktrace_line?/1)
  end
  defp stacktrace?(_), do: false

  defp stacktrace_line?({_,_,_,[file: _, line: _]}), do: true
  defp stacktrace_line?(_), do: false

  @doc false
  def format_stack(stacktrace) do
    IO.warn "Appsignal.ErrorHandler.format_stack/1 is deprecated. Use Appsignal.Backtrace.from_stacktrace/1 instead."
    Backtrace.from_stacktrace(stacktrace)
  end

  defp crash_name(pid, []), do: inspect(pid)
  defp crash_name(pid, name), do: "#{inspect(name)} (#{inspect(pid)})"

  @doc """
  Extract a consise reason from the given error reason, stripping it from long stack traces and the like.
  Also returns a 'message' which is supposed to contain extra error information.
  """
  @spec extract_reason_and_message(any(), binary()) :: {any(), binary()}
  def extract_reason_and_message(reason, message) when is_binary(reason) do
    {reason, message}
  end
  def extract_reason_and_message(reason, message) when is_atom(reason) do
    try do
      {Exception.message(reason.exception([])), message}
    rescue
      UndefinedFunctionError -> {"#{inspect reason}", message}
    end
  end
  def extract_reason_and_message(%Protocol.UndefinedError{value: {:error, {error = %{}, _stack}}}, message) do
    extract_reason_and_message(error, message)
  end

  if Appsignal.phoenix? do
    def extract_reason_and_message(%Phoenix.Template.UndefinedError{assigns: %{conn: %{assigns: %{kind: :error, reason: reason}}}}, message) do
      extract_reason_and_message(reason, message)
    end
  end
  def extract_reason_and_message(r = %{}, message) do
    msg = Exception.message(r)
    {"#{inspect r.__struct__}", prefixed(message, msg)}
  end
  def extract_reason_and_message({r = %{}, _}, message) do
    extract_reason_and_message(r, message)
  end
  def extract_reason_and_message({kind, _} = reason, message) do
    {inspect(kind), prefixed(message, inspect(reason))}
  end
  def extract_reason_and_message(any, message) do
    # inspect any term; truncate it
    {"#{inspect any}", message}
  end

  defp prefixed(nil, msg), do: msg
  defp prefixed("", msg), do: msg
  defp prefixed(pre, msg), do: pre <> ": " <> msg

  @pid_or_ref_regex ~r/\<(\d+\.)+\d+\>/
  def normalize_reason(reason) do
    Regex.replace(@pid_or_ref_regex, reason, "<...>")
  end
end

defmodule Appsignal.ErrorHandler do
  @moduledoc """
  Error handler which sends all crash reports to the AppSignal backend.

  When you add `:appsignal` to your application's dependencies, this
  error logger will automatically be installed. All processes that are
  supervised, like GenServers, Tasks, Agents, Supervisored will be
  monitored for crashes. In the case of a crash, the AppSignal error
  handler collects error information and sends it to the backend.

  """

  require Logger

  alias Appsignal.{Transaction, Backtrace}

  @doc """
  Retrieve the last Appsignal.Transaction.t that the error logger picked up
  """
  @spec get_last_transaction :: Transaction.t | nil
  def get_last_transaction do
    :gen_event.call(:error_logger, Appsignal.ErrorHandler, :get_last_transaction)
  end

  def init(_) do
    # state of the error handler holds the last matched transaction
    {:ok, nil}
  end

  def handle_event(event, state) do
    state =
     case match_event(event) do
        {origin, reason, message, stack, conn} ->
          transaction = Transaction.lookup_or_create_transaction(origin)
          if transaction != nil do
            submit_transaction(transaction, normalize_reason(reason), message, stack, %{}, conn)
          end
        :nomatch ->
          state
      end
    {:ok, state}
  end

  def submit_transaction(transaction, reason, message, stack, metadata, conn \\ nil)
  def submit_transaction(transaction, reason, message, stack, metadata, nil) do
    Transaction.set_error(transaction, reason, message, stack)
    Transaction.set_meta_data(metadata)
    Transaction.finish(transaction)
    Transaction.complete(transaction)
    Logger.debug("Submitting #{inspect transaction}: #{message}")
    transaction
  end
  if Appsignal.plug? do
    def submit_transaction(transaction, reason, message, stack, metadata, conn) do
      if conn do
        Transaction.set_request_metadata(transaction, conn)
      end
      submit_transaction(transaction, reason, message, stack, metadata)
    end
  end

  def handle_call(:get_last_transaction, state) do
    {:ok, state, state}
  end

  @doc false
  @spec match_event(term) :: {pid, term, String.t, list, Map.t} | :nomatch
  def match_event({:error_report, _gleader, {origin, :crash_report, report}}) do
    match_error_report(origin, report)
  end
  def match_event(_event) do
    :nomatch
  end

  defp match_error_report(origin, [[{:initial_call, _},
                                    {:pid, pid},
                                    {:registered_name, name},
                                    {:error_info, {_kind, exception, stack}} | _], _linked]) do
    msg = "Process #{crash_name(pid, name)} terminating"
    stacktrace = extract_stacktrace(exception) || stack
    {reason, message} = extract_reason_and_message(exception, msg)
    {origin, reason, message, Backtrace.from_stacktrace(stacktrace), nil}
  end

  defp extract_stacktrace({_, stacktrace}) do
    case stacktrace?(stacktrace) do
      true -> stacktrace
      false -> nil
    end
  end
  defp extract_stacktrace(_), do: nil

  defp stacktrace?(stacktrace) when is_list(stacktrace) do
    Enum.all?(stacktrace, &stacktrace_line?/1)
  end
  defp stacktrace?(_), do: false

  defp stacktrace_line?({_,_,_,[file: _, line: _]}), do: true
  defp stacktrace_line?(_), do: false

  @doc false
  def format_stack(stacktrace) do
    IO.warn "Appsignal.ErrorHandler.format_stack/1 is deprecated. Use Appsignal.Backtrace.from_stacktrace/1 instead."
    Backtrace.from_stacktrace(stacktrace)
  end

  defp crash_name(pid, []), do: inspect(pid)
  defp crash_name(pid, name), do: "#{inspect(name)} (#{inspect(pid)})"

  @doc """
  Extract a consise reason from the given error reason, stripping it from long stack traces and the like.
  Also returns a 'message' which is supposed to contain extra error information.
  """
  @spec extract_reason_and_message(any(), binary()) :: {any(), binary()}
  def extract_reason_and_message(reason, message) when is_binary(reason) do
    {reason, message}
  end
  def extract_reason_and_message(reason, message) when is_atom(reason) do
    try do
      {Exception.message(reason.exception([])), message}
    rescue
      UndefinedFunctionError -> {"#{inspect reason}", message}
    end
  end
  def extract_reason_and_message(%Protocol.UndefinedError{value: {:error, {error = %{}, _stack}}}, message) do
    extract_reason_and_message(error, message)
  end

  if Appsignal.phoenix? do
    def extract_reason_and_message(%Phoenix.Template.UndefinedError{assigns: %{conn: %{assigns: %{kind: :error, reason: reason}}}}, message) do
      extract_reason_and_message(reason, message)
    end
  end
  def extract_reason_and_message(r = %{}, message) do
    msg = Exception.message(r)
    {"#{inspect r.__struct__}", prefixed(message, msg)}
  end
  def extract_reason_and_message({r = %{}, _}, message) do
    extract_reason_and_message(r, message)
  end
  def extract_reason_and_message({kind, _} = reason, message) do
    {inspect(kind), prefixed(message, inspect(reason))}
  end
  def extract_reason_and_message(any, message) do
    # inspect any term; truncate it
    {"#{inspect any}", message}
  end

  defp prefixed(nil, msg), do: msg
  defp prefixed("", msg), do: msg
  defp prefixed(pre, msg), do: pre <> ": " <> msg

  @pid_or_ref_regex ~r/\<(\d+\.)+\d+\>/
  def normalize_reason(reason) do
    Regex.replace(@pid_or_ref_regex, reason, "<...>")
  end
end


Refactor tests to reduce duplication and make readable

defmodule PlugGraphqlTest do
  use ExUnit.Case, async: true
  use Plug.Test

  # The GraphQL schema we're going to use
  defmodule TestSchema do
    def schema do
      %GraphQL.Schema{
        query: %GraphQL.ObjectType{
          name: "RootQueryType",
          fields: [
            %GraphQL.FieldDefinition{
              name: "greeting",
              type: "String",
              resolve: &TestSchema.greeting/1,
            }
          ]
        }
      }
    end

    def greeting(name: name), do: "Hello, #{name}!"
    def greeting(_), do: greeting(name: "world")
  end

  # Setup a Plug which calls the Plug under test
  defmodule TestPlug do
    use Plug.Builder

    plug GraphQL.Plug.GraphQLEndpoint, TestSchema.schema
  end

  def assert_query({method, path, params}, {status, body}) do
    assert_response conn(method, path, params), status, body
  end

  def assert_response(conn, status, body) do
    # Q: what can be passed in here instead of []?
    # Q: how does schema get through? Is this Plug.Builder macro magic?
    conn = TestPlug.call conn, []

    assert conn.status == status
    assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
    assert conn.resp_body == String.strip(body)
    assert conn.halted == true
  end

  test "GET and POST successful query" do
    success = ~S({"data":{"greeting":"Hello, world!"}})
    assert_query {:get,  "/", query: "{greeting}"}, {200, success}
    assert_query {:post, "/", query: "{greeting}"}, {200, success}
  end

  test "missing query" do
    no_query_found_error = ~S({"errors":[{"message":"Must provide query string."}]})
    assert_query {:get,  "/", nil},         {400, no_query_found_error}
    assert_query {:get,  "/", query: nil},  {400, no_query_found_error}
    assert_query {:get,  "/", query: ""},   {400, no_query_found_error}
    assert_query {:get,  "/", query: "  "}, {400, no_query_found_error}
    assert_query {:post, "/", nil},         {400, no_query_found_error}
    assert_query {:post, "/", query: nil},  {400, no_query_found_error}
    assert_query {:post, "/", query: ""},   {400, no_query_found_error}
    assert_query {:post, "/", query: "  "}, {400, no_query_found_error}
  end

  test "invalid query error" do
    syntax_error = ~S({"errors":[{"message":"GraphQL: syntax error before:  on line 1","line_number":1}]})
    assert_query {:get,  "/", query: "{"}, {400, syntax_error}
    assert_query {:post, "/", query: "{"}, {400, syntax_error}
  end

  test "invalid http verbs" do
    invalid_verb_error = ~S({"errors":[{"message":"GraphQL only supports GET and POST requests."}]})
    assert_query {:put,     "/", query: "{greeting}"}, {400, invalid_verb_error}
    # assert_query {:head,    "/", query: "{greeting}"}, {400, invalid_verb_error}
    assert_query {:patch,   "/", query: "{greeting}"}, {400, invalid_verb_error}
    assert_query {:delete,  "/", query: "{greeting}"}, {400, invalid_verb_error}
    assert_query {:options, "/", query: "{greeting}"}, {400, invalid_verb_error}
  end
end

defmodule PlugGraphqlTest do
  use ExUnit.Case, async: true
  use Plug.Test

  # The GraphQL schema we're going to use
  defmodule TestSchema do
    def schema do
      %GraphQL.Schema{
        query: %GraphQL.ObjectType{
          name: "RootQueryType",
          fields: [
            %GraphQL.FieldDefinition{
              name: "greeting",
              type: "String",
              resolve: &TestSchema.greeting/1,
            }
          ]
        }
      }
    end

    def greeting(name: name), do: "Hello, #{name}!"
    def greeting(_), do: greeting(name: "world")
  end

  # Setup a Plug which calls the Plug under test
  defmodule TestPlug do
    use Plug.Builder

    plug GraphQL.Plug.GraphQLEndpoint, TestSchema.schema
  end

  test "GET query" do
    conn = conn(:get, "/", query: "{greeting}")
    |> TestPlug.call [] # Q: what can be passed in here instead of []? how does schema get through? is this Plug.Builder macro magic?

    assert conn.status == 200
    assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
    assert conn.resp_body == String.strip """
      {"data":{"greeting":"Hello, world!"}}
    """
    assert conn.halted == true
  end

  test "GET no query" do
    conn = conn(:get, "/")
    |> TestPlug.call []

    assert conn.status == 400
    assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
    assert conn.resp_body == String.strip """
      {"errors":[{"message":"Must provide query string."}]}
    """
    assert conn.halted == true
  end

  test "GET blank query" do
    conn = conn(:get, "/", query: "")
    |> TestPlug.call []

    assert conn.status == 400
    assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
    assert conn.resp_body == String.strip """
      {"errors":[{"message":"Must provide query string."}]}
    """
    assert conn.halted == true
  end

  test "GET spaces only query" do
    conn = conn(:get, "/", query: "    ")
    |> TestPlug.call []

    assert conn.status == 400
    assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
    assert conn.resp_body == String.strip """
      {"errors":[{"message":"Must provide query string."}]}
    """
    assert conn.halted == true
  end

  test "GET empty query" do
    conn = conn(:get, "/", query: nil)
    |> TestPlug.call []

    assert conn.status == 400
    assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
    assert conn.resp_body == String.strip """
      {"errors":[{"message":"Must provide query string."}]}
    """
    assert conn.halted == true
  end

  test "GET error" do
    conn = conn(:get, "/", query: "{")
    |> TestPlug.call []

    assert conn.status == 400
    assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
    assert conn.resp_body == String.strip """
      {"errors":[{"message":"GraphQL: syntax error before:  on line 1","line_number":1}]}
    """
    assert conn.halted == true
  end

  test "POST query" do
    conn = conn(:get, "/", query: "{greeting}")
    |> TestPlug.call []

    assert conn.status == 200
    assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
    assert conn.resp_body == String.strip """
      {"data":{"greeting":"Hello, world!"}}
    """
    assert conn.halted == true
  end

  test "POST error" do
    conn = conn(:post, "/", query: "{")
    |> TestPlug.call []

    assert conn.status == 400
    assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
    assert conn.resp_body == String.strip """
      {"errors":[{"message":"GraphQL: syntax error before:  on line 1","line_number":1}]}
    """
    assert conn.halted == true
  end

  test "PUT error" do
    conn = conn(:put, "/", query: "{")
    |> TestPlug.call []

    assert conn.status == 400
    assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
    assert conn.resp_body == String.strip """
      {"errors":[{"message":"GraphQL only supports GET and POST requests."}]}
    """
    assert conn.halted == true
  end

  test "PATCH error" do
    conn = conn(:patch, "/", query: "{")
    |> TestPlug.call []

    assert conn.status == 400
    assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
    assert conn.resp_body == String.strip """
      {"errors":[{"message":"GraphQL only supports GET and POST requests."}]}
    """
    assert conn.halted == true
  end

  test "DELETE error" do
    conn = conn(:patch, "/", query: "{")
    |> TestPlug.call []

    assert conn.status == 400
    assert get_resp_header(conn, "content-type") == ["application/json; charset=utf-8"]
    assert conn.resp_body == String.strip """
      {"errors":[{"message":"GraphQL only supports GET and POST requests."}]}
    """
    assert conn.halted == true
  end
end


Add configuration options to Mailgun documentation (#652)

defmodule Swoosh.Adapters.Mailgun do
  @moduledoc ~S"""
  An adapter that sends email using the Mailgun API.

  For reference: [Mailgun API docs](https://documentation.mailgun.com/en/latest/api-sending.html#sending)

  ## Dependency

  Mailgun adapter requires `Plug` to work properly.

  ## Configuration options

  * `:api_key` - the API key used with Mailgun
  * `:domain` - the domain you will be sending emails from
  * `:base_url` - the url to use as the API endpoint. For EU domains, use https://api.eu.mailgun.net/v3

  ## Example

      # config/config.exs
      config :sample, Sample.Mailer,
        adapter: Swoosh.Adapters.Mailgun,
        api_key: "my-api-key",
        domain: "avengers.com"

      # lib/sample/mailer.ex
      defmodule Sample.Mailer do
        use Swoosh.Mailer, otp_app: :sample
      end

  ## Using with provider options

      import Swoosh.Email

      new()
      |> from({"T Stark", "tony.stark@example.com"})
      |> to({"Steve Rogers", "steve.rogers@example.com"})
      |> to("wasp.avengers@example.com")
      |> reply_to("office.avengers@example.com")
      |> cc({"Bruce Banner", "hulk.smash@example.com"})
      |> cc("thor.odinson@example.com")
      |> bcc({"Clinton Francis Barton", "hawk.eye@example.com"})
      |> bcc("beast.avengers@example.com")
      |> subject("Hello, Avengers!")
      |> html_body("
Hello
")
      |> text_body("Hello")
      |> put_provider_option(:custom_vars, %{"key" => "value"})
      |> put_provider_option(:recipient_vars, %{"steve.rogers@example.com": %{var1: 123}, "juan.diaz@example.com": %{var1: 456}})
      |> put_provider_option(:sending_options, %{dkim: "yes", tracking: "no"})
      |> put_provider_option(:tags, ["worldwide-peace", "unity"])
      |> put_provider_option(:template_name, "avengers-templates")

  ## Provider options

    * `:custom_vars` (map) - used to translate to `v:my-var`, now
      `h:X-Mailgun-Variables`, add custom data to email

    * `:recipient_vars` (map) - `recipient-variables`, assign
      custom variable for each email recipient

    * `:sending_options` (map) - `o:my-key`, all the sending options

    * `:tags` (list[string]) - `o:tag`, was added in before `:sending_options`,
      kept for backward compatibility, use `:sending_options` instead

    * `:template_name` (string) - `template`, name of template created at Mailgun

  ## Custom headers

  Headers added via `Email.header/3` will be translated to (`h:`) values that
  Mailgun recognizes.
  """

  use Swoosh.Adapter, required_config: [:api_key, :domain], required_deps: [plug: Plug.Conn.Query]

  alias Swoosh.Email
  import Swoosh.Email.Render

  @base_url "https://api.mailgun.net/v3"
  @api_endpoint "/messages"

  @impl true
  def deliver(%Email{} = email, config \\ []) do
    headers = prepare_headers(email, config)
    url = [base_url(config), "/", config[:domain], @api_endpoint]

    case Swoosh.ApiClient.post(url, headers, prepare_body(email), email) do
      {:ok, 200, _headers, body} ->
        {:ok, %{id: Swoosh.json_library().decode!(body)["id"]}}

      {:ok, code, _headers, body} when code > 399 ->
        case Swoosh.json_library().decode(body) do
          {:ok, error} -> {:error, {code, error}}
          {:error, _} -> {:error, {code, body}}
        end

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

  defp base_url(config), do: config[:base_url] || @base_url

  defp prepare_headers(email, config) do
    [
      {"User-Agent", "swoosh/#{Swoosh.version()}"},
      {"Authorization", "Basic #{auth(config)}"},
      {"Content-Type", content_type(email)}
    ]
  end

  defp auth(config), do: Base.encode64("api:#{config[:api_key]}")

  defp content_type(%{attachments: []}), do: "application/x-www-form-urlencoded"
  defp content_type(%{}), do: "multipart/form-data"

  defp prepare_body(email) do
    %{}
    |> prepare_from(email)
    |> prepare_to(email)
    |> prepare_subject(email)
    |> prepare_html(email)
    |> prepare_text(email)
    |> prepare_cc(email)
    |> prepare_bcc(email)
    |> prepare_reply_to(email)
    |> prepare_attachments(email)
    |> prepare_custom_vars(email)
    |> prepare_sending_options(email)
    |> prepare_recipient_vars(email)
    |> prepare_tags(email)
    |> prepare_custom_headers(email)
    |> prepare_template(email)
    |> encode_body()
  end

  defp prepare_custom_vars(body, %{provider_options: %{custom_vars: custom_vars}}) do
    Map.put(body, "h:X-Mailgun-Variables", Swoosh.json_library().encode!(custom_vars))
  end

  defp prepare_custom_vars(body, _email), do: body

  defp prepare_sending_options(body, %{provider_options: %{sending_options: sending_options}}) do
    Enum.reduce(sending_options, body, fn {k, v}, body ->
      Map.put(body, "o:#{k}", encode_variable(v))
    end)
  end

  defp prepare_sending_options(body, _email), do: body

  defp prepare_recipient_vars(body, %{provider_options: %{recipient_vars: recipient_vars}}) do
    Map.put(body, "recipient-variables", encode_variable(recipient_vars))
  end

  defp prepare_recipient_vars(body, _email), do: body

  defp prepare_tags(body, %{provider_options: %{tags: tags}}) when is_list(tags) do
    Map.put(body, "o:tag", tags)
  end

  defp prepare_tags(body, _email), do: body

  defp prepare_custom_headers(body, %{headers: headers}) do
    Enum.reduce(headers, body, fn {k, v}, body -> Map.put(body, "h:#{k}", v) end)
  end

  defp prepare_attachments(body, %{attachments: []}), do: body

  defp prepare_attachments(body, %{attachments: attachments}) do
    {normal_attachments, inline_attachments} =
      Enum.split_with(attachments, fn %{type: type} -> type == :attachment end)

    body
    |> Map.put(:attachments, Enum.map(normal_attachments, &prepare_file(&1, "attachment")))
    |> Map.put(:inline, Enum.map(inline_attachments, &prepare_file(&1, "inline")))
  end

  defp prepare_file(%{data: nil} = attachment, type) do
    {:file, attachment.path,
     {"form-data", [{"name", ~s/"#{type}"/}, {"filename", ~s/"#{attachment.filename}"/}]}, []}
  end

  defp prepare_file(attachment, type) do
    {"attachment-data", attachment.data,
     {"form-data", [{"name", ~s/"#{type}"/}, {"filename", ~s/"#{attachment.filename}"/}]},
     [{"Content-Type", attachment.content_type}]}
  end

  defp prepare_from(body, %{from: from}), do: Map.put(body, :from, render_recipient(from))

  defp prepare_to(body, %{to: to}), do: Map.put(body, :to, render_recipient(to))

  defp prepare_reply_to(body, %{reply_to: nil}), do: body

  defp prepare_reply_to(body, %{reply_to: {_name, address}}),
    do: Map.put(body, "h:Reply-To", address)

  defp prepare_cc(body, %{cc: []}), do: body
  defp prepare_cc(body, %{cc: cc}), do: Map.put(body, :cc, render_recipient(cc))

  defp prepare_bcc(body, %{bcc: []}), do: body
  defp prepare_bcc(body, %{bcc: bcc}), do: Map.put(body, :bcc, render_recipient(bcc))

  defp prepare_subject(body, %{subject: subject}), do: Map.put(body, :subject, subject)

  defp prepare_text(body, %{text_body: nil}), do: body
  defp prepare_text(body, %{text_body: text_body}), do: Map.put(body, :text, text_body)

  defp prepare_html(body, %{html_body: nil}), do: body
  defp prepare_html(body, %{html_body: html_body}), do: Map.put(body, :html, html_body)

  defp prepare_template(body, %{provider_options: %{template_name: template_name}}),
    do: Map.put(body, "template", template_name)

  defp prepare_template(body, _), do: body

  defp encode_body(%{attachments: attachments, inline: inline} = params) do
    {:multipart,
     params
     |> Map.drop([:attachments, :inline])
     |> Enum.map(fn {k, v} -> {to_string(k), v} end)
     |> Kernel.++(attachments)
     |> Kernel.++(inline)}
  end

  defp encode_body(no_attachments), do: Plug.Conn.Query.encode(no_attachments)

  defp encode_variable(var) when is_map(var) or is_list(var),
    do: Swoosh.json_library().encode!(var)

  defp encode_variable(var), do: var
end

defmodule Swoosh.Adapters.Mailgun do
  @moduledoc ~S"""
  An adapter that sends email using the Mailgun API.

  For reference: [Mailgun API docs](https://documentation.mailgun.com/en/latest/api-sending.html#sending)

  ## Dependency

  Mailgun adapter requires `Plug` to work properly.

  ## Example

      # config/config.exs
      config :sample, Sample.Mailer,
        adapter: Swoosh.Adapters.Mailgun,
        api_key: "my-api-key",
        domain: "avengers.com"

      # lib/sample/mailer.ex
      defmodule Sample.Mailer do
        use Swoosh.Mailer, otp_app: :sample
      end

  ## Using with provider options

      import Swoosh.Email

      new()
      |> from({"T Stark", "tony.stark@example.com"})
      |> to({"Steve Rogers", "steve.rogers@example.com"})
      |> to("wasp.avengers@example.com")
      |> reply_to("office.avengers@example.com")
      |> cc({"Bruce Banner", "hulk.smash@example.com"})
      |> cc("thor.odinson@example.com")
      |> bcc({"Clinton Francis Barton", "hawk.eye@example.com"})
      |> bcc("beast.avengers@example.com")
      |> subject("Hello, Avengers!")
      |> html_body("
Hello
")
      |> text_body("Hello")
      |> put_provider_option(:custom_vars, %{"key" => "value"})
      |> put_provider_option(:recipient_vars, %{"steve.rogers@example.com": %{var1: 123}, "juan.diaz@example.com": %{var1: 456}})
      |> put_provider_option(:sending_options, %{dkim: "yes", tracking: "no"})
      |> put_provider_option(:tags, ["worldwide-peace", "unity"])
      |> put_provider_option(:template_name, "avengers-templates")

  ## Provider options

    * `:custom_vars` (map) - used to translate to `v:my-var`, now
      `h:X-Mailgun-Variables`, add custom data to email

    * `:recipient_vars` (map) - `recipient-variables`, assign
      custom variable for each email recipient

    * `:sending_options` (map) - `o:my-key`, all the sending options

    * `:tags` (list[string]) - `o:tag`, was added in before `:sending_options`,
      kept for backward compatibility, use `:sending_options` instead

    * `:template_name` (string) - `template`, name of template created at Mailgun

  ## Custom headers

  Headers added via `Email.header/3` will be translated to (`h:`) values that
  Mailgun recognizes.
  """

  use Swoosh.Adapter, required_config: [:api_key, :domain], required_deps: [plug: Plug.Conn.Query]

  alias Swoosh.Email
  import Swoosh.Email.Render

  @base_url "https://api.mailgun.net/v3"
  @api_endpoint "/messages"

  @impl true
  def deliver(%Email{} = email, config \\ []) do
    headers = prepare_headers(email, config)
    url = [base_url(config), "/", config[:domain], @api_endpoint]

    case Swoosh.ApiClient.post(url, headers, prepare_body(email), email) do
      {:ok, 200, _headers, body} ->
        {:ok, %{id: Swoosh.json_library().decode!(body)["id"]}}

      {:ok, code, _headers, body} when code > 399 ->
        case Swoosh.json_library().decode(body) do
          {:ok, error} -> {:error, {code, error}}
          {:error, _} -> {:error, {code, body}}
        end

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

  defp base_url(config), do: config[:base_url] || @base_url

  defp prepare_headers(email, config) do
    [
      {"User-Agent", "swoosh/#{Swoosh.version()}"},
      {"Authorization", "Basic #{auth(config)}"},
      {"Content-Type", content_type(email)}
    ]
  end

  defp auth(config), do: Base.encode64("api:#{config[:api_key]}")

  defp content_type(%{attachments: []}), do: "application/x-www-form-urlencoded"
  defp content_type(%{}), do: "multipart/form-data"

  defp prepare_body(email) do
    %{}
    |> prepare_from(email)
    |> prepare_to(email)
    |> prepare_subject(email)
    |> prepare_html(email)
    |> prepare_text(email)
    |> prepare_cc(email)
    |> prepare_bcc(email)
    |> prepare_reply_to(email)
    |> prepare_attachments(email)
    |> prepare_custom_vars(email)
    |> prepare_sending_options(email)
    |> prepare_recipient_vars(email)
    |> prepare_tags(email)
    |> prepare_custom_headers(email)
    |> prepare_template(email)
    |> encode_body()
  end

  defp prepare_custom_vars(body, %{provider_options: %{custom_vars: custom_vars}}) do
    Map.put(body, "h:X-Mailgun-Variables", Swoosh.json_library().encode!(custom_vars))
  end

  defp prepare_custom_vars(body, _email), do: body

  defp prepare_sending_options(body, %{provider_options: %{sending_options: sending_options}}) do
    Enum.reduce(sending_options, body, fn {k, v}, body ->
      Map.put(body, "o:#{k}", encode_variable(v))
    end)
  end

  defp prepare_sending_options(body, _email), do: body

  defp prepare_recipient_vars(body, %{provider_options: %{recipient_vars: recipient_vars}}) do
    Map.put(body, "recipient-variables", encode_variable(recipient_vars))
  end

  defp prepare_recipient_vars(body, _email), do: body

  defp prepare_tags(body, %{provider_options: %{tags: tags}}) when is_list(tags) do
    Map.put(body, "o:tag", tags)
  end

  defp prepare_tags(body, _email), do: body

  defp prepare_custom_headers(body, %{headers: headers}) do
    Enum.reduce(headers, body, fn {k, v}, body -> Map.put(body, "h:#{k}", v) end)
  end

  defp prepare_attachments(body, %{attachments: []}), do: body

  defp prepare_attachments(body, %{attachments: attachments}) do
    {normal_attachments, inline_attachments} =
      Enum.split_with(attachments, fn %{type: type} -> type == :attachment end)

    body
    |> Map.put(:attachments, Enum.map(normal_attachments, &prepare_file(&1, "attachment")))
    |> Map.put(:inline, Enum.map(inline_attachments, &prepare_file(&1, "inline")))
  end

  defp prepare_file(%{data: nil} = attachment, type) do
    {:file, attachment.path,
     {"form-data", [{"name", ~s/"#{type}"/}, {"filename", ~s/"#{attachment.filename}"/}]}, []}
  end

  defp prepare_file(attachment, type) do
    {"attachment-data", attachment.data,
     {"form-data", [{"name", ~s/"#{type}"/}, {"filename", ~s/"#{attachment.filename}"/}]},
     [{"Content-Type", attachment.content_type}]}
  end

  defp prepare_from(body, %{from: from}), do: Map.put(body, :from, render_recipient(from))

  defp prepare_to(body, %{to: to}), do: Map.put(body, :to, render_recipient(to))

  defp prepare_reply_to(body, %{reply_to: nil}), do: body

  defp prepare_reply_to(body, %{reply_to: {_name, address}}),
    do: Map.put(body, "h:Reply-To", address)

  defp prepare_cc(body, %{cc: []}), do: body
  defp prepare_cc(body, %{cc: cc}), do: Map.put(body, :cc, render_recipient(cc))

  defp prepare_bcc(body, %{bcc: []}), do: body
  defp prepare_bcc(body, %{bcc: bcc}), do: Map.put(body, :bcc, render_recipient(bcc))

  defp prepare_subject(body, %{subject: subject}), do: Map.put(body, :subject, subject)

  defp prepare_text(body, %{text_body: nil}), do: body
  defp prepare_text(body, %{text_body: text_body}), do: Map.put(body, :text, text_body)

  defp prepare_html(body, %{html_body: nil}), do: body
  defp prepare_html(body, %{html_body: html_body}), do: Map.put(body, :html, html_body)

  defp prepare_template(body, %{provider_options: %{template_name: template_name}}),
    do: Map.put(body, "template", template_name)

  defp prepare_template(body, _), do: body

  defp encode_body(%{attachments: attachments, inline: inline} = params) do
    {:multipart,
     params
     |> Map.drop([:attachments, :inline])
     |> Enum.map(fn {k, v} -> {to_string(k), v} end)
     |> Kernel.++(attachments)
     |> Kernel.++(inline)}
  end

  defp encode_body(no_attachments), do: Plug.Conn.Query.encode(no_attachments)

  defp encode_variable(var) when is_map(var) or is_list(var),
    do: Swoosh.json_library().encode!(var)

  defp encode_variable(var), do: var
end


Improve typespec of EEx.Engine.fetch_assign! (#6118)

Whenever EEx templates are used to pass variables via "assigns" whatever
this data structure is will be passed into "EEx.Engine.fetch_assign" and
then into "Access.fetch/2".

So it makes sense to use the same typespec for the both. Otherwise
dialyzer will complain about every instance where keyword lists are
passed into functions like "eval_string", "function_from_file" and
"function_from_string".

defmodule EEx.Engine do
  @moduledoc ~S"""
  Basic EEx engine that ships with Elixir.

  An engine needs to implement four functions:

    * `init(opts)` - returns the initial buffer

    * `handle_body(quoted)` - receives the final built quoted
      expression, should do final post-processing and return a
      quoted expression.

    * `handle_text(buffer, text)` - it receives the buffer,
      the text and must return a new quoted expression.

    * `handle_expr(buffer, marker, expr)` - it receives the buffer,
      the marker, the expr and must return a new quoted expression.

      The marker is what follows exactly after `<%`. For example,
      `<% foo %>` has an empty marker, but `<%= foo %>` has `"="`
      as marker. The allowed markers so far are: `""` and `"="`.

      Read `handle_expr/3` below for more information about the markers
      implemented by default by this engine.

  `EEx.Engine` can be used directly if one desires to use the
  default implementations for the functions above.
  """

  @callback init(opts :: Keyword.t) :: Macro.t
  @callback handle_body(quoted :: Macro.t) :: Macro.t
  @callback handle_text(buffer :: Macro.t, text :: String.t) :: Macro.t
  @callback handle_expr(buffer :: Macro.t, marker :: String.t, expr :: Macro.t) :: Macro.t

  @doc false
  defmacro __using__(_) do
    quote do
      @behaviour EEx.Engine

      def init(opts) do
        EEx.Engine.init(opts)
      end

      def handle_body(quoted) do
        EEx.Engine.handle_body(quoted)
      end

      def handle_text(buffer, text) do
        EEx.Engine.handle_text(buffer, text)
      end

      def handle_expr(buffer, marker, expr) do
        EEx.Engine.handle_expr(buffer, marker, expr)
      end

      defoverridable [handle_body: 1, handle_expr: 3, handle_text: 2, init: 1]
    end
  end

  @doc """
  Handles assigns in quoted expressions.

  A warning will be printed on missing assigns.
  Future versions will raise.

  This can be added to any custom engine by invoking
  `handle_assign/1` with `Macro.prewalk/2`:

      def handle_expr(buffer, token, expr) do
        expr = Macro.prewalk(expr, &EEx.Engine.handle_assign/1)
        EEx.Engine.handle_expr(buffer, token, expr)
      end

  """
  @spec handle_assign(Macro.t) :: Macro.t
  def handle_assign({:@, meta, [{name, _, atom}]}) when is_atom(name) and is_atom(atom) do
    line = meta[:line] || 0
    quote line: line, do: EEx.Engine.fetch_assign!(var!(assigns), unquote(name))
  end
  def handle_assign(arg) do
    arg
  end

  @doc false
  # TODO: Raise on 2.0
  @spec fetch_assign!(Access.t, Access.key) :: term | nil
  def fetch_assign!(assigns, key) do
    case Access.fetch(assigns, key) do
      {:ok, val} ->
        val
      :error ->
        keys = Enum.map(assigns, &elem(&1, 0))
        IO.warn "assign @#{key} not available in EEx template. " <>
                "Please ensure all assigns are given as options. " <>
                "Available assigns: #{inspect keys}"
        nil
    end
  end

  @doc """
  Returns an empty string as initial buffer.
  """
  def init(_opts) do
    ""
  end

  @doc """
  The default implementation simply returns the given expression.
  """
  def handle_body(quoted) do
    quoted
  end

  @doc """
  The default implementation simply concatenates text to the buffer.
  """
  def handle_text(buffer, text) do
    quote do: unquote(buffer) <> unquote(text)
  end

  @doc """
  Implements expressions according to the markers.

      <% Elixir expression - inline with output %>
      <%= Elixir expression - replace with result %>

  All other markers are not implemented by this engine.
  """
  def handle_expr(buffer, "=", expr) do
    quote do
      tmp1 = unquote(buffer)
      tmp1 <> String.Chars.to_string(unquote(expr))
    end
  end

  def handle_expr(buffer, "", expr) do
    quote do
      tmp2 = unquote(buffer)
      unquote(expr)
      tmp2
    end
  end
end

defmodule EEx.Engine do
  @moduledoc ~S"""
  Basic EEx engine that ships with Elixir.

  An engine needs to implement four functions:

    * `init(opts)` - returns the initial buffer

    * `handle_body(quoted)` - receives the final built quoted
      expression, should do final post-processing and return a
      quoted expression.

    * `handle_text(buffer, text)` - it receives the buffer,
      the text and must return a new quoted expression.

    * `handle_expr(buffer, marker, expr)` - it receives the buffer,
      the marker, the expr and must return a new quoted expression.

      The marker is what follows exactly after `<%`. For example,
      `<% foo %>` has an empty marker, but `<%= foo %>` has `"="`
      as marker. The allowed markers so far are: `""` and `"="`.

      Read `handle_expr/3` below for more information about the markers
      implemented by default by this engine.

  `EEx.Engine` can be used directly if one desires to use the
  default implementations for the functions above.
  """

  @callback init(opts :: Keyword.t) :: Macro.t
  @callback handle_body(quoted :: Macro.t) :: Macro.t
  @callback handle_text(buffer :: Macro.t, text :: String.t) :: Macro.t
  @callback handle_expr(buffer :: Macro.t, marker :: String.t, expr :: Macro.t) :: Macro.t

  @doc false
  defmacro __using__(_) do
    quote do
      @behaviour EEx.Engine

      def init(opts) do
        EEx.Engine.init(opts)
      end

      def handle_body(quoted) do
        EEx.Engine.handle_body(quoted)
      end

      def handle_text(buffer, text) do
        EEx.Engine.handle_text(buffer, text)
      end

      def handle_expr(buffer, marker, expr) do
        EEx.Engine.handle_expr(buffer, marker, expr)
      end

      defoverridable [handle_body: 1, handle_expr: 3, handle_text: 2, init: 1]
    end
  end

  @doc """
  Handles assigns in quoted expressions.

  A warning will be printed on missing assigns.
  Future versions will raise.

  This can be added to any custom engine by invoking
  `handle_assign/1` with `Macro.prewalk/2`:

      def handle_expr(buffer, token, expr) do
        expr = Macro.prewalk(expr, &EEx.Engine.handle_assign/1)
        EEx.Engine.handle_expr(buffer, token, expr)
      end

  """
  @spec handle_assign(Macro.t) :: Macro.t
  def handle_assign({:@, meta, [{name, _, atom}]}) when is_atom(name) and is_atom(atom) do
    line = meta[:line] || 0
    quote line: line, do: EEx.Engine.fetch_assign!(var!(assigns), unquote(name))
  end
  def handle_assign(arg) do
    arg
  end

  @doc false
  # TODO: Raise on 2.0
  @spec fetch_assign!(map, Map.key) :: term | nil
  def fetch_assign!(assigns, key) do
    case Access.fetch(assigns, key) do
      {:ok, val} ->
        val
      :error ->
        keys = Enum.map(assigns, &elem(&1, 0))
        IO.warn "assign @#{key} not available in EEx template. " <>
                "Please ensure all assigns are given as options. " <>
                "Available assigns: #{inspect keys}"
        nil
    end
  end

  @doc """
  Returns an empty string as initial buffer.
  """
  def init(_opts) do
    ""
  end

  @doc """
  The default implementation simply returns the given expression.
  """
  def handle_body(quoted) do
    quoted
  end

  @doc """
  The default implementation simply concatenates text to the buffer.
  """
  def handle_text(buffer, text) do
    quote do: unquote(buffer) <> unquote(text)
  end

  @doc """
  Implements expressions according to the markers.

      <% Elixir expression - inline with output %>
      <%= Elixir expression - replace with result %>

  All other markers are not implemented by this engine.
  """
  def handle_expr(buffer, "=", expr) do
    quote do
      tmp1 = unquote(buffer)
      tmp1 <> String.Chars.to_string(unquote(expr))
    end
  end

  def handle_expr(buffer, "", expr) do
    quote do
      tmp2 = unquote(buffer)
      unquote(expr)
      tmp2
    end
  end
end


Print all of the error messages using color :error.

Signed-off-by: José Valim <0c2436ea76ed86e37cc05f66dea18d48ef390882@plataformatec.com.br>

defmodule IEx.Server do
  @moduledoc false

  @doc """
  Eval loop for an IEx session. Its responsibilities include:

    * reading input
    * trapping exceptions in the code being evaluated
    * keeping input history

  """
  def start(config) do
    Process.put :iex_history, []

    { _, _, scope } = :elixir.eval('require IEx.Helpers', [], 0, config.scope)
    config = config.scope(scope)

    config = case config.dot_iex_path do
      ""   -> config                     # don't load anything
      nil  -> load_dot_iex(config)       # load .iex from predefined locations
      path -> load_dot_iex(config, path) # load from `path`
    end

    IO.puts "Interactive Elixir (#{System.version}) - press Ctrl+C to exit (type h() ENTER for help)"
    do_loop(config)
  end

  defp do_loop(config) do
    counter = config.counter
    code    = config.cache
    line    = io_get(config)

    unless line == :eof do
      new_config =
        try do
          eval(code, line, counter, config)
        rescue
          exception ->
            print_exception(exception, System.stacktrace)
            config.cache('')
        catch
          kind, error ->
            print_error(kind, error, System.stacktrace)
            config.cache('')
        end

      do_loop(new_config)
    end
  end

  # Instead of doing just `:elixir.eval`, we first parse the expression to see
  # if it's well formed. If parsing succeeds, we evaluate the AST as usual.
  #
  # If parsing fails, this might be a TokenMissingError which we treat in
  # a special way (to allow for continuation of an expression on the next
  # line in IEx). In case of any other error, we let :elixir_translator
  # to re-raise it.
  #
  # Returns updated config.
  #
  # The first two clauses provide support for the break-trigger allowing to
  # break out from a pending incomplete expression. See
  # https://github.com/elixir-lang/elixir/issues/1089 for discussion.
  #
  @break_trigger '#iex:break\n'
  defp eval(_, @break_trigger, _, config=IEx.Config[cache: '']) do
    # do nothing
    config
  end

  defp eval(_, @break_trigger, line_no, _) do
    :elixir_errors.parse_error(line_no, "iex", 'incomplete expression', [])
  end

  defp eval(code_so_far, latest_input, line_no, config) do
    code = code_so_far ++ latest_input
    case :elixir_translator.forms(code, line_no, "iex", []) do
      { :ok, forms } ->
        { result, new_binding, scope } =
          :elixir.eval_forms(forms, config.binding, config.scope)

        io_put result

        config = config.result(result)
        update_history(config.cache(code).scope(nil))
        config.update_counter(&1+1).cache('').binding(new_binding).scope(scope)

      { :error, { line_no, error, token } } ->
        if token == [] do
          # Update config.cache so that IEx continues to add new input to
          # the unfinished expression in `code`
          config.cache(code)
        else
          # Encountered malformed expression
          :elixir_errors.parse_error(line_no, "iex", error, token)
        end
    end
  end

  # Locates and loads an .iex file from one of predefined locations. Returns
  # new config.
  defp load_dot_iex(config, path // nil) do
    candidates = if path do
      [path]
    else
      Enum.map [".iex", "~/.iex"], Path.expand(&1)
    end
    path = Enum.find candidates, fn path -> File.regular?(path) end
    if nil?(path) do
      config
    else
      try do
        code = File.read!(path)
        scope = :elixir.scope_for_eval(config.scope, file: path)

        # Evaluate the contents in the same environment do_loop will run in
        { _result, binding, scope } =
          :elixir.eval(:unicode.characters_to_list(code),
                       config.binding,
                       0,
                       scope)

        scope = :elixir.scope_for_eval(scope, file: "iex")
        config.binding(binding).scope(scope)
      rescue
        exception ->
          print_exception(exception, System.stacktrace)
          System.halt(1)
      catch
        kind, error ->
          print_error(kind, error, System.stacktrace)
          System.halt(1)
      end
    end
  end

  defp update_history(config) do
    current = Process.get :iex_history
    Process.put :iex_history, [config|current]
  end

  defp io_get(config) do
    prefix = if config.cache != [], do: "..."

    prompt =
      if is_alive do
        "#{prefix || remote_prefix}(#{node})#{config.counter}> "
      else
        "#{prefix || "iex"}(#{config.counter})> "
      end

    case IO.gets(:stdio, prompt) do
      :eof -> :eof
      { :error, _ } -> ''
      data -> :unicode.characters_to_list(data)
    end
  end

  defp io_put(result) do
    IO.puts :stdio, IEx.color(:eval_result, inspect(result, IEx.Options.get(:inspect)))
  end

  defp io_error(result) do
    IO.puts :stdio, IEx.color(:error, result)
  end

  defp remote_prefix do
    if node == node(:erlang.group_leader), do: "iex", else: "rem"
  end

  defp print_exception(exception, stacktrace) do
    print_stacktrace stacktrace, fn ->
      "** (#{inspect exception.__record__(:name)}) #{exception.message}"
    end
  end

  defp print_error(kind, reason, stacktrace) do
    print_stacktrace stacktrace, fn ->
      "** (#{kind}) #{inspect(reason)}"
    end
  end

  defp print_stacktrace(trace, callback) do
    try do
      io_error callback.()
      io_error Exception.format_stacktrace(trace)
    catch
      _, _ ->
        io_error "** (IEx.Error) error when printing exception message and stacktrace"
    end
  end
end

defmodule IEx.Server do
  @moduledoc false

  @doc """
  Eval loop for an IEx session. Its responsibilities include:

    * reading input
    * trapping exceptions in the code being evaluated
    * keeping input history

  """
  def start(config) do
    Process.put :iex_history, []

    { _, _, scope } = :elixir.eval('require IEx.Helpers', [], 0, config.scope)
    config = config.scope(scope)

    config = case config.dot_iex_path do
      ""   -> config                     # don't load anything
      nil  -> load_dot_iex(config)       # load .iex from predefined locations
      path -> load_dot_iex(config, path) # load from `path`
    end

    IO.puts "Interactive Elixir (#{System.version}) - press Ctrl+C to exit (type h() ENTER for help)"
    do_loop(config)
  end

  defp do_loop(config) do
    counter = config.counter
    code    = config.cache
    line    = io_get(config)

    unless line == :eof do
      new_config =
        try do
          eval(code, line, counter, config)
        rescue
          exception ->
            print_exception(exception, System.stacktrace)
            config.cache('')
        catch
          kind, error ->
            print_error(kind, error, System.stacktrace)
            config.cache('')
        end

      do_loop(new_config)
    end
  end

  # Instead of doing just `:elixir.eval`, we first parse the expression to see
  # if it's well formed. If parsing succeeds, we evaluate the AST as usual.
  #
  # If parsing fails, this might be a TokenMissingError which we treat in
  # a special way (to allow for continuation of an expression on the next
  # line in IEx). In case of any other error, we let :elixir_translator
  # to re-raise it.
  #
  # Returns updated config.
  #
  # The first two clauses provide support for the break-trigger allowing to
  # break out from a pending incomplete expression. See
  # https://github.com/elixir-lang/elixir/issues/1089 for discussion.
  #
  @break_trigger '#iex:break\n'
  defp eval(_, @break_trigger, _, config=IEx.Config[cache: '']) do
    # do nothing
    config
  end

  defp eval(_, @break_trigger, line_no, _) do
    :elixir_errors.parse_error(line_no, "iex", 'incomplete expression', [])
  end

  defp eval(code_so_far, latest_input, line_no, config) do
    code = code_so_far ++ latest_input
    case :elixir_translator.forms(code, line_no, "iex", []) do
      { :ok, forms } ->
        { result, new_binding, scope } =
          :elixir.eval_forms(forms, config.binding, config.scope)

        io_put result

        config = config.result(result)
        update_history(config.cache(code).scope(nil))
        config.update_counter(&1+1).cache('').binding(new_binding).scope(scope)

      { :error, { line_no, error, token } } ->
        if token == [] do
          # Update config.cache so that IEx continues to add new input to
          # the unfinished expression in `code`
          config.cache(code)
        else
          # Encountered malformed expression
          :elixir_errors.parse_error(line_no, "iex", error, token)
        end
    end
  end

  # Locates and loads an .iex file from one of predefined locations. Returns
  # new config.
  defp load_dot_iex(config, path // nil) do
    candidates = if path do
      [path]
    else
      Enum.map [".iex", "~/.iex"], Path.expand(&1)
    end
    path = Enum.find candidates, fn path -> File.regular?(path) end
    if nil?(path) do
      config
    else
      try do
        code = File.read!(path)
        scope = :elixir.scope_for_eval(config.scope, file: path)

        # Evaluate the contents in the same environment do_loop will run in
        { _result, binding, scope } =
          :elixir.eval(:unicode.characters_to_list(code),
                       config.binding,
                       0,
                       scope)

        scope = :elixir.scope_for_eval(scope, file: "iex")
        config.binding(binding).scope(scope)
      rescue
        exception ->
          print_exception(exception, System.stacktrace)
          System.halt(1)
      catch
        kind, error ->
          print_error(kind, error, System.stacktrace)
          System.halt(1)
      end
    end
  end

  defp update_history(config) do
    current = Process.get :iex_history
    Process.put :iex_history, [config|current]
  end

  defp io_get(config) do
    prefix = if config.cache != [], do: "..."

    prompt =
      if is_alive do
        "#{prefix || remote_prefix}(#{node})#{config.counter}> "
      else
        "#{prefix || "iex"}(#{config.counter})> "
      end

    case IO.gets(:stdio, prompt) do
      :eof -> :eof
      { :error, _ } -> ''
      data -> :unicode.characters_to_list(data)
    end
  end

  defp io_put(result) do
    IO.puts :stdio, IEx.color(:eval_result, inspect(result, IEx.Options.get(:inspect)))
  end

  defp io_error(result) do
    IO.puts :stdio, result
  end

  defp remote_prefix do
    if node == node(:erlang.group_leader), do: "iex", else: "rem"
  end

  defp print_exception(exception, stacktrace) do
    print_stacktrace stacktrace, fn ->
      "** (#{inspect exception.__record__(:name)}) #{exception.message}"
    end
  end

  defp print_error(kind, reason, stacktrace) do
    print_stacktrace stacktrace, fn ->
      "** (#{kind}) #{inspect(reason)}"
    end
  end

  defp print_stacktrace(trace, callback) do
    try do
      io_error callback.()
      io_error Exception.format_stacktrace(trace)
    catch
      _, _ ->
        io_error "** (IEx.Error) error when printing exception message and stacktrace"
    end
  end
end


Just 'nil' might have lived long enough, 'receive' definitly does.

defmodule Processes do
  use Koans

  @intro "Processes"

  koan "You are a process" do
    assert Process.alive?(self()) == ___
  end

  koan "You can ask a process to introduce itself" do
    information = Process.info(self())

    assert information[:status] == ___
  end

  koan "Processes are referenced by their process ID (pid)" do
    assert is_pid(self()) == ___
  end

  koan "New processes are spawned functions" do
    value = spawn(fn -> receive do
                      end
                end)

    assert is_pid(value) == ___
  end

  koan "Processes die when their function exits" do
    fast_process = spawn(fn -> :timer.sleep(10) end)
    slow_process = spawn(fn -> :timer.sleep(1000) end)

    # All spawned functions are executed concurrently with the current process.
    # You check back on slow_process and fast_process 50ms later. Let's
    # see if they are still alive!
    :timer.sleep(50)

    assert Process.alive?(fast_process) == ___
    assert Process.alive?(slow_process) == ___
  end

  koan "Processes can send and receive messages" do
    send self(), "hola!"

    receive do
      msg -> assert msg == ___
    end
  end

  koan "A process will wait forever for a message" do
    wait_forever = fn ->
      receive do
      end
    end

    pid = spawn(wait_forever)

    assert Process.alive?(pid) == ___
  end

  koan "Received messages are queued, first in first out" do
    send self(), "hola!"
    send self(), "como se llama?"

    assert_receive ___
    assert_receive ___
  end

  koan "A common pattern is to include the sender in the message, so that it can reply" do
    greeter = fn ->
      receive do
        {:hello, sender} -> send sender, :how_are_you?
      end
    end

    pid = spawn(greeter)

    send pid, {:hello, self()}
    assert_receive ___
  end

  def yelling_echo_loop do
    receive do
      {caller, value} ->
        send caller, String.upcase(value)
        yelling_echo_loop()
    end
  end

  koan "Use tail recursion to receive multiple messages" do
    pid = spawn_link(&yelling_echo_loop/0)

    send pid, {self(), "o"}
    assert_receive ___

    send pid, {self(), "hai"}
    assert_receive ___
  end

  def state(value) do
    receive do
      {caller, :get} ->
        send caller, value
        state(value)
      {caller, :set, new_value} ->
        state(new_value)
    end
  end

  koan "Processes can be used to hold state" do
    initial_state = "foo"
    pid = spawn(fn ->
      state(initial_state)
    end)

    send pid, {self(), :get}
    assert_receive ___

    send pid, {self(), :set, "bar"}
    send pid, {self(), :get}
    assert_receive ___
  end

  koan "Waiting for a message can get boring" do
    parent = self()
    spawn(fn -> receive do
                after
                  5 -> send parent, {:waited_too_long, "I am impatient"}
                end
           end)

    assert_receive ___
  end

  koan "Trapping will allow you to react to someone terminating the process" do
    parent = self()
    pid = spawn(fn ->
                      Process.flag(:trap_exit, true)
                      send parent, :ready
                      receive do
                        {:EXIT, _pid, reason} -> send parent, {:exited, reason}
                      end
              end)

    receive do
      :ready -> true
    end

    Process.exit(pid, :random_reason)

    assert_receive ___
  end

  koan "Parent processes can trap exits for children they are linked to" do
    Process.flag(:trap_exit, true)
    spawn_link(fn -> Process.exit(self(), :normal) end)

    assert_receive {:EXIT, _pid, ___}
  end

  koan "If you monitor your children, you'll be automatically informed for their depature" do
    spawn_monitor(fn -> Process.exit(self(), :normal) end)

    assert_receive {:DOWN, _ref, :process, _pid, ___}
  end
end

defmodule Processes do
  use Koans

  @intro "Processes"

  koan "You are a process" do
    assert Process.alive?(self()) == ___
  end

  koan "You can ask a process to introduce itself" do
    information = Process.info(self())

    assert information[:status] == ___
  end

  koan "Processes are referenced by their process ID (pid)" do
    assert is_pid(self()) == ___
  end

  koan "New processes are spawned functions" do
    value = spawn(fn -> nil end)

    assert is_pid(value) == ___
  end

  koan "Processes die when their function exits" do
    fast_process = spawn(fn -> :timer.sleep(10) end)
    slow_process = spawn(fn -> :timer.sleep(1000) end)

    # All spawned functions are executed concurrently with the current process.
    # You check back on slow_process and fast_process 50ms later. Let's
    # see if they are still alive!
    :timer.sleep(50)

    assert Process.alive?(fast_process) == ___
    assert Process.alive?(slow_process) == ___
  end

  koan "Processes can send and receive messages" do
    send self(), "hola!"

    receive do
      msg -> assert msg == ___
    end
  end

  koan "A process will wait forever for a message" do
    wait_forever = fn ->
      receive do
      end
    end

    pid = spawn(wait_forever)

    assert Process.alive?(pid) == ___
  end

  koan "Received messages are queued, first in first out" do
    send self(), "hola!"
    send self(), "como se llama?"

    assert_receive ___
    assert_receive ___
  end

  koan "A common pattern is to include the sender in the message, so that it can reply" do
    greeter = fn ->
      receive do
        {:hello, sender} -> send sender, :how_are_you?
      end
    end

    pid = spawn(greeter)

    send pid, {:hello, self()}
    assert_receive ___
  end

  def yelling_echo_loop do
    receive do
      {caller, value} ->
        send caller, String.upcase(value)
        yelling_echo_loop()
    end
  end

  koan "Use tail recursion to receive multiple messages" do
    pid = spawn_link(&yelling_echo_loop/0)

    send pid, {self(), "o"}
    assert_receive ___

    send pid, {self(), "hai"}
    assert_receive ___
  end

  def state(value) do
    receive do
      {caller, :get} ->
        send caller, value
        state(value)
      {caller, :set, new_value} ->
        state(new_value)
    end
  end

  koan "Processes can be used to hold state" do
    initial_state = "foo"
    pid = spawn(fn ->
      state(initial_state)
    end)

    send pid, {self(), :get}
    assert_receive ___

    send pid, {self(), :set, "bar"}
    send pid, {self(), :get}
    assert_receive ___
  end

  koan "Waiting for a message can get boring" do
    parent = self()
    spawn(fn -> receive do
                after
                  5 -> send parent, {:waited_too_long, "I am impatient"}
                end
           end)

    assert_receive ___
  end

  koan "Trapping will allow you to react to someone terminating the process" do
    parent = self()
    pid = spawn(fn ->
                      Process.flag(:trap_exit, true)
                      send parent, :ready
                      receive do
                        {:EXIT, _pid, reason} -> send parent, {:exited, reason}
                      end
              end)

    receive do
      :ready -> true
    end

    Process.exit(pid, :random_reason)

    assert_receive ___
  end

  koan "Parent processes can trap exits for children they are linked to" do
    Process.flag(:trap_exit, true)
    spawn_link(fn -> Process.exit(self(), :normal) end)

    assert_receive {:EXIT, _pid, ___}
  end

  koan "If you monitor your children, you'll be automatically informed for their depature" do
    spawn_monitor(fn -> Process.exit(self(), :normal) end)

    assert_receive {:DOWN, _ref, :process, _pid, ___}
  end
end