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 7 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 };
75 changes: 46 additions & 29 deletions src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { BINDINGS_PARAM_KEY, DATA_PARAM_KEY } from './constants';
import { JsonTemplateLexer } from './lexer';
import { JsonTemplateParser } from './parser';
import { JsonTemplateTranslator } from './translator';
import { EngineOptions, Expression } from './types';
import { CommonUtils } from './utils';
import { EngineOptions, Expression, FlatMappingAST, FlatMappingPaths } from './types';
koladilip marked this conversation as resolved.
Show resolved Hide resolved
import { CreateAsyncFunction, convertToObjectMapping } 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 +12,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 @@ -32,29 +21,16 @@ export class JsonTemplateEngine {
}

private static compileAsAsync(
templateOrExpr: string | Expression,
templateOrExpr: string | Expression | FlatMappingPaths[],
options?: EngineOptions,
): Function {
return CommonUtils.CreateAsyncFunction(
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();
}

static translate(templateOrExpr: string | Expression, options?: EngineOptions): string {
if (typeof templateOrExpr === 'string') {
return this.translateTemplate(templateOrExpr, options);
}
return this.translateExpression(templateOrExpr);
}

private static translateTemplate(template: string, options?: EngineOptions): string {
return this.translateExpression(this.parse(template, options));
}
Expand All @@ -64,7 +40,48 @@ export class JsonTemplateEngine {
return translator.translate();
}

evaluate(data: any, bindings: Record<string, any> = {}): any {
static parseMappingPaths(mappings: FlatMappingPaths[]): FlatMappingAST[] {
return mappings.map((mapping) => ({
input: JsonTemplateEngine.parse(mapping.input).statements[0],
output: JsonTemplateEngine.parse(mapping.output).statements[0],
}));
}

static create(
templateOrExpr: string | Expression | FlatMappingPaths[],
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: string, options?: EngineOptions): Expression {
const lexer = new JsonTemplateLexer(template);
const parser = new JsonTemplateParser(lexer, options);
return parser.parse();
}

static translate(
template: string | Expression | FlatMappingPaths[],
options?: EngineOptions,
): string {
if (typeof template === 'string') {
return this.translateTemplate(template, options);
}
let templateExpr = template as Expression;
if (Array.isArray(template)) {
templateExpr = convertToObjectMapping(this.parseMappingPaths(template));
}
return this.translateExpression(templateExpr);
}

evaluate(data: unknown, bindings: Record<string, unknown> = {}): unknown {
return this.fn(data ?? {}, bindings);
}
}
38 changes: 29 additions & 9 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,12 +81,16 @@ 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 {
return this.matchPathSelector() || this.matchID();
return this.matchPathType() || this.matchPathSelector() || this.matchID();
}

matchSpread(): boolean {
Expand All @@ -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,8 +149,24 @@ 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);
}

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

matchFunction(): boolean {
Expand Down Expand Up @@ -317,7 +337,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 +403,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 @@ -565,7 +585,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 +614,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 Down
10 changes: 9 additions & 1 deletion src/operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function endsWith(val1, val2): string {
}

function containsStrict(val1, val2): string {
return `(typeof ${val1} === 'string' && ${val1}.includes(${val2}))`;
return `((typeof ${val1} === 'string' || Array.isArray(${val1})) && ${val1}.includes(${val2}))`;
}

function contains(val1, val2): string {
Expand Down Expand Up @@ -74,10 +74,18 @@ export const binaryOperators = {

'=$': (val1, val2): string => endsWith(val2, val1),

contains: containsStrict,

'==*': (val1, val2): string => containsStrict(val2, val1),

'=*': (val1, val2): string => contains(val2, val1),

size: (val1, val2): string => `${val1}.length === ${val2}`,

empty: (val1, val2): string => `(${val1}.length === 0) === ${val2}`,

subsetof: (val1, val2): string => `${val1}.every((el) => {return ${val2}.includes(el);})`,

'+': (val1, val2): string => `${val1}+${val2}`,

'-': (val1, val2): string => `${val1}-${val2}`,
Expand Down
Loading
Loading