# `Macaw.Data.Schema`
[🔗](https://github.com/dcoai/macaw_data/blob/v0.5.3/lib/macaw/data/schema.ex#L1)

A source's schema: what each of its fields means (spec/01 §1.2).

A schema maps a field, named as the source's rows name it (an atom or a string), to a
**description**: its `type` (`:time`, `:number`, `:category`, `:text`, `:color` or
`:point`, a geographic position), and as much of `unit`, `quantity`, `label`, `order`,
`status`, `polarity`, `target`, `continuity`, `window`, `metric_kind`, `bounds` and
`identity` as the source knows, and for a `:category` field the `values` it takes,
every one. It says what the numbers *are*, whoever draws them, and never how they are
drawn: a description has no color key, since color is the theme's to choose from the
meaning. A declared value may be an entry, `%{value: v, name: "Display", color: c}`,
whose color is a data color, a literal (spec/01 §1.7).

A `unit` is a UCUM code `Macaw.Data.Unit` reads and a `quantity` one of
`Macaw.Data.Quantity.all/0`, and when both are given the unit measures the quantity
(spec/01 §1.6). A field whose value names the thing a row is about says
`identity: true`, several such fields together naming it (spec/01 §1.4). The window
field is `window_field/1`'s (spec/01 §1.5).

A schema is identified by its `version`: the kernel's digest of its map form
(`Macaw.Layers.Document.digest/1`, spec/01 §1.10), the same on every node and under
every release, and new with any change of content. Names, states and roles count by
their spelling, so `:latency` and `"latency"` are one field of one version, as they are
after a trip through JSON. A `Macaw.Data.Delivery` names the version its rows were
produced under; `Macaw.Data.Schema.Cache` holds schemas by `{source, version}`.
`compatibility/2` says when one schema's rows read as another's (spec/01 §1.9).
`to_json/1` and `from_json/2` are its public JSON form (spec/01 §1.11).

Ported from visualize's `Visualize.Data.Schema`, with the `:point` type, literal data
colors, the window field, the kernel's digest and a public JSON form.

## Examples

    iex> {:ok, schema} =
    ...>   Macaw.Data.Schema.new(%{
    ...>     t: %{type: :time},
    ...>     latency: %{type: :number, unit: "s", quantity: :duration, label: "Request time"},
    ...>     state: %{type: :category, order: ["ok", "warn", "fail"],
    ...>              status: %{"ok" => :good, "warn" => :warning, "fail" => :bad}}
    ...>   })
    iex> Macaw.Data.Schema.fetch(schema, "latency", :unit)
    {:ok, "s"}
    iex> String.length(Macaw.Data.Schema.version(schema))
    64
    iex> Macaw.Data.Schema.new(%{state: %{type: :category, colors: %{"ok" => "#0a0"}}})
    {:error, [{[:state, :colors], {:color_key, :colors}}]}

# `description`

```elixir
@type description() :: %{
  :type =&gt; :time | :number | :category | :text | :color | :point,
  optional(:unit) =&gt; String.t(),
  optional(:quantity) =&gt; atom(),
  optional(:label) =&gt; String.t(),
  optional(:order) =&gt; [String.t() | atom() | number(), ...],
  optional(:status) =&gt; %{required(term()) =&gt; atom()},
  optional(:polarity) =&gt; :higher_is_better | :lower_is_better | :neutral,
  optional(:target) =&gt; number(),
  optional(:continuity) =&gt; number(),
  optional(:window) =&gt; boolean(),
  optional(:metric_kind) =&gt; :gauge | :counter | :rate,
  optional(:bounds) =&gt; [number() | nil],
  optional(:identity) =&gt; boolean(),
  optional(:values) =&gt; [value() | value_entry(), ...]
}
```

What one field means (spec/01 §1.2).

# `error`

```elixir
@type error() :: {[name() | atom() | non_neg_integer() | term()], term()}
```

A fault of `new/1`: the path from the schema to the value, and why.

# `incompatibility`

```elixir
@type incompatibility() ::
  :missing
  | {:type, atom(), atom()}
  | {:unit, term(), term()}
  | {:identity, boolean(), boolean()}
```

Why a field of one schema cannot read another's rows (spec/01 §1.2, visualize D-159).

# `name`

```elixir
@type name() :: atom() | String.t()
```

A field name, as the feed's rows spell it.

# `t`

```elixir
@type t() :: %Macaw.Data.Schema{
  fields: %{required(name()) =&gt; description()},
  identity: [name()] | nil,
  version: String.t()
}
```

# `value`

```elixir
@type value() :: String.t() | atom() | number()
```

A category field's value: a string, an atom or a number.

# `value_entry`

```elixir
@type value_entry() :: %{
  :value =&gt; value(),
  optional(:name) =&gt; String.t(),
  optional(:color) =&gt; String.t()
}
```

A declared value with what a person reads of it (spec/01 §1.2, visualize D-224): its display
`name`, and a `color` hint, a data color: a literal (spec/01 §1.7).

# `attributes`

```elixir
@spec attributes() :: [atom(), ...]
```

The fourteen description keys, `:type` first (spec/01 §1.2).

# `color?`

```elixir
@spec color?(term()) :: boolean()
```

Whether a value is a data color as a schema holds one (spec/01 §1.7): a literal, `#rgb`,
`#rgba`, `#rrggbb` or `#rrggbbaa` in hexadecimal, or a CSS named color. A theme role or a
typed color is a drawing color, which a schema never names.

    iex> Macaw.Data.Schema.color?("#2e7d32")
    true
    iex> Macaw.Data.Schema.color?("Orange")
    true
    iex> Macaw.Data.Schema.color?({:color, :good})
    false

# `compatibility`

```elixir
@spec compatibility(t(), t()) :: :ok | {:error, [{name(), incompatibility()}, ...]}
```

Whether `other`'s rows read as `schema`'s (spec/01 §1.2, visualize D-159): for each field of
`schema`, `other` has a field of the same name compared as strings, of the same type,
and for a `:number` field a unit that converts into `schema`'s — two absent units
agree, one absent facing a present one does not. `{:error, faults}` lists every fault,
`field` spelled as `schema` spells it, ordered by its name as a string. Labels and facts
are not compared, and `other`'s extra fields are not faults — except identity (visualize D-167):
a field one schema declares `identity: true` and the other does not is `{:identity, a,
b}`, `a` and `b` whether each declares it, even a field only `other` has, since
`other`'s rows read as `schema`'s would merge the things `other` tells apart.

    iex> live = Macaw.Data.Schema.new!(%{t: %{type: :time}, value: %{type: :number, unit: "ms"}})
    iex> history = Macaw.Data.Schema.new!(%{"t" => %{type: :time}, "value" => %{type: :number, unit: "s", label: "Load"}})
    iex> Macaw.Data.Schema.compatibility(live, history)
    :ok
    iex> bytes = Macaw.Data.Schema.new!(%{"value" => %{type: :number, unit: "By"}})
    iex> Macaw.Data.Schema.compatibility(live, bytes)
    {:error, [{:t, :missing}, {:value, {:unit, "ms", "By"}}]}
    iex> hosts = Macaw.Data.Schema.new!(%{host: %{type: :category, identity: true}})
    iex> ifaces = Macaw.Data.Schema.new!(%{host: %{type: :category, identity: true},
    ...>   iface: %{type: :category, identity: true}})
    iex> Macaw.Data.Schema.compatibility(hosts, ifaces)
    {:error, [{:iface, {:identity, false, true}}]}

# `entry_keys`

```elixir
@spec entry_keys() :: [atom(), ...]
```

A value entry's three keys, `:value` first (spec/01 §1.3): what a codec reads an entry's
keys as, never making an atom of another. Public, so a host that encodes schemas itself
(metresis) reads it rather than a hidden function.

    iex> Macaw.Data.Schema.entry_keys()
    [:value, :name, :color]

# `fetch`

```elixir
@spec fetch(t(), name()) :: {:ok, description()} | :error
```

A field's description, by either spelling of its name (spec/01 §1.1).

    iex> schema = Macaw.Data.Schema.new!(%{"v" => %{type: :number}})
    iex> Macaw.Data.Schema.fetch(schema, :v)
    {:ok, %{type: :number}}
    iex> Macaw.Data.Schema.fetch(schema, :w)
    :error

# `fetch`

```elixir
@spec fetch(t(), name(), atom()) :: {:ok, term()} | :error
```

One attribute of a field's description; `:error` when the field or the attribute is absent.

# `format_error`

```elixir
@spec format_error(error()) :: String.t()
```

One fault of `new/1` as a line a person reads.

    iex> Macaw.Data.Schema.format_error({[:latency, :unit], {:invalid_for, :category}})
    "latency.unit: is not a key a :category field takes"

# `from_json`

```elixir
@spec from_json(binary(), keyword()) :: {:ok, t()} | {:error, [error(), ...]}
```

The schema a JSON document holds: read through the kernel's limits and scan first
(`Macaw.Layers.Limits`, `Macaw.Layers.Scan`), so a document over a limit is refused
before it is decoded, then decoded and read by `from_map/1`. `limits:` tightens them.

# `from_map`

```elixir
@spec from_map(term()) :: {:ok, t()} | {:error, [error(), ...]}
```

The schema a map form holds (spec/01 §1.11): `new/1` of what it reads. Each word is read
through its own closed vocabulary and every name, state and role is a string, so no atom
is made. A key no description or value entry has stays its string, for `new/1` to
refuse by name.

# `identity`

```elixir
@spec identity(t()) :: [name()]
```

The fields the schema declares `identity: true`, as it spells them, ordered by name as a
string; `[]` when it declares none (spec/01 §1.2, visualize D-167).

    iex> schema = Macaw.Data.Schema.new!(%{
    ...>   iface: %{type: :category, identity: true},
    ...>   host: %{type: :category, identity: true},
    ...>   bytes: %{type: :number}
    ...> })
    iex> Macaw.Data.Schema.identity(schema)
    [:host, :iface]

# `identity`

```elixir
@spec identity(t(), map()) :: term()
```

A row's identity under the schema (spec/01 §1.2, visualize D-167): the bare value of the one
identity field, a map from field to value, keyed as the schema spells them, for several,
and `nil` when the schema declares none or the row has no value for one of them. Each
field is read by either spelling of its name.

    iex> one = Macaw.Data.Schema.new!(%{id: %{type: :category, identity: true}, x: %{type: :number}})
    iex> Macaw.Data.Schema.identity(one, %{"id" => "frog-1", "x" => 3})
    "frog-1"
    iex> two = Macaw.Data.Schema.new!(%{host: %{type: :category, identity: true},
    ...>   iface: %{type: :category, identity: true}})
    iex> Macaw.Data.Schema.identity(two, %{host: "mx1", iface: "eth0", bytes: 9})
    %{host: "mx1", iface: "eth0"}
    iex> Macaw.Data.Schema.identity(two, %{host: "mx1"})
    nil

# `new`

```elixir
@spec new(term()) :: {:ok, t()} | {:error, [error(), ...]}
```

The schema of a map of field descriptions, or every fault of it by path (spec/01 §1.2).

    iex> Macaw.Data.Schema.new(%{v: %{type: :category, unit: "s"}})
    {:error, [{[:v, :unit], {:invalid_for, :category}}]}

    iex> Macaw.Data.Schema.new(%{v: %{type: :number, unit: "By", quantity: :duration}})
    {:error, [{[:v, :quantity], {:quantity, :duration, "By"}}]}

# `new!`

```elixir
@spec new!(term()) :: t()
```

The schema of a map of field descriptions, or `ArgumentError` naming every fault.

# `to_json`

```elixir
@spec to_json(t()) :: String.t()
```

The schema's JSON form: `to_map/1`, encoded.

# `to_map`

```elixir
@spec to_map(t()) :: [[term()]]
```

The schema's map form (spec/01 §1.11): the JSON-ready list of `[name, description]`
pairs, sorted by name, every name, state and role written by its spelling, every word
of the vocabulary (`type`, `quantity`, `polarity`, `metric_kind`) as its string.

# `value_colors`

```elixir
@spec value_colors(t(), name()) :: %{required(value()) =&gt; String.t()}
```

A map from each declared value with a `color` to its color, as written, a literal
(spec/01 §1.3); `%{}` when none has one, or the field declares no values. A drawing
library may use it as a color map; a design's own colors outrank it.

    iex> schema = Macaw.Data.Schema.new!(%{state: %{type: :category, values: [
    ...>   %{value: "ok", color: "#2e7d32"}, %{value: "warn", color: "orange"}]}})
    iex> Macaw.Data.Schema.value_colors(schema, :state)
    %{"ok" => "#2e7d32", "warn" => "orange"}

# `value_names`

```elixir
@spec value_names(t(), name()) :: %{required(value()) =&gt; String.t()}
```

A map from each declared value with a `name` to its name (spec/01 §1.2, visualize D-224); `%{}`
when none has one, or the field declares no values.

    iex> schema = Macaw.Data.Schema.new!(%{state: %{type: :category, values: [
    ...>   %{value: "ok", name: "OK"}, %{value: "down", color: "#c62828"}, "unknown"]}})
    iex> Macaw.Data.Schema.value_names(schema, "state")
    %{"ok" => "OK"}

# `values`

```elixir
@spec values(t(), name()) :: [value(), ...] | nil
```

A field's declared values, bare, in their declared order (spec/01 §1.2, visualize D-224): each
value entry's `value`, and each bare value as it is. `nil` when the schema has no such
field or the field declares no `values` — an open set, where `[]` would say a closed one
holding nothing. Read by either spelling of the field's name.

What compares or lists a field's values reads them here, never through `fetch/3`, whose
`:values` is the list as declared, entries and all.

    iex> schema = Macaw.Data.Schema.new!(%{state: %{type: :category, values: [
    ...>   %{value: "ok", name: "OK", color: "#2e7d32"}, "unknown"]}})
    iex> Macaw.Data.Schema.values(schema, :state)
    ["ok", "unknown"]
    iex> Macaw.Data.Schema.values(schema, :other)
    nil

# `version`

```elixir
@spec version(t()) :: String.t()
```

The schema's version: the SHA-256 of its canonical encoding as 64 lowercase hex digits
(spec/01 §1.10), computed once by `new/1`.

# `window_field`

```elixir
@spec window_field(t()) :: name() | nil
```

The schema's window field (spec/01 §1.5): the field a request's window applies to, by
which rows are ordered, trimmed and routed. It is the field described `window: true`,
else the schema's one `:time` field when it does not say `window: false`; `nil` when
there is neither, and a schema with several `:time` fields and none marked has none.

    iex> alias Macaw.Data.Schema
    iex> Schema.window_field(Schema.new!(%{t: %{type: :time}, v: %{type: :number}}))
    :t
    iex> Schema.window_field(Schema.new!(%{tick: %{type: :number, window: true}, v: %{type: :number}}))
    :tick
    iex> Schema.window_field(Schema.new!(%{a: %{type: :time}, b: %{type: :time}}))
    nil

# `window_kind`

```elixir
@spec window_kind(t()) :: :range | :box | nil
```

The window that applies to the schema's window field (spec/01 §1.5): `:range` over a
`:time` or a `:number` field, from one instant or value to another; `:box` over a
`:point` field, a region of positions; `nil` when the schema has no window field.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
