lib/data_provider/data.ex

defmodule DataProvider.Data do
  @moduledoc ~S"""
  The data module of `DataProvider` which result of `DataProvider` processing

  In this struct:

    * `items` - it is a list of result user request
  """

  alias __MODULE__.QueryModifier
  alias __MODULE__.ListModifier
  alias Ecto.Query

  defstruct items: [],
            total_count: 0

  @typedoc ~S"""
  DataProvider data value, contains:

    * `items` - List of found data for exactly this `DataProvider`

    * `total_count` - integer value with count of items in `items`

  """
  @type t() :: %__MODULE__{items: list(), total_count: integer()}

  @doc ~S"""
  Creates `DataProvider.Data` struct by received `find_result` and `DataProvider`.

  Args:

    * `data_provider` - data provider, which been use by user for getting
    `find_result`

    * `find_result` - value, which been returned by `find/1` function of
    user's `DataProvider` implementation

  Function will execute behaviour depending on what value was in `find_result`

    1. If received `find_result` is a `Ecto.Query` then it will be processed
    by `DataProvider.QueryModifier`. The reveived query will be executed and
    result of execution gonna be put into `items` of new `DataProvider.Data`
    struct.

    2. If received `find_result` is a `list()`, then it will be processed by
    `DataProvider.ListModifier`. The received data will be put into `items`
    of new `DataProvider.Data` struct.

  """
  @spec create(DataProvider.t, Query.t | list()) :: __MODULE__.t
  def create(%DataProvider{module: module} = data_provider, %Query{} = find_result) do
    repo = apply(module, :repo, [])
    query = QueryModifier.modify(find_result, data_provider)

    %__MODULE__{}
    |> load_items(repo, query)
    |> load_total_count(repo, find_result)
  end
  def create(%DataProvider{} = data_provider, find_result) when is_list(find_result) do
    %__MODULE__{
      items: ListModifier.modify(find_result, data_provider),
      total_count: Enum.count(find_result)
    }
  end

  # load `:items` into received `DataProvider.Data`

  @spec load_items(__MODULE__.t, any(), Query.t) :: __MODULE__.t
  defp load_items(%__MODULE__{} = data, repo, %Query{} = query) do
    try do
      repo.__info__(:functions)
    rescue
      _ -> []
    end
    |> Keyword.has_key?(:all)
    |> case do
         true -> %{data | items: apply(repo, :all, [query])}
         false ->
           message = "error of calling `Repo.all/2`, make sure that your `#{repo}` module is correct `Ecto.Repo` implementation"
           raise(DataProvider.RepoCallError, message: message)
       end
  end

  # load `:total_count` into received `DataProvider.Data`

  @spec load_total_count(__MODULE__.t, any(), Query.t) :: __MODULE__.t
  defp load_total_count(%__MODULE__{} = data, repo, %Query{} = query) do
    try do
      repo.__info__(:functions)
    rescue
      _ -> []
    end
    |> Keyword.has_key?(:aggregate)
    |> case do
         true -> %{data | total_count: apply(repo, :aggregate, [query, :count])}
         false ->
           message = "error of calling `Repo.aggregate/2`, make sure that your `#{repo}` module is correct `Ecto.Repo` implementation"
           raise(DataProvider.RepoCallError, message: message)
       end
  end
end