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 structuring or destructuring data, with a focus on JSON values, objects, and arrays with “minimal” extra syntax.

Some parts of templates represent and break down “simple” values like numbers, strings, booleans, or null, while others are suitable for representing structures like objects and arrays. We can also use templates to manage encodings like base64-encoded JSON.

Pardon provides a string-interpolation syntax and a javascript/typescript syntax for templating, the javascript syntax is a little nicer in JSON contexts like request bodies, while the interpolation syntax is necessary for other parts of the http request, like the request origin, pathname, query params and headers.

Interpolation templates are surrounded by {{ and }}. Inside we have 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.)

GET https://todo.example.com/TODOS/{{todo}}

In JSON bodies, references might be preferable, as they are shorter to write. Any raw value identifier in a JSON template 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
}

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 an interpolated template with a default value.

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

We can omit the “optional” hint (?) with this expression since there will always be a default.

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 returns) 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...

Pardon supports rendering data in scopes. Besides the root scope, additional scopes are introduced inside elements of indeterminate arrays or objects.

An array with one exactly element is [{ key, value }], while an indeterminate array of data is described with [...{ key, value }], and an indeterminate object could be described with { ...{ info } }.

Data matched to this will not become available in the global scope, since there could be zero, one, or more key and values produced. However key and value variables referenced from within each array element will be defined.

Let’s look at a template for exactly one item.

Loading Template Playground...

Adding a second value { id: "T1002", task: "world", done: false }, to the data would not work as the arrays now have different lengths, and there would no longer be a unique definition of id, task or done.

To work around this, we use an indeterminate array, which can merged with any number of items…

Loading Template Playground...

… using ellipsis to identify the template for items in the array.

The data is now compatible with the template, but no variables are defined for the root scope.

The values for each id, task, and done value are in the scope of each array element.

To export these values into the root scope, we can define them via a pathed-reference, like todos.id, todos.task:

Loading Template Playground...

The todos value is implied to be an array of objects structured as { id, task, done } elements, so the actual data paths are todos[0].id or todos[1].task etc…

This works in reverse as well, specifying an array of todos as input and the template:

Loading Template Playground...

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 } * [...template]. The first part is a template which is applied to each item to resolve a value for key and then the todos array is represented internally as a map based on that key.

Loading Template Playground...

In the above example, we omitted the id: todos.id field since it’s already represented.

This can also be used in reverse, although in this case we need to add todos.$key to render the id field back:

Loading Template Playground...

Keyed arrays can also work unscoped. For this usage, we omit the ... example element and only use the key mapping with a concrete array (with keys evident).

Loading Template Playground...

With this variant, the values for hello and hi are bound to the root scope. And the additional entry is maintained as-is.

One last feature for dealing with arrays is when we have a single element and we want to treat it as a single-item array. This is for interoperability with the Java/Jackson WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED and ACCEPT_SINGLE_VALUE_AS_ARRAY functionality.

We specify these in Pardon with ![...list.field] syntax. Consider the following template expansion

Loading Template Playground...

This demonstates the following cases:

CaseDescription
onea single item list renders unwrapped
twoa multi-item list renders as a list
threea non-array match is rendered unwrapped
foura single item list stays as a list
fivea multi-item list also remains as a list

and in all cases the variable/value data is in lists.

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 requests.

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 = ...
}

The body template can be interpreted using the special template syntax, but it can also be treated as url-encoded form data (that still supports interpolations), or plain text. The other values in an HTTP template only allow for {{ ... }}-style interpolations.

The body template makes a best-guess on what encoding it has. Some options are text, raw, json (includes script), and form.

A Content-Type header will help pardon guess about the type.

There is also a meta “header” which will override any guesses. E.g.,

[body]: form

(Meta headers are not legal HTTP headers and are not sent).

If the body parses as JSON it is JSON, if the body parses as a script, it is also JSON, unless it defines another encoding at the top level. If the body doesn’t parse, it is treated as “text”, and interpolations still apply.

For example, we can define a form using either form() encoding or directly as text.

POST https://...
form({ a: 'b' })
POST https://...
[body]: form
a=b&c=d
POST https://...
Content-Type: x-www-form-urlencoded
a=b&c=d

Specifying [body]: raw, for instance, turns off all template processing and sends the body exactly as is.

Explore Pardon’s testcase generation system and HTTPS flows.

A deeper dive into the features and syntax transformations available in the template runtime.