lib/table/reader.ex

defprotocol Table.Reader do
  @moduledoc """
  A protocol to read tabular data.
  """

  @typedoc """
  Describes row-based traversal.

  The enumerable should yield rows, where each row is a series of
  values. The values should follow the order of columns in the
  metadata.
  """
  @type row_reader :: {:rows, metadata(), Enumerable.t()}

  @typedoc """
  Describes column-based traversal.

  The enumerable should yield columns, where each column is a series
  of values. The columns should have the same order as columns in the
  metadata.
  """
  @type column_reader :: {:columns, metadata(), Enumerable.t()}

  @typedoc """
  Table metadata.

  User-specific metadata can be added in the form of namespaced
  tuples as keys. The first element is the namespace key, typically an
  atom, and the second is any term. The value can also be anything.
  """
  @type metadata :: %{
          required(:columns) => list(Table.column()),
          optional(:count) => non_neg_integer(),
          optional({term(), term()}) => any()
        }

  @doc """
  Returns information on how to traverse the given tabular data.

  There are generally two distinct ways of traversing tabular data,
  that is, a row-based one and a column-based one. Depending on the
  underlying data representation, one of them is more natural and
  more efficient.

  The `init/1` return value describes either of the two traversal
  types, see `t:row_reader/0` and `t:column_reader/0` respectively.

  Some structs may be tabular only in a subset of cases, therefore
  `:none` may be returned to indicate that there is no valid data to
  read.

  The `init/1` function should not initiate any form of traversal
  or open existing resources. If any setup is necessary, it should
  be peformed when the underlying row or column enumerables are
  traversed.
  """
  @spec init(t()) :: row_reader() | column_reader() | :none
  def init(tabular)
end

defimpl Table.Reader, for: List do
  def init(list) do
    with :none <- Table.Reader.Enumerable.init_columns(list),
         :none <- Table.Reader.Enumerable.init_rows(list),
         do: :none
  end
end

defimpl Table.Reader, for: Map do
  def init(map) do
    Table.Reader.Enumerable.init_columns(map)
  end
end