std.config

Layered configuration: environment wins over file, file wins over default.

import std.config

cfg = config.load({
    port: { default: 8080, env: "APP_PORT" },
    host: { default: "127.0.0.1", env: "APP_HOST" },
    db_password: { env: "APP_DB_PASSWORD", required: true, secret: true },
}, "config.toml")

print(cfg.port)                 # APP_PORT, else the file, else 8080
print(cfg.db_password)          # [secret] - reveal(cfg.db_password) to use it

config.load(spec, file?) takes one entry per key. Each key's descriptor is { default?, env?, required?, secret? }, and the file, when given, is the layer between the defaults and the environment.

Needs env and fs:read for package code.

The precedence, and why this way round

Environment beats file because a deployment must be able to override what a checked-in file says without editing it. That is the standard shape for a service.

std.defaults follows the same rule since 0.58: its environment block applies only to variables the shell has not set. The two answer different questions:

precedencefor
std.configenv > file > defaulta deployed service reading its settings
std.defaultsenv > filea project's defaults for the variables its code reads

Use config for runtime settings that vary by environment; use defaults for project facts that should be the same everywhere.

Secret fields

A key with secret: true arrives already wrapped in secret, so they render as [secret] through printing, logging and tracing, and need reveal to use. Declaring them is one line and it removes a whole class of accidental credential leak.

That covers the load error too: a secret field whose value does not match the type of its default reports the variable without the value.

config: APP_DB_PASSWORD is not an integer     # secret: named, not shown
config: APP_PORT: 'eighty' is not an integer  # ordinary: shown, so you can fix it

Environment names

Each key names its own variable with env; a key without one is read from the file and the default only. A common prefix is worth using - APP_PORT rather than PORT stops a generic name from being picked up from an unrelated part of the environment.

Types come from the defaults

A value from the environment is text. A key's default tells config what type to coerce to, so port is an int because 8080 is; a Bool default reads true/1/yes/on and false/0/no/off. A key with no default stays a string. A default is therefore worth providing for every key, even where you expect it always to be set.

Errors

A file you name that is missing raises { kind: "fs", path }; a malformed one raises { kind: "parse" } with its format. A required key that no layer supplies is an error naming every missing key, and its variable. Validate at startup and fail loudly - a service that starts with half its configuration is worse than one that refuses to start.