lib/ankh/http/response.ex

defmodule Ankh.HTTP.Response do
  @moduledoc """
  Ankh HTTP Response
  """

  alias Ankh.HTTP

  @typedoc "Response status"
  @type status :: pos_integer()

  @type t() :: %__MODULE__{
          status: status(),
          body: HTTP.body(),
          headers: HTTP.headers(),
          trailers: HTTP.headers(),
          body_fetched: boolean(),
          complete: boolean()
        }
  defstruct status: 200,
            body: [],
            headers: [],
            trailers: [],
            body_fetched: false,
            complete: false

  @spec new(Enum.t()) :: t()
  def new(attrs \\ []), do: struct(__MODULE__, attrs)

  @spec set_status(t(), status()) :: t()
  def set_status(response, status), do: %{response | status: status}

  @spec set_body(t(), iodata()) :: t()
  def set_body(response, body), do: %{response | body: body}

  @spec fetch_body(t()) :: t()
  def fetch_body(%__MODULE__{body_fetched: false, body: body} = response) do
    fetched_body =
      body
      |> Enum.reverse()
      |> IO.iodata_to_binary()

    %{response | body_fetched: true, body: fetched_body}
  end

  def fetch_body(%__MODULE__{body_fetched: true} = response), do: response

  @spec fetch_header_values(t(), HTTP.header_name()) :: [HTTP.header_value()]
  defdelegate fetch_header_values(request, header), to: HTTP

  @spec fetch_trailer_values(t(), HTTP.header_name()) :: [HTTP.header_value()]
  defdelegate fetch_trailer_values(request, trailer), to: HTTP

  @spec put_header(t(), HTTP.header_name(), HTTP.header_value()) :: t()
  defdelegate put_header(request, name, value), to: HTTP

  @spec put_headers(t(), HTTP.headers()) :: t()
  defdelegate put_headers(request, headers), to: HTTP

  @spec put_trailer(t(), HTTP.header_name(), HTTP.header_value()) :: t()
  defdelegate put_trailer(request, name, value), to: HTTP

  @spec put_trailers(t(), HTTP.headers()) :: t()
  defdelegate put_trailers(request, trailers), to: HTTP

  @spec validate_body(t()) :: {:ok, t()} | :error
  defdelegate validate_body(response), to: HTTP

  @spec validate_headers(HTTP.headers(), boolean(), [HTTP.header_name()]) ::
          :ok | {:error, :protocol_error}
  def validate_headers(headers, strict, forbidden \\ []),
    do: do_validate_headers(headers, strict, forbidden, false, false)

  defp do_validate_headers(
         [],
         _strict,
         _forbidden,
         true = _status,
         _end_pseudo
       ),
       do: :ok

  defp do_validate_headers([], _strict, _forbidden, _status, _end_pseudo),
    do: {:error, :protocol_error}

  defp do_validate_headers(
         [{":status", value} | rest],
         strict,
         forbidden,
         false = _status,
         false = _end_pseudo
       ) do
    case Integer.parse(value) do
      {status, ""} when status in 100..599 ->
        do_validate_headers(rest, strict, forbidden, true, false)

      _ ->
        {:error, :protocol_error}
    end
  end

  defp do_validate_headers(
         [{":" <> _pseaudo_header, _value} | _rest],
         _strict,
         _forbidden,
         _stats,
         _end_pseudo
       ),
       do: {:error, :protocol_error}

  defp do_validate_headers([{header, value} | rest], strict, forbidden, stats, _end_pseudo) do
    case {String.downcase(header), value} do
      {"te", value} when value != "trailers" ->
        {:error, :protocol_error}

      {name, _value} ->
        if name not in forbidden and HTTP.header_name_valid?(header, strict) do
          do_validate_headers(rest, strict, forbidden, stats, true)
        else
          {:error, :protocol_error}
        end
    end
  end
end