///| A named configuration layer, usually base/dev/prod/local/secret.
pub struct ConfigLayer {
name : String
priority : Int
document : ConfigDocument
} derive(@debug.Debug, Eq)
///| One effective value after layered merging.
pub struct ResolvedValue {
section : String
key : String
value : String
source_layer : String
source : String
span : Span
overwritten_by : Array[String]
} derive(@debug.Debug, Eq)
///| A merged configuration view with traceable values.
pub struct ConfigView {
values : Array[ResolvedValue]
diagnostics : Array[Diagnostic]
} derive(@debug.Debug, Eq)
///| Creates a layer with an explicit priority. Higher priority wins.
pub fn layer(name : String, priority : Int, document : ConfigDocument) -> ConfigLayer {
{ name, priority, document }
}
///| Merges layers in priority order and keeps the final source for every key.
pub fn merge_layers(layers : Array[ConfigLayer]) -> ConfigView {
let values : Array[ResolvedValue] = []
let diagnostics : Array[Diagnostic] = []
let ordered = sort_layers(layers)
for layer_item in ordered {
append_diagnostics(diagnostics, layer_item.document.diagnostics)
for entry in layer_item.document.entries {
let path = entry.path()
let existing_index = find_resolved_index(values, path)
match existing_index {
None => {
values.push({
section: entry.section,
key: entry.key,
value: entry.value,
source_layer: layer_item.name,
source: entry.source,
span: entry.span,
overwritten_by: [],
})
}
Some(index) => {
let previous = values[index]
let overwritten_by = previous.overwritten_by.copy()
overwritten_by.push(layer_item.name)
values[index] = {
section: entry.section,
key: entry.key,
value: entry.value,
source_layer: layer_item.name,
source: entry.source,
span: entry.span,
overwritten_by,
}
diagnostics.push(info(
"overlay",
"key '" + path + "' from layer '" + layer_item.name + "' overrides layer '" + previous.source_layer + "'",
span=Some(entry.span),
))
}
}
}
}
{ values, diagnostics }
}
fn append_diagnostics(target : Array[Diagnostic], source : Array[Diagnostic]) -> Unit {
for item in source {
target.push(item)
}
}
fn sort_layers(layers : Array[ConfigLayer]) -> Array[ConfigLayer] {
let result = layers.copy()
let mut i = 1
while i < result.length() {
let current = result[i]
let mut j = i - 1
while j >= 0 && result[j].priority > current.priority {
result[j + 1] = result[j]
j = j - 1
}
result[j + 1] = current
i = i + 1
}
result
}
fn find_resolved_index(values : Array[ResolvedValue], path : String) -> Int? {
let mut index = 0
while index < values.length() {
if entry_path(values[index].section, values[index].key) == path {
return Some(index)
}
index = index + 1
}
None
}
///| Finds a raw resolved value by section and key.
pub fn ConfigView::get(self : ConfigView, section : String, key : String) -> String? {
let path = entry_path(section, key)
for item in self.values {
if entry_path(item.section, item.key) == path {
return Some(item.value)
}
}
None
}
///| Finds a raw resolved value by dotted path.
pub fn ConfigView::get_path(self : ConfigView, path : String) -> String? {
for item in self.values {
if entry_path(item.section, item.key) == path {
return Some(item.value)
}
}
None
}
///| Reads a boolean value using common configuration spellings.
pub fn ConfigView::get_bool(self : ConfigView, path : String) -> Bool? {
match self.get_path(path) {
None => None
Some(value) => parse_bool_value(value)
}
}
///| Reads an integer value.
pub fn ConfigView::get_int(self : ConfigView, path : String) -> Int? {
match self.get_path(path) {
None => None
Some(value) => parse_int_value(value)
}
}
///| Reads a comma-separated list.
pub fn ConfigView::get_list(self : ConfigView, path : String) -> Array[String]? {
match self.get_path(path) {
None => None
Some(value) => Some(value.split(",").map(part => part.to_owned().trim().to_owned()).collect())
}
}
fn parse_bool_value(value : String) -> Bool? {
let normalized = value.trim().to_owned()
if normalized == "true" || normalized == "TRUE" || normalized == "True" ||
normalized == "yes" || normalized == "YES" || normalized == "Yes" ||
normalized == "on" || normalized == "ON" || normalized == "On" ||
normalized == "1" {
Some(true)
} else if normalized == "false" || normalized == "FALSE" || normalized == "False" ||
normalized == "no" || normalized == "NO" || normalized == "No" ||
normalized == "off" || normalized == "OFF" || normalized == "Off" ||
normalized == "0" {
Some(false)
} else {
None
}
}
fn parse_int_value(value : String) -> Int? {
let trimmed = value.trim().to_owned()
if trimmed == "" {
None
} else {
parse_decimal_int(trimmed)
}
}
fn parse_decimal_int(text : String) -> Int? {
let mut index = 0
let mut sign = 1
if text.has_prefix("-") {
sign = -1
index = 1
} else if text.has_prefix("+") {
index = 1
}
if index >= text.length() {
return None
}
let mut value = 0
while index < text.length() {
let ch = text[index]
if ch < '0' || ch > '9' {
return None
}
value = value * 10 + (ch.to_int() - '0'.to_int())
index = index + 1
}
Some(value * sign)
}