Rupa.IR (rupa v0.1.0)

Copy Markdown View Source

The intermediate representation: what the staging pass turns a schema into.

A schema says what you meant. The IR says what the decoder will do, in the order it will do it, with everything the schema only implied made explicit:

  • refs are resolved — a ref that is not on a cycle is replaced by the definition itself, so the decoder never looks anything up; only genuinely recursive refs stay as nodes, and those live in a table beside the tree
  • an object's fields become an ordered list, each with the wire key it arrives under, the one it leaves under, the key it takes in the decoded map, whether it is required, and what it defaults to — so rename_all:, from:, to: and keys: are all spent here
  • into: stays a bare module name on the object, checked against that module's defstruct while staging, so a backend builds the struct without asking anything
  • a scalar's constraints become an ordered list of checks
  • enum and literal become one lookup table from wire value to decoded value
  • a tagged union becomes a table from tag string to branch, with the decoded tag atom interned here and never from wire data

Nothing here is a runtime artifact: a pattern is still its source string, not a compiled regex, because the module backend has to embed these nodes in generated code. Compiling the regex is the backend's job.

Both backends consume this, which is the point: a feature is written once. Rupa.explain/1 prints it, so what you read is what runs, whichever backend you are on.

Summary

Types

A staged schema: the root node, plus the definitions that recursion needs.

t()

Types

program()

@type program() :: %{root: t(), defs: %{required(atom()) => t()}}

A staged schema: the root node, plus the definitions that recursion needs.

t()

@type t() ::
  Rupa.IR.Scalar.t()
  | Rupa.IR.Const.t()
  | Rupa.IR.Object.t()
  | Rupa.IR.Array.t()
  | Rupa.IR.Fixed.t()
  | Rupa.IR.Dict.t()
  | Rupa.IR.Nullable.t()
  | Rupa.IR.Union.t()
  | Rupa.IR.Tagged.t()
  | Rupa.IR.Ref.t()