The index over a datamodel document's events and sends keys
(ADR-0004 decisions 1-5): per event name, the type of its payload.
events declares the events a chart receives, by name, and the type of
each one's payload - the value a chart's conditions read under
_event.data. sends declares the events a chart sends, by name, and the
type of the payload each carries. Both keys are optional: a document
without either declares no events or no sends, which is the state every
document written before the two keys existed is in.
from_document/1 is the index. It reads events, sends and, to resolve
each entry's type, types; 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, and index/1 does not read either key:
every answer it gives is the answer it gave before the keys existed.
An entry: name and type required
An entry carries name (the event name as a chart spells it) and type
(the payload's type), each a non-empty string, and the optional label
(what a pane renders) and note (prose for a reader, carrying no
contract). An entry missing name or type, or carrying either as
anything but a non-empty string, declares nothing: it is dropped rather
than raised on, and the rest of the list is indexed around it - ADR-0001
decision 6's posture for a half-written declaration. A label or note
that is not a string is nil.
Each list keeps its own order, and a repeated name keeps its first
occurrence, as types does. The two lists are separate: a name may appear
in both, and an entry of one never shadows an entry of the other.
A type resolves, or it is unknown
type is one of the nine types the set is closed at, looked at first, or
the name of a record or shape the types key declares, which is
{:declared, name}. A spelling that names neither is :unknown: unknown,
not wrong. There is no inline spelling: a type that is a map spelling a
shape is not a string, so the entry declares nothing.
Reported, never refused
An entry whose type is :unknown stays in the index with that type, and
unresolved/1 lists the entries whose type resolves to nothing. A document
with such an entry indexes and answers every other question exactly as it
would without it; which unresolved entry matters to a host is the host's
to decide (ADR-0001 decision 12).
Not declared paths
The two keys contribute no path to
StatifierDatamodel.Index.declared_paths/1 and nothing to
StatifierDatamodel.Index.path_types/1, as types contributes none. The
event scope is a different thing: one scope of paths every event carries,
in the document's own addressing. An events entry declares one event's
payload, read under _event.data, and the two do not overlap.
Summary
Types
One entry of events or sends, as indexed: its name, its payload's type
expression (:unknown for a spelling that names nothing), and its label
and note, each nil when the document gives no string.
The two sections, by the key a document spells.
Each section in list order, the first occurrence of a name kept.
Functions
The entry of section named name.
Indexes a decoded datamodel document's events and sends keys.
The entries whose type resolves to nothing, each section in its own list order.
Types
@type entry() :: %{ name: String.t(), type: StatifierDatamodel.Types.t(), label: String.t() | nil, note: String.t() | nil }
One entry of events or sends, as indexed: its name, its payload's type
expression (:unknown for a spelling that names nothing), and its label
and note, each nil when the document gives no string.
@type section() :: :events | :sends
The two sections, by the key a document spells.
Each section in list order, the first occurrence of a name kept.
Functions
The entry of section named name.
:error for a name the section does not hold, for a section other than
:events or :sends, and for anything that is not an index: an entry
there would be a false claim.
iex> alias StatifierDatamodel.Events
iex> events = Events.from_document(%{"events" => [
...> %{"name" => "loan.returned", "type" => "object", "label" => "Copy returned"}]})
iex> {:ok, entry} = Events.fetch(events, :events, "loan.returned")
iex> {entry.type, entry.label}
{:object, "Copy returned"}
iex> Events.fetch(events, :sends, "loan.returned")
:error
iex> Events.fetch(events, :events, "loan.lost")
:error
Indexes a decoded datamodel document's events and sends keys.
Total: every input has an answer and none of them raise. A document without the keys declares no events and no sends, and so does an input that is not a document.
iex> alias StatifierDatamodel.Events
iex> events = Events.from_document(%{
...> "scopes" => [],
...> "types" => [
...> %{"name" => "loans.renewal", "kind" => "record", "label" => "Renewal",
...> "fields" => [%{"name" => "due_on", "type" => "date", "required?" => true}]}],
...> "events" => [
...> %{"name" => "loan.renewed", "type" => "loans.renewal", "label" => "Loan renewed"},
...> %{"name" => "loan.lost", "type" => "loans.loss"},
...> %{"name" => "loan.renewed", "type" => "object"},
...> %{"type" => "object", "label" => "No name"}],
...> "sends" => [
...> %{"name" => "patron.due_soon", "type" => "object", "note" => "Sent before the due date."}]})
iex> Enum.map(events.events, &{&1.name, &1.type})
[{"loan.renewed", {:declared, "loans.renewal"}}, {"loan.lost", :unknown}]
iex> [notice] = events.sends
iex> {notice.name, notice.type, notice.label, notice.note}
{"patron.due_soon", :object, nil, "Sent before the due date."}
iex> StatifierDatamodel.Events.from_document(%{"scopes" => []})
%{events: [], sends: []}
iex> StatifierDatamodel.Events.from_document(["loan.renewed"])
%{events: [], sends: []}
The entries whose type resolves to nothing, each section in its own list order.
A fact, not a refusal: every entry listed here is still in the index, typed
:unknown, and every other answer stands. Anything that is not an index
lists nothing.
iex> alias StatifierDatamodel.Events
iex> events = Events.from_document(%{
...> "events" => [
...> %{"name" => "loan.renewed", "type" => "object"},
...> %{"name" => "loan.lost", "type" => "loans.loss"}],
...> "sends" => [%{"name" => "patron.due_soon", "type" => "notices.due_soon"}]})
iex> unresolved = Events.unresolved(events)
iex> {Enum.map(unresolved.events, & &1.name), Enum.map(unresolved.sends, & &1.name)}
{["loan.lost"], ["patron.due_soon"]}