Scopes
Intent
quario assembles a scope per band, and it decides which names an expression
may read. Most authoring mistakes are anchor mistakes. A group header reaches
for @. A detail cell reaches for a handle that’s not in scope. An
expression reads a page number where no target has decided page numbers yet.
Knowing which names bind where turns those from render errors into lines you
never write.
Design decisions
@ and $ are the two ends of one chain. Reports nest as deep as you
like, but only two anchors stay fixed. @ is the row in front of you and $
is the report root. You reach everything between them by name, which keeps an
expression’s meaning independent of how many groups happen to enclose it.
A named group handle covers every level in between. A group’s name
becomes the handle its own bands read, along with everything nested inside
them. A detail row three groups deep reads @.price, region.subtotal and
$.grandTotal in one expression, because scopes inherit down the chain.
Renaming a group is the one edit that moves an expression’s meaning.
Group bands leave @ unbound. A report header, a group footer and an
empty band have no single current row, so quario binds nothing rather than
picking one. @.anything throws there. Read the group’s own values through its
handle. Where a band needs the whole row it grouped by, declare by: "=@" so
the handle’s key is that row.
$.input carries the whole render payload. data narrows the report to a
set of rows, and a header usually needs the document those rows came from.
$.input is the object you passed to render, untouched. The renderer copies
nothing onto $, so an invoice number stays where your application put it.
$.params holds constants the definition declares. They’re JSON values
written into the document itself, which is what separates them from $.input.
A Date, a Map, a NaN or an Infinity is a definition error. Constants
that belong to the report travel with the report.
Every absent read resolves to null. A missing variable, a missing field
and the values quario injects all come back null, so x == null holds
without special handling. Reading a property through a null base still throws,
which is what ?. is for.
Anchors pass through the same prototype guard as any property. @ and $
are ordinary identifiers to the expression engine. $.constructor throws
as @.constructor does. An anchor isn’t a way around the guard.
page binds inside page bands and nowhere else. Page numbers exist only
once a target has decided where the pages fall, which happens after it walks
the body. Binding page in a detail cell would promise a number that no
unpaginated target could supply.
API walkthrough
Reading the current row
@ is the detail row the walk sits on. It’s bound in detail items and in
the per-row expressions that decide the walk, which are by, where, sort,
and the aggregate and running expressions. Outside those it’s unbound, and
reading through it throws rather than resolving to null. That fail-loud
behaviour is deliberate, because a silently null row is a report that renders
blank cells and ships.
@.field reads one field of that row. Arithmetic on it happens in the
expression, so @.qty * @.unitPrice keeps the calculation in the definition
your reviewers can read.
by: "=@" makes a group key the whole row rather than one of its fields.
That’s the route for a group band that needs everything about the row it
opened on, since @ isn’t bound in the band itself.
Reading the report root
$ is the synthetic report root, and it’s in scope in every band. It’s
not your input document. It’s the root quario assembles, with your document
nested inside it.
$.input is that whole document, unflattened. A report whose data is
$.invoice.lines[*] still reads $.input.invoice.number in its header, which
is how one compiled report serves both the rows and the metadata around them.
$.params holds constants the definition declares. Use it for values that
belong to the document rather than to the data, such as a threshold a
conditional style compares against.
$.<aggregate> reads a report-level aggregate by the name you declared it
under. Aggregate names go bare under $, which is why input and params
stay reserved and a group aggregate can’t take the name key.
{
"value": "{{ @.amount }} of {{ region.subtotal }}, invoice {{ $.input.invoice.number }}"
}Reading an enclosing group
<groupName>.key is the value that group instance grouped by. For a group
named project grouping on [email protected], project.key is the project name.
<groupName>.<aggregate> is one of that instance’s aggregates, scoped to
the rows in it. project.lineTotal covers one project while $.subtotal
covers the document, and the two are readable side by side because the handle
and the root are both in scope.
Handles are unique across the document and can’t take the engine’s own names,
so @, $, run, loop and page are all rejected as group names.
Reading a running total
run.<name> is a running accumulator on a detail row, holding a different
value per row according to the rows before it in render order. Declare it under
run, and the name is yours. The reducers are the six aggregate ones plus
prev, which holds the previous row’s value and is null on the first.
Scope decides when a runner resets. A run block on the report persists across
the whole document. A run block on a group starts again at each instance of
that group, which is what a per-invoice running balance wants.
When no run block is in scope at all, run itself is unbound and
run.<name> throws, the same fail-loud rule @ follows. With any block in
scope, an undeclared name reads as null instead.
{
"run": { "prevRegion": "prev:[email protected]" },
"detail": {
"columns": [
{ "header": { "value": "Region" }, "value": "{{ @.region }}",
"visible": "[email protected] != run.prevRegion" }
]
}
}A hidden table cell keeps its slot, so suppressing the repeats leaves the
column aligned. Filtering with where isn’t the same move, because it would
drop those rows from the aggregates and the runners too.
Reading the page
page.number is the current page and page.total is the count for the
current sequence, both 1-based. A group declaring reset: "page" starts a new
sequence, which is how a three-page invoice reads as 1 of 3 inside a batch of
two hundred.
Both bind in page band items only. Targets that don’t paginate ignore the band rather than guessing a number.
Handling an absent value
?? supplies a default for a read that came back null, as in
@.discount ?? 0.
?. reads through a base that may itself be null. @.address?.city is
safe where @.address.city throws, because absence normalizes to null one
level at a time.
== null is the test that holds for every kind of absence under the
expression engine’s strict equality. One comparison covers a missing field, a
missing variable, and an injected null alike.