StatifierDatamodel.Reads (StatifierDatamodel v0.7.1)

Copy Markdown View Source

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).

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

read()

@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()

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

Why a read is reported.

segment()

@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.

Functions

undeclared(document, reads)

@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"]}}]