Skip to content

Commit 9e12bf3

Browse files
cawalchspringcomp
andauthored
feat: Implement ternary operator (#50)
* 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 --> * fixed operator precedence --------- Co-authored-by: Springcomp <springcomp@users.noreply.github.com>
1 parent 88e0323 commit 9e12bf3

File tree

11 files changed

+256
-93
lines changed

11 files changed

+256
-93
lines changed
File renamed without changes.

README.md

Lines changed: 107 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
# @jmespath-community/jmespath
44

5-
65
@jmespath-community/jmespath is a **TypeScript** implementation of the [JMESPath](https://jmespath.site/) spec.
76

87
JMESPath is a query language for JSON. It will take a JSON document
@@ -20,12 +19,9 @@ npm install @jmespath-community/jmespath
2019
### `search(data: JSONValue, expression: string): JSONValue`
2120

2221
```javascript
23-
import { search } from '@jmespath-community/jmespath';
22+
import { search } from "@jmespath-community/jmespath";
2423

25-
search(
26-
{foo: {bar: {baz: [0, 1, 2, 3, 4]}}},
27-
"foo.bar.baz[2]"
28-
);
24+
search({ foo: { bar: { baz: [0, 1, 2, 3, 4] } } }, "foo.bar.baz[2]");
2925

3026
// OUTPUTS: 2
3127
```
@@ -35,142 +31,181 @@ In the example we gave the `search` function input data of
3531
expression `foo.bar.baz[2]`, and the `search` function evaluated
3632
the expression against the input data to produce the result `2`.
3733

38-
The JMESPath language can do *a lot* more than select an element
39-
from a list. Here are a few more examples:
34+
The JMESPath language can do _a lot_ more than select an element
35+
from a list. Here are a few more examples:
4036

4137
```javascript
42-
import { search } from '@jmespath-community/jmespath';
38+
import { search } from "@jmespath-community/jmespath";
4339

4440
/* --- EXAMPLE 1 --- */
4541

4642
let JSON_DOCUMENT = {
4743
foo: {
4844
bar: {
49-
baz: [0, 1, 2, 3, 4]
50-
}
51-
}
45+
baz: [0, 1, 2, 3, 4],
46+
},
47+
},
5248
};
5349

5450
search(JSON_DOCUMENT, "foo.bar");
5551
// OUTPUTS: { baz: [ 0, 1, 2, 3, 4 ] }
5652

57-
5853
/* --- EXAMPLE 2 --- */
5954

6055
JSON_DOCUMENT = {
61-
"foo": [
62-
{"first": "a", "last": "b"},
63-
{"first": "c", "last": "d"}
64-
]
56+
foo: [
57+
{ first: "a", last: "b" },
58+
{ first: "c", last: "d" },
59+
],
6560
};
6661

67-
search(JSON_DOCUMENT, "foo[*].first")
62+
search(JSON_DOCUMENT, "foo[*].first");
6863
// OUTPUTS: [ 'a', 'c' ]
6964

70-
7165
/* --- EXAMPLE 3 --- */
7266

7367
JSON_DOCUMENT = {
74-
"foo": [
75-
{"age": 20},
76-
{"age": 25},
77-
{"age": 30},
78-
{"age": 35},
79-
{"age": 40}
80-
]
81-
}
68+
foo: [{ age: 20 }, { age: 25 }, { age: 30 }, { age: 35 }, { age: 40 }],
69+
};
8270

8371
search(JSON_DOCUMENT, "foo[?age > `30`]");
8472
// OUTPUTS: [ { age: 35 }, { age: 40 } ]
8573
```
8674

8775
### `compile(expression: string): ExpressionNodeTree`
8876

89-
You can precompile all your expressions ready for use later on. the `compile`
77+
You can precompile all your expressions ready for use later on. The `compile`
9078
function takes a JMESPath expression and returns an abstract syntax tree that
9179
can be used by the TreeInterpreter function
9280

9381
```javascript
94-
import { compile, TreeInterpreter } from '@jmespath-community/jmespath';
82+
import { compile, TreeInterpreter } from "@jmespath-community/jmespath";
9583

96-
const ast = compile('foo.bar');
84+
const ast = compile("foo.bar");
9785

98-
TreeInterpreter.search(ast, {foo: {bar: 'BAZ'}})
86+
TreeInterpreter.search(ast, { foo: { bar: "BAZ" } });
9987
// RETURNS: "BAZ"
100-
10188
```
10289

10390
---
91+
10492
## EXTENSIONS TO ORIGINAL SPEC
10593

10694
1. ### Register you own custom functions
10795

108-
#### `registerFunction(functionName: string, customFunction: RuntimeFunction, signature: InputSignature[]): void`
109-
110-
Extend the list of built in JMESpath expressions with your own functions.
111-
112-
```javascript
113-
import {search, registerFunction, TYPE_NUMBER} from '@jmespath-community/jmespath'
114-
96+
#### `registerFunction(functionName: string, customFunction: RuntimeFunction, signature: InputSignature[]): void`
11597

116-
search({ foo: 60, bar: 10 }, 'divide(foo, bar)')
117-
// THROWS ERROR: Error: Unknown function: divide()
98+
Extend the list of built in JMESpath expressions with your own functions.
11899

119-
registerFunction(
120-
'divide', // FUNCTION NAME
121-
(resolvedArgs) => { // CUSTOM FUNCTION
122-
const [dividend, divisor] = resolvedArgs;
123-
return dividend / divisor;
124-
},
125-
[{ types: [TYPE_NUMBER] }, { types: [TYPE_NUMBER] }] //SIGNATURE
126-
);
100+
```javascript
101+
import { search, registerFunction, TYPE_NUMBER } from "@jmespath-community/jmespath";
127102

128-
search({ foo: 60,bar: 10 }, 'divide(foo, bar)');
129-
// OUTPUTS: 6
103+
search({ foo: 60, bar: 10 }, "divide(foo, bar)");
104+
// THROWS ERROR: Error: Unknown function: divide()
130105

131-
```
106+
registerFunction(
107+
"divide", // FUNCTION NAME
108+
(resolvedArgs) => {
109+
// CUSTOM FUNCTION
110+
const [dividend, divisor] = resolvedArgs;
111+
return dividend / divisor;
112+
},
113+
[{ types: [TYPE_NUMBER] }, { types: [TYPE_NUMBER] }], //SIGNATURE
114+
);
132115

133-
Optional arguments are supported by setting `{..., optional: true}` in argument signatures
116+
search({ foo: 60, bar: 10 }, "divide(foo, bar)");
117+
// OUTPUTS: 6
118+
```
134119

120+
Optional arguments are supported by setting `{..., optional: true}` in argument signatures
135121

136-
```javascript
122+
```javascript
123+
registerFunction(
124+
"divide",
125+
(resolvedArgs) => {
126+
const [dividend, divisor] = resolvedArgs;
127+
return dividend / divisor ?? 1; //OPTIONAL DIVISOR THAT DEFAULTS TO 1
128+
},
129+
[{ types: [TYPE_NUMBER] }, { types: [TYPE_NUMBER], optional: true }], //SIGNATURE
130+
);
137131

138-
registerFunction(
139-
'divide',
140-
(resolvedArgs) => {
141-
const [dividend, divisor] = resolvedArgs;
142-
return dividend / divisor ?? 1; //OPTIONAL DIVISOR THAT DEFAULTS TO 1
143-
},
144-
[{ types: [TYPE_NUMBER] }, { types: [TYPE_NUMBER], optional: true }] //SIGNATURE
145-
);
146-
147-
search({ foo: 60, bar: 10 }, 'divide(foo)');
148-
// OUTPUTS: 60
149-
150-
```
132+
search({ foo: 60, bar: 10 }, "divide(foo)");
133+
// OUTPUTS: 60
134+
```
151135
152136
2. ### Root value access with `$` symbol
153137
154138
```javascript
155-
156-
search({foo: {bar: 999}, baz: [1, 2, 3]}, '$.baz[*].[@, $.foo.bar]')
139+
search({ foo: { bar: 999 }, baz: [1, 2, 3] }, "$.baz[*].[@, $.foo.bar]");
157140

158141
// OUTPUTS:
159142
// [ [ 1, 999 ], [ 2, 999 ], [ 3, 999 ] ]
160143
```
161144
162-
163145
## More Resources
164146
165147
The example above only show a small amount of what
166148
a JMESPath expression can do. If you want to take a
167-
tour of the language, the *best* place to go is the
149+
tour of the language, the _best_ place to go is the
168150
[JMESPath Tutorial](http://jmespath.site/main#tutorial).
169151
170152
One of the best things about JMESPath is that it is
171153
implemented in many different programming languages including
172-
python, ruby, php, lua, etc. To see a complete list of libraries,
154+
python, ruby, php, lua, etc. To see a complete list of libraries,
173155
check out the [JMESPath libraries page](http://jmespath.site/main#libraries).
174156
175157
And finally, the full JMESPath specification can be found
176158
on the [JMESPath site](https://jmespath.site/main/#specification).
159+
160+
## Experimental Features
161+
162+
### Ternary Operations (`? :`)
163+
164+
**Supported Version:** 1.1.6
165+
166+
Experimental support for [ternary operations](https://github.com/jmespath-community/jmespath.spec/discussions/179) has been added, allowing for conditional logic within your JMESPath expressions. The syntax is `condition ? value_if_true : value_if_false`.
167+
168+
- **Condition:** The expression before the `?`. JMESPath determines truthiness based on the evaluated value:
169+
- `true` is truthy.
170+
- Any non-empty object, array, or string is truthy.
171+
- Any non-zero number is truthy.
172+
- `false`, `null`, empty objects `{}`, empty arrays `[]`, and empty strings `''` are falsy.
173+
- **Value if true:** The expression between the `?` and `:`. This is evaluated and returned if the condition is truthy.
174+
- **Value if false:** The expression after the `:`. This is evaluated and returned if the condition is falsy.
175+
176+
**Examples:**
177+
178+
Basic usage:
179+
180+
```javascript
181+
search({ is_active: true, user: "Alice" }, "is_active ? user : 'Guest'");
182+
// OUTPUTS: "Alice"
183+
184+
search({ is_active: false, user: "Bob" }, "is_active ? user : 'Guest'");
185+
// OUTPUTS: "Guest"
186+
```
187+
188+
Truthiness with different types:
189+
190+
```javascript
191+
search({ data: [1, 2] }, "data ? 'has_data' : 'no_data'");
192+
// OUTPUTS: "has_data"
193+
194+
search({ data: [] }, "data ? 'has_data' : 'no_data'");
195+
// OUTPUTS: "no_data"
196+
197+
search({ count: 5 }, "count ? 'count_present' : 'no_count'");
198+
// OUTPUTS: "count_present"
199+
200+
search({ count: 0 }, "count ? 'count_present' : 'no_count'");
201+
// OUTPUTS: "no_count"
202+
```
203+
204+
Nested Ternaries:
205+
206+
```javascript
207+
search({ a: true, b: false, val1: 10, val2: 20, val3: 30 }, "a ? (b ? val1 : val2) : val3");
208+
// OUTPUTS: 20
209+
```
210+
211+
This feature is currently experimental and its syntax or behavior might change in future releases.

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: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ const bindingPower: Record<string, number> = {
3434
[Token.TOK_ROOT]: 0,
3535
[Token.TOK_ASSIGN]: 1,
3636
[Token.TOK_PIPE]: 1,
37-
[Token.TOK_OR]: 2,
38-
[Token.TOK_AND]: 3,
37+
[Token.TOK_QUESTION]: 2,
38+
[Token.TOK_OR]: 3,
39+
[Token.TOK_AND]: 4,
3940
[Token.TOK_EQ]: 5,
4041
[Token.TOK_GT]: 5,
4142
[Token.TOK_LT]: 5,
@@ -198,6 +199,17 @@ class TokenParser {
198199

199200
led(tokenName: string, left: ExpressionNode): ExpressionNode {
200201
switch (tokenName) {
202+
case Token.TOK_QUESTION: {
203+
const trueExpr = this.expression(0);
204+
this.match(Token.TOK_COLON);
205+
const falseExpr = this.expression(0);
206+
return {
207+
type: 'Ternary',
208+
condition: left,
209+
trueExpr,
210+
falseExpr,
211+
};
212+
}
201213
case Token.TOK_DOT: {
202214
const rbp = bindingPower.Dot;
203215
if (this.lookahead(0) !== Token.TOK_STAR) {
@@ -478,7 +490,7 @@ class TokenParser {
478490
let keyName: string;
479491
let value: ExpressionNode;
480492
// tslint:disable-next-line: prettier
481-
for (;;) {
493+
for (; ;) {
482494
keyToken = this.lookaheadToken(0);
483495
if (!identifierTypes.includes(keyToken.type)) {
484496
throw new Error(`Syntax error: expecting an identifier token, got: ${keyToken.type}`);

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 = {};

0 commit comments

Comments
 (0)