lib/maxwell/adapter/util.ex

defmodule Maxwell.Adapter.Util do
  @moduledoc """
  Utils for Adapter
  """

  @chunk_size 4 * 1024 * 1024
  alias Maxwell.{Conn, Multipart, Query}

  @doc """
  Append path and query string to url

  * `url`   - `conn.url`
  * `path`  - `conn.path`
  * `query` - `conn.query`
  * `type`  - `:char_list` or `:string`, default is :string

  ### Examples

      #http://example.com/home?name=foo
      iex> url_serialize("http://example.com", "/home", %{"name" => "foo"})

  """
  def url_serialize(url, path, query_string, type \\ :string) do
    url = url |> append_query_string(path, query_string)

    case type do
      :string -> url
      :char_list -> url |> to_charlist
    end
  end

  @doc """
  Converts the headers map to a list of tuples.

     * `headers`   - `Map.t`, for example: `%{"content-type" => "application/json"}`

  ### Examples

       iex> headers_serialize(%{"content-type" => "application/json"})
       [{"content-type", "application/json"}]
  """
  def header_serialize(headers) do
    Enum.into(headers, [])
  end

  @doc """
  Add `content-type` to headers if don't have;
  Add `content-length` to headers if not chunked

     * `chunked`  - `boolean`, is chunked mode
     * `conn`  - `Maxwell.Conn`

  """
  def file_header_transform(chunked, conn) do
    %Conn{req_body: {:file, filepath}, req_headers: req_headers} = conn

    req_headers =
      case Map.has_key?(req_headers, "content-type") do
        true ->
          req_headers

        false ->
          content_type =
            filepath
            |> Path.extname()
            |> String.trim_leading(".")
            |> MIME.type()

          conn
          |> Conn.put_req_header("content-type", content_type)
          |> Map.get(:req_headers)
      end

    case chunked or Map.has_key?(req_headers, "content-length") do
      true ->
        req_headers

      false ->
        len = :filelib.file_size(filepath)

        conn
        |> Conn.put_req_header("content-length", len)
        |> Map.get(:req_headers)
    end
  end

  @doc """
  Check req_headers has transfer-encoding: chunked.

     * `conn`  - `Maxwell.Conn`

  """
  def chunked?(conn) do
    case Conn.get_req_header(conn, "transfer-encoding") do
      nil -> false
      type -> "chunked" == String.downcase(type)
    end
  end

  @doc """
  Encode multipart form.

    * `conn`  - `Maxwell.Conn`
    * `multiparts` - see `Maxwell.Multipart.encode_form/2`

  """
  def multipart_encode(conn, multiparts) do
    boundary = Multipart.new_boundary()
    body = {&multipart_body/1, {:start, boundary, multiparts}}

    len = Multipart.len_mp_stream(boundary, multiparts)

    headers =
      conn
      |> Conn.put_req_header("content-type", "multipart/form-data; boundary=#{boundary}")
      |> Conn.put_req_header("content-length", len)
      |> Map.get(:req_headers)

    {headers, body}
  end

  @doc """
  Fetch the first element from stream.

  """
  def stream_iterate(filepath) when is_binary(filepath) do
    filepath
    |> File.stream!([], @chunk_size)
    |> stream_iterate
  end

  def stream_iterate(next_stream_fun) when is_function(next_stream_fun, 1) do
    case next_stream_fun.({:cont, nil}) do
      {:suspended, item, next_stream_fun} -> {:ok, item, next_stream_fun}
      {:halted, _} -> :eof
      {:done, _} -> :eof
    end
  end

  def stream_iterate(stream) do
    case Enumerable.reduce(stream, {:cont, nil}, fn item, nil -> {:suspend, item} end) do
      {:suspended, item, next_stream} -> {:ok, item, next_stream}
      {:done, _} -> :eof
      {:halted, _} -> :eof
    end
  end

  defp multipart_body({:start, boundary, multiparts}) do
    {body, _size} = Multipart.encode_form(boundary, multiparts)
    {:ok, body, :end}
  end

  defp multipart_body(:end), do: :eof

  defp append_query_string(url, path, query) when query == %{}, do: url <> path

  defp append_query_string(url, path, query) do
    query_string = Query.encode(query)
    url <> path <> "?" <> query_string
  end
end