> this document (README) is partially generated by AI. Please report any bugs you find.
## hutc
Lua-driven HTTP API test runner built in Rust.
## Installation
Install from crates.io:
```bash
cargo install hutc
```
## Quick Start
1. Generate Lua language-server definitions:
```bash
hutc init
```
This creates `defs/hutc.defs.lua`.
2. Create a test file at `tests/health.lua`:
```lua
local client = http():base_url("https://example.com")
test("health endpoint is up", function()
local res = client:req():path("/health"):get()
expect(res.status):to_equal(200)
expect(res.ok):to_be_true()
end)
```
3. Run tests:
```bash
hutc test
```
## Default Paths
- `hutc test` reads `.lua` files from `tests/`
- `hutc init` writes to `defs/hutc.defs.lua`
## Lua API
### Globals
| `test(name, fn)` | Registers a test case |
| `expect(value)` | Creates an assertion object |
| `http()` | Creates an HTTP client |
| `log(...)` | Prints debug values |
### Assertions
`expect(value)` returns an `Expect` object:
| `:msg("message")` | Adds custom message prefix on failure |
| `:to_equal(expected)` | Asserts equality |
| `:to_not_equal(expected)` | Asserts inequality |
| `:to_exist()` | Asserts value is not `nil` |
| `:to_be_nil()` | Asserts value is `nil` |
| `:to_be_true()` | Asserts value is `true` |
| `:to_be_false()` | Asserts value is `false` |
| `:to_be_type(type)` | Asserts value has expected type |
| `:to_contain(substring)` | Asserts string contains substring |
| `:to_be_greater_than(n)` | Asserts value > n |
| `:to_be_lesser_than(n)` | Asserts value < n |
| `:to_be_between(min, max)` | Asserts min <= value <= max |
Example:
```lua
expect(res.status):msg("status mismatch"):to_equal(200)
expect(res.json.user):to_exist()
expect(res.ok):to_be_true()
expect(res.body):to_contain("success")
```
### HTTP Client
Create a client:
```lua
local client = http():base_url("https://api.example.com")
```
Create a request with `client:req()`, then chain request-builder methods:
| `:path("/users")` | Relative path (uses `base_url`) |
| `:url("https://...")` | Absolute URL (overrides path/base_url) |
| `:header("k", "v")` | Single header |
| `:headers({ k = "v" })` | Multiple headers |
| `:query("k", "v")` | Single query param |
| `:queries({ k = "v" })` | Multiple query params |
| `:body("text")` | Plain text body |
| `:body_bytes("raw")` | Raw bytes body |
| `:json('{"k":"v"}')` | JSON body as raw JSON string |
| `:form({ k = "v" })` | Form body |
| `:timeout_ms(5000)` | Request timeout in milliseconds |
| `:bearer("token")` | Sets `Authorization: Bearer <token>` |
Execute with one of:
| `:get()` | Execute as GET request |
| `:post()` | Execute as POST request |
| `:put()` | Execute as PUT request |
| `:patch()` | Execute as PATCH request |
| `:delete()` | Execute as DELETE request |
| `:send()` | Execute with default method (GET) |
### HTTP Response
Each request returns a response table:
| `status` | `integer` | HTTP status code |
| `ok` | `boolean` | `true` for `2xx` responses |
| `body` | `string` | Raw response body |
| `url` | `string` | Final response URL |
| `duration_ms` | `integer` | Request duration |
| `headers` | `table<string, string>` | Response headers |
| `content_type` | `string?` | Response content-type header |
| `json` | `any?` | Parsed JSON (if body is valid JSON) |
| `json_error` | `string?` | JSON parse error (if parsing failed) |
## Example
```lua
local client = http():base_url("https://jsonplaceholder.typicode.com")
test("GET /posts returns data", function()
local res = client
:req()
:path("/posts")
:query("_limit", "1")
:get()
expect(res.status):to_equal(200)
expect(res.json):to_exist()
expect(res.json[1].id):to_exist()
end)
test("POST /posts creates resource", function()
local res = client
:req()
:path("/posts")
:json('{"title":"hello","body":"world","userId":1}')
:post()
expect(res.status):to_equal(201)
expect(res.json.id):to_exist()
end)
```
## Development
Run from source:
```bash
cargo run -- test
```