Pardon Templates
Pardon’s template engine is the foundation of how Pardon understands and builds requests. The engine’s main operation is “merging”: this mechanism implements both conflict detection (matching requests) and progressive enhancement (extending requests).
For the following discussion, basic understanding of programming terms is assumed.
The present discussion covers the overall idea of Pardon’s templates. The two phase approach: resolution and rendering, and how that provides some flexibility with how requests are expressed.
Templates
Section titled “Templates”HTTP is parsed before being matched into a base 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 data is merged with a template for http requessts.
{ 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 = ...}
As might be expected, merging objects merges their fields:
POST
is merged with {{method}}
, http://service
with {{origin}}
, etc…
Some templates transform what they match to an internal representation, and render that to an external one.
For instance, the search
params,
headers
and body
templates break down their values further as they merge.
Often, body
represents as json()
data. Externally this is the text json and internally
it is the parsed object value. Note these are not 1:1, the external representation may be formatted with newlines/spaces/indentation, but this is discarded
by parsing it to the internal representation.
Template Values
Section titled “Template Values”Most of the magic happens with scalars: scalars are the simple values of an object, numbers, strings, booleans, or null
.
(“scalar” is a conventional word to describe “individual values”).
A simple pattern like "{{x}}"
can represent any scalar value.
A non-simple pattern like "{{x}} {{y}}"
generally function as a string only, e.g, in this case, matching and rendering
values like "hello world"
.
Templates can also represent objects (with things in fields), or arrays (list of things). For example, the parsed HTTP request is an object.
Templates cannot merge if their values are incompatible, e.g., the value "a"
cannot directly merge with "b"
.
Also arrays and objects can’t merge with each other, nor with scalar values.
As we saw above, objects merge field-by-field, while arrays are trickier.
Template Scopes and Arrays
Section titled “Template Scopes and Arrays”Suppose we were parsing a 3d model, we might want a { position: [x,y,z] }
.
This template would only match another 3-element array. It doesn’t make sense to add or remove dimensions.
If we were building a shopping cart, we might want a template { items: [{ product: items.product, quantity: items.quantity = 1 }] }
that could match any number of items, including 0, but it declares that each item should have
a product
and defaults the quantity to 1 if not specified.
In objects with arrays, if the template has a single item, each item is templated in a separate scope.
For instance, the template
{ "items": ["{{items.item}}"]}
is compatible with the template (or value) { "items": ["a", "b", "c"] }
.
This behavior can depends on the mode of the template expansion: sometimes a single-element array is a template for each item, and sometimes it’s a single element.
If needed, ++
and --
can be used to change the templating mode.
If we’re specifying a request that only supports single element arrays, we can
use ++
to represent that structure.
++["{{item}}"] // unscoped item variable, array has one element.
Rendering Templates
Section titled “Rendering Templates”Templates processing is two phase: the resolution phase when the templates are merged, and the rendering phase. The first phase is used to select the correct template for rendering. Pardon requires this resolution to be unique: your collection templates should be 1:1 with your service APIs, and which template was selected is important metadata in your history.
Once a template is selected, rendering can proceed. Rendering is when expressions (scripts) are executed. Separating resolution from execution ensures no extraneous script actions will be taken.
A template depending on a variable, like "{{world}}"
, cannot be rendered without
providing some value for world
. A value may be resolved through many mechanisms:
- merging the template with a value.
- resolving the value somewhere else.
- evaluating the value from this template.
- evaluating the value somewhere else.
- resolving the value from another evaluation.
A simple remplate can just be a constant, "{{hello = 'world'}}"
this provides a default value.
Once a value is resolved (through matching or script evaluation), it can be referenced in other parts of the same template or as values in scripts.
We’ve seen more interesting templates in examples/todo/collection/todo/user-auth.mix
:
running an imported function for generating the authorization token.
In the template, that call looks like this
Authorization: User {{@token = authorizeUser({ username, origin })}}
the expression refers to three values: authorizeUser
is imported from the seript, and
username
and origin
are template variables: In order to execute the expression,
all the username
and origin
are resolved/evaluated first.
Let’s suppose that instead of using pardon’s secrets
mechanism, we wanted to
have a local files with usernames and passwords (per environment).
To load a password, we make a function loadPassword({ env, username })
.
For making the authorization call, our template would start like this
PUT https://todo.example.com/users
{ "username": "{{username}}", "password": "{{@password = loadPassword({ username })}}"}
We could provide username=test-user
as an input above the request, or
we can provide the partial request, which defines implicitly as
"{{username}}"
merges with "test-user"
, so the following two requests
inputs do the same thing.
PUT https://todo.example.com/users
{ "username": "test-user"}
username=test-userPUT https://todo.example.com/users
Next Steps
Section titled “Next Steps”A discussion of the pattern syntax.
Exploring a small collection of endpoints for our service example.
An explanation of how templates are composed into schemas.