import { TokenType } from './utils/constants.ts'; import { JsonPrimitive, JsonKey, JsonObject, JsonArray, JsonStruct, } from './utils/types.ts'; const { LEFT_BRACE, RIGHT_BRACE, LEFT_BRACKET, RIGHT_BRACKET, COLON, COMMA, TRUE, FALSE, NULL, STRING, NUMBER, SEPARATOR, } = TokenType; // Parser States enum TokenParserState { VALUE, KEY, COLON, COMMA, ENDED, ERROR, SEPARATOR, } // Parser Modes export enum TokenParserMode { OBJECT, ARRAY, } export interface StackElement { key: JsonKey; value: JsonStruct; mode: TokenParserMode | undefined; emit: boolean; } export interface TokenParserOptions { paths?: string[]; keepStack?: boolean; separator?: string; } const defaultOpts: TokenParserOptions = { paths: undefined, keepStack: true, separator: undefined, }; export class TokenParserError extends Error { constructor(message: string) { super(message); // Typescript is broken. This is a workaround Object.setPrototypeOf(this, TokenParserError.prototype); } } export default class TokenParser { private readonly paths?: (string[] | undefined)[]; private readonly keepStack: boolean; private readonly separator?: string; private state: TokenParserState = TokenParserState.VALUE; private mode: TokenParserMode | undefined = undefined; private key: JsonKey = undefined; private value: JsonPrimitive | JsonStruct | undefined = undefined; private stack: StackElement[] = []; constructor(opts?: TokenParserOptions) { opts = { ...defaultOpts, ...opts }; if (opts.paths) { this.paths = opts.paths.map((path) => { if (path === undefined || path === "$*") return undefined; if (!path.startsWith("$")) throw new TokenParserError( `Invalid selector "${path}". Should start with "$".` ); const pathParts = path.split(".").slice(1); if (pathParts.includes("")) throw new TokenParserError( `Invalid selector "${path}". ".." syntax not supported.` ); return pathParts; }); } this.keepStack = opts.keepStack as boolean; this.separator = opts.separator; } private shouldEmit(): boolean { if (!this.paths) return true; return this.paths.some((path) => { if (path === undefined) return true; if (path.length !== this.stack.length) return false; for (let i = 0; i < path.length - 1; i++) { const selector = path[i]; const key = this.stack[i + 1].key; if (selector === "*") continue; if (selector !== key) return false; } const selector = path[path.length - 1]; if (selector === "*") return true; return selector === this.key?.toString(); }); } private push(): void { this.stack.push({ key: this.key, value: this.value as JsonStruct, mode: this.mode, emit: this.shouldEmit(), }); } private pop(): void { const value = this.value; let emit; ({ key: this.key, value: this.value, mode: this.mode, emit, } = this.stack.pop() as StackElement); this.state = this.mode !== undefined ? TokenParserState.COMMA : TokenParserState.VALUE; this.emit(value as JsonPrimitive | JsonStruct, emit); } private emit(value: JsonPrimitive | JsonStruct, emit: boolean): void { if ( !this.keepStack && this.value && this.stack.every((item) => !item.emit) ) { // eslint-disable-next-line @typescript-eslint/no-explicit-any delete (this.value as JsonStruct as any)[this.key as string | number]; } if (emit) { this.onValue( value, // eslint-disable-next-line @typescript-eslint/no-explicit-any this.key as JsonKey as any, // eslint-disable-next-line @typescript-eslint/no-explicit-any this.value as JsonStruct | undefined as any, this.stack ); } if (this.stack.length === 0) { if (this.separator) { this.state = TokenParserState.SEPARATOR; } else if (this.separator === undefined) { this.end(); } // else if separator === '', expect next JSON object. } } public get isEnded(): boolean { return this.state === TokenParserState.ENDED; } public write(token: TokenType.LEFT_BRACE, value: "{"): void; public write(token: TokenType.RIGHT_BRACE, value: "}"): void; public write(token: TokenType.LEFT_BRACKET, value: "["): void; public write(token: TokenType.RIGHT_BRACKET, value: "]"): void; public write(token: TokenType.COLON, value: ":"): void; public write(token: TokenType.COMMA, value: ","): void; public write(token: TokenType.TRUE, value: true): void; public write(token: TokenType.FALSE, value: false): void; public write(token: TokenType.NULL, value: null): void; public write(token: TokenType.STRING, value: string): void; public write(token: TokenType.NUMBER, value: number): void; public write(token: TokenType.SEPARATOR, value: string): void; public write(token: TokenType, value: JsonPrimitive): void { if (this.state === TokenParserState.VALUE) { if ( token === STRING || token === NUMBER || token === TRUE || token === FALSE || token === NULL ) { if (this.mode === TokenParserMode.OBJECT) { (this.value as JsonObject)[this.key as string] = value; this.state = TokenParserState.COMMA; } else if (this.mode === TokenParserMode.ARRAY) { (this.value as JsonArray).push(value); this.state = TokenParserState.COMMA; } this.emit(value, this.shouldEmit()); return; } if (token === LEFT_BRACE) { this.push(); if (this.mode === TokenParserMode.OBJECT) { this.value = (this.value as JsonObject)[this.key as string] = {}; } else if (this.mode === TokenParserMode.ARRAY) { const val = {}; (this.value as JsonArray).push(val); this.value = val; } else { this.value = {}; } this.mode = TokenParserMode.OBJECT; this.state = TokenParserState.KEY; this.key = undefined; return; } if (token === LEFT_BRACKET) { this.push(); if (this.mode === TokenParserMode.OBJECT) { this.value = (this.value as JsonObject)[this.key as string] = []; } else if (this.mode === TokenParserMode.ARRAY) { const val: JsonArray = []; (this.value as JsonArray).push(val); this.value = val; } else { this.value = []; } this.mode = TokenParserMode.ARRAY; this.state = TokenParserState.VALUE; this.key = 0; return; } if ( this.mode === TokenParserMode.ARRAY && token === RIGHT_BRACKET && (this.value as JsonArray).length === 0 ) { this.pop(); return; } } if (this.state === TokenParserState.KEY) { if (token === STRING) { this.key = value as string; this.state = TokenParserState.COLON; return; } if ( token === RIGHT_BRACE && Object.keys(this.value as JsonObject).length === 0 ) { this.pop(); return; } } if (this.state === TokenParserState.COLON) { if (token === COLON) { this.state = TokenParserState.VALUE; return; } } if (this.state === TokenParserState.COMMA) { if (token === COMMA) { if (this.mode === TokenParserMode.ARRAY) { this.state = TokenParserState.VALUE; (this.key as number) += 1; return; } /* istanbul ignore else */ if (this.mode === TokenParserMode.OBJECT) { this.state = TokenParserState.KEY; return; } } if ( (token === RIGHT_BRACE && this.mode === TokenParserMode.OBJECT) || (token === RIGHT_BRACKET && this.mode === TokenParserMode.ARRAY) ) { this.pop(); return; } } if (this.state === TokenParserState.SEPARATOR) { if (token === SEPARATOR && value === this.separator) { this.state = TokenParserState.VALUE; return; } } this.error( new TokenParserError( `Unexpected ${TokenType[token]} (${JSON.stringify(value)}) in state ${ TokenParserState[this.state] }` ) ); } public error(err: Error): void { if (this.state !== TokenParserState.ENDED) { this.state = TokenParserState.ERROR; } this.onError(err); } public end(): void { if ( (this.state !== TokenParserState.VALUE && this.state !== TokenParserState.SEPARATOR) || this.stack.length > 0 ) { this.error( new Error( `Parser ended in mid-parsing (state: ${ TokenParserState[this.state] }). Either not all the data was received or the data was invalid.` ) ); } else { this.state = TokenParserState.ENDED; this.onEnd(); } } public onValue( value: JsonPrimitive | JsonStruct, key: number, parent: JsonArray, stack: StackElement[] ): void; public onValue( value: JsonPrimitive | JsonStruct, key: string, parent: JsonObject, stack: StackElement[] ): void; public onValue( value: JsonPrimitive | JsonStruct, key: undefined, parent: undefined, stack: [] ): void; public onValue( /* eslint-disable @typescript-eslint/no-unused-vars */ value: JsonPrimitive | JsonStruct, key: JsonKey | undefined, parent: JsonStruct | undefined, stack: StackElement[] /* eslint-enable @typescript-eslint/no-unused-vars */ ): void { // Override me throw new TokenParserError( 'Can\'t emit data before the "onValue" callback has been set up.' ); } public onError(err: Error): void { // Override me throw err; } public onEnd(): void { // Override me } }