///|
/// 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()
}