Ignore dialyzer on gun_open/4 To handle gun_opts with keys from master :gun branch

if Code.ensure_loaded?(:gun) do
  defmodule Tesla.Adapter.Gun do
    @moduledoc """
    Adapter for [gun](https://github.com/ninenines/gun).

    Remember to add `{:gun, "~> 1.3"}` to dependencies.
    In version 1.3 gun sends `host` header with port. Fixed in master branch.
    Also, you need to recompile tesla after adding `:gun` dependency:

    ```
    mix deps.clean tesla
    mix deps.compile tesla
    ```

    ## Example usage

    ```
    # set globally in config/config.exs
    config :tesla, :adapter, Tesla.Adapter.Gun

    # set per module
    defmodule MyClient do
      use Tesla
      adapter Tesla.Adapter.Gun
    end
    ```

    ## Adapter specific options

    - `:timeout` - Time, while process, will wait for gun messages.
    - `:body_as` - What will be returned in `%Tesla.Env{}` body key. Possible values - `:plain`, `:stream`, `:chunks`. Defaults to `:plain`.
        - `:plain` - as binary.
        - `:stream` - as stream. If you don't want to close connection (because you want to reuse it later) pass `close_conn: false` in adapter opts.
        - `:chunks` - as chunks. You can get response body in chunks using `Tesla.Adapter.Gun.read_chunk/3` function.
        Processing of the chunks and checking body size must be done by yourself. Example of processing function is in `test/tesla/adapter/gun_test.exs` - `Tesla.Adapter.GunTest.read_body/4`. If you don't need connection later don't forget to close it with `Tesla.Adapter.Gun.close/1`.
    - `:max_body` - Max response body size in bytes. Works only with `body_as: :plain`, with other settings you need to check response body size by yourself.
    - `:conn` - Opened connection pid with gun. Is used for reusing gun connections.
    - `:close_conn` - Close connection or not after receiving full response body. Is used for reusing gun connections. Defaults to `true`.
    - `:certificates_verification` - Add SSL certificates verification. [erlang-certifi](https://github.com/certifi/erlang-certifi) [ssl_verify_fun.erl](https://github.com/deadtrickster/ssl_verify_fun.erl)
    - `:proxy` - Proxy for requests. **Socks proxy are supported only for gun master branch**. Examples: `{'localhost', 1234}`, `{{127, 0, 0, 1}, 1234}`, `{:socks5, 'localhost', 1234}`.

    ## [Gun options](https://ninenines.eu/docs/en/gun/1.3/manual/gun/)

    - `:connect_timeout` - Connection timeout.
    - `:http_opts` - Options specific to the HTTP protocol.
    - `:http2_opts` -  Options specific to the HTTP/2 protocol.
    - `:protocols` - Ordered list of preferred protocols. Defaults: `[:http2, :http]`- for :tls, `[:http]` - for :tcp.
    - `:trace` - Whether to enable dbg tracing of the connection process. Should only be used during debugging. Default: false.
    - `:transport` - Whether to use TLS or plain TCP. The default varies depending on the port used. Port 443 defaults to tls. All other ports default to tcp.
    - `:transport_opts` - Transport options. They are TCP options or TLS options depending on the selected transport. Default: `[]`. Gun version: 1.3
    - `:tls_opts` - TLS transport options. Default: `[]`. Gun from master branch.
    - `:tcp_opts` - TCP trasnport options. Default: `[]`. Gun from master branch.
    - `:socks_opts` - Options for socks. Default: `[]`. Gun from master branch.
    - `:ws_opts` - Options specific to the Websocket protocol. Default: `%{}`.
        - `:compress` - Whether to enable permessage-deflate compression. This does not guarantee that compression will be used as it is the server that ultimately decides. Defaults to false.
        - `:protocols` - A non-empty list enables Websocket protocol negotiation. The list of protocols will be sent in the sec-websocket-protocol request header. The handler module interface is currently undocumented and must be set to `gun_ws_h`.
    """
    @behaviour Tesla.Adapter
    alias Tesla.Multipart

    # TODO: update list after update to gun 2.0
    @gun_keys [
      :connect_timeout,
      :http_opts,
      :http2_opts,
      :protocols,
      :retry,
      :retry_timeout,
      :trace,
      :transport,
      :socks_opts,
      :ws_opts
    ]

    @default_timeout 1_000

    @impl Tesla.Adapter
    def call(env, opts) do
      with {:ok, status, headers, body} <- request(env, opts) do
        {:ok, %{env | status: status, headers: format_headers(headers), body: body}}
      end
    end

    @doc """
    Reads chunk of the response body.

    Returns `{:fin, binary()}` if all body received, otherwise returns `{:nofin, binary()}`.
    """
    @spec read_chunk(pid(), reference(), keyword() | map()) ::
            {:fin, binary()} | {:nofin, binary()} | {:error, atom()}
    def read_chunk(pid, stream, opts) do
      with {status, _} = chunk when status in [:fin, :error] <- do_read_chunk(pid, stream, opts) do
        if opts[:close_conn], do: close(pid)
        chunk
      end
    end

    defp do_read_chunk(pid, stream, opts) do
      receive do
        {:gun_data, ^pid, ^stream, :fin, body} ->
          {:fin, body}

        {:gun_data, ^pid, ^stream, :nofin, part} ->
          {:nofin, part}

        {:DOWN, _, _, _, reason} ->
          {:error, reason}
      after
        opts[:timeout] || @default_timeout ->
          {:error, :recv_chunk_timeout}
      end
    end

    @doc """
    Brutally close the `gun` connection.
    """
    @spec close(pid()) :: :ok
    defdelegate close(pid), to: :gun

    defp format_headers(headers) do
      for {key, value} <- headers do
        {String.downcase(to_string(key)), to_string(value)}
      end
    end

    defp request(env, opts) do
      request(
        Tesla.Adapter.Shared.format_method(env.method),
        Tesla.build_url(env.url, env.query),
        format_headers(env.headers),
        env.body || "",
        Tesla.Adapter.opts(
          [close_conn: true, body_as: :plain, send_body: :at_once, receive: true],
          env,
          opts
        )
        |> Enum.into(%{})
      )
    end

    defp request(method, url, headers, %Stream{} = body, opts),
      do: do_request(method, url, headers, body, Map.put(opts, :send_body, :stream))

    defp request(method, url, headers, body, opts) when is_function(body),
      do: do_request(method, url, headers, body, Map.put(opts, :send_body, :stream))

    defp request(method, url, headers, %Multipart{} = mp, opts) do
      headers = headers ++ Multipart.headers(mp)
      body = Multipart.body(mp)

      do_request(method, url, headers, body, Map.put(opts, :send_body, :stream))
    end

    defp request(method, url, headers, body, opts),
      do: do_request(method, url, headers, body, opts)

    defp do_request(method, url, headers, body, opts) do
      uri = URI.parse(url)
      path = Tesla.Adapter.Shared.prepare_path(uri.path, uri.query)

      with {:ok, pid, opts} <- open_conn(uri, opts) do
        stream = open_stream(pid, method, path, headers, body, opts)
        response = read_response(pid, stream, opts)

        if opts[:close_conn] and opts[:body_as] not in [:stream, :chunks] do
          close(pid)
        end

        response
      end
    end

    @dialyzer [{:nowarn_function, open_conn: 2}, :no_match]
    defp open_conn(%{scheme: scheme, host: host, port: port}, %{conn: conn} = opts)
         when is_pid(conn) do
      info = :gun.info(conn)

      conn_scheme =
        case info do
          # gun master branch support, which has `origin_scheme` in connection info
          %{origin_scheme: scheme} -> scheme
          %{transport: :tls} -> "https"
          _ -> "http"
        end

      conn_host =
        case :inet.ntoa(info.origin_host) do
          {:error, :einval} -> info.origin_host
          ip -> ip
        end

      if conn_scheme == scheme and to_string(conn_host) == host and info.origin_port == port do
        {:ok, conn, Map.put(opts, :receive, false)}
      else
        {:error, :invalid_conn}
      end
    end

    defp open_conn(uri, opts) do
      opts = maybe_add_transport(uri, opts)

      tls_opts =
        if uri.scheme == "https" do
          opts
          |> fetch_tls_opts()
          |> maybe_add_verify_options(opts, uri)
        else
          []
        end

      gun_opts = Map.take(opts, @gun_keys)

      with {:ok, conn} <- do_open_conn(uri, opts, gun_opts, tls_opts) do
        {:ok, conn, opts}
      end
    end

    defp maybe_add_transport(%URI{scheme: "https"}, opts), do: Map.put(opts, :transport, :tls)
    defp maybe_add_transport(_, opts), do: opts

    # Support for gun master branch where transport_opts, were splitted to tls_opts and tcp_opts
    # https://github.com/ninenines/gun/blob/491ddf58c0e14824a741852fdc522b390b306ae2/doc/src/manual/gun.asciidoc#changelog
    # TODO: remove after update to gun 2.0
    defp fetch_tls_opts(%{tls_opts: tls_opts}) when is_list(tls_opts), do: tls_opts
    defp fetch_tls_opts(%{transport_opts: tls_opts}) when is_list(tls_opts), do: tls_opts
    defp fetch_tls_opts(_), do: []

    defp maybe_add_verify_options(tls_opts, %{certificates_verification: true}, %{host: host}) do
      charlist =
        host
        |> to_charlist()
        |> :idna.encode()

      security_opts = [
        verify: :verify_peer,
        cacertfile: CAStore.file_path(),
        depth: 20,
        reuse_sessions: false,
        verify_fun: {&:ssl_verify_hostname.verify_fun/3, [check_hostname: charlist]}
      ]

      Keyword.merge(security_opts, tls_opts)
    end

    defp maybe_add_verify_options(tls_opts, _, _), do: tls_opts

    defp do_open_conn(uri, %{proxy: {proxy_host, proxy_port}}, gun_opts, tls_opts) do
      connect_opts =
        uri
        |> tunnel_opts()
        |> tunnel_tls_opts(uri.scheme, tls_opts)

      with {:ok, pid} <- :gun.open(proxy_host, proxy_port, gun_opts),
           {:ok, _} <- :gun.await_up(pid),
           stream <- :gun.connect(pid, connect_opts),
           {:response, :fin, 200, _} <- :gun.await(pid, stream) do
        {:ok, pid}
      end
    end

    defp do_open_conn(uri, %{proxy: {proxy_type, proxy_host, proxy_port}}, gun_opts, tls_opts) do
      version =
        proxy_type
        |> to_string()
        |> String.last()
        |> case do
          "4" -> 4
          _ -> 5
        end

      socks_opts =
        uri
        |> tunnel_opts()
        |> tunnel_tls_opts(uri.scheme, tls_opts)
        |> Map.put(:version, version)

      gun_opts =
        gun_opts
        |> Map.put(:protocols, [:socks])
        |> Map.update(:socks_opts, socks_opts, &Map.merge(socks_opts, &1))

      with {:ok, pid} <- :gun.open(proxy_host, proxy_port, gun_opts),
           {:ok, _} <- :gun.await_up(pid) do
        {:ok, pid}
      else
        {:error, {:options, {:protocols, [:socks]}}} ->
          {:error, "socks protocol is not supported"}

        error ->
          error
      end
    end

    defp do_open_conn(uri, opts, gun_opts, tls_opts) do
      tcp_opts = Map.get(opts, :tcp_opts, [])

      # if gun used from master
      opts_with_master_keys =
        gun_opts
        |> Map.put(:tls_opts, tls_opts)
        |> Map.put(:tcp_opts, tcp_opts)

      host = domain_or_ip(uri.host)

      with {:ok, pid} <- gun_open(host, uri.port, opts_with_master_keys, opts) do
        {:ok, pid}
      else
        {:error, {:options, {key, _}}} when key in [:tcp_opts, :tls_opts] ->
          gun_open(host, uri.port, Map.put(gun_opts, :transport_opts, tls_opts), opts)

        error ->
          error
      end
    end

    @dialyzer [{:nowarn_function, gun_open: 4}, :no_match]
    defp gun_open(host, port, gun_opts, opts) do
      with {:ok, pid} <- :gun.open(host, port, gun_opts),
           {_, true, _} <- {:receive, opts[:receive], pid},
           {_, {:ok, _}, _} <- {:up, :gun.await_up(pid), pid} do
        {:ok, pid}
      else
        {:receive, false, pid} ->
          {:ok, pid}

        {:up, error, pid} ->
          close(pid)
          error

        error ->
          error
      end
    end

    defp tunnel_opts(uri) do
      host = domain_or_ip(uri.host)
      %{host: host, port: uri.port}
    end

    defp tunnel_tls_opts(opts, "https", tls_opts) do
      http2_opts = %{protocols: [:http2], transport: :tls, tls_opts: tls_opts}
      Map.merge(opts, http2_opts)
    end

    defp tunnel_tls_opts(opts, _, _), do: opts

    defp open_stream(pid, method, path, headers, body, opts) do
      req_opts = %{reply_to: opts[:reply_to] || self()}

      open_stream(pid, method, path, headers, body, req_opts, opts[:send_body])
    end

    defp open_stream(pid, method, path, headers, body, req_opts, :stream) do
      stream = :gun.request(pid, method, path, headers, "", req_opts)
      for data <- body, do: :ok = :gun.data(pid, stream, :nofin, data)
      :gun.data(pid, stream, :fin, "")
      stream
    end

    defp open_stream(pid, method, path, headers, body, req_opts, :at_once),
      do: :gun.request(pid, method, path, headers, body, req_opts)

    defp read_response(pid, stream, opts) do
      receive? = opts[:receive]

      receive do
        {:gun_response, ^pid, ^stream, :fin, status, headers} ->
          {:ok, status, headers, ""}

        {:gun_response, ^pid, ^stream, :nofin, status, headers} ->
          format_response(pid, stream, opts, status, headers, opts[:body_as])

        {:gun_up, ^pid, _protocol} when receive? ->
          read_response(pid, stream, opts)

        {:gun_error, ^pid, reason} ->
          {:error, reason}

        {:gun_down, ^pid, _, _, _, _} when receive? ->
          read_response(pid, stream, opts)

        {:DOWN, _, _, _, reason} ->
          {:error, reason}
      after
        opts[:timeout] || @default_timeout ->
          {:error, :recv_response_timeout}
      end
    end

    defp format_response(pid, stream, opts, status, headers, :plain) do
      case read_body(pid, stream, opts) do
        {:ok, body} ->
          {:ok, status, headers, body}

        {:error, error} ->
          # prevent gun sending messages to owner process, if body is too large and connection is not closed
          :ok = :gun.flush(stream)

          {:error, error}
      end
    end

    defp format_response(pid, stream, opts, status, headers, :stream) do
      stream_body =
        Stream.resource(
          fn -> %{pid: pid, stream: stream} end,
          fn
            %{pid: pid, stream: stream} ->
              case read_chunk(pid, stream, opts) do
                {:nofin, part} -> {[part], %{pid: pid, stream: stream}}
                {:fin, body} -> {[body], %{pid: pid, final: :fin}}
              end

            %{pid: pid, final: :fin} ->
              {:halt, %{pid: pid}}
          end,
          fn %{pid: pid} ->
            if opts[:close_conn], do: close(pid)
          end
        )

      {:ok, status, headers, stream_body}
    end

    defp format_response(pid, stream, opts, status, headers, :chunks) do
      {:ok, status, headers, %{pid: pid, stream: stream, opts: Enum.into(opts, [])}}
    end

    defp read_body(pid, stream, opts, acc \\ "") do
      limit = opts[:max_body]

      receive do
        {:gun_data, ^pid, ^stream, :fin, body} ->
          check_body_size(acc, body, limit)

        {:gun_data, ^pid, ^stream, :nofin, part} ->
          with {:ok, acc} <- check_body_size(acc, part, limit) do
            read_body(pid, stream, opts, acc)
          end

        {:DOWN, _, _, _, reason} ->
          {:error, reason}
      after
        opts[:timeout] || @default_timeout ->
          {:error, :recv_body_timeout}
      end
    end

    defp check_body_size(acc, part, nil), do: {:ok, acc <> part}

    defp check_body_size(acc, part, limit) do
      body = acc <> part

      if limit - byte_size(body) >= 0 do
        {:ok, body}
      else
        {:error, :body_too_large}
      end
    end

    defp domain_or_ip(host) do
      charlist = to_charlist(host)

      case :inet.parse_address(charlist) do
        {:error, :einval} ->
          :idna.encode(charlist)

        {:ok, ip} ->
          ip
      end
    end
  end
end

if Code.ensure_loaded?(:gun) do
  defmodule Tesla.Adapter.Gun do
    @moduledoc """
    Adapter for [gun](https://github.com/ninenines/gun).

    Remember to add `{:gun, "~> 1.3"}` to dependencies.
    In version 1.3 gun sends `host` header with port. Fixed in master branch.
    Also, you need to recompile tesla after adding `:gun` dependency:

    ```
    mix deps.clean tesla
    mix deps.compile tesla
    ```

    ## Example usage

    ```
    # set globally in config/config.exs
    config :tesla, :adapter, Tesla.Adapter.Gun

    # set per module
    defmodule MyClient do
      use Tesla
      adapter Tesla.Adapter.Gun
    end
    ```

    ## Adapter specific options

    - `:timeout` - Time, while process, will wait for gun messages.
    - `:body_as` - What will be returned in `%Tesla.Env{}` body key. Possible values - `:plain`, `:stream`, `:chunks`. Defaults to `:plain`.
        - `:plain` - as binary.
        - `:stream` - as stream. If you don't want to close connection (because you want to reuse it later) pass `close_conn: false` in adapter opts.
        - `:chunks` - as chunks. You can get response body in chunks using `Tesla.Adapter.Gun.read_chunk/3` function.
        Processing of the chunks and checking body size must be done by yourself. Example of processing function is in `test/tesla/adapter/gun_test.exs` - `Tesla.Adapter.GunTest.read_body/4`. If you don't need connection later don't forget to close it with `Tesla.Adapter.Gun.close/1`.
    - `:max_body` - Max response body size in bytes. Works only with `body_as: :plain`, with other settings you need to check response body size by yourself.
    - `:conn` - Opened connection pid with gun. Is used for reusing gun connections.
    - `:close_conn` - Close connection or not after receiving full response body. Is used for reusing gun connections. Defaults to `true`.
    - `:certificates_verification` - Add SSL certificates verification. [erlang-certifi](https://github.com/certifi/erlang-certifi) [ssl_verify_fun.erl](https://github.com/deadtrickster/ssl_verify_fun.erl)
    - `:proxy` - Proxy for requests. **Socks proxy are supported only for gun master branch**. Examples: `{'localhost', 1234}`, `{{127, 0, 0, 1}, 1234}`, `{:socks5, 'localhost', 1234}`.

    ## [Gun options](https://ninenines.eu/docs/en/gun/1.3/manual/gun/)

    - `:connect_timeout` - Connection timeout.
    - `:http_opts` - Options specific to the HTTP protocol.
    - `:http2_opts` -  Options specific to the HTTP/2 protocol.
    - `:protocols` - Ordered list of preferred protocols. Defaults: `[:http2, :http]`- for :tls, `[:http]` - for :tcp.
    - `:trace` - Whether to enable dbg tracing of the connection process. Should only be used during debugging. Default: false.
    - `:transport` - Whether to use TLS or plain TCP. The default varies depending on the port used. Port 443 defaults to tls. All other ports default to tcp.
    - `:transport_opts` - Transport options. They are TCP options or TLS options depending on the selected transport. Default: `[]`. Gun version: 1.3
    - `:tls_opts` - TLS transport options. Default: `[]`. Gun from master branch.
    - `:tcp_opts` - TCP trasnport options. Default: `[]`. Gun from master branch.
    - `:socks_opts` - Options for socks. Default: `[]`. Gun from master branch.
    - `:ws_opts` - Options specific to the Websocket protocol. Default: `%{}`.
        - `:compress` - Whether to enable permessage-deflate compression. This does not guarantee that compression will be used as it is the server that ultimately decides. Defaults to false.
        - `:protocols` - A non-empty list enables Websocket protocol negotiation. The list of protocols will be sent in the sec-websocket-protocol request header. The handler module interface is currently undocumented and must be set to `gun_ws_h`.
    """
    @behaviour Tesla.Adapter
    alias Tesla.Multipart

    # TODO: update list after update to gun 2.0
    @gun_keys [
      :connect_timeout,
      :http_opts,
      :http2_opts,
      :protocols,
      :retry,
      :retry_timeout,
      :trace,
      :transport,
      :socks_opts,
      :ws_opts
    ]

    @default_timeout 1_000

    @impl Tesla.Adapter
    def call(env, opts) do
      with {:ok, status, headers, body} <- request(env, opts) do
        {:ok, %{env | status: status, headers: format_headers(headers), body: body}}
      end
    end

    @doc """
    Reads chunk of the response body.

    Returns `{:fin, binary()}` if all body received, otherwise returns `{:nofin, binary()}`.
    """
    @spec read_chunk(pid(), reference(), keyword() | map()) ::
            {:fin, binary()} | {:nofin, binary()} | {:error, atom()}
    def read_chunk(pid, stream, opts) do
      with {status, _} = chunk when status in [:fin, :error] <- do_read_chunk(pid, stream, opts) do
        if opts[:close_conn], do: close(pid)
        chunk
      end
    end

    defp do_read_chunk(pid, stream, opts) do
      receive do
        {:gun_data, ^pid, ^stream, :fin, body} ->
          {:fin, body}

        {:gun_data, ^pid, ^stream, :nofin, part} ->
          {:nofin, part}

        {:DOWN, _, _, _, reason} ->
          {:error, reason}
      after
        opts[:timeout] || @default_timeout ->
          {:error, :recv_chunk_timeout}
      end
    end

    @doc """
    Brutally close the `gun` connection.
    """
    @spec close(pid()) :: :ok
    defdelegate close(pid), to: :gun

    defp format_headers(headers) do
      for {key, value} <- headers do
        {String.downcase(to_string(key)), to_string(value)}
      end
    end

    defp request(env, opts) do
      request(
        Tesla.Adapter.Shared.format_method(env.method),
        Tesla.build_url(env.url, env.query),
        format_headers(env.headers),
        env.body || "",
        Tesla.Adapter.opts(
          [close_conn: true, body_as: :plain, send_body: :at_once, receive: true],
          env,
          opts
        )
        |> Enum.into(%{})
      )
    end

    defp request(method, url, headers, %Stream{} = body, opts),
      do: do_request(method, url, headers, body, Map.put(opts, :send_body, :stream))

    defp request(method, url, headers, body, opts) when is_function(body),
      do: do_request(method, url, headers, body, Map.put(opts, :send_body, :stream))

    defp request(method, url, headers, %Multipart{} = mp, opts) do
      headers = headers ++ Multipart.headers(mp)
      body = Multipart.body(mp)

      do_request(method, url, headers, body, Map.put(opts, :send_body, :stream))
    end

    defp request(method, url, headers, body, opts),
      do: do_request(method, url, headers, body, opts)

    defp do_request(method, url, headers, body, opts) do
      uri = URI.parse(url)
      path = Tesla.Adapter.Shared.prepare_path(uri.path, uri.query)

      with {:ok, pid, opts} <- open_conn(uri, opts) do
        stream = open_stream(pid, method, path, headers, body, opts)
        response = read_response(pid, stream, opts)

        if opts[:close_conn] and opts[:body_as] not in [:stream, :chunks] do
          close(pid)
        end

        response
      end
    end

    @dialyzer [{:nowarn_function, open_conn: 2}, :no_match]
    defp open_conn(%{scheme: scheme, host: host, port: port}, %{conn: conn} = opts)
         when is_pid(conn) do
      info = :gun.info(conn)

      conn_scheme =
        case info do
          # gun master branch support, which has `origin_scheme` in connection info
          %{origin_scheme: scheme} -> scheme
          %{transport: :tls} -> "https"
          _ -> "http"
        end

      conn_host =
        case :inet.ntoa(info.origin_host) do
          {:error, :einval} -> info.origin_host
          ip -> ip
        end

      if conn_scheme == scheme and to_string(conn_host) == host and info.origin_port == port do
        {:ok, conn, Map.put(opts, :receive, false)}
      else
        {:error, :invalid_conn}
      end
    end

    defp open_conn(uri, opts) do
      opts = maybe_add_transport(uri, opts)

      tls_opts =
        if uri.scheme == "https" do
          opts
          |> fetch_tls_opts()
          |> maybe_add_verify_options(opts, uri)
        else
          []
        end

      gun_opts = Map.take(opts, @gun_keys)

      with {:ok, conn} <- do_open_conn(uri, opts, gun_opts, tls_opts) do
        {:ok, conn, opts}
      end
    end

    defp maybe_add_transport(%URI{scheme: "https"}, opts), do: Map.put(opts, :transport, :tls)
    defp maybe_add_transport(_, opts), do: opts

    # Support for gun master branch where transport_opts, were splitted to tls_opts and tcp_opts
    # https://github.com/ninenines/gun/blob/491ddf58c0e14824a741852fdc522b390b306ae2/doc/src/manual/gun.asciidoc#changelog
    # TODO: remove after update to gun 2.0
    defp fetch_tls_opts(%{tls_opts: tls_opts}) when is_list(tls_opts), do: tls_opts
    defp fetch_tls_opts(%{transport_opts: tls_opts}) when is_list(tls_opts), do: tls_opts
    defp fetch_tls_opts(_), do: []

    defp maybe_add_verify_options(tls_opts, %{certificates_verification: true}, %{host: host}) do
      charlist =
        host
        |> to_charlist()
        |> :idna.encode()

      security_opts = [
        verify: :verify_peer,
        cacertfile: CAStore.file_path(),
        depth: 20,
        reuse_sessions: false,
        verify_fun: {&:ssl_verify_hostname.verify_fun/3, [check_hostname: charlist]}
      ]

      Keyword.merge(security_opts, tls_opts)
    end

    defp maybe_add_verify_options(tls_opts, _, _), do: tls_opts

    defp do_open_conn(uri, %{proxy: {proxy_host, proxy_port}}, gun_opts, tls_opts) do
      connect_opts =
        uri
        |> tunnel_opts()
        |> tunnel_tls_opts(uri.scheme, tls_opts)

      with {:ok, pid} <- :gun.open(proxy_host, proxy_port, gun_opts),
           {:ok, _} <- :gun.await_up(pid),
           stream <- :gun.connect(pid, connect_opts),
           {:response, :fin, 200, _} <- :gun.await(pid, stream) do
        {:ok, pid}
      end
    end

    defp do_open_conn(uri, %{proxy: {proxy_type, proxy_host, proxy_port}}, gun_opts, tls_opts) do
      version =
        proxy_type
        |> to_string()
        |> String.last()
        |> case do
          "4" -> 4
          _ -> 5
        end

      socks_opts =
        uri
        |> tunnel_opts()
        |> tunnel_tls_opts(uri.scheme, tls_opts)
        |> Map.put(:version, version)

      gun_opts =
        gun_opts
        |> Map.put(:protocols, [:socks])
        |> Map.update(:socks_opts, socks_opts, &Map.merge(socks_opts, &1))

      with {:ok, pid} <- :gun.open(proxy_host, proxy_port, gun_opts),
           {:ok, _} <- :gun.await_up(pid) do
        {:ok, pid}
      else
        {:error, {:options, {:protocols, [:socks]}}} ->
          {:error, "socks protocol is not supported"}

        error ->
          error
      end
    end

    defp do_open_conn(uri, opts, gun_opts, tls_opts) do
      tcp_opts = Map.get(opts, :tcp_opts, [])

      # if gun used from master
      opts_with_master_keys =
        gun_opts
        |> Map.put(:tls_opts, tls_opts)
        |> Map.put(:tcp_opts, tcp_opts)

      host = domain_or_ip(uri.host)

      with {:ok, pid} <- gun_open(host, uri.port, opts_with_master_keys, opts) do
        {:ok, pid}
      else
        {:error, {:options, {key, _}}} when key in [:tcp_opts, :tls_opts] ->
          gun_open(host, uri.port, Map.put(gun_opts, :transport_opts, tls_opts), opts)

        error ->
          error
      end
    end

    defp gun_open(host, port, gun_opts, opts) do
      with {:ok, pid} <- :gun.open(host, port, gun_opts),
           {_, true, _} <- {:receive, opts[:receive], pid},
           {_, {:ok, _}, _} <- {:up, :gun.await_up(pid), pid} do
        {:ok, pid}
      else
        {:receive, false, pid} ->
          {:ok, pid}

        {:up, error, pid} ->
          close(pid)
          error

        error ->
          error
      end
    end

    defp tunnel_opts(uri) do
      host = domain_or_ip(uri.host)
      %{host: host, port: uri.port}
    end

    defp tunnel_tls_opts(opts, "https", tls_opts) do
      http2_opts = %{protocols: [:http2], transport: :tls, tls_opts: tls_opts}
      Map.merge(opts, http2_opts)
    end

    defp tunnel_tls_opts(opts, _, _), do: opts

    defp open_stream(pid, method, path, headers, body, opts) do
      req_opts = %{reply_to: opts[:reply_to] || self()}

      open_stream(pid, method, path, headers, body, req_opts, opts[:send_body])
    end

    defp open_stream(pid, method, path, headers, body, req_opts, :stream) do
      stream = :gun.request(pid, method, path, headers, "", req_opts)
      for data <- body, do: :ok = :gun.data(pid, stream, :nofin, data)
      :gun.data(pid, stream, :fin, "")
      stream
    end

    defp open_stream(pid, method, path, headers, body, req_opts, :at_once),
      do: :gun.request(pid, method, path, headers, body, req_opts)

    defp read_response(pid, stream, opts) do
      receive? = opts[:receive]

      receive do
        {:gun_response, ^pid, ^stream, :fin, status, headers} ->
          {:ok, status, headers, ""}

        {:gun_response, ^pid, ^stream, :nofin, status, headers} ->
          format_response(pid, stream, opts, status, headers, opts[:body_as])

        {:gun_up, ^pid, _protocol} when receive? ->
          read_response(pid, stream, opts)

        {:gun_error, ^pid, reason} ->
          {:error, reason}

        {:gun_down, ^pid, _, _, _, _} when receive? ->
          read_response(pid, stream, opts)

        {:DOWN, _, _, _, reason} ->
          {:error, reason}
      after
        opts[:timeout] || @default_timeout ->
          {:error, :recv_response_timeout}
      end
    end

    defp format_response(pid, stream, opts, status, headers, :plain) do
      case read_body(pid, stream, opts) do
        {:ok, body} ->
          {:ok, status, headers, body}

        {:error, error} ->
          # prevent gun sending messages to owner process, if body is too large and connection is not closed
          :ok = :gun.flush(stream)

          {:error, error}
      end
    end

    defp format_response(pid, stream, opts, status, headers, :stream) do
      stream_body =
        Stream.resource(
          fn -> %{pid: pid, stream: stream} end,
          fn
            %{pid: pid, stream: stream} ->
              case read_chunk(pid, stream, opts) do
                {:nofin, part} -> {[part], %{pid: pid, stream: stream}}
                {:fin, body} -> {[body], %{pid: pid, final: :fin}}
              end

            %{pid: pid, final: :fin} ->
              {:halt, %{pid: pid}}
          end,
          fn %{pid: pid} ->
            if opts[:close_conn], do: close(pid)
          end
        )

      {:ok, status, headers, stream_body}
    end

    defp format_response(pid, stream, opts, status, headers, :chunks) do
      {:ok, status, headers, %{pid: pid, stream: stream, opts: Enum.into(opts, [])}}
    end

    defp read_body(pid, stream, opts, acc \\ "") do
      limit = opts[:max_body]

      receive do
        {:gun_data, ^pid, ^stream, :fin, body} ->
          check_body_size(acc, body, limit)

        {:gun_data, ^pid, ^stream, :nofin, part} ->
          with {:ok, acc} <- check_body_size(acc, part, limit) do
            read_body(pid, stream, opts, acc)
          end

        {:DOWN, _, _, _, reason} ->
          {:error, reason}
      after
        opts[:timeout] || @default_timeout ->
          {:error, :recv_body_timeout}
      end
    end

    defp check_body_size(acc, part, nil), do: {:ok, acc <> part}

    defp check_body_size(acc, part, limit) do
      body = acc <> part

      if limit - byte_size(body) >= 0 do
        {:ok, body}
      else
        {:error, :body_too_large}
      end
    end

    defp domain_or_ip(host) do
      charlist = to_charlist(host)

      case :inet.parse_address(charlist) do
        {:error, :einval} ->
          :idna.encode(charlist)

        {:ok, ip} ->
          ip
      end
    end
  end
end


Have the "mock" server send a [] when asked for a commit status that doesn't exist

defmodule BorsNG.GitHub.ServerMock do
  use GenServer

  @moduledoc """
  Provides a fake connection to GitHub's REST API.

  This is used for unit testing and when running in a "dev" environment,
  like on a local machine.
  It's basically just a genserver frontend for a big map;
  you can put and get its state,
  and other functions will mutate or read subsets of it.

  For example, I can run `iex -S mix phoenix.server` and do this:

      iex> # Push state to "GitHub"
      iex> alias BorsNG.GitHub
      iex> alias BorsNG.GitHub.ServerMock
      iex> ServerMock.put_state(%{
      ...>   {:installation, 91} => %{ repos: [
      ...>     %GitHub.Repo{
      ...>       id: 14,
      ...>       name: "test/repo",
      ...>       owner: %{
      ...>         id: 6,
      ...>         login: "bors-fanboi",
      ...>         avatar_url: "data:image/svg+xml,<svg></svg>",
      ...>         type: :user
      ...>       }}
      ...>   ] },
      ...>   {{:installation, 91}, 14} => %{
      ...>     branches: %{},
      ...>     comments: %{1 => []},
      ...>     pulls: %{
      ...>       1 => %GitHub.Pr{
      ...>         number: 1,
      ...>         title: "Test",
      ...>         body: "Mess",
      ...>         state: :open,
      ...>         base_ref: "master",
      ...>         head_sha: "00000001",
      ...>         user: %GitHub.User{
      ...>           id: 6,
      ...>           login: "bors-fanboi",
      ...>           avatar_url: "data:image/svg+xml,<svg></svg>"}}},
      ...>     statuses: %{},
      ...>     files: %{}}})
      iex> GitHub.get_open_prs!({{:installation, 91}, 14})
      [
        %BorsNG.GitHub.Pr{
          number: 1,
          title: "Test",
          body: "Mess",
          state: :open,
          base_ref: "master",
          head_sha: "00000001",
          user: %BorsNG.GitHub.User{
            id: 6,
            login: "bors-fanboi",
            avatar_url: "data:image/svg+xml,<svg></svg>"}}]
  """

  def start_link do
    GenServer.start_link(__MODULE__, :ok, name: BorsNG.GitHub)
  end

  @type tconn :: BorsNG.GitHub.tconn
  @type ttoken :: BorsNG.GitHub.ttoken
  @type trepo :: BorsNG.GitHub.trepo
  @type tuser :: BorsNG.GitHub.User.t

  @type tbranch :: bitstring
  @type tcommit :: bitstring

  @type tstate :: %{
    tconn => %{
      branches: %{tbranch => tcommit},
      comments: %{integer => [bitstring]},
      statuses: %{tbranch => %{bitstring => :open | :closed | :running}},
      files: %{tbranch => %{bitstring => bitstring}}
    },
    {:installation, number} => %{
      repos: [trepo]
    },
    :users => %{bitstring => tuser}
  }

  def put_state(state) do
    GenServer.call(BorsNG.GitHub, {:put_state, state})
  end

  def get_state do
    GenServer.call(BorsNG.GitHub, {:get_state})
  end

  def init(:ok) do
    {:ok, %{}}
  end

  def handle_call({:put_state, state}, _from, _) do
    {:reply, :ok, state}
  end

  def handle_call({:get_state}, _from, state) do
    {:reply, state, state}
  end

  def handle_call({type, t, args}, _from, state) do
    {res, state} = do_handle_call(type, t, args, state)
    {:reply, res, state}
  end

  def do_handle_call(:get_pr, repo_conn, {pr_xref}, state) do
    with({:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, pulls} <- Map.fetch(repo, :pulls),
      do: Map.fetch(pulls, pr_xref))
    |> case do
      {:ok, _} = res -> {res, state}
      _ -> {{:error, :get_pr}, state}
    end
  end

  def do_handle_call(:get_installation_repos, installation_conn, {}, state) do
    with({:ok, installation} <- Map.fetch(state, installation_conn),
         {:ok, repos} <- Map.fetch(installation, :repos),
      do: {:ok, repos})
    |> case do
      {:ok, _} = res -> {res, state}
      _ -> {{:error, :get_open_prs}, state}
    end
  end

  def do_handle_call(:get_open_prs, repo_conn, {}, state) do
    with({:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, pulls} <- Map.fetch(repo, :pulls),
      do: {:ok, Map.values(pulls) |> Enum.filter(&(&1.state == :open))})
    |> case do
      {:ok, _} = res -> {res, state}
      _ -> {{:error, :get_open_prs}, state}
    end
  end

  def do_handle_call(:push, repo_conn, {sha, to}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, branches} <- Map.fetch(repo, :branches) do
      branches = %{branches | to => sha}
      repo = %{repo | branches: branches}
      state = %{state | repo_conn => repo}
      {{:ok, sha}, state}
    end
    |> case do
      {{:ok, _}, _} = res -> res
      _ -> {{:error, :push}, state}
    end
  end

  def do_handle_call(:get_branch, repo_conn, {from}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, branches} <- Map.fetch(repo, :branches) do
      sha = case branches[from] do
        nil -> from
        sha -> sha
      end
      {{:ok, %{commit: sha, tree: sha}}, state}
    end
    |> case do
      {{:ok, _}, _} = res -> res
      _ -> {{:error, :get_branch}, state}
    end
  end

  def do_handle_call(:delete_branch, repo_conn, {branch}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, branches} <- Map.fetch(repo, :branches) do
      branches = Map.delete(branches, branch)
      repo = %{repo | branches: branches}
      state = %{state | repo_conn => repo}
      {:ok, state}
    end
    |> case do
      {:ok, _} = res -> res
      _ -> {{:error, :get_branch}, state}
    end
  end

  def do_handle_call(:merge_branch, repo_conn, {%{
    from: from,
    to: to,
    commit_message: _commit_message}}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, branches} <- Map.fetch(repo, :branches) do
      base = branches[to]
      head = case branches[from] do
        nil -> from
        head -> head
      end
      nsha = base <> head
      branches = %{branches | to => nsha}
      repo = %{repo | branches: branches}
      state = %{state | repo_conn => repo}
      {{:ok, %{commit: nsha, tree: nsha}}, state}
    end
    |> case do
      {{:ok, _}, _} = res -> res
      _ -> {{:error, :merge_branch}, state}
    end
  end

  def do_handle_call(:synthesize_commit, repo_conn, {%{
    branch: branch,
    tree: tree,
    parents: parents,
    commit_message: _commit_message}}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, branches} <- Map.fetch(repo, :branches) do
      nsha = parents
      |> Enum.reverse()
      |> Enum.reduce(&<>/2)
      ^nsha = tree
      branches = Map.put(branches, branch, nsha)
      repo = %{repo | branches: branches}
      state = %{state | repo_conn => repo}
      {{:ok, nsha}, state}
    end
    |> case do
      {{:ok, _}, _} = res -> res
      _ -> {{:error, :synthesize_commit}, state}
    end
  end

  def do_handle_call(:force_push, repo_conn, {sha, to}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, branches} <- Map.fetch(repo, :branches) do
      branches = %{branches | to => sha}
      repo = %{repo | branches: branches}
      state = %{state | repo_conn => repo}
      {{:ok, sha}, state}
    end
    |> case do
      {{:ok, _}, _} = res -> res
      _ -> {{:error, :force_push}, state}
    end
  end

  def do_handle_call(:get_commit_status, repo_conn, {sha}, state) do
    with({:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, statuses} <- Map.fetch(repo, :statuses),
      do: {:ok, statuses[sha] || %{}})
    |> case do
      {:ok, _} = res -> {res, state}
      _ -> {{:error, :get_commit_status}, state}
    end
  end

  def do_handle_call(:get_labels, repo_conn, {issue_xref}, state) do
    with({:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, labels} <- Map.fetch(repo, :labels),
      do: {:ok, labels[issue_xref]})
    |> case do
      {:ok, _} = res -> {res, state}
      _ -> {{:ok, []}, state}
    end
  end

  def do_handle_call(:get_file, repo_conn, {branch, path}, state) do
    with({:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, files} <- Map.fetch(repo, :files),
      do: {:ok, files[branch][path]})
    |> case do
      {:ok, _} = res -> {res, state}
      _ -> {{:error, :get_file}, state}
    end
  end

  def do_handle_call(:post_comment, repo_conn, {number, body}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, comments} <- Map.fetch(repo, :comments),
         {:ok, c} <- Map.fetch(comments, number) do
      c = [body | c]
      comments = %{comments | number => c}
      repo = %{repo | comments: comments}
      state = %{state | repo_conn => repo}
      {:ok, state}
    end
    |> case do
      {:ok, state} -> {:ok, state}
      _ -> {{:error, :post_comment}, state}
    end
  end

  def do_handle_call(:post_commit_status, repo_conn, {sha, status, _}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, statuses} <- Map.fetch(repo, :statuses) do
      sha_statuses = case Map.has_key?(statuses, sha) do
        false -> %{"bors" => status}
        true ->
          statuses
          |> Map.fetch!(sha)
          |> Map.put("bors", status)
      end
      statuses = Map.put(statuses, sha, sha_statuses)
      repo = %{repo | statuses: statuses}
      state = %{state | repo_conn => repo}
      {:ok, state}
    end
    |> case do
      {:ok, state} -> {:ok, state}
      _ -> {{:error, :post_commit_status}, state}
    end
  end

  def do_handle_call(
    :get_user_by_login, _token, {login}, state
  ) do
    with({:ok, users} <- Map.fetch(state, :users),
      do: {:ok, users[login]})
    |> case do
      {:ok, user} -> {{:ok, user}, state}
      _ -> {{:ok, nil}, state}
    end
  end

end

defmodule BorsNG.GitHub.ServerMock do
  use GenServer

  @moduledoc """
  Provides a fake connection to GitHub's REST API.

  This is used for unit testing and when running in a "dev" environment,
  like on a local machine.
  It's basically just a genserver frontend for a big map;
  you can put and get its state,
  and other functions will mutate or read subsets of it.

  For example, I can run `iex -S mix phoenix.server` and do this:

      iex> # Push state to "GitHub"
      iex> alias BorsNG.GitHub
      iex> alias BorsNG.GitHub.ServerMock
      iex> ServerMock.put_state(%{
      ...>   {:installation, 91} => %{ repos: [
      ...>     %GitHub.Repo{
      ...>       id: 14,
      ...>       name: "test/repo",
      ...>       owner: %{
      ...>         id: 6,
      ...>         login: "bors-fanboi",
      ...>         avatar_url: "data:image/svg+xml,<svg></svg>",
      ...>         type: :user
      ...>       }}
      ...>   ] },
      ...>   {{:installation, 91}, 14} => %{
      ...>     branches: %{},
      ...>     comments: %{1 => []},
      ...>     pulls: %{
      ...>       1 => %GitHub.Pr{
      ...>         number: 1,
      ...>         title: "Test",
      ...>         body: "Mess",
      ...>         state: :open,
      ...>         base_ref: "master",
      ...>         head_sha: "00000001",
      ...>         user: %GitHub.User{
      ...>           id: 6,
      ...>           login: "bors-fanboi",
      ...>           avatar_url: "data:image/svg+xml,<svg></svg>"}}},
      ...>     statuses: %{},
      ...>     files: %{}}})
      iex> GitHub.get_open_prs!({{:installation, 91}, 14})
      [
        %BorsNG.GitHub.Pr{
          number: 1,
          title: "Test",
          body: "Mess",
          state: :open,
          base_ref: "master",
          head_sha: "00000001",
          user: %BorsNG.GitHub.User{
            id: 6,
            login: "bors-fanboi",
            avatar_url: "data:image/svg+xml,<svg></svg>"}}]
  """

  def start_link do
    GenServer.start_link(__MODULE__, :ok, name: BorsNG.GitHub)
  end

  @type tconn :: BorsNG.GitHub.tconn
  @type ttoken :: BorsNG.GitHub.ttoken
  @type trepo :: BorsNG.GitHub.trepo
  @type tuser :: BorsNG.GitHub.User.t

  @type tbranch :: bitstring
  @type tcommit :: bitstring

  @type tstate :: %{
    tconn => %{
      branches: %{tbranch => tcommit},
      comments: %{integer => [bitstring]},
      statuses: %{tbranch => %{bitstring => :open | :closed | :running}},
      files: %{tbranch => %{bitstring => bitstring}}
    },
    {:installation, number} => %{
      repos: [trepo]
    },
    :users => %{bitstring => tuser}
  }

  def put_state(state) do
    GenServer.call(BorsNG.GitHub, {:put_state, state})
  end

  def get_state do
    GenServer.call(BorsNG.GitHub, {:get_state})
  end

  def init(:ok) do
    {:ok, %{}}
  end

  def handle_call({:put_state, state}, _from, _) do
    {:reply, :ok, state}
  end

  def handle_call({:get_state}, _from, state) do
    {:reply, state, state}
  end

  def handle_call({type, t, args}, _from, state) do
    {res, state} = do_handle_call(type, t, args, state)
    {:reply, res, state}
  end

  def do_handle_call(:get_pr, repo_conn, {pr_xref}, state) do
    with({:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, pulls} <- Map.fetch(repo, :pulls),
      do: Map.fetch(pulls, pr_xref))
    |> case do
      {:ok, _} = res -> {res, state}
      _ -> {{:error, :get_pr}, state}
    end
  end

  def do_handle_call(:get_installation_repos, installation_conn, {}, state) do
    with({:ok, installation} <- Map.fetch(state, installation_conn),
         {:ok, repos} <- Map.fetch(installation, :repos),
      do: {:ok, repos})
    |> case do
      {:ok, _} = res -> {res, state}
      _ -> {{:error, :get_open_prs}, state}
    end
  end

  def do_handle_call(:get_open_prs, repo_conn, {}, state) do
    with({:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, pulls} <- Map.fetch(repo, :pulls),
      do: {:ok, Map.values(pulls) |> Enum.filter(&(&1.state == :open))})
    |> case do
      {:ok, _} = res -> {res, state}
      _ -> {{:error, :get_open_prs}, state}
    end
  end

  def do_handle_call(:push, repo_conn, {sha, to}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, branches} <- Map.fetch(repo, :branches) do
      branches = %{branches | to => sha}
      repo = %{repo | branches: branches}
      state = %{state | repo_conn => repo}
      {{:ok, sha}, state}
    end
    |> case do
      {{:ok, _}, _} = res -> res
      _ -> {{:error, :push}, state}
    end
  end

  def do_handle_call(:get_branch, repo_conn, {from}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, branches} <- Map.fetch(repo, :branches) do
      sha = case branches[from] do
        nil -> from
        sha -> sha
      end
      {{:ok, %{commit: sha, tree: sha}}, state}
    end
    |> case do
      {{:ok, _}, _} = res -> res
      _ -> {{:error, :get_branch}, state}
    end
  end

  def do_handle_call(:delete_branch, repo_conn, {branch}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, branches} <- Map.fetch(repo, :branches) do
      branches = Map.delete(branches, branch)
      repo = %{repo | branches: branches}
      state = %{state | repo_conn => repo}
      {:ok, state}
    end
    |> case do
      {:ok, _} = res -> res
      _ -> {{:error, :get_branch}, state}
    end
  end

  def do_handle_call(:merge_branch, repo_conn, {%{
    from: from,
    to: to,
    commit_message: _commit_message}}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, branches} <- Map.fetch(repo, :branches) do
      base = branches[to]
      head = case branches[from] do
        nil -> from
        head -> head
      end
      nsha = base <> head
      branches = %{branches | to => nsha}
      repo = %{repo | branches: branches}
      state = %{state | repo_conn => repo}
      {{:ok, %{commit: nsha, tree: nsha}}, state}
    end
    |> case do
      {{:ok, _}, _} = res -> res
      _ -> {{:error, :merge_branch}, state}
    end
  end

  def do_handle_call(:synthesize_commit, repo_conn, {%{
    branch: branch,
    tree: tree,
    parents: parents,
    commit_message: _commit_message}}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, branches} <- Map.fetch(repo, :branches) do
      nsha = parents
      |> Enum.reverse()
      |> Enum.reduce(&<>/2)
      ^nsha = tree
      branches = Map.put(branches, branch, nsha)
      repo = %{repo | branches: branches}
      state = %{state | repo_conn => repo}
      {{:ok, nsha}, state}
    end
    |> case do
      {{:ok, _}, _} = res -> res
      _ -> {{:error, :synthesize_commit}, state}
    end
  end

  def do_handle_call(:force_push, repo_conn, {sha, to}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, branches} <- Map.fetch(repo, :branches) do
      branches = %{branches | to => sha}
      repo = %{repo | branches: branches}
      state = %{state | repo_conn => repo}
      {{:ok, sha}, state}
    end
    |> case do
      {{:ok, _}, _} = res -> res
      _ -> {{:error, :force_push}, state}
    end
  end

  def do_handle_call(:get_commit_status, repo_conn, {sha}, state) do
    with({:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, statuses} <- Map.fetch(repo, :statuses),
      do: {:ok, statuses[sha]})
    |> case do
      {:ok, _} = res -> {res, state}
      _ -> {{:error, :get_commit_status}, state}
    end
  end

  def do_handle_call(:get_labels, repo_conn, {issue_xref}, state) do
    with({:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, labels} <- Map.fetch(repo, :labels),
      do: {:ok, labels[issue_xref]})
    |> case do
      {:ok, _} = res -> {res, state}
      _ -> {{:ok, []}, state}
    end
  end

  def do_handle_call(:get_file, repo_conn, {branch, path}, state) do
    with({:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, files} <- Map.fetch(repo, :files),
      do: {:ok, files[branch][path]})
    |> case do
      {:ok, _} = res -> {res, state}
      _ -> {{:error, :get_file}, state}
    end
  end

  def do_handle_call(:post_comment, repo_conn, {number, body}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, comments} <- Map.fetch(repo, :comments),
         {:ok, c} <- Map.fetch(comments, number) do
      c = [body | c]
      comments = %{comments | number => c}
      repo = %{repo | comments: comments}
      state = %{state | repo_conn => repo}
      {:ok, state}
    end
    |> case do
      {:ok, state} -> {:ok, state}
      _ -> {{:error, :post_comment}, state}
    end
  end

  def do_handle_call(:post_commit_status, repo_conn, {sha, status, _}, state) do
    with {:ok, repo} <- Map.fetch(state, repo_conn),
         {:ok, statuses} <- Map.fetch(repo, :statuses) do
      sha_statuses = case Map.has_key?(statuses, sha) do
        false -> %{"bors" => status}
        true ->
          statuses
          |> Map.fetch!(sha)
          |> Map.put("bors", status)
      end
      statuses = Map.put(statuses, sha, sha_statuses)
      repo = %{repo | statuses: statuses}
      state = %{state | repo_conn => repo}
      {:ok, state}
    end
    |> case do
      {:ok, state} -> {:ok, state}
      _ -> {{:error, :post_commit_status}, state}
    end
  end

  def do_handle_call(
    :get_user_by_login, _token, {login}, state
  ) do
    with({:ok, users} <- Map.fetch(state, :users),
      do: {:ok, users[login]})
    |> case do
      {:ok, user} -> {{:ok, user}, state}
      _ -> {{:ok, nil}, state}
    end
  end

end


adding get methods to homepage

adding get methods to homepage

defmodule Reef.HomeModel do

  import Reef.Helpers

  def index do
    %{
      home: %{
        links: [
          %{id: "self", method: "GET", href: url()},
          %{id: "home", method: "GET", href: url()},
          %{id: "resolve", method: "POST", href: url()},
          %{id: "rules", method: "GET", href: link("rules")}
        ]
      }
    }
  end

end

defmodule Reef.HomeModel do

  import Reef.Helpers

  def index do
    %{
      home: %{
        links: [
          %{id: "self", href: url()},
          %{id: "home", href: url()},
          %{id: "resolve", method: "POST", href: url()},
          %{id: "rules", href: link("rules")}
        ]
      }
    }
  end

end


A test case for empty receive block

A test case for empty receive block

Code.require_file "../test_helper.exs", __DIR__

defrecord BadRange, first: 0, last: 0 do
  defoverridable [first: 1]

  def first(_) do
    :not_optimizable
  end
end

defmodule Kernel.RecordRewriterTest do
  use ExUnit.Case, async: true

  ## Helpers

  defmacrop clause(expr) do
    quote do: extract_clause(unquote(Macro.escape(expr)))
  end

  defp extract_clause(fun) do
    { { :fun, _, { :clauses, clauses } }, _ } =
      :elixir_translator.translate_each(fun, :elixir.scope_for_eval([]))
    hd(clauses)
  end

  defp optimize_clause(clause) do
    { clause, dict, res } = Kernel.RecordRewriter.optimize_clause(__MODULE__, clause)
    dict = Enum.map dict, fn { k, { v, _ } } -> { k, v }; other -> other; end
    { clause, dict, res }
  end

  ## Inference tests

  test "simple atom" do
    clause = clause(fn -> :foo end)
    assert optimize_clause(clause) == { clause, [], nil }
  end

  test "with left-side arg match" do
    clause = clause(fn(x = Macro.Env[]) -> :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "with right-side arg match" do
    clause = clause(fn(Macro.Env[] = x) -> :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "with nested left-side arg match" do
    clause = clause(fn(x = y = Macro.Env[]) -> :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Macro.Env], nil }
  end

  test "with nested right-side arg match" do
    clause = clause(fn(Macro.Env[] = x = y) -> :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Macro.Env], nil }
  end

  test "with deep left-side arg match" do
    clause = clause(fn({ x, y = Macro.Env[] }) -> :foo end)
    assert optimize_clause(clause) == { clause, [y: Macro.Env], nil }
  end

  test "with deep right-side arg match" do
    clause = clause(fn({ x, Macro.Env[] = y }) -> :foo end)
    assert optimize_clause(clause) == { clause, [y: Macro.Env], nil }
  end

  test "with tuple match" do
    clause = clause(fn({ x, y } = { Macro.Env[], Range[] }) -> :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Range], nil }

    clause = clause(fn({ Macro.Env[], y } = { x, Range[] }) -> :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Range], nil }
  end

  test "inside body" do
    clause = clause(fn(x = Macro.Env[]) -> y = x end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Macro.Env], { Macro.Env, nil } }
  end

  test "inside body with variable overridden" do
    clause = clause(fn(x = Macro.Env[], y = Range[]) -> y = x end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Range, "y@1": Macro.Env], { Macro.Env, nil } }
  end

  test "inside body with nested tuple" do
    clause = clause(fn(x = Range[]) -> ^x = Range[first: { :hello, 2 }] end)
    assert optimize_clause(clause) == { clause, [x: Range], { Range, [nil, { :hello, nil }, nil] } }
  end

  test "conflicting definition" do
    clause = clause(fn(x = Macro.Env[]) -> ^x = Range[]; :foo end)
    assert optimize_clause(clause) == { clause, [x: nil], nil }
  end

  test "inside list" do
    clause = clause(fn -> [x = Macro.Env[]]; :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "inside tuple" do
    clause = clause(fn -> { x = Macro.Env[] }; :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "inside bin" do
    clause = clause(fn -> << x = Macro.Env[] >>; :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "inside operator" do
    clause = clause(fn -> 1 + (x = Macro.Env[]); :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "inside block" do
    clause = clause(fn -> :foo; (x = Macro.Env[]; x) end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }
  end

  test "inside local call" do
    clause = clause(fn -> (x = Macro.Env[]).(y = Range[]) end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Range], nil }
  end

  test "inside remote call" do
    clause = clause(fn -> x.call(y = Range[]) end)
    assert optimize_clause(clause) == { clause, [y: Range], nil }
  end

  test "inside list comprehension" do
    clause = clause(fn -> lc x = Macro.Env[] inlist sample, do: x end)
    assert optimize_clause(clause) == { clause, [], nil }
  end

  test "inside function retrieval" do
    clause = clause(fn -> function(x = Macro.Env[], y, z) end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "inside anonymous function" do
    clause = clause(fn -> fn (x = Macro.Env[]) -> x end end)
    assert optimize_clause(clause) == { clause, [], nil }
  end

  test "inside case" do
    clause = clause(fn -> case x = Macro.Env[] do _ -> x end end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }

    clause = clause(fn -> case something do x = Macro.Env[] -> x; Macro.Env[] = x -> x end end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }

    clause = clause(fn -> case something do x = Macro.Env[] -> x; Macro.Env[] = y -> y end end)
    assert optimize_clause(clause) == { clause, [], { Macro.Env, nil } }

    clause = clause(fn -> case something do 1 -> x = Macro.Env[]; 2 -> x = Macro.Env[] end end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }

    clause = clause(fn -> case something do 1 -> x = Macro.Env[]; 2 -> x = Range[] end end)
    assert optimize_clause(clause) == { clause, [x: nil], nil }

    clause = clause(fn -> case something do x = Macro.Env[] -> x; x = Range[] -> x; _ -> :ok end end)
    assert optimize_clause(clause) == { clause, [x: nil], nil }
  end

  test "inside case with nested tuple" do
    clause = clause(fn -> case something do x = Range[first: { :foo, 2 }] -> x; Range[] = x -> x end end)
    assert optimize_clause(clause) == { clause, [x: Range], { Range, nil } }

    clause = clause(fn -> case something do x = Range[first: { :foo, 2 }] -> x; Range[first: { :foo, 2 }] = x -> x end end)
    assert optimize_clause(clause) == { clause, [x: Range], { Range, [nil, { :foo, nil }, nil] } }
  end

  test "empty receive" do
    clause = clause(fn -> receive do end end)
    assert optimize_clause(clause) == { clause, [], [] }
  end

  test "inside receive" do
    clause = clause(fn -> receive do x = Macro.Env[] -> x; Macro.Env[] = x -> x end end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }

    clause = clause(fn -> receive do x = Macro.Env[] -> x; Macro.Env[] = y -> y end end)
    assert optimize_clause(clause) == { clause, [], { Macro.Env, nil } }

    clause = clause(fn -> receive do 1 -> x = Macro.Env[]; 2 -> x = Macro.Env[] end end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }

    clause = clause(fn -> receive do 1 -> x = Macro.Env[]; 2 -> x = Range[] end end)
    assert optimize_clause(clause) == { clause, [x: nil], nil }
  end

  test "inside receive with after" do
    clause = clause(fn -> receive do 1 -> x = Macro.Env[]; after 2 -> x = Macro.Env[]; end end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }

    clause = clause(fn -> receive do 1 -> x = Macro.Env[]; after 2 -> x = Range[]; end end)
    assert optimize_clause(clause) == { clause, [x: nil], nil }
  end

  test "inside try" do
    clause = clause(fn -> try do x = Macro.Env[]; x end end)
    assert optimize_clause(clause) == { clause, [], { Macro.Env, nil } }

    clause = clause(fn -> try do x = Macro.Env[]; x; catch :oops -> x = Macro.Env[]; end end)
    assert optimize_clause(clause) == { clause, [], { Macro.Env, nil } }

    clause = clause(fn -> try do x = Macro.Env[]; x; after x = Macro.Env[]; end end)
    assert optimize_clause(clause) == { clause, [], { Macro.Env, nil } }
  end

  ## Rewrite tests

  test "getter call is rewriten" do
    { clause, rewriten } =
      { clause(fn(x = Range[]) -> x.first end), clause(fn(x = Range[]) -> :erlang.element(2, x) end) }

    assert optimize_clause(clause) == { rewriten, [x: Range], nil }
  end

  test "direct getter call is rewriten" do
    { clause, rewriten } =
      { clause(fn() -> Range[].first end), clause(fn() -> :erlang.element(2, Range[]) end) }

    assert optimize_clause(clause) == { rewriten, [], nil }
  end

  test "setter call is rewriten" do
    { clause, rewriten } =
      { clause(fn(x = Range[]) -> x.first(:first) end), clause(fn(x = Range[]) -> :erlang.setelement(2, x, :first) end) }

    assert optimize_clause(clause) == { rewriten, [x: Range], { Range, nil } }
  end

  test "nested setter call is rewriten" do
    { clause, rewriten } =
      { clause(fn(x = Range[]) -> x.first(:first).last(:last) end), clause(fn(x = Range[]) -> :erlang.setelement(3, :erlang.setelement(2, x, :first), :last) end) }

    assert optimize_clause(clause) == { rewriten, [x: Range], { Range, nil } }
  end

  test "updater call is rewriten" do
    { clause, rewriten } =
      { clause(fn(x = Range[]) -> x.update_first(&1 + 1) end), clause(fn(x = Range[]) -> Range.update_first(&1 + 1, x) end) }
    assert optimize_clause(clause) == { rewriten, [x: Range], { Range, nil } }
  end

  test "update call is rewriten" do
    { clause, rewriten } =
      { clause(fn(x = Range[]) -> x.update(first: 1) end), clause(fn(x = Range[]) -> Range.update([first: 1], x) end) }
    assert optimize_clause(clause) == { rewriten, [x: Range], { Range, nil } }
  end

  test "fallback for unknown fields" do
    { clause, rewriten } =
      { clause(fn(x = Range[]) -> x.unknown(1, 2) end), clause(fn(x = Range[]) -> Range.unknown(1, 2, x) end) }
    assert optimize_clause(clause) == { rewriten, [x: Range], nil }
  end

  test "fallback for rewriten fields" do
    { clause, rewriten } =
      { clause(fn(x = BadRange[]) -> x.first end), clause(fn(x = BadRange[]) -> BadRange.first(x) end) }
    assert optimize_clause(clause) == { rewriten, [x: BadRange], nil }
  end

  test "noop for not records fields" do
    clause = clause(fn(x = { :not_a_record, _ }) -> x.unknown end)
    assert optimize_clause(clause) == { clause, [x: :not_a_record], nil }
  end

  test "noop for conflicting inference" do
    clause = clause(fn(x = Macro.Env[]) -> ^x = Range[]; x.first end)
    assert optimize_clause(clause) == { clause, [x: nil], nil }
  end
end

Code.require_file "../test_helper.exs", __DIR__

defrecord BadRange, first: 0, last: 0 do
  defoverridable [first: 1]

  def first(_) do
    :not_optimizable
  end
end

defmodule Kernel.RecordRewriterTest do
  use ExUnit.Case, async: true

  ## Helpers

  defmacrop clause(expr) do
    quote do: extract_clause(unquote(Macro.escape(expr)))
  end

  defp extract_clause(fun) do
    { { :fun, _, { :clauses, clauses } }, _ } =
      :elixir_translator.translate_each(fun, :elixir.scope_for_eval([]))
    hd(clauses)
  end

  defp optimize_clause(clause) do
    { clause, dict, res } = Kernel.RecordRewriter.optimize_clause(__MODULE__, clause)
    dict = Enum.map dict, fn { k, { v, _ } } -> { k, v }; other -> other; end
    { clause, dict, res }
  end

  ## Inference tests

  test "simple atom" do
    clause = clause(fn -> :foo end)
    assert optimize_clause(clause) == { clause, [], nil }
  end

  test "with left-side arg match" do
    clause = clause(fn(x = Macro.Env[]) -> :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "with right-side arg match" do
    clause = clause(fn(Macro.Env[] = x) -> :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "with nested left-side arg match" do
    clause = clause(fn(x = y = Macro.Env[]) -> :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Macro.Env], nil }
  end

  test "with nested right-side arg match" do
    clause = clause(fn(Macro.Env[] = x = y) -> :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Macro.Env], nil }
  end

  test "with deep left-side arg match" do
    clause = clause(fn({ x, y = Macro.Env[] }) -> :foo end)
    assert optimize_clause(clause) == { clause, [y: Macro.Env], nil }
  end

  test "with deep right-side arg match" do
    clause = clause(fn({ x, Macro.Env[] = y }) -> :foo end)
    assert optimize_clause(clause) == { clause, [y: Macro.Env], nil }
  end

  test "with tuple match" do
    clause = clause(fn({ x, y } = { Macro.Env[], Range[] }) -> :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Range], nil }

    clause = clause(fn({ Macro.Env[], y } = { x, Range[] }) -> :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Range], nil }
  end

  test "inside body" do
    clause = clause(fn(x = Macro.Env[]) -> y = x end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Macro.Env], { Macro.Env, nil } }
  end

  test "inside body with variable overridden" do
    clause = clause(fn(x = Macro.Env[], y = Range[]) -> y = x end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Range, "y@1": Macro.Env], { Macro.Env, nil } }
  end

  test "inside body with nested tuple" do
    clause = clause(fn(x = Range[]) -> ^x = Range[first: { :hello, 2 }] end)
    assert optimize_clause(clause) == { clause, [x: Range], { Range, [nil, { :hello, nil }, nil] } }
  end

  test "conflicting definition" do
    clause = clause(fn(x = Macro.Env[]) -> ^x = Range[]; :foo end)
    assert optimize_clause(clause) == { clause, [x: nil], nil }
  end

  test "inside list" do
    clause = clause(fn -> [x = Macro.Env[]]; :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "inside tuple" do
    clause = clause(fn -> { x = Macro.Env[] }; :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "inside bin" do
    clause = clause(fn -> << x = Macro.Env[] >>; :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "inside operator" do
    clause = clause(fn -> 1 + (x = Macro.Env[]); :foo end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "inside block" do
    clause = clause(fn -> :foo; (x = Macro.Env[]; x) end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }
  end

  test "inside local call" do
    clause = clause(fn -> (x = Macro.Env[]).(y = Range[]) end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env, y: Range], nil }
  end

  test "inside remote call" do
    clause = clause(fn -> x.call(y = Range[]) end)
    assert optimize_clause(clause) == { clause, [y: Range], nil }
  end

  test "inside list comprehension" do
    clause = clause(fn -> lc x = Macro.Env[] inlist sample, do: x end)
    assert optimize_clause(clause) == { clause, [], nil }
  end

  test "inside function retrieval" do
    clause = clause(fn -> function(x = Macro.Env[], y, z) end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], nil }
  end

  test "inside anonymous function" do
    clause = clause(fn -> fn (x = Macro.Env[]) -> x end end)
    assert optimize_clause(clause) == { clause, [], nil }
  end

  test "inside case" do
    clause = clause(fn -> case x = Macro.Env[] do _ -> x end end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }

    clause = clause(fn -> case something do x = Macro.Env[] -> x; Macro.Env[] = x -> x end end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }

    clause = clause(fn -> case something do x = Macro.Env[] -> x; Macro.Env[] = y -> y end end)
    assert optimize_clause(clause) == { clause, [], { Macro.Env, nil } }

    clause = clause(fn -> case something do 1 -> x = Macro.Env[]; 2 -> x = Macro.Env[] end end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }

    clause = clause(fn -> case something do 1 -> x = Macro.Env[]; 2 -> x = Range[] end end)
    assert optimize_clause(clause) == { clause, [x: nil], nil }

    clause = clause(fn -> case something do x = Macro.Env[] -> x; x = Range[] -> x; _ -> :ok end end)
    assert optimize_clause(clause) == { clause, [x: nil], nil }
  end

  test "inside case with nested tuple" do
    clause = clause(fn -> case something do x = Range[first: { :foo, 2 }] -> x; Range[] = x -> x end end)
    assert optimize_clause(clause) == { clause, [x: Range], { Range, nil } }

    clause = clause(fn -> case something do x = Range[first: { :foo, 2 }] -> x; Range[first: { :foo, 2 }] = x -> x end end)
    assert optimize_clause(clause) == { clause, [x: Range], { Range, [nil, { :foo, nil }, nil] } }
  end

  test "inside receive" do
    clause = clause(fn -> receive do x = Macro.Env[] -> x; Macro.Env[] = x -> x end end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }

    clause = clause(fn -> receive do x = Macro.Env[] -> x; Macro.Env[] = y -> y end end)
    assert optimize_clause(clause) == { clause, [], { Macro.Env, nil } }

    clause = clause(fn -> receive do 1 -> x = Macro.Env[]; 2 -> x = Macro.Env[] end end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }

    clause = clause(fn -> receive do 1 -> x = Macro.Env[]; 2 -> x = Range[] end end)
    assert optimize_clause(clause) == { clause, [x: nil], nil }
  end

  test "inside receive with after" do
    clause = clause(fn -> receive do 1 -> x = Macro.Env[]; after 2 -> x = Macro.Env[]; end end)
    assert optimize_clause(clause) == { clause, [x: Macro.Env], { Macro.Env, nil } }

    clause = clause(fn -> receive do 1 -> x = Macro.Env[]; after 2 -> x = Range[]; end end)
    assert optimize_clause(clause) == { clause, [x: nil], nil }
  end

  test "inside try" do
    clause = clause(fn -> try do x = Macro.Env[]; x end end)
    assert optimize_clause(clause) == { clause, [], { Macro.Env, nil } }

    clause = clause(fn -> try do x = Macro.Env[]; x; catch :oops -> x = Macro.Env[]; end end)
    assert optimize_clause(clause) == { clause, [], { Macro.Env, nil } }

    clause = clause(fn -> try do x = Macro.Env[]; x; after x = Macro.Env[]; end end)
    assert optimize_clause(clause) == { clause, [], { Macro.Env, nil } }
  end

  ## Rewrite tests

  test "getter call is rewriten" do
    { clause, rewriten } =
      { clause(fn(x = Range[]) -> x.first end), clause(fn(x = Range[]) -> :erlang.element(2, x) end) }

    assert optimize_clause(clause) == { rewriten, [x: Range], nil }
  end

  test "direct getter call is rewriten" do
    { clause, rewriten } =
      { clause(fn() -> Range[].first end), clause(fn() -> :erlang.element(2, Range[]) end) }

    assert optimize_clause(clause) == { rewriten, [], nil }
  end

  test "setter call is rewriten" do
    { clause, rewriten } =
      { clause(fn(x = Range[]) -> x.first(:first) end), clause(fn(x = Range[]) -> :erlang.setelement(2, x, :first) end) }

    assert optimize_clause(clause) == { rewriten, [x: Range], { Range, nil } }
  end

  test "nested setter call is rewriten" do
    { clause, rewriten } =
      { clause(fn(x = Range[]) -> x.first(:first).last(:last) end), clause(fn(x = Range[]) -> :erlang.setelement(3, :erlang.setelement(2, x, :first), :last) end) }

    assert optimize_clause(clause) == { rewriten, [x: Range], { Range, nil } }
  end

  test "updater call is rewriten" do
    { clause, rewriten } =
      { clause(fn(x = Range[]) -> x.update_first(&1 + 1) end), clause(fn(x = Range[]) -> Range.update_first(&1 + 1, x) end) }
    assert optimize_clause(clause) == { rewriten, [x: Range], { Range, nil } }
  end

  test "update call is rewriten" do
    { clause, rewriten } =
      { clause(fn(x = Range[]) -> x.update(first: 1) end), clause(fn(x = Range[]) -> Range.update([first: 1], x) end) }
    assert optimize_clause(clause) == { rewriten, [x: Range], { Range, nil } }
  end

  test "fallback for unknown fields" do
    { clause, rewriten } =
      { clause(fn(x = Range[]) -> x.unknown(1, 2) end), clause(fn(x = Range[]) -> Range.unknown(1, 2, x) end) }
    assert optimize_clause(clause) == { rewriten, [x: Range], nil }
  end

  test "fallback for rewriten fields" do
    { clause, rewriten } =
      { clause(fn(x = BadRange[]) -> x.first end), clause(fn(x = BadRange[]) -> BadRange.first(x) end) }
    assert optimize_clause(clause) == { rewriten, [x: BadRange], nil }
  end

  test "noop for not records fields" do
    clause = clause(fn(x = { :not_a_record, _ }) -> x.unknown end)
    assert optimize_clause(clause) == { clause, [x: :not_a_record], nil }
  end

  test "noop for conflicting inference" do
    clause = clause(fn(x = Macro.Env[]) -> ^x = Range[]; x.first end)
    assert optimize_clause(clause) == { clause, [x: nil], nil }
  end
end


Make sure to save the tools version.

Make sure to save the tools version.

defmodule Farmbot.Farmware.Installer.Repository.SyncTask do
  @moduledoc """
  Init module for installing first party farmware repo. Requires internet.
  """

  use Task, restart: :transient
  use Farmbot.Logger
  alias Farmbot.System.ConfigStorage
  import ConfigStorage, only: [get_config_value: 3, update_config_value: 4]
  alias Farmbot.Farmware
  alias Farmware.Installer

  @data_path Application.get_env(:farmbot, :data_path) || raise "No configured data_path."

  @doc false
  def start_link(_) do
    sync_all()
    maybe_install_farmware_tools()
    :ignore
  end

  def maybe_install_farmware_tools do
    try do
      release_url = get_config_value(:string, "settings", "farmware_tools_release_url")
      install_commit = get_config_value(:string, "settings", "farmware_tools_install_commit")
      commit_and_url_or_nil = get_install_url(release_url, install_commit)
      if commit_and_url_or_nil do
          Logger.busy 3, "Downloading Farmware tools."
          do_install_farmware_tools(commit_and_url_or_nil)
          Logger.success 3, "Downloaded Farmware tools."
      else
        Logger.debug 3, "Farmware tools up to date."
      end

    rescue
      reason -> Logger.error 1, "Failed to install Farmware Tools: #{Exception.message(reason)}"
    end
  end

  def get_install_url(release_url, installed_commit) do
    case :httpc.request(:get, {~c(#{release_url}), [{~c(user-agent), ~c(farmbot-os)}, {~c(content-type), ~c(application/json)}]}, [], [{:body_format, :binary}]) do
      {:ok, {{_, 200, _}, _, msg}} ->
        release = Poison.decode!(msg)
        release_commit = release["target_commitish"]
        if release_commit != installed_commit do
          {release_commit, release["zipball_url"]}
        else
          nil
        end
      {:ok, {{_, _, _}, _, msg}} ->
        message = Poison.decode!(msg) |> Map.get("message") || msg
        Logger.error 1, "Could not check for farmware_tools updates #{release_url}: [#{to_string(message)}]"
        nil
    end
  end

  def do_install_farmware_tools({commit, zipball_url}) when is_binary(commit) and is_binary(zipball_url) do
    farmware_tools_root_path = Path.join(@data_path, "farmware_tools")
    zip_file = "/tmp/farmware-tools.zip"

    File.mkdir_p!(farmware_tools_root_path)
    {:ok, ^zip_file} = Farmbot.HTTP.download_file(zipball_url, zip_file)

    fun = fn({:zip_file, dir, _info, _, _, _}) ->
      [_ | rest] = Path.split(to_string(dir))
      List.first(rest) == "farmware_tools"
    end

    case :zip.extract(~c(#{zip_file}), [:memory, file_filter: fun]) do
      {:ok, list} when is_list(list) ->
        Enum.each(list, fn({filename, data}) ->
          out_file = Path.join([farmware_tools_root_path, Path.basename(to_string(filename))])
          File.write!(out_file, data)
        end)
      {:error, reason} -> raise(reason)
    end

    update_config_value(:string, "settings", "farmware_tools_install_commit", commit)
    :ok
  end

  def sync_all do
    Logger.busy 2, "Syncing all Farmware repos. This may take a while."
    setup_repos()

    synced = fetch_and_sync()
    fw_dir = Installer.install_root_path
    if File.exists?(fw_dir) do
      sync_not_in_repos(fw_dir, synced)
    end
  end

  defp setup_repos do
    # first party farmware url could be nil. This would mean it is disabled.
    fpf_url = get_config_value(:string, "settings", "first_party_farmware_url")
    # if fpf_url isn't nil, check if its been enabled, if not enable it.
    if fpf_url do
      unless ConfigStorage.get_farmware_repo_by_url(fpf_url) do
        Installer.add_repo(fpf_url)
      end
    else
      Logger.warn 2, "First party farmware is disabled."
    end
  end

  defp fetch_and_sync do
    repos = ConfigStorage.all_farmware_repos()
    Enum.reduce(repos, [], fn(repo, acc) ->
      case Installer.sync_repo(repo) do
        {:ok, list_of_entries} ->
          Enum.map(list_of_entries, &(Map.get(&1, :name))) ++ acc
        {:error, _} -> acc
      end
    end)
  end

  defp sync_not_in_repos(fw_dir, synced) do
    all_fws = File.ls!(fw_dir)
    not_in_repos = all_fws -- synced
    for fw_name <- not_in_repos do
      case Farmware.lookup(fw_name) do
        {:ok, %Farmware{} = farmware} ->
          Logger.busy 3, "Syncing: #{inspect farmware}"
          Installer.install(farmware.url)
        _ -> :ok
      end
    end
  end

end

defmodule Farmbot.Farmware.Installer.Repository.SyncTask do
  @moduledoc """
  Init module for installing first party farmware repo. Requires internet.
  """

  use Task, restart: :transient
  use Farmbot.Logger
  alias Farmbot.System.ConfigStorage
  import ConfigStorage, only: [get_config_value: 3]
  alias Farmbot.Farmware
  alias Farmware.Installer

  @data_path Application.get_env(:farmbot, :data_path) || raise "No configured data_path."

  @doc false
  def start_link(_) do
    sync_all()
    maybe_install_farmware_tools()
    :ignore
  end

  def maybe_install_farmware_tools do
    try do
      release_url = get_config_value(:string, "settings", "farmware_tools_release_url")
      install_commit = get_config_value(:string, "settings", "farmware_tools_install_commit")
      commit_and_url_or_nil = get_install_url(release_url, install_commit)
      if commit_and_url_or_nil do
          Logger.busy 3, "Downloading Farmware tools."
          do_install_farmware_tools(commit_and_url_or_nil)
          Logger.success 3, "Downloaded Farmware tools."
      else
        Logger.debug 3, "Farmware tools up to date."
      end

    rescue
      reason -> Logger.error 1, "Failed to install Farmware Tools: #{Exception.message(reason)}"
    end
  end

  def get_install_url(release_url, installed_commit) do
    case :httpc.request(:get, {~c(#{release_url}), [{~c(user-agent), ~c(farmbot-os)}, {~c(content-type), ~c(application/json)}]}, [], [{:body_format, :binary}]) do
      {:ok, {{_, 200, _}, _, msg}} ->
        release = Poison.decode!(msg)
        release_commit = release["target_commitish"]
        if release_commit != installed_commit do
          {release_commit, release["zipball_url"]}
        else
          nil
        end
      {:ok, {{_, _, _}, _, msg}} ->
        message = Poison.decode!(msg) |> Map.get("message") || msg
        Logger.error 1, "Could not check for farmware_tools updates #{release_url}: [#{to_string(message)}]"
        nil
    end
  end

  def do_install_farmware_tools({commit, zipball_url}) when is_binary(commit) and is_binary(zipball_url) do
    farmware_tools_root_path = Path.join(@data_path, "farmware_tools")
    zip_file = "/tmp/farmware-tools.zip"

    File.mkdir_p!(farmware_tools_root_path)
    {:ok, ^zip_file} = Farmbot.HTTP.download_file(zipball_url, zip_file)

    fun = fn({:zip_file, dir, _info, _, _, _}) ->
      [_ | rest] = Path.split(to_string(dir))
      List.first(rest) == "farmware_tools"
    end

    case :zip.extract(~c(#{zip_file}), [:memory, file_filter: fun]) do
      {:ok, list} when is_list(list) ->
        Enum.each(list, fn({filename, data}) ->
          out_file = Path.join([farmware_tools_root_path, Path.basename(to_string(filename))])
          File.write!(out_file, data)
        end)
      {:error, reason} -> raise(reason)
    end
  end

  def sync_all do
    Logger.busy 2, "Syncing all Farmware repos. This may take a while."
    setup_repos()

    synced = fetch_and_sync()
    fw_dir = Installer.install_root_path
    if File.exists?(fw_dir) do
      sync_not_in_repos(fw_dir, synced)
    end
  end

  defp setup_repos do
    # first party farmware url could be nil. This would mean it is disabled.
    fpf_url = get_config_value(:string, "settings", "first_party_farmware_url")
    # if fpf_url isn't nil, check if its been enabled, if not enable it.
    if fpf_url do
      unless ConfigStorage.get_farmware_repo_by_url(fpf_url) do
        Installer.add_repo(fpf_url)
      end
    else
      Logger.warn 2, "First party farmware is disabled."
    end
  end

  defp fetch_and_sync do
    repos = ConfigStorage.all_farmware_repos()
    Enum.reduce(repos, [], fn(repo, acc) ->
      case Installer.sync_repo(repo) do
        {:ok, list_of_entries} ->
          Enum.map(list_of_entries, &(Map.get(&1, :name))) ++ acc
        {:error, _} -> acc
      end
    end)
  end

  defp sync_not_in_repos(fw_dir, synced) do
    all_fws = File.ls!(fw_dir)
    not_in_repos = all_fws -- synced
    for fw_name <- not_in_repos do
      case Farmware.lookup(fw_name) do
        {:ok, %Farmware{} = farmware} ->
          Logger.busy 3, "Syncing: #{inspect farmware}"
          Installer.install(farmware.url)
        _ -> :ok
      end
    end
  end

end


remove unused warning

remove unused warning

defmodule Mix.Tasks.Breadcrumble.Gen.Views do
  use Mix.Task
  import Mix.Generator

  @shortdoc "Generates view and template for the Phoenix application"

  def run(_args) do
    app_module = Mix.Project.config[:app] |> Atom.to_string |> Mix.Utils.camelize
    create_file Path.join(["web", "views", "breadcrumble_view.ex"])
                |> Path.relative_to(Mix.Project.app_path),
                view_template(app_module: app_module)
    create_file Path.join(["web", "templates", "breadcrumble", "breadcrumb.html.eex"])
                |> Path.relative_to(Mix.Project.app_path),
                breadcrumb_text
  end

  embed_template :view, """
  defmodule <%= @app_module %>.BreadcrumbleView do
    use <%= @app_module %>.Web, :view
  end
  """
  embed_text :breadcrumb, from_file("../../../../priv/templates/breadcrumb.html.eex")
end

defmodule Mix.Tasks.Breadcrumble.Gen.Views do
  use Mix.Task
  import Mix.Generator

  @shortdoc "Generates view and template for the Phoenix application"

  def run(args) do
    app_module = Mix.Project.config[:app] |> Atom.to_string |> Mix.Utils.camelize
    create_file Path.join(["web", "views", "breadcrumble_view.ex"])
                |> Path.relative_to(Mix.Project.app_path),
                view_template(app_module: app_module)
    create_file Path.join(["web", "templates", "breadcrumble", "breadcrumb.html.eex"])
                |> Path.relative_to(Mix.Project.app_path),
                breadcrumb_text
  end

  embed_template :view, """
  defmodule <%= @app_module %>.BreadcrumbleView do
    use <%= @app_module %>.Web, :view
  end
  """
  embed_text :breadcrumb, from_file("../../../../priv/templates/breadcrumb.html.eex")
end


Remove unwanted files

Remove unwanted files

# AUTOGENERATED FILE
#
# As of autopkgtest 3.5, Ruby packages that use gem2deb are automatically
# detected, and if debian/tests/control does not exist the contents below will
# be assumed (with the exception of gem2deb being filtered out from
# @builddeps@):

Test-Command: gem2deb-test-runner --autopkgtest --check-dependencies 2>&1
Depends: @, @builddeps@, gem2deb-test-runner

# if you would need to change something to the above, to that and rename this
# file to `control`. Otherwise, you should probably delete this file
# (recommended).