# `Riddler.Screens`
[🔗](https://github.com/riddler/riddler-ex/blob/v0.4.0/lib/riddler/screens.ex#L1)

A document against one visitor: what they are shown, and what could not be
decided.

`Riddler.Screens.Document` answers whether a document is well formed, on its
own, before anyone arrives. This module answers the other question: given a
host's `context` and a visitor's `responses`, which nodes are shown, what do
their templates say, and which container won. `resolve/2` answers it for a
whole document and `resolve_screen/3` for the one screen a host is about to
render.

Both are pure and total over anything an admitted document can hold: they
consult no store, raise on nothing, and report what they could not decide
rather than refusing it. Both report through the same pair of diagnostics:
`resolve/2` carries them on the resolved document, `resolve_screen/3` answers
with them beside the screen.

## The root

The root is the map a condition and a template both read, and it has exactly
two keys: `"context"`, what the host knows, and `"responses"`, what the
visitor has submitted so far. Both are string-keyed, because both are decoded
JSON, and a response is addressed `responses.<key>` where the key is the key
of the question node it belongs to.

## What resolution does to a node

A node with no condition is shown. A node whose condition evaluates false is
hidden silently, because that is the condition doing its job. A node whose
condition cannot be evaluated - a variable the root does not carry, an
operand of the wrong type, anything that is not true or false - is hidden
*and* reported in `undecidable_conditions`: the author asked a question the
runtime could not answer, and a visitor should not be shown a node on a
guess.

A container resolves to the first of its candidates whose condition holds,
and the winner replaces the container, so nothing downstream needs to know a
container was ever there. A container no candidate wins resolves to nothing.

Every template field - `text`, `label` and `placeholder`, and the `label` of
each of a choice question's `answer_options` - is rendered leniently: a
variable the root does not carry renders as the empty string and is reported
in `missing_variables`, so a visitor sees a screen rather than an error page.
An option's `value` is literal and is left as written. A node that is hidden is never rendered, so it contributes no
missing variables.

The output carries no `condition` anywhere, and carries `writes` through
untouched: what a button sets is the host's to apply when the visitor presses
it.

## What a set of responses has to satisfy

`validate_screen/3` and `validate_screen/4` answer the last question:
whether what the visitor typed is enough to submit the screen. They resolve
the screen first and check only what came back, so a question a condition hid
cannot fail, and they consult what the node declares - `required`, `format`,
and `min` and `max` on the numeric formats; a choice question's
`answer_options`, `select` and selection bounds; and a boolean question's
`true` or `false`. The arity-4 form takes the key of the button the visitor
pressed and honours its `validates`.

They take the same root `resolve/2` and `resolve_screen/3` take, and the
responses they check are the ones inside it. The screen validated is the
screen shown: a question the host's `context` made visible to this visitor
is a question this visitor can fail. A condition that root could not decide
is a finding rather than a pass wherever the press validates, because a node
hidden from the validator and not from the visitor is the one case this
cannot guess at.

    iex> document =
    ...>   Riddler.Screens.Document.admit(%{
    ...>     "schema_version" => 1,
    ...>     "id" => "edoc_checkout",
    ...>     "screens" => [
    ...>       %{
    ...>         "key" => "card",
    ...>         "title" => "Your card",
    ...>         "nodes" => [
    ...>           %{
    ...>             "type" => "text",
    ...>             "key" => "card_intro",
    ...>             "text" => "Charging {{ context.tenant_name }}."
    ...>           },
    ...>           %{
    ...>             "type" => "text",
    ...>             "key" => "card_expiring",
    ...>             "condition" => "context.card_expires_within_days < 30",
    ...>             "text" => "The card on file expires soon."
    ...>           }
    ...>         ]
    ...>       }
    ...>     ]
    ...>   })
    iex> root = %{"context" => %{"tenant_name" => "Acme", "card_expires_within_days" => 9}}
    iex> {:ok, resolved} = Riddler.Screens.resolve(document, root)
    iex> Enum.map(hd(resolved.screens).nodes, & &1.text)
    ["Charging Acme.", "The card on file expires soon."]
    iex> resolved.diagnostics
    %{missing_variables: [], undecidable_conditions: []}

# `resolve`

```elixir
@spec resolve(Riddler.Screens.Document.t(), map()) ::
  {:ok, Riddler.Screens.Resolved.t()}
```

Resolves every screen of a document against a root.

Always `{:ok, resolved}`: resolution reports rather than refuses, so a
condition it could not evaluate and a variable it could not find come back in
the resolved document's diagnostics.

The root is read for `"context"` and `"responses"` and nothing else; either
one absent is the same as it being empty.

    iex> document =
    ...>   Riddler.Screens.Document.admit(%{
    ...>     "schema_version" => 1,
    ...>     "id" => "edoc_signup",
    ...>     "screens" => [
    ...>       %{
    ...>         "key" => "account",
    ...>         "title" => "Create your account",
    ...>         "nodes" => [
    ...>           %{
    ...>             "type" => "text",
    ...>             "key" => "account_greeting",
    ...>             "condition" => "responses.first_name != ''",
    ...>             "text" => "Nice to meet you, {{ responses.first_name }}."
    ...>           }
    ...>         ]
    ...>       }
    ...>     ]
    ...>   })
    iex> {:ok, resolved} = Riddler.Screens.resolve(document, %{})
    iex> {hd(resolved.screens).nodes, resolved.diagnostics.undecidable_conditions}
    {[], [%{key: "account_greeting", condition: "responses.first_name != ''"}]}

# `resolve_screen`

```elixir
@spec resolve_screen(Riddler.Screens.Document.t(), term(), map()) ::
  {:ok, Riddler.Screens.Resolved.screen(),
   Riddler.Screens.Resolved.diagnostics()}
  | {:error, :no_such_screen}
```

Resolves the one screen a host is about to render.

`{:ok, screen, diagnostics}`: the screen, and what resolving *that* screen
could not decide. The diagnostics are the shape `resolve/2` carries on a
resolved document - `missing_variables` and `undecidable_conditions`, both in
document order - narrowed to the one screen. A caller is never handed a
screen whose resolution reported something without being handed the report,
which is what lets a single-screen host tell a node hidden by a condition
that held from a node hidden because the condition could not be decided.

Returns `{:error, :no_such_screen}` when the document declares no screen
under that key: asking for a screen that is not there is the host's mistake,
not a visitor's, and is worth an error rather than an empty screen.

    iex> document =
    ...>   Riddler.Screens.Document.admit(%{
    ...>     "schema_version" => 1,
    ...>     "id" => "edoc_signup",
    ...>     "screens" => [
    ...>       %{
    ...>         "key" => "account",
    ...>         "title" => "Create your account",
    ...>         "nodes" => [
    ...>           %{
    ...>             "type" => "text",
    ...>             "key" => "account_greeting",
    ...>             "condition" => "responses.first_name != ''",
    ...>             "text" => "Nice to meet you."
    ...>           }
    ...>         ]
    ...>       }
    ...>     ]
    ...>   })
    iex> {:ok, screen, diagnostics} = Riddler.Screens.resolve_screen(document, "account", %{})
    iex> {screen.key, screen.title, screen.nodes}
    {"account", "Create your account", []}
    iex> diagnostics
    %{
      missing_variables: [],
      undecidable_conditions: [
        %{key: "account_greeting", condition: "responses.first_name != ''"}
      ]
    }
    iex> Riddler.Screens.resolve_screen(document, "plan", %{})
    {:error, :no_such_screen}

# `validate_screen`

```elixir
@spec validate_screen(Riddler.Screens.Document.t(), term(), map()) ::
  :ok | {:error, [Riddler.Finding.t()]} | {:error, :no_such_screen}
```

Validates a visitor's responses against the screen they were shown.

`:ok`, or every reason the screen is not ready to be submitted. Checks run
over the *resolved* screen and nothing else: the screen is resolved against
the root the host resolved with, so a node a condition hid is a node the
visitor never saw and cannot be held to. A question the visitor was never
asked cannot fail.

The root is the one `resolve/2` and `resolve_screen/3` take, read for
`"context"` and `"responses"` and nothing else, and the responses that are
checked are the ones inside it. A screen validated is the screen shown: a
question a `context` condition made visible to this visitor is a question
this visitor can fail.

What is checked is what the node declares. `required` is unanswered when the
response is absent or is a string of whitespace. `format` names one of the
validation formats the internal Riddler.Screens.Document.formats/0 lists,
and a blank response that is not required is not put to it - a format has
nothing to say about text a visitor did not type. The numeric formats also
honour `min` and `max` where the question declares them. A question whose
`pattern` does not compile is not checked against that pattern here: the
defect is the document's and `Riddler.Screens.Document.validate/1` reports it as
`document.invalid_pattern`, so a host that validates responses without
validating documents is not told, and every response to that question
satisfies the pattern check.

A choice question and a boolean question are checked whatever they
declare, and each answers at most one finding, the first of these that
applies. Nothing selected - absent or `null`, or `""` under `select:
"one"`, `[]` under `select: "many"` - is `response.required` where the
question is required and nothing otherwise, whatever its selection bounds.
A response that is not the shape its type fixes - one string under
`"one"`, a list of different strings under `"many"`, `true` or `false` for
a boolean question - is `response.invalid_shape`. A required boolean
question whose response is `false` is `response.required`. A value no
option carries, compared as written, is `response.not_an_option`, one
finding naming every such value. Under `"many"`, fewer values than
`min_selections` or more than `max_selections` is `response.out_of_range`,
its `field` naming the bound. A `format` on either type is a field nothing
consults. A choice question whose `select` is neither value is the
document's defect, reported as `document.invalid_select`, and its response
is held to `required` and to nothing else.

Every finding names the node it is about in `node_key`, the field that was
not satisfied in `field`, and a stable `code`: `response.required`,
`response.format`, `response.out_of_range`, `response.invalid_shape`,
`response.not_an_option` or `response.undecidable`. There is one finding
per failing node, because an empty field is one thing wrong with a screen
and not three.

`response.undecidable` is the one that is not about a response. A condition
this root could not decide - a variable the root does not carry, an operand
of the wrong type - is reported as a finding on the node that carries it,
with `field` `"condition"`. So is a condition that is not valid predicator at
all, and one that is not a string: the code is the one fail-closed answer for
every condition resolution could not decide, so that a host which skipped
`Riddler.Screens.Document.validate/1` is told rather than let through. The
`message` says which of the three it was, and carries the place the compiler
or the evaluator gave where there is one, as `position` does. For a
condition that compiles, the message also says what the evaluator answered:
a name the root does not carry, a value that is not true or false, or an
evaluation error such as a division by zero or an unknown function. Where
the evaluator's answer does not tell a missing key from an operand of the
wrong type, the message says only that the condition could not be decided.
A condition that does not parse is also the document's own
`document.invalid_condition`, which is where an author fixing the document
is told about it.

A condition that could not be decided says the
root the host handed in does not carry what the document asks about, which
is a defect in the call rather than a property of the visitor, so treating
the node as hidden and answering `:ok` would accept a submission nobody
checked. It is reported wherever the pressed button validates. The press
that does not is one through a button declaring `validates` as `false`,
which answers `:ok` without running a check at all - see
`validate_screen/4`. This arity-3 form presses no button, so there is no
press to read `validates` from and it reports the finding whatever buttons
the screen carries. A button declaring `validates` as `false` and carrying
no key at all is no exception: the opt-out is a property of a press, and
nothing can press a button nothing can name.

A condition this root *decides* false is a different thing and stays silent:
the node is hidden, and a question the visitor was never asked cannot fail.
So is a variable a template wanted and the root did not carry - that renders
as the empty string, is reported in `missing_variables`, and is no finding.

`{:error, :no_such_screen}` comes straight back from `resolve_screen/3`: a
host asking about a screen the document does not declare is told so rather
than told its responses are fine.

    iex> document =
    ...>   Riddler.Screens.Document.admit(%{
    ...>     "schema_version" => 1,
    ...>     "id" => "edoc_checkout",
    ...>     "screens" => [
    ...>       %{
    ...>         "key" => "card",
    ...>         "title" => "Your card",
    ...>         "nodes" => [
    ...>           %{
    ...>             "type" => "text_question",
    ...>             "key" => "billing_email",
    ...>             "label" => "Where should the receipt go?",
    ...>             "required" => true,
    ...>             "format" => "email"
    ...>           }
    ...>         ]
    ...>       }
    ...>     ]
    ...>   })
    iex> Riddler.Screens.validate_screen(document, "card", %{"responses" => %{"billing_email" => "ada@example.com"}})
    :ok
    iex> {:error, [finding]} = Riddler.Screens.validate_screen(document, "card", %{"responses" => %{"billing_email" => "ada"}})
    iex> {finding.code, finding.node_key, finding.field}
    {"response.format", "billing_email", "format"}
    iex> {:error, [finding]} = Riddler.Screens.validate_screen(document, "card", %{})
    iex> finding.code
    "response.required"
    iex> Riddler.Screens.validate_screen(document, "billing", %{})
    {:error, :no_such_screen}

# `validate_screen`

```elixir
@spec validate_screen(Riddler.Screens.Document.t(), term(), map(), term()) ::
  :ok | {:error, [Riddler.Finding.t()]} | {:error, :no_such_screen}
```

Validates the responses the way the button the visitor pressed asks for.

The same checks as `validate_screen/3`, with one addition: the pressed
button's `validates`. It defaults to true, so a button that says nothing
validates the screen it submits; a button that declares `false` answers
`:ok` without running a check, which is what lets a Back button leave a
half-filled screen. A key that names no button on the resolved screen
validates too, because the default is what a button that is not there
carries.

    iex> screen = %{
    ...>   "key" => "card",
    ...>   "title" => "Your card",
    ...>   "nodes" => [
    ...>     %{"type" => "text_question", "key" => "billing_email", "label" => "Receipt to", "required" => true},
    ...>     %{"type" => "button", "key" => "card_back", "label" => "Back", "outcome" => "went_back", "validates" => false},
    ...>     %{"type" => "button", "key" => "card_pay", "label" => "Pay", "outcome" => "paid"}
    ...>   ]
    ...> }
    iex> document =
    ...>   Riddler.Screens.Document.admit(%{
    ...>     "schema_version" => 1,
    ...>     "id" => "edoc_checkout",
    ...>     "screens" => [screen]
    ...>   })
    iex> Riddler.Screens.validate_screen(document, "card", %{}, "card_back")
    :ok
    iex> {:error, [finding]} = Riddler.Screens.validate_screen(document, "card", %{}, "card_pay")
    iex> finding.code
    "response.required"

---

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