///|
/// Formatting options for INI and properties output.
pub struct FormatOptions {
redact_secrets : Bool
include_source_comments : Bool
blank_line_between_sections : Bool
quote_values : Bool
} derive(@debug.Debug, Eq)
///|
pub fn default_format_options() -> FormatOptions {
{
redact_secrets: false,
include_source_comments: true,
blank_line_between_sections: true,
quote_values: false,
}
}
///|
pub fn safe_format_options() -> FormatOptions {
{
redact_secrets: true,
include_source_comments: false,
blank_line_between_sections: true,
quote_values: false,
}
}
///|
/// Renders a merged view as INI text.
pub fn render_ini(
view : ConfigView,
options? : FormatOptions = default_format_options(),
) -> String {
let builder = StringBuilder()
let sections = collect_sections(view)
render_global_entries(builder, view, options)
let mut wrote_any_section = has_global_entries(view)
for section_name in sections {
if section_name != "" {
if wrote_any_section && options.blank_line_between_sections {
builder.write_string("\n")
}
builder.write_string("[" + section_name + "]\n")
for item in view.values {
if item.section == section_name {
render_entry_line(builder, item, options)
}
}
wrote_any_section = true
}
}
builder.to_string()
}
///|
fn has_global_entries(view : ConfigView) -> Bool {
for item in view.values {
if item.section == "" {
return true
}
}
false
}
///|
fn render_global_entries(
builder : StringBuilder,
view : ConfigView,
options : FormatOptions,
) -> Unit {
for item in view.values {
if item.section == "" {
render_entry_line(builder, item, options)
}
}
}
///|
fn render_entry_line(
builder : StringBuilder,
item : ResolvedValue,
options : FormatOptions,
) -> Unit {
let path = entry_path(item.section, item.key)
if options.include_source_comments {
builder.write_string(
"; from " +
item.source_layer +
" " +
item.source +
":" +
item.span.line.to_string() +
"\n",
)
}
let value = if options.redact_secrets {
redact_value(path, item.value)
} else {
item.value
}
builder.write_string(
item.key + " = " + format_value(value, options.quote_values) + "\n",
)
}
///|
fn format_value(value : String, quote : Bool) -> String {
if quote ||
value.contains("#") ||
value.contains(";") ||
value.contains("\n") ||
value.contains("\r") ||
value.contains("\t") ||
value.has_prefix(" ") ||
value.has_suffix(" ") {
"\"" +
value
.replace(old="\\", new="\\\\")
.replace(old="\"", new="\\\"")
.replace(old="\r", new="\\r")
.replace(old="\n", new="\\n")
.replace(old="\t", new="\\t") +
"\""
} else {
value
}
}
///|
fn collect_sections(view : ConfigView) -> Array[String] {
let sections : Array[String] = []
for item in view.values {
if item.section != "" && !contains_section(sections, item.section) {
sections.push(item.section)
}
}
sections
}
///|
fn contains_section(sections : Array[String], name : String) -> Bool {
for section_name in sections {
if section_name == name {
return true
}
}
false
}
///|
/// Renders a merged view as Java-style properties text.
pub fn render_properties(
view : ConfigView,
options? : FormatOptions = default_format_options(),
) -> String {
let builder = StringBuilder()
for item in view.values {
let path = entry_path(item.section, item.key)
if options.include_source_comments {
builder.write_string(
"# from " +
item.source_layer +
" " +
item.source +
":" +
item.span.line.to_string() +
"\n",
)
}
let value = if options.redact_secrets {
redact_value(path, item.value)
} else {
item.value
}
builder.write_string(
escape_properties_key(path) + "=" + escape_properties_value(value) + "\n",
)
}
builder.to_string()
}
///|
fn escape_properties_value(value : String) -> String {
value
.replace(old="\\", new="\\\\")
.replace(old="\n", new="\\n")
.replace(old="\t", new="\\t")
}
///|
/// Returns a document preview with defaults from schema filled into missing keys.
pub fn render_defaults_preview(
schema : ConfigSchema,
view : ConfigView,
) -> String {
let builder = StringBuilder()
for schema_section in schema.sections {
builder.write_string("[" + schema_section.name + "]\n")
for schema_field in schema_section.fields {
let key = field_key(schema_field.path)
match view.get_path(schema_field.path) {
Some(value) =>
builder.write_string(
key + " = " + redact_value(schema_field.path, value) + "\n",
)
None =>
match schema_field.default_value {
Some(default_value) =>
builder.write_string(
key +
" = " +
redact_value(schema_field.path, default_value) +
" ; default\n",
)
None =>
if schema_field.required {
builder.write_string("; " + key + " = \n")
}
}
}
}
builder.write_string("\n")
}
builder.to_string()
}
///|
fn field_key(path : String) -> String {
let parts = path.split(".").map(part => part.to_owned()).collect()
if parts.length() == 0 {
path
} else {
parts[parts.length() - 1]
}
}
///|
/// Produces a plain key inventory grouped by section.
pub fn render_inventory(view : ConfigView) -> String {
let builder = StringBuilder()
builder.write_string("Configuration Inventory\n")
builder.write_string("=======================\n\n")
for item in view.values {
let path = entry_path(item.section, item.key)
builder.write_string(
"- " +
path +
" (" +
item.source_layer +
", " +
item.source +
":" +
item.span.line.to_string() +
")\n",
)
}
builder.to_string()
}