Skip to content

Pardon Templates

Pardon’s template engine is the foundation for how Pardon understands and builds requests.

For the following discussion, basic understanding of programming terms is assumed.

Templates are a novel way of building JSON data, or really any data that can be structures as dictionaries and lists (or objects and arrays). Unlike most schema implementations, Pardon’s templates are designed to look like the data they represent as much as possible.

Templates are composed of nodes that represent values, variables, structures (such as objects or arrays). There are also nodes that transform data, operating on an interior representation with an external encoding.

Nodes support two operations: merge and render. Merging is used to match and extend requests, as well as to handle responses. Rendering a template is (obviously) necessary for building requests but can also be useful to process responses (e.g., hide secrets, etc).

Some parts of templates represent and break down “atomic” values like numbers, strings, booleans, or null, while others are suitable for representing structures like objects and arrays.

There is both a string interpolation and a javascript syntax representation for templating. The string interpolation form is necessary for the request origin, pathname, query params and headers, while the syntax version is more suitable for processing the JSON request body.

Interpolations are structured with the following pieces, all of which are optional.

{{ ? name = expr % /regex/ }}

The name identifies this interpolation: if a value is matched,… or a value is passed,… or a value is evaluated somewhere else.

Names can be hyphenated (but cannot start with a hyphen.)

Interpolations can be used to match, capture, reuse, and redact specific data in an http exchange.

In JSON bodies, references might be preferable, as they are shorter to write. Any raw identifier in the JSON is treated as a reference! For example, the POST task api in the introduction can be represented a little more ergonomically with references, especially the task which appears as a shorthand property:

POST https://todo.example.com/todos
Authorization: User {{@token}}
Content-Type: application/json
{
"task": "{{task}}",
"done": "{{?done}}"
}
POST https://todo.example.com/todos
Authorization: User {{@token}}
Content-Type: application/json
{
task,
done: done.$optional
}

The done: done.$optional might be longer than even the canonical JSON form "done": "{{?done}}", but one might argue that it’s simpler to read and type (no need to open and close with "{{ and }}", just add .$optional).

For simple values, the choice to use references or interpolations is purely a stylistic one.

Expressions in the javascript syntax is “(anything in parentheses)”. For instance a default value of done can be provided with

{
done: done = (false)
}

This is essentially the same as the interpolated template

{
"done": "{{done = false}}"
}

We can omit the optional hint with this expression since there will always be a default.

Try it out, here’s the above template, but try editing it

Loading Template Playground...

The values below the dashed line are the values of any variables and references bound by the template.

Sometimes values have specific encodings that can be broken down further, for instance, you might have an APIs that takes (or return) base64-encoded JSON strings.

Pardon can describe this with base64(json(...)).

Loading Template Playground...

We can bind references to each step here to see what’s going on, try updating the above with data: base64(j = json(o = { hello })) to see the j and o values.

Encodings can also be used to extract data from encoded values! In the following example, we’re matching the template with an already base64-encoded value, and we still get hello=world out.

Loading Template Playground...

HTTP is parsed before being matched into a base http template. The template text on the left is parsed into an object for further processing.

http input
POST http://origin/path?x=y
header: value
content
parsed
{
method: "POST",
origin: "http://origin",
pathname: "/path",
searchParams:
new URLSearchParams("?x=y"),
headers:
new Headers({ "header": "value" }),
body: 'content'
}

This parsed object data is then merged with a template that represents all http requessts.

parsed
{
method: "POST",
origin: "http://service",
pathname: "/path",
searchParams:
new URLSearchParams("?x=y"),
headers:
new Headers({ "header": "value" }),
body: 'content'
}
template
{
method: "{{method}}",
origin: "{{origin}}",
pathname: "{{...pathname}}",
searchParams: search = ...,
headers: headers = ...,
body: body = ...
}

Merging objects naturally merges their field values, i.e., the value "POST" is merged with {{method}}, "http://service" with {{origin}}, etc…

Some parts transform what they match to an internal representation, like base64(json(...)) but more specialized for processing those parts: In particular, the search params, headers and body templates further break down their values. body can act as a json, form, or text encoding.

Processing arrays requires setting up a template to process multiple items. Let’s start with the template for one item:

Loading Template Playground...

adding a second value { id: "T1002", task: "world", done: false }, to the data does not work as the arrays now have different lengths.

To work around this, template needs to specify any number of items…

Loading Template Playground...

… identifying the item as an example of what goes into the array, without specifying a size.

This template now merges, but notice there’s no values exported!

This is because because each id, task, and done value are in the scope of each array element. For them to show up in the output, we need to give them a property path:

Loading Template Playground...

Now, as a user of this data, you might not want to have to search the array for a particular id: it would be more natural to bind this as a map.

Pardon supports this with a little syntax: { id: key } * [...items]. The first part is a template which is merged with each item purely to deteremine a value for key, and then the items are represented internally as a map.

Loading Template Playground...

These structures might be even more useful in reverse, although it does require adding todos.$key to render the id back into the list.

Loading Template Playground...

Note that within expressions (parenthesized), references should be direct names, you can refer to todos anywhere in the above template, and directly to task, done, etc… only within each item (you can’t use todos.task because it would be ambiguous whether todos was the value object or todos.task was the scoped reference).

The following docs need a refresh.