# `StatifierDatamodel.Declarations`
[🔗](https://github.com/riddler/statifier_datamodel/blob/v0.7.1/lib/statifier_datamodel/declarations.ex#L1)

The index over a datamodel document's `types` key (ADR-0001 decision 5):
the named record and shape declarations, by name.

A declaration is a `name`, a `kind` - `record` or `shape` - a `label`, and
an ordered list of fields. A **record** is a fact about what a write puts
at a path; a **shape** is a constraint a read places on a path. Identity
is nominal: two records with identical fields are two records. The one
widening between them is the read check, which is
`StatifierDatamodel.Types`'.

`from_document/1` is the index. It reads the `types` key and nothing else,
so a caller asking *is this a datamodel document at all* asks
`StatifierDatamodel.Index.index/1`, which is where that question is
answered; a document without the key declares no types, and so does an
input that is not a document.

## A half-written declaration declares nothing

Decision 6's addition, kept literally: a declaration missing `name`, `kind`
or `fields`, or whose `kind` is neither `record` nor `shape`, is dropped
rather than raised on, and the rest of the list is indexed around it.
`label` is not among those keys - the record's own type for a declaration
carries `label: String.t() | nil` - so a declaration with no usable label
still declares its fields, and a pane renders the name it has.

A repeated name keeps its first occurrence, in list order, which is the
rule `StatifierDatamodel.Index` already uses for a repeated path.

## A field's type resolves, or it is unknown

A field carries `name`, `type`, and the optional `required?` (default
`false`), `label` and `one_of`; a `list` field carries `item_type` under
the same rule. A field's type is one of the nine types the index closes
the set at, or the `name` of another declaration - resolved against the
whole list, so declarations may reference each other in either order. A
reference to a name the list does not declare is `nil`: unknown, on the
same stance an undeclared path gets, rather than a failed admission.

A field with no usable `name` contributes nothing - it can neither be
covered nor named in a coverage failure - and a repeated field name keeps
its first occurrence, for the same reason a repeated path does.

`note` is read by nobody: the record gives it to a human reader and says
it carries no contract, and the record's own type for a declaration and
for a field carries no `note` key. It is left in the document.

# `declaration`

```elixir
@type declaration() :: %{
  name: String.t(),
  kind: :record | :shape,
  label: String.t() | nil,
  fields: [field()]
}
```

One declaration: a nominal name, its kind, its label and its ordered fields.

# `field`

```elixir
@type field() :: %{
  name: String.t(),
  type: StatifierDatamodel.Types.t() | nil,
  item_type: StatifierDatamodel.Types.t() | nil,
  required?: boolean(),
  label: String.t() | nil,
  one_of: [term()] | nil
}
```

One field of a declaration, normalized.

`type` and `item_type` are type expressions (`t:StatifierDatamodel.Types.t/0`),
or `nil` for a spelling that names neither a type in the closed set nor a
declaration in this document.

# `t`

```elixir
@type t() :: %{optional(String.t()) =&gt; declaration()}
```

The declarations of one document, by name.

# `fetch`

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

The declaration named `name`.

    iex> alias StatifierDatamodel.Declarations
    iex> declarations = Declarations.from_document(%{"types" => [
    ...>   %{"name" => "Settleable", "kind" => "shape", "label" => "Settleable",
    ...>     "fields" => [%{"name" => "amount_cents", "type" => "integer", "required?" => true}]}]})
    iex> {:ok, shape} = Declarations.fetch(declarations, "Settleable")
    iex> shape.kind
    :shape
    iex> Declarations.fetch(declarations, "Refundable")
    :error

# `from_document`

```elixir
@spec from_document(term()) :: t()
```

Indexes a decoded datamodel document's `types` key to `name -> declaration`.

Total: every input has an answer and none of them raise. A document
without the key - the state every document written before the key existed
is in - declares nothing, and so does an input that is not a document.

    iex> alias StatifierDatamodel.Declarations
    iex> declarations = Declarations.from_document(%{"scopes" => [], "types" => [
    ...>   %{"name" => "cards.card", "kind" => "record", "label" => "Card",
    ...>     "fields" => [%{"name" => "last4", "type" => "string", "required?" => true}]},
    ...>   %{"name" => "cards.credit_txn", "kind" => "record", "label" => "Credit transaction",
    ...>     "fields" => [%{"name" => "card", "type" => "cards.card", "required?" => true}]}]})
    iex> Map.keys(declarations) |> Enum.sort()
    ["cards.card", "cards.credit_txn"]
    iex> {:ok, txn} = Declarations.fetch(declarations, "cards.credit_txn")
    iex> txn.kind
    :record
    iex> [card] = txn.fields
    iex> {card.type, card.required?}
    {{:declared, "cards.card"}, true}

    iex> StatifierDatamodel.Declarations.from_document(%{"scopes" => []})
    %{}
    iex> StatifierDatamodel.Declarations.from_document(["cards.card"])
    %{}

---

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