# Hen
Run API requests as files, from the command line.
```text
name = Test Collection File
description = A collection of mock requests for testing this syntax.
$ api_key = $(./get_secret.sh)
$ username = $(echo $USER)
$ api_origin = https://lorem-api.com/api
---
# Load other requests.
<< .fragment.hen
---
Some descriptive title for the prompt.
POST {{ api_origin }}/echo/[[ foo ]]
* Authorization = {{ api_key }}
? query_param_1 = value
? username = {{ username }}
~~~ application/json
{ "lorem" : "ipsum" }
~~~
! sh ./callback.sh
```
## Installation
```bash
cargo install hen
```
## Usage
```text
Usage: hen [OPTIONS] [PATH] [SELECTOR]
Arguments:
[PATH]
[SELECTOR]
Options:
--export
--benchmark <BENCHMARK>
--input <KEY=VALUE>
-v, --verbose
-h, --help Print help
-V, --version Print version
```
### Execute a Request
To execute a request, use the `hen` command on a file containing a request:
```bash
hen /path/to/collection_directory/collection_file.hen
```
This will prompt you to select a request from the file to execute.
### Specifying a Request
You can specify the nth request directly by providing an index as the second argument:
```bash
hen /path/to/collection_directory/collection_file.hen 0
```
This will bypass the request selection prompt and execute the first request in the file.
Conversely, all requests can be executed with the `all` selector:
```bash
hen /path/to/collection_directory/collection_file.hen all
```
### Selecting a Collection
Alternatively, you can specify a directory of collections. This will prompt you to select a collection file and then a request from that file.
```bash
hen /path/to/collection_directory
```
> If the directory contains only one collection file, that file will be selected automatically, bypassing the prompt.
> Dotfiles (files starting with `.`) are ignored by the prompt.
## Defining an API Request
An API request is defined in a text file with the `.hen` extension.
At a minimum, a request must have a method and a URL. The method is one of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, or `OPTIONS`. The URL is the endpoint of the request.
```text
[description]
METHOD url
[* header_key = header_value]
[? query_key = query_value]
[~ form_key = form_value]
[~~~ [content_type]
body
~~~]
[! callback]
```
### Headers
Headers are key-value pairs that are sent with the request. Headers are specified with the `*` character. For example,
```text
* Authorization = abc123
* Content-Type = application/json
```
### Query Parameters
Query parameters are key-value pairs that are appended to the URL. Query parameters are specified with the `?` character. For example,
```text
? page = 2
? limit = 10
```
### Multipart Form Data
Multipart form data is used to send files or text fields with a request. Multipart form data is specified with the `~` character. For example,
```text
$ file_1 = $(cat ./test_file.txt)
---
POST https://lorem-api.com/api/echo
# form data can be used to send text data
~ form_text_1 = lorem ipsum.
~ form_text_2 = {{ file_1 }}
# form data can also be used to send files
~ file_1 = @./test_file.txt
```
### Request Body
The request body is the data sent with the request. The body is a multiline block specified with the `~~~` characters. The body can optionally be followed by a content type. For example,
```text
~~~ application/json
{
"key": "value"
}
~~~
```
### User Prompts in Requests
User input can be requested interactively at runtime by using the `[[ variable_name ]]` syntax. A prompt may be used as a value for a query, header, form, or in the request body or URL. For example,
```text
GET https://example.com/todos/[[ todo_id ]]
? page = [[ page ]]
* Origin = [[ origin ]]
~ file = @[[ file_path ]]
```
Prompts made in a request will be displayed in the terminal when the request is executed.
#### Supplying Prompt Values from the CLI
Use the `--input` flag to pre-fill prompt values and skip interactivity. Each `--input` expects `key=value` and may be repeated. Keys correspond to the placeholder name inside `[[ ... ]]`.
```bash
hen --input foo=bar --input password=secret ./collection.hen
```
When a matching prompt is encountered, the provided value is used automatically. Any remaining prompts still fall back to the interactive dialog.
### Callbacks
Callbacks are shell commands that are executed after a request is made. Callbacks are defined in the request definition with the `!` character. For example,
```text
GET https://lorem-api.com/api/user
# inline shell command
! echo "Request completed."
# a shell script
! sh ./post_request.sh
```
If a request has multiple callbacks, they are executed in the order they are defined, top to bottom.
```text
GET https://lorem-api.com/api/user
# This is executed first
! echo '1'
# This is executed second
! echo '2'
```
#### Callback Execution Context
Callbacks are executed with response data passed as environment variables. The following environment variables are available to callbacks:
- `STATUS`: The HTTP status code of the response.
- `RESPONSE`: The response body of the request.
- `DESCRIPTION`: The description of the request.
For example, the following callback will assert that the status code of the response is 200.
```bash
#!/bin/bash
# ./post_request.sh
if [ "$STATUS" -eq "200" ]; then
echo "✅ [$DESCRIPTION] Received status 200"
echo $result
else
echo "❌ [$DESCRIPTION] Expected status 200 but got $STATUS"
echo $result
fi
```
```text
Echo body w. callback
POST https://lorem-api.com/api/health
! sh ./post_request.sh
```
## Defining an API Collection
A file containing multiple requests is called a collection. Collections can be used to group related requests together.
Collections can start with a preamble that contains metadata about the collection. The preamble is separated from the requests by a line containing three dashes `---`. The same line is also used to separate requests from each other.
```text
name = Optional Collection Name
description = Optional Collection Description
[VARIABLES]
[GLOBAL HEADERS]
[GLOBAL QUERIES]
[GLOBAL CALLBACKS]
---
[request 1]
---
[request 2]
---
etc.
```
### Global Headers, Queries and Callbacks
Any headers, queries or callbacks defined in the collection preamble become global and are included in all requests in the collection.
In the example below, the `Authorization` header and `page` query is included in all requests in the collection. When each request is executed and a response received, the callback `echo "Request completed."` is executed.
```text
* Authorization = foo
? page = 2
! echo "Request completed."
---
GET https://api.example.com/users
---
GET https://api.example.com/posts
```
> Global callbacks are executed before request-specific callbacks.
### User Prompts in Collections
User prompts can be used in a collection preamble. The user is prompted when the collection is loaded: either directly via the CLI or as a prompt in the interactive mode.
```text
$ foo = [[ bar ]]
---
POST https://lorem-api.com/api/echo
~~~ application/json
{ "foo" : "{{ foo }}" }
~~~
```
### Global Variables
Global variables are key/value pairs defined in the preamble of a collection file with the `$` character. For example,
```text
$ api_origin = https://example.com
$ api_key = abc123
$ username = alice
```
Variables can be used in the request definition by enclosing the variable name in double curly braces. For example,
```text
GET {{ api_origin }}/todos/2
* Authorization = {{ api_key }}
? username = {{ username }}
```
Variables can also be set dynamically by running a shell command. For example,
```text
$ api_key = $(./get_secret.sh)
$ username = $(echo $USER)
```
Or by setting the variable interactively:
```text
$ api_key = [[ api_key ]]
```
#### Request-Level Variables
Variables can also be declared inside an individual request. These request-level variables use the same `$ name = value` syntax and may appear before the HTTP method line or intermixed with other request statements. When both a global variable and a request variable share the same key, the request variable takes precedence for that request only.
```text
$ api_origin = https://lorem-api.com/api
---
Fetch banner image
$ banner_text = Promo%20Time!
$ banner_fill = 444444
GET {{ api_origin }}/image?text={{ banner_text }}&fill={{ banner_fill }}
```
Request variables fully support shell substitutions (`$(...)`) and interactive prompts (`[[ ... ]]`), and are resolved using the same working directory as the collection. This makes it easy to specialize a base request without modifying the collection preamble or other requests.
## Additional Syntax
### Comments
Comments are lines that are ignored by the parser. Comments start with the `#` character. For example,
```text
# This is a comment
GET https://example.com/todos/2
```
### Fragments
Fragments are reusable blocks of text that can be included in multiple requests. Fragments are defined in a separate file and included in a request with the `<<` character. For example,
```text
# .fragment.hen
* Authorization = abc123
```
```text
GET https://example.com/todos/2
<< .fragment.hen
```
Fragment paths can be absolute or relative to the collection file.
Fragments can contain multiple requests, headers, query parameters, and request bodies. Fragments can also contain variables and other fragments.
## Additional Features
### Export Requests
Requests can be exported as curl commands. This is useful for debugging or sharing requests with others.
```text
$ API_URL = https://lorem-api.com/api
---
POST {{ API_URL }}/echo
~~~ application/json
{ "foo" : "bar" }
~~~
```
```bash
curl -X POST 'https://lorem-api.com/api/echo' -H 'Content-Type: application/json' -d ' { "foo" : "bar" }'
```
Exporting happens once all variables and prompts have been resolved and the request is ready to be executed. Callbacks are ignored during export.
### Benchmarking
Requests can be benchmarked by specifying the `--benchmark` flag with the number of iterations to run. This will run the request the specified number of times and output the average time taken to complete the request.
```bash
hen /path/to/collection_file.hen --benchmark 10
```
```text
Benchmarking request: Echo form data
[##################################################] 100.00%
Mean Duration: 399.95937ms
Variance (%): 0.6831308901940525
```
Notes:
- Callbacks are ignored when benchmarking.
- User prompts will still be executed in benchmarked requests, and so should be avoided, or used in the preamble only.
## Examples
### Basic Request
```text
GET https://lorem-api.com/api/user/foo
```
### Request with Headers, Query Parameters, and Form Data
```text
POST https://lorem-api.com/api/echo
# Header
* foo = abc123
# Query
? bar = abc123
# Form Data
~ baz = abc123
```
### Request with Request Body
```text
POST https://lorem-api.com/api/jwt
~~~ application/json
{
"username": "bar",
"password": "qux"
}
~~~
```
### Request with Callback
```bash
#!/bin/bash
if [ "$STATUS" -eq "200" ]; then
echo "✅ [$DESCRIPTION] Received status 200"
else
echo "❌ [$DESCRIPTION] Expected status 200 but got $STATUS"
fi
```
```text
GET https://lorem-api.com/api/user
! sh callback.sh
```