Skip to content

Latest commit

 

History

History
111 lines (85 loc) · 3.43 KB

File metadata and controls

111 lines (85 loc) · 3.43 KB

Configuration

Typed configuration with zero boilerplate: describe your config as an interface, bind it to a file with readConfig, and the compiler synthesizes an implementor that loads the file once and deserializes each value.

import "std/config.xi"

type TaxConfig = { percent: Number, rate: Integer }

interface AppConfig {
    mapper projectName() -> String      // reads the `projectName` key
    mapper tax() -> TaxConfig           // reads + decodes the `tax` key
    mapper flags() -> Flags
}

module App  { bind AppConfig -> readConfig("application.yaml") }
module Test { bind AppConfig -> readConfig("application-test.yaml") }

Inject it like any other dependency:

async entry (cfg: AppConfig, logger: Logger) main(args: String[]) -> Integer {
    logger.info("project = " + cfg.projectName())
    logger.info("tax = " + number_to_str(cfg.tax().percent) + "%")
    return 0
}
# application.yaml
projectName: Ledger
tax:
  percent: 20.0
  rate: 3

readConfig<T> — read any file into a value

Beyond binding a whole interface, you can read a single file into a value with the generic readConfig<T> form. The format is chosen by extension — JSON, YAML, and XML are all supported:

import "std/config.xi"
type Tax = { percent: Number, rate: Integer }

let tax  = readConfig<Tax>("tax.yaml")     // or .json / .xml
let tax2 = readConfig<Tax>(path)           // the path can be dynamic

Primitives and (nested) compounds decode automatically via the derived codec; a missing key is the zero value.

How it works

  • Each interface method name maps to a top-level config key (tax() → the tax key).
  • The return type drives deserialization: primitives (String, Number, Integer, Bool) are read directly; compound types are decoded via the derived JSON codec (nested objects supported).
  • The file is read once (a singleton) on first use.
  • YAML and JSON are both supported, chosen by the file extension.
  • A missing key yields the type's zero value (empty string, 0, false, all-zero compound) — it never crashes.

Live reload — ApplicationConfig

std/config ships an ApplicationConfig service (default impl FileApplicationConfig). Inject it and call watch(file, topic); a background watcher polls the file's mtime and publishes a ConfigChanged { file } event (via std/events) whenever it's edited — re-read the config in a listener to hot-reload:

import "std/config.xi"

class Reloader {
    deps {}
    listener onChange(e: ConfigChanged) on "config.changed" {
        let cfg = readConfig<AppConfig>(e.file)     // pick up the new values
        ...
    }
}

async entry (cfg: ApplicationConfig) main(args: String[]) -> Integer {
    cfg.watch("application.yaml", "config.changed")
    let pump = Events.runAsync()                     // deliver events on a worker
    ...
}

Bind your own ApplicationConfig (an OS-native watcher, or a no-op in tests) to change the watching strategy — callers don't change.

Test configuration

In a test build (xi test), a bind inside module Test wins over module App, so tests transparently load a test config:

module App  { bind AppConfig -> readConfig("application.yaml") }
module Test { bind AppConfig -> readConfig("application-test.yaml") }

See examples/config_demo.xi.

readConfig is recognized by the compiler only as a bind target — it is not a callable function.