Introduction to Pardon
Pardon is a polite http framework.
Pardon is a template processor: templates are used to describe, match, format, extend, and parse http requests and the responses. Templates are written in a dialect of the “HTTP format”.
Templates supply the “etiquette” that is easy to forget. “Did you remember your content-type header?”. Of course you did, you were focused on the data. Pardon takes care of the syntax part of the request so you can focus on the semantics.
Authorization tokens? Request signing? Switching environments? All easily done in Pardon,… and yet nothing is built-in.
Interactive Demo
Section titled “Interactive Demo”Let’s consider the requests for the backend of a simple multi-user todo-style application.
- The API manages users and todos.
- The login API exchanges password-based authentication for a bearer token.
- Logged in users can edit their todo list (create, read, update, delete, and list).
- Todo objects have a “task” (description) and a “done” flag.
Please familiarize yourself with the following 8 requests to manage users and todos.
Create a user
POST https://todo.example.com/usersContent-Type: application/json
{ "username": "{{username}}", "password": "{{@password}}"}
Login: returns an authorization token that identifies the user
PUT https://todo.example.com/usersContent-Type: application/json
{ "username": "{{username}}", "password": "{{@password}}"}
Delete a user
DELETE https://todo.example.com/usersAuthorization: User {{@token}}
Create a new todo
POST https://todo.example.com/todosAuthorization: User {{@token}}Content-Type: application/json
{ "task": "{{task}}", "done": "{{done}}"}
Get all the todos for a user
GET https://todo.example.com/todosAuthorization: User {{@token}}
Get the state of a single todo
PUT https://todo.example.com/todos/{{todo}}Authorization: User {{@token}}
Update a todo
PUT https://todo.example.com/todos/{{todo}}Authorization: User {{@token}}Content-Type: application/json
{ "task": "{{?task}}", "done": "{{?done}}"}
Delete a todo
DELETE https://todo.example.com/todos/{{todo}}Authorization: User {{@token}}
A pardon collection would contain the above templates inside .https
files,
or “HTTP-schema” files of request and response templates.
Response templates extract and redact values in a response, for example, the user login HTTP schema can specifies a redacted-value placeholder in the response:
>>>PUT https://todo.example.com/usersContent-Type: application/json
{ "username": "{{username}}", "password": "{{@password = secrets.password}}"}
<<<200 OK
{ "token": "{{@token}}"}
The {{@token}}
here is important for two reasons, primarily it identifies the
field as a secret which masks it in logs, but also a script can use the
extracted value when using pardon()
’s Javascript API to access the authorization call.
Here’s what that script might look like.
import { pardon } from "pardon";
export async function authorizeUser(username) { const { ingress: { response: res, secrets: { token } }, } = await pardon({ username })` PUT https://todo.example.com/users `();
if (!token) { throw new Error( `failed to authorize user ${username}: (${res.status}) ${res.body}`, ); }
return token;}
Using our todo API, we can get a feel for Pardon works.
We will go through some basic flows.
- create a user.
- create a todo item.
- mark the todo as completed.
In this tutorial, changing tabs updates the request: you will need to run each request before the following one.
Register a new user, with a password.
@password=pwPOST https://todo.example.com/users
Create a todo for your user.
POST https://todo.example.com/todos
{ "task": "learn to use pardon"}
Mark your todo item, done.
PUT https://todo.example.com/todos/{{todo}}
{ "done": true}
Let’s go over what we just did.
First we created a user with name “test” using input values.
The @password
is a secret input, Pardon won’t save this directly
in the request history.
username=test@password=pwPOST https://todo.example.com/users
POST https://todo.example.com/userscontent-type: application/json
{ "username": "test", "password": "pw"}
See pardon also supplies the necessary but forgettable content-type
header here.
It’s not a guess, nor is it based on the request body: it’s in the template.
After sending this out, the server-emulator in this page registered the test user.
The full https schema for the create user endpoint includes a response matcher
and a post request script. The response matcher assures that the script is only
run if the call was successful. What it does is register the password
for the user
in the context of the value of { username }
.
Assigning secrets allows the secrets.password
expression
in the login
script to retrieve the password.
>>>POST https://todo.example.com/usersContent-Type: application/json
{ username: "{{username}}", password: "{{@password}}"}
<<<200
!!!secrets({ username }).password = password
Now that the user is created and the password is stored, we moved on to creating a todo.
If you navigate back to that second request and unlock the secrets for viewing, you will see
POST https://todo.example.com/todosauthorization: User jwt.eyJ1c2VybmFtZSI6InRlc3QifQ==content-type: application/json
{ "task": "learn pardon"}
while this authorization token is just {"username":"test"}
in base64, it is being
generated from the login call.
The todo create call has the following schema,
import: ./authorization.ts: - authorizeUser>>>POST https://todo.example.com/todosContent-Type: application/jsonAuthorization: User {{@token = authorizeUser(username)}}
{ task: "{{task}}", done: "{{?done}}"}
<<<200
{ "id": "{{+todo}}"}
At the top, we see a yaml-format configuration section. Configuration can provide defaults, runtime (or “render-time”) values for the templates, imports from scripts, and environment configuration.
Here the configuration is importing the method used in the template, {{@token = authorizeUser({ username, origin })}}
,
when we render this request, the result of running authorizeUser({ username, origin })
is interpolated here.
Since the collection is in control of this script, practically any authorization mechanism can be supported
(read from a file, execute a local process, etc…).
Looking at the request and response, we can see that some of the variables have “hints”:
The plain {{task}}
is mandatory input, and the request will not render without it.
However, the {{?done}}
is marked optional by the ?
-mark, if the value is not supplied
pardon can omimt the entire field.
In the response, the +
in {{+todo}}
marks a dataflow output,
this is why the todo=T1001
was added automatically for the next request.
Feel free to explore the other rest calls, listing todos, deleting todos and/or users, by editing the request input.
Here are some ideas for exploration.
Try using input values in the template, instead of the request body.
username=testdone=truePUT https://todo.example.com/todos/T1001
Try the authorization request.
- Notice how Pardon adds the password (use the lock to toggle showing the value).
- Try overriding it in the request and seeing how that works for you. (Then try overriding it the other way.)
PUT https://todo.example.com/users
{ username: "test" }
Try adding query params, headers, or more fields to the request body. Pardon incorporates these additions while still applying templates.
Try adding "parameterized": "{{value}}"
fields
and then specify value=...
above the request.
(Note that the saved passwords which are supplied by secrets.password
will be lost if the page is reloaded, you can resupply them with the login command,
just change the create user call from POST
to PUT
, or reset the server state).
Next Steps
Section titled “Next Steps”For better or worse, this tutorial only scratches the surface of Pardon’s capabilities.
For more hands-on experience running pardon, where we will run the desktop application against the same server we have in this page.
For a deeper dive into the template runtime, and the programming model.