Skip to content

Commit

Permalink
Modify path to take an array
Browse files Browse the repository at this point in the history
  • Loading branch information
juanjoDiaz committed Nov 11, 2020
1 parent dc068c1 commit 792fe29
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 88 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,12 @@ The available options are:

```javascript
{
path: <string>,
paths: <string[]>,
keepStack: <boolean>, // whether to keep all the properties in the stack
}
```

* path: Paths to emit. Defaults to `undefined` which emits everything. The `path` option is intended to suppot jsonpath although at the time being it only supports the root object selector (`$`) and subproperties selectors including wildcards (`$.a`, `$.*`, `$.a.b`, , `$.*.b`, etc).
* paths: Array of paths to emit. Defaults to `undefined` which emits everything. The paths are intended to suppot jsonpath although at the time being it only supports the root object selector (`$`) and subproperties selectors including wildcards (`$.a`, `$.*`, `$.a.b`, , `$.*.b`, etc).
* keepStack: Whether to keep full objects on the stack even if they won't be emitted. Defaults to `true`. When set to `false` the it does preserve properties in the parent object some ancestor will be emitted. This means that the parent object passed to the `onValue` function will be empty, which doesn't reflect the truth, but it's more memory-efficient.

#### Methods
Expand Down Expand Up @@ -201,7 +201,7 @@ You can subscribe to the resulting data using the
```javascript
import { JsonParser } from '@streamparser/json';

const parser = new JsonParser({ stringBufferSize: undefined, path: '$' });
const parser = new JsonParser({ stringBufferSize: undefined, paths: ['$'] });
parser.onValue = console.log;

parser.write('"Hello world!"'); // logs "Hello world!"
Expand Down Expand Up @@ -262,7 +262,7 @@ Imagine an endpoint that send a large amount of JSON objects one after the other
```js
import { JsonParser } from '@streamparser/json';

const jsonparser = new JsonParser({ stringBufferSize: undefined, path: '$.*' });
const jsonparser = new JsonParser({ stringBufferSize: undefined, paths: ['$.*'] });
parser.onValue = (value, key, parent, stack) => {
if (stack.length === 0) /* We are done. Exit. */;
// By default, the parser keeps all the child elements in memory until the root parent is emitted.
Expand Down
8 changes: 4 additions & 4 deletions dist/deno/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,12 @@ The available options are:

```javascript
{
path: <string>,
paths: <string[]>,
keepStack: <boolean>, // whether to keep all the properties in the stack
}
```

* path: Paths to emit. Defaults to `undefined` which emits everything. The `path` option is intended to suppot jsonpath although at the time being it only supports the root object selector (`$`) and subproperties selectors including wildcards (`$.a`, `$.*`, `$.a.b`, , `$.*.b`, etc).
* paths: Array of paths to emit. Defaults to `undefined` which emits everything. The paths are intended to suppot jsonpath although at the time being it only supports the root object selector (`$`) and subproperties selectors including wildcards (`$.a`, `$.*`, `$.a.b`, , `$.*.b`, etc).
* keepStack: Whether to keep full objects on the stack even if they won't be emitted. Defaults to `true`. When set to `false` the it does preserve properties in the parent object some ancestor will be emitted. This means that the parent object passed to the `onValue` function will be empty, which doesn't reflect the truth, but it's more memory-efficient.

#### Methods
Expand Down Expand Up @@ -201,7 +201,7 @@ You can subscribe to the resulting data using the
```javascript
import { JsonParser } from '@streamparser/json';

const parser = new JsonParser({ stringBufferSize: undefined, path: '$' });
const parser = new JsonParser({ stringBufferSize: undefined, paths: ['$'] });
parser.onValue = console.log;

parser.write('"Hello world!"'); // logs "Hello world!"
Expand Down Expand Up @@ -262,7 +262,7 @@ Imagine an endpoint that send a large amount of JSON objects one after the other
```js
import { JsonParser } from '@streamparser/json';

const jsonparser = new JsonParser({ stringBufferSize: undefined, path: '$.*' });
const jsonparser = new JsonParser({ stringBufferSize: undefined, paths: ['$.*'] });
parser.onValue = (value, key, parent, stack) => {
if (stack.length === 0) /* We are done. Exit. */;
// By default, the parser keeps all the child elements in memory until the root parent is emitted.
Expand Down
4 changes: 2 additions & 2 deletions dist/deno/jsonparse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ export default class JSONParser {
this.parser.onValue = cb;
}

public end() {
this.tokenizer.end();
public end(): void {
this.parser.end();
this.tokenizer.end();
}
}
62 changes: 37 additions & 25 deletions dist/deno/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,25 @@ export interface StackElement {
}

export interface ParserOptions {
path?: string;
paths?: string[];
keepStack?: boolean;
}

const defaultOpts: ParserOptions = {
path: undefined,
paths: undefined,
keepStack: true,
};

export class TokenParserError extends Error {}
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 Parser {
private readonly path?: string[];
private readonly paths?: (string[] | undefined)[];
private readonly keepStack: boolean;
private state: ParserState = ParserState.VALUE;
private mode: ParserMode | undefined = undefined;
Expand All @@ -60,31 +66,38 @@ export default class Parser {
constructor(opts?: ParserOptions) {
opts = { ...defaultOpts, ...opts };

if (opts.path === undefined || opts.path === '$*') {
this.path = undefined;
} else {
if (!opts.path.startsWith('$')) throw new TokenParserError(`Invalid selector "${opts.path}". Should start with "$".`);
this.path = opts.path.split('.').slice(1);
if (this.path.includes('')) throw new TokenParserError(`Invalid selector "${opts.path}". ".." syntax not supported.`);
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;
}

private shouldEmit(): boolean {
if (this.path === undefined) return true;
if (this.path.length !== this.stack.length) return false;

for (let i = 0; i < this.path.length - 1; i++) {
const selector = this.path[i];
const key = this.stack[i + 1].key;
if (selector === '*') continue;
if (selector !== key) return false;
}
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 = this.path[this.path.length - 1];
if (selector === '*') return true;
return selector === this.key?.toString();
const selector = path[path.length - 1];
if (selector === '*') return true;
return selector === this.key?.toString();
}) ;
}

private push(): void {
Expand Down Expand Up @@ -221,15 +234,14 @@ export default class Parser {
}

this.error(new TokenParserError(`Unexpected ${TokenType[token]} (${JSON.stringify(value)}) in state ${ParserState[this.state]}`));
return;
}

public error(err: Error) {
public error(err: Error): never {
this.state = ParserState.ERROR;
throw err;
}

public end() {
public end(): void {
if (this.state !== ParserState.VALUE || this.stack.length > 0) {
this.error(new TokenParserError(`Parser ended in mid-parsing (state: ${ParserState[this.state]}). Either not all the data was received or the data was invalid.`));
}
Expand Down
17 changes: 9 additions & 8 deletions dist/deno/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ const defaultOpts: TokenizerOptions = {
numberBufferSize: 0,
};

export class TokenizerError extends Error {}
export class TokenizerError extends Error {
constructor(message: string) {
super(message);
// Typescript is broken. This is a workaround
Object.setPrototypeOf(this, TokenizerError.prototype);
}
}

export default class Tokenizer {
private state = TokenizerStates.START;
Expand Down Expand Up @@ -99,7 +105,6 @@ export default class Tokenizer {
buffer = Uint8Array.from(input);
} else {
this.error(new TypeError("Unexpected type. The `write` function only accepts TypeArrays and Strings.",));
return;
}

for (var i = 0; i < buffer.length; i += 1) {
Expand Down Expand Up @@ -493,16 +498,13 @@ export default class Tokenizer {
continue;
}
break;
case TokenizerStates.ERROR:;
return;
}

this.error(new TokenizerError(
`Unexpected "${String.fromCharCode(n)}" at position "${i}" in state ${
TokenizerStates[this.state]
}`
));
return;
}
}

Expand All @@ -519,19 +521,18 @@ export default class Tokenizer {
return Number(numberStr);
}

public error(err: Error) {
public error(err: Error): never {
this.state = TokenizerStates.ERROR;
throw err;
}

public end() {
public end(): void {
if (this.state !== TokenizerStates.START) {
this.error(new TokenizerError(
`Tokenizer ended in the middle of a token (state: ${
TokenizerStates[this.state]
}). Either not all the data was received or the data was invalid.`
));
return;
}

this.state = TokenizerStates.ENDED;
Expand Down
49 changes: 28 additions & 21 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ export interface StackElement {
}

export interface ParserOptions {
path?: string;
paths?: string[];
keepStack?: boolean;
}

const defaultOpts: ParserOptions = {
path: undefined,
paths: undefined,
keepStack: true,
};

Expand All @@ -55,7 +55,7 @@ export class TokenParserError extends Error {
}

export default class Parser {
private readonly path?: string[];
private readonly paths?: (string[] | undefined)[];
private readonly keepStack: boolean;
private state: ParserState = ParserState.VALUE;
private mode: ParserMode | undefined = undefined;
Expand All @@ -66,31 +66,38 @@ export default class Parser {
constructor(opts?: ParserOptions) {
opts = { ...defaultOpts, ...opts };

if (opts.path === undefined || opts.path === '$*') {
this.path = undefined;
} else {
if (!opts.path.startsWith('$')) throw new TokenParserError(`Invalid selector "${opts.path}". Should start with "$".`);
this.path = opts.path.split('.').slice(1);
if (this.path.includes('')) throw new TokenParserError(`Invalid selector "${opts.path}". ".." syntax not supported.`);
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;
}

private shouldEmit(): boolean {
if (this.path === undefined) return true;
if (this.path.length !== this.stack.length) return false;

for (let i = 0; i < this.path.length - 1; i++) {
const selector = this.path[i];
const key = this.stack[i + 1].key;
if (selector === '*') continue;
if (selector !== key) return false;
}
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 = this.path[this.path.length - 1];
if (selector === '*') return true;
return selector === this.key?.toString();
const selector = path[path.length - 1];
if (selector === '*') return true;
return selector === this.key?.toString();
}) ;
}

private push(): void {
Expand Down
12 changes: 6 additions & 6 deletions test/keepStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ import JsonParser from "../src/jsonparse";
const { test } = tap;

const testData = [
{ value: '{ "a": { "b": 1, "c": 2, "d": 3, "e": 4 } }', path: "$", expected: 1 },
{ value: '{ "a": { "b": 1, "c": 2, "d": 3, "e": 4 } }', path: "$.a.*", expected: 4 },
{ value: '{ "a": { "b": 1, "c": 2, "d": 3, "e": 4 } }', path: "$.a.e", expected: 1 },
{ value: '{ "a": { "b": [1,2,3,4,5,6] } }', path: "$.a.b.*", expected: 6 },
{ value: '{ "a": { "b": 1, "c": 2, "d": 3, "e": 4 } }', paths: ["$"], expected: 1 },
{ value: '{ "a": { "b": 1, "c": 2, "d": 3, "e": 4 } }', paths: ["$.a.*"], expected: 4 },
{ value: '{ "a": { "b": 1, "c": 2, "d": 3, "e": 4 } }', paths: ["$.a.e"], expected: 1 },
{ value: '{ "a": { "b": [1,2,3,4,5,6] } }', paths: ["$.a.b.*"], expected: 6 },
];

testData.forEach(({ value, path, expected }) => {
testData.forEach(({ value, paths, expected }) => {
test(`should keep parent empty if keepStack === false`, {}, (t) => {
t.plan(expected);

const p = new JsonParser({ path, keepStack: false });
const p = new JsonParser({ paths, keepStack: false });
p.onValue = (value, key, parent) => {
if (parent === undefined) {
t.pass();
Expand Down
Loading

0 comments on commit 792fe29

Please sign in to comment.