///|
/// The text form of a show helper.
pub(all) enum ShowFormat {
Toml
Json
Ion
} derive(Eq, Debug)
///|
pub extend ShowFormat with Eq::{equal, not_equal}
///|
pub extend ShowFormat with @debug.Debug::{to_repr}
///|
/// 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`.
pub fn get_text(config : Config, key : String) -> String raise ConfigError {
let path = KeyPath::parse(key)
let value = redacted_at(config, path)
match value.ion.kind() {
String(text) => text
_ => {
let json = @ion_json.to_json(value.ion) catch {
err => raise Render(format="JSON", key=path, reason=err.message())
}
json.stringify()
}
}
}
///|
/// The value at the key path `key` as a JSON object with the fields `key` and
/// `value`, after redaction. A missing key raises `KeyNotFound`.
pub fn get_json(config : Config, key : String) -> Json raise ConfigError {
let path = KeyPath::parse(key)
let value = redacted_at(config, path)
let rendered = @ion_json.to_json(value.ion) catch {
err => raise Render(format="JSON", key=path, reason=err.message())
}
{ "key": Json::string(path.to_string()), "value": rendered }
}
///|
/// Render the redacted configuration as TOML, JSON, or Ion text.
pub fn show(config : Config, format : ShowFormat) -> String raise ConfigError {
let redacted = redact(config.root)
match format {
Json => show_json_value(redacted, { names: ["(root)"] }).stringify()
Ion =>
@ion_text.write_ion(redacted.ion) catch {
err =>
raise Render(
format="Ion",
key={ names: ["(root)"] },
reason=err.message(),
)
}
Toml => to_toml_value(redacted, []).to_string()
}
}
///|
/// Source reports as text: highest priority first.
pub fn path_text(reports : Array[SourceReport]) -> String {
let lines = ["Configuration sources (in priority order):"]
for report in reports.rev() {
let mark = if report.status is Loaded { "[✓]" } else { "[✗]" }
lines.push("")
lines.push("\{mark} \{report.kind.report_name()} (\{report.name})")
if report.path is Some(path) {
lines.push(" Path: \{path}")
}
lines.push(" Status: \{status_text(report.status)}")
lines.push(" Priority: \{report.priority}")
for warning in report.warnings {
lines.push(" Warning: \{warning_text(warning)}")
}
}
lines.join("\n")
}
///|
/// Source reports as JSON, in ascending priority (merge order).
pub fn path_json(reports : Array[SourceReport]) -> Json {
Json::array(
reports.map(report => {
let object : Map[String, Json] = {
"kind": Json::string(report.kind.report_name()),
"name": Json::string(report.name),
"priority": Json::number(report.priority.to_double()),
"status": Json::string(status_text(report.status)),
"candidates": Json::array(
report.candidates.map(path => Json::string(path)),
),
}
match report.path {
Some(path) => object["path"] = Json::string(path)
None => object["path"] = Json::null()
}
if !report.warnings.is_empty() {
object["warnings"] = Json::array(
report.warnings.map(warning => Json::string(warning_text(warning))),
)
}
Json::object(object)
}),
)
}
///|
/// The origins of `key`: one entry for a leaf, or each descendant leaf for a
/// struct. A missing key raises `KeyNotFound`.
pub fn explain(
config : Config,
key : String,
) -> Array[(KeyPath, Array[Origin])] raise ConfigError {
let path = KeyPath::parse(key)
guard config.get(path) is Some(value) else { raise KeyNotFound(key=path) }
if value.ion.kind() is Struct(_) &&
!is_secret_reference(value, config.secret_syntax) {
return leaves(value, secret_syntax=config.secret_syntax).map(leaf => {
let full = { names: [..path.names, ..leaf.0.names] }
(full, config.origins(full))
})
}
[(path, config.origins(path))]
}
///|
fn redacted_at(config : Config, path : KeyPath) -> ConfigValue raise ConfigError {
let redacted = redact(config.root)
guard redacted.at(path.names) is Some(value) else {
raise KeyNotFound(key=path)
}
value
}
///|
fn status_text(status : SourceStatus) -> String {
match status {
Loaded => "loaded"
NotFound => "not-found"
Skipped => "skipped"
}
}
///|
fn warning_text(warning : ConfigWarning) -> String {
match warning {
KeyRemoved(key, origin) =>
"removed key \{key} from \{origin_text(origin)}"
Note(source~, message~) => "\{source}: \{message}"
}
}
///|
fn show_json_value(value : ConfigValue, path : KeyPath) -> Json raise ConfigError {
match value.ion.kind() {
Null(_) => Json::null()
Bool(flag) => Json::boolean(flag)
Int(number) => Json::string(number.to_string())
Float(number) => {
if number.is_nan() || number.is_inf() {
raise Render(
format="JSON",
key=path,
reason="JSON cannot hold NaN or infinity",
)
}
Json::number(number)
}
Decimal(number) => Json::string(number.to_ion_string())
Timestamp(stamp) => Json::string(stamp.to_ion_string())
Symbol(token) => Json::string(token.text().unwrap_or(""))
String(text) => Json::string(text)
Blob(bytes) | Clob(bytes) =>
Json::string(@base64.encode(bytes[:]))
List(items) | Sexp(items) =>
Json::array(items.map(item => show_json_value({ ion: item }, path)))
Struct(fields) => {
let object : Map[String, Json] = Map([])
for field in fields {
let name = field.name().text().unwrap_or("")
object[name] = show_json_value(
{ ion: field.value() },
{ names: [..path.names, name] },
)
}
Json::object(object)
}
}
}
///|
/// Render an Ion timestamp as a TOML date-time value, not a quoted string.
fn timestamp_to_toml(
stamp : @ion.Timestamp,
key : KeyPath,
) -> @toml_lib.TomlValue raise ConfigError {
let text = stamp.to_ion_string()
let document = @toml_lib.parse("v = \{text}") catch {
err =>
raise Render(
format="TOML",
key~,
reason="cannot write timestamp as TOML date-time: \{err}",
)
}
match document {
TomlTable(entries) =>
match entries.get("v") {
Some(TomlDateTime(_) as value) => value
Some(_) =>
raise Render(
format="TOML",
key~,
reason="timestamp did not parse as a TOML date-time",
)
None =>
raise Render(
format="TOML",
key~,
reason="timestamp did not parse as a TOML date-time",
)
}
_ =>
raise Render(
format="TOML",
key~,
reason="timestamp did not parse as a TOML date-time",
)
}
}
///|
fn to_toml_value(
value : ConfigValue,
path : Array[String],
) -> @toml_lib.TomlValue raise ConfigError {
let key : KeyPath = if path.is_empty() {
{ names: ["(root)"] }
} else {
{ names: path.copy() }
}
match value.ion.kind() {
Null(_) => raise Render(format="TOML", key~, reason="TOML cannot hold null")
Bool(flag) => @toml_lib.TomlBoolean(flag)
Int(number) => @toml_lib.TomlInteger(number.to_int64())
Float(number) => @toml_lib.TomlFloat(number)
Decimal(number) => @toml_lib.TomlString(number.to_ion_string())
Timestamp(stamp) => timestamp_to_toml(stamp, key)
Symbol(token) => @toml_lib.TomlString(token.text().unwrap_or(""))
String(text) => @toml_lib.TomlString(text)
Blob(bytes) | Clob(bytes) => @toml_lib.TomlString(@base64.encode(bytes[:]))
List(items) | Sexp(items) => {
let out = []
for item in items {
if item.is_null() {
raise Render(
format="TOML",
key~,
reason="TOML cannot hold null in a list",
)
}
out.push(to_toml_value({ ion: item }, path))
}
@toml_lib.TomlArray(out)
}
Struct(fields) => {
let table : Map[String, @toml_lib.TomlValue] = Map([])
for field in fields {
let name = field.name().text().unwrap_or("")
table[name] = to_toml_value({ ion: field.value() }, [..path, name])
}
@toml_lib.TomlTable(table)
}
}
}