Config

The config source-stdlib module provides helpers for nested configuration dictionaries. It is useful for merging defaults, environment-specific override layers, parsed config files, and application settings passed into framework components.

import config;
import io;

let cfg = config.layer([
    {"debug": false, "db": {"host": "localhost", "port": 5432}},
    {"debug": true, "db": {"database": "app"}}
]);

io.println(config.require(cfg, "db.host"));
io.println(config.getOr(cfg, "cache.ttl", 300));

Functions

Function Returns Description
clone(data) dict<string, any> Deep-copy a configuration dictionary and its nested lists
merge(base, overrides) dict<string, any> Recursively merge overrides into a copy of base
defaults(values, defaultValues) dict<string, any> Apply defaults first, then explicit values
layer(layers) dict<string, any> Merge a list of dictionaries from first to last
has(data, path) bool Test whether a dotted path exists
get(data, path) any Return a required dotted-path value or throw ValueError
getOr(data, path, fallback) any Return a dotted-path value or fallback
require(data, path) any Alias for get when the call site wants explicit required semantics
parse(format, text) Config Parse a serde-supported document into a Config object
load(options) Config Load layered files, explicit environment bindings, and overrides
validate(options) dict<string, any> Load and report {valid, errors, config} without throwing for schema errors

merge is recursive only when both sides at a key are dictionaries. Otherwise the override value replaces the base value.

let base = {
    "app": {"name": "Geb", "debug": false},
    "cache": {"ttl": 60}
};

let local = {
    "app": {"debug": true},
    "cache": {"driver": "redis"}
};

let merged = config.merge(base, local);
io.println(merged["app"]["name"]);
io.println(merged["app"]["debug"]);
io.println(merged["cache"]["driver"]);

Dotted Paths

Dotted paths walk nested dictionaries. They do not parse list indexes.

let settings = {
    "db": {
        "primary": {
            "host": "localhost",
            "port": 5432
        }
    }
};

io.println(config.has(settings, "db.primary.host"));
io.println(config.get(settings, "db.primary.port"));
io.println(config.getOr(settings, "db.replica.host", "none"));

get and require throw ValueError when any path segment is missing. getOr returns the fallback if a segment is missing or a non-dictionary value is encountered before the end of the path.

Config

Config is an immutable-style wrapper around a cloned dictionary.

Method Returns Description
has(path) bool Test whether a dotted path exists
get(path) any Return a required dotted-path value
require(path) any Alias for get
getOr(path, fallback) any Return a value or fallback
toDict() dict<string, any> Return a deep copy of the stored config
let cfg = config.Config({
    "mail": {"from": "[email protected]"}
});

io.println(cfg.require("mail.from"));
io.println(cfg.getOr("mail.transport", "smtp"));

Parsing

config.parse(format, text) delegates to serde.parse, so it supports the formats available through the serialization modules, such as json, yaml, toml, and xml where the parsed top-level value is an object.

let cfg = config.parse("json", '{"server":{"port":8080}}');
io.println(cfg.require("server.port"));

Use Config.toDict() when an API expects a plain dictionary:

let options = cfg.toDict();

Layered loading

config.load(options) accepts the following keys. Unknown keys are errors.

Key Value
baseDir Base directory for relative paths; defaults to the current working directory
defaults Base dictionary; defaults to {}
files Ordered list of {path, format?} entries; format is json, yaml, or toml and defaults to the file extension
dotenvFiles Ordered list of paths parsed as environment data
env Mapping from config dotted paths to {name, type?} environment bindings
overrides Final dictionary applied after every other source
schema Optional schema passed to schema.validate

Sources are merged in this order: defaults, config files, dotenv files through the named bindings, process environment through the same bindings, and final overrides. Later values take precedence. Nested dictionaries merge recursively; lists and scalar values replace earlier values. Inputs and returned dictionaries are deep-copied, so modifying them does not change a loaded Config.

Bindings default to string. Their type can be int, float, bool, or json. Boolean values accept true and false without regard to case; numeric values must parse completely. An absent variable leaves the existing value untouched. An empty variable is present and may fail a typed conversion. Only named variables are read. Dotenv files do not change the process environment. A dotted binding can create missing nested dictionaries; it fails if an intermediate value is a scalar.

import config;

let settings = config.load({
    "defaults": {"server": {"host": "127.0.0.1", "port": 8080}},
    "files": [{"path": "settings.json"}, {"path": "local.toml"},
              {"path": "site.yaml"}],
    "dotenvFiles": [".env"],
    "env": {"server.port": {"name": "APP_PORT", "type": "int"}},
    "overrides": {"server": {"host": "0.0.0.0"}}
});

Files must exist and have a dictionary at the root. load throws a catchable ValueError for bad options, files, conversion values, or schema failures. validate(options) returns a dictionary with valid, errors, and config; schema errors appear in errors while config retains the loaded values.