import { TokenType } from './utils/constants' import { JsonPrimitive, JsonKey, JsonObject, JsonArray, JsonStruct } from './utils/types' 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 } }