Pardon Templates
Pardon’s template/schema engine is the foundation of how Pardon understands and builds requests. The engine’s main operation is “matching”, but the logic includes both conflict detection and progressive enhancement.
The composition of multiple (compatible) templates forms a “schema” But we’ll defer the discussion of exactly what schemas represent and how they are composed for later, because we can use basic templates without understanding them.
Templates
Pardon’s templates are HTTP-like text, but commonly they represent, in part, JSON requests.
But to understand how pardon handles the overall request object we need to first look at how it would operate on a single (string) value in a JSON request.
A “simple” pattern (where the entire string is a single interpolated value)
can match simple values like strings (text), matching these two (in either order)
would be allowed, and additionally resolve name
to "some value"
.
"{{name}}"
"some value"
Pardon also allows simple templates to match numbers as values,
here we resolve the value name
to 123
(as a number rather than as a string).
"{{name}}"
123
We can also have non-simple templates, a non-simple template can also break/compose a string (this only makes sense for strings, of course).
"{{greeting}} {{planetoid}}"
"hello world"
If multiple templates for the same value are combined, Pardon will try
to resolve everything. Here "hello world"
provides a value for the template,
and so we can resolve both the "{{value}}"
and the "{{greeting}} {{planetoid}}"
template against that.
(This makes sense because /^(.*) (.*)$/
can only match one way).
"{{greeting}} {{planetoid}}"
"{{value}}"
"hello world"
This resolves the three variables as one might expect.
greeting = "hello"
planetoid = "world"
value = "hello world"
Attempting to extend/merge an incompatible template fails: Pardon recognizes these conflicts.
"hello"
"world"
Pardon would also reject this combination, as the template would match the regular expression /^(.+) (.+)$/
and "hello"
is missing a space.
"hello"
"{{greeting}} {{planetoid}}"
Rendering Templates
All by itself, the template "{{hello}}"
cannot be rendered without
providing some value for hello
. If this template were matched against the text "world"
, then
hello
would be resolved to the value "world"
.
The value for hello
could also be provided externally as a value.
We can also provide expressions (javascript!) in templates, which will be evaluated if no value could be resolved.
"{{hello = 'world'}}"
"world"
But these expression-values match as normal, skipping evaluation.
"{{hello = 'world'}}"
"greetings"
"greetings"
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.
For example, consider the following template and it’s default output
{ a: "{{hello = 'world'}}", A: "{{HELLO = hello.toUpperCase()}}" }
{ a: "world", A: "WORLD" }
If we merge this schema we can get different results
If we specify the value of a
we resolve hello="planet"
which becomes
and the expression for A
uses it instead of the default.
{ a: "planet" }
{ a: "planet", A: "PLANET" }
Alternatively, we can resolve both values which skips any expression evaluation here.
{ a: "planet",A: "jupiter" }
{ a: "planet", A: "jupiter" }
If we only specify A
, then a
retains its default expression and A
gets
overridden.
{ A: "jupiter" }
{ a: "world", A: "jupiter" }
Pardon can handle any evaluation order that does not result in a cycle (including asynchronous / promise expressions).
Exploring a template
Moving on to a (slightly more) real example: a REST API that creates products with a name and a price.
>>>POST https://example.com/products
{ "name": "{{name}}", "price": "{{price}}"}
This is parsed into a structure and applied to a template defined roughly as the following
{ method: "{{method}}", url: { origin: "{{origin}}", pathname: "{{pathname}}" }, headers: [...], body: ...}
{ method: "POST", url: { origin: "https://example.com", pathname: "/products" }, headers: [], body: { "name": "{{name}}", "price": "{{price}}" }}
Anyway, let’s explore how pardon behaves with this template with these quick exercises.
Try updating the name or price values. Note that since
price is a javascript number value, the rendering of e.g., "price": 9.00
will
become "price": 9
.
Conversely, providing arrays or objects for these fields will currently confuse Pardon. (note: I have not decided if this is a bug or just how things work.)
POST https://example.com/products
{ "name": "Fez", "price": 9.99}
Other fields can be added freely into the template.
POST https://example.com/products
{ "name": "Fez", "price": 9.99, "description": "Fezzes are cool"}
We can also add query params and pardon can integrate it into the request.
POST https://example.com/products?hello=world
{ "name": "Fez", "price": 9.99}
Headers and query params can also be added, the base template does not restrict additional data so long as the additional data can be merged in.
POST https://example.com/productsBowties: also cool
{ "name": "Fez", "price": 9.99, "description": "fezzes are cool"}
We’ve covered the basics of how a template supports building a request. Feel free to explore negative cases as well: change the URL path or origin, remove fields from the request, etc… and watch for when pardon can no longer match the template () or render it ().
Scripted values
Expressions can use other values from the template, either resolved through matching or evaluated in other expressions.
For example, since we have a value for {{name}}
matched by the template, we
can use the name
value in an expression for another field.
"pardon‽"
. You can see the price
value change automatically as
you add or remove letters from the name value.
POST https://example.com/products
{ "name": "pardon‽", "price": "{{ price = name.length * 10 }}"}
Inflation has dropped the demand for luxury letters: we need to make a drastic price-cut to maintain our order flow rates, price is now 25 cents a letter.
POST https://example.com/products
{ "name": "pardon‽", "price": "{{ price = name.length / 4 }}"}
That didn’t drive orders enough and management is desperate: we need to highlight our payment plan offering:
POST https://example.com/products
{ "name": "pardon‽", "price": "{{ price = name.length*0.25 }}", "description": "{{= `${name.toUpperCase()} for only 4 easy payments of ${price/4}!`}}"}
(note that parameter expressions don’t need a name, we just start with {{=
…)
Next Steps
A thorough discussion of the pattern syntax.
Exploring a small collection of endpoints for our service example.
An explanation of how templates are composed into schemas.