External inputs
Rank can read from the outside world, but every external dependency must be explicitly declared. The compiler refuses to perform ambient IO — every network request, file read, or environment lookup is part of the dependency graph and can be listed with rank deps.
External inputs are source nodes in the DAG — they have no incoming edges. Bindings that read from them depend on those nodes like any other:
Environment variables
Env<T> {} reads named environment variables from the host. The type parameter is an object type describing which variables to read:
AppEnv = Object {
DATABASE_URL: string,
PORT?: string,
DEBUG?: string,
...: string,
}
env = Env<AppEnv> {}
port = env.PORT ?? `5432`
The environment is snapshotted exactly once per compilation. Only fields declared in the schema type are materialized into the resulting value. The ...: string rest type allows additional keys to exist in the environment without being read.
Optional fields (field?: string) become null when the variable is absent. Required fields cause a compile error if absent.
When the host value is text but the rest of the program wants a non-string type, decode it at the boundary:
use std::Decode
AppEnv = Object {
PORT?: Decode<8080 | 8443>,
DEBUG?: Decode<bool>,
...: string,
}
env = Env<AppEnv> {}
port = env.PORT ?? 8080
debug = env.DEBUG ?? false
This keeps parsing at the edge instead of matching on raw strings later. See Standard library.
File reads
File::Read<T> { path, format } reads a structured file at a relative path:
use std::File
use std::Path
use std::collections::{ map }
use std::list::{ reduce }
Items = [Object { id: string, name: string, count: number }]
response = File::Read<Items> {
path: Path::join([`data`, `items.json`]),
format: `json`,
}
items = response.body
total = items |> map(|item| item.count) |> reduce(0, |acc, n| acc + n)
The result is an object with two fields:
| Field | Type | Description |
|---|---|---|
body | T | Parsed, type-checked file contents |
ctx | Object { path: string, format: string } | Metadata about the read |
Supported formats:
| Format | Decoded as |
|---|---|
json | Ordinary Rank lists and objects |
yaml | Ordinary Rank lists and objects |
csv | List of header-keyed objects with string values |
dotenv | Flat string-keyed object, with the same field-local and schema-wide Decode<...> support as Env<T> {} |
path must be a Path::RelativePath value. It resolves relative to the containing module file. Absolute paths and paths that escape upward (../) are rejected. The path must be statically evaluable — it cannot depend on a runtime value.
For .env-style files, keep parsing at the boundary just like Env<T> {}:
use std::Decode
use std::File
use std::Path
Config = Object {
APP_ENV: `dev` | `prod`,
PORT?: Decode<8080 | 9000>,
DEBUG?: Decode<bool>,
...: string,
}
config = File::Read<Config> {
path: Path::join([`.env`]),
format: `dotenv`,
}
port = config.body.PORT ?? 8080
debug = config.body.DEBUG ?? false
Dotenv decoding stays intentionally narrow in v0:
- the file decodes as a flat string-keyed object
- field-local
Decode<U>and schema-wideFile::Read<Decode<T>>are supported for the same scalar targets asEnv<T> {} - one dotenv entry does not decode into a nested list or object value
- interpolation and shell-style evaluation are not supported
HTTP fetches
HTTP::Fetch<T> { ... } makes a network request and decodes the response body against T:
use std::HTTP
Response = Object {
users: [Object { id: number, name: string }],
}
response = HTTP::Fetch<Response> {
url: `https://api.example.com/users`,
method: `GET`,
headers: {
Accept: `application/json`,
},
cacheKey: `users-v1`,
}
users = response.body.users
status = response.ctx.status
Request fields
| Field | Required | Description |
|---|---|---|
url | yes | Request URL (must be a string literal or statically evaluable) |
method | yes | HTTP method: GET, POST, etc. |
headers | no | Object of header name → value strings |
cacheKey | yes | Stable string key used to replay the response deterministically |
Result shape
The result is an object with two fields:
| Field | Type | Description |
|---|---|---|
body | T | Parsed, type-checked response body |
ctx | see below | Request metadata |
ctx fields:
| Field | Type | Description |
|---|---|---|
status | number | HTTP status code |
cacheKey | string | The cacheKey value from the request |
headers | Object { ...: string } | Response headers |
HTTP snapshot caching
The cacheKey field is required. It makes fetches deterministic: two runs with the same cacheKey and a snapshot file replay identically without a live network request.
Save a snapshot after a live run:
rank src/main.rank --write-http-cache snapshots.json
Replay from the snapshot on subsequent runs:
rank src/main.rank --http-cache snapshots.json
The snapshot cache is a rank/http-cache JSON document keyed by each fetch's cacheKey. Committing the snapshot to source control makes CI runs fully reproducible without network access.
Providers
Providers are out-of-process extensions registered in rank.toml. They expose typed functions under a namespace and can come from a local path or an npm package. Pure reusable library code belongs under [dependencies] instead, which may be local, npm-backed, or git-backed. See the Provider guide, Dependencies, and Provider authoring guide for full details.
use faker::{ Generate, UUID }
spec = { id: UUID {} }
pub main = || Generate<Object { id: string }> {
spec,
seed: 42,
}
Provider capabilities
Providers that need network access or other privileged capabilities must declare them in their manifest. At the call site, the capability must be explicitly opted into:
rank src/main.rank --allow-provider-capability network
This makes capability use auditable — a CI script that does not pass --allow-provider-capability will fail if the program unexpectedly depends on a network-capable provider.