///|
priv enum Section {
Global
NamedSection(String)
} derive(Eq, Hash, Show)
///|
priv struct Key(String) derive(Eq, Hash)
///|
priv struct Value(String) derive(Show)
///|
struct IniFile {
sections : @hashmap.HashMap[Section, @hashmap.HashMap[Key, Value]]
// default not case sensitive
is_case_sensitive : Bool
}
///|
pub fn IniFile::new(is_case_sensitive? : Bool = false) -> IniFile {
IniFile::{ sections: @hashmap.new(), is_case_sensitive }
}
///|
fn IniFile::new_with_sections(
sections : @hashmap.HashMap[Section, @hashmap.HashMap[Key, Value]],
is_case_sensitive? : Bool = false,
) -> IniFile {
IniFile::{ sections, is_case_sensitive }
}
///|
fn IniFile::lower_if_needed(self : IniFile, s : String) -> String {
if self.is_case_sensitive {
s
} else {
s.to_lower()
}
}
///|
/// get is a alias for get_string
pub fn IniFile::get(
self : IniFile,
section? : String = "",
key : String,
) -> String? {
self.get_string(section~, key)
}
///|
pub fn IniFile::get_string(
self : IniFile,
section? : String = "",
key : String,
) -> String? {
let section_map = self.get_section(
match section {
"" => Section::Global
_ => Section::NamedSection(self.lower_if_needed(section))
},
create_if_not_exists=false,
)
guard section_map is Some(_) else { None }
let section_map = section_map.unwrap()
let value = section_map.get(self.lower_if_needed(key))
guard value is Some(_) else { None }
let value = value.unwrap()
let Value(v) = value
Some(v)
}
///|
pub fn IniFile::get_bool(
self : IniFile,
section? : String = "",
key : String,
) -> Bool? {
let value = self.get_string(section~, key)
guard value is Some(_) else { None }
let value = value.unwrap()
match value.to_lower() {
"true" | "yes" | "on" => Some(true)
"false" | "no" | "off" => Some(false)
_ => Some(false)
}
}
// TODO: Add support for other types like Int, Float, etc.
///|
fn IniFile::get_section(
self : IniFile,
section : Section,
create_if_not_exists? : Bool = false,
) -> @hashmap.HashMap[Key, Value]? {
let section_map = self.sections.get(section)
match section_map {
None =>
if create_if_not_exists {
let new_section = @hashmap.new()
self.sections.set(section, new_section)
Some(new_section)
} else {
None
}
Some(_) => section_map
}
}
///|
fn IniFile::get_section_or_create(
self : IniFile,
section : Section,
) -> @hashmap.HashMap[Key, Value] {
let section_map = self.get_section(section, create_if_not_exists=true)
section_map.unwrap()
}
///|
pub fn IniFile::set(
self : IniFile,
section? : String = "",
key : String,
value : String,
) -> Unit {
let section_map = self.get_section_or_create(
match section {
"" => Section::Global
_ => Section::NamedSection(self.lower_if_needed(section))
},
)
section_map.set(self.lower_if_needed(key), Value(value))
}
///|
/// Converts the IniFile to a standard INI format string
pub fn IniFile::to_string(self : IniFile) -> String {
fn escape_char(c : Char) -> String {
match c {
'\\' => "\\\\"
'\'' => "\\\'"
'"' => "\\\""
'\u0000' => "\\0"
'\u0007' => "\\a"
'\b' => "\\b"
'\t' => "\\t"
'\r' => "\\r"
'\n' => "\\n"
';' => "\\;"
'#' => "\\#"
'=' => "\\="
':' => "\\:"
_ if c.is_control() || c.is_whitespace() || not(c.is_printable()) => {
let unicode = c.to_int().to_string(radix=16)
guard unicode.length() <= 4 else {
abort("Unicode escape sequence too long")
}
return if unicode.length() < 4 {
let padding = "0".repeat(4 - unicode.length())
"\\x" + padding + unicode
} else {
"\\x" + unicode
}
}
_ => c.to_string()
}
}
fn escape_value(value : String) -> String {
let str_list = value.iter().collect().map(escape_char)
let builder = StringBuilder::new()
str_list.each(fn(c) { builder.write_string(c) })
builder.to_string()
}
let buf = StringBuilder::new()
// Global section first
let global_section = self.sections.get(Section::Global)
if global_section is Some(_) {
global_section
.unwrap()
.iter()
.each(fn(kv) {
let (key, value) = kv
let Key(key) = key
let Value(value) = value
buf.write_string(key)
buf.write_char('=')
buf.write_string(escape_value(value))
buf.write_char('\n')
})
}
// Other sections
self.sections
.iter()
.each(fn(section) {
let (section, map) = section
match section {
Section::Global => return
Section::NamedSection(name) => {
buf.write_char('[')
buf.write_string(name)
buf.write_char(']')
buf.write_char('\n')
map
.iter()
.each(fn(kv) {
let (key, value) = kv
let Key(key) = key
let Value(value) = value
buf.write_string(key)
buf.write_char('=')
buf.write_string(escape_value(value))
buf.write_char('\n')
})
}
}
})
buf.to_string()
}
///|
test "INIFile::new/default_case_sensitivity" {
let ini = IniFile::new()
inspect(ini.is_case_sensitive, content="false")
}
///|
test "INIFile::new/case_sensitive_true" {
let ini = IniFile::new(is_case_sensitive=true)
inspect(ini.is_case_sensitive, content="true")
}
///|
test "INIFile::new/case_sensitive_false" {
let ini = IniFile::new(is_case_sensitive=false)
inspect(ini.is_case_sensitive, content="false")
}
///|
test "INIFile::get/basic_functionality" {
let ini = {
let temp = IniFile::new()
temp.set(section="section1", "key1", "value1")
temp.set("key2", "value2")
temp
}
inspect(ini.get(section="section1", "key1"), content="Some(\"value1\")")
inspect(ini.get("key2"), content="Some(\"value2\")")
inspect(ini.get(section="nonexistent", "key1"), content="None")
inspect(ini.get("nonexistent"), content="None")
}
///|
test "INIFile::get/case_sensitivity" {
let ini = {
let temp = IniFile::new(is_case_sensitive=true)
temp..set(section="Section", "Key", "value")
temp
}
inspect(ini.get(section="Section", "Key"), content="Some(\"value\")")
inspect(ini.get(section="section", "Key"), content="None")
inspect(ini.get(section="Section", "key"), content="None")
}
///|
test "INIFile::get/empty_strings" {
let ini = {
let temp = IniFile::new()
temp..set(section="", "key", "value")
temp..set(section="section", "", "value")
temp
}
inspect(ini.get(section="", "key"), content="Some(\"value\")")
inspect(ini.get(section="section", ""), content="Some(\"value\")")
}
///|
test "INIFile::get_bool/true_values" {
let ini = {
let obj = IniFile::new()
obj..set("key1", "true")
obj..set("key2", "yes")
obj..set("key3", "on")
obj..set(section="test", "key4", "TRUE")
obj..set(section="test", "key5", "Yes")
obj..set(section="test", "key6", "ON")
obj
}
inspect(ini.get_bool("key1"), content="Some(true)")
inspect(ini.get_bool("key2"), content="Some(true)")
inspect(ini.get_bool("key3"), content="Some(true)")
inspect(ini.get_bool(section="test", "key4"), content="Some(true)")
inspect(ini.get_bool(section="test", "key5"), content="Some(true)")
inspect(ini.get_bool(section="test", "key6"), content="Some(true)")
}
///|
test "INIFile::get_bool/false_values" {
let ini = {
let ini = IniFile::new()
ini.set("key1", "false")
ini.set("key2", "no")
ini.set("key3", "off")
ini.set("key4", "invalid")
ini
}
inspect(ini.get_bool("key1"), content="Some(false)")
inspect(ini.get_bool("key2"), content="Some(false)")
inspect(ini.get_bool("key3"), content="Some(false)")
inspect(ini.get_bool("key4"), content="Some(false)")
}
///|
test "INIFile::get_bool/nonexistent_values" {
let ini = IniFile::new()
inspect(ini.get_bool(section="nonexistent", "key"), content="None")
inspect(ini.get_bool("nonexistent"), content="None")
}
///|
test "INIFile::set/basic_functionality" {
let ini = {
let ini = IniFile::new()
ini.set("key", "value")
ini
}
inspect(ini.get("key"), content="Some(\"value\")")
ini.set(section="section", "key", "value")
inspect(ini.get(section="section", "key"), content="Some(\"value\")")
}
///|
test "INIFile::set/case_sensitivity" {
let ini = {
let ini = IniFile::new(is_case_sensitive=true)
ini.set(section="Section", "Key", "value1")
ini.set(section="section", "key", "value2")
ini
}
inspect(ini.get(section="Section", "Key"), content="Some(\"value1\")")
inspect(ini.get(section="section", "key"), content="Some(\"value2\")")
}
///|
test "INIFile::set/overwrite" {
let ini = {
let temp = IniFile::new()
temp.set("key", "value1")
temp.set("key", "value2")
temp
}
inspect(ini.get("key"), content="Some(\"value2\")")
ini
..set(section="section", "key", "value1")
.set(section="section", "key", "value2")
inspect(ini.get(section="section", "key"), content="Some(\"value2\")")
}
///|
/// Test IniFile::to_string covers escaping special characters in key and value.
/// This test ensures that keys and values containing special INI characters
/// (such as '=', ';', '#', '\n', quotes, and leading/trailing spaces) are correctly output.
test "INIFile::to_string/escape_special_characters" {
let kvs = [
("key1", "\\value1"),
("key2", "value2\'"),
("key3", "value3\""),
("key4", "value4\n"),
("key5", "value5\r"),
("key6", "value6\t"),
("key7", "value7;"),
("key8", "value8#"),
("key9", "value9:"),
("key10", "value10="),
("key11", "value11 \t"),
("key12", " value12"),
("key13", "\u0000"),
("key14", "\u0007"),
]
let sections = [
// "" => Section::Global
"", "sec1=", "k", "v",
]
let ini1 = IniFile::new()
kvs
.iter()
.each(fn(kv) {
let (key, value) = kv
sections.iter().each(fn(section) { ini1.set(section~, key, value) })
})
let output = ini1.to_string()
let ini2 = parse(output)
kvs
.iter()
.each(fn(kv) {
let (key, _) = kv
sections
.iter()
.each(fn(section) {
let ini1_value = ini1.get(section~, key)
let ini2_value = ini2.get(section~, key)
inspect(ini1_value == ini2_value, content="true")
})
})
}