Riddler resolves dynamic content for a host application. A host authors its screens as JSON documents - copy, questions, buttons, safe templates and the conditions that show or hide each one - and Riddler answers what one visitor is shown, given what the host knows about them. The host renders what comes back.

Why Riddler

Content that changes with the reader - a returning patron greeted by name, a guardian's question shown only to a visitor under eighteen - tends to end up as conditions and string interpolation scattered through a host's views, where an author cannot see them and nothing checks them until a visitor trips over one. Riddler moves both into the document: a condition is a predicator expression and a template is an allowlisted subset of Liquid, so a document is checked on its own before anyone sees it, every reason it is wrong comes back at once with a stable code, and resolving it against a visitor is a pure function over decoded data. Rendering, storage and the editor stay the host's: nothing in this package draws markup, writes to a store or knows that a browser exists.

Installation

Add riddler to the dependencies in your mix.exs:

def deps do
  [
    {:riddler, "~> 0.3.0"}
  ]
end

Pre-1.0. The public API is still moving, and a minor version may break it. Pinning to an exact minor - ~> X.Y.0 - is the recommended way to consume the package until 1.0.

Basic usage

A visitor becomes a library patron through a few screens. The first one greets a returning visitor by name and asks for an email address for due-date reminders. Riddler takes the document as decoded JSON, with string keys throughout, admits and checks it, resolves it against what the host knows, and checks what the visitor typed before the host accepts it:

iex> alias Riddler.Screens
iex> alias Riddler.Screens.Document
iex> document =
...>   Document.admit(%{
...>     "schema_version" => 1,
...>     "id" => "patron_registration",
...>     "screens" => [
...>       %{"key" => "patron_details", "title" => "Get your library card",
...>         "nodes" => [
...>           %{"type" => "text", "key" => "welcome_back",
...>             "condition" => "context.returning == 'yes'",
...>             "text" => "Welcome back, {{ context.first_name }}."},
...>           %{"type" => "text_question", "key" => "reminder_email",
...>             "label" => "Email for due-date reminders",
...>             "required" => true, "format" => "email"},
...>           %{"type" => "button", "key" => "details_next",
...>             "label" => "Continue", "outcome" => "details_submitted"}
...>         ]}
...>     ]
...>   })
iex> {:ok, ^document} = Document.validate(document)
iex> root = %{"context" => %{"returning" => "yes", "first_name" => "Ada"}}
iex> {:ok, resolved} = Screens.resolve(document, root)
iex> Enum.map(hd(resolved.screens).nodes, & &1.key)
["welcome_back", "reminder_email", "details_next"]
iex> hd(hd(resolved.screens).nodes).text
"Welcome back, Ada."
iex> typed = Map.put(root, "responses", %{"reminder_email" => "ada"})
iex> {:error, [finding]} = Screens.validate_screen(document, "patron_details", typed)
iex> {finding.code, finding.node_key}
{"response.format", "reminder_email"}

Documentation

Compatibility

Riddler needs Elixir 1.18 or later (elixir: "~> 1.18"). It has two runtime dependencies: predicator ~> 9.4, which evaluates the conditions a document declares, and solid ~> 1.3, which parses the template subset. Its functions take a document already decoded, so a host decodes with whichever JSON library it already uses. This version reads documents that declare schema_version 1 and ships one content kind, screens; a document naming no kind is a screen document. Nothing in the statifier family is a dependency: a host that uses both calls into Riddler, never the reverse.

License

MIT. See LICENSE.