Skip to content
You are reading v0.3, which is not the current release. Go to v0.4.

std::config

Settings read out of the environment, through a schema, and reported all at once.

The environment is a source like any other: a JSON body, a query string and a database row are all read by one Schema<A>, and so is this. What this module adds is the walk – which variables a shape needs, and what to call them – and the report, which spells a path the way a deployment does.

derive(Show, Decode)
type Listen = { host: String, port: Int };
derive(Show, Decode)
type Settings = { listen: Listen, password: Redacted<String>, debug: Option<Bool> };
fn settings() -> Validated<Settings, Rejection> with { env: Env } {
read(Settings::schema())
}

reads LISTEN_HOST, LISTEN_PORT, PASSWORD and DEBUG, and answers every problem rather than the first: a person fixing a deployment wants the list, not one line of it per restart.

A field is its name in upper case; a nested record’s fields are joined to it with _, so listen.port is LISTEN_PORT. A list is one variable split on commas, TAGS=a,b. A variant is one variable holding the case, MODE=Remote, and its payload fields beside it, MODE_URL. A key given by key("http_host", ..) replaces the field’s name in that scheme and nothing else. An optional nested record is present when any of its variables is.

Every variable a shape needs is variables, which is the question a deployment asks and a program should not have to be started to answer.

The environment cannot say whether 8080 was meant as a number, so it does not: every value is Raw::Untyped, which every primitive reads. Strictness is a fact about the source, and this source has none to offer.

A variable the manifest does not grant is a different thing from one nobody set, and the fix is a line in khora.toml rather than a line in a deployment script. Env::variable raises for it; this catches the raise into Raw::Denied, which decodes as Problem::Denied, and report names the file.

Functions: read · variables · describe · report

pub fn read<A>(schema: Schema<A>) -> Validated<A, Rejection> with { env: Env }

Reads the settings a schema describes, every problem reported.

The schema’s shape decides which variables are read and what they are called; the schema’s decoder decides what they mean. A record schema is what this is for: a bare int() at the top level has no name to read.

import std::config::{read, report};
import std::env::{Env};
import std::schema::{Decode};
derive(Decode)
pub type Settings = { listen_port: Int, database_url: String };
pub fn main() -> Int {
with { env: Env::real() } {
match read(Settings::schema()) {
Validated::Valid(settings) => start(settings),
Validated::Invalid(problems) => { print(report(problems)); 1 },
}
}
}

The shape decides the variable names: listen_port is read from LISTEN_PORT. A record written once with derive(Decode) reads from the environment, a request body and a test fixture alike, which is what it means for the schema to be a property of the type.

pub fn variables(shape: Shape) -> List<String>

The variables a shape needs, in the order they would be read.

[LISTEN_HOST, LISTEN_PORT, PASSWORD, DEBUG] for the settings above.

pub fn describe(problem: Rejection) -> String

One line a person can act on, with the path spelled as the variable it came from: LISTEN_PORT should be a whole number, and is "eighty".

pub fn report(problems: List<Rejection>) -> String

Every reason, one per line, in the order they were found.

What a service prints just before it stops, and the reason the whole module answers Validated: this list is allowed to have more than one thing in it.

Khora 0.3.0built frombd964738afb6on 2026-10-17 20:41ZVersioningSecurityContributingReleasesSource