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, inPredicator.Analysis.references/1's form: each segment a string, an integer for a literal index, or:dynamicfor a computed key;location- anything the host wants back. The check never reads it.
The rules
- A read rooted at
_eventwhose second segment isdatais 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. - An event name matches as SCXML matches a descriptor: a declared
name equal to it, or one it is a token prefix of (
loanmatchesloan.renewed,loan.redoes not), and*matches every declared event. A name ending.*matches as the name without it does. - The
_eventsystem fields are not payload reads. A read of_eventalone, or ofname,type,sendid,origin,origintypeorinvokeidunder it, is skipped. Any other field under_eventis one no event carries, and is reported on every event the read's names match. - A path beneath
_event.datais 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 alistinto itsitem_type. A type that is unknown - a name the document does not declare, which includes a field type indexednil- declares whatever lies beneath it, and so does alistwhose item type is unknown.objectand the other scalars declare no member. - A
:dynamicsegment ends the checked prefix there. What follows a computed key is not checked. - A read whose names match no declared event is reported as such.
- 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 inStatifierDatamodel.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_eventand its names match no event the document declares;{:undeclared, event_name, prefix}- the payload of the declared eventevent_namedoes not declare the path;prefixis 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'seventsorder;{:undeclared_path, prefix}- a read of another root that the declared paths do not hold;prefixis 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).
Summary
Types
One read a condition makes: the event names it is read under, the path it reads, and a location the check passes back untouched.
Why a read is reported.
One step of a path, in Predicator.Analysis.references/1's form: a name,
a literal integer index, or :dynamic for a computed key.
Functions
The reads document does not declare, in input order, each paired with
why.
Types
One read a condition makes: the event names it is read under, the path it reads, and a location the check passes back untouched.
@type reason() :: :no_declared_event | {:undeclared, String.t(), [segment()]} | {:undeclared_path, [String.t()]}
Why a read is reported.
One step of a path, in Predicator.Analysis.references/1's form: a name,
a literal integer index, or :dynamic for a computed key.
Functions
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"]}}]