Riddler.Screens.Document (Riddler v0.1.0)

Copy Markdown View Source

The screen document: what a host declares, and why one is refused.

A document is the contract between a host that authors content and any runtime that shows it. This module is the two halves of admitting one. admit/1 turns decoded JSON into the struct the rest of the package reads, or says the input is not a document at all. validate/1 takes that struct and returns every reason the document is wrong, all of them, so an author fixing what they wrote learns everything in one pass.

Neither raises on anything a host can produce, and neither consults a context: a document is wrong or right on its own, before a visitor exists, which is what lets an editor answer the author while the author is still looking at it.

What admit/1 takes and what it builds

It takes the decoded JSON map - string keys throughout, as Jason.decode!/1 gives - and builds a struct whose fields are atoms. The two are different things: what the host writes is JSON, and what this package reads is the struct. A screen is %{key: ..., title: ..., nodes: [...]} and a node is an atom-keyed map carrying :type, its :key and :condition where the document declared them, and the fields its type names. metadata is the one map that keeps its string keys: it is an open map by declaration, and a host may put what it likes beside name, description and domain.

The envelope's kind names the content kind the document belongs to, and a document that omits it is a screen document: the admitted struct carries "screens" for it, so a document authored before kinds existed is admitted unchanged. A kind this package has no runtime for is carried as it was written and refused by validate/1.

admit/1 answers nil for an input that is not a screen document - not a map, or a map whose spine is not a document's: no list of screens, a screen that is not an object, a screen with no list of nodes, a node that is not an object, a variant whose candidates are not a list. That is the whole of what it refuses. Everything else - a missing field, a key of the wrong shape, a level of 9, a condition that does not parse - is a document that exists and is wrong, and saying which is validate/1's job.

A field this version does not know is not carried onto the admitted node. metadata is the declared place for what a host wants to keep beside the vocabulary, and the admitted node map is atom-keyed by declaration, so there is nowhere on a node for an unrecognized key to live. Whether such a key should also be a finding is not decided by the record this module implements, and this version does not raise one.

The findings validate/1 raises

One code per check, stable, and a host switches on the code rather than on the wording:

  • document.unknown_kind - the envelope names a content kind this package has no runtime for. The finding carries the value and no node key, because the envelope is the document's and not any one node's.
  • document.unknown_type - the registry has no such type.
  • document.duplicate_key - a key used twice anywhere in the document.
  • document.invalid_key - a key missing, or not matching [a-z][a-z0-9_]*.
  • document.missing_field - a field the node's type requires.
  • document.level_out_of_range - a heading level outside 1 to 6.
  • document.invalid_condition - a condition that does not parse.
  • document.invalid_template - a template field holding a construct outside the template subset. The finding carries the construct the template refused, and the node's key.
  • document.invalid_writes - a write that does not address a response, or whose value is not the constant form.
  • document.unknown_format - a format name this package does not know.
  • document.empty_variant - a variant with no candidates.
  • document.unreachable_variant_candidate - an unconditional candidate that is not last, which buries every candidate after it.

Examples

iex> doc = Riddler.Screens.Document.admit(%{
...>   "schema_version" => 1,
...>   "id" => "edoc_signup",
...>   "screens" => [%{"key" => "account", "title" => "Create your account", "nodes" => [
...>     %{"type" => "heading", "key" => "account_heading", "level" => 1, "text" => "Create your account"}
...>   ]}]
...> })
iex> {:ok, ^doc} = Riddler.Screens.Document.validate(doc)
iex> hd(hd(doc.screens).nodes).key
"account_heading"

iex> Riddler.Screens.Document.admit("not a document")
nil

Summary

Types

An admitted screen: its key, its title, and its nodes in order.

t()

An admitted document.

Functions

Turns a decoded JSON document into the struct, or answers nil.

Returns {:ok, document} or every finding against it.

Types

screen()

@type screen() :: %{
  key: term(),
  title: term(),
  nodes: [Riddler.Screens.Type.node_t()]
}

An admitted screen: its key, its title, and its nodes in order.

t()

@type t() :: %Riddler.Screens.Document{
  id: term(),
  kind: term(),
  metadata: %{optional(String.t()) => term()},
  schema_version: term(),
  screens: [screen()]
}

An admitted document.

Functions

admit(raw)

@spec admit(term()) :: t() | nil

Turns a decoded JSON document into the struct, or answers nil.

Total: it never raises, whatever it is handed. nil means the input is not a screen document. A document that is wrong rather than absent is admitted here and refused by validate/1.

iex> Riddler.Screens.Document.admit(%{"screens" => []})
%Riddler.Screens.Document{schema_version: nil, kind: "screens", id: nil, metadata: %{}, screens: []}

iex> Riddler.Screens.Document.admit(%{"screens" => "three of them"})
nil

validate(document)

@spec validate(t()) :: {:ok, t()} | {:error, [Riddler.Finding.t()]}

Returns {:ok, document} or every finding against it.

The checks are the document's own - keys, conditions, templates, duplicates, and the fields each type requires - together with the checks each type raises about itself. Findings come back in document order, node by node, with the duplicate keys first because they are about the document rather than about any one node.

iex> doc = Riddler.Screens.Document.admit(%{"screens" => [
...>   %{"key" => "account", "title" => "Create your account", "nodes" => [
...>     %{"type" => "carousel", "key" => "pictures"}
...>   ]}
...> ]})
iex> {:error, [finding]} = Riddler.Screens.Document.validate(doc)
iex> {finding.code, finding.node_key}
{"document.unknown_type", "pictures"}