Sign in

    moon-lander-config

    Layered, typed configuration for the Moon-Lander framework

    Download zip
    Author
    Version
    0.7.0
    License
    Apache-2.0
    Last updated
    8 minutes ago
    Downloads
    203

    #Configuration

    The config package (@config) loads configuration for a MoonBit application. It reads ordered sources, parses files with the formats that you register, and merges everything into one value tree. The package is pure: it never reads a file, the process environment, or the working directory itself. A host gives it those inputs, so a load gives the same result on wasm, wasm-gc, js, and native.

    Import moonrockz/moon-lander-config as @config and a format package such as moonrockz/moon-lander-config/parsers/toml as @toml. Each mbt check block on this page runs as a test with mise run test.

    [!NOTE] The configuration core is in progress. This page shows the API that exists today. Merge policies, secrets, and validation come in later changes. The design overview shows the full plan.

    #One load, start to finish

    An application describes its sources with a builder. The pure load reads only the builder and a file tree. When the tree does not hold a file that a source needs, load asks for it. The host reads the files and calls load again.

    graph TD B[Builder and file tree] --> L[Load] L --> N[Need: file requests] N --> H[Host adds the answers to the tree] H --> A[Load again] A --> C[Done: Config]

    StepWhat happens
    DescribeAdd sources and formats to a ConfigBuilder, in merge order
    Load@config.load(builder, tree) gives Need(requests) or Done(config)
    AnswerThe host adds each answer to the tree and loads again
    MergeEach source is parsed, filtered, and merged over the sources before it
    ReadConfig::get, get_text, Config::reports, and Config::warnings

    #The smallest program

    Sources that hold values need no files. build loads them over an empty, complete file tree. A later source wins.

    ///|
    test "the smallest program" {
    let config = @config.build([
    @config.in_memory_pairs([("app.name", @config.string("demo"))]),
    @config.in_memory_pairs([("app.name", @config.string("acme"))]),
    ])
    @expect.expect(@config.get_text(config, "app.name")).to_equal("acme")
    }

    #Sources and the builder

    Each source has a kind, a display name, a display priority, and an input. Add them to the builder in merge order: low first, high last. Every function returns a new builder, so you can keep a base builder and extend it.

    ConstructorKindInput
    ConfigSource::defaults(json), @config.defaults(builder, json)DefaultsA JSON object, for example Defaults::default().to_json()
    ConfigSource::defaults_value(value)DefaultsA value tree
    in_memory(value), in_memory_pairs(pairs)InMemoryA value tree, or key-path and value pairs
    ConfigSource::file(path, role~), @config.file(builder, path)File(role)One explicit file
    ConfigSource::candidates(paths, display~)File(role)A list of candidate files at one location
    ConfigSource::env(prefix~), @config.env(builder, prefix~)EnvironmentVariables that start with PREFIX_
    ConfigSource::overrides(pairs), @config.overrides(builder, pairs)OverridesKey-path pairs, for example from the command line
    ConfigSource::skipped(kind)anyNothing; the report shows Skipped

    ///|
    test "describe sources with a builder" {
    let base = @config.builder()
    |> @config.format(@toml.format())
    |> @config.defaults({ "server": { "port": 8080, "host": "localhost" } })
    let builder = base
    |> @config.file("/srv/app.toml")
    |> @config.add(
    @config.in_memory_pairs([("server.port", @config.int(9000))]),
    )
    let tree = @config.FileTree::new(complete=true).with_file(
    @config.FileTreePath::parse("/srv/app.toml"),
    (
    #|[server]
    #|host = "example.org"
    ),
    )
    guard @config.load(builder, tree) is Done(config) else {
    fail("a complete tree never asks for files")
    }
    @expect.expect(@config.get_text(config, "server.host")).to_equal(
    "example.org",
    )
    @expect.expect(@config.get_text(config, "server.port")).to_equal("9000")
    }

    #The layer stack

    Merge order decides which value wins. Each source also has a priority that reports show. When you give no priority, it comes from the kind and the file role. An in-memory source takes the priority of the source before it, so it fits at any position.

    Overrides (700) | command-line values Environment variables (600) | APP_SERVER__PORT=9000 User override file (400) | app.user.toml Local override file (390) | app.local.toml Environment file (375) | app.Staging.toml Workspace member file (350) | pkgs/api/app.toml Workspace, explicit, or legacy file (300) | app.toml Global user file (200) | ~/.config/app/ System file (100) | /etc/app/ Defaults (0) | compiled in

    Added in this order, the sources merge from the bottom up, so the top layer wins. A priority that is lower than the one before it is an error, because the list order and the reports would disagree. Equal priorities are allowed.

    ///|
    test "a decreasing priority is an error" {
    let builder = @config.builder()
    |> @config.add(@config.ConfigSource::skipped(Environment))
    |> @config.file("/srv/app.toml", priority=300)
    try @config.load(builder, @config.FileTree::new(complete=true)) catch {
    SourceOrder(previous~, previous_priority~, ..) =>
    @expect.expect((previous, previous_priority)).to_equal((
    "environment", 600,
    ))
    _ => fail("expected a source-order error")
    } noraise {
    _ => fail("expected a source-order error")
    }
    }

    #Files and the request loop

    A FileTree is an immutable snapshot of what the host has read: directory listings, file text, and absence records. A path is held, missing, or unknown. load asks only for unknown paths, and it asks for all of them in one step. In a complete tree every path that the tree does not hold is missing, so load never asks.

    Each path in the tree and in a request is a FileTreePath. Make one with FileTreePath::parse from the portable text, which uses /, or with FileTreePath::from_native from the native text of a platform. See File-tree paths.

    FileTree callEffect
    FileTree::new(complete?)An empty tree
    with_file(path, text)Holds the text of a file
    with_listing(dir, entries)Holds a directory listing with the real names; an Inaccessible entry can be a file or a directory, so the load asks for it when it is a candidate
    with_missing(path)Records a path as absent; a directory makes every path under it absent

    A host runs this loop. Native and js hosts read the disk; a wasm host gives a complete tree. The loop below answers from a map, so it runs on every target.

    ///|
    /// Answer one request from `disk`, a map from file path text to file text. A
    /// directory exists when a file path in `disk` is below it.
    fn answer(
    tree : @config.FileTree,
    request : @config.FileRequest,
    disk : Map[String, String],
    ) -> @config.FileTree raise @config.ConfigError {
    match request {
    ReadFile(path) =>
    match disk.get(path.to_string()) {
    Some(text) => tree.with_file(path, text)
    None => tree.with_missing(path)
    }
    ListDir(dir) => {
    let entries : Array[@config.DirEntry] = []
    for text, _ in disk {
    let path = @config.FileTreePath::parse(text)
    guard path != dir && path.is_within(dir) else { continue }
    let below = path.names()[dir.names().length():]
    let entry : @config.DirEntry = if below.length() > 1 {
    { name: below[0], kind: Directory }
    } else {
    { name: below[0], kind: File }
    }
    if !entries.contains(entry) {
    entries.push(entry)
    }
    }
    if entries.is_empty() {
    tree.with_missing(dir)
    } else {
    tree.with_listing(dir, entries)
    }
    }
    }
    }

    ///|
    /// Load until the configuration is done, answering each request from `disk`.
    fn load_from(
    builder : @config.ConfigBuilder,
    disk : Map[String, String],
    ) -> @config.Config raise @config.ConfigError {
    let mut tree = @config.FileTree::new()
    while true {
    match @config.load(builder, tree) {
    Done(config) => return config
    Need(requests) =>
    for request in requests {
    tree = answer(tree, request, disk)
    }
    }
    }
    abort("the loop ends only with a configuration")
    }

    ///|
    test "a host answers the requests of a load" {
    let builder = @config.builder()
    |> @config.working_dir("/srv")
    |> @config.format(@toml.format())
    |> @config.file("conf/app.toml")
    let config = load_from(builder, { "/srv/conf/app.toml": "name = \"acme\"" })
    @expect.expect(@config.get_text(config, "name")).to_equal("acme")
    @expect.expect(config.reports()[0].path).to_equal(Some("/srv/conf/app.toml"))
    }

    A relative path joins the working directory that @config.working_dir sets. A missing explicit file is an error, FileNotFound.

    #File-tree paths

    A FileTreePath is a parsed path of the file-tree protocol. It has a root and a list of names, and it uses / as the separator on every platform. The fields are private, so FileTreePath::parse and FileTreePath::from_native are the only ways to make one. A failure is the configuration error InvalidFilePath with the input text and a FilePathRule.

    RootExampleParent of the root
    POSIX/w/app.tomlnone
    DriveC:/w/app.tomlnone; the parent of C:/app.toml is C:/
    UNC share//server/share/app.tomlnone; //server is not a root
    Current directoryapp.toml, .none; the parent of app.toml is .

    RuleRejected text
    Empty""
    Nultext with a NUL character
    BackslashC:\w\app.toml in parse; from_native(text, Windows) converts it
    DriveRelativeC: and C:app.toml, which name the current directory of the drive
    IncompleteUnc//server and //server/
    NotAName"", ., .. or a name with / in child

    A path keeps the exact case and the order of its names. parse leaves out empty names and . names. It keeps .., because through a symbolic link the parent on the disk is not the lexical parent. The operations parent, name, child, join, resolve, root and is_within work on the parsed names; they do not print and parse the path again. A path does not tell that a file exists or that a browser mount holds it.

    ///|
    test "file-tree paths keep their root" {
    let file = @config.FileTreePath::parse("C:/w/app.toml")
    @expect.expect(file.parent().map(dir => dir.to_string())).to_equal(Some("C:/w"))
    let root = @config.FileTreePath::parse("D:/")
    @expect.expect(root.parent()).to_equal(None)
    @expect.expect(root.join("acme.toml").to_string()).to_equal("D:/acme.toml")
    let native = @config.FileTreePath::from_native("C:\\w\\app.toml", Windows)
    @expect.expect(native).to_equal(file)
    @expect.expect(file.to_native(Windows)).to_equal("C:\\w\\app.toml")
    try @config.FileTreePath::parse("C:app.toml") catch {
    InvalidFilePath(rule~, ..) => @expect.expect(rule).to_equal(DriveRelative)
    _ => fail("expected an invalid-path error")
    } noraise {
    _ => fail("C:app.toml is relative to the current directory of drive C")
    }
    }

    #Candidate files

    A candidate list names the files that may hold one source, for example app.toml and app.yaml in one directory. A candidate exists when the listing of its directory holds its exact name as a file, also on a file system that ignores case. The load never chooses between two existing candidates.

    Existing candidatesResult
    NoneThe report shows NotFound and the source adds nothing
    OneThe load reads and parses it; the report shows Loaded and its path
    More than oneAmbiguous(display~, location~, candidates~)

    ///|
    test "one existing candidate loads" {
    let builder = @config.builder()
    |> @config.format(@toml.format())
    |> @config.add(
    @config.ConfigSource::candidates(
    ["/srv/app.toml", "/srv/.config/app.toml"],
    display="App",
    ),
    )
    let config = load_from(builder, { "/srv/.config/app.toml": "mode = \"dev\"" })
    let report = config.reports()[0]
    @expect.expect((report.status, report.path)).to_equal((
    Loaded,
    Some("/srv/.config/app.toml"),
    ))
    }

    ///|
    test "two existing candidates are ambiguous" {
    let builder = @config.builder()
    |> @config.format(@toml.format())
    |> @config.add(
    @config.ConfigSource::candidates(
    ["/srv/app.toml", "/srv/.config/app.toml"],
    display="App",
    ),
    )
    let disk = { "/srv/app.toml": "", "/srv/.config/app.toml": "" }
    try load_from(builder, disk) catch {
    Ambiguous(location~, candidates~, ..) as error => {
    @expect.expect(location).to_equal("/srv")
    @expect.expect(candidates.length()).to_equal(2)
    @expect.expect(error.message()).to_start_with(
    "Ambiguous App configuration; found:",
    )
    }
    _ => fail("expected an ambiguity error")
    } noraise {
    _ => fail("expected an ambiguity error")
    }
    }

    #Formats

    A format turns file text into a value tree with a struct root. Register the formats that the application reads; the file extension selects one, compared in lowercase. parse_file gives other packages the same selection.

    PackageFormatExtensionsStatus
    parsers/tomlTOMLtomlExact integers, dates and times
    parsers/yamlYAMLyaml, ymlDefault and strict profiles
    parsers/json5JSON and JSON5json, json5Comments allowed; see package README for precision limits
    parsers/ionIonionText only; one top-level struct

    ///|
    test "the extension selects the format" {
    let formats = @config.builder()
    |> @config.format(@toml.format())
    |> @config.ConfigBuilder::formats
    @expect.expect(@config.parse_file(formats, "/srv/APP.TOML", "a = 1")).to_equal(
    @config.fields([("a", @config.int(1))]),
    )
    try @config.parse_file(formats, "/srv/app.ion", "{}") catch {
    UnsupportedFormat(extensions~, ..) =>
    @expect.expect(extensions).to_equal(["toml"])
    _ => fail("expected an unsupported-format error")
    } noraise {
    _ => fail("expected an unsupported-format error")
    }
    }

    The formats virtual package supplies all four format packages by default. An executable-package override selects TOML only or an application-defined set without changing the caller's import or registration loop. This selection happens at build time and can remove unused parsers from the executable. The guide explains MoonBit virtual packages, their pkg.mbti contract, and the difference between selecting linked code and registering formats in a builder.

    #Merge rules

    BaseOverlayResult
    anynullThe base value stays, at any depth
    structstructFields merge one by one; base fields first, new fields after
    anyany other valueThe overlay wins
    listlistThe overlay list replaces the base list

    A merged configuration never holds a null struct field: a null with no base value adds nothing. Nulls inside lists stay. The merge never changes its inputs.

    #Where each value came from

    Every leaf of the merged tree has a non-empty list of origins: the kind, the display name, the priority, and the file path of the source that set it. A list counts as one leaf. A struct has no origin of its own, and a value that replaces a struct removes the records below it. Reports name each kind as Morphir does, for example project for a workspace file and user-override for a user override.

    ///|
    test "the origin of each value" {
    let config = @config.build([
    @config.ConfigSource::defaults({ "ir": { "layout": "tree", "mode": "safe" } }),
    @config.in_memory_pairs(
    [("ir.mode", @config.string("fast"))],
    name="command line",
    ),
    ])
    let origins = config.provenance().map(record => {
    (record.0.to_string(), record.1.map(origin => origin.source))
    })
    @expect.expect(origins).to_equal([
    ("ir.layout", ["defaults"]),
    ("ir.mode", ["command line"]),
    ])
    @expect.expect(config.reports()[0].kind.report_name()).to_equal("defaults")
    }

    #Excluding a subtree

    A source can drop key paths after the parse and before the merge. A layer that a project inherits can drop the identity of another project without a warning. A member layer can drop a key that only the workspace may set, with a warning.

    ///|
    test "an excluded key warns" {
    let source = @config.in_memory_pairs([
    ("workspace.out_dir", @config.string("out")),
    ("ir.layout", @config.string("tree")),
    ])
    .exclude([@config.KeyPath::parse("workspace.out_dir")], warn=true)
    let config = @config.build([source])
    @expect.expect(config.get(@config.KeyPath::parse("workspace.out_dir"))).to_equal(
    None,
    )
    @expect.expect(config.warnings().length()).to_equal(1)
    }

    #Source providers

    A source provider gives sources during the load, at its position in the list. Discovery uses this hook. A provider gets a LoadContext with the file tree, the platform, the variable set, the working directory, and the formats. It gives Need(requests) until the tree holds what it reads, then Done with its sources and warnings. A provider reads only the context, never the process.

    Use Expansion::new(sources, warnings?, profiles?) instead of a record literal. Ordinary providers omit profiles; their loaded configuration reports Off. Provider decorators read defensive sources(), warnings() and profiles() accessors and pass the metadata through the constructor. A successful load can retain only one enabled resolution. Config::profiles() reads that stored state; it does not run discovery again.

    ///|
    test "a provider adds the file of its directory" {
    let local_file : @config.SourceProvider = {
    name: "local",
    expand: context => {
    guard context.working_dir is Some(dir) else {
    return Done(@config.Expansion::new([]))
    }
    Done(@config.Expansion::new([
    @config.ConfigSource::candidates(
    ["\{dir}/app.local.toml"],
    display="local",
    role=LocalOverride,
    ),
    ]))
    },
    }
    let builder = @config.builder()
    |> @config.working_dir("/srv")
    |> @config.format(@toml.format())
    |> @config.provider(local_file)
    let config = load_from(builder, { "/srv/app.local.toml": "debug = true" })
    @expect.expect(@config.get_text(config, "debug")).to_equal("true")
    @expect.expect(config.reports()[0].priority).to_equal(390)
    }

    #Bind typed values

    Read a section into a typed value with bind, or the full configuration with bind_root. Any type that implements FromJson works, usually through derive(FromJson). A section that no source sets decodes as null, so an Option target gives None.

    ///|
    struct ServerOptions {
    host : String
    port : Int
    } derive(FromJson, Eq, Debug)

    ///|
    test "bind a section to a record" {
    let config = @config.build([
    @config.ConfigSource::defaults({
    "server": { "host": "localhost", "port": 8080 },
    }),
    ])
    let server : ServerOptions = config |> @config.bind("server")
    @expect.expect(server).to_equal({ host: "localhost", port: 8080 })
    let metrics : ServerOptions? = config.bind("metrics")
    @expect.expect(metrics).to_equal(None)
    }

    A decode failure raises Decode. Its text names the decode path, the decoder detail, and the sources that set the bad value, as <path>: <detail> (<blame text>). A path inside a list shows the index, for example targets[1].level. The text never shows the configured value.

    BlameTextWhen
    Set(origins)set in project /repo/app.tomlA source set the leaf or the list
    Table(origins)table set in ...The key is a table, for example a missing field
    Unsetnot set by any sourceNo source sets the key

    ///|
    test "a decode error names the file that set the value" {
    let builder = @config.builder()
    |> @config.format(@toml.format())
    |> @config.file("/srv/app.toml")
    let tree = @config.FileTree::new(complete=true).with_file(
    @config.FileTreePath::parse("/srv/app.toml"),
    "server.host = \"example.org\"\nserver.port = \"eighty\"",
    )
    guard @config.load(builder, tree) is Done(config) else { fail("no config") }
    try {
    let _ : ServerOptions = config |> @config.bind("server")
    } catch {
    error =>
    @expect.expect(error.message()).to_equal(
    "server.port: Int::from_json: expected number (set in project /srv/app.toml)",
    )
    } noraise {
    _ => fail("expected a decode error")
    }
    }

    #Bind over a default value

    bind_or merges the configured section over a typed default, with the same rules as the configuration merge: tables merge, a configured list or scalar replaces the default, and an absent section keeps the default. The type needs ToJson as well as FromJson. A failure in a value that the default gave says from the bind default.

    ///|
    struct ServerDefaults {
    host : String
    port : Int
    tags : Array[String]
    } derive(FromJson, ToJson, Eq, Debug)

    ///|
    test "configured keys win over the default" {
    let config = @config.build([
    @config.in_memory_pairs([
    ("server.port", @config.int(9000)),
    ("server.tags", @config.list([@config.string("eu")])),
    ]),
    ])
    let server = config
    |> @config.bind_or("server", ({
    host: "localhost",
    port: 8080,
    tags: ["default"],
    } : ServerDefaults))
    @expect.expect(server).to_equal({ host: "localhost", port: 9000, tags: ["eu"] })
    @expect.expect(config.bind_or("mode", "safe")).to_equal("safe")
    }

    Binding decodes through FromJson, so its rules apply:

    CaseResult
    Int from 1.51: the fraction is lost without an error; bind_with rejects it
    Int64 from a numberAn error: Int64 decodes only from text such as "42"; bind_with reads the number
    An Option field that is absentNone
    An unknown fieldIgnored

    The configuration never holds a null field, so a null in a source does not reach an Option field: the field is absent and decodes as None.

    #Secret references in a bound section

    Binding never resolves a secret reference such as { env = "API_TOKEN" }. FromJson would read a reference as an ordinary table, and binding can not tell which keys a type reads. So bind, bind_root and bind_or fail with Decode at the first reference in the bound section, with the detail is a secret reference; bind it as SecretRef with bind_with. Decode the section with FromConfig and bind_with, and give the field the type SecretRef (see "Exact decoding with FromConfig"). Or bind a narrower section that holds no reference. Read the secret value with Config::secret.

    ///|
    struct ApiClient {
    url : String
    } derive(FromJson)

    ///|
    test "a secret reference stops binding" {
    let builder = @config.builder()
    |> @config.format(@toml.format())
    |> @config.file("/srv/app.toml")
    let tree = @config.FileTree::new(complete=true).with_file(
    @config.FileTreePath::parse("/srv/app.toml"),
    "api.url = \"https://example.org\"\napi.token = { env = \"API_TOKEN\" }",
    )
    guard @config.load(builder, tree) is Done(config) else { fail("no config") }
    try {
    let client : ApiClient = config |> @config.bind("api")
    ignore(client.url)
    } catch {
    error =>
    @expect.expect(error.message()).to_equal(
    "api.token: is a secret reference; bind it as SecretRef with bind_with (set in project /srv/app.toml)",
    )
    } noraise {
    _ => fail("expected a decode error")
    }
    let url : String = config |> @config.bind("api.url")
    @expect.expect(url).to_equal("https://example.org")
    }

    #Validate bound values

    A type declares its rules with the Validate trait. validate is pure: it returns every broken rule as a Violation and never raises. violation(fields,detail) names fields of the section; a rule over two fields names both, so the error shows the source of each value. nested(field, found) moves the violations of a nested value below its field. Map, Array and Option values check each value, below its key or list index. A type with no rules writes impl Validate for T and uses the default, which finds nothing.

    validated(value, config, key) checks a bound value and gives an Options[T]: the value, the key path of its section, and an optional instance name. All violations raise one Validation error, with one line for each path: <path>: <detail> (<blame text>). No options value exists after a failure. options(config, key) is the short form of bind, then validated.

    ///|
    struct IrSettings {
    layout : String
    strict_mode : Bool
    } derive(FromJson, ToJson)

    ///|
    impl @config.Validate for IrSettings with validate(self) {
    if self.layout == "split" && !self.strict_mode {
    [
    @config.violation(
    ["layout", "strict_mode"],
    "split layout needs strict_mode = true",
    ),
    ]
    } else {
    []
    }
    }

    ///|
    test "validation names each value and its source" {
    let builder = @config.builder()
    |> @config.format(@toml.format())
    |> @config.file("/srv/app.toml")
    let tree = @config.FileTree::new(complete=true).with_file(
    @config.FileTreePath::parse("/srv/app.toml"),
    "ir.layout = \"split\"",
    )
    guard @config.load(builder, tree) is Done(config) else { fail("no config") }
    let default : IrSettings = { layout: "tree", strict_mode: false }
    try {
    let ir = config
    |> @config.bind_or("ir", default)
    |> @config.validated(config, "ir")
    ignore(ir.value)
    } catch {
    error =>
    @expect.expect(error.message()).to_equal(
    (
    #|ir.layout: split layout needs strict_mode = true (set in project /srv/app.toml)
    #|ir.strict_mode: split layout needs strict_mode = true (not set by any source)
    ),
    )
    } noraise {
    _ => fail("expected a validation error")
    }
    }

    #Named options

    named(config, key, bind_one) binds each child of a section as one named instance of the same type. It calls bind_one with the configuration and the key text of each child, for example backends.ts, so bind_or, bind and validated all fit in it. Each options value gets the child name as its instance name. The Decode and Validation failures of all instances raise one Validation error. A section that no source sets gives no instances.

    ///|
    struct BackendSettings {
    out_dir : String
    level : Int
    } derive(FromJson, ToJson)

    ///|
    impl @config.Validate for BackendSettings with validate(self) {
    if self.level > 0 {
    []
    } else {
    [@config.violation(["level"], "must be positive")]
    }
    }

    ///|
    test "bind each backend as a named instance" {
    let config = @config.build([
    @config.in_memory_pairs([
    ("backends.ts.out_dir", @config.string("dist/ts")),
    ("backends.scala.level", @config.int(2)),
    ]),
    ])
    let backends : @config.NamedOptions[BackendSettings] = config
    |> @config.named("backends", (config, key) => {
    config
    |> @config.bind_or(key, ({ out_dir: "out", level: 1 } : BackendSettings))
    |> @config.validated(config, key)
    })
    @expect.expect(backends.names()).to_equal(["ts", "scala"])
    guard backends.get("ts") is Some(ts) else { fail("no ts instance") }
    @expect.expect(ts.name).to_equal(Some("ts"))
    @expect.expect(ts.key.to_string()).to_equal("backends.ts")
    @expect.expect(ts.value.out_dir).to_equal("dist/ts")
    }

    #Unknown keys

    FromJson ignores keys that a type does not read, so a misspelled key has no effect. bind_or can find these keys: with unknown=Reject, it writes the bound value back with ToJson and compares the keys. Each configured key that the written value does not hold is a failure with its sources, and all of them raise one Validation error. The check also looks in list items. The default is Allow, which ignores unknown keys.

    Reject is correct only when ToJson writes every key that FromJson accepts, under the same name. A derived FromJson and ToJson pair with the same arguments does this. A hand-written decoder can accept a key that its encoder does not write, for example an alias. For such a type, use RejectExcept(keys). It accepts the listed key paths, relative to the section, and the keys below them.

    ///|
    test "a misspelled key is an error with its source" {
    let builder = @config.builder()
    |> @config.format(@toml.format())
    |> @config.file("/srv/app.toml")
    let tree = @config.FileTree::new(complete=true).with_file(
    @config.FileTreePath::parse("/srv/app.toml"),
    "ir.layout = \"split\"\nir.strict_mod = true",
    )
    guard @config.load(builder, tree) is Done(config) else { fail("no config") }
    let default : IrSettings = { layout: "tree", strict_mode: false }
    let ir = config |> @config.bind_or("ir", default)
    @expect.expect(ir.strict_mode).to_equal(false)
    try {
    config |> @config.bind_or("ir", default, unknown=Reject) |> ignore
    } catch {
    error =>
    @expect.expect(error.message()).to_equal(
    "ir.strict_mod: is an unknown key (set in project /srv/app.toml)",
    )
    } noraise {
    _ => fail("expected a validation error")
    }
    let legacy = config
    |> @config.bind_or("ir", default, unknown=RejectExcept(["strict_mod"]))
    @expect.expect(legacy.layout).to_equal("split")
    }

    #Exact decoding with FromConfig

    FromJson reads numbers as Double, so it loses the scale of a decimal, the digits of a large integer, and the fraction of an Int. A type that implements FromConfig decodes straight from the configuration tree instead, and bind_with(config, key) binds it. Each decoder gets a ConfigNode: its decode path, its value, and the sources to blame.

    ConfigNode operationResult
    get(name)Decode the field name with FromConfig
    child(name)The node of the field name; it has no value when no source sets it
    children()The fields of a table, with their nodes
    items()The items of a list; each item has the index in its path
    json()Decode the node with FromJson, with the same error text as bind
    fail(detail)Raise Decode at the node with its sources
    path(), value(), blame()The decode path, the value, and the sources to blame

    The package supplies FromConfig for String, Bool, Int, Int64, UInt64, Double, @bigint.BigInt, @ion.Decimal, @ion.Timestamp, @lander_core.TimeLimit, SecretRef, T?, Array[T], Map[String, T] and Sourced[T]. The integer types accept only an integer value in their range, so 80.5 fails for Int. @ion.Decimal accepts a decimal or an integer. Double accepts a float, an integer or a decimal. A node with no value gives None for T? and fails with no value for the other types.

    Sourced[T] holds a value and the sources to blame for it, so the application can resolve a relative path against the file that set it. SecretRef accepts a secret reference and gives it unresolved; every other type fails at a reference.

    ///|
    struct Invoice {
    rate : @ion.Decimal
    max_items : Int64
    lock : @config.Sourced[String]
    server : ServerOptions
    }

    ///|
    impl @config.FromConfig for Invoice with from_config(node) {
    {
    rate: node.get("rate"),
    max_items: node.get("max_items"),
    lock: node.get("lock"),
    server: node.child("server").json(),
    }
    }

    ///|
    test "exact decoding keeps the decimal scale and the file" {
    let builder = @config.builder()
    |> @config.format(@ion_format.format())
    |> @config.file("/srv/app.ion")
    let tree = @config.FileTree::new(complete=true).with_file(
    @config.FileTreePath::parse("/srv/app.ion"),
    (
    #|{
    #| billing: {
    #| rate: 1.50,
    #| max_items: 9007199254740993,
    #| lock: "deps.lock",
    #| server: { host: "localhost", port: 8080 },
    #| },
    #|}
    ),
    )
    guard @config.load(builder, tree) is Done(config) else { fail("no config") }
    let billing : Invoice = config |> @config.bind_with("billing")
    @expect.expect(billing.rate.coefficient().to_string()).to_equal("150")
    @expect.expect(billing.rate.exponent()).to_equal(-2)
    @expect.expect(billing.max_items).to_equal(9007199254740993L)
    guard billing.lock.blame is Set([origin]) else { fail("one origin") }
    @expect.expect(origin.location).to_equal(Some("/srv/app.ion"))
    @expect.expect(billing.server).to_equal({ host: "localhost", port: 8080 })
    }

    #Time limits

    @lander_core.TimeLimit from moonrockz/moon-lander-core binds from text in its text form, such as "30s", "1.5s" or "1m 30s". A number fails, because it has no unit. Text that does not parse fails with a decode error that names the broken rule and the sources to blame. The bound value gives its milliseconds as an Int, which the timeouts of moonbitlang/async take:

    ///|
    test "bind a timeout from configuration text" {
    let builder = @config.builder()
    |> @config.format(@toml.format())
    |> @config.file("/srv/app.toml")
    let tree = @config.FileTree::new(complete=true).with_file(
    @config.FileTreePath::parse("/srv/app.toml"), "[http]\ntimeout = \"1m 30s\"",
    )
    guard @config.load(builder, tree) is Done(config) else { fail("no config") }
    let timeout : @lander_core.TimeLimit = config
    |> @config.bind_with("http.timeout")
    @expect.expect(timeout.to_millis()).to_equal(90000)
    }

    #Environment variables and overrides

    An environment source reads the variables that start with its prefix. __ separates names, and the names are written in lower case, so ACME_SERVER__PORT=9000 sets server.port. Values get a type: true and false, integers, and JSON lists or tables; other text stays a string. The origin of each value names its variable. An overrides source, for example from the command line, has the highest default priority, 700.

    ///|
    test "an environment variable overrides a file" {
    let builder = @config.builder()
    |> @config.env_vars({ "ACME_SERVER__PORT": "9000", "HOME": "/home/u" })
    |> @config.format(@toml.format())
    |> @config.file("/srv/acme.toml")
    |> @config.env(prefix="ACME")
    let tree = @config.FileTree::new(complete=true).with_file(
    @config.FileTreePath::parse("/srv/acme.toml"),
    "server.port = 8080\nserver.host = \"example.org\"",
    )
    guard @config.load(builder, tree) is Done(config) else { fail("no config") }
    @expect.expect(@config.get_text(config, "server.port")).to_equal("9000")
    let origin = config.origins(@config.KeyPath::parse("server.port"))[0]
    @expect.expect(origin.location).to_equal(Some("ACME_SERVER__PORT"))
    }

    RuleExample
    The prefix matches in any caseacme_ui__color=false sets ui.color
    _ after the prefix is skipped; a single _ stays in a nameACME__IR_FORMAT=3 sets ir_format
    An empty name is an errorACME_A____B raises InvalidEnvKey
    Reserved names stay outreserved=["ACME_HOME"]
    A shorter path merges lastACME_IR=x replaces ACME_IR__MODE=fast
    Discover with no variable setThe report shows NotFound

    #Hosts

    The core never reads the disk or the process. A host gives it those inputs.

    TargetHow to load
    native, js@host.load(builder) from host reads the files, and the process variables and working directory when the builder holds none. The js adapter runs on Node and Bun
    wasm, wasm-gcThe host gives a complete FileTree, a variable set, and a working directory, then calls @config.load once

    ///|
    test "a wasm host gives every input" {
    let builder = @config.builder()
    |> @config.env_vars({ "ACME_MODE": "fast" })
    |> @config.working_dir("/app")
    |> @config.format(@toml.format())
    |> @config.file("acme.toml")
    let tree = @config.FileTree::new(complete=true).with_file(
    @config.FileTreePath::parse("/app/acme.toml"),
    "mode = \"safe\"",
    )
    guard @config.load(builder, tree) is Done(config) else {
    fail("a complete tree never asks for files")
    }
    @expect.expect(@config.get_text(config, "mode")).to_equal("safe")
    }

    #Errors

    Every failure is one ConfigError case with labelled fields, so a caller matches the case and does not compare text. message() gives text for people.

    CaseWhen
    Parse, InvalidRoot, DuplicateKeyA file or value tree is not valid
    FileNotFoundAn explicit file does not exist
    UnsupportedFormatNo registered format reads the extension
    AmbiguousMore than one candidate exists
    SourceOrderA priority decreases
    InvalidKeyPath, KeyNotFound, RenderA key path or a lookup fails

    Later changes raise the other cases: environment keys, merge keys, secrets, discovery, and binding.

    FromConfig

    pub(open) trait FromConfig {
    fn from_config(ConfigNode) -> Self raise ConfigError
    }

    Exact decoding from the configuration tree. Unlike FromJson, it keeps Ion decimals, timestamps and integers of any size, and it can read the blame of each value.
    impl FromConfig for Bool
    impl FromConfig for Int
    impl FromConfig for Int64
    impl FromConfig for Option[T]
    impl FromConfig for Array[T]
    impl FromConfig for Map[String, T]

    SecretResolver

    pub(open) trait SecretResolver {
    fn resolve(Self, SecretRef, Origin) -> Secret raise SecretError
    }

    Resolves a secret reference. The origin lets a file resolver anchor a relative path to the directory of the declaring file.

    Validate

    pub(open) trait Validate {
    fn validate(Self) -> Array[Violation] = _
    }

    Validation rules that run after decoding. validate is pure: it returns every broken rule and never raises. A type with no rules uses the default, which returns no violations.
    impl Validate for Option[T]
    impl Validate for Array[T]
    impl Validate for Map[String, T]

    ConfigError

    pub(all) suberror ConfigError {
    Profile(failure~ : ProfileFailure)
    Parse(format~ : String, origin~ : String, message~ : String, line~ : Int?, column~ : Int?)
    UnsupportedYaml(origin~ : String, feature~ : String, line~ : Int, column~ : Int)
    InvalidRoot(origin~ : String, found~ : String)
    DuplicateKey(origin~ : String, key~ : KeyPath)
    FileNotFound(path~ : String)
    UnsupportedFormat(path~ : String, extensions~ : Array[String])
    Ambiguous(display~ : String, location~ : String, candidates~ : Array[String])
    InvalidKeyPath(text~ : String, reason~ : String)
    InvalidFilePath(text~ : String, rule~ : FilePathRule)
    InvalidEnvKey(variable~ : String)
    MergeKey(key~ : KeyPath, index~ : Int, source~ : String)
    InvalidSecret(key~ : KeyPath, origin~ : String, reason~ : String)
    KeyNotFound(key~ : KeyPath)
    Render(format~ : String, key~ : KeyPath, reason~ : String)
    SourceOrder(source~ : String, priority~ : Int, previous~ : String, previous_priority~ : Int)
    OutsideBoundary(start~ : String, boundary~ : String)
    InvalidEnvironmentName(name~ : String, reason~ : String)
    ConventionPolicy(input~ : String, rule~ : ConventionRule)
    UnknownProject(selector~ : String, workspace~ : String)
    AmbiguousProject(selector~ : String, matches~ : Array[String])
    Decode(failure~ : BindFailure)
    Validation(failures~ : Array[BindFailure])
    } derive(Eq,
    Debug
    )

    A configuration failure. Each case is one failure class, so a caller can match it without a string compare. The core, conventions, and binding capabilities raise these cases and have no error type of their own.

    ConfigError::equal

    fn ConfigError::equal(ConfigError, ConfigError) -> Bool

    ConfigError::message

    fn ConfigError::message(self : ConfigError) -> String

    Text for people that describes the failure. Paths show as the error holds them; the host adapter gives them in the / form.

    ConfigError::not_equal

    fn ConfigError::not_equal(x : ConfigError, y : ConfigError) -> Bool

    SecretError

    pub(all) suberror SecretError {
    MissingConfigKey(key~ : KeyPath)
    EmptyEnvironment(variable~ : String)
    FileRead(path~ : String, reason~ : String)
    InvalidSecretValue(reason~ : String)
    CommandFailed(reason~ : String)
    KeyringLookupFailed(reason~ : String)
    } derive(Eq,
    Debug
    )

    A failure while resolving a secret. Messages never hold the secret or the backend output.

    SecretError::equal

    fn SecretError::equal(SecretError, SecretError) -> Bool

    SecretError::message

    fn SecretError::message(self : SecretError) -> String

    Text for people that names the failure class and never holds a secret.

    SecretError::not_equal

    fn SecretError::not_equal(x : SecretError, y : SecretError) -> Bool

    BindFailure

    pub(all) struct BindFailure {
    at : Array[Located]
    detail : String
    } derive(Eq,
    Debug
    )

    A binding failure: the decoder or validation detail and the paths that it names. at holds one or more paths and is never empty.

    BindFailure::equal

    fn BindFailure::equal(BindFailure, BindFailure) -> Bool

    BindFailure::not_equal

    fn BindFailure::not_equal(x : BindFailure, y : BindFailure) -> Bool

    Blame

    pub(all) enum Blame {
    Set(Array[Origin])
    Table(Array[Origin])
    Default
    Unset
    } derive(Eq,
    Debug
    )

    The sources to blame for a value that failed to decode.

    Blame::equal

    fn Blame::equal(Blame, Blame) -> Bool

    Blame::not_equal

    fn Blame::not_equal(x : Blame, y : Blame) -> Bool

    Blame::to_repr

    Config

    pub struct Config {
    // private fields
    }

    A merged configuration with the reports of its sources. Its root is always a struct.

    Config::bind

    fn[T :
    FromJson
    ] Config::bind(self : Config, key : String) -> T raise ConfigError

    Decode the section at key into a T, as bind does.

    Config::bind_or

    fn[T :
    FromJson
    + ToJson] Config::bind_or(self : Config, key : String, default : T, unknown? : UnknownKeys) -> T raise ConfigError

    Decode the section at key merged over default, as bind_or does.

    Config::get

    fn Config::get(self : Config, key : KeyPath) -> ConfigValue?

    The value at key, or None when no value is there.

    Config::origins

    fn Config::origins(self : Config, key : KeyPath) -> Array[Origin]

    The origins of the leaf at key: one origin for a replaced leaf or list. A struct has no origin of its own, so a struct or a missing key gives an empty array.

    Config::profiles

    fn Config::profiles(self : Config) -> ProfileState

    The profile state recorded by the successful load.

    Config::provenance

    fn Config::provenance(self : Config) -> Array[(KeyPath, Array[Origin])]

    The origins of every leaf, in tree order. Each array is not empty.

    Config::reports

    fn Config::reports(self : Config) -> Array[SourceReport]

    The source reports in merge order.

    Config::secret

    fn[R : SecretResolver] Config::secret(self : Config, key : String, resolver : R) -> Secret raise SecretError

    Resolve the secret at key. A plain string is a literal secret and does not call the resolver. A secret reference calls resolver with the leaf origin.

    Config::value

    fn Config::value(self : Config) -> ConfigValue

    The merged value tree.

    Config::warnings

    fn Config::warnings(self : Config) -> Array[ConfigWarning]

    The warnings of the load: the warnings of the source providers, then the warnings of each source report in merge order.

    ConfigBuilder

    pub struct ConfigBuilder {
    // private fields
    }

    An immutable list of sources and source providers in merge order, the registered formats, and the explicit inputs of one run: the variable set, the working directory, and the platform. Each function returns a new builder.

    ConfigBuilder::formats

    fn ConfigBuilder::formats(self : ConfigBuilder) -> Array[Format]

    The registered formats in registration order.

    ConfigBuilder::given_env_vars

    fn ConfigBuilder::given_env_vars(self : ConfigBuilder) -> Map[String, String]?

    The variable set that the builder holds, or None when nobody gave one. A host reads the process variables only when this is None.

    ConfigBuilder::given_file_paths

    fn ConfigBuilder::given_file_paths(self : ConfigBuilder) -> Array[String]

    The path texts of the file, candidate and parsed-file sources that the builder holds, in add order, as the caller gave them. The load parses each text one time; an adapter with stricter path rules, such as the browser adapter, checks the texts here before the load. Providers are not included.

    ConfigBuilder::given_platform

    fn ConfigBuilder::given_platform(self : ConfigBuilder) -> Platform?

    The platform that the builder holds, or None when nobody gave one.

    ConfigBuilder::given_working_dir

    fn ConfigBuilder::given_working_dir(self : ConfigBuilder) -> String?

    The working directory that the builder holds, or None when nobody gave one.

    ConfigNode

    pub struct ConfigNode {
    // private fields
    }

    One value of the configuration tree for exact decoding: its decode path, the value or its absence, and the sources to blame. A node in a list has a decode path with an index suffix and the blame of the list.

    ConfigNode::blame

    fn ConfigNode::blame(self : ConfigNode) -> Blame

    The sources to blame for the value of the node.

    ConfigNode::child

    fn ConfigNode::child(self : ConfigNode, name : String) -> ConfigNode

    The node of the field name. The node has no value when this node is not a table or has no such field.

    ConfigNode::children

    fn ConfigNode::children(self : ConfigNode) -> Array[(String, ConfigNode)]

    The fields of a table, with their nodes, in configuration order. A node that is not a table has no fields.

    ConfigNode::fail

    fn[T] ConfigNode::fail(self : ConfigNode, detail : String) -> T raise ConfigError

    Raise Decode at the node with detail and the blame of the node. Use it for a rule that a FromConfig implementation checks while it decodes.

    ConfigNode::get

    fn[T : FromConfig] ConfigNode::get(self : ConfigNode, name : String) -> T raise ConfigError

    Decode the field name exactly.

    ConfigNode::items

    fn ConfigNode::items(self : ConfigNode) -> Array[ConfigNode]

    The items of a list, in order. Each item node has the index in its decode path and the blame of the list. A node that is not a list has no items.

    ConfigNode::json

    Decode the node with FromJson, with the same error translation as bind: a failure names the full decode path from the bound section. A node with no value decodes as null. A secret reference in the node raises Decode.

    ConfigNode::path

    fn ConfigNode::path(self : ConfigNode) -> DecodePath

    The decode path of the node.

    ConfigNode::value

    fn ConfigNode::value(self : ConfigNode) -> ConfigValue?

    The value of the node, or None when no source sets it.

    ConfigSource

    pub struct ConfigSource {
    // private fields
    }

    An immutable configuration source: a kind, a display name, an optional display priority, the list merge policy, key-path policies, the input, and the key paths to exclude.

    ConfigSource::candidates

    fn ConfigSource::candidates(paths : Array[String], display~ : String, role? : FileRole, name? : String, priority? : Int) -> ConfigSource

    A file source over an explicit list of candidate files at one location. No existing candidate gives the status NotFound, one loads it, and more than one raises Ambiguous with display. A candidate exists when the listing of its directory holds its exact name as a file. The name is display when no name is given.

    ConfigSource::defaults

    fn ConfigSource::defaults(json : Json, name? : String, priority? : Int) -> ConfigSource raise ConfigError

    A defaults source from a JSON value, for example Defaults::default().to_json(). The root must be an object.

    ConfigSource::defaults_value

    fn ConfigSource::defaults_value(value : ConfigValue, name? : String, priority? : Int) -> ConfigSource raise ConfigError

    A defaults source from a value tree. The root must be a struct.

    ConfigSource::env

    fn ConfigSource::env(prefix~ : String, reserved? : Array[String], selection? : SourceSelection[Map[String, String]], name? : String, priority? : Int) -> ConfigSource

    An environment variable source. A variable counts when its name, in upper case, starts with prefix and _ and is not in reserved (compared in upper case). After the prefix, leading _ characters go, __ separates names, and each name is written in lower case: ACME_SERVER__PORT sets server.port. An empty name raises InvalidEnvKey.

    Discover reads the variable set of the builder, and reports NotFound when the builder holds none. Explicit(vars) reads only vars. Skip loads nothing. Each value gets a type: true or false in any case is a Boolean, an optional sign and digits that fit Int64 are an integer, JSON text that starts with [ or { is a list or a struct, and other text is a string. The origin of each value names its variable.

    ConfigSource::exclude

    fn ConfigSource::exclude(self : ConfigSource, paths : Array[KeyPath], warn~ : Bool) -> ConfigSource

    A new source that removes the subtrees at paths after the parse and before the merge. When warn is true, each removed key that the source held adds a KeyRemoved warning to its report.

    ConfigSource::file

    fn ConfigSource::file(path : String, role? : FileRole, name? : String, priority? : Int) -> ConfigSource

    A file source at an explicit path. A relative path joins the working directory of the builder. The extension selects the registered format. The load raises FileNotFound when the file does not exist. The name is the path when no name is given.

    ConfigSource::kind

    The kind of the source.

    ConfigSource::name

    fn ConfigSource::name(self : ConfigSource) -> String

    The display name of the source.

    ConfigSource::overrides

    fn ConfigSource::overrides(pairs : Array[(String, ConfigValue)], name? : String, priority? : Int) -> ConfigSource raise ConfigError

    An overrides source from key-path pairs, for example command-line values. It has the kind Overrides and the default priority 700.

    ConfigSource::overrides_value

    fn ConfigSource::overrides_value(value : ConfigValue, name? : String, priority? : Int) -> ConfigSource raise ConfigError

    An overrides source from a value tree. The root must be a struct.

    ConfigSource::parsed_file

    fn ConfigSource::parsed_file(path : String, value : ConfigValue, role? : FileRole, name? : String, priority? : Int) -> ConfigSource raise ConfigError

    A file source whose text the caller already read and turned into a value tree, for example a legacy file with its own format. The report and the origins show path, which joins the working directory when it is relative. The root must be a struct.

    ConfigSource::skipped

    fn ConfigSource::skipped(kind : SourceKind, name? : String, priority? : Int) -> ConfigSource

    A source that loads nothing and reports the status Skipped. The name is the report name of kind when no name is given.

    ConfigSource::with_key_policy

    fn ConfigSource::with_key_policy(self : ConfigSource, key : KeyPath, policy : MergePolicy) -> ConfigSource

    A new source with the list merge policy policy at key. A key-path policy beats the source policy.

    ConfigSource::with_policy

    fn ConfigSource::with_policy(self : ConfigSource, policy : MergePolicy) -> ConfigSource

    A new source with the list merge policy policy for every list that has no key-path policy.

    ConfigValue

    pub struct ConfigValue {
    // private fields
    } derive(
    Debug
    )

    An immutable configuration value over an Ion value. Every format and source gives values of this one type before the merge.
    impl Eq for ConfigValue

    ConfigValue::equal

    fn ConfigValue::equal(self : ConfigValue, other : ConfigValue) -> Bool

    ConfigValue::from_ion

    fn ConfigValue::from_ion(ion :
    Ion
    , origin? : String) -> ConfigValue raise ConfigError

    A source tree from an Ion value. The root must be a struct, else this raises InvalidRoot with origin. A struct field name with no text, such as an unresolved symbol ID, raises Parse for the Ion format. Two fields with the same name in one struct raise DuplicateKey. The key path of a field in a list item holds the struct field names only, because a key path does not select list items.

    ConfigValue::from_json

    fn ConfigValue::from_json(json : Json, origin? : String) -> ConfigValue raise ConfigError

    A source tree from a JSON value, through its Ion form. The rules of ConfigValue::from_ion apply.

    ConfigValue::not_equal

    fn ConfigValue::not_equal(x : ConfigValue, y : ConfigValue) -> Bool

    ConfigValue::to_ion

    The Ion value.

    ConfigValue::type_name

    fn ConfigValue::type_name(self : ConfigValue) -> String

    The Ion type name in lowercase, for example struct or list.

    ConfigWarning

    pub(all) enum ConfigWarning {
    KeyRemoved(KeyPath, Origin)
    Note(source~ : String, message~ : String)
    } derive(Eq,
    Debug
    )

    A warning of a load.

    ConfigWarning::equal

    ConfigWarning::not_equal

    fn ConfigWarning::not_equal(x : ConfigWarning, y : ConfigWarning) -> Bool

    ConventionRule

    pub(all) enum ConventionRule {
    EmptyExtension
    ExtensionCharacters
    EmptyFormatGroup
    DuplicateExtension
    EmptyFormatOrder
    UnregisteredExtension
    SupplementalName
    SupplementalExtension
    SupplementalOutsideGroup
    SupplementalPrimaryCollision
    } derive(Eq,
    Debug
    )

    The construction and registration rules of an ordered format policy.

    ConventionRule::equal

    ConventionRule::not_equal

    fn ConventionRule::not_equal(x : ConventionRule, y : ConventionRule) -> Bool

    DecodePath

    pub(all) struct DecodePath {
    key : KeyPath?
    suffix : Array[DecodeStep]
    } derive(Eq,
    Debug
    )

    The path of a value that failed to decode. key holds the leading names up to the first list index, and None is the root of the bound tree. suffix is empty or starts with an Index step.
    impl Show for DecodePath

    DecodePath::equal

    fn DecodePath::equal(DecodePath, DecodePath) -> Bool

    DecodePath::not_equal

    fn DecodePath::not_equal(x : DecodePath, y : DecodePath) -> Bool

    DecodePath::output

    fn DecodePath::output(self : DecodePath, logger : &Logger) -> Unit

    DecodePath::to_string

    fn DecodePath::to_string(self : DecodePath) -> String

    DecodeStep

    pub(all) enum DecodeStep {
    Field(String)
    Index(Int)
    } derive(Eq,
    Debug
    )

    One step of a decode path after the key path: a list index or a field name.

    DecodeStep::equal

    fn DecodeStep::equal(DecodeStep, DecodeStep) -> Bool

    DecodeStep::not_equal

    fn DecodeStep::not_equal(x : DecodeStep, y : DecodeStep) -> Bool

    DirEntry

    pub(all) struct DirEntry {
    name : String
    kind : EntryKind
    } derive(Eq,
    Debug
    )

    One entry of a directory listing, with its real name.

    DirEntry::equal

    fn DirEntry::equal(DirEntry, DirEntry) -> Bool

    DirEntry::not_equal

    fn DirEntry::not_equal(x : DirEntry, y : DirEntry) -> Bool

    DirEntry::to_repr

    EntryKind

    pub(all) enum EntryKind {
    File
    Directory
    Other
    Inaccessible
    } derive(Eq,
    Debug
    )

    The kind of a directory entry.

    EntryKind::equal

    fn EntryKind::equal(EntryKind, EntryKind) -> Bool

    EntryKind::not_equal

    fn EntryKind::not_equal(x : EntryKind, y : EntryKind) -> Bool

    EnvResolver

    pub struct EnvResolver {
    // private fields
    }

    A resolver over an explicit variable map. It resolves only Env references.

    EnvResolver::resolve

    fn EnvResolver::resolve(self : EnvResolver, reference : SecretRef, _origin : Origin) -> Secret raise SecretError

    Expansion

    pub struct Expansion {
    // private fields
    }

    The sources that a provider gives, in merge order, and its warnings.

    Expansion::new

    fn Expansion::new(sources : Array[ConfigSource], warnings? : Array[ConfigWarning], profiles? : ProfileResolution) -> Expansion

    Provider output. Omit profiles for an ordinary source provider.

    Expansion::profiles

    fn Expansion::profiles(self : Expansion) -> ProfileResolution?

    The immutable optional resolution, for a provider decorator.

    Expansion::sources

    fn Expansion::sources(self : Expansion) -> Array[ConfigSource]

    A copy of this provider's sources, for a provider decorator.

    Expansion::warnings

    fn Expansion::warnings(self : Expansion) -> Array[ConfigWarning]

    A copy of this provider's warnings, for a provider decorator.

    FilePathRule

    pub(all) enum FilePathRule {
    Empty
    Nul
    Backslash
    DriveRelative
    IncompleteUnc
    NotAName
    } derive(Eq,
    Debug
    )

    The rule that a file-tree path text breaks.

    FilePathRule::describe

    fn FilePathRule::describe(self : FilePathRule) -> String

    The rule in words, for an error message.

    FilePathRule::equal

    FilePathRule::not_equal

    fn FilePathRule::not_equal(x : FilePathRule, y : FilePathRule) -> Bool

    FileRequest

    pub(all) enum FileRequest {
    ListDir(FileTreePath)
    ReadFile(FileTreePath)
    } derive(Eq, Hash,
    Debug
    )

    A request that the pure load passes out to a host: the listing of a directory or the text of a file.

    FileRequest::equal

    fn FileRequest::equal(FileRequest, FileRequest) -> Bool

    FileRequest::hash

    fn FileRequest::hash(self : FileRequest) -> Int

    FileRequest::hash_combine

    fn FileRequest::hash_combine(FileRequest, Hasher) -> Unit

    FileRequest::not_equal

    fn FileRequest::not_equal(x : FileRequest, y : FileRequest) -> Bool

    FileRole

    pub(all) enum FileRole {
    Explicit
    System
    Global
    Workspace
    Member
    EnvironmentFile
    ProfileFile(ProfileName)
    LocalOverride
    UserOverride
    Legacy
    } derive(Eq,
    Debug
    )

    The role of a file source. Reports and error text show a role by its report name, for example user-override.

    FileRole::equal

    fn FileRole::equal(FileRole, FileRole) -> Bool

    FileRole::not_equal

    fn FileRole::not_equal(x : FileRole, y : FileRole) -> Bool

    FileRole::to_repr

    FileTree

    pub struct FileTree {
    // private fields
    }

    An immutable snapshot of the files that a load can read: directory listings, file text, and absence records, keyed by path with / separators. In a complete tree, a path that the tree does not hold is absent. The pure load asks only for paths that the tree does not know.

    FileTree::file

    fn FileTree::file(self : FileTree, path : FileTreePath) -> Lookup[String]

    The text of the file path.

    FileTree::file_request

    fn FileTree::file_request(self : FileTree, path : FileTreePath) -> FileRequest

    The request that answers is_file(path) when it gives Unknown: the listing of the directory of path, or the file itself when the tree holds that listing and the entry is Inaccessible.

    FileTree::is_complete

    fn FileTree::is_complete(self : FileTree) -> Bool

    Whether the tree is complete.

    FileTree::is_file

    fn FileTree::is_file(self : FileTree, path : FileTreePath) -> Lookup[Bool]

    Whether path exists as a file: the listing of its directory holds its exact name as a file. The compare is case-sensitive on every platform. A missing directory gives Found(false).

    An Inaccessible entry with the exact name is a file when the tree holds its text. When the tree does not hold its text, the answer is Unknown, and file_request asks the host for the file, so the host reports its error. In a complete tree the entry is a file, so a read of it raises file-not-found and the entry never disappears without a report.

    FileTree::listing

    fn FileTree::listing(self : FileTree, dir : FileTreePath) -> Lookup[Array[DirEntry]]

    The listing of the directory dir.

    FileTree::new

    fn FileTree::new(complete? : Bool) -> FileTree

    An empty tree. With complete=true, every path is absent until a with_* call adds it, so a load over it never asks for files.

    FileTree::with_file

    fn FileTree::with_file(self : FileTree, path : FileTreePath, text : String) -> FileTree

    A new tree that also holds the text of the file path.

    FileTree::with_listing

    fn FileTree::with_listing(self : FileTree, dir : FileTreePath, entries : Array[DirEntry]) -> FileTree

    A new tree that also holds the listing of the directory dir.

    FileTree::with_missing

    fn FileTree::with_missing(self : FileTree, path : FileTreePath) -> FileTree

    A new tree that records path as absent. When path is a directory, every path under it is also absent.

    FileTreePath

    pub struct FileTreePath {
    // private fields
    } derive(Eq, Hash)

    A path in the file-tree protocol: a root and a list of names, with / as the separator on every platform. The root is /, a drive root such as C:/, a UNC share root such as //server/share, or . for a relative path. Make a value with parse or from_native.

    A path keeps the exact case and the order of its names. Empty names and . names are left out, so /w//a/./b/ is /w/a/b. A .. name stays, because the parent of a symbolic link on the disk is not the lexical parent. A path does not tell that a file exists, can be read, or is in a browser mount.

    FileTreePath::child

    fn FileTreePath::child(self : FileTreePath, name : String) -> FileTreePath raise ConfigError

    The path of the entry name in the directory self. The name must not be empty, . or .., and must not hold /, \ or a NUL character.

    FileTreePath::common_dir

    fn FileTreePath::common_dir(paths : Array[FileTreePath]) -> FileTreePath?

    The longest directory that holds every path in paths: the common root and names of their directories. None when paths is empty or the paths have different roots.

    FileTreePath::compare

    fn FileTreePath::compare(self : FileTreePath, other : FileTreePath) -> Int

    FileTreePath::equal

    FileTreePath::from_absolute

    The file-tree path of the host path path. This is a checked conversion: AbsolutePath uses / for drive and UNC paths, but it allows \ and a NUL character in a POSIX path, which a file-tree path does not.

    FileTreePath::from_native

    fn FileTreePath::from_native(text : String, platform : Platform) -> FileTreePath raise ConfigError

    Parse a path in the native form of platform. On Windows \ becomes / first, so C:\w\app.toml gives C:/w/app.toml and \\server\share\a gives //server/share/a. A drive-relative path such as C:w stays an error. On other platforms this is parse.

    FileTreePath::hash

    fn FileTreePath::hash(self : FileTreePath) -> Int

    FileTreePath::hash_combine

    fn FileTreePath::hash_combine(FileTreePath, Hasher) -> Unit

    FileTreePath::is_absolute

    fn FileTreePath::is_absolute(self : FileTreePath) -> Bool

    Whether the path starts at /, a drive root, or a UNC share root, and not at ..

    FileTreePath::is_root

    fn FileTreePath::is_root(self : FileTreePath) -> Bool

    Whether the path is a root: /, a drive root, a UNC share root, or ..

    FileTreePath::is_within

    fn FileTreePath::is_within(self : FileTreePath, ancestor : FileTreePath) -> Bool

    Whether self is ancestor or below it, name by name: /w/config2 is not below /w/config.

    FileTreePath::join

    fn FileTreePath::join(self : FileTreePath, relative : String) -> FileTreePath raise ConfigError

    Parse the portable text relative and resolve it below self. An absolute text gives its own path.

    FileTreePath::name

    fn FileTreePath::name(self : FileTreePath) -> String?

    The last name of the path, or None for a root.

    FileTreePath::names

    fn FileTreePath::names(self : FileTreePath) -> Array[String]

    The names of the path below its root, in order.

    FileTreePath::not_equal

    fn FileTreePath::not_equal(x : FileTreePath, y : FileTreePath) -> Bool

    FileTreePath::op_ge

    fn FileTreePath::op_ge(x : FileTreePath, y : FileTreePath) -> Bool

    FileTreePath::op_gt

    fn FileTreePath::op_gt(x : FileTreePath, y : FileTreePath) -> Bool

    FileTreePath::op_le

    fn FileTreePath::op_le(x : FileTreePath, y : FileTreePath) -> Bool

    FileTreePath::op_lt

    fn FileTreePath::op_lt(x : FileTreePath, y : FileTreePath) -> Bool

    FileTreePath::output

    fn FileTreePath::output(self : FileTreePath, logger : &Logger) -> Unit

    FileTreePath::parent

    fn FileTreePath::parent(self : FileTreePath) -> FileTreePath?

    The directory that holds the path, or None for a root. The parent of C:/acme.toml is C:/, and the parent of acme.toml is ..

    FileTreePath::parse

    fn FileTreePath::parse(text : String) -> FileTreePath raise ConfigError

    Parse the portable text form of a path. The text must not be empty, and it must not hold a NUL character or \. A drive letter and : must have / after them, and // must start a server and a share name.

    FileTreePath::resolve

    fn FileTreePath::resolve(self : FileTreePath, other : FileTreePath) -> FileTreePath

    other when it is absolute, else other below self. No .. name is applied.

    FileTreePath::root

    The root of the path: /, a drive root, a UNC share root, or ..

    FileTreePath::to_native

    fn FileTreePath::to_native(self : FileTreePath, platform : Platform) -> String

    The text of the path in the native form of platform, for a file-system call. Only Windows paths change: / becomes \.

    FileTreePath::to_repr

    FileTreePath::to_string

    fn FileTreePath::to_string(self : FileTreePath) -> String

    Format

    pub(all) struct Format {
    name : String
    extensions : Array[String]
    parse : (String, String) -> ConfigValue raise ConfigError
    }

    A configuration file format. parse takes the text and the origin, for example a file path, and gives a value tree with a struct root.

    KeyPath

    pub struct KeyPath {
    // private fields
    } derive(Eq, Hash,
    Debug
    )

    A path to a value in a configuration tree: a non-empty list of non-empty field names. Name matches are exact and case-sensitive. A key path does not select list items.
    impl Show for KeyPath

    KeyPath::equal

    fn KeyPath::equal(KeyPath, KeyPath) -> Bool

    KeyPath::from_names

    fn KeyPath::from_names(names : Array[String]) -> KeyPath raise ConfigError

    A key path from field names, from the root down. The list and each name must not be empty.

    KeyPath::hash

    fn KeyPath::hash(self : KeyPath) -> Int

    KeyPath::hash_combine

    fn KeyPath::hash_combine(KeyPath, Hasher) -> Unit

    KeyPath::names

    fn KeyPath::names(self : KeyPath) -> Array[String]

    The field names, from the root down.

    KeyPath::not_equal

    fn KeyPath::not_equal(x : KeyPath, y : KeyPath) -> Bool

    KeyPath::output

    fn KeyPath::output(self : KeyPath, logger : &Logger) -> Unit

    KeyPath::parse

    fn KeyPath::parse(text : String) -> KeyPath raise ConfigError

    Parse the dotted text form, for example ir.strict_mode. A name that contains . or " uses double quotes, for example servers."eu.west".port. In a quoted name, \" gives " and \\ gives \.

    KeyPath::to_repr

    KeyPath::to_string

    fn KeyPath::to_string(self : KeyPath) -> String

    LoadContext

    pub(all) struct LoadContext {
    tree : FileTree
    platform : Platform?
    env_vars : Map[String, String]
    working_dir : FileTreePath?
    formats : Array[Format]
    }

    The inputs that a source provider gets during a load. platform is None when no host gave it, and env_vars is empty when the builder holds no variable set. working_dir is the parsed working directory of the builder.

    LoadStep

    pub(all) enum LoadStep[T] {
    Need(Array[FileRequest])
    Done(T)
    } derive(Eq,
    Debug
    )

    One step of a pure load: the file requests that it still needs, or the result.

    LoadStep::equal

    fn[T : Eq] LoadStep::equal(LoadStep[T], LoadStep[T]) -> Bool

    LoadStep::not_equal

    fn[T : Eq] LoadStep::not_equal(x : LoadStep[T], y : LoadStep[T]) -> Bool

    Located

    pub(all) struct Located {
    path : DecodePath
    blame : Blame
    } derive(Eq,
    Debug
    )

    A decode path and the sources to blame for it.

    Located::equal

    fn Located::equal(Located, Located) -> Bool

    Located::not_equal

    fn Located::not_equal(x : Located, y : Located) -> Bool

    Located::to_repr

    Lookup

    pub(all) enum Lookup[T] {
    Found(T)
    Missing
    Unknown
    } derive(Eq,
    Debug
    )

    What a file tree knows about a path: it holds the value, it records the path as absent, or it does not know.

    Lookup::equal

    fn[T : Eq] Lookup::equal(Lookup[T], Lookup[T]) -> Bool

    Lookup::not_equal

    fn[T : Eq] Lookup::not_equal(x : Lookup[T], y : Lookup[T]) -> Bool

    MergePolicy

    pub(all) enum MergePolicy {
    Replace
    Append
    MergeByKey(String)
    } derive(Eq,
    Debug
    )

    How two lists merge at one key path. The default is Replace. A key-path policy beats the source policy.
    impl Show for MergePolicy

    MergePolicy::equal

    fn MergePolicy::equal(MergePolicy, MergePolicy) -> Bool

    MergePolicy::not_equal

    fn MergePolicy::not_equal(x : MergePolicy, y : MergePolicy) -> Bool

    MergePolicy::output

    fn MergePolicy::output(self : MergePolicy, logger : &Logger) -> Unit

    MergePolicy::to_string

    fn MergePolicy::to_string(self : MergePolicy) -> String

    NamedOptions

    pub struct NamedOptions[T] {
    // private fields
    }

    The options values of the children of one section, one for each instance name, in configuration order.

    NamedOptions::get

    fn[T] NamedOptions::get(self : NamedOptions[T], name : String) -> Options[T]?

    The options value of the instance name, or None when the section has no child with that name.

    NamedOptions::names

    fn[T] NamedOptions::names(self : NamedOptions[T]) -> Array[String]

    The instance names, in configuration order.

    Options

    pub struct Options[T] {
    value : T
    key : KeyPath
    name : String?
    }

    A bound and validated value, the key path of its section, and the instance name of a named option. An options value exists only after decoding and validation succeed.

    Origin

    pub(all) struct Origin {
    kind : SourceKind
    source : String
    priority : Int
    location : String?
    } derive(Eq,
    Debug
    )

    The source that set a value: its kind, display name, display priority, and location. The location is a file path in the / form or an environment variable name.

    Origin::equal

    fn Origin::equal(Origin, Origin) -> Bool

    Origin::not_equal

    fn Origin::not_equal(x : Origin, y : Origin) -> Bool

    Origin::to_repr

    Platform

    pub(all) enum Platform {
    Linux
    MacOs
    Windows
    } derive(Eq,
    Debug
    )

    The operating system of a run.

    Platform::equal

    fn Platform::equal(Platform, Platform) -> Bool

    Platform::not_equal

    fn Platform::not_equal(x : Platform, y : Platform) -> Bool

    Platform::to_repr

    ProfileFailure

    pub struct ProfileFailure {
    // private fields
    } derive(Eq,
    Debug
    )

    A profile failure with its raw input, typed rule and input origin.

    ProfileFailure::cycle

    The closed cycle path, or an empty collection for another failure.

    ProfileFailure::equal

    ProfileFailure::input

    fn ProfileFailure::input(self : ProfileFailure) -> String

    The raw name or control input that failed.

    ProfileFailure::message

    fn ProfileFailure::message(self : ProfileFailure) -> String

    A human-readable failure. Programs can inspect the typed rule instead.

    ProfileFailure::new

    fn ProfileFailure::new(input : String, rule : ProfileRule, origin? : ProfileOrigin, cycle? : ProfileNames) -> ProfileFailure

    Keep the evidence for a profile failure. A cycle holds its closed path.

    ProfileFailure::not_equal

    fn ProfileFailure::not_equal(x : ProfileFailure, y : ProfileFailure) -> Bool

    ProfileFailure::origin

    The boundary or control that supplied the input.

    ProfileFailure::rule

    The rule that the input breaks.

    ProfileName

    pub struct ProfileName {
    // private fields
    } derive(Eq, Hash,
    Debug
    )

    A parsed lowercase ASCII profile name, from 1 to 64 characters. The first character is a letter; later characters may also be digits, _, or -.
    impl Show for ProfileName

    ProfileName::equal

    fn ProfileName::equal(ProfileName, ProfileName) -> Bool

    ProfileName::hash

    fn ProfileName::hash(self : ProfileName) -> Int

    ProfileName::hash_combine

    fn ProfileName::hash_combine(ProfileName, Hasher) -> Unit

    ProfileName::not_equal

    fn ProfileName::not_equal(x : ProfileName, y : ProfileName) -> Bool

    ProfileName::output

    fn ProfileName::output(self : ProfileName, logger : &Logger) -> Unit

    ProfileName::parse

    fn ProfileName::parse(text : String, origin? : ProfileOrigin) -> ProfileName raise ConfigError

    Parse one profile name at its input boundary.

    ProfileName::to_string

    fn ProfileName::to_string(self : ProfileName) -> String

    ProfileNames

    pub struct ProfileNames {
    // private fields
    } derive(Eq,
    Debug
    )

    An immutable ordered collection of parsed names. Repeats are retained.

    ProfileNames::contains

    fn ProfileNames::contains(self : ProfileNames, name : ProfileName) -> Bool

    Whether the collection contains the parsed name.

    ProfileNames::equal

    ProfileNames::names

    A copy of the names in their original order.

    ProfileNames::new

    Copy the supplied names into an immutable collection.

    ProfileNames::not_equal

    fn ProfileNames::not_equal(x : ProfileNames, y : ProfileNames) -> Bool

    ProfileNames::parse_csv

    fn ProfileNames::parse_csv(text : String, origin? : ProfileOrigin) -> ProfileNames raise ConfigError

    Parse a comma-separated selector, trimming ASCII whitespace around names. Empty names, including an empty whole selector, are an error.

    ProfileOrigin

    pub(all) enum ProfileOrigin {
    Policy
    Discovery
    Variable(String)
    Control(Origin, KeyPath)
    } derive(Eq,
    Debug
    )

    The input boundary or literal control that supplied profile information.

    ProfileOrigin::equal

    ProfileOrigin::not_equal

    fn ProfileOrigin::not_equal(x : ProfileOrigin, y : ProfileOrigin) -> Bool

    ProfileReason

    pub(all) enum ProfileReason {
    Root(ProfileName, ProfileOrigin)
    Include(ProfileName, ProfileOrigin)
    GroupMember(ProfileName, ProfileName, ProfileOrigin)
    } derive(Eq,
    Debug
    )

    Why a name appears in the final active order.

    ProfileReason::equal

    ProfileReason::not_equal

    fn ProfileReason::not_equal(x : ProfileReason, y : ProfileReason) -> Bool

    ProfileResolution

    pub struct ProfileResolution {
    // private fields
    } derive(Eq,
    Debug
    )

    Immutable activation metadata retained only by a successful complete load.

    ProfileResolution::active

    The final active names in merge order.

    ProfileResolution::contains

    fn ProfileResolution::contains(self : ProfileResolution, name : ProfileName) -> Bool

    Whether the parsed name is active.

    ProfileResolution::controls

    A defensive copy of the interpreted control origins.

    ProfileResolution::disabled

    fn ProfileResolution::disabled(self : ProfileResolution) -> Bool

    Whether an explicit input disabled processing.

    ProfileResolution::equal

    ProfileResolution::new

    fn ProfileResolution::new(selector : ProfileSelector, roots : ProfileNames, active : ProfileNames, disabled? : Bool, reasons? : Array[ProfileReason], controls? : Array[ProfileOrigin], authorities? : Array[Origin], failure_origin? : ProfileOrigin) -> ProfileResolution raise ConfigError

    Record a resolved ordered set. The active set has at most 128 distinct names. Repeated active names keep their last occurrence. failure_origin identifies the expansion input that first exceeds the active-name limit. authorities names every eligible base source, including a base whose controls a later base fully replaces. Existing callers derive that list from controls. Winning control origins stay separate from eligibility.

    ProfileResolution::not_equal

    fn ProfileResolution::not_equal(x : ProfileResolution, y : ProfileResolution) -> Bool

    ProfileResolution::permits_controls

    fn ProfileResolution::permits_controls(self : ProfileResolution, origin : Origin) -> Bool

    Whether this source is an eligible base control source. Authority is separate from winning control origins, so fully overridden bases remain eligible. Legacy constructors derive authority from their control origins.

    ProfileResolution::reasons

    A defensive copy of the activation reasons.

    ProfileResolution::roots

    The selected active or default roots, before group expansion.

    ProfileResolution::selector

    The winning selection source.

    ProfileResolution::with_control_authority

    fn ProfileResolution::with_control_authority(self : ProfileResolution, origin : Origin) -> ProfileResolution

    Authorize an additional ordinary source identity for an already resolved base. Providers use this when moving a base's values without changing its control origins. This returns a new resolution and preserves disabled mode.

    ProfileRule

    pub(all) enum ProfileRule {
    NameLength(Int)
    NameStart
    NameCharacter
    EmptyElement
    ExpectedList
    ExpectedString
    ExpectedTable
    UnknownField
    NonLiteral
    ForbiddenLocation
    Cycle
    GroupLimit(Int)
    MemberLimit(Int)
    DepthLimit(Int)
    ActiveLimit(Int)
    CompetingProviders(String, String)
    } derive(Eq,
    Debug
    )

    A typed profile rule. Count failures retain the observed count.

    ProfileRule::equal

    fn ProfileRule::equal(ProfileRule, ProfileRule) -> Bool

    ProfileRule::not_equal

    fn ProfileRule::not_equal(x : ProfileRule, y : ProfileRule) -> Bool

    ProfileSelection

    pub(all) enum ProfileSelection {
    Unspecified
    Selected(ProfileNames)
    Disabled
    } derive(Eq,
    Debug
    )

    An activation input. An explicit empty selection keeps includes; Disabled suppresses all profile processing.

    ProfileSelection::equal

    ProfileSelection::from_variable

    fn ProfileSelection::from_variable(text : String, variable~ : String) -> ProfileSelection raise ConfigError

    Interpret a present selector variable. An empty or ASCII-whitespace-only value disables profile processing; absence is handled as Unspecified.

    ProfileSelection::not_equal

    fn ProfileSelection::not_equal(x : ProfileSelection, y : ProfileSelection) -> Bool

    ProfileSelector

    pub(all) enum ProfileSelector {
    Discovery
    Variable(String)
    BaseActive(Origin)
    BaseDefault(Origin)
    Fallback
    } derive(Eq,
    Debug
    )

    The winning activation selector. File selectors retain their file origin.

    ProfileSelector::equal

    ProfileSelector::not_equal

    fn ProfileSelector::not_equal(x : ProfileSelector, y : ProfileSelector) -> Bool

    ProfileState

    pub(all) enum ProfileState {
    Off
    Resolved(ProfileResolution)
    } derive(Eq,
    Debug
    )

    Off is different from an enabled policy whose active result is empty.

    ProfileState::contains

    fn ProfileState::contains(self : ProfileState, name : ProfileName) -> Bool

    Whether the parsed name is active in this loaded state.

    ProfileState::equal

    ProfileState::not_equal

    fn ProfileState::not_equal(x : ProfileState, y : ProfileState) -> Bool

    Secret

    pub struct Secret {
    // private fields
    }

    An opaque resolved secret. Show writes <redacted>.
    impl Show for Secret

    Secret::expose

    fn Secret::expose(self : Secret) -> String

    The secret text. Callers that display a value must not use this.

    Secret::output

    fn Secret::output(_self : Secret, logger : &Logger) -> Unit

    Secret::to_string

    fn Secret::to_string(self : Secret) -> String

    SecretRef

    pub(all) enum SecretRef {
    Env(String)
    File(String)
    Command(Array[String])
    Keyring(service~ : String, account~ : String)
    } derive(Eq,
    Debug
    )

    A secret reference leaf: one of the Morphir shapes.

    SecretRef::equal

    fn SecretRef::equal(SecretRef, SecretRef) -> Bool

    SecretRef::from_config

    fn SecretRef::from_config(node : ConfigNode) -> SecretRef raise ConfigError

    SecretRef::not_equal

    fn SecretRef::not_equal(x : SecretRef, y : SecretRef) -> Bool

    SecretSyntax

    pub(all) enum SecretSyntax {
    Standard
    None
    } derive(Eq,
    Debug
    )

    Whether the load and the merge treat one-field env / file / command / keyring structs as secret references.

    SecretSyntax::equal

    SecretSyntax::none

    Turn secret-reference recognition off.

    SecretSyntax::not_equal

    fn SecretSyntax::not_equal(x : SecretSyntax, y : SecretSyntax) -> Bool

    SecretSyntax::standard

    fn SecretSyntax::standard() -> SecretSyntax

    The Morphir secret-reference shapes. Recognition is on when the syntax is Standard.

    ShowFormat

    pub(all) enum ShowFormat {
    Toml
    Json
    Ion
    } derive(Eq,
    Debug
    )

    The text form of a show helper.

    ShowFormat::equal

    fn ShowFormat::equal(ShowFormat, ShowFormat) -> Bool

    ShowFormat::not_equal

    fn ShowFormat::not_equal(x : ShowFormat, y : ShowFormat) -> Bool

    SourceKind

    pub(all) enum SourceKind {
    Defaults
    InMemory
    File(FileRole)
    Environment
    Overrides
    } derive(Eq,
    Debug
    )

    The kind of a configuration source.

    SourceKind::equal

    fn SourceKind::equal(SourceKind, SourceKind) -> Bool

    SourceKind::not_equal

    fn SourceKind::not_equal(x : SourceKind, y : SourceKind) -> Bool

    SourceKind::report_name

    fn SourceKind::report_name(self : SourceKind) -> String

    The report name of a source kind, as Morphir names its layers. A file source shows the name of its role: project for workspace, explicit, and legacy files.

    SourceProvider

    pub(all) struct SourceProvider {
    name : String
    expand : (LoadContext) -> LoadStep[Expansion] raise ConfigError
    }

    A step of the builder that gives sources during the load. expand gives the file requests that it still needs, or its sources. With a complete tree it must give its sources.

    SourceReport

    pub(all) struct SourceReport {
    kind : SourceKind
    name : String
    priority : Int
    path : String?
    candidates : Array[String]
    status : SourceStatus
    warnings : Array[ConfigWarning]
    } derive(Eq,
    Debug
    )

    The report of one source after a load. path is the file that the source read, and candidates are the files that a candidate list looked for.

    SourceReport::equal

    SourceReport::not_equal

    fn SourceReport::not_equal(x : SourceReport, y : SourceReport) -> Bool

    SourceSelection

    pub(all) enum SourceSelection[T] {
    Discover
    Explicit(T)
    Skip
    } derive(Eq,
    Debug
    )

    The selection of a source location or input. Discover uses the values that a host or a source provider supplies, Explicit uses only the given value, and Skip loads nothing.

    SourceSelection::equal

    fn[T : Eq] SourceSelection::equal(SourceSelection[T], SourceSelection[T]) -> Bool

    SourceSelection::not_equal

    fn[T : Eq] SourceSelection::not_equal(x : SourceSelection[T], y : SourceSelection[T]) -> Bool

    SourceStatus

    pub(all) enum SourceStatus {
    Loaded
    NotFound
    Skipped
    } derive(Eq,
    Debug
    )

    What a load did with a source.

    SourceStatus::equal

    SourceStatus::not_equal

    fn SourceStatus::not_equal(x : SourceStatus, y : SourceStatus) -> Bool

    Sourced

    pub struct Sourced[T] {
    value : T
    blame : Blame
    } derive(Eq,
    Debug
    )

    A decoded value with the sources to blame for it, for example to resolve a relative path against the file that set it.
    impl FromConfig for Sourced[T]

    Sourced::equal

    fn[T : Eq] Sourced::equal(Sourced[T], Sourced[T]) -> Bool

    Sourced::from_config

    fn[T : FromConfig] Sourced::from_config(node : ConfigNode) -> Sourced[T] raise ConfigError

    Sourced::not_equal

    fn[T : Eq] Sourced::not_equal(x : Sourced[T], y : Sourced[T]) -> Bool

    UnknownKeys

    pub(all) enum UnknownKeys {
    Allow
    Reject
    RejectExcept(Array[String])
    } derive(Eq,
    Debug
    )

    What bind_or does with a configured key that the bound value does not hold. FromJson ignores unknown keys, so a misspelled key has no effect unless the check is on.

    UnknownKeys::equal

    fn UnknownKeys::equal(UnknownKeys, UnknownKeys) -> Bool

    UnknownKeys::not_equal

    fn UnknownKeys::not_equal(x : UnknownKeys, y : UnknownKeys) -> Bool

    Violation

    pub(all) struct Violation {
    at : Array[Array[DecodeStep]]
    detail : String
    } derive(Eq,
    Debug
    )

    A broken validation rule: the paths of the values it names, relative to the bound section, and a detail text. An empty at names the section.

    Violation::equal

    fn Violation::equal(Violation, Violation) -> Bool

    Violation::not_equal

    fn Violation::not_equal(x : Violation, y : Violation) -> Bool

    add

    fn add(builder : ConfigBuilder, source : ConfigSource) -> ConfigBuilder

    Add source after the sources that the builder holds.

    bind

    fn[T :
    FromJson
    ] bind(config : Config, key : String) -> T raise ConfigError

    Decode the section at key into a T. key is the text form of a key path, for example ir or servers."eu.west". A section that no source sets decodes as null, so an Option target gives None. A decode failure raises Decode with the decode path, the decoder detail, and the sources to blame. A secret reference in the section raises Decode, and binding never resolves it. Binding does not change the configuration.

    bind_or

    fn[T :
    FromJson
    + ToJson] bind_or(config : Config, key : String, default : T, unknown? : UnknownKeys) -> T raise ConfigError

    Decode the section at key merged over default. The default is written with ToJson, the configured section merges over it with the core merge rules, and the result decodes into a T: tables merge, a configured list or scalar replaces the default, and an absent section keeps the default. A None field of the default is not written, so a configured value fills it. A failure in a value that the default gave has the blame Default. A secret reference in the configured section raises Decode before the merge. The default does not change.

    unknown turns on the unknown-key check: with Reject, each configured key that the ToJson form of the result does not hold is a failure with its sources, and all of them raise one Validation error. The check is correct only when ToJson writes every key that FromJson accepts, under the same name; RejectExcept lists the other keys that the type accepts.

    bind_root

    fn[T :
    FromJson
    ] bind_root(config : Config) -> T raise ConfigError

    Decode the full configuration into a T. Decode paths start at the root.

    bind_with

    fn[T : FromConfig] bind_with(config : Config, key : String) -> T raise ConfigError

    Decode the section at key exactly into a T. A section that no source sets gives a node with no value, so an Option target gives None. A failure raises Decode with the decode path and the sources to blame. Binding never resolves a secret reference: only SecretRef accepts one.

    bool

    fn bool(value : Bool) -> ConfigValue

    A Boolean value.

    build

    fn build(sources : Array[ConfigSource]) -> Config raise ConfigError

    Load the sources in list order over a complete empty file tree: a later source wins. A file source raises FileNotFound, so this suits value sources.

    builder

    fn builder() -> ConfigBuilder

    A builder with no sources, no formats, and no run inputs.

    default_redact_fragments

    fn default_redact_fragments() -> Array[String]

    The Morphir key-name fragments that mark a sensitive key.

    defaults

    fn defaults(builder : ConfigBuilder, json : Json) -> ConfigBuilder raise ConfigError

    Add a defaults source from a JSON value, for example Defaults::default().to_json().

    env

    fn env(builder : ConfigBuilder, prefix~ : String, reserved? : Array[String], selection? : SourceSelection[Map[String, String]]) -> ConfigBuilder

    Add an environment variable source, as ConfigSource::env gives it.

    env_resolver

    fn env_resolver(vars : Map[String, String]) -> EnvResolver

    A resolver over vars. File, command, and keyring references raise InvalidSecretValue.

    env_vars

    fn env_vars(builder : ConfigBuilder, vars : Map[String, String]) -> ConfigBuilder

    Set the variable set of the run. Source providers read variables only from this set.

    explain

    fn explain(config : Config, key : String) -> Array[(KeyPath, Array[Origin])] raise ConfigError

    The origins of key: one entry for a leaf, or each descendant leaf for a struct. A missing key raises KeyNotFound.

    fields

    fn fields(fields : Array[(String, ConfigValue)]) -> ConfigValue

    A struct value with the fields in the given order. When a name occurs more than once, the last value wins at the position of the first.

    file

    fn file(builder : ConfigBuilder, path : String, role? : FileRole, name? : String, priority? : Int) -> ConfigBuilder

    Add a file source at an explicit path, as ConfigSource::file gives it.

    float

    fn float(value : Double) -> ConfigValue

    A floating-point value.

    format

    fn format(builder : ConfigBuilder, format : Format) -> ConfigBuilder

    Register format after the formats that the builder holds.

    get_json

    fn get_json(config : Config, key : String) -> Json raise ConfigError

    The value at the key path key as a JSON object with the fields key and value, after redaction. A missing key raises KeyNotFound.

    get_text

    fn get_text(config : Config, key : String) -> String raise ConfigError

    The value at the key path key as text, after redaction: a string gives its text, and any other value gives compact JSON. A missing key raises KeyNotFound.

    in_memory

    fn in_memory(value : ConfigValue, name? : String, priority? : Int) -> ConfigSource raise ConfigError

    An in-memory source from a value tree. The root must be a struct.

    in_memory_pairs

    fn in_memory_pairs(pairs : Array[(String, ConfigValue)], name? : String, priority? : Int) -> ConfigSource raise ConfigError

    An in-memory source from flat key-path pairs, for example ("ir.layout", @config.string("tree")). Pairs apply in list order with the merge rules, so a later pair wins. Pair values keep their type.

    int

    fn int(value : Int64) -> ConfigValue

    An integer value.

    list

    fn list(items : Array[ConfigValue]) -> ConfigValue

    A list value.

    load

    fn load(builder : ConfigBuilder, tree : FileTree) -> LoadStep[Config] raise ConfigError

    Load the configuration from the builder and the file tree. This is pure: it reads only its inputs and gives the same result on every target.

    It runs each provider at its position, then checks the priorities over the expanded list and raises SourceOrder when a priority decreases. When a provider or a file source needs a path that the tree does not know, it gives all such requests in one Need. Otherwise it parses, excludes, and merges the sources in list order and gives Done.

    merge

    fn merge(base : ConfigValue, overlay : ConfigValue, policy? : MergePolicy, key_policies? : Array[(KeyPath, MergePolicy)], source? : String, secret_syntax? : SecretSyntax) -> ConfigValue raise ConfigError

    Merge overlay over base. A null overlay keeps the base value. A secret reference replaces and is not merged with a struct. Two structs merge field by field, with base fields first and new overlay fields after. Two lists use policy, or a matching entry of key_policies when one exists for the path. In all other cases the overlay value wins. The inputs do not change, and unchanged subtrees are shared.

    named

    fn[T] named(config : Config, key : String, bind_one : (Config, String) -> Options[T] raise ConfigError) -> NamedOptions[T] raise ConfigError

    Bind each child of the section at key as one named instance. bind_one gets the configuration and the key text of one child, for example backends.ts, and gives its options value; bind, bind_or and bind_with with validated all fit. Each options value gets the child name as its instance name. The Decode and Validation failures of all instances raise one Validation error. A section that no source sets gives no instances. A section that is not a table raises Decode.

    nested

    fn nested(field : String, found : Array[Violation]) -> Array[Violation]

    The violations of the value in field, with each path moved below field. A violation of the whole nested value names field.

    options

    fn[T :
    FromJson
    + Validate] options(config : Config, key : String) -> Options[T] raise ConfigError

    Bind the section at key and validate it: bind, then validated.

    overrides

    fn overrides(builder : ConfigBuilder, pairs : Array[(String, ConfigValue)]) -> ConfigBuilder raise ConfigError

    Add an overrides source from key-path pairs.

    parse_file

    fn parse_file(formats : Array[Format], path : String, text : String) -> ConfigValue raise ConfigError

    Parse the text of the file path with the format that its extension selects from formats. The extension compares in lowercase. No match raises UnsupportedFormat with the registered extensions.

    path_json

    fn path_json(reports : Array[SourceReport]) -> Json

    Source reports as JSON, in ascending priority (merge order).

    path_text

    fn path_text(reports : Array[SourceReport]) -> String

    Source reports as text: highest priority first.

    platform

    fn platform(builder : ConfigBuilder, p : Platform) -> ConfigBuilder

    Set the platform of the run.

    provider

    fn provider(builder : ConfigBuilder, p : SourceProvider) -> ConfigBuilder

    Add a source provider. During the load, its sources take its place in merge order.

    redact

    fn redact(value : ConfigValue, fragments? : Array[String]) -> ConfigValue

    A copy of value with each sensitive subtree replaced by the text <redacted>. A key is sensitive when its name, in lowercase with - changed to _, holds a fragment. The default fragment list is Morphir's.

    secret_syntax

    fn secret_syntax(builder : ConfigBuilder, syntax : SecretSyntax) -> ConfigBuilder

    Set whether one-field env / file / command / keyring structs are secret references. The default is SecretSyntax::standard().

    show

    fn show(config : Config, format : ShowFormat) -> String raise ConfigError

    Render the redacted configuration as TOML, JSON, or Ion text.

    string

    fn string(text : String) -> ConfigValue

    A string value.

    validated

    fn[T : Validate] validated(value : T, config : Config, key : String, name? : String) -> Options[T] raise ConfigError

    Validate value, the section at key of config, and give it as an options value. Each violation becomes a failure with its full decode path and the sources to blame, and all of them raise one Validation. The value comes first, so validated ends a binding pipeline: config |> bind_or("ir", default) |> validated(config, "ir").

    violation

    fn violation(fields : Array[String], detail : String) -> Violation

    A violation that names one field of the section for each name in fields, or the section itself when fields is empty. A rule over two fields names both, so the error shows the source of each value.

    working_dir

    fn working_dir(builder : ConfigBuilder, dir : String) -> ConfigBuilder

    Set the working directory of the run. A relative explicit file path joins it.