lib/horizon/collection.ex

defmodule Stellar.Horizon.Collection do
  @moduledoc """
  Paginates data and returns collection-based results for multiple Horizon endpoints.
  """
  alias Stellar.Horizon.CollectionError

  @type resource :: {module(), function()}
  @type records :: [resource(), ...]
  @type response :: map()
  @type pagination :: {function(), Keyword.t()}

  @type t :: %__MODULE__{records: records(), prev: function(), next: function()}

  defstruct [:records, :next, :prev]

  @spec new(response :: response(), resource :: resource()) :: t()
  def new(
        %{
          _embedded: %{records: records},
          _links: %{prev: %{href: prev_url}, next: %{href: next_url}}
        },
        {resource, paginate_fun}
      ) do
    %__MODULE__{
      records: Enum.map(records, &resource.new/1),
      prev: paginate({paginate_fun, prev_url}),
      next: paginate({paginate_fun, next_url})
    }
  end

  def new(_response, _resource), do: raise(CollectionError, :invalid_collection)

  @spec paginate(pagination :: pagination()) :: function()
  defp paginate({paginate_fn, url}) do
    fn ->
      url
      |> pagination_query()
      |> paginate_fn.()
    end
  end

  @spec pagination_query(url :: String.t()) :: Keyword.t()
  defp pagination_query(url) do
    case URI.parse(url) do
      %URI{query: query} when not is_nil(query) ->
        query
        |> URI.query_decoder()
        |> Enum.map(fn {key, value} -> {String.to_atom(key), value} end)

      _uri ->
        []
    end
  end
end