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.
  """
  @type metadata :: %{
          columns: list(Table.column())
        }

  @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.
  """
  @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