From 16d0b60d88034ec6d3ed61be05fb3b0862d5a36b Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 15 Apr 2025 12:14:24 +0200 Subject: [PATCH 1/3] initial commit --- toml/_parser.ts | 165 ++++++++++++++++++++++++--------------------- toml/parse_test.ts | 26 +++---- 2 files changed, 102 insertions(+), 89 deletions(-) diff --git a/toml/_parser.ts b/toml/_parser.ts index 2013eaf3db3b..70e8877b698f 100644 --- a/toml/_parser.ts +++ b/toml/_parser.ts @@ -18,16 +18,18 @@ type ParseResult = Success | Failure; type ParserComponent = (scanner: Scanner) => ParseResult; -type BlockParseResultBody = { - type: "Block"; - value: Record; -} | { +type Table = { type: "Table"; - key: string[]; + keys: string[]; value: Record; -} | { +}; +type TableArray = { type: "TableArray"; - key: string[]; + keys: string[]; + value: Record; +}; +type Block = { + type: "Block"; value: Record; }; @@ -133,55 +135,81 @@ function failure(): Failure { * * e.g. `unflat(["a", "b", "c"], 1)` returns `{ a: { b: { c: 1 } } }` */ -export function unflat( +export function unflat>( keys: string[], - values: unknown = {}, -): Record { - return keys.reduceRight( - (acc, key) => ({ [key]: acc }), - values, - ) as Record; + values: unknown, +): T { + return keys.reduceRight((acc, key) => ({ [key]: acc }), values) as T; } -export function deepAssignWithTable(target: Record, table: { - type: "Table" | "TableArray"; - key: string[]; - value: Record; -}) { - if (table.key.length === 0 || table.key[0] == null) { +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function getTargetValue(target: Record, 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, { - type: table.type, - key: table.key.slice(1), - value: table.value, - }); - } else { - throw new Error("Unexpected assign"); +function deepAssignTable>( + target: T, + 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: T, + 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: T, + 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); } } @@ -189,8 +217,13 @@ export function deepAssignWithTable(target: Record, table: { // Parser combinators and generators // --------------------------------- -function or(parsers: ParserComponent[]): ParserComponent { - return (scanner: Scanner): ParseResult => { +// deno-lint-ignore no-explicit-any +function or[]>( + parsers: T, +): ParserComponent< + ReturnType extends ParseResult ? R : never +> { + return (scanner: Scanner) => { for (const parse of parsers) { const result = parse(scanner); if (result.ok) return result; @@ -731,9 +764,7 @@ export const value = or([ export const pair = kv(dottedKey, "=", value); -export function block( - scanner: Scanner, -): ParseResult { +export function block(scanner: Scanner): ParseResult { scanner.nextUntilChar(); const result = merge(repeat(pair))(scanner); if (result.ok) return success({ type: "Block", value: result.body }); @@ -742,7 +773,7 @@ export function block( export const tableHeader = surround("[", dottedKey, "]"); -export function table(scanner: Scanner): ParseResult { +export function table(scanner: Scanner): ParseResult { scanner.nextUntilChar(); const header = tableHeader(scanner); if (!header.ok) return failure(); @@ -750,16 +781,14 @@ export function table(scanner: Scanner): ParseResult { const b = block(scanner); return success({ type: "Table", - key: header.body, + keys: header.body, value: b.ok ? b.body.value : {}, }); } export const tableArrayHeader = surround("[[", dottedKey, "]]"); -export function tableArray( - scanner: Scanner, -): ParseResult { +export function tableArray(scanner: Scanner): ParseResult { scanner.nextUntilChar(); const header = tableArrayHeader(scanner); if (!header.ok) return failure(); @@ -767,7 +796,7 @@ export function tableArray( const b = block(scanner); return success({ type: "TableArray", - key: header.body, + keys: header.body, value: b.ok ? b.body.value : {}, }); } @@ -777,23 +806,7 @@ export function toml( ): ParseResult> { const blocks = repeat(or([block, tableArray, table]))(scanner); if (!blocks.ok) return failure(); - let 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; - } - } - } + const body = blocks.body.reduce(deepAssign, {}); return success(body); } diff --git a/toml/parse_test.ts b/toml/parse_test.ts index 8aea632c96bb..86c1e800b92b 100644 --- a/toml/parse_test.ts +++ b/toml/parse_test.ts @@ -5,7 +5,7 @@ import { bareKey, basicString, dateTime, - deepAssignWithTable, + deepAssign, dottedKey, float, inlineTable, @@ -213,7 +213,7 @@ fizz.buzz = true `.trim()), { type: "Table", - key: ["foo", "bar"], + keys: ["foo", "bar"], value: { baz: true, fizz: { @@ -224,7 +224,7 @@ fizz.buzz = true ); assertEquals(parse(`[only.header]`), { type: "Table", - key: ["only", "header"], + keys: ["only", "header"], value: {}, }); assertThrows(() => parse("")); @@ -410,11 +410,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" }, }, ); @@ -450,11 +450,11 @@ Deno.test({ bar: null, }; - deepAssignWithTable( + deepAssign( source, { type: "TableArray", - key: ["foo", "items"], + keys: ["foo", "items"], value: { email: "mail@example.com" }, }, ); @@ -471,11 +471,11 @@ Deno.test({ bar: null, }, ); - deepAssignWithTable( + deepAssign( source, { type: "TableArray", - key: ["foo", "items"], + keys: ["foo", "items"], value: { email: "sub@example.com" }, }, ); @@ -498,11 +498,11 @@ Deno.test({ assertThrows( () => - deepAssignWithTable( + deepAssign( source, { type: "TableArray", - key: [], + keys: [], value: { email: "sub@example.com" }, }, ), @@ -512,11 +512,11 @@ Deno.test({ assertThrows( () => - deepAssignWithTable( + deepAssign( source, { type: "TableArray", - key: ["bar", "items"], + keys: ["bar", "items"], value: { email: "mail@example.com" }, }, ), From 46ad2b0c068407276d8bf6f71c89ac35ff198fba Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 15 Apr 2025 12:32:24 +0200 Subject: [PATCH 2/3] add test --- toml/parse_test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/toml/parse_test.ts b/toml/parse_test.ts index 86c1e800b92b..66b0957639d7 100644 --- a/toml/parse_test.ts +++ b/toml/parse_test.ts @@ -523,6 +523,20 @@ Deno.test({ Error, "Unexpected assign", ); + + assertThrows( + () => + deepAssign( + source, + { + type: "Table", + keys: ["bar", "items"], + value: { email: "mail@example.com" }, + }, + ), + Error, + "Unexpected assign", + ); }, }); From 98f678f340bb12ef29833c0a056ed9126f9da58e Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 15 Apr 2025 12:49:09 +0200 Subject: [PATCH 3/3] update --- toml/_parser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/toml/_parser.ts b/toml/_parser.ts index 70e8877b698f..6d4ef563a4fa 100644 --- a/toml/_parser.ts +++ b/toml/_parser.ts @@ -221,7 +221,8 @@ export function deepAssign>( function or[]>( parsers: T, ): ParserComponent< - ReturnType extends ParseResult ? R : never + ReturnType extends ParseResult ? R + : Failure > { return (scanner: Scanner) => { for (const parse of parsers) {