diff --git a/toml/_parser.ts b/toml/_parser.ts index 56dbe107ad4b..33994f191617 100644 --- a/toml/_parser.ts +++ b/toml/_parser.ts @@ -18,16 +18,18 @@ type ParseResult<T> = Success<T> | Failure; type ParserComponent<T = unknown> = (scanner: Scanner) => ParseResult<T>; -type BlockParseResultBody = { +type Block = { type: "Block"; value: Record<string, unknown>; -} | { +}; +type Table = { type: "Table"; - key: string[]; + keys: string[]; value: Record<string, unknown>; -} | { +}; +type TableArray = { type: "TableArray"; - key: string[]; + keys: string[]; value: Record<string, unknown>; }; @@ -151,45 +153,74 @@ export function unflat( ) as Record<string, unknown>; } -export function deepAssignWithTable(target: Record<string, unknown>, table: { - type: "Table" | "TableArray"; - key: string[]; - value: Record<string, unknown>; -}) { - if (table.key.length === 0 || table.key[0] == null) { +function isObject(value: unknown): value is Record<string, unknown> { + return typeof value === "object" && value !== null; +} + +function getTargetValue(target: Record<string, unknown>, keys: string[]) { + const key = keys[0]; + if (!key) { throw new Error( "Cannot parse the TOML: key length is not a positive number", ); } - const value = target[table.key[0]]; + return target[key]; +} - if (typeof value === "undefined") { - Object.assign( - target, - unflat( - table.key, - table.type === "Table" ? table.value : [table.value], - ), - ); - } else if (Array.isArray(value)) { - if (table.type === "TableArray" && table.key.length === 1) { - value.push(table.value); - } else { - const last = value[value.length - 1]; - deepAssignWithTable(last, { - type: table.type, - key: table.key.slice(1), - value: table.value, - }); - } - } else if (typeof value === "object" && value !== null) { - deepAssignWithTable(value as Record<string, unknown>, { - type: table.type, - key: table.key.slice(1), - value: table.value, - }); - } else { - throw new Error("Unexpected assign"); +function deepAssignTable( + target: Record<string, unknown>, + table: Table, +) { + const { keys, type, value } = table; + const currentValue = getTargetValue(target, keys); + + if (currentValue === undefined) { + return Object.assign(target, unflat(keys, value)); + } + if (Array.isArray(currentValue)) { + const last = currentValue.at(-1); + deepAssign(last, { type, keys: keys.slice(1), value }); + return target; + } + if (isObject(currentValue)) { + deepAssign(currentValue, { type, keys: keys.slice(1), value }); + return target; + } + throw new Error("Unexpected assign"); +} + +function deepAssignTableArray( + target: Record<string, unknown>, + table: TableArray, +) { + const { type, keys, value } = table; + const currentValue = getTargetValue(target, keys); + + if (currentValue === undefined) { + return Object.assign(target, unflat(keys, [value])); + } + if (Array.isArray(currentValue)) { + currentValue.push(value); + return target; + } + if (isObject(currentValue)) { + deepAssign(currentValue, { type, keys: keys.slice(1), value }); + return target; + } + throw new Error("Unexpected assign"); +} + +export function deepAssign( + target: Record<string, unknown>, + body: Block | Table | TableArray, +) { + switch (body.type) { + case "Block": + return deepMerge(target, body.value); + case "Table": + return deepAssignTable(target, body); + case "TableArray": + return deepAssignTableArray(target, body); } } @@ -725,7 +756,7 @@ export const pair = kv(dottedKey, "=", value); export function block( scanner: Scanner, -): ParseResult<BlockParseResultBody> { +): ParseResult<Block> { scanner.nextUntilChar(); const result = merge(repeat(pair))(scanner); if (result.ok) return success({ type: "Block", value: result.body }); @@ -734,7 +765,7 @@ export function block( export const tableHeader = surround("[", dottedKey, "]"); -export function table(scanner: Scanner): ParseResult<BlockParseResultBody> { +export function table(scanner: Scanner): ParseResult<Table> { scanner.nextUntilChar(); const header = tableHeader(scanner); if (!header.ok) return failure(); @@ -742,7 +773,7 @@ export function table(scanner: Scanner): ParseResult<BlockParseResultBody> { const b = block(scanner); return success({ type: "Table", - key: header.body, + keys: header.body, value: b.ok ? b.body.value : {}, }); } @@ -751,7 +782,7 @@ export const tableArrayHeader = surround("[[", dottedKey, "]]"); export function tableArray( scanner: Scanner, -): ParseResult<BlockParseResultBody> { +): ParseResult<TableArray> { scanner.nextUntilChar(); const header = tableArrayHeader(scanner); if (!header.ok) return failure(); @@ -759,7 +790,7 @@ export function tableArray( const b = block(scanner); return success({ type: "TableArray", - key: header.body, + keys: header.body, value: b.ok ? b.body.value : {}, }); } @@ -768,24 +799,8 @@ export function toml( scanner: Scanner, ): ParseResult<Record<string, unknown>> { const blocks = repeat(or([block, tableArray, table]))(scanner); - let body = {}; - if (!blocks.ok) return success(body); - for (const block of blocks.body) { - switch (block.type) { - case "Block": { - body = deepMerge(body, block.value); - break; - } - case "Table": { - deepAssignWithTable(body, block); - break; - } - case "TableArray": { - deepAssignWithTable(body, block); - break; - } - } - } + if (!blocks.ok) return success({}); + const body = blocks.body.reduce(deepAssign, {}); return success(body); } diff --git a/toml/parse_test.ts b/toml/parse_test.ts index a89f367956d7..2a4b904774f5 100644 --- a/toml/parse_test.ts +++ b/toml/parse_test.ts @@ -7,7 +7,7 @@ import { binary, boolean, dateTime, - deepAssignWithTable, + deepAssign, dottedKey, float, hex, @@ -242,7 +242,7 @@ fizz.buzz = true `.trim()), { type: "Table", - key: ["foo", "bar"], + keys: ["foo", "bar"], value: { baz: true, fizz: { @@ -253,7 +253,7 @@ fizz.buzz = true ); assertEquals(parse(`[only.header]`), { type: "Table", - key: ["only", "header"], + keys: ["only", "header"], value: {}, }); assertThrows(() => parse("")); @@ -473,7 +473,7 @@ Deno.test({ }); Deno.test({ - name: "parse() handles deepAssignWithTable", + name: "(private) deepAssign() works correctly", fn() { const source = { foo: { @@ -491,11 +491,11 @@ Deno.test({ }, }; - deepAssignWithTable( + deepAssign( source, { type: "Table", - key: ["foo", "items", "profile", "email", "x"], + keys: ["foo", "items", "profile", "email", "x"], value: { main: "mail@example.com" }, }, ); @@ -524,18 +524,18 @@ Deno.test({ }); Deno.test({ - name: "parse() handles deepAssignWithTable / TableArray", + name: "(private) deepAssign() handles Table and TableArray correctly", fn() { const source = { foo: {}, bar: null, }; - deepAssignWithTable( + deepAssign( source, { type: "TableArray", - key: ["foo", "items"], + keys: ["foo", "items"], value: { email: "mail@example.com" }, }, ); @@ -552,11 +552,11 @@ Deno.test({ bar: null, }, ); - deepAssignWithTable( + deepAssign( source, { type: "TableArray", - key: ["foo", "items"], + keys: ["foo", "items"], value: { email: "sub@example.com" }, }, ); @@ -579,11 +579,11 @@ Deno.test({ assertThrows( () => - deepAssignWithTable( + deepAssign( source, { type: "TableArray", - key: [], + keys: [], value: { email: "sub@example.com" }, }, ), @@ -593,11 +593,25 @@ Deno.test({ assertThrows( () => - deepAssignWithTable( + deepAssign( source, { type: "TableArray", - key: ["bar", "items"], + keys: ["bar", "items"], + value: { email: "mail@example.com" }, + }, + ), + Error, + "Unexpected assign", + ); + + assertThrows( + () => + deepAssign( + source, + { + type: "Table", + keys: ["bar", "items"], value: { email: "mail@example.com" }, }, ),