///|
/// 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.
pub fn[T : FromJson] bind(config : Config, key : String) -> T raise ConfigError {
let path = KeyPath::parse(key)
match config.get(path) {
Some(value) => {
let at = section_path(Some(path))
config.check_no_secret(at, value)
decode(config, at, section_json(config, at, value))
}
None => decode(config, section_path(Some(path)), Json::null())
}
}
///|
/// Decode the section at `key` into a `T`, as `bind` does.
pub fn[T : FromJson] Config::bind(self : Config, key : String) -> T raise ConfigError {
bind(self, key)
}
///|
/// Decode the full configuration into a `T`. Decode paths start at the root.
pub fn[T : FromJson] bind_root(config : Config) -> T raise ConfigError {
let at = section_path(None)
config.check_no_secret(at, config.root)
decode(config, at, section_json(config, at, config.root))
}
///|
/// The detail of a decode error at a secret reference.
let secret_detail = "is a secret reference; bind it as SecretRef with bind_with"
///|
/// Raise `Decode` at the first secret reference in `value`, the value at the
/// decode path `at`. `FromJson` reads a reference as an ordinary table, and binding can
/// not tell which keys a type reads, so every reference in the section fails.
/// The check never resolves the reference.
fn Config::check_no_secret(
self : Config,
at : DecodePath,
value : ConfigValue,
) -> Unit raise ConfigError {
guard self.secret_syntax is Standard else { return }
guard secret_steps(value, self.secret_syntax) is Some(steps) else { return }
let path = at.below(steps)
raise Decode(failure={
at: [{ path, blame: self.blame(path) }],
detail: secret_detail,
})
}
///|
/// The steps from `value` to its first secret reference, in field and list
/// order, or `None` when it holds none.
fn secret_steps(
value : ConfigValue,
syntax : SecretSyntax,
) -> Array[DecodeStep]? {
if is_secret_reference(value, syntax) {
return Some([])
}
match value.ion.kind() {
Struct(fields) =>
for field in fields {
guard field.name().text() is Some(name) else { continue }
if secret_steps({ ion: field.value() }, syntax) is Some(steps) {
return Some([Field(name), ..steps])
}
}
List(items) | Sexp(items) =>
for index, item in items {
if secret_steps({ ion: item }, syntax) is Some(steps) {
return Some([Index(index), ..steps])
}
}
_ => ()
}
None
}
///|
/// The decode path of the section at `key`, or of the root for `None`.
fn section_path(key : KeyPath?) -> DecodePath {
{ key, suffix: [] }
}
///|
/// The decode path of `steps` below the section at `key`.
fn decode_path_below(key : KeyPath?, steps : Array[DecodeStep]) -> DecodePath {
section_path(key).below(steps)
}
///|
/// The decode path of `steps` below this path. Before the first list index,
/// field steps join the key path; the steps from the first list index on go
/// to the suffix.
fn DecodePath::below(self : DecodePath, steps : Array[DecodeStep]) -> DecodePath {
let names = match self.key {
Some(key) => key.names.copy()
None => []
}
let suffix = self.suffix.copy()
for step in steps {
match step {
Field(name) if suffix.is_empty() => names.push(name)
_ => suffix.push(step)
}
}
{ key: if names.is_empty() { None } else { Some({ names, }) }, suffix }
}
///|
/// The JSON form of the value at the decode path `at`. A value with no JSON
/// form fails as a decode error at `at`.
fn section_json(
config : Config,
at : DecodePath,
value : ConfigValue,
) -> Json raise ConfigError {
@ion_json.to_json(value.ion) catch {
error =>
raise Decode(failure={
at: [{ path: at, blame: config.blame(at) }],
detail: "the value has no JSON form: \{error.message()}",
})
}
}
///|
/// Decode `json`, the value at the decode path `at`, with `FromJson`. A
/// failure raises `Decode` at the full decode path below `at`.
fn[T : FromJson] decode(
config : Config,
at : DecodePath,
json : Json,
blame? : (DecodePath) -> Blame,
) -> T raise ConfigError {
let blame = blame.unwrap_or(path => config.blame(path))
@json.from_json(json) catch {
JsonDecodeError((pointer, detail)) => {
let path = decode_path(at, pointer.to_string(), json)
raise Decode(failure={ at: [{ path, blame: blame(path) }], detail })
}
}
}
///|
/// 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.
pub fn[T : FromJson + ToJson] bind_or(
config : Config,
key : String,
default : T,
unknown? : UnknownKeys = Allow,
) -> T raise ConfigError {
let path = KeyPath::parse(key)
// Synthetic roots keep the rule that a tree root is a struct, so a scalar or
// list default works too.
let default_root = fields([("value", { ion: @ion.Ion::Ion(ToJson::to_json(default)) })])
let section = config.get(path)
let merged = match section {
Some(section) => {
config.check_no_secret(section_path(Some(path)), section)
merge(default_root, fields([("value", section)]))
}
None => default_root
}
let value = merged.field("value").unwrap_or({ ion: @ion.Ion::Ion(Json::null()) })
let json = section_json(config, section_path(Some(path)), value)
let result : T = decode(config, section_path(Some(path)), json, blame=decode_path => {
match config.blame(decode_path) {
Unset | Table([]) => {
let below = match decode_path.key {
Some(failing) => failing.names[path.names.length():].to_owned()
None => []
}
if default_root.at(["value", ..below]) is Some(_) {
Default
} else {
Unset
}
}
blame => blame
}
})
if section is Some(section) && !(unknown is Allow) {
config.check_unknown_keys(path, section, ToJson::to_json(result), unknown)
}
result
}
///|
/// Decode the section at `key` merged over `default`, as `bind_or` does.
pub fn[T : FromJson + ToJson] Config::bind_or(
self : Config,
key : String,
default : T,
unknown? : UnknownKeys = Allow,
) -> T raise ConfigError {
bind_or(self, key, default, unknown~)
}
///|
/// The decode path of a JSON Pointer (RFC 6901) below the decode path `at`.
/// A segment is a list index when the JSON value there is an array. The key
/// path takes the names up to the first list index; the suffix takes the
/// steps from that index on.
fn decode_path(at : DecodePath, pointer : String, json : Json) -> DecodePath {
let steps : Array[DecodeStep] = []
let mut node : Json? = Some(json)
for segment in pointer_segments(pointer) {
let step : DecodeStep = match (node, list_index(segment)) {
(Some(Array(items)), Some(index)) => {
node = items.get(index)
Index(index)
}
(Some(Object(fields)), _) => {
node = fields.get(segment)
Field(segment)
}
_ => {
node = None
Field(segment)
}
}
steps.push(step)
}
at.below(steps)
}
///|
/// The unescaped segments of a JSON Pointer: `~1` is `/` and `~0` is `~`.
fn pointer_segments(pointer : String) -> Array[String] {
guard pointer.has_prefix("/") else { return [] }
pointer[1:]
.split("/")
.map(segment => {
segment
.to_owned()
.replace_all(old="~1", new="/")
.replace_all(old="~0", new="~")
})
.collect()
}
///|
/// The list index that `segment` names, when it is a decimal number.
fn list_index(segment : String) -> Int? {
guard !segment.is_empty() && segment.length() <= 9 else { return None }
let mut index = 0
for char in segment {
guard char >= '0' && char <= '9' else { return None }
index = index * 10 + (char.to_int() - '0'.to_int())
}
Some(index)
}
///|
/// The sources to blame for a decode path. Provenance has no list index, so
/// the lookup uses the key part. A leaf or a list gives `Set` with its origins.
/// A table gives `Table` with the distinct origins of its leaves, in merge
/// order. A key that no source sets gives `Unset`.
fn Config::blame(self : Config, path : DecodePath) -> Blame {
guard path.key is Some(key) else { return Table(self.table_origins([])) }
let origins = self.origins(key)
if !origins.is_empty() {
return Set(origins)
}
match self.get(key) {
Some(value) if value.ion.kind() is Struct(_) =>
Table(self.table_origins(key.names))
_ => Unset
}
}
///|
/// The distinct origins of the leaves below `names`, in merge order.
fn Config::table_origins(self : Config, names : Array[String]) -> Array[Origin] {
let prefix : KeyPath = { names, }
let origins : Array[Origin] = []
for leaf in self.provenance() {
let (path, leaf_origins) = leaf
guard is_prefix(prefix, path) else { continue }
for origin in leaf_origins {
if !origins.contains(origin) {
origins.push(origin)
}
}
}
// A report with no path can stand for origins with locations, for example
// the variables of an environment source.
let rank = (origin : Origin) => {
self.reports
.search_by(report => {
report.kind == origin.kind &&
report.name == origin.source &&
report.priority == origin.priority &&
(report.path is None || report.path == origin.location)
})
.unwrap_or(self.reports.length())
}
// Order by report position; ties keep their first-seen order.
let ranked = origins.mapi((index, origin) => (rank(origin), index, origin))
ranked.sort_by((a, b) => (a.0, a.1).compare((b.0, b.1)))
ranked.map(entry => entry.2)
}