Skip to content

Commit 0be4827

Browse files
committed
chore(json): Improve typing and code structure
1 parent 0dc3524 commit 0be4827

File tree

11 files changed

+459
-99
lines changed

11 files changed

+459
-99
lines changed
Lines changed: 343 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
// deck.gl
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) vis.gl contributors
4+
5+
/**
6+
* Sources:
7+
* - Copyright (c) 2013 Stephen Oney, http://jsep.from.so/, MIT License
8+
* - Copyright (c) 2023 Don McCurdy, https://github.com/donmccurdy/expression-eval, MIT License
9+
*/
10+
11+
import jsep from 'jsep';
12+
13+
/** Default operator precedence from https://github.com/EricSmekens/jsep/blob/master/src/jsep.js#L55 */
14+
const DEFAULT_PRECEDENCE = {
15+
'||': 1,
16+
'&&': 2,
17+
'|': 3,
18+
'^': 4,
19+
'&': 5,
20+
'==': 6,
21+
'!=': 6,
22+
'===': 6,
23+
'!==': 6,
24+
'<': 7,
25+
'>': 7,
26+
'<=': 7,
27+
'>=': 7,
28+
'<<': 8,
29+
'>>': 8,
30+
'>>>': 8,
31+
'+': 9,
32+
'-': 9,
33+
'*': 10,
34+
'/': 10,
35+
'%': 10
36+
};
37+
38+
const binops = {
39+
'||': (a: unknown, b: unknown) => {
40+
return a || b;
41+
},
42+
'&&': (a: unknown, b: unknown) => {
43+
return a && b;
44+
},
45+
'|': (a: number, b: number) => {
46+
return a | b;
47+
},
48+
'^': (a: number, b: number) => {
49+
return a ^ b;
50+
},
51+
'&': (a: number, b: number) => {
52+
return a & b;
53+
},
54+
'==': (a: unknown, b: unknown) => {
55+
// eslint-disable-next-line eqeqeq
56+
return a == b;
57+
},
58+
'!=': (a: unknown, b: unknown) => {
59+
// eslint-disable-next-line eqeqeq
60+
return a != b;
61+
},
62+
'===': (a: unknown, b: unknown) => {
63+
return a === b;
64+
},
65+
'!==': (a: unknown, b: unknown) => {
66+
return a !== b;
67+
},
68+
'<': (a: number | string, b: number | string) => {
69+
return a < b;
70+
},
71+
'>': (a: number | string, b: number | string) => {
72+
return a > b;
73+
},
74+
'<=': (a: number | string, b: number | string) => {
75+
return a <= b;
76+
},
77+
'>=': (a: number | string, b: number | string) => {
78+
return a >= b;
79+
},
80+
'<<': (a: number, b: number) => {
81+
return a << b;
82+
},
83+
'>>': (a: number, b: number) => {
84+
return a >> b;
85+
},
86+
'>>>': (a: number, b: number) => {
87+
return a >>> b;
88+
},
89+
'+': (a: unknown, b: unknown) => {
90+
// @ts-expect-error
91+
return a + b;
92+
},
93+
'-': (a: number, b: number) => {
94+
return a - b;
95+
},
96+
'*': (a: number, b: number) => {
97+
return a * b;
98+
},
99+
'/': (a: number, b: number) => {
100+
return a / b;
101+
},
102+
'%': (a: number, b: number) => {
103+
return a % b;
104+
}
105+
};
106+
107+
const unops = {
108+
'-': (a: number) => {
109+
return -a;
110+
},
111+
'+': (a: unknown) => {
112+
// @ts-expect-error
113+
// eslint-disable-next-line no-implicit-coercion
114+
return +a;
115+
},
116+
'~': (a: number) => {
117+
return ~a;
118+
},
119+
'!': (a: unknown) => {
120+
return !a;
121+
}
122+
};
123+
124+
declare type operand = number | string;
125+
declare type unaryCallback = (a: operand) => operand;
126+
declare type binaryCallback = (a: operand, b: operand) => operand;
127+
128+
type AnyExpression =
129+
| jsep.ArrayExpression
130+
| jsep.BinaryExpression
131+
| jsep.MemberExpression
132+
| jsep.CallExpression
133+
| jsep.ConditionalExpression
134+
| jsep.Identifier
135+
| jsep.Literal
136+
| jsep.LogicalExpression
137+
| jsep.ThisExpression
138+
| jsep.UnaryExpression;
139+
140+
function evaluateArray(list, context) {
141+
return list.map(function (v) {
142+
return evaluate(v, context);
143+
});
144+
}
145+
146+
async function evaluateArrayAsync(list, context) {
147+
const res = await Promise.all(list.map(v => evalAsync(v, context)));
148+
return res;
149+
}
150+
151+
function evaluateMember(node: jsep.MemberExpression, context: object) {
152+
const object = evaluate(node.object, context);
153+
let key: string;
154+
if (node.computed) {
155+
key = evaluate(node.property, context);
156+
} else {
157+
key = (node.property as jsep.Identifier).name;
158+
}
159+
if (/^__proto__|prototype|constructor$/.test(key)) {
160+
throw Error(`Access to member "${key}" disallowed.`);
161+
}
162+
return [object, object[key]];
163+
}
164+
165+
async function evaluateMemberAsync(node: jsep.MemberExpression, context: object) {
166+
const object = await evalAsync(node.object, context);
167+
let key: string;
168+
if (node.computed) {
169+
key = await evalAsync(node.property, context);
170+
} else {
171+
key = (node.property as jsep.Identifier).name;
172+
}
173+
if (/^__proto__|prototype|constructor$/.test(key)) {
174+
throw Error(`Access to member "${key}" disallowed.`);
175+
}
176+
return [object, object[key]];
177+
}
178+
179+
// eslint-disable-next-line complexity
180+
function evaluate(_node: jsep.Expression, context: object) {
181+
const node = _node as AnyExpression;
182+
183+
switch (node.type) {
184+
case 'ArrayExpression':
185+
return evaluateArray(node.elements, context);
186+
187+
case 'BinaryExpression':
188+
return binops[node.operator](evaluate(node.left, context), evaluate(node.right, context));
189+
190+
case 'CallExpression':
191+
let caller: object;
192+
let fn: Function;
193+
let assign: unknown[];
194+
if (node.callee.type === 'MemberExpression') {
195+
assign = evaluateMember(node.callee as jsep.MemberExpression, context);
196+
caller = assign[0] as object;
197+
fn = assign[1] as Function;
198+
} else {
199+
fn = evaluate(node.callee, context);
200+
}
201+
if (typeof fn !== 'function') {
202+
return undefined;
203+
}
204+
return fn.apply(caller!, evaluateArray(node.arguments, context));
205+
206+
case 'ConditionalExpression':
207+
return evaluate(node.test, context)
208+
? evaluate(node.consequent, context)
209+
: evaluate(node.alternate, context);
210+
211+
case 'Identifier':
212+
return context[node.name];
213+
214+
case 'Literal':
215+
return node.value;
216+
217+
case 'LogicalExpression':
218+
if (node.operator === '||') {
219+
return evaluate(node.left, context) || evaluate(node.right, context);
220+
} else if (node.operator === '&&') {
221+
return evaluate(node.left, context) && evaluate(node.right, context);
222+
}
223+
return binops[node.operator](evaluate(node.left, context), evaluate(node.right, context));
224+
225+
case 'MemberExpression':
226+
return evaluateMember(node, context)[1];
227+
228+
case 'ThisExpression':
229+
return context;
230+
231+
case 'UnaryExpression':
232+
return unops[node.operator](evaluate(node.argument, context));
233+
234+
default:
235+
return undefined;
236+
}
237+
}
238+
239+
// eslint-disable-next-line complexity
240+
async function evalAsync(_node: jsep.Expression, context: object) {
241+
const node = _node as AnyExpression;
242+
243+
// Brackets used for some case blocks here, to avoid edge cases related to variable hoisting.
244+
// See: https://stackoverflow.com/questions/57759348/const-and-let-variable-shadowing-in-a-switch-statement
245+
switch (node.type) {
246+
case 'ArrayExpression':
247+
return await evaluateArrayAsync(node.elements, context);
248+
249+
case 'BinaryExpression': {
250+
const [left, right] = await Promise.all([
251+
evalAsync(node.left, context),
252+
evalAsync(node.right, context)
253+
]);
254+
return binops[node.operator](left, right);
255+
}
256+
257+
case 'CallExpression': {
258+
let caller: object;
259+
let fn: Function;
260+
let assign: unknown[];
261+
if (node.callee.type === 'MemberExpression') {
262+
assign = await evaluateMemberAsync(node.callee as jsep.MemberExpression, context);
263+
caller = assign[0] as object;
264+
fn = assign[1] as Function;
265+
} else {
266+
fn = await evalAsync(node.callee, context);
267+
}
268+
if (typeof fn !== 'function') {
269+
return undefined;
270+
}
271+
return await fn.apply(caller!, await evaluateArrayAsync(node.arguments, context));
272+
}
273+
274+
case 'ConditionalExpression':
275+
return (await evalAsync(node.test, context))
276+
? await evalAsync(node.consequent, context)
277+
: await evalAsync(node.alternate, context);
278+
279+
case 'Identifier':
280+
return context[node.name];
281+
282+
case 'Literal':
283+
return node.value;
284+
285+
case 'LogicalExpression': {
286+
if (node.operator === '||') {
287+
return (await evalAsync(node.left, context)) || (await evalAsync(node.right, context));
288+
} else if (node.operator === '&&') {
289+
return (await evalAsync(node.left, context)) && (await evalAsync(node.right, context));
290+
}
291+
292+
const [left, right] = await Promise.all([
293+
evalAsync(node.left, context),
294+
evalAsync(node.right, context)
295+
]);
296+
297+
return binops[node.operator](left, right);
298+
}
299+
300+
case 'MemberExpression':
301+
return (await evaluateMemberAsync(node, context))[1];
302+
303+
case 'ThisExpression':
304+
return context;
305+
306+
case 'UnaryExpression':
307+
return unops[node.operator](await evalAsync(node.argument, context));
308+
309+
default:
310+
return undefined;
311+
}
312+
}
313+
314+
function compile(expression: string | jsep.Expression): (context: object) => any {
315+
return evaluate.bind(null, jsep(expression));
316+
}
317+
318+
function compileAsync(expression: string | jsep.Expression): (context: object) => Promise<any> {
319+
return evalAsync.bind(null, jsep(expression));
320+
}
321+
322+
// Added functions to inject Custom Unary Operators (and override existing ones)
323+
function addUnaryOp(operator: string, _function: unaryCallback): void {
324+
jsep.addUnaryOp(operator);
325+
unops[operator] = _function;
326+
}
327+
328+
// Added functions to inject Custom Binary Operators (and override existing ones)
329+
function addBinaryOp(
330+
operator: string,
331+
precedenceOrFn: number | binaryCallback,
332+
_function: binaryCallback
333+
): void {
334+
if (_function) {
335+
jsep.addBinaryOp(operator, precedenceOrFn as number);
336+
binops[operator] = _function;
337+
} else {
338+
jsep.addBinaryOp(operator, DEFAULT_PRECEDENCE[operator] || 1);
339+
binops[operator] = precedenceOrFn;
340+
}
341+
}
342+
343+
export {jsep as parse, evaluate as eval, evalAsync, compile, compileAsync, addUnaryOp, addBinaryOp};

modules/json/src/helpers/convert-functions.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
// SPDX-License-Identifier: MIT
33
// Copyright (c) vis.gl contributors
44

5-
import parseExpressionString from './parse-expression-string';
5+
import {parseExpressionString} from './parse-expression-string';
66

77
import {FUNCTION_IDENTIFIER} from '../syntactic-sugar';
8+
import {type JSONConfiguration } from '../json-configuration';
89

910
function hasFunctionIdentifier(value) {
1011
return typeof value === 'string' && value.startsWith(FUNCTION_IDENTIFIER);
@@ -14,9 +15,11 @@ function trimFunctionIdentifier(value) {
1415
return value.replace(FUNCTION_IDENTIFIER, '');
1516
}
1617

17-
// Try to determine if any props are function valued
18-
// and if so convert their string values to functions
19-
export default function convertFunctions(props, configuration) {
18+
/**
19+
* Tries to determine if any props are "function valued"
20+
* and if so convert their string values to functions
21+
*/
22+
export function convertFunctions(props, configuration: JSONConfiguration) {
2023
// Use deck.gl prop types if available.
2124
const replacedProps = {};
2225
for (const propName in props) {

modules/json/src/helpers/execute-function.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22
// SPDX-License-Identifier: MIT
33
// Copyright (c) vis.gl contributors
44

5-
// This attempts to execute a function
6-
export function executeFunction(targetFunction, props, configuration) {
5+
import {type JSONConfiguration } from "../json-configuration";
6+
7+
/**
8+
* Attempt to execute a function
9+
*/
10+
export function executeFunction(targetFunction, props, configuration: JSONConfiguration) {
711
// Find the function
8-
const matchedFunction = configuration.functions[targetFunction];
12+
const matchedFunction = configuration.config.functions[targetFunction];
913

1014
// Check that the function is in the configuration.
1115
if (!matchedFunction) {
12-
const {log} = configuration; // eslint-disable-line
16+
const {log} = configuration.config; // eslint-disable-line
1317
if (log) {
1418
const stringProps = JSON.stringify(props, null, 0).slice(0, 40);
1519
log.warn(`JSON converter: No registered function ${targetFunction}(${stringProps}...) `);

0 commit comments

Comments
 (0)