///|
/// Create or get a nested table structure from a path
fn create_nested_table(
root_table : Map[String, TomlValue],
path : Array[String],
) -> Map[String, TomlValue] raise TableConflicts {
let mut current_table = root_table
for i, key in path {
let is_final = i == path.length() - 1
match current_table.get(key) {
Some(TomlTable(existing_table)) => {
if existing_table.contains("\u0000__inline__") {
raise InlineTableImmutable(key)
}
current_table = existing_table
}
Some(TomlArray(arr)) if arr.length() > 0 && !is_final => {
// Navigate into the last element of an array of tables
// (only for intermediate path segments, not the final key)
match arr[arr.length() - 1] {
TomlTable(last_table) => {
if last_table.contains("\u0000__inline__") {
raise InlineTableImmutable(key)
}
current_table = last_table
}
_ => raise ExpectedTable(key, TomlArray(arr))
}
}
Some(value) => raise ExpectedTable(key, value)
None => {
// Reject adding new keys to inline tables
if current_table.contains("\u0000__inline__") {
raise InlineTableImmutable(key)
}
let new_table = Map::new()
current_table[key] = TomlTable(new_table)
current_table = new_table
}
}
}
current_table
}
///|
priv suberror TableConflicts {
ExpectedTable(String, TomlValue)
ExpectedArray(String, TomlValue)
DuplicateKey(String)
InlineTableImmutable(String)
} derive(ToJson, Debug)
///|
/// Show impl for error messages
impl Show for TableConflicts with output(self, logger) {
match self {
ExpectedTable(key, value) => {
logger.write_string("ExpectedTable(\"")
logger.write_string(key)
logger.write_string("\", \"")
logger.write_string(value.type_name())
logger.write_string("\")")
}
ExpectedArray(key, value) => {
logger.write_string("ExpectedArray(\"")
logger.write_string(key)
logger.write_string("\", \"")
logger.write_string(value.type_name())
logger.write_string("\")")
}
DuplicateKey(key) => {
logger.write_string("DuplicateKey(\"")
logger.write_string(key)
logger.write_string("\")")
}
InlineTableImmutable(key) => {
logger.write_string("InlineTableImmutable(\"")
logger.write_string(key)
logger.write_string("\")")
}
}
}
///|
/// Create or append to an array of tables structure in TOML.
///
/// This function handles the `[[table]]` syntax in TOML, which creates an array
/// of tables. If the array doesn't exist, it creates it with the first table.
/// If the array exists, it appends a new table to it.
///
/// ## Parameters
/// - `root_table`: The root table where the array of tables will be created/modified
/// - `path`: The dotted key path to the array (e.g., ["servers", "alpha"] for [[servers.alpha]])
/// - `error_tokens`: Token view for error reporting
///
/// ## Returns
/// The newly created empty table that was added to the array
///
/// ## Errors
/// - Raises if the path conflicts with existing non-table values
/// - Raises if the final key exists but is not an array
///
/// ## Example
/// ```
/// // For TOML: [[servers.alpha]]
/// // Creates: { servers: { alpha: [{}] } }
/// // Second call appends: { servers: { alpha: [{}, {}] } }
/// ```
fn create_array_of_tables(
root_table : Map[String, TomlValue],
path : Array[String],
) -> Map[String, TomlValue] raise TableConflicts {
let mut current_table = root_table
// Navigate to the parent of the final key
for key in path[:-1] {
match current_table.get(key) {
Some(TomlTable(existing_table)) => {
if existing_table.contains("\u0000__inline__") {
raise InlineTableImmutable(key)
}
current_table = existing_table
}
Some(TomlArray(arr)) if arr.length() > 0 => {
// Navigate into the last element of an array of tables
match arr[arr.length() - 1] {
TomlTable(last_table) => {
if last_table.contains("\u0000__inline__") {
raise InlineTableImmutable(key)
}
current_table = last_table
}
_ => raise ExpectedTable(key, TomlArray(arr))
}
}
Some(value) => raise ExpectedTable(key, value)
None => {
let new_table = Map::new()
current_table[key] = TomlTable(new_table)
current_table = new_table
}
}
}
// Handle the final key as an array of tables
let final_key = path[path.length() - 1]
let new_table = {}
match current_table.get(final_key) {
Some(TomlArray(existing_array)) => {
// Verify this is an array-of-tables (created by [[ ]]), not a static array (= [...])
// Static arrays are either empty or contain non-table values
let is_array_of_tables = existing_array.length() > 0 && existing_array.iter().all(
fn(v) { v is TomlTable(_) },
)
if !is_array_of_tables {
raise ExpectedArray(final_key, TomlArray(existing_array))
}
existing_array.push(TomlTable(new_table))
new_table
}
Some(value) => raise ExpectedArray(final_key, value)
None => {
// Create new array with the first table
let array = Array::new()
array.push(TomlTable(new_table))
current_table[final_key] = TomlArray(array)
new_table
}
}
}
///|
test "create_array_of_tables - first table creation" {
// Test creating the first table in an array of tables
let root_table : Map[String, TomlValue] = {}
// Create first table in array [[servers]]
let new_table = create_array_of_tables(root_table, ["servers"])
// Add some data to the new table
new_table["name"] = TomlString("alpha")
new_table["ip"] = TomlString("10.0.0.1")
inspect(
TomlTable(root_table),
content=(
#|[[servers]]
#|name = "alpha"
#|ip = "10.0.0.1"
#|
),
)
}
///|
test "create_array_of_tables - append to existing array" {
// Test appending to an existing array of tables
let root_table : Map[String, TomlValue] = {}
// Create first table
let table1 = create_array_of_tables(root_table, ["products"])
table1["name"] = TomlString("Hammer")
table1["sku"] = TomlInteger(738594937)
// Append second table to same array
let table2 = create_array_of_tables(root_table, ["products"])
table2["name"] = TomlString("Nail")
table2["sku"] = TomlInteger(284758393)
// Append third table
let table3 = create_array_of_tables(root_table, ["products"])
table3["name"] = TomlString("Screwdriver")
table3["sku"] = TomlInteger(987654321)
inspect(
TomlTable(root_table),
content=(
#|[[products]]
#|name = "Hammer"
#|sku = 738594937
#|
#|[[products]]
#|name = "Nail"
#|sku = 284758393
#|
#|[[products]]
#|name = "Screwdriver"
#|sku = 987654321
#|
),
)
}
///|
test "create_array_of_tables - nested path creation" {
// Test creating array of tables with nested path
let root_table : Map[String, TomlValue] = {}
// Create [[fruit.physical.color]]
let color_table1 = create_array_of_tables(root_table, [
"fruit", "physical", "color",
])
color_table1["name"] = TomlString("red")
color_table1["code"] = TomlString("#FF0000")
// Add another color
let color_table2 = create_array_of_tables(root_table, [
"fruit", "physical", "color",
])
color_table2["name"] = TomlString("green")
color_table2["code"] = TomlString("#00FF00")
inspect(
TomlTable(root_table),
content=(
#|[fruit]
#|[fruit.physical]
#|[[fruit.physical.color]]
#|name = "red"
#|code = "#FF0000"
#|
#|[[fruit.physical.color]]
#|name = "green"
#|code = "#00FF00"
#|
),
)
}
///|
test "create_array_of_tables - mixed with regular tables" {
// Test array of tables mixed with regular tables
let root_table : Map[String, TomlValue] = Map::new()
// First create a regular table structure
set_dotted_key_value(root_table, ["owner", "name"], TomlString("Tom"))
set_dotted_key_value(root_table, ["owner", "dob"], TomlString("1979-05-27"))
// Now create array of tables at a different path
let db1 = create_array_of_tables(root_table, ["database", "servers"])
db1["host"] = TomlString("192.168.1.1")
db1["port"] = TomlInteger(5432)
let db2 = create_array_of_tables(root_table, ["database", "servers"])
db2["host"] = TomlString("192.168.1.2")
db2["port"] = TomlInteger(5433)
json_inspect(root_table, content=({"owner":["TomlTable",{"name":["TomlString","Tom"],"dob":["TomlString","1979-05-27"]}],"database":["TomlTable",{"servers":["TomlArray",[["TomlTable",{"host":["TomlString","192.168.1.1"],"port":["TomlInteger","5432"]}],["TomlTable",{"host":["TomlString","192.168.1.2"],"port":["TomlInteger","5433"]}]]]}]}))
}
///|
#skip("error message is not correct")
test "create_array_of_tables - path conflicts with non-table" {
// Test error when intermediate path conflicts with non-table value
let root_table : Map[String, TomlValue] = Map::new()
// Set a string value at "products"
root_table["products"] = TomlString("not a table")
// Try to create array of tables at ["products", "items"]
// This should fail because "products" is not a table
let result = try? create_array_of_tables(root_table, ["products", "items"])
debug_inspect(
result,
content=(
#|Err(Failure("/Users/hongbozhang/git/toml-parser/parser.mbt:1038:11-1038:45 FAILED: Array of tables path conflicts with existing value: products at the end of input"))
),
)
}
///|
#skip("error message is not correct")
test "create_array_of_tables - final key conflicts with non-array" {
// Test error when final key exists but is not an array
let root_table : Map[String, TomlValue] = Map::new()
// Create a regular table at servers.alpha
set_dotted_key_value(
root_table,
["servers", "alpha"],
TomlTable(Map::from_array([("name", TomlString("main"))])),
)
// Try to treat servers.alpha as array of tables
let result = try? create_array_of_tables(root_table, ["servers", "alpha"])
debug_inspect(
result,
content=(
#|Err(Failure("/Users/hongbozhang/git/toml-parser/parser.mbt:1049:11-1049:45 FAILED: Array of tables conflicts with existing non-array value: alpha at the end of input"))
),
)
}
///|
test "create_array_of_tables - multiple nested arrays" {
// Test creating multiple different arrays of tables
let root_table : Map[String, TomlValue] = Map::new()
// Create first [[fruits]]
let fruit1 = create_array_of_tables(root_table, ["fruits"])
fruit1["name"] = TomlString("apple")
// Create second [[fruits]]
let fruit2 = create_array_of_tables(root_table, ["fruits"])
fruit2["name"] = TomlString("banana")
// Create [[vegetables]]
let veg1 = create_array_of_tables(root_table, ["vegetables"])
veg1["name"] = TomlString("carrot")
veg1["color"] = TomlString("orange")
let veg2 = create_array_of_tables(root_table, ["vegetables"])
veg2["name"] = TomlString("lettuce")
veg2["color"] = TomlString("green")
json_inspect(root_table, content={
"fruits": [
"TomlArray",
[
["TomlTable", { "name": ["TomlString", "apple"] }],
["TomlTable", { "name": ["TomlString", "banana"] }],
],
],
"vegetables": [
"TomlArray",
[
[
"TomlTable",
{
"name": ["TomlString", "carrot"],
"color": ["TomlString", "orange"],
},
],
[
"TomlTable",
{
"name": ["TomlString", "lettuce"],
"color": ["TomlString", "green"],
},
],
],
],
})
}
///|
test "create_array_of_tables - empty table in array" {
// Test that empty tables are correctly added to arrays
let root_table : Map[String, TomlValue] = Map::new()
// Create empty table in array
let _empty_table1 = create_array_of_tables(root_table, ["empty", "tables"])
// Don't add any fields
let _empty_table2 = create_array_of_tables(root_table, ["empty", "tables"])
// Also empty
let table3 = create_array_of_tables(root_table, ["empty", "tables"])
table3["has_content"] = TomlBoolean(true)
json_inspect(root_table, content={
"empty": [
"TomlTable",
{
"tables": [
"TomlArray",
[
["TomlTable", {}],
["TomlTable", {}],
["TomlTable", { "has_content": ["TomlBoolean", true] }],
],
],
},
],
})
}
///|
/// Set a value using a dotted key path in a table
fn set_dotted_key_value(
table : Map[String, TomlValue],
key_path : Array[String],
value : TomlValue,
mark_implicit~ : Bool = false,
) -> Unit raise TableConflicts {
guard key_path.length() != 0
if key_path.length() == 1 {
// Simple key assignment — reject duplicates
let key = key_path[0]
if table.contains(key) {
raise DuplicateKey(key)
}
table[key] = value
return
}
// Navigate through the dotted path, creating tables as needed.
// Mark newly created intermediate tables as "defined" so they can't
// be reopened with [table] headers later.
let mut current_table = table
for i = 0; i < key_path.length() - 1; i = i + 1 {
let key = key_path[i]
match current_table.get(key) {
Some(TomlTable(existing_table)) => {
// Reject extending inline tables
if existing_table.contains("\u0000__inline__") {
raise InlineTableImmutable(key)
}
// Reject using dotted keys to extend a table that was explicitly
// defined with a [table] header in a different section.
// (Tables defined by dotted keys have __dotted__ marker, not __defined__)
if mark_implicit && existing_table.contains("\u0000__header__") {
raise DuplicateKey(key)
}
current_table = existing_table
}
Some(value) => raise ExpectedTable(key, value)
None => {
// Create new nested table
let new_table = Map::new()
if mark_implicit {
new_table["\u0000__defined__"] = TomlBoolean(true)
}
current_table[key] = TomlTable(new_table)
current_table = new_table
}
}
}
// Set the final value — reject duplicates
let final_key = key_path[key_path.length() - 1]
if current_table.contains(final_key) {
raise DuplicateKey(final_key)
}
current_table[final_key] = value
}
///|
test "set dotted key value - simple key" {
// Test setting a simple key (path length 1)
let table : Map[String, TomlValue] = Map::new()
set_dotted_key_value(table, ["name"], TomlString("Alice"))
json_inspect(table, content={ "name": ["TomlString", "Alice"] })
}
///|
test "set dotted key value - nested keys creating tables" {
// Test creating nested tables automatically
let table : Map[String, TomlValue] = Map::new()
set_dotted_key_value(table, ["server", "host"], TomlString("localhost"))
set_dotted_key_value(table, ["server", "port"], TomlInteger(8080))
json_inspect(table, content=({"server":["TomlTable",{"host":["TomlString","localhost"],"port":["TomlInteger","8080"]}]}))
}
///|
test "set dotted key value - deep nesting" {
// Test deep nested table creation
let table : Map[String, TomlValue] = Map::new()
set_dotted_key_value(
table,
["a", "b", "c", "d", "e"],
TomlString("deep value"),
)
json_inspect(table, content=({"a":["TomlTable",{"b":["TomlTable",{"c":["TomlTable",{"d":["TomlTable",{"e":["TomlString","deep value"]}]}]}]}]}))
}
///|
test "set dotted key value - multiple values same nested table" {
// Test adding multiple values to the same nested table
let table : Map[String, TomlValue] = Map::new()
// Add multiple values at different levels
set_dotted_key_value(table, ["database", "server"], TomlString("postgres"))
set_dotted_key_value(table, ["database", "port"], TomlInteger(5432))
set_dotted_key_value(
table,
["database", "credentials", "user"],
TomlString("admin"),
)
set_dotted_key_value(
table,
["database", "credentials", "password"],
TomlString("secret"),
)
set_dotted_key_value(table, ["database", "options", "ssl"], TomlBoolean(true))
json_inspect(table, content=({"database":["TomlTable",{"server":["TomlString","postgres"],"port":["TomlInteger","5432"],"credentials":["TomlTable",{"user":["TomlString","admin"],"password":["TomlString","secret"]}],"options":["TomlTable",{"ssl":["TomlBoolean",true]}]}]}))
}
///|
test "set dotted key value - reject duplicate key" {
let table : Map[String, TomlValue] = Map::new()
set_dotted_key_value(table, ["config", "timeout"], TomlInteger(30))
// Duplicate key should be rejected
let result = try? set_dotted_key_value(
table, ["config", "timeout"], TomlInteger(60),
)
inspect(result, content="Err(DuplicateKey(\"timeout\"))")
}
///|
test "set dotted key value - add to existing table" {
// Test adding values to an already existing table
let table : Map[String, TomlValue] = Map::new()
// Create initial structure
set_dotted_key_value(table, ["app", "name"], TomlString("MyApp"))
set_dotted_key_value(table, ["app", "version"], TomlString("1.0.0"))
// Add more values to the same table later
set_dotted_key_value(table, ["app", "author"], TomlString("Developer"))
set_dotted_key_value(table, ["app", "license"], TomlString("MIT"))
json_inspect(table, content=({"app":["TomlTable",{"name":["TomlString","MyApp"],"version":["TomlString","1.0.0"],"author":["TomlString","Developer"],"license":["TomlString","MIT"]}]}))
}
///|
test "set dotted key value - conflict with non-table value" {
let table : Map[String, TomlValue] = {}
// Set a string value at "config"
set_dotted_key_value(table, ["config"], TomlString("simple value"))
// Try to treat "config" as a table - this should raise an error
try
set_dotted_key_value(table, ["config", "nested"], TomlString("value"))
catch {
err =>
debug_inspect(
err,
content=(
#|ExpectedTable("config", TomlString("simple value"))
),
)
} noraise {
_ => fail("Expected error but got success")
}
}
///|
test "set dotted key value - mixed types in path" {
// Test with different value types at different levels
let table : Map[String, TomlValue] = Map::new()
set_dotted_key_value(
table,
["root", "array"],
TomlArray([TomlInteger(1), TomlInteger(2), TomlInteger(3)]),
)
set_dotted_key_value(table, ["root", "float"], TomlFloat(3.14))
set_dotted_key_value(table, ["root", "nested", "bool"], TomlBoolean(false))
set_dotted_key_value(
table,
["root", "nested", "datetime"],
TomlDateTime(@tokenize.TomlDateTime::OffsetDateTime("2024-01-01T12:00:00Z")),
)
json_inspect(table, content=({"root":["TomlTable",{"array":["TomlArray",[["TomlInteger","1"],["TomlInteger","2"],["TomlInteger","3"]]],"float":["TomlFloat",3.14],"nested":["TomlTable",{"bool":["TomlBoolean",false],"datetime":["TomlDateTime",["OffsetDateTime","2024-01-01T12:00:00Z"]]}]}]}))
}
///|
#skip("empty path edge case to fix later")
test "set dotted key value - empty path edge case" {
// Test with empty path (edge case)
let table : Map[String, TomlValue] = Map::new()
// Set a value first
set_dotted_key_value(table, ["key"], TomlString("value"))
// Try with empty path - currently does nothing
set_dotted_key_value(table, [], TomlString("should not appear"))
// Table should only have the first key
json_inspect(table, content={ "key": ["TomlString", "value"] })
}