Skip to main content

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:

FieldTypeDescription
bodyTParsed, type-checked file contents
ctxObject { path: string, format: string }Metadata about the read

Supported formats:

FormatDecoded as
jsonOrdinary Rank lists and objects
yamlOrdinary Rank lists and objects
csvList of header-keyed objects with string values
dotenvFlat 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-wide File::Read<Decode<T>> are supported for the same scalar targets as Env<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

FieldRequiredDescription
urlyesRequest URL (must be a string literal or statically evaluable)
methodyesHTTP method: GET, POST, etc.
headersnoObject of header name → value strings
cacheKeyyesStable string key used to replay the response deterministically

Result shape

The result is an object with two fields:

FieldTypeDescription
bodyTParsed, type-checked response body
ctxsee belowRequest metadata

ctx fields:

FieldTypeDescription
statusnumberHTTP status code
cacheKeystringThe cacheKey value from the request
headersObject { ...: 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.