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:
| precedence | for | |
|---|---|---|
std.config | env > file > default | a deployed service reading its settings |
std.defaults | env > file | a 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.