JSON PressBack to site

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:

  1. Page builtins: page and pages.
  2. The current row's fields. A dotted name is reduced to its last segment, so record.status reads STATUS.
  3. Request params. Parameter keys are uppercased before lookup, so params.reportTitle is available as reportTitle, REPORTTITLE or ReportTitle.

Literals

A field that is absent from every row is an error, not an empty string; see “Unknown fields and sparse rows” below.

Operators

OperatorMeaning
&concatenate
+, -, *, /, %numeric arithmetic
==, !=equal / not equal
<, <=, >, >=ordered comparison
&&, ||, !boolean and / or / not
condition ? when_true : when_falseconditional

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

FunctionResult
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, &params, &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.