Sheetshow.Schema (Sheetshow v0.1.0)

Copy Markdown View Source

What a row means: a keyword list of {name, type}, and the casting either way across it.

iex> schema = [item: :string, cost: :decimal, on: :date]
iex> Sheetshow.Schema.validate(schema)
:ok

A schema is a plain keyword list, not a struct, so it composes with ++ and reads as data in a config file. The order is the column order.

TypeWritten fromRead back as
:stringString.t()String.t()
:integerinteger()integer()
:floatnumber()float()
:booleantrue / falsetrue / false
:dateDate.t()Date.t()
:datetimeNaiveDateTime.t() / DateTime.t()NaiveDateTime.t()
:timeTime.t()Time.t()
:decimala numeric String.t()String.t()
:jsonany term JSON can encodethe decoded term

:decimal is text on both sides, so a double never touches it and 1000.00 stays 1000.00. It is a string here because Sheetshow has no dependencies: Decimal.to_string/1 on the way in and Decimal.new/1 on the way out is the whole of it, and your app decides which library that is.

Writing is strict. encode/2 refuses a value of the wrong type or a key the schema does not have: your code wrote it, so your code should hear about it. Reading is lenient. cast/2 is given whatever a human left in the cell, so it coerces what Sheets itself would have coerced (a number in a :string column, a whole number in an :integer one), and where a value will not cast at all it hands back nil and an error beside it rather than failing the read.

Summary

Functions

The values of one row, in schema order, as a record, with an error beside each cell that would not cast.

Reads a cell the way a :boolean column does, which is also how a log reads its deleted flag. Blank is false; TRUE, true and 1 are true.

The column names, in order, as they read in a header row.

A record as the cell values of one row, in schema order. A column the record says nothing about is nil, which is an empty cell.

Every type a column can have.

Checks a schema: at least one column, names that are atoms and appear once, types from types/0.

Types

fields()

@type fields() :: %{optional(name()) => term()}

name()

@type name() :: atom()

t()

@type t() :: [{name(), type()}]

type()

@type type() ::
  :string
  | :integer
  | :float
  | :boolean
  | :date
  | :datetime
  | :time
  | :decimal
  | :json

Functions

cast(values, schema)

@spec cast([term()], t()) :: {fields(), %{optional(name()) => Sheetshow.Error.t()}}

The values of one row, in schema order, as a record, with an error beside each cell that would not cast.

Nothing here fails: a cell Sheets or a person left in a state the column did not expect comes back as nil in the record and an entry in the errors, so one bad cell costs you that field rather than the row or the read.

iex> Sheetshow.Schema.cast(["Rent", "1000.00"], item: :string, cost: :decimal)
{%{item: "Rent", cost: "1000.00"}, %{}}

iex> {record, errors} = Sheetshow.Schema.cast(["abc"], cost: :integer)
iex> {record.cost, errors.cost.reason}
{nil, :cast}

cast_boolean(value)

@spec cast_boolean(term()) :: {:ok, boolean()} | :error

Reads a cell the way a :boolean column does, which is also how a log reads its deleted flag. Blank is false; TRUE, true and 1 are true.

iex> Sheetshow.Schema.cast_boolean("TRUE")
{:ok, true}
iex> Sheetshow.Schema.cast_boolean("")
{:ok, false}
iex> Sheetshow.Schema.cast_boolean("perhaps")
:error

columns(schema)

@spec columns(t()) :: [String.t()]

The column names, in order, as they read in a header row.

iex> Sheetshow.Schema.columns(item: :string, cost: :decimal)
["item", "cost"]

encode(record, schema)

@spec encode(fields(), t()) ::
  {:ok, [Sheetshow.Value.t()]} | {:error, Sheetshow.Error.t()}

A record as the cell values of one row, in schema order. A column the record says nothing about is nil, which is an empty cell.

Strict: a value of the wrong type, or a key the schema has no column for, is an error.

iex> Sheetshow.Schema.encode(%{item: "Rent", cost: "1000.00"}, item: :string, cost: :decimal)
{:ok, ["Rent", "1000.00"]}

iex> {:error, %Sheetshow.Error{reason: :invalid_record}} =
...>   Sheetshow.Schema.encode(%{cost: 1000.0}, cost: :decimal)

types()

@spec types() :: [type()]

Every type a column can have.

iex> :decimal in Sheetshow.Schema.types()
true

validate(schema)

@spec validate(term()) :: :ok | {:error, Sheetshow.Error.t()}

Checks a schema: at least one column, names that are atoms and appear once, types from types/0.

iex> {:error, %Sheetshow.Error{reason: :invalid_schema}} =
...>   Sheetshow.Schema.validate(item: :text)