///| 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.has_prefix(" ") || value.has_suffix(" ") {
"\"" + value.replace(old="\\", new="\\\\").replace(old="\"", new="\\\"") + "\""
} 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(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 + " = " + value + "\n")
None => match schema_field.default_value {
Some(default_value) => builder.write_string(key + " = " + 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()
}