Skip to content

Better TOML types #6656

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 4 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
76 changes: 40 additions & 36 deletions toml/_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// This module is browser compatible.

import { deepMerge } from "@std/collections/deep-merge";
import { TOMLArray, TOMLTable, TOMLValue } from "./types";

// ---------------------------
// Interfaces and base classes
Expand All @@ -16,27 +17,27 @@ interface Failure {
}
type ParseResult<T> = Success<T> | Failure;

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

type Block = {
type: "Block";
value: Record<string, unknown>;
value: TOMLTable;
};
type Table = {
type: "Table";
keys: string[];
value: Record<string, unknown>;
value: TOMLTable;
};
type TableArray = {
type: "TableArray";
keys: string[];
value: Record<string, unknown>;
value: TOMLTable;
};

export class Scanner {
#whitespace = /[ \t]/;
readonly #whitespace = /[ \t]/;
readonly #source: string;
#position = 0;
#source: string;

constructor(source: string) {
this.#source = source;
Expand Down Expand Up @@ -131,18 +132,21 @@ export class Scanner {
// Utilities
// -----------------------

function success<T>(body: T): Success<T> {
function success<T extends TOMLValue|void>(body: T): Success<T> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why |void part necessary?

return { ok: true, body };
}
function failure(): Failure {
return { ok: false };
}

type DeepStringRecord<T> = {[Key: string]: DeepStringRecord<T>|T};
/**
* Creates a nested object from the keys and values.
*
* e.g. `unflat(["a", "b", "c"], 1)` returns `{ a: { b: { c: 1 } } }`
*/
export function unflat<T>(keys: string[], values: T): DeepStringRecord<T>;
export function unflat(keys: string[]): Record<string, unknown>;
export function unflat(
keys: string[],
values: unknown = {},
Expand All @@ -157,7 +161,7 @@ function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

function getTargetValue(target: Record<string, unknown>, keys: string[]) {
function getTargetValue<T>(target: Record<string, T>, keys: string[]) {
const key = keys[0];
if (!key) {
throw new Error(
Expand All @@ -167,8 +171,8 @@ function getTargetValue(target: Record<string, unknown>, keys: string[]) {
return target[key];
}

function deepAssignTable(
target: Record<string, unknown>,
function deepAssignTable<T extends Record<string, unknown>>(
target: T,
table: Table,
) {
const { keys, type, value } = table;
Expand All @@ -189,8 +193,8 @@ function deepAssignTable(
throw new Error("Unexpected assign");
}

function deepAssignTableArray(
target: Record<string, unknown>,
function deepAssignTableArray<T extends Record<string, unknown>>(
target: T,
table: TableArray,
) {
const { type, keys, value } = table;
Expand Down Expand Up @@ -229,25 +233,25 @@ export function deepAssign(
// ---------------------------------

// deno-lint-ignore no-explicit-any
function or<T extends readonly ParserComponent<any>[]>(
function or<T extends readonly ParserComponent<TOMLValue>[]>(
parsers: T,
): ParserComponent<
ReturnType<T[number]> extends ParseResult<infer R> ? R : Failure
> {
return (scanner: Scanner) => {
return ((scanner: Scanner) => {
for (const parse of parsers) {
const result = parse(scanner);
if (result.ok) return result;
}
return failure();
};
}) as unknown as any;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs // deno-lint-ignore no-explicit-any directive before this line to pass lint check. See the deno lint output

}

/** Join the parse results of the given parser into an array.
*
* If the parser fails at the first attempt, it will return an empty array.
*/
function join<T>(
function join<T extends TOMLValue>(
parser: ParserComponent<T>,
separator: string,
): ParserComponent<T[]> {
Expand All @@ -273,7 +277,7 @@ function join<T>(
*
* This requires the parser to succeed at least once.
*/
function join1<T>(
function join1<T extends TOMLValue>(
parser: ParserComponent<T>,
separator: string,
): ParserComponent<T[]> {
Expand All @@ -294,13 +298,13 @@ function join1<T>(
};
}

function kv<T>(
function kv<T extends TOMLValue>(
keyParser: ParserComponent<string[]>,
separator: string,
valueParser: ParserComponent<T>,
): ParserComponent<{ [key: string]: unknown }> {
): ParserComponent<TOMLTable> {
const Separator = character(separator);
return (scanner: Scanner): ParseResult<{ [key: string]: unknown }> => {
return (scanner: Scanner): ParseResult<TOMLTable> => {
const position = scanner.position;
const key = keyParser(scanner);
if (!key.ok) return failure();
Expand All @@ -322,12 +326,12 @@ function kv<T>(
}

function merge(
parser: ParserComponent<unknown[]>,
): ParserComponent<Record<string, unknown>> {
return (scanner: Scanner): ParseResult<Record<string, unknown>> => {
parser: ParserComponent<TOMLValue[]>,
): ParserComponent<TOMLTable> {
return (scanner: Scanner): ParseResult<TOMLTable> => {
const result = parser(scanner);
if (!result.ok) return failure();
let body = {};
let body: TOMLTable = {};
for (const record of result.body) {
if (typeof record === "object" && record !== null) {
body = deepMerge(body, record);
Expand All @@ -337,7 +341,7 @@ function merge(
};
}

function repeat<T>(
function repeat<T extends TOMLValue>(
parser: ParserComponent<T>,
): ParserComponent<T[]> {
return (scanner: Scanner) => {
Expand All @@ -353,7 +357,7 @@ function repeat<T>(
};
}

function surround<T>(
function surround<T extends TOMLValue>(
left: string,
parser: ParserComponent<T>,
right: string,
Expand Down Expand Up @@ -446,7 +450,7 @@ export function basicString(scanner: Scanner): ParseResult<string> {
scanner.skipWhitespaces();
if (scanner.char() !== '"') return failure();
scanner.next();
const acc = [];
const acc: string[] = [];
while (scanner.char() !== '"' && !scanner.eof()) {
if (scanner.char() === "\n") {
throw new SyntaxError("Single-line string cannot contain EOL");
Expand Down Expand Up @@ -691,13 +695,13 @@ export function localTime(scanner: Scanner): ParseResult<string> {
return success(match);
}

export function arrayValue(scanner: Scanner): ParseResult<unknown[]> {
export function arrayValue(scanner: Scanner): ParseResult<TOMLArray> {
scanner.skipWhitespaces();

if (scanner.char() !== "[") return failure();
scanner.next();

const array: unknown[] = [];
const array: TOMLValue[] = [];
while (!scanner.eof()) {
scanner.nextUntilChar();
const result = value(scanner);
Expand All @@ -718,7 +722,7 @@ export function arrayValue(scanner: Scanner): ParseResult<unknown[]> {

export function inlineTable(
scanner: Scanner,
): ParseResult<Record<string, unknown>> {
): ParseResult<TOMLTable> {
scanner.nextUntilChar();
if (scanner.char(1) === "}") {
scanner.next(2);
Expand Down Expand Up @@ -759,7 +763,7 @@ export function block(
): ParseResult<Block> {
scanner.nextUntilChar();
const result = merge(repeat(pair))(scanner);
if (result.ok) return success({ type: "Block", value: result.body });
if (result.ok) return success({ type: "Block" as const, value: result.body });
return failure();
}

Expand All @@ -772,7 +776,7 @@ export function table(scanner: Scanner): ParseResult<Table> {
scanner.nextUntilChar();
const b = block(scanner);
return success({
type: "Table",
type: "Table" as const,
keys: header.body,
value: b.ok ? b.body.value : {},
});
Expand All @@ -789,18 +793,18 @@ export function tableArray(
scanner.nextUntilChar();
const b = block(scanner);
return success({
type: "TableArray",
type: "TableArray" as const,
keys: header.body,
value: b.ok ? b.body.value : {},
});
}

export function toml(
scanner: Scanner,
): ParseResult<Record<string, unknown>> {
): ParseResult<TOMLTable> {
const blocks = repeat(or([block, tableArray, table]))(scanner);
if (!blocks.ok) return success({});
const body = blocks.body.reduce(deepAssign, {});
const body = blocks.body.reduce(deepAssign, {}) as TOMLTable;
return success(body);
}

Expand All @@ -812,7 +816,7 @@ function createParseErrorMessage(scanner: Scanner, message: string) {
return `Parse error on line ${row}, column ${column}: ${message}`;
}

export function parserFactory<T>(parser: ParserComponent<T>) {
export function parserFactory<T extends TOMLValue>(parser: ParserComponent<T>) {
return (tomlString: string): T => {
const scanner = new Scanner(tomlString);
try {
Expand Down
4 changes: 4 additions & 0 deletions toml/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type TOMLTable = {[Key in string]: TOMLValue} & {[Key in string]: TOMLValue|undefined};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd prefer the capitalization of TomlTable. See https://docs.deno.com/runtime/contributing/style_guide/#naming-convention

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second part {[Key in string]: TOMLValue|undefined} looks strange to me. Is it possible for key to be undefined? Is that spec compliant?

export type TOMLArray = TOMLValue[] | readonly TOMLValue[];
export type TOMLPrimitive = string | number | boolean | Date;
export type TOMLValue = TOMLPrimitive | TOMLArray | TOMLTable;
Loading