HTTP & Networking

Parsley uses the fetch operator <=/= to make HTTP requests. You create a URL or request handle on the right side, and the operator fetches the content. Response data is automatically parsed based on the handle's format.

The Fetch Operator (<=/=)

The fetch operator works like the file read operator (<==) but for network resources:

let response <=/= JSON(@https://api.example.com/users)

The left side is a variable binding. The right side is a URL handle (a format function wrapping a URL literal) or a plain URL.

Fetch as an Expression

The fetch operator can also be used as a standalone expression on the right side of an assignment. In this form, <=/= is a prefix operator — the result is captured into a variable:

let response = <=/= JSON(@https://api.example.com/users)
response.data                    // the parsed content
response.ok                      // true if status 200–299

This is equivalent to the statement form (let response <=/= ...) but works anywhere an expression is expected — in function arguments, conditionals, or chained operations:

// Use in a conditional
if ((<=/= JSON(@https://api.example.com/health)).ok) {
    "API is up"
}

// Pass directly to a function
processUsers(<=/= JSON(@https://api.example.com/users))

URL Handles

Wrap a URL literal in a format function to control how the response is parsed:

Handle Parses as Description
JSON(@https://...) dictionary/array Parse response as JSON
YAML(@https://...) dictionary/array Parse response as YAML
text(@https://...) string Raw response body
lines(@https://...) array Response split into lines
raw(@https://...) array Raw byte array
let users <=/= JSON(@https://api.example.com/users)
let readme <=/= text(@https://raw.githubusercontent.com/user/repo/main/README.md)

A plain URL (without a format wrapper) is fetched as text:

let html <=/= @https://example.com

Response Object

When assigned to a single variable, the fetch operator returns a response dictionary with metadata:

let response <=/= JSON(@https://api.example.com/users)
response.data                    // the parsed content
response.status                  // 200
response.statusText              // "200 OK"
response.ok                      // true (status 200–299)
response.url                     // final URL (after redirects)
response.headers                 // response headers dictionary

The response wraps the parsed data alongside HTTP metadata. Access the data directly through dictionary destructuring or via the data property.

Error Handling

Use the {data, error} destructuring pattern to capture network errors instead of halting:

let {data, error} <=/= JSON(@https://api.example.com/users)
if (error) {
    "Fetch failed: " + error
} else {
    for (user in data) {
        user.name
    }
}

The error-capture pattern returns a dictionary with these fields:

Field Type Description
data varies or null Parsed response content (null on error)
error string or null Error message (null on success)
status integer HTTP status code (0 if request failed entirely)
headers dictionary Response headers (empty dict if request failed)

Without the {data, error} pattern, a failed fetch produces a network-class error that propagates up the call stack.

HTTP Methods

The default HTTP method is GET. To use other methods, pass an options dictionary as the second argument to a format factory function, or use the write operator (==>) with method accessors.

Options Dictionary

Pass method, body, headers, and timeout as a second argument to any format factory:

// POST with JSON body
let {data, error} <=/= JSON(@https://api.example.com/users, {
    method: "POST",
    body: {name: "Alice", email: "alice@example.com"},
    headers: {Authorization: "Bearer token123"}
})
Option Type Default Description
method string "GET" HTTP method (GET, POST, PUT, DELETE, PATCH)
body any none Request body (dictionaries/arrays auto-serialized as JSON)
headers dictionary none Custom request headers
timeout integer 30000 Timeout in milliseconds

When body is a dictionary or array, it is automatically JSON-encoded and Content-Type is set to application/json (unless you override it in headers).

Method Accessors

Format factory handles have .get, .post, .put, .patch, and .delete accessors that return a new request handle with the method set:

let api = JSON(@https://api.example.com/users)
api.get                              // GET request handle
api.post                             // POST request handle
api.put                              // PUT request handle
api.delete                           // DELETE request handle
Accessor Method Use with
.get GET <=/= (default, rarely needed)
.post POST =/=> (default, rarely needed)
.put PUT =/=>
.patch PATCH =/=>
.delete DELETE <=/=

The Remote Write Operator (=/=>)

Use =/=> to send data to a network target. The left side is the data to send (becomes the request body). The right side is a URL handle. The method defaults to POST unless the handle specifies PUT or PATCH:

// POST (default)
{name: "Alice", email: "alice@example.com"} =/=> JSON(@https://api.example.com/users)

// PUT (explicit via accessor)
{name: "Alice Smith"} =/=> JSON(@https://api.example.com/users/123).put

// PATCH
{age: 31} =/=> JSON(@https://api.example.com/users/123).patch

The =/=> operator only accepts network targets (HTTP request handles or SFTP file handles). For local file writes, use ==>.

The append variant =/=>> works the same way but signals append semantics (relevant for SFTP targets):

"log entry\n" =/=>> text(sftp, "/var/log/app.log")

Remote Write as an Expression

Like fetch, the remote write operator is a true expression — it returns a response object that you can capture:

// Capture the full response
let response = payload =/=> JSON(@https://api.example.com/items)
response.data                    // response body (parsed)
response.status                  // HTTP status code
response.ok                      // true if status 200–299

This works for all remote write variants (=/=> and =/=>>).

Error Handling for Remote Writes

// Capture the full response
let response = payload =/=> JSON(@https://api.example.com/items)
if (!response.ok) {
    `Failed: {response.status} - {response.error}`
}

// Destructured capture
let {data, error} = payload =/=> JSON(@https://api.example.com/items)
if (error) {
    `Error: {error}`
}

When using {data, error} destructuring on a remote write expression, the typed response is automatically converted to the legacy {data, error, status, headers} shape for compatibility.

Examples

// PUT with options dictionary
let {data, error} <=/= JSON(@https://api.example.com/users/123, {
    method: "PUT",
    body: {name: "Alice Smith"}
})

// DELETE (no body needed)
let {data, error} <=/= JSON(@https://api.example.com/users/123, {
    method: "DELETE"
})

Interpolated URLs

URL literals support interpolation with @(...) syntax:

let userId = 123
let user <=/= JSON(@(https://api.example.com/users/{userId}))

SFTP

Parsley supports SFTP connections for reading and writing files on remote servers.

Creating an SFTP Connection

let sftp = @sftp("sftp://user@host:22", {
    keyFile: @~/.ssh/id_rsa
})

The first argument is an SFTP URL. The second (optional) argument is an options dictionary:

Option Type Description
keyFile path or string Path to SSH private key
passphrase string Passphrase for encrypted key
password string Password authentication
knownHostsFile path or string Path to known_hosts file
timeout duration Connection timeout (default 30s)

At least one authentication method (key file or password) must be provided.

Reading and Writing via SFTP

Use the network I/O operators with SFTP connections:

// Read a remote JSON file (network read)
let config <=/= JSON(sftp, "/etc/app/config.json")

// Write to a remote file (network write)
"new content" =/=> text(sftp, "/var/data/output.txt")

// Append to a remote log (network append)
"log entry\n" =/=>> text(sftp, "/var/log/app.log")

Connection Methods

Method Returns Description
.close() null Close the SFTP connection

Common Patterns

Fetch and Transform

let users <=/= JSON(@https://api.example.com/users)
let names = for (user in users) {
    user.name
}

API with Authentication

let request = {
    url: @https://api.example.com/data,
    method: "GET",
    format: "json",
    headers: {
        Authorization: "Bearer " + apiToken,
        Accept: "application/json"
    }
}
let {data, error} <=/= request

Safe Fetch with Fallback

let {data, error} <=/= JSON(@https://api.example.com/config)
let config = if (error) { defaults } else { data }

Key Differences from Other Languages

See Also