14 releases (6 breaking)

0.7.1 Oct 29, 2024
0.7.0 Oct 29, 2024
0.6.1 Oct 26, 2024
0.5.1 Oct 25, 2024
0.1.0 Oct 20, 2024

#109 in Web programming

Download history 799/week @ 2024-10-19 445/week @ 2024-10-26 48/week @ 2024-11-02 1/week @ 2024-11-09 8/week @ 2024-11-16 2/week @ 2024-11-23

73 downloads per month

MIT license

44KB
740 lines

Hen

Run API requests as files, from the command line.

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

cargo install hen

Usage

Usage: hen [OPTIONS] [PATH] [SELECTOR]

Arguments:
  [PATH]
  [SELECTOR]

Options:
      --export
      --benchmark <BENCHMARK>
  -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:

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:

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:

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.

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.


[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,

* 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,

? 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,

$ 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,

~~~ application/json
{
  "key": "value"
}
~~~

User Prompts

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,

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.

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,

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.

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.

#!/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
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.

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.

* 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

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.

$ foo = [[ bar ]]
---
POST https://lorem-api.com/api/echo

~~~ application/json
{ "foo" : "{{ foo }}" }
~~~

Variables

Variables are key/value pairs defined in the preamble of a collection file with the $ character. For example,

$ 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,

GET {{ api_origin }}/todos/2

* Authorization = {{ api_key }}

? username = {{ username }}

Variables can also be set dynamically by running a shell command. For example,

$ api_key = $(./get_secret.sh)
$ username = $(echo $USER)

Or by setting the variable interactively:

$ api_key = [[ api_key ]]

Additional Syntax

Comments

Comments are lines that are ignored by the parser. Comments start with the # character. For example,

# 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,

# .fragment.hen

* Authorization = abc123
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.

 $ API_URL = https://lorem-api.com/api

---

POST {{ API_URL }}/echo

~~~ application/json
{ "foo" : "bar" }
~~~
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.

hen /path/to/collection_file.hen --benchmark 10
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

GET https://lorem-api.com/api/user/foo

Request with Headers, Query Parameters, and Form Data

POST https://lorem-api.com/api/echo

# Header
* foo = abc123

# Query
? bar = abc123

# Form Data
~ baz = abc123

Request with Request Body

POST https://lorem-api.com/api/jwt

~~~ application/json
{
  "username": "bar",
  "password": "qux"
}
~~~

Request with Callback

#!/bin/bash

if [ "$STATUS" -eq "200" ]; then
    echo "✅ [$DESCRIPTION] Received status 200"
else
    echo "❌ [$DESCRIPTION] Expected status 200 but got $STATUS"
fi
GET https://lorem-api.com/api/user

! sh callback.sh

Dependencies

~11–22MB
~313K SLoC