///|
/// 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.
pub fn ConfigSource::env(
prefix~ : String,
reserved? : Array[String] = [],
selection? : SourceSelection[Map[String, String]] = Discover,
name? : String = "environment",
priority? : Int,
) -> ConfigSource {
let selection = match selection {
Explicit(vars) => SourceSelection::Explicit(vars.copy())
other => other
}
{
kind: Environment,
name,
priority,
policy: Replace,
key_policies: [],
input: Env(selection, prefix, reserved.copy()),
excluded: [],
}
}
///|
/// Add an environment variable source, as `ConfigSource::env` gives it.
pub fn env(
builder : ConfigBuilder,
prefix~ : String,
reserved? : Array[String] = [],
selection? : SourceSelection[Map[String, String]] = Discover,
) -> ConfigBuilder {
add(builder, ConfigSource::env(prefix~, reserved~, selection~))
}
///|
/// An overrides source from key-path pairs, for example command-line values.
/// It has the kind `Overrides` and the default priority 700.
pub fn ConfigSource::overrides(
pairs : Array[(String, ConfigValue)],
name? : String = "overrides",
priority? : Int,
) -> ConfigSource raise ConfigError {
{ ..in_memory_pairs(pairs, name~, priority?), kind: Overrides }
}
///|
/// An overrides source from a value tree. The root must be a struct.
pub fn ConfigSource::overrides_value(
value : ConfigValue,
name? : String = "overrides",
priority? : Int,
) -> ConfigSource raise ConfigError {
{ ..in_memory(value, name~, priority?), kind: Overrides }
}
///|
/// Add an overrides source from key-path pairs.
pub fn overrides(
builder : ConfigBuilder,
pairs : Array[(String, ConfigValue)],
) -> ConfigBuilder raise ConfigError {
add(builder, ConfigSource::overrides(pairs))
}
///|
/// One tree for each matching variable, with the variable name. Deeper keys
/// come first, then keys and variable names in order, so a shorter path merges
/// last and wins, whatever the order of `vars`.
fn env_pieces(
vars : Map[String, String],
prefix : String,
reserved : Array[String],
) -> Array[(ConfigValue, String?)] raise ConfigError {
let start = prefix.to_upper() + "_"
let reserved = reserved.map(name => name.to_upper())
let keys : Array[(Array[String], String, String)] = []
for variable, text in vars {
let upper = variable.to_upper()
guard upper.has_prefix(start) && !reserved.contains(upper) else { continue }
let mut rest = variable[start.length():].to_owned()
while rest.has_prefix("_") {
rest = rest[1:].to_owned()
}
let names = rest.split("__").map(name => name.to_owned().to_lower()).collect()
if names.iter().any(name => name.is_empty()) {
raise InvalidEnvKey(variable~)
}
keys.push((names, variable, text))
}
keys.sort_by((a, b) => {
let by_depth = b.0.length().compare(a.0.length())
if by_depth != 0 {
return by_depth
}
let by_key = a.0.join(".").compare(b.0.join("."))
if by_key != 0 {
return by_key
}
a.1.compare(b.1)
})
keys.map(key => (nest({ names: key.0 }, env_value(key.2)), Some(key.1)))
}
///|
/// The typed value of an environment variable.
fn env_value(text : String) -> ConfigValue {
match text.to_lower() {
"true" => return bool(true)
"false" => return bool(false)
_ => ()
}
if parse_int64(text) is Some(number) {
return int(number)
}
if text.has_prefix("[") || text.has_prefix("{") {
let json = @json.parse(text) catch { _ => return string(text) }
if json is (Array(_) | Object(_)) {
return { ion: @ion.Ion::Ion(json) }
}
}
string(text)
}
///|
/// The `Int64` that `text` names: an optional sign and decimal digits. Leading
/// zeros are allowed. Text out of range gives `None`.
fn parse_int64(text : String) -> Int64? {
let (negative, digits) = if text.has_prefix("-") {
(true, text[1:].to_owned())
} else if text.has_prefix("+") {
(false, text[1:].to_owned())
} else {
(false, text)
}
guard !digits.is_empty() else { return None }
// Work in negative numbers, which reach Int64 minimum.
let mut value = 0L
for char in digits {
guard char >= '0' && char <= '9' else { return None }
let digit = (char.to_int() - '0'.to_int()).to_int64()
guard value >= (@int64.MIN_VALUE + digit) / 10L else { return None }
value = value * 10L - digit
}
if negative {
Some(value)
} else if value == @int64.MIN_VALUE {
None
} else {
Some(-value)
}
}