///| A variable placeholder found in a config value, such as ${DATABASE_URL}.
pub struct Placeholder {
path : String
name : String
raw : String
} derive(@debug.Debug, Eq)
///| A resolved placeholder value supplied by a caller.
pub struct PlaceholderValue {
name : String
value : String
} derive(@debug.Debug, Eq)
pub fn placeholder_value(name : String, value : String) -> PlaceholderValue {
{ name, value }
}
///| Finds ${NAME} placeholders in all effective values.
pub fn find_placeholders(view : ConfigView) -> Array[Placeholder] {
let placeholders : Array[Placeholder] = []
for item in view.values {
let path = entry_path(item.section, item.key)
append_placeholders(placeholders, path, item.value)
}
placeholders
}
fn append_placeholders(placeholders : Array[Placeholder], path : String, value : String) -> Unit {
let mut index = 0
while index < value.length() {
if index + 2 <= value.length() && value[index:index + 2].to_owned() == "${" {
let end = find_placeholder_end(value, index + 2)
match end {
None => index = index + 2
Some(end_index) => {
let raw = value[index:end_index + 1].to_owned()
let name = value[index + 2:end_index].to_owned()
placeholders.push({ path, name, raw })
index = end_index + 1
}
}
} else {
index = index + 1
}
}
}
fn find_placeholder_end(value : String, start : Int) -> Int? {
let mut index = start
while index < value.length() {
if value[index] == '}' {
return Some(index)
}
index = index + 1
}
None
}
///| Reports placeholders without supplied values.
pub fn unresolved_placeholder_diagnostics(view : ConfigView, supplied : Array[PlaceholderValue]) -> Array[Diagnostic] {
let diagnostics : Array[Diagnostic] = []
for placeholder in find_placeholders(view) {
if !has_placeholder_value(supplied, placeholder.name) {
diagnostics.push(warning("unresolved-placeholder", "key '" + placeholder.path + "' references unresolved placeholder '" + placeholder.raw + "'"))
}
}
diagnostics
}
fn has_placeholder_value(values : Array[PlaceholderValue], name : String) -> Bool {
for item in values {
if item.name == name {
return true
}
}
false
}
///| Replaces placeholders in a value.
pub fn expand_placeholders(value : String, supplied : Array[PlaceholderValue]) -> String {
let builder = StringBuilder()
let mut index = 0
while index < value.length() {
if index + 2 <= value.length() && value[index:index + 2].to_owned() == "${" {
let end = find_placeholder_end(value, index + 2)
match end {
None => {
builder.write_char(value[index].to_int().unsafe_to_char())
index = index + 1
}
Some(end_index) => {
let name = value[index + 2:end_index].to_owned()
match lookup_placeholder_value(supplied, name) {
None => builder.write_string(value[index:end_index + 1].to_owned())
Some(replacement) => builder.write_string(replacement)
}
index = end_index + 1
}
}
} else {
builder.write_char(value[index].to_int().unsafe_to_char())
index = index + 1
}
}
builder.to_string()
}
fn lookup_placeholder_value(values : Array[PlaceholderValue], name : String) -> String? {
for item in values {
if item.name == name {
return Some(item.value)
}
}
None
}
///| Escapes a properties key.
pub fn escape_properties_key(key : String) -> String {
let builder = StringBuilder()
let mut index = 0
while index < key.length() {
let ch = key[index]
if ch == ' ' || ch == ':' || ch == '=' || ch == '#' || ch == '!' || ch == '\\' {
builder.write_char('\\')
}
builder.write_char(ch.to_int().unsafe_to_char())
index = index + 1
}
builder.to_string()
}
///| Unescapes common properties escape sequences.
pub fn unescape_properties_value(value : String) -> String {
let builder = StringBuilder()
let mut index = 0
while index < value.length() {
if value[index] == '\\' && index + 1 < value.length() {
let next = value[index + 1]
if next == 'n' {
builder.write_char('\n')
} else if next == 't' {
builder.write_char('\t')
} else if next == 'r' {
builder.write_char('\r')
} else {
builder.write_char(next.to_int().unsafe_to_char())
}
index = index + 2
} else {
builder.write_char(value[index].to_int().unsafe_to_char())
index = index + 1
}
}
builder.to_string()
}
///| Produces a placeholder inventory table.
pub fn render_placeholder_report(view : ConfigView, supplied : Array[PlaceholderValue]) -> String {
let builder = StringBuilder()
builder.write_string("| Path | Placeholder | Resolved |\n")
builder.write_string("| --- | --- | --- |\n")
for placeholder in find_placeholders(view) {
builder.write_string("| " + placeholder.path + " | " + placeholder.raw + " | " + yes_no_placeholder(has_placeholder_value(supplied, placeholder.name)) + " |\n")
}
builder.to_string()
}
fn yes_no_placeholder(value : Bool) -> String {
if value { "yes" } else { "no" }
}
///| Renders properties with placeholders expanded.
pub fn render_expanded_properties(view : ConfigView, supplied : Array[PlaceholderValue]) -> String {
let builder = StringBuilder()
for item in view.values {
let path = entry_path(item.section, item.key)
builder.write_string(escape_properties_key(path) + "=" + escape_properties_value(expand_placeholders(item.value, supplied)) + "\n")
}
builder.to_string()
}