Skip to content

Commit d7cc730

Browse files
committed
feat: Implement ternary operator
This commit introduces the ternary operator (? :) to the expression language. The following changes were made: - Added TernaryNode to AST.type.ts - Added TOK_QUESTION to Lexer.ts and Lexer.type.ts - Updated Parser.ts to handle the ternary operator - Updated TreeInterpreter.ts to evaluate ternary expressions - Updated compliance tests (points to `feature/ternary-operator` branch for now) <!-- ps-id: 23d6602d-719c-495d-8421-97a050351f6f -->
1 parent 9cb9462 commit d7cc730

File tree

10 files changed

+146
-18
lines changed

10 files changed

+146
-18
lines changed

.npmrc renamed to .nvmrc

File renamed without changes.

src/AST.type.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ export interface VariableNode {
7878
readonly name: string;
7979
}
8080

81+
export interface TernaryNode {
82+
readonly type: 'Ternary';
83+
readonly condition: ExpressionNode;
84+
readonly trueExpr: ExpressionNode;
85+
readonly falseExpr: ExpressionNode;
86+
}
87+
8188
type BinaryExpressionType =
8289
| 'AndExpression'
8390
| 'IndexExpression'
@@ -139,6 +146,7 @@ export type ExpressionNode =
139146
| FunctionNode
140147
| LetExpressionNode
141148
| BindingNode
142-
| VariableNode;
149+
| VariableNode
150+
| TernaryNode;
143151

144152
export type ExpressionReference = { expref: true } & ExpressionNode;

src/Lexer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const basicTokens: Record<string, Token> = {
1616
'}': Token.TOK_RBRACE,
1717
'+': Token.TOK_PLUS,
1818
'%': Token.TOK_MODULO,
19+
'?': Token.TOK_QUESTION,
1920
'\u2212': Token.TOK_MINUS,
2021
'\u00d7': Token.TOK_MULTIPLY,
2122
'\u00f7': Token.TOK_DIVIDE,

src/Lexer.type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export enum Token {
3939
TOK_LBRACKET = 'Lbracket',
4040
TOK_LPAREN = 'Lparen',
4141
TOK_LITERAL = 'Literal',
42+
TOK_QUESTION = 'Question',
4243
}
4344

4445
export type LexerTokenValue = JSONValue;

src/Parser.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { LexerToken, Token } from './Lexer.type';
2020
import { Options } from './Parser.type';
2121

2222
const bindingPower: Record<string, number> = {
23+
[Token.TOK_QUESTION]: 4,
2324
[Token.TOK_EOF]: 0,
2425
[Token.TOK_VARIABLE]: 0,
2526
[Token.TOK_UNQUOTEDIDENTIFIER]: 0,
@@ -202,6 +203,17 @@ class TokenParser {
202203

203204
led(tokenName: string, left: ExpressionNode): ExpressionNode {
204205
switch (tokenName) {
206+
case Token.TOK_QUESTION: {
207+
const trueExpr = this.expression(0);
208+
this.match(Token.TOK_COLON);
209+
const falseExpr = this.expression(0);
210+
return {
211+
type: 'Ternary',
212+
condition: left,
213+
trueExpr,
214+
falseExpr,
215+
};
216+
}
205217
case Token.TOK_DOT: {
206218
const rbp = bindingPower.Dot;
207219
if (this.lookahead(0) !== Token.TOK_STAR) {

src/TreeInterpreter.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,20 @@ export class TreeInterpreter {
3333

3434
visit(node: ExpressionNode, value: JSONValue | ExpressionNode): JSONValue | ExpressionNode | ExpressionReference {
3535
switch (node.type) {
36+
case 'Ternary': {
37+
const condition = this.visit(node.condition, value);
38+
if (!isFalse(condition)) {
39+
return this.visit(node.trueExpr, value);
40+
}
41+
return this.visit(node.falseExpr, value);
42+
}
3643
case 'Field':
3744
const identifier = node.name;
38-
let result: JSONValue = null;
39-
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
40-
result = (value as JSONObject)[identifier] ?? null;
45+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
46+
return null;
4147
}
42-
return result;
48+
// return the value of the field
49+
return (value as JSONObject)[identifier] ?? null;
4350
case 'LetExpression': {
4451
const { bindings, expression } = node;
4552
let scope = {};

src/utils/index.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,33 +40,34 @@ export const strictDeepEqual = (first: unknown, second: unknown): boolean => {
4040
};
4141

4242
export const isFalse = (obj: unknown): boolean => {
43+
// falsey values
44+
if (obj === null || obj === undefined || obj === false) {
45+
return true;
46+
}
47+
if (typeof obj === 'string') {
48+
return obj === '';
49+
}
4350
if (typeof obj === 'object') {
44-
if (obj === null) {
45-
return true;
46-
}
4751
if (Array.isArray(obj)) {
4852
return obj.length === 0;
4953
}
50-
// eslint-disable-next-line @typescript-eslint/naming-convention
51-
for (const _key in obj) {
52-
return false;
54+
if (obj === null) {
55+
return true;
5356
}
54-
return true;
57+
// check if object is empty
58+
return Object.keys(obj).length === 0;
5559
}
56-
return !(typeof obj === 'number' || obj);
60+
return false;
5761
};
5862

5963
export const isAlpha = (ch: string): boolean => {
60-
// tslint:disable-next-line: strict-comparisons
6164
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_';
6265
};
6366

6467
export const isNum = (ch: string): boolean => {
65-
// tslint:disable-next-line: strict-comparisons
6668
return (ch >= '0' && ch <= '9') || ch === '-';
6769
};
6870
export const isAlphaNum = (ch: string): boolean => {
69-
// tslint:disable-next-line: strict-comparisons
7071
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch === '_';
7172
};
7273

test/jmespath-ternary.spec.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { search } from '../src';
2+
import { describe, it, expect } from 'vitest';
3+
4+
describe('JMESPath Ternary Operations', () => {
5+
const data = {
6+
true: true,
7+
false: false,
8+
foo: 'foo',
9+
bar: 'bar',
10+
baz: 'baz',
11+
qux: 'qux',
12+
quux: 'quux',
13+
two: 2,
14+
fourty: 40,
15+
};
16+
17+
describe('Basic Ternary Operations', () => {
18+
it('should return foo when condition is true', () => {
19+
expect(search(data, 'true ? foo : bar')).toBe('foo');
20+
});
21+
22+
it('should return bar when condition is false', () => {
23+
expect(search(data, 'false ? foo : bar')).toBe('bar');
24+
});
25+
26+
it('should return bar when condition is null', () => {
27+
expect(search(data, '`null` ? foo : bar')).toBe('bar');
28+
});
29+
30+
it('should return bar when condition is empty array', () => {
31+
expect(search(data, '`[]` ? foo : bar')).toBe('bar');
32+
});
33+
34+
it('should return bar when condition is not an empty array', () => {
35+
expect(search(data, '`[1]` ? foo : bar')).toBe('foo');
36+
});
37+
38+
it('should return bar when condition is empty object', () => {
39+
expect(search(data, '`{}` ? foo : bar')).toBe('bar');
40+
});
41+
42+
it('should return bar when condition is empty string', () => {
43+
expect(search(data, "'' ? foo : bar")).toBe('bar');
44+
});
45+
});
46+
47+
describe('Chained Ternary Operations', () => {
48+
it('should handle chained ternary operations', () => {
49+
expect(search(data, 'foo ? bar ? baz : qux : quux')).toBe('baz');
50+
});
51+
});
52+
53+
describe('Ternary Operations with Precedence', () => {
54+
it('should handle precedence with pipes', () => {
55+
expect(search(data, 'false ? foo | bar | @ : baz')).toBe('baz');
56+
});
57+
58+
it('should handle precedence with arithmetic', () => {
59+
expect(search(data, 'foo ? fourty + two : `false`')).toBe(42);
60+
});
61+
});
62+
63+
describe('Nested Ternary Operations', () => {
64+
it('should handle left-nested ternary operations', () => {
65+
expect(search(data, 'true ? (true ? foo : bar) : baz')).toBe('foo');
66+
expect(search(data, 'true ? (false ? foo : bar) : baz')).toBe('bar');
67+
expect(search(data, 'false ? (true ? foo : bar) : baz')).toBe('baz');
68+
});
69+
70+
it('should handle right-nested ternary operations', () => {
71+
expect(search(data, 'true ? foo : (true ? bar : baz)')).toBe('foo');
72+
expect(search(data, 'false ? foo : (true ? bar : baz)')).toBe('bar');
73+
expect(search(data, 'false ? foo : (false ? bar : baz)')).toBe('baz');
74+
});
75+
76+
it('should handle multiple nested ternary operations', () => {
77+
expect(search(data, 'true ? (true ? (true ? foo : bar) : baz) : quux')).toBe('foo');
78+
expect(search(data, 'true ? (true ? (false ? foo : bar) : baz) : quux')).toBe('bar');
79+
expect(search(data, 'true ? (false ? (true ? foo : bar) : baz) : quux')).toBe('baz');
80+
expect(search(data, 'false ? (true ? (true ? foo : bar) : baz) : quux')).toBe('quux');
81+
});
82+
83+
it('should handle mixed nested ternary operations with literals', () => {
84+
expect(search(data, 'true ? (`null` ? foo : bar) : baz')).toBe('bar');
85+
expect(search(data, 'true ? (`[]` ? foo : bar) : baz')).toBe('bar');
86+
expect(search(data, 'true ? (`{}` ? foo : bar) : baz')).toBe('bar');
87+
expect(search(data, "true ? ('' ? foo : bar) : baz")).toBe('bar');
88+
});
89+
90+
it('should handle nested ternary operations with arithmetic', () => {
91+
const testData = {
92+
...data,
93+
threshold: 40,
94+
};
95+
expect(search(testData, 'true ? (fourty + two > threshold ? foo : bar) : baz')).toBe('foo');
96+
expect(search(testData, 'true ? (fourty + two < threshold ? foo : bar) : baz')).toBe('bar');
97+
});
98+
});
99+
});

tsconfig.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
"sourceMap": true,
2424
"strict": true,
2525
"strictNullChecks": true,
26-
"suppressImplicitAnyIndexErrors": true,
2726
"typeRoots": ["node_modules/@types"]
2827
},
2928
"exclude": ["node_modules", "dist", "test"],

0 commit comments

Comments
 (0)