Expressions
Expressions are a small, side-effect-free language used in title text, summary text, detail columns and the note band. Every expression evaluates to a string; numeric operators parse numbers on demand.
Expressions are limited to 32 nested parentheses and 128 operator or argument separator characters. Padding width and number-format pictures are limited to 4,096 characters; concatenation, replacement and function arguments are limited to 16 MiB. Expressions exceeding these limits fail instead of exhausting resources.
Resolution order
Names are resolved case-insensitively:
- Page builtins:
pageandpages. - The current row's fields. A dotted name is reduced to its last segment, so
record.statusreadsSTATUS. - Request
params. Parameter keys are uppercased before lookup, soparams.reportTitleis available asreportTitle,REPORTTITLEorReportTitle.
Literals
- Strings use either quote style:
"Invoice"or'Invoice'. A doubled quote inside the same quote character escapes it ('It''s'→It's). - Numbers:
12,1.5. - Booleans:
true,false. nullis the empty string.
A field that is absent from every row is an error, not an empty string; see “Unknown fields and sparse rows” below.
Operators
| Operator | Meaning |
|---|---|
& | concatenate |
+, -, *, /, % | numeric arithmetic |
==, != | equal / not equal |
<, <=, >, >= | ordered comparison |
&&, ||, ! | boolean and / or / not |
condition ? when_true : when_false | conditional |
Comparisons return true or false. If both sides parse as numbers the
comparison is numeric; otherwise it is a case-insensitive string comparison. The
number test uses the trimmed string, so " 10 " < "9" is false (numeric
10 < 9); two non-numeric values compare as text, so "apple" < "Banana" is true.
&&, ||, the ternary operator and if(...) are lazy: only the selected
branch is evaluated, so the untaken branch may reference a field that is absent.
Built-in functions
| Function | Result |
|---|---|
upper(s), lower(s) | change case |
trim(s), ltrim(s), rtrim(s) | trim both / left / right |
len(s) | number of characters |
left(s, n), right(s, n) | first/last n characters |
mid(s, start, n) | n characters from 1-based start |
replace(s, find, with) | replace every occurrence |
concat(a, b, ...) | concatenate all arguments |
empty(s) | true when the trimmed value is blank |
coalesce(a, b, ...), default(a, b, ...) | first non-blank argument |
if(condition, when_true, when_false) | lazy conditional |
to_number(s) | numeric prefix (to_number("03") → 3) |
format_number(value, picture) | format with the picture subset below |
format_date(value, pattern) | format dates or date-times using yyyy, MM, dd, HH, mm, ss |
now() | current server date and time as dd/mm/yyyy HH:MM:SS |
today() | today as dd/mm/yyyy |
time() | server time as HH:MM:SS |
pad_left(s, width, fill?), pad_right(s, width, fill?) | pad to width, default fill is a space |
format_number understands the numeric picture subset: 9/#/0 set the
width, 0 after the decimal point sets the number of decimals, @L pads with
leading zeroes and @B left-aligns. For example
format_number(to_number(month), '@L 99') renders 03 and
format_number(amount, '0.00') renders two decimals.
format_date accepts yyyy-MM-dd, dd/MM/yyyy, and yyyy/MM/dd, with
optional HH:mm:ss; ISO dates also accept T before the time. Date-only
values format time as midnight. For example, format_date(now(), 'dd/MM/yyyy HH:mm:ss').
An unknown function or an unknown field is a hard error: the render fails with
422 render_failed and the message names the missing name.
Custom functions
The HTTP API exposes only the built-ins above. A Rust caller rendering in-process can register additional functions for its own templates:
use jsonpress_core::expr::CustomFunctions;
use jsonpress_core::options::RenderOptions;
let mut functions = CustomFunctions::new();
functions.insert("SHOUT", |args| {
Ok(args.first().map(|value| value.to_uppercase()).unwrap_or_default())
});
let options = RenderOptions { functions, ..RenderOptions::default() };
render_definition_pdf_with(&definition, &rows, ¶ms, &clock, &options)?;
Function names are matched case-insensitively. Arguments arrive already evaluated, in source order. A function that is neither built in nor registered is an error.
Node callers can use the jsonpress package to register JavaScript
functions; see node.md.
Page numbers
page and pages are available in title, detail, note and summary expressions.
Use page & ' / ' & pages (or page() & ' / ' & pages()) to display, for
example, 2 / 10. The slash must be quoted: / by itself means division.
Pagination is planned before drawing, so both values are correct on every page
and pages is the final page count. They win over row fields and parameters of
the same name.
Unknown fields and sparse rows
Rows may be sparse. The renderer takes the union of all keys across the batch and
fills a missing key with "" in each row that lacks it. Therefore a field
present in at least one row is safe to reference and renders blank in the
other rows.
A field present in no row is unknown. Binding it is an error and the render
is rejected as 422 render_failed; the message names the field:
{
"error": {
"code": "render_failed",
"message": "cannot evaluate report expression `NO_SUCH_FIELD`: unknown field `NO_SUCH_FIELD`",
"details": ["cannot evaluate report expression `NO_SUCH_FIELD`: unknown field `NO_SUCH_FIELD`"]
}
}
This is deliberate: a typo in a template should fail the render, not produce a report with a silently empty column.
Template version
Version 2 is the expression language described here and the only template version the HTTP API accepts.