Skip to content
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

feat: add support for json path syntax #54

Merged
merged 29 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
63f4953
feat: add support for json path syntax
koladilip May 17, 2024
4b2c463
chore: update read me for json paths
koladilip May 17, 2024
ffe4f14
fix: array all filters syntax tree
koladilip May 20, 2024
3b90cc9
chore: add json path tests
koladilip May 22, 2024
a2af49f
refactor: use path type stack to handle child paths
koladilip May 22, 2024
e6309c1
feat: add flat to object mapping converter
koladilip May 24, 2024
312cad1
refactor: static utils to modules
koladilip May 24, 2024
f864b2f
feat: add support for regexp
koladilip May 25, 2024
7789cae
feat: add anyof noneof operators
koladilip May 25, 2024
17241c3
feat: add support for json path functions
koladilip May 26, 2024
89a7388
refactor: update mappings test case
koladilip May 27, 2024
078895a
refactor: address pr comments
koladilip May 27, 2024
f6b2fab
fix: typos
koladilip May 27, 2024
b1cc70d
fix: parse mappings paths
koladilip May 28, 2024
dab28b3
fix: multiple indexes in json paths
koladilip May 28, 2024
7bc0733
fix: unused import
koladilip May 28, 2024
2ff4c0a
refactor: convertToObjectMapping
koladilip May 28, 2024
aeb5912
fix: imports
koladilip May 28, 2024
e7b37fa
refactor: comparisons tests
koladilip May 29, 2024
a9e591a
feat: add reverse translator
koladilip May 30, 2024
71f3b06
fix: formatting issue reverse translator
koladilip May 30, 2024
6b5269e
refactor: add error handling in flat mapping convertor
koladilip Jun 1, 2024
9fa7b50
fix: error handling in convertor
koladilip Jun 3, 2024
644f7aa
fix: typo in file name
koladilip Jun 3, 2024
0625be4
refactor: address pr comments on template parsing
koladilip Jun 3, 2024
9ca6100
fix: sonar lint issues
koladilip Jun 3, 2024
e386ebb
fix: eslint issue
koladilip Jun 3, 2024
71e11c0
refactor: mappings convertor
koladilip Jun 3, 2024
84aaabf
feat: add util function convert mappings to template
koladilip Jun 4, 2024
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,6 @@ build/
.stryker-tmp

Mac
.DS_Store
.DS_Store

saikumarrs marked this conversation as resolved.
Show resolved Hide resolved
test/test_engine.ts
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.19.0
18.20.3
8 changes: 8 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ If we use this rich path`~r a.b.c` then it automatically handles following varia
- `{"a": [{ "b": [{"c": 2}]}]}`
Refer this [example](test/scenarios/paths/rich_path.jt) for more details.

#### Json Paths
We support some features of [JSON Path](https://goessner.net/articles/JsonPath/index.html#) syntax using path option (`~j`).
Note: This is an experimental feature and may not support all the features of JSON Paths.

Refer this [example](test/scenarios/paths/json_path.jt) for more details.

#### Simple selectors

```js
Expand Down Expand Up @@ -333,6 +339,8 @@ We can override the default path option using tags.
~s a.b.c
// Use ~r to treat a.b.c as rich path
~r a.b.c
// Use ~j for using json paths
~j items[?(@.a>1)]
```

**Note:** Rich paths are slower compare to the simple paths.
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { SyntaxType } from './types';

export const VARS_PREFIX = '___';
export const DATA_PARAM_KEY = '___d';
export const BINDINGS_PARAM_KEY = '___b';
export const BINDINGS_CONTEXT_KEY = '___b.context.';
export const RESULT_KEY = '___r';
export const FUNCTION_RESULT_KEY = '___f';
export const EMPTY_EXPR = { type: SyntaxType.EMPTY };
81 changes: 50 additions & 31 deletions src/engine.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
/* eslint-disable import/no-cycle */
import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY } from './constants';
import { JsonTemplateLexer } from './lexer';
import { JsonTemplateParser } from './parser';
import { JsonTemplateReverseTranslator } from './reverse_translator';
import { JsonTemplateTranslator } from './translator';
import { EngineOptions, Expression } from './types';
import { CommonUtils } from './utils';
import { EngineOptions, Expression, FlatMappingPaths, TemplateInput } from './types';
import { CreateAsyncFunction, convertToObjectMapping, isExpression } from './utils';

export class JsonTemplateEngine {
private readonly fn: Function;
Copy link

Choose a reason for hiding this comment

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

Replace Function type with more specific function types to enhance type safety.

- private readonly fn: Function;
- private constructor(fn: Function) {
+ private readonly fn: (...args: any[]) => any;
+ private constructor(fn: (...args: any[]) => any) {

Also applies to: 13-13, 20-20, 25-25

Committable suggestion was skipped due low confidence.

Tools
Biome

[error] 11-11: Don't use 'Function' as a type.

Expand All @@ -12,17 +14,6 @@ export class JsonTemplateEngine {
this.fn = fn;
}

static create(templateOrExpr: string | Expression, options?: EngineOptions): JsonTemplateEngine {
return new JsonTemplateEngine(this.compileAsAsync(templateOrExpr, options));
}

static createAsSync(
templateOrExpr: string | Expression,
options?: EngineOptions,
): JsonTemplateEngine {
return new JsonTemplateEngine(this.compileAsSync(templateOrExpr, options));
}

private static compileAsSync(
templateOrExpr: string | Expression,
options?: EngineOptions,
Expand All @@ -31,40 +22,68 @@ export class JsonTemplateEngine {
return Function(DATA_PARAM_KEY, BINDINGS_PARAM_KEY, this.translate(templateOrExpr, options));
Copy link

Choose a reason for hiding this comment

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

Replace this with JsonTemplateEngine and Function type with a more specific function type in static methods to enhance clarity and type safety.

- return Function(DATA_PARAM_KEY, BINDINGS_PARAM_KEY, this.translate(templateOrExpr, options));
+ return (data: typeof DATA_PARAM_KEY, bindings: typeof BINDINGS_PARAM_KEY) => JsonTemplateEngine.translate(templateOrExpr, options);

Also applies to: 11-11

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
return Function(DATA_PARAM_KEY, BINDINGS_PARAM_KEY, this.translate(templateOrExpr, options));
return (data: typeof DATA_PARAM_KEY, bindings: typeof BINDINGS_PARAM_KEY) => JsonTemplateEngine.translate(templateOrExpr, options);
Tools
Biome

[error] 22-22: Using this in a static context can be confusing.

this refers to the class.
Unsafe fix: Use the class name instead.

}

private static compileAsAsync(
templateOrExpr: string | Expression,
options?: EngineOptions,
): Function {
return CommonUtils.CreateAsyncFunction(
private static compileAsAsync(templateOrExpr: TemplateInput, options?: EngineOptions): Function {
return CreateAsyncFunction(
DATA_PARAM_KEY,
BINDINGS_PARAM_KEY,
this.translate(templateOrExpr, options),
);
}

static parse(template: string, options?: EngineOptions): Expression {
const lexer = new JsonTemplateLexer(template);
const parser = new JsonTemplateParser(lexer, options);
return parser.parse();
private static translateExpression(expr: Expression): string {
const translator = new JsonTemplateTranslator(expr);
return translator.translate();
}

static translate(templateOrExpr: string | Expression, options?: EngineOptions): string {
if (typeof templateOrExpr === 'string') {
return this.translateTemplate(templateOrExpr, options);
private static parseMappingPaths(
mappings: FlatMappingPaths[],
options?: EngineOptions,
): Expression {
const flatMappingAST = mappings.map((mapping) => ({
...mapping,
inputExpr: JsonTemplateEngine.parse(mapping.input, options).statements[0],
outputExpr: JsonTemplateEngine.parse(mapping.output, options).statements[0],
}));
return convertToObjectMapping(flatMappingAST);
}
Comment on lines +38 to +48
Copy link

Choose a reason for hiding this comment

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

Consider refactoring parseMappingPaths to improve readability by breaking down complex expressions.

+ const flatMappingAST = mappings.map((mapping) => {
+   const inputExpr = JsonTemplateEngine.parse(mapping.input, options).statements[0];
+   const outputExpr = JsonTemplateEngine.parse(mapping.output, options).statements[0];
+   return { ...mapping, inputExpr, outputExpr };
+ });
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
private static parseMappingPaths(
mappings: FlatMappingPaths[],
options?: EngineOptions,
): Expression {
const flatMappingAST = mappings.map((mapping) => ({
...mapping,
inputExpr: JsonTemplateEngine.parse(mapping.input, options).statements[0],
outputExpr: JsonTemplateEngine.parse(mapping.output, options).statements[0],
}));
return convertToObjectMapping(flatMappingAST);
}
private static parseMappingPaths(
mappings: FlatMappingPaths[],
options?: EngineOptions,
): Expression {
const flatMappingAST = mappings.map((mapping) => {
const inputExpr = JsonTemplateEngine.parse(mapping.input, options).statements[0];
const outputExpr = JsonTemplateEngine.parse(mapping.output, options).statements[0];
return { ...mapping, inputExpr, outputExpr };
});
return convertToObjectMapping(flatMappingAST);
}


static create(templateOrExpr: TemplateInput, options?: EngineOptions): JsonTemplateEngine {
return new JsonTemplateEngine(this.compileAsAsync(templateOrExpr, options));
}

static createAsSync(
templateOrExpr: string | Expression,
options?: EngineOptions,
): JsonTemplateEngine {
return new JsonTemplateEngine(this.compileAsSync(templateOrExpr, options));
}

static parse(template: TemplateInput, options?: EngineOptions): Expression {
if (isExpression(template)) {
return template as Expression;
}
return this.translateExpression(templateOrExpr);
if (typeof template === 'string') {
const lexer = new JsonTemplateLexer(template);
const parser = new JsonTemplateParser(lexer, options);
return parser.parse();
}
return this.parseMappingPaths(template as FlatMappingPaths[], options);
}

private static translateTemplate(template: string, options?: EngineOptions): string {
static translate(template: TemplateInput, options?: EngineOptions): string {
return this.translateExpression(this.parse(template, options));
}

private static translateExpression(expr: Expression): string {
const translator = new JsonTemplateTranslator(expr);
return translator.translate();
static reverseTranslate(expr: Expression, options?: EngineOptions): string {
const translator = new JsonTemplateReverseTranslator(options);
return translator.translate(expr);
}

static convertMappingsToTemplate(mappings: FlatMappingPaths[], options?: EngineOptions): string {
return this.reverseTranslate(this.parseMappingPaths(mappings, options), options);
}

evaluate(data: any, bindings: Record<string, any> = {}): any {
evaluate(data: unknown, bindings: Record<string, unknown> = {}): unknown {
return this.fn(data ?? {}, bindings);
}
}
108 changes: 98 additions & 10 deletions src/lexer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BINDINGS_PARAM_KEY, VARS_PREFIX } from './constants';
import { VARS_PREFIX } from './constants';
import { JsonTemplateLexerError } from './errors';
import { Keyword, Token, TokenType } from './types';

Expand Down Expand Up @@ -81,8 +81,12 @@ export class JsonTemplateLexer {
return this.match('~r');
}

matchJsonPath(): boolean {
return this.match('~j');
}

matchPathType(): boolean {
return this.matchRichPath() || this.matchSimplePath();
return this.matchRichPath() || this.matchJsonPath() || this.matchSimplePath();
}

matchPath(): boolean {
Expand Down Expand Up @@ -113,7 +117,7 @@ export class JsonTemplateLexer {
const token = this.lookahead();
if (token.type === TokenType.PUNCT) {
const { value } = token;
return value === '.' || value === '..' || value === '^';
return value === '.' || value === '..' || value === '^' || value === '$' || value === '@';
}

return false;
Expand Down Expand Up @@ -145,10 +149,38 @@ export class JsonTemplateLexer {
return token.type === TokenType.KEYWORD && token.value === val;
}

matchContains(): boolean {
return this.matchKeywordValue(Keyword.CONTAINS);
}

matchEmpty(): boolean {
return this.matchKeywordValue(Keyword.EMPTY);
}

matchSize(): boolean {
return this.matchKeywordValue(Keyword.SIZE);
}

matchSubsetOf(): boolean {
return this.matchKeywordValue(Keyword.SUBSETOF);
}

matchAnyOf(): boolean {
return this.matchKeywordValue(Keyword.ANYOF);
}

matchNoneOf(): boolean {
return this.matchKeywordValue(Keyword.NONEOF);
}

matchIN(): boolean {
return this.matchKeywordValue(Keyword.IN);
}

matchNotIN(): boolean {
return this.matchKeywordValue(Keyword.NOT_IN);
}

matchFunction(): boolean {
return this.matchKeywordValue(Keyword.FUNCTION);
}
Expand Down Expand Up @@ -259,7 +291,12 @@ export class JsonTemplateLexer {
};
}

const token = this.scanPunctuator() ?? this.scanID() ?? this.scanString() ?? this.scanInteger();
const token =
this.scanRegularExpressions() ??
this.scanPunctuator() ??
this.scanID() ??
this.scanString() ??
this.scanInteger();
if (token) {
return token;
}
Expand Down Expand Up @@ -294,7 +331,8 @@ export class JsonTemplateLexer {
token.type === TokenType.FLOAT ||
token.type === TokenType.STR ||
token.type === TokenType.NULL ||
token.type === TokenType.UNDEFINED
token.type === TokenType.UNDEFINED ||
token.type === TokenType.REGEXP
);
}

Expand All @@ -317,7 +355,7 @@ export class JsonTemplateLexer {
}

private static isIdStart(ch: string) {
return ch === '$' || ch === '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
return ch === '_' || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');
koladilip marked this conversation as resolved.
Show resolved Hide resolved
}

private static isIdPart(ch: string) {
Expand Down Expand Up @@ -383,7 +421,7 @@ export class JsonTemplateLexer {
JsonTemplateLexer.validateID(id);
return {
type: TokenType.ID,
value: id.replace(/^\$/, `${BINDINGS_PARAM_KEY}`),
value: id,
range: [start, this.idx],
};
}
Expand Down Expand Up @@ -521,7 +559,7 @@ export class JsonTemplateLexer {
};
}
} else if (ch1 === '=') {
if ('^$*'.indexOf(ch2) >= 0) {
if ('^$*~'.indexOf(ch2) >= 0) {
this.idx += 2;
return {
type: TokenType.PUNCT,
Expand Down Expand Up @@ -565,7 +603,7 @@ export class JsonTemplateLexer {
const start = this.idx;
const ch1 = this.codeChars[this.idx];

if (',;:{}()[]^+-*/%!><|=@~#?\n'.includes(ch1)) {
if (',;:{}()[]^+-*/%!><|=@~$#?\n'.includes(ch1)) {
return {
type: TokenType.PUNCT,
value: ch1,
Expand Down Expand Up @@ -594,7 +632,7 @@ export class JsonTemplateLexer {
const ch1 = this.codeChars[this.idx];
const ch2 = this.codeChars[this.idx + 1];

if (ch1 === '~' && 'rs'.includes(ch2)) {
if (ch1 === '~' && 'rsj'.includes(ch2)) {
this.idx += 2;
return {
type: TokenType.PUNCT,
Expand All @@ -618,6 +656,56 @@ export class JsonTemplateLexer {
}
}

private static isValidRegExp(regexp: string, modifiers: string) {
try {
RegExp(regexp, modifiers);
return true;
} catch (e) {
return false;
}
}

private getRegExpModifiers(): string {
let modifiers = '';
while ('gimsuyv'.includes(this.codeChars[this.idx])) {
modifiers += this.codeChars[this.idx];
this.idx++;
}
return modifiers;
}

private scanRegularExpressions(): Token | undefined {
const start = this.idx;
const ch1 = this.codeChars[this.idx];

if (ch1 === '/') {
let end = this.idx + 1;
while (end < this.codeChars.length) {
if (this.codeChars[end] === '\n') {
return;
}
if (this.codeChars[end] === '/') {
break;
}
end++;
}

if (end < this.codeChars.length) {
this.idx = end + 1;
const regexp = this.getCode(start + 1, end);
const modifiers = this.getRegExpModifiers();
if (!JsonTemplateLexer.isValidRegExp(regexp, modifiers)) {
JsonTemplateLexer.throwError("invalid regular expression '%0'", regexp);
}
return {
type: TokenType.REGEXP,
value: this.getCode(start, this.idx),
range: [start, this.idx],
};
}
}
}

private scanPunctuator(): Token | undefined {
return (
this.scanPunctuatorForDots() ??
Expand Down
Loading
Loading