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

ADR-0004 decision 6: the read check over reads handed in as data.

A chart's conditions read paths: `_event.data.due_on` on a transition
taken on `loan.renewed`, or `loan.copy_id` from the chart's own data.
`undeclared/2` is given a datamodel document and a list of those reads,
and answers the ones the document does not declare, each with why.

This package never parses an expression and takes no dependency to do so.
The paths come from the expression language: a host lists the reads of
each condition with `Predicator.Analysis.references/1`, adds the event
names the condition is read under and a location of its own, and hands
the list in. The how-to guide "Check the paths a chart's conditions read"
shows the few lines that takes.

## A read

A read is a map of three keys:

  * `events` - the event names the condition is read under: a
    transition's event descriptors, `[]` for a condition no event guards;
  * `path` - the path the condition reads, in
    `Predicator.Analysis.references/1`'s form: each segment a string, an
    integer for a literal index, or `:dynamic` for a computed key;
  * `location` - anything the host wants back. The check never reads it.

## The rules

  1. **A read rooted at `_event` whose second segment is `data`** is
     checked against the payload type of every declared event its names
     match. A read under several names must be declared on each event
     they match.
  2. **An event name matches as SCXML matches a descriptor**: a declared
     name equal to it, or one it is a token prefix of (`loan` matches
     `loan.renewed`, `loan.re` does not), and `*` matches every declared
     event. A name ending `.*` matches as the name without it does.
  3. **The `_event` system fields are not payload reads.** A read of
     `_event` alone, or of `name`, `type`, `sendid`, `origin`,
     `origintype` or `invokeid` under it, is skipped. Any other field
     under `_event` is one no event carries, and is reported on every
     event the read's names match.
  4. **A path beneath `_event.data` is declared when each segment is.** A
     string names a field of a declared record or shape and continues with
     that field's type; an integer descends a `list` into its `item_type`.
     A type that is unknown - a name the document does not declare, which
     includes a field type indexed `nil` - declares whatever lies beneath
     it, and so does a `list` whose item type is unknown. `object` and the
     other scalars declare no member.
  5. **A `:dynamic` segment ends the checked prefix there.** What follows a
     computed key is not checked.
  6. **A read whose names match no declared event is reported as such.**
  7. **Any other root is checked against the declared paths, and only when
     the document declares one.** The read's string segments, up to the
     first that is not a string, joined with `.`, must be in
     `StatifierDatamodel.Document.declared_paths/1`'s set. A document
     declaring no path reports nothing for such a read.

An input that is not a datamodel document - one
`StatifierDatamodel.Index.index/1` does not admit - has nothing to check a
read against, and reports nothing.

## An improper list is not a list

An improper list, `[read | :more]`, is a term no JSON decoder and no
expression analysis produces, and the check reads one as a value that is
not a list, as `StatifierDatamodel.Conformance` does. Reads handed in as
an improper list report nothing, as reads that are not a list do. A read
whose `path` is an improper list is not a read, and reports nothing. A
read whose `events` is an improper list names no event, so a read under
`_event` matches none and is `:no_declared_event`. In the document, an
improper list anywhere is read as absent: an improper `scopes` is not a
datamodel document, and an improper `entries`, `fields`, `types` or
`events` declares nothing.

## The answer

`{read, reason}` pairs, in the order the reads came in, the read exactly
as it was handed in. `reason` is one of three:

  * `:no_declared_event` - the read is under `_event` and its names match
    no event the document declares;
  * `{:undeclared, event_name, prefix}` - the payload of the declared
    event `event_name` does not declare the path; `prefix` is the read's
    path up to and including the first segment that is not declared. A
    read under names matching several events carries one pair for each
    event it fails on, in the document's `events` order;
  * `{:undeclared_path, prefix}` - a read of another root that the
    declared paths do not hold; `prefix` is the checked prefix, the read's
    string segments up to the first that is not a string.

A read that is declared, skipped or not a read at all is not in the
answer.

## Advisory, never a gate

Every answer is a fact. Nothing refuses a document and nothing raises;
whether a reported read stops a host's pipeline is the host's to decide
(ADR-0001 decision 12).

# `read`

```elixir
@type read() :: %{events: [String.t()], path: [segment()], location: term()}
```

One read a condition makes: the event names it is read under, the path it
reads, and a location the check passes back untouched.

# `reason`

```elixir
@type reason() ::
  :no_declared_event
  | {:undeclared, String.t(), [segment()]}
  | {:undeclared_path, [String.t()]}
```

Why a read is reported.

# `segment`

```elixir
@type segment() :: String.t() | integer() | :dynamic
```

One step of a path, in `Predicator.Analysis.references/1`'s form: a name,
a literal integer index, or `:dynamic` for a computed key.

# `undeclared`

```elixir
@spec undeclared(term(), [read()]) :: [{read(), reason()}]
```

The reads `document` does not declare, in input order, each paired with
why.

The library loan. The document declares two local paths and the payloads
of the two events a loan chart receives:

    iex> alias StatifierDatamodel.Reads
    iex> document = %{
    ...>   "version" => 1,
    ...>   "scopes" => [%{"scope" => "local", "entries" => [
    ...>     %{"name" => "copy_id", "path" => "loan.copy_id", "type" => "string"},
    ...>     %{"name" => "due_on", "path" => "loan.due_on", "type" => "date"}]}],
    ...>   "types" => [
    ...>     %{"name" => "loans.renewal", "kind" => "record", "fields" => [
    ...>       %{"name" => "copy_id", "type" => "string", "required?" => true},
    ...>       %{"name" => "due_on", "type" => "date", "required?" => true}]},
    ...>     %{"name" => "loans.return", "kind" => "record", "fields" => [
    ...>       %{"name" => "copy_id", "type" => "string", "required?" => true},
    ...>       %{"name" => "returned_at", "type" => "datetime", "required?" => true}]}],
    ...>   "events" => [
    ...>     %{"name" => "loan.renewed", "type" => "loans.renewal"},
    ...>     %{"name" => "loan.returned", "type" => "loans.return"}]}
    iex> Reads.undeclared(document, [
    ...>   %{events: ["loan.renewed"], path: ["_event", "data", "due_on"], location: 1},
    ...>   %{events: ["loan"], path: ["_event", "data", "due_on"], location: 2},
    ...>   %{events: ["patron.registered"], path: ["_event", "data", "patron_id"], location: 3},
    ...>   %{events: ["loan.renewed"], path: ["_event", "name"], location: 4},
    ...>   %{events: [], path: ["loan", "due_on"], location: 5},
    ...>   %{events: [], path: ["loan", "fine_cents"], location: 6}])
    [
      {%{events: ["loan"], path: ["_event", "data", "due_on"], location: 2},
       {:undeclared, "loan.returned", ["_event", "data", "due_on"]}},
      {%{events: ["patron.registered"], path: ["_event", "data", "patron_id"], location: 3},
       :no_declared_event},
      {%{events: [], path: ["loan", "fine_cents"], location: 6},
       {:undeclared_path, ["loan", "fine_cents"]}}
    ]

A computed key ends what is checked, and a document that declares no path
checks no other root:

    iex> alias StatifierDatamodel.Reads
    iex> document = %{"scopes" => [],
    ...>   "events" => [%{"name" => "loan.renewed", "type" => "object"}]}
    iex> Reads.undeclared(document, [
    ...>   %{events: ["loan.renewed"], path: ["_event", "data", :dynamic, "title"], location: nil},
    ...>   %{events: ["loan.renewed"], path: ["_event", "data", "title"], location: nil},
    ...>   %{events: [], path: ["loan", "fine_cents"], location: nil}])
    [{%{events: ["loan.renewed"], path: ["_event", "data", "title"], location: nil},
      {:undeclared, "loan.renewed", ["_event", "data", "title"]}}]

---

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