Layered, typed configuration for the Moon-Lander framework
[!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.
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]| Step | What happens |
|---|---|
| Describe | Add sources and formats to a ConfigBuilder, in merge order |
| Load | @config.load(builder, tree) gives Need(requests) or Done(config) |
| Answer | The host adds each answer to the tree and loads again |
| Merge | Each source is parsed, filtered, and merged over the sources before it |
| Read | Config::get, get_text, Config::reports, and Config::warnings |
///|
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")
}| Constructor | Kind | Input |
|---|---|---|
| ConfigSource::defaults(json), @config.defaults(builder, json) | Defaults | A JSON object, for example Defaults::default().to_json() |
| ConfigSource::defaults_value(value) | Defaults | A value tree |
| in_memory(value), in_memory_pairs(pairs) | InMemory | A 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~) | Environment | Variables that start with PREFIX_ |
| ConfigSource::overrides(pairs), @config.overrides(builder, pairs) | Overrides | Key-path pairs, for example from the command line |
| ConfigSource::skipped(kind) | any | Nothing; 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")
}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///|
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")
}
}| FileTree call | Effect |
|---|---|
| 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 |
///|
/// 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"))
}| Root | Example | Parent of the root |
|---|---|---|
| POSIX | /w/app.toml | none |
| Drive | C:/w/app.toml | none; the parent of C:/app.toml is C:/ |
| UNC share | //server/share/app.toml | none; //server is not a root |
| Current directory | app.toml, . | none; the parent of app.toml is . |
| Rule | Rejected text |
|---|---|
| Empty | "" |
| Nul | text with a NUL character |
| Backslash | C:\w\app.toml in parse; from_native(text, Windows) converts it |
| DriveRelative | C: and C:app.toml, which name the current directory of the drive |
| IncompleteUnc | //server and //server/ |
| NotAName | "", ., .. or a name with / in child |
///|
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")
}
}| Existing candidates | Result |
|---|---|
| None | The report shows NotFound and the source adds nothing |
| One | The load reads and parses it; the report shows Loaded and its path |
| More than one | Ambiguous(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")
}
}| Package | Format | Extensions | Status |
|---|---|---|---|
| parsers/toml | TOML | toml | Exact integers, dates and times |
| parsers/yaml | YAML | yaml, yml | Default and strict profiles |
| parsers/json5 | JSON and JSON5 | json, json5 | Comments allowed; see package README for precision limits |
| parsers/ion | Ion | ion | Text 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")
}
}| Base | Overlay | Result |
|---|---|---|
| any | null | The base value stays, at any depth |
| struct | struct | Fields merge one by one; base fields first, new fields after |
| any | any other value | The overlay wins |
| list | list | The overlay list replaces the base list |
///|
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")
}///|
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)
}///|
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)
}///|
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)
}| Blame | Text | When |
|---|---|---|
| Set(origins) | set in project /repo/app.toml | A source set the leaf or the list |
| Table(origins) | table set in ... | The key is a table, for example a missing field |
| Unset | not set by any source | No 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")
}
}///|
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")
}| Case | Result |
|---|---|
| Int from 1.5 | 1: the fraction is lost without an error; bind_with rejects it |
| Int64 from a number | An error: Int64 decodes only from text such as "42"; bind_with reads the number |
| An Option field that is absent | None |
| An unknown field | Ignored |
///|
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")
}///|
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")
}
}///|
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")
}///|
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")
}| ConfigNode operation | Result |
|---|---|
| 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 |
///|
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 })
}///|
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)
}///|
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"))
}| Rule | Example |
|---|---|
| The prefix matches in any case | acme_ui__color=false sets ui.color |
| _ after the prefix is skipped; a single _ stays in a name | ACME__IR_FORMAT=3 sets ir_format |
| An empty name is an error | ACME_A____B raises InvalidEnvKey |
| Reserved names stay out | reserved=["ACME_HOME"] |
| A shorter path merges last | ACME_IR=x replaces ACME_IR__MODE=fast |
| Discover with no variable set | The report shows NotFound |
| Target | How 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-gc | The 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")
}| Case | When |
|---|---|
| Parse, InvalidRoot, DuplicateKey | A file or value tree is not valid |
| FileNotFound | An explicit file does not exist |
| UnsupportedFormat | No registered format reads the extension |
| Ambiguous | More than one candidate exists |
| SourceOrder | A priority decreases |
| InvalidKeyPath, KeyNotFound, Render | A key path or a lookup fails |
impl FromConfig for Boolimpl FromConfig for Intimpl FromConfig for Int64impl FromConfig for UInt64impl FromConfig for Doubleimpl FromConfig for Stringimpl FromConfig for Option[T]impl FromConfig for BigIntimpl FromConfig for Array[T]impl FromConfig for Map[String, T]impl FromConfig for Decimalimpl FromConfig for Timestampimpl FromConfig for TimeLimitpub(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)pub struct Config {
// private fields
}fn[T : FromJson + ToJson] Config::bind_or(self : Config, key : String, default : T, unknown? : UnknownKeys) -> T raise ConfigErrorfn[R : SecretResolver] Config::secret(self : Config, key : String, resolver : R) -> Secret raise SecretErrorpub struct ConfigBuilder {
// private fields
}pub struct ConfigNode {
// private fields
}pub struct ConfigSource {
// private fields
}fn ConfigSource::candidates(paths : Array[String], display~ : String, role? : FileRole, name? : String, priority? : Int) -> ConfigSourcefn ConfigSource::defaults(json : Json, name? : String, priority? : Int) -> ConfigSource raise ConfigErrorfn ConfigSource::defaults_value(value : ConfigValue, name? : String, priority? : Int) -> ConfigSource raise ConfigErrorfn ConfigSource::env(prefix~ : String, reserved? : Array[String], selection? : SourceSelection[Map[String, String]], name? : String, priority? : Int) -> ConfigSourcefn ConfigSource::file(path : String, role? : FileRole, name? : String, priority? : Int) -> ConfigSourcefn ConfigSource::overrides(pairs : Array[(String, ConfigValue)], name? : String, priority? : Int) -> ConfigSource raise ConfigErrorfn ConfigSource::overrides_value(value : ConfigValue, name? : String, priority? : Int) -> ConfigSource raise ConfigErrorfn ConfigSource::parsed_file(path : String, value : ConfigValue, role? : FileRole, name? : String, priority? : Int) -> ConfigSource raise ConfigErrorfn ConfigSource::with_key_policy(self : ConfigSource, key : KeyPath, policy : MergePolicy) -> ConfigSourceimpl Eq for ConfigValueimpl Show for DecodePathpub struct EnvResolver {
// private fields
}impl SecretResolver for EnvResolverfn EnvResolver::resolve(self : EnvResolver, reference : SecretRef, _origin : Origin) -> Secret raise SecretErrorpub struct Expansion {
// private fields
}fn Expansion::new(sources : Array[ConfigSource], warnings? : Array[ConfigWarning], profiles? : ProfileResolution) -> Expansionpub(all) enum FileRole {
Explicit
System
Global
Workspace
Member
EnvironmentFile
ProfileFile(ProfileName)
LocalOverride
UserOverride
Legacy
} derive(Eq, Debug)pub struct FileTree {
// private fields
}impl Compare for FileTreePathimpl Show for FileTreePathimpl Debug for FileTreePathpub(all) struct Format {
name : String
extensions : Array[String]
parse : (String, String) -> ConfigValue raise ConfigError
}impl Show for MergePolicypub struct NamedOptions[T] {
// private fields
}pub(all) struct Origin {
kind : SourceKind
source : String
priority : Int
location : String?
} derive(Eq, Debug)fn ProfileFailure::new(input : String, rule : ProfileRule, origin? : ProfileOrigin, cycle? : ProfileNames) -> ProfileFailureimpl Show for ProfileNamefn ProfileNames::parse_csv(text : String, origin? : ProfileOrigin) -> ProfileNames raise ConfigErrorpub(all) enum ProfileReason {
Root(ProfileName, ProfileOrigin)
Include(ProfileName, ProfileOrigin)
GroupMember(ProfileName, ProfileName, ProfileOrigin)
} derive(Eq, Debug)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 ConfigErrorfn ProfileResolution::with_control_authority(self : ProfileResolution, origin : Origin) -> ProfileResolutionfn ProfileSelection::from_variable(text : String, variable~ : String) -> ProfileSelection raise ConfigErrorpub struct Secret {
// private fields
}impl FromConfig for SecretRefpub(all) struct SourceProvider {
name : String
expand : (LoadContext) -> LoadStep[Expansion] raise ConfigError
}pub(all) struct SourceReport {
kind : SourceKind
name : String
priority : Int
path : String?
candidates : Array[String]
status : SourceStatus
warnings : Array[ConfigWarning]
} derive(Eq, Debug)impl FromConfig for Sourced[T]fn[T : FromJson + ToJson] bind_or(config : Config, key : String, default : T, unknown? : UnknownKeys) -> T raise ConfigErrorfn env(builder : ConfigBuilder, prefix~ : String, reserved? : Array[String], selection? : SourceSelection[Map[String, String]]) -> ConfigBuilderfn file(builder : ConfigBuilder, path : String, role? : FileRole, name? : String, priority? : Int) -> ConfigBuilderfn in_memory(value : ConfigValue, name? : String, priority? : Int) -> ConfigSource raise ConfigErrorfn in_memory_pairs(pairs : Array[(String, ConfigValue)], name? : String, priority? : Int) -> ConfigSource raise ConfigErrorfn merge(base : ConfigValue, overlay : ConfigValue, policy? : MergePolicy, key_policies? : Array[(KeyPath, MergePolicy)], source? : String, secret_syntax? : SecretSyntax) -> ConfigValue raise ConfigErrorfn[T] named(config : Config, key : String, bind_one : (Config, String) -> Options[T] raise ConfigError) -> NamedOptions[T] raise ConfigErrorfn overrides(builder : ConfigBuilder, pairs : Array[(String, ConfigValue)]) -> ConfigBuilder raise ConfigErrorfn parse_file(formats : Array[Format], path : String, text : String) -> ConfigValue raise ConfigErrorfn[T : Validate] validated(value : T, config : Config, key : String, name? : String) -> Options[T] raise ConfigError