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
Section titled “Templates”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 Variables
Section titled “Interpolation Variables”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.)
{{ ? name = expr % /regex/ }}
The hint adds some flavor to how this value is processed, a ?
allows this value to be omitted,
a @
marks it as a secret, ...
modifies the default regex in some contexts (like in the pathname,
to allow a value to match multiple path segments), and -
removes a value from rendering entirely.
{{ ? name = expr % /regex/ }}
The expression provides a default value if one is not available through matching and merging. Expressions can be arbitrary asynchronous javascript expressions.
{{ ? name = expr % /regex/ }}
The regex can be used to restrict the kinds of values that qualify for this field.
GET https://todo.example.com/TODOS/{{todo}}
Template References
Section titled “Template References”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/todosAuthorization: User {{@token}}Content-Type: application/json
{ "task": "{{task}}", "done": "{{?done}}"}
POST https://todo.example.com/todosAuthorization: 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.
task="learn pardon"
{ task, done: done = (false)}
The values below the dashed line are the values of any variables and references bound by the template.
Encodings
Section titled “Encodings”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(...))
.
hello=world
{ data: base64(json({ hello }))}
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.
{ data: base64(json({ hello }))}---{ "data": "eyJoZWxsbyI6IndvcmxkIn0=" }
Structured data
Section titled “Structured data”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 value
s produced. However
key
and value
variables referenced from within each array element
will be defined.
Arrays in templates
Section titled “Arrays in templates”Let’s look at a template for exactly one item.
[ { id, task, done }]---[ { id: "T1001", task: "hello", done: false }]
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…
[ ...{ id, task, done }]---[ { id: "T1001", task: "hello", done: false }, { id: "T1002", task: "world", done: false }]
… 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
:
[ ...{ id: todos.id, task: todos.task, done: todos.done }]---[ { id: "T1001", task: "hello", done: false }, { id: "T1002", task: "world", done: true }]
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:
todos=[ { id="T1001" task="hello" done=false } { id="T1002" task="world" done=true }]
[ ...{ id: todos.id, task: todos.task, done: todos.done }]
Mapped Data
Section titled “Mapped Data”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.
{ id: key } * [ ...{ task: todos.task, done: todos.done }]---[ { id: "T1001", task: "hello", done: false }, { id: "T1002", task: "world", done: true }]
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:
todos={ T1001={ task=hello done=false } T1002={ task=world done=true }}
{ id: key } * [ ...{ id: todos.$key, task: todos.task, done: todos.done }]
Unscoped Mapped Data
Section titled “Unscoped Mapped Data”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).
{ key } * [ { key: "hello", value: hello }, { key: "hi", value: hi },]---hello=world[ { key: "hi", value: "developer" }, { key: "extra", value: "data" },]
With this variant, the values for hello
and hi
are bound to the root scope.
And the additional entry is maintained as-is.
Unwrapped Single-Item Arrays
Section titled “Unwrapped Single-Item Arrays”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
one=[1]two=[2,2]{ one: ![...one.$value], two: ![...two.$value], three: ![...three.$value], four: ![...four.$value], five: ![...five.$value]}---{ three: 3, four: [4], five: [5,5]}
This demonstates the following cases:
Case | Description |
---|---|
one | a single item list renders unwrapped |
two | a multi-item list renders as a list |
three | a non-array match is rendered unwrapped |
four | a single item list stays as a list |
five | a multi-item list also remains as a list |
and in all cases the variable/value data is in lists.
HTTP Templates
Section titled “HTTP Templates”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.
POST http://origin/path?x=yheader: value
content
{ 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.
{ method: "POST", origin: "http://service", pathname: "/path", searchParams: new URLSearchParams("?x=y"), headers: new Headers({ "header": "value" }), body: 'content'}
{ 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.
HTTP Request and Response Bodies
Section titled “HTTP Request and Response Bodies”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.
Next Steps
Section titled “Next Steps”Explore Pardon’s testcase generation system and HTTPS flows.
A deeper dive into the features and syntax transformations available in the template runtime.