lib/blockchain_api/blockchain_api.ex

defmodule HeliumElixir.BlockchainApi do
  use HTTPoison.Base
  require HTTPoison.Retry
  alias HTTPoison.Retry
  alias HTTPoison.Response
  alias HTTPoison.Error
  alias HeliumElixir.BlockchainApi.BlockchainRequestConfig
  alias HeliumElixir.BlockchainApi.TakeMinConfig

  defp respond({:ok, %Response{body: body, status_code: status_code}}) do
    case status_code do
      code when code <= 299 ->
        response = body |> Jason.decode!()
        {:ok, response}

      _ ->
        {:error, message: "Request failed with status_code #{status_code}", code: status_code}
    end
  end

  defp respond({:error, %Error{reason: reason}}) do
    {:error, message: reason, code: -1}
  end

  def post_json(
        %BlockchainRequestConfig{headers: headers, base_url: base_url, path: path},
        body \\ %{}
      ) do
    post(base_url <> path, Jason.encode!(body), headers) |> respond()
  end

  def get_json(
        %BlockchainRequestConfig{
          headers: headers,
          base_url: base_url,
          path: path,
          recv_timeout: recv_timeout,
          max_retries: max_retries,
          hackney: hackney
        },
        params \\ %{}
      ) do
    get(base_url <> path, headers,
      params: params,
      follow_redirect: true,
      recv_timeout: recv_timeout,
      hackney: hackney
    )
    |> Retry.autoretry(
      max_attempts: max_retries,
      wait: 15000,
      retry_unknown_errors: true
    )
    |> respond()
  end

  @doc """
  Pages the given endpoint until `min_count` is reached.
  The data returned will be at least the amount requested unless no more items exist.

  Returns `{:ok, %{"data" => data, "cursor" => cursor}}`.
  """
  def take_min(
        %BlockchainRequestConfig{} = config,
        %TakeMinConfig{min_count: min_count, params: params, acc: acc, cursor: cursor}
      ) do
    case get_json(config, Map.merge(params, %{cursor: cursor})) do
      {:ok, %{"data" => data, "cursor" => next_cursor}} ->
        next_data = acc ++ data
        acc_count = length(next_data)

        if(acc_count >= min_count || is_nil(next_cursor)) do
          {:ok, %{"data" => next_data, "cursor" => next_cursor}}
        else
          take_min(config, %TakeMinConfig{
            min_count: min_count,
            params: params,
            acc: next_data,
            cursor: next_cursor
          })
        end

      {:ok, %{"data" => data}} ->
        {:ok, %{"data" => acc ++ data, "cursor" => ""}}

      {:error, message: message, code: code} ->
        {:error, message: message, code: code}
    end
  end

  def each_page(%BlockchainRequestConfig{} = config, fun, cursor \\ nil) do
    case get_json(config, %{cursor: cursor}) do
      {:ok, %{"data" => data, "cursor" => next_cursor}} ->
        fun.(data)
        each_page(config, fun, next_cursor)

      {:ok, %{"data" => data}} ->
        fun.(data)
        :ok

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