Skip to content

refactor(toml): rewrite deepAssignWithTable #6580

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 83 additions & 75 deletions toml/_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,18 @@ type ParseResult<T> = Success<T> | Failure;

type ParserComponent<T = unknown> = (scanner: Scanner) => ParseResult<T>;

type BlockParseResultBody = {
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>;
};
type Block = {
type: "Block";
value: Record<string, unknown>;
};

Expand Down Expand Up @@ -141,55 +143,81 @@ function failure(): Failure {
*
* e.g. `unflat(["a", "b", "c"], 1)` returns `{ a: { b: { c: 1 } } }`
*/
export function unflat(
export function unflat<T extends Record<string, unknown>>(
keys: string[],
values: unknown = {},
): Record<string, unknown> {
return keys.reduceRight(
(acc, key) => ({ [key]: acc }),
values,
) as Record<string, unknown>;
values: unknown,
): T {
return keys.reduceRight((acc, key) => ({ [key]: acc }), values) as T;
}

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<T extends Record<string, unknown>>(
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<T extends Record<string, unknown>>(
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<T extends Record<string, unknown>>(
target: T,
body: Block | Table | TableArray,
) {
switch (body.type) {
case "Block":
return deepMerge(target, body.value);
case "Table":
return deepAssignTable<T>(target, body);
case "TableArray":
return deepAssignTableArray<T>(target, body);
}
}

Expand Down Expand Up @@ -723,9 +751,7 @@ export const value = or([

export const pair = kv(dottedKey, "=", value);

export function block(
scanner: Scanner,
): ParseResult<BlockParseResultBody> {
export function block(scanner: Scanner): ParseResult<Block> {
scanner.nextUntilChar();
const result = merge(repeat(pair))(scanner);
if (result.ok) return success({ type: "Block", value: result.body });
Expand All @@ -734,32 +760,30 @@ 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();
scanner.nextUntilChar();
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<BlockParseResultBody> {
export function tableArray(scanner: Scanner): ParseResult<TableArray> {
scanner.nextUntilChar();
const header = tableArrayHeader(scanner);
if (!header.ok) return failure();
scanner.nextUntilChar();
const b = block(scanner);
return success({
type: "TableArray",
key: header.body,
keys: header.body,
value: b.ok ? b.body.value : {},
});
}
Expand All @@ -768,24 +792,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);
}

Expand Down
40 changes: 27 additions & 13 deletions toml/parse_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
binary,
boolean,
dateTime,
deepAssignWithTable,
deepAssign,
dottedKey,
float,
hex,
Expand Down Expand Up @@ -242,7 +242,7 @@ fizz.buzz = true
`.trim()),
{
type: "Table",
key: ["foo", "bar"],
keys: ["foo", "bar"],
value: {
baz: true,
fizz: {
Expand All @@ -253,7 +253,7 @@ fizz.buzz = true
);
assertEquals(parse(`[only.header]`), {
type: "Table",
key: ["only", "header"],
keys: ["only", "header"],
value: {},
});
assertThrows(() => parse(""));
Expand Down Expand Up @@ -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" },
},
);
Expand Down Expand Up @@ -531,11 +531,11 @@ Deno.test({
bar: null,
};

deepAssignWithTable(
deepAssign(
source,
{
type: "TableArray",
key: ["foo", "items"],
keys: ["foo", "items"],
value: { email: "mail@example.com" },
},
);
Expand All @@ -552,11 +552,11 @@ Deno.test({
bar: null,
},
);
deepAssignWithTable(
deepAssign(
source,
{
type: "TableArray",
key: ["foo", "items"],
keys: ["foo", "items"],
value: { email: "sub@example.com" },
},
);
Expand All @@ -579,11 +579,11 @@ Deno.test({

assertThrows(
() =>
deepAssignWithTable(
deepAssign(
source,
{
type: "TableArray",
key: [],
keys: [],
value: { email: "sub@example.com" },
},
),
Expand All @@ -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" },
},
),
Expand Down
Loading