diff --git a/package.json b/package.json index ff45a1a1f3..c0b0df5579 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ }, "peerDependencies": { "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/json-expression": "^1.0.0", "@jsonjoy.com/json-pack": "^1.0.4", "@jsonjoy.com/json-pointer": "^1.0.0", "@jsonjoy.com/util": "^1.3.0", @@ -135,6 +136,7 @@ }, "devDependencies": { "@jsonjoy.com/base64": "^1.1.2", + "@jsonjoy.com/json-expression": "^1.0.0", "@jsonjoy.com/json-pack": "^1.0.4", "@jsonjoy.com/json-pointer": "^1.0.0", "@jsonjoy.com/util": "^1.3.0", @@ -193,7 +195,6 @@ "json-crdt-extensions", "json-crdt-peritext-ui", "json-crdt", - "json-expression", "json-hash", "json-ot", "json-patch-ot", diff --git a/src/index.ts b/src/index.ts index 2f9c570d7e..ab2584d75f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,5 @@ export type * from '@jsonjoy.com/util/lib/json-brand/types'; export type * from './json-crdt'; export type * from './json-crdt-patch'; export type * from './json-crdt-extensions'; -export type * from './json-expression/types'; export type * from './json-patch/types'; export type * from './json-schema/types'; diff --git a/src/json-expression/README.md b/src/json-expression/README.md deleted file mode 100644 index f8a292b3cd..0000000000 --- a/src/json-expression/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# JSON Expression - -JSON Expression is a simple JSON DSL, which allows to write expressions and -evaluate expressions. - -For example, the following expression - -```js -['+', 1, 2]; // 1 + 2 -``` - -evaluates to 3. - -## Example - -Consider you application receives a stream of JSON Cloud Events, like this: - -```js -{ - "specversion" : "1.0", - "type" : "com.example.someevent", - "source" : "/mycontext", - "subject": null, - "id" : "C234-1234-1234", - "time" : "2018-04-05T17:31:00Z", - "comexampleextension1" : "value", - "comexampleothervalue" : 5, - "datacontenttype" : "application/json", - "data" : { - "appinfoA" : "abc", - "appinfoB" : 123, - "appinfoC" : true - } -} -``` - -You could write and compile a JSON Expression to efficiently filter out events -you are interested in, for example your expression could look like this: - -```js -[ - 'and', - ['==', ['$', '/specversion'], '1.0'], - ['starts', ['$', '/type'], 'com.example.'], - ['in', ['$', '/datacontenttype'], [['application/octet-stream', 'application/json']]], - ['==', ['$', '/data/appinfoA'], 'abc'], -]; -``` - -## Documentation - -`json-expression` library supports few dozen operators, see full list in `Expr` -type [here](./types.ts). - -Parsing rules: - -1. JSON Expression is a valid JSON value. -2. All expressions are JSON arrays, which start with a string which specifies - the operator and remaining array elements are operands. For example, the - "get" operator fetches some value from supplied data using JSON - Pointer:`["get", "/some/path"]`. -3. All other values are treated as literals. Except for arrays, which need to - be enclosed in square brackets. For example, to specify an empty array, you - box your array in square brackets: `[[]]`. This evaluates to an empty array - JSON value `[]`. - -## Reference - -`json-expression` library can immediately evaluate expressions or it can -compile an efficient expression to a function, which will execute about -an order of magnitude faster. - -Evaluating expression immediately as-is. - -```ts -import {evaluate} from 'json-joy/lib/json-expression'; - -const expression = ['+', 1, ['$', '/foo']]; -const data = {foo: 2}; - -evaluate(expression, {data}); // 3 -``` - -Pre-compiling expression to an optimized function. - -```ts -import {JsonExpressionCodegen} from 'json-joy/lib/json-expression'; - -const expression = ['+', 1, ['$', '/foo']]; -const codegen = new JsonExpressionCodegen({expression}); -const fn = codegen.run().compile(); -const data = {foo: 2}; - -fn({data}); // 3 -``` - -## Benchmark - -``` -node benchmarks/json-expression/main.js -json-joy/json-expression JsonExpressionCodegen x 14,557,786 ops/sec ±0.09% (100 runs sampled), 69 ns/op -json-joy/json-expression JsonExpressionCodegen with codegen x 170,098 ops/sec ±0.13% (101 runs sampled), 5879 ns/op -json-joy/json-expression evaluate x 864,956 ops/sec ±0.10% (101 runs sampled), 1156 ns/op -json-logic-js x 821,799 ops/sec ±0.18% (99 runs sampled), 1217 ns/op -Fastest is json-joy/json-expression JsonExpressionCodegen -``` diff --git a/src/json-expression/Vars.ts b/src/json-expression/Vars.ts deleted file mode 100644 index fb1bd1d955..0000000000 --- a/src/json-expression/Vars.ts +++ /dev/null @@ -1,36 +0,0 @@ -import {get, toPath, validateJsonPointer} from '@jsonjoy.com/json-pointer'; - -export class Vars { - protected readonly vars: Map = new Map(); - - constructor(public readonly env: unknown) { - this.env = env; - } - - public get(name: string): unknown { - if (!name) return this.env; - return this.vars.get(name); - } - - public set(name: string, value: unknown): void { - if (!name) throw new Error('Invalid varname.'); - this.vars.set(name, value); - } - - public has(name: string): boolean { - if (!name) return true; - return this.vars.has(name); - } - - public del(name: string): boolean { - if (!name) throw new Error('Invalid varname.'); - return this.vars.delete(name); - } - - public find(name: string, pointer: string): unknown { - const data = this.get(name); - validateJsonPointer(pointer); - const path = toPath(pointer); - return get(data, path); - } -} diff --git a/src/json-expression/__bench__/main.ts b/src/json-expression/__bench__/main.ts deleted file mode 100644 index 3114234808..0000000000 --- a/src/json-expression/__bench__/main.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* tslint:disable no-console */ - -// npx ts-node src/json-expression/__bench__/main.ts - -import * as Benchmark from 'benchmark'; -import {JsonExpressionCodegen} from '../codegen'; -import {Expr} from '../types'; -import {evaluate} from '../evaluate'; -import {operatorsMap} from '../operators'; -import {Vars} from '../Vars'; -const jsonLogic = require('json-logic-js'); - -const json = { - specversion: '1.0', - type: 'com.example.someevent', - source: '/mycontext', - subject: null, - id: 'C234-1234-1234', - time: '2018-04-05T17:31:00Z', - comexampleextension1: 'value', - comexampleothervalue: 5, - datacontenttype: 'application/json', - data: { - appinfoA: 'abc', - appinfoB: 123, - appinfoC: true, - }, -}; - -const expression: Expr = [ - 'and', - ['==', ['get', '/specversion'], '1.0'], - ['starts', ['get', '/type'], 'com.example.'], - ['in', ['get', '/datacontenttype'], [['application/octet-stream', 'application/json']]], - ['==', ['$', '/data/appinfoA'], 'abc'], -]; - -const jsonLogicExpression = { - and: [ - {'==': [{var: 'specversion'}, '1.0']}, - {'==': [{substr: [{var: 'type'}, 0, 12]}, 'com.example.']}, - {in: [{var: 'datacontenttype'}, ['application/octet-stream', 'application/json']]}, - {'==': [{var: 'data.appinfoA'}, 'abc']}, - ], -}; - -const codegen = new JsonExpressionCodegen({expression, operators: operatorsMap}); -const fn = codegen.run().compile(); - -const suite = new Benchmark.Suite(); -suite - .add(`json-joy/json-expression JsonExpressionCodegen`, () => { - fn({vars: new Vars(json)}); - }) - .add(`json-joy/json-expression JsonExpressionCodegen with codegen`, () => { - const codegen = new JsonExpressionCodegen({expression, operators: operatorsMap}); - const fn = codegen.run().compile(); - fn({vars: new Vars(json)}); - }) - .add(`json-joy/json-expression evaluate`, () => { - evaluate(expression, {vars: new Vars(json)}); - }) - .add(`json-logic-js`, () => { - jsonLogic.apply(jsonLogicExpression, json); - }) - .on('cycle', (event: any) => { - console.log(String(event.target) + `, ${Math.round(1000000000 / event.target.hz)} ns/op`); - }) - .on('complete', () => { - console.log('Fastest is ' + suite.filter('fastest').map('name')); - }) - .run(); diff --git a/src/json-expression/__tests__/codegen.spec.ts b/src/json-expression/__tests__/codegen.spec.ts deleted file mode 100644 index 9f831cd74a..0000000000 --- a/src/json-expression/__tests__/codegen.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {Vars} from '../Vars'; -import {JsonExpressionCodegen} from '../codegen'; -import {operatorsMap} from '../operators'; -import {Expr, JsonExpressionCodegenContext} from '../types'; -import {jsonExpressionCodegenTests} from './jsonExpressionCodegenTests'; -import {jsonExpressionEvaluateTests} from './jsonExpressionEvaluateTests'; -import {jsonExpressionUnitTests} from './jsonExpressionUnitTests'; - -const check = ( - expression: Expr, - expected: unknown, - data: unknown = null, - options: JsonExpressionCodegenContext = {}, -) => { - const codegen = new JsonExpressionCodegen({ - ...options, - expression, - operators: operatorsMap, - }); - const fn = codegen.run().compile(); - const result = fn(new Vars(data)); - expect(result).toStrictEqual(expected); -}; - -jsonExpressionUnitTests(check); -jsonExpressionCodegenTests(check); -jsonExpressionEvaluateTests(check); diff --git a/src/json-expression/__tests__/evaluate.spec.ts b/src/json-expression/__tests__/evaluate.spec.ts deleted file mode 100644 index 65f1ccc6dc..0000000000 --- a/src/json-expression/__tests__/evaluate.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {Vars} from '../Vars'; -import {evaluate} from '../evaluate'; -import {Expr, JsonExpressionCodegenContext} from '../types'; -import {jsonExpressionCodegenTests} from './jsonExpressionCodegenTests'; -import {jsonExpressionEvaluateTests} from './jsonExpressionEvaluateTests'; -import {jsonExpressionUnitTests} from './jsonExpressionUnitTests'; - -const check = ( - expression: Expr, - expected: unknown, - data: unknown = null, - options: JsonExpressionCodegenContext = {}, -) => { - const res = evaluate(expression, {...options, vars: new Vars(data)}); - expect(res).toStrictEqual(expected); -}; - -jsonExpressionUnitTests(check); -jsonExpressionEvaluateTests(check); -jsonExpressionCodegenTests(check, {skipOperandArityTests: true}); diff --git a/src/json-expression/__tests__/impure.spec.ts b/src/json-expression/__tests__/impure.spec.ts deleted file mode 100644 index ffa493f78f..0000000000 --- a/src/json-expression/__tests__/impure.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {Vars} from '../Vars'; -import {JsonExpressionCodegen} from '../codegen'; -import {operatorsMap} from '../operators'; -import {Expr, JsonExpressionCodegenContext} from '../types'; - -const compile = (expression: Expr, options: JsonExpressionCodegenContext = {}) => { - const codegen = new JsonExpressionCodegen({ - ...options, - expression, - operators: operatorsMap, - }); - const fn = codegen.run().compile(); - return (data: unknown) => fn(new Vars(data)); -}; - -test('can execute expression twice with different inputs', () => { - const fn = compile(['+', 1, ['$', '']]); - expect(fn(2)).toBe(3); - expect(fn(3)).toBe(4); -}); - -test('constant expression is collapsed', () => { - const fn = compile(['+', 1, 2]); - expect(fn(2)).toBe(3); - expect(fn(3)).toBe(3); -}); - -test('linked in dependencies are linked only once', () => { - const fn = compile(['/', ['/', ['$', ''], 2], 3]); - expect(fn(24)).toBe(4); - // Check that "slash" function is linked only once. -}); diff --git a/src/json-expression/__tests__/jsonExpressionCodegenTests.ts b/src/json-expression/__tests__/jsonExpressionCodegenTests.ts deleted file mode 100644 index 13efa73a12..0000000000 --- a/src/json-expression/__tests__/jsonExpressionCodegenTests.ts +++ /dev/null @@ -1,816 +0,0 @@ -import {Expr, JsonExpressionCodegenContext} from '../types'; - -export type Check = ( - expression: Expr, - expected: unknown, - data?: unknown, - options?: JsonExpressionCodegenContext, -) => void; - -export const jsonExpressionCodegenTests = ( - check: Check, - {skipOperandArityTests}: {skipOperandArityTests?: boolean} = {}, -) => { - describe('Codegen tests', () => { - describe('get', () => { - test('can pick from object', () => { - check(['get', '/foo'], 'bar', {foo: 'bar'}); - check(['$', '/foo'], 'bar', {foo: 'bar'}); - check(['$', '/baz', 123], 123, {foo: 'bar'}); - }); - - test('can pick using expression', () => { - check(['get', ['get', '/pointer']], 'bar', {foo: 'bar', pointer: '/foo'}); - }); - - test('can pick itself recursively', () => { - check(['$', ['$', '/pointer']], '/pointer', {foo: 'bar', pointer: '/pointer'}); - }); - }); - - describe('eq', () => { - test('on two literals', () => { - check(['==', 1, 2], false); - check(['==', {foo: 'bar'}, {foo: 'bar'}], true); - check(['==', {foo: 'bar'}, {foo: 'baz'}], false); - check(['==', [[]], [[]]], true); - }); - - test('literal and expression', () => { - check(['eq', 3, ['$', '/foo', null]], false); - check(['eq', 'bar', ['eq', 1, 1]], false); - check(['eq', true, ['eq', 1, 1]], true); - }); - - test('together with get', () => { - check(['eq', 3, ['$', '/foo']], true, {foo: 3}); - check(['eq', ['$', '/foo'], ['$', '/foo']], true, {foo: 3}); - check(['eq', ['$', '/foo'], ['$', '/bar']], true, {foo: 3, bar: 3}); - }); - }); - - describe('ne', () => { - test('on two literals', () => { - check(['!=', 1, 2], true); - check(['!=', {foo: 'bar'}, {foo: 'bar'}], false); - check(['!=', {foo: 'bar'}, {foo: 'baz'}], true); - check(['!=', [[]], [[]]], false); - }); - - test('literal and expression', () => { - check(['ne', 3, ['$', '/foo', null]], true); - check(['ne', 'bar', ['eq', 1, 1]], true); - check(['!=', true, ['eq', 1, 1]], false); - }); - - test('together with get', () => { - check(['ne', 3, ['$', '/foo']], false, {foo: 3}); - }); - }); - - describe('not', () => { - test('on two literals', () => { - check(['!', ['==', 1, 2]], true); - check(['!', ['==', {foo: 'bar'}, {foo: 'bar'}]], false); - check(['not', ['==', {foo: 'bar'}, {foo: 'baz'}]], true); - check(['not', ['==', [[]], [[]]]], false); - }); - - test('literal and expression', () => { - check(['!', ['eq', 3, ['$', '/foo', null]]], true); - check(['not', ['eq', 'bar', ['eq', 1, 1]]], true); - check(['not', ['eq', true, ['eq', 1, 1]]], false); - }); - - test('together with get', () => { - check(['!', ['eq', 3, ['$', '/foo']]], false, {foo: 3}); - }); - }); - - describe('if', () => { - test('works as ternary conditional expression', () => { - check(['if', true, 1, 2], 1); - check(['if', false, 1, 2], 2); - check(['?', true, 1, 2], 1); - check(['?', false, 1, 2], 2); - }); - - test('all operands are expressions', () => { - const data = { - foo: 1, - bar: 2, - baz: 3, - }; - check(['if', ['$', '/foo'], ['$', '/bar'], ['$', '/baz']], 2, data); - check(['if', ['>', ['$', '/foo'], 10], ['$', '/bar'], ['$', '/baz']], 3, data); - }); - }); - - describe('and', () => { - test('two operand case', () => { - check(['and', true, true], true); - check(['and', true, false], false); - check(['and', false, false], false); - check(['and', false, true], false); - check(['&&', true, true], true); - check(['&&', true, false], false); - check(['&&', false, false], false); - check(['&&', false, true], false); - }); - - test('two operand case', () => { - check(['and', 1, 1], 1); - check(['and', 1, 0], 0); - check(['and', 0, 1], 0); - check(['and', 0, 0], 0); - }); - - test('three operand case', () => { - check(['and', true, true, true], true); - check(['and', true, false, true], false); - }); - - test('operands are expressions', () => { - check(['and', ['get', '/0'], ['get', '/0']], 1, [1, 0]); - check(['and', ['get', '/0'], ['get', '/1']], 0, [1, 0]); - check(['and', ['get', '/0'], 1], 1, [1, 0]); - check(['and', ['get', '/0'], 0], 0, [1, 0]); - }); - }); - - describe('or', () => { - test('two operand case', () => { - check(['or', true, true], true); - check(['or', true, false], true); - check(['or', false, false], false); - check(['or', false, true], true); - check(['||', true, true], true); - check(['||', true, false], true); - check(['||', false, false], false); - check(['||', false, true], true); - }); - - test('two operand case - numbers', () => { - check(['or', 1, 1], 1); - check(['or', 1, 0], 1); - check(['or', 0, 1], 1); - check(['or', 0, 0], 0); - }); - - test('three operand case', () => { - check(['or', true, true, true], true); - check(['or', true, false, true], true); - check(['or', false, false, false], false); - }); - - test('operands are expressions', () => { - check(['or', ['get', '/0'], ['get', '/0']], 1, [1, 0]); - check(['or', ['get', '/0'], ['get', '/1']], 1, [1, 0]); - check(['or', ['get', '/0'], 1], 1, [1, 0]); - check(['or', ['get', '/0'], 0], 1, [1, 0]); - check(['or', ['get', '/1'], 0], 0, [1, 0]); - }); - }); - - describe('type', () => { - test('when operand is literal', () => { - check(['type', 1], 'number'); - check(['type', true], 'boolean'); - check(['type', null], 'null'); - check(['type', 'asdf'], 'string'); - check(['type', [[]]], 'array'); - check(['type', {}], 'object'); - }); - - test('when operand is expression', () => { - check(['type', ['get', '/foo']], 'number', {foo: 1}); - check(['type', ['get', '/foo']], 'boolean', {foo: false}); - check(['type', ['get', '/foo']], 'null', {foo: null}); - check(['type', ['get', '/foo']], 'string', {foo: ''}); - check(['type', ['get', '/foo']], 'array', {foo: []}); - check(['type', ['get', '/foo']], 'object', {foo: {}}); - }); - }); - - describe('bool', () => { - test('when operand is literal', () => { - check(['bool', 1], true); - check(['bool', 0], false); - check(['bool', 0.0], false); - check(['bool', ''], false); - check(['bool', 'asdf'], true); - check(['bool', {}], true); - check(['bool', [[]]], true); - check(['bool', true], true); - check(['bool', false], false); - check(['bool', null], false); - }); - - test('when operand is expression', () => { - check(['bool', ['get', '/foo']], true, {foo: 1}); - check(['bool', ['get', '/foo']], false, {foo: 0}); - }); - }); - - describe('num', () => { - test('when operand is literal', () => { - check(['num', 1], 1); - check(['num', 0], 0); - check(['num', 0.0], 0.0); - check(['num', ''], 0); - check(['num', '1'], 1); - check(['num', '1.1'], 1.1); - check(['num', '1.6'], 1.6); - check(['num', 'asdf'], 0); - check(['num', {}], 0); - check(['num', [[]]], 0); - check(['num', true], 1); - check(['num', false], 0); - check(['num', null], 0); - }); - - test('when operand is expression', () => { - check(['num', ['get', '/foo']], 1, {foo: 1}); - check(['num', ['get', '/foo']], 5, {foo: '5'}); - }); - }); - - describe('starts', () => { - test('when operands are literals', () => { - check(['starts', 'asdf', 'as'], true); - check(['starts', 'asdf', 'az'], false); - }); - - test('when "inner" operand is literal', () => { - check(['starts', ['get', '/a'], 'docu'], true, {a: 'document-123', b: 'doc'}); - }); - - test('when operands are expressions', () => { - check(['starts', ['get', '/a'], ['get', '/b']], true, {a: 'document-123', b: 'doc'}); - check(['starts', ['get', '/a'], 'document-'], true, {a: 'document-123', b: 'doc'}); - check(['starts', ['get', '/a'], 'document2-'], false, {a: 'document-123', b: 'doc'}); - }); - }); - - describe('contains', () => { - test('when operands are literals', () => { - check(['contains', 'asdf', 'as'], true); - check(['contains', 'asdf', 'az'], false); - check(['contains', 'zzasdf', 'az'], false); - check(['contains', 'az', 'az'], true); - check(['contains', '1az', 'az'], true); - check(['contains', '1az2', 'az'], true); - }); - - test('when operands are expressions', () => { - check(['contains', ['get', '/a'], ['get', '/b']], true, {a: 'document-123', b: 'me'}); - check(['contains', ['get', '/a'], ['get', '/b']], true, {a: 'document-123', b: 'do'}); - check(['contains', ['get', '/a'], ['get', '/b']], true, {a: 'document-123', b: '123'}); - check(['contains', ['get', '/a'], ['get', '/b']], false, {a: 'document-123', b: 'me__'}); - }); - }); - - describe('ends', () => { - test('when operands are literals', () => { - check(['ends', 'asdf', 'df'], true); - check(['ends', 'asdf', 'f'], true); - check(['ends', 'asdf', 'f3'], false); - }); - - test('when "inner" operand is literals', () => { - check(['ends', ['get', '/a'], '-123'], true, {a: 'document-123', b: '-123'}); - expect(() => check(['ends', ['get', '/a'], '-1234'], true, {a: 'document-123', b: '-123'})).toThrow(); - }); - - test('when operands are expressions', () => { - check(['ends', ['get', '/a'], ['get', '/b']], true, {a: 'document-123', b: '-123'}); - check(['ends', ['get', '/a'], ['get', '/b']], false, {a: 'document-123', b: '-1234'}); - }); - }); - - describe('matches', () => { - if (!skipOperandArityTests) { - test('throws on too few operands', () => { - expect(() => check(['matches', 'asdf'] as any, '')).toThrowError( - new Error('"matches" operator expects 2 operands.'), - ); - expect(() => check(['matches', 'asdf', 'asdf', 'asdf'] as any, '')).toThrowError( - new Error('"matches" operator expects 2 operands.'), - ); - }); - } - - test('throws when pattern is not a string', () => { - expect(() => check(['matches', 'adsf', 123 as any], 123)).toThrowError( - new Error('"matches" second argument should be a regular expression string.'), - ); - }); - - test('works with literals', () => { - check( - ['matches', 'aaabbb', 'a{3}b{3}'], - true, - {}, - { - createPattern: (pattern: string) => { - const regex = new RegExp(pattern); - return (subject: string) => regex.test(subject); - }, - }, - ); - }); - - test('works with expressions', () => { - check( - ['matches', ['$', '/foo'], 'a{3}b{3}'], - true, - { - foo: 'aaabbb', - }, - { - createPattern: (pattern: string) => { - const regex = new RegExp(pattern); - return (subject: string) => regex.test(subject); - }, - }, - ); - check( - ['matches', ['$', '/foo'], 'a{3}b{3}'], - false, - { - foo: 'aabbb', - }, - { - createPattern: (pattern: string) => { - const regex = new RegExp(pattern); - return (subject: string) => regex.test(subject); - }, - }, - ); - }); - }); - - describe('$?', () => { - if (!skipOperandArityTests) { - test('accepts only one operand', () => { - const callback = () => check(['$?', '/foo', '/bar'] as any, true, {foo: 123}); - expect(callback).toThrowError(new Error('"$?" operator expects 1 operands.')); - }); - } - - test('validates JSON Pointer', () => { - const callback = () => check(['$?', null] as any, true, {foo: 123}); - expect(callback).toThrowError(new Error('varname must be a string.')); - }); - - test('check if data member is defined', () => { - check(['$?', '/foo'], true, {foo: [0, 1]}); - check(['$?', '/foo/0'], true, {foo: [0, 1]}); - check(['$?', '/foo/1'], true, {foo: [0, 1]}); - check(['$?', '/foo/2'], false, {foo: [0, 1]}); - check(['$?', '/bar'], false, {foo: [0, 1]}); - }); - }); - - describe('in', () => { - test('works with literals', () => { - check(['in', [[]], 'foo'], false, {foo: 'bar'}); - check(['in', [['a']], 'foo'], false, {foo: 'bar'}); - check(['in', [['foo']], 'foo'], true, {foo: 'bar'}); - check(['in', [['a', {b: 'b'}]], 'foo'], false, {foo: 'bar'}); - check(['in', [['a', {b: 'b'}]], {b: 'b'}], true, {foo: 'bar'}); - }); - - test('works with expressions', () => { - check(['in', [[]], ['$', '/foo']], false, {foo: 'bar'}); - check(['in', [['gg']], ['$', '/foo']], false, {foo: 'bar'}); - check(['in', [['gg', 'bar']], ['$', '/foo']], true, {foo: 'bar'}); - check(['in', [['bar']], ['$', '/foo']], true, {foo: 'bar'}); - check(['in', [['bar1']], ['$', '/foo']], false, {foo: 'bar'}); - check(['in', [['gg', 'bar', 'ss']], ['$', '/foo']], true, {foo: 'bar'}); - check(['in', ['$', '/lol'], ['$', '/foo']], true, {foo: 'bar', lol: ['gg', 'bar', 'ss']}); - check(['in', ['$', '/lol'], ['$', '/foo']], false, {foo: 'bar', lol: ['gg', 'ss']}); - check(['in', ['$', '/lol'], 'ss'], true, {foo: 'bar', lol: ['gg', 'ss']}); - }); - }); - - describe('cat', () => { - if (!skipOperandArityTests) { - test('throws on too few operands', () => { - expect(() => check(['cat', 'a'], '')).toThrowError(new Error('"." operator expects at least two operands.')); - }); - } - - test('works with literals', () => { - check(['cat', 'a', 'ds'], 'ads'); - }); - - test('works with expressions', () => { - check(['cat', ['get', '/2'], ['get', '/1'], ['get', '/0']], 'cba', ['a', 'b', 'c']); - }); - }); - - describe('substr', () => { - if (!skipOperandArityTests) { - test('throws on too few or too many operands', () => { - expect(() => check(['substr', 'str'] as any, '')).toThrowError( - new Error('"substr" operator expects 3 operands.'), - ); - expect(() => check(['substr', 'str', 1, 1, 1] as any, '')).toThrowError( - new Error('"substr" operator expects 3 operands.'), - ); - }); - } - - test('works with literals', () => { - check(['substr', '0123456789', 0, 3], '012'); - check(['substr', '0123456789', 1, 3], '12'); - check(['substr', '0123456789', -4, 3], ''); - check(['substr', '0123456789', 7, 7 + 4], '789'); - }); - - test('works with expressions', () => { - check(['substr', ['$', '/str'], 0, 3], '012', {str: '0123456789'}); - check(['substr', ['$', '/str'], ['$', '/from'], 2 + 3], '234', {str: '0123456789', from: 2}); - check(['substr', ['$', '/str'], ['$', '/from'], ['$', '/len']], '23', {str: '0123456789', from: 2, len: 2 + 2}); - }); - }); - - describe('less than', () => { - if (!skipOperandArityTests) { - test('throws on too few or too many operands', () => { - expect(() => check(['<', 1] as any, '')).toThrowError(new Error('"<" operator expects 2 operands.')); - expect(() => check(['<', 1, 2, 3] as any, '')).toThrowError(new Error('"<" operator expects 2 operands.')); - }); - } - - test('works with literals', () => { - check(['<', 1, 2.4], true); - check(['<', 3.33, 3.333], true); - check(['<', 1, '2.4'], true); - check(['<', '2.4', 0], false); - }); - - test('works with expressions', () => { - check(['<', ['$', '/0'], ['$', '/1']], true, [1, 2.4]); - check(['<', ['$', '/0'], ['$', '/1']], true, [3.33, 3.333]); - check(['<', ['$', '/1'], ['$', '/0']], false, [1, 2.4]); - check(['<', ['$', '/1'], ['$', '/1']], false, [1, 2.4]); - check(['<', ['$', '/0'], ['$', '/0']], false, [0, 2.4]); - }); - }); - - describe('less than or equal', () => { - if (!skipOperandArityTests) { - test('throws on too few or too many operands', () => { - expect(() => check(['<=', 1] as any, '')).toThrowError(new Error('"<=" operator expects 2 operands.')); - expect(() => check(['<=', 1, 2, 3] as any, '')).toThrowError(new Error('"<=" operator expects 2 operands.')); - }); - } - - test('works with literals', () => { - check(['<=', 1, 2.4], true); - check(['<=', 1, '2.4'], true); - check(['<=', 3.33, 3.333], true); - check(['<=', '2.4', 0], false); - check(['<=', 0, 0], true); - }); - - test('works with expressions', () => { - check(['<=', ['$', '/0'], ['$', '/1']], true, [1, 2.4]); - check(['<=', ['$', '/0'], ['$', '/1']], true, [3.33, 3.333]); - check(['<=', ['$', '/1'], ['$', '/0']], false, [1, 2.4]); - check(['<=', ['$', '/1'], ['$', '/1']], true, [1, 2.4]); - check(['<=', ['$', '/0'], ['$', '/0']], true, [0, 2.4]); - }); - }); - - describe('greater than', () => { - if (!skipOperandArityTests) { - test('throws on too few or too many operands', () => { - expect(() => check(['>', 1] as any, '')).toThrowError(new Error('">" operator expects 2 operands.')); - expect(() => check(['>', 1, 2, 3] as any, '')).toThrowError(new Error('">" operator expects 2 operands.')); - }); - } - - test('works with literals', () => { - check(['>', 1, 2.4], false); - check(['>', 1, '2.4'], false); - check(['>', '2.4', 0], true); - check(['>', 3.333, 3.33], true); - check(['>', 0, 0], false); - }); - - test('works with expressions', () => { - check(['>', ['$', '/0'], ['$', '/1']], false, [1, 2.4]); - check(['>', ['$', '/1'], ['$', '/0']], true, [1, 2.4]); - check(['>', ['$', '/0'], ['$', '/1']], true, [3.333, 3.33]); - check(['>', ['$', '/1'], ['$', '/1']], false, [1, 2.4]); - check(['>', ['$', '/0'], ['$', '/0']], false, [0, 2.4]); - }); - }); - - describe('greater than or equal', () => { - if (!skipOperandArityTests) { - test('throws on too few or too many operands', () => { - expect(() => check(['>=', 1] as any, '')).toThrowError(new Error('">=" operator expects 2 operands.')); - expect(() => check(['>=', 1, 2, 3] as any, '')).toThrowError(new Error('">=" operator expects 2 operands.')); - }); - } - - test('works with literals', () => { - check(['>=', 1, 2.4], false); - check(['>=', 1, '2.4'], false); - check(['>=', '2.4', 0], true); - check(['>=', 3.333, 3.33], true); - check(['>=', 0, 0], true); - }); - - test('works with expressions', () => { - check(['>=', ['$', '/0'], ['$', '/1']], false, [1, 2.4]); - check(['>=', ['$', '/1'], ['$', '/0']], true, [1, 2.4]); - check(['>=', ['$', '/0'], ['$', '/1']], true, [3.333, 3.33]); - check(['>=', ['$', '/1'], ['$', '/1']], true, [1, 2.4]); - check(['>=', ['$', '/0'], ['$', '/0']], true, [0, 2.4]); - }); - }); - - describe('between', () => { - if (!skipOperandArityTests) { - test('throws on too few or too many operands', () => { - expect(() => check(['><', 1] as any, '')).toThrowError(new Error('"><" operator expects 3 operands.')); - expect(() => check(['><', 1, 2] as any, '')).toThrowError(new Error('"><" operator expects 3 operands.')); - expect(() => check(['><', 1, 2, 3, 4] as any, '')).toThrowError( - new Error('"><" operator expects 3 operands.'), - ); - }); - } - - test('ne ne works', () => { - check(['><', 5, 1, 6], true); - check(['><', 5, 5, 6], false); - check(['><', 5, 4.9, 6], true); - check(['><', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [5, 4.9, 6]); - check(['><', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [5, 4.9, 5.1]); - check(['><', ['$', '/0'], ['$', '/1'], ['$', '/2']], false, [5, 4.9, 5]); - }); - - test('eq ne works', () => { - check(['=><', 5, 1, 6], true); - check(['=><', 5, 5, 6], true); - check(['=><', 5, 5, 5], false); - check(['=><', 5, 4.9, 6], true); - check(['=><', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [5, 4.9, 6]); - check(['=><', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [5, 4.9, 5.1]); - check(['=><', ['$', '/0'], ['$', '/1'], ['$', '/2']], false, [5, 4.9, 5]); - check(['=><', ['$', '/0'], ['$', '/1'], ['$', '/2']], false, [3, 4.9, 4.9]); - }); - - test('ne eq works', () => { - check(['><=', 5, 1, 6], true); - check(['><=', 5, 5, 6], false); - check(['><=', 5, 5, 5], false); - check(['><=', 5, 4.9, 6], true); - check(['><=', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [5, 4.9, 6]); - check(['><=', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [5, 4.9, 5.1]); - check(['><=', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [5, 4.9, 5]); - check(['><=', ['$', '/0'], ['$', '/1'], ['$', '/2']], false, [3, 3, 4.9]); - check(['><=', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [3, 2.99, 4.9]); - check(['><=', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [3, 2.99, 3]); - }); - - test('eq eq works', () => { - check(['=><=', 5, 1, 6], true); - check(['=><=', 5, 5, 6], true); - check(['=><=', 5, 5.01, 6], false); - check(['=><=', 5, 5, 5], true); - check(['=><=', 5, 4.9, 6], true); - check(['=><=', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [5, 4.9, 6]); - check(['=><=', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [5, 4.9, 5.1]); - check(['=><=', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [5, 4.9, 5]); - check(['=><=', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [3, 3, 4.9]); - check(['=><=', ['$', '/0'], ['$', '/1'], ['$', '/2']], false, [3, 3.01, 4.9]); - check(['=><=', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [3, 2.99, 4.9]); - check(['=><=', ['$', '/0'], ['$', '/1'], ['$', '/2']], true, [3, 2.99, 3]); - }); - }); - - describe('min', () => { - if (!skipOperandArityTests) { - test('throws on too few operands', () => { - expect(() => check(['min', 1] as any, '')).toThrowError( - new Error('"min" operator expects at least two operands.'), - ); - }); - } - - test('works with literals', () => { - check(['min', 1, 2], 1); - check(['min', 1, 2, null], 0); - check(['min', 1, 2, 0.4], 0.4); - check(['min', 1, 2, 0.4, '.1'], 0.1); - }); - - test('works with expressions', () => { - check(['min', ['$', '/1'], ['$', '/2'], ['$', '/0']], 3.3, [3.3, 4.4, 5.5]); - }); - }); - - describe('max', () => { - if (!skipOperandArityTests) { - test('throws on too few operands', () => { - expect(() => check(['max', 1] as any, '')).toThrowError( - new Error('"max" operator expects at least two operands.'), - ); - }); - } - - test('works with literals', () => { - check(['max', 1, 2], 2); - check(['max', 1, 2, 2.4], 2.4); - check(['max', 1, 2, 2.4, '4.1'], 4.1); - }); - - test('works with expressions', () => { - check(['max', ['$', '/1'], ['$', '/2'], ['$', '/0']], 5.5, [3.3, 4.4, 5.5]); - }); - }); - - describe('plus', () => { - if (!skipOperandArityTests) { - test('throws on too few operands', () => { - expect(() => check(['+', 1] as any, '')).toThrowError( - new Error('"+" operator expects at least two operands.'), - ); - }); - } - - test('works with literals', () => { - check(['+', 1, 2, 3, 4], 10); - }); - - test('does not concatenate strings', () => { - check(['+', '1', 1], 2); - check(['+', ['$', '/0'], ['$', '/1']], 2, ['1', 1]); - }); - - test('works with expressions', () => { - check(['+', ['$', '/0'], ['$', '/1'], ['$', '/2'], ['$', '/3']], 10, [1, 2, 3, 4]); - }); - }); - - describe('minus', () => { - if (!skipOperandArityTests) { - test('throws on too few operands', () => { - expect(() => check(['-', 1] as any, '')).toThrowError( - new Error('"-" operator expects at least two operands.'), - ); - }); - } - - test('works with literals', () => { - check(['-', 4, 1, 2, 3], -2); - }); - - test('works with expressions', () => { - check(['-', ['$', '/0'], ['$', '/1'], ['$', '/2'], ['$', '/3']], -8, [1, 2, 3, 4]); - }); - }); - - describe('multiplication', () => { - if (!skipOperandArityTests) { - test('throws on too few operands', () => { - expect(() => check(['*', 1] as any, '')).toThrowError( - new Error('"*" operator expects at least two operands.'), - ); - }); - } - - test('works with literals', () => { - check(['*', 1, 2, 3, 4], 24); - }); - - test('works with expressions', () => { - check(['*', ['$', '/0'], ['$', '/1'], ['$', '/2'], ['$', '/3']], 24, [1, 2, 3, 4]); - }); - }); - - describe('division', () => { - if (!skipOperandArityTests) { - test('throws on too few operands', () => { - expect(() => check(['/', 1] as any, '')).toThrowError( - new Error('"/" operator expects at least two operands.'), - ); - }); - } - - test('works with literals', () => { - check(['/', 1, 1], 1); - check(['/', 5, 2], 2.5); - }); - - test('works with expressions', () => { - check(['/', ['$', '/0'], ['$', '/1']], 0.5, [1, 2]); - check(['/', ['$', '/0'], ['$', '/1']], 1, [1, 1]); - }); - }); - - describe('mod', () => { - if (!skipOperandArityTests) { - test('throws on too few operands', () => { - expect(() => check(['%', 1] as any, '')).toThrowErrorMatchingInlineSnapshot( - `""%" operator expects at least two operands."`, - ); - }); - } - - test('works with literals', () => { - check(['%', 1, 1], 0); - check(['%', 5, 2], 1); - }); - - test('works with expressions', () => { - check(['%', ['$', '/0'], ['$', '/1']], 1, [1, 2]); - check(['%', ['$', '/0'], ['$', '/1']], 1, [5, 2]); - check(['%', ['$', '/0'], ['$', '/1']], 3, [7, 4]); - }); - }); - - describe('round', () => { - if (!skipOperandArityTests) { - test('throws on too few operands', () => { - expect(() => check(['round', 1, 1] as any, '')).toThrowError( - new Error('"round" operator expects 1 operands.'), - ); - }); - } - - test('works with literals', () => { - check(['round', 1.5], 2); - check(['round', 1.3], 1); - check(['round', 1], 1); - check(['round', '3.6'], 4); - check(['round', 3.6], 4); - }); - - test('works with expressions', () => { - check(['round', ['$', '/0']], 2, [1.5]); - check(['round', ['$', '/0']], 1, [1]); - check(['round', ['$', '/0']], 4, ['3.6']); - check(['round', ['$', '/0']], 4, [3.6]); - }); - }); - - describe('ceil', () => { - if (!skipOperandArityTests) { - test('throws on too few operands', () => { - expect(() => check(['ceil', 1, 1] as any, '')).toThrowError(new Error('"ceil" operator expects 1 operands.')); - }); - } - - test('works with literals', () => { - check(['ceil', 1.5], 2); - check(['ceil', 1.3], 2); - check(['ceil', 1], 1); - check(['ceil', '3.6'], 4); - check(['ceil', 3.6], 4); - }); - - test('works with expressions', () => { - check(['ceil', ['$', '/0']], 2, [1.5]); - check(['ceil', ['$', '/0']], -1, [-1.2]); - check(['ceil', ['$', '/0']], -1, [-1.8]); - check(['ceil', ['$', '/0']], 1, [1]); - check(['ceil', ['$', '/0']], 4, ['3.6']); - check(['ceil', ['$', '/0']], 4, [3.6]); - }); - }); - - describe('floor', () => { - if (!skipOperandArityTests) { - test('throws on too few operands', () => { - expect(() => check(['floor', 1, 1] as any, '')).toThrowError( - new Error('"floor" operator expects 1 operands.'), - ); - }); - } - - test('works with literals', () => { - check(['floor', 1.5], 1); - check(['floor', 1.3], 1); - check(['floor', 1], 1); - check(['floor', '3.6'], 3); - check(['floor', 3.6], 3); - }); - - test('works with expressions', () => { - check(['floor', ['$', '/0']], 1, [1.5]); - check(['floor', ['$', '/0']], -2, [-1.2]); - check(['floor', ['$', '/0']], -2, [-1.8]); - check(['floor', ['$', '/0']], 1, [1]); - check(['floor', ['$', '/0']], 3, ['3.6']); - check(['floor', ['$', '/0']], 3, [3.6]); - }); - }); - }); -}; diff --git a/src/json-expression/__tests__/jsonExpressionEvaluateTests.ts b/src/json-expression/__tests__/jsonExpressionEvaluateTests.ts deleted file mode 100644 index 2f98bcba1a..0000000000 --- a/src/json-expression/__tests__/jsonExpressionEvaluateTests.ts +++ /dev/null @@ -1,479 +0,0 @@ -import {Vars} from '../Vars'; -import {evaluate} from '../evaluate'; -import {Expr} from '../types'; -import {Check} from './jsonExpressionCodegenTests'; - -export const jsonExpressionEvaluateTests = (check: Check) => { - describe('Evaluate tests', () => { - describe('get', () => { - test('can pick from data', () => { - const data = { - a: { - b: { - c: 1, - }, - }, - }; - const expression = ['$', '/a/b/c']; - const res = evaluate(expression, {vars: new Vars(data)}); - expect(res).toBe(1); - }); - - test('can pick from data with "get" expression', () => { - const data = { - a: { - b: { - c: 1, - }, - }, - }; - const expression = ['get', '/a/b/c']; - const res = evaluate(expression, {vars: new Vars(data)}); - expect(res).toBe(1); - }); - }); - - describe('and', () => { - test('works in base case', () => { - check(['&&', true, true], true, null); - check(['&&', true, false], false, null); - check(['&&', false, true], false, null); - check(['&&', false, false], false, null); - check(['and', true, true], true, null); - check(['and', true, false], false, null); - check(['and', false, true], false, null); - check(['and', false, false], false, null); - }); - - test('works with number', () => { - check(['&&', 1, 1], 1, null); - check(['&&', 1, 0], 0, null); - check(['&&', 0, 1], 0, null); - check(['&&', 0, 0], 0, null); - }); - - test('true on multiple truthy values', () => { - const data = { - true: true, - false: false, - one: 1, - zero: 0, - }; - check(['&&', ['$', '/true'], ['$', '/one'], ['$', '/true']], true, data); - check(['&&', ['$', '/true'], ['$', '/one']], 1, data); - }); - - test('false on single falsy value', () => { - const data = { - true: true, - false: false, - one: 1, - zero: 0, - }; - check(['&&', ['$', '/true'], ['$', '/one'], ['$', '/zero']], 0, data); - }); - }); - - describe('eq', () => { - test('equals return true', () => { - const data = { - true: true, - false: false, - one: 1, - zero: 0, - }; - check(['eq', ['$', '/true'], true], true, data); - check(['eq', {foo: 'bar'}, {foo: 'bar'}], true, data); - check(['==', {foo: 'bar'}, {foo: 'bar'}], true, data); - check(['eq', {foo: 'bar'}, {foo: 'baz'}], false, data); - check(['==', {foo: 'bar'}, {foo: 'baz'}], false, data); - }); - }); - - describe('in', () => { - test('can deeply match one of multiple values', () => { - const data = { - contentType: 'application/json', - data: { - foo: 'bar', - }, - }; - check(['in', [['application/octet-stream', 'application/json']], ['get', '/contentType']], true, data); - check(['in', [['application/json']], ['get', '/contentType']], true, data); - check(['in', [['application/octet-stream', 'application/json2']], ['get', '/contentType']], false, data); - check(['in', [[{}]], ['get', '/data']], false, data); - check(['in', [[{foo: 'bar'}]], ['get', '/data']], true, data); - }); - }); - - describe('ne', () => { - test('equals return true', () => { - const data = { - true: true, - false: false, - one: 1, - zero: 0, - }; - check(['ne', ['$', '/true'], true], false, data); - check(['ne', {foo: 'bar'}, {foo: 'bar'}], false, data); - check(['!=', {foo: 'bar'}, {foo: 'bar'}], false, data); - check(['ne', {foo: 'bar'}, {foo: 'baz'}], true, data); - check(['!=', {foo: 'bar'}, {foo: 'baz'}], true, data); - }); - }); - - describe('if', () => { - test('works', () => { - const data = { - true: true, - false: false, - one: 1, - zero: 0, - }; - check(['if', true, ['$', '/one'], ['$', '/true']], 1, data); - check(['if', false, ['$', '/one'], ['$', '/true']], true, data); - check(['?', true, '1', '2'], '1', data); - check(['?', 0, '1', '2'], '2', data); - check(['?', ['get', '/true'], '1', '2'], '1', data); - }); - }); - - describe('or', () => { - test('works in base case', () => { - check(['||', true, true], true, null); - check(['||', true, false], true, null); - check(['||', false, true], true, null); - check(['||', false, false], false, null); - check(['or', true, true], true, null); - check(['or', true, false], true, null); - check(['or', false, true], true, null); - check(['or', false, false], false, null); - }); - }); - - describe('not', () => { - test('works in base case', () => { - check(['!', true], false, null); - check(['!', false], true, null); - check(['not', true], false, null); - check(['not', false], true, null); - }); - }); - - describe('type', () => { - test('returns value types', () => { - check(['type', null], 'null'); - check(['type', 123], 'number'); - check(['type', [[]]], 'array'); - check(['type', {}], 'object'); - check(['type', ''], 'string'); - check(['type', false], 'boolean'); - }); - }); - - describe('defined', () => { - test('works', () => { - const data = {foo: 'bar'}; - check(['$?', '/foo'], true, data); - check(['get?', '/foo2'], false, data); - }); - }); - - describe('bool', () => { - test('converts value to boolean', () => { - check(['bool', null], false); - check(['bool', 123], true); - }); - }); - - describe('num', () => { - test('converts value to number', () => { - check(['num', '123.4'], 123.4); - check(['num', {}], 0); - }); - }); - - describe('str', () => { - test('converts value to string', () => { - check(['str', 123], '123'); - }); - }); - - describe('starts', () => { - test('returns true when string starts with another sub-string', () => { - const data = {a: 'asdf', b: 'as'}; - check(['starts', 'asdf', ['$', '/b']], true, data); - check(['starts', ['$', '/a'], ['$', '/b']], true, data); - check(['starts', ['$', '/b'], ['$', '/b']], true, data); - check(['starts', 'gg', ['$', '/b']], false, data); - check(['starts', ['$', '/b'], ['$', '/a']], false, data); - }); - }); - - describe('contains', () => { - test('returns true when string contains another string', () => { - const data = {a: 'asdf', b: 'as'}; - check(['contains', '123456789', '456'], true, data); - check(['contains', '123456789', '1'], true, data); - check(['contains', '123456789', '9'], true, data); - check(['contains', '123456789', 'df'], false, data); - }); - }); - - describe('ends', () => { - test('returns true when string ends with give sub-string', () => { - const data = {a: 'asdf', b: 'as'}; - check(['ends', '123456789', '789'], true, data); - check(['ends', '123456789', '9'], true, data); - check(['ends', '123456789', '78'], false, data); - }); - }); - - describe('cat', () => { - test('works', () => { - check(['cat', '789', '123456789'], '789123456789'); - check(['.', '789', '123456789'], '789123456789'); - check(['.', '1', 'a', 'gg'], '1agg'); - }); - }); - - describe('substr', () => { - test('works', () => { - check(['substr', '12345', 1, 1 + 2], '23'); - }); - }); - - describe('<', () => { - test('works', () => { - check(['<', 1, 2], true); - check(['<', 1, 1.1], true); - check(['<', 1, 1], false); - }); - }); - - describe('<=', () => { - test('works', () => { - check(['<=', 1, 2], true); - check(['<=', 1, 1], true); - check(['<=', 1, 0], false); - }); - }); - - describe('>', () => { - test('works', () => { - check(['>', 2, 1], true); - check(['>', 1, 1], false); - }); - }); - - describe('>=', () => { - test('works', () => { - check(['>=', 2, 1], true); - check(['>=', 1, 1], true); - check(['>=', 0, 1], false); - }); - }); - - describe('min', () => { - test('works', () => { - check(['min', 2, 1], 1); - check(['min', '2', 1], 1); - }); - }); - - describe('max', () => { - test('works', () => { - check(['max', 2, 1], 2); - check(['max', '2', 1], 2); - }); - }); - - describe('+', () => { - test('works', () => { - check(['+', 2, 1, 3], 6); - check(['+', 2, 1, 3.1], 6.1); - }); - }); - - describe('-', () => { - test('works', () => { - check(['-', 2, 1], 1); - check(['-', 5, 1], 4); - check(['-', 5, 1, 3], 1); - }); - }); - - describe('*', () => { - test('works', () => { - check(['*', 2, 1], 2); - check(['*', 1, 2, 3], 6); - }); - }); - - describe('/', () => { - test('works', () => { - check(['/', 6, 2], 3); - }); - }); - - describe('%', () => { - test('works', () => { - check(['%', 6, 2], 0); - check(['%', 6, 4], 2); - }); - }); - - describe('scenarios', () => { - test('can filter messages', () => { - const data = { - chan: 'slides-123', - data: { - type: 'cursor-move', - username: 'uk/hardy', - pos: [309, 123], - }, - }; - - const expression1: Expr = [ - 'and', - ['==', ['get', '/chan'], 'slides-123'], - ['==', ['get', '/data/type'], 'cursor-move'], - ['>', ['$', '/data/pos/0'], 300], - ['starts', ['$', '/data/username'], 'uk/'], - ]; - check(expression1, true, data); - - const expression2: Expr = [ - 'and', - ['==', ['get', '/chan'], 'slides-123'], - ['==', ['get', '/data/type'], 'cursor-move'], - ['>', ['$', '/data/pos/1'], 555], - ['starts', ['$', '/data/username'], 'uk/'], - ]; - check(expression2, false, data); - }); - - describe('feature parity with AWS SNS filtering policies (https://docs.aws.amazon.com/sns/latest/dg/sns-subscription-filter-policies.html)', () => { - // { - // "store": ["example_corp"], - // "event": [{"anything-but": "order_cancelled"}], - // "customer_interests": [ - // "rugby", - // "football", - // "baseball" - // ], - // "price_usd": [{"numeric": [">=", 100]}] - // } - test('can work as AWS sample filtering policy - 1', () => { - const data = { - store: 'example_corp', - event: 'order_created', - customer_interests: 'football', - price_usd: 105.95, - }; - - const expression1: Expr = [ - 'and', - ['==', ['get', '/store'], 'example_corp'], - ['!', ['==', ['get', '/event'], 'order_cancelled']], - ['in', [['rugby', 'football', 'baseball']], ['get', '/customer_interests']], - ['>=', ['$', '/price_usd'], 100], - ]; - check(expression1, true, data); - - const expression2: Expr = [ - 'and', - ['==', ['get', '/store'], 'some_other_example_corp'], - ['!', ['==', ['get', '/event'], 'order_cancelled']], - ['in', [['rugby', 'football', 'baseball']], ['get', '/customer_interests']], - ['>=', ['$', '/price_usd'], 100], - ]; - check(expression2, false, data); - }); - - // "key_b": ["value_one"], - test('can match a single value', () => { - const data = { - key_b: 'value_one', - }; - check(['==', ['get', '/key_b'], 'value_one'], true, data); - check(['==', ['get', '/key_b'], 'value_two'], false, data); - }); - - // "key_a": ["value_one", "value_two", "value_three"], - test('can match multiple values', () => { - const data = { - key_a: 'value_three', - }; - check(['in', [['value_one', 'value_two', 'value_three']], ['get', '/key_a']], true, data); - check(['in', [['value_one', 'value_two', 'value_four']], ['get', '/key_a']], false, data); - }); - - // "price": {"Type": "Number.Array", "Value": "[100, 50]"} - test('can match value in array', () => { - const data = { - price: [100, 50], - }; - check(['in', ['$', '/price'], 100], true, data); - check(['in', ['$', '/price'], 50], true, data); - check(['in', ['$', '/price'], 1], false, data); - }); - - // "customer_interests": [{"prefix": "bas"}] - test('can match by prefix', () => { - const data = { - customer_interests: 'baseball', - }; - check(['starts', ['get', '/customer_interests'], 'bas'], true, data); - check(['starts', ['get', '/customer_interests'], 'rug'], false, data); - }); - - // "customer_interests": [{"anything-but": ["rugby", "tennis"]}] - test('anything but', () => { - const data = { - customer_interests: 'rugby', - }; - check(['!', ['in', [['rugby', 'tennis']], ['get', '/customer_interests']]], false, data); - check(['not', ['in', [['football', 'tennis']], ['get', '/customer_interests']]], true, data); - }); - - // "event": [{"anything-but": {"prefix":"order-"}}] - test('anything but with prefix', () => { - const data = { - event: 'order-return', - }; - check(['!', ['starts', ['get', '/event'], 'order-']], false, data); - check(['not', ['starts', ['get', '/event'], 'log-']], true, data); - }); - - // "source_ip": [{"cidr": "10.0.0.0/24"}] - xtest('IP address matching', () => { - // const data = { - // source_ip: '10.0.0.255', - // }; - // check(['cidr', '10.0.0.0/24', ['get', '/source_ip']], true, data); - }); - - // "price_usd": [{"numeric": [">", 0, "<=", 150]}] - xtest('between operator', () => { - // const data = { - // price_usd: 100, - // }; - // check(['><=', 0, 150, ['/price_usd']], true, data); - }); - - // "store": [{"exists": true}] - // "store": [{"exists": false}] - test('attribute key matching', () => { - const data = { - store: 'Halloween Inc', - }; - check(['$?', '/store'], true, data); - check(['get?', '/foo'], false, data); - check(['!', ['$?', '/store']], false, data); - check(['!', ['$?', '/foo']], true, data); - }); - }); - }); - }); -}; diff --git a/src/json-expression/__tests__/jsonExpressionUnitTests.ts b/src/json-expression/__tests__/jsonExpressionUnitTests.ts deleted file mode 100644 index 0377665706..0000000000 --- a/src/json-expression/__tests__/jsonExpressionUnitTests.ts +++ /dev/null @@ -1,2341 +0,0 @@ -import {Expr, JsonExpressionCodegenContext} from '../types'; - -export type Check = ( - expression: Expr, - expected: unknown, - data?: unknown, - options?: JsonExpressionCodegenContext, -) => void; - -export const jsonExpressionUnitTests = ( - check: Check, - {skipOperandArityTests}: {skipOperandArityTests?: boolean} = {}, -) => { - describe('Arithmetic operators', () => { - describe('add or +', () => { - test('can add numbers', () => { - check(['add', 1, 2], 3); - check(['+', 1, 2], 3); - }); - - test('evaluates sub-expressions', () => { - check(['add', 1, ['add', 1, 1]], 3); - check(['+', 1, ['+', 1, 1]], 3); - }); - - test('is variadic', () => { - check(['add', 1, 1, 1, 1], 4); - check(['+', 1, 2, 3, 4], 10); - }); - - test('casts strings to numbers', () => { - check(['add', '2', '2'], 4); - check(['+', '1', '10.5'], 11.5); - }); - - test('throws on too few arguments', () => { - expect(() => check(['add', 1], 2)).toThrowErrorMatchingInlineSnapshot( - `""+" operator expects at least two operands."`, - ); - expect(() => check(['+', 1], 2)).toThrowErrorMatchingInlineSnapshot( - `""+" operator expects at least two operands."`, - ); - }); - }); - - describe('subtract or -', () => { - test('two operands', () => { - check(['subtract', 1, 2], -1); - check(['-', 1, 2], -1); - }); - - test('evaluates sub-expressions', () => { - check(['subtract', 1, ['subtract', 1, 1]], 1); - check(['-', 1, ['-', 1, 1]], 1); - }); - - test('is variadic', () => { - check(['subtract', 1, 1, 1, 1], -2); - check(['-', 1, 2, 3, 4], -8); - }); - - test('casts strings to numbers', () => { - check(['subtract', '2', '2'], 0); - check(['-', '1', '10.5'], -9.5); - }); - - test('throws on too few arguments', () => { - expect(() => check(['subtract', 1], 2)).toThrowErrorMatchingInlineSnapshot( - `""-" operator expects at least two operands."`, - ); - expect(() => check(['-', 1], 2)).toThrowErrorMatchingInlineSnapshot( - `""-" operator expects at least two operands."`, - ); - }); - }); - - describe('multiply or *', () => { - test('two operands', () => { - check(['multiply', 1, 2], 2); - check(['*', 3, 2], 6); - }); - - test('evaluates sub-expressions', () => { - check(['multiply', 1, ['multiply', 1, 1]], 1); - check(['*', 0.5, ['*', 4, 4]], 8); - }); - - test('is variadic', () => { - check(['multiply', 2, 2, 2, 2], 16); - check(['*', 1, 2, 3, 4], 24); - }); - - test('casts strings to numbers', () => { - check(['multiply', '2', '2'], 4); - check(['*', '1', '10.5'], 10.5); - }); - - test('throws on too few arguments', () => { - expect(() => check(['multiply', 1], 2)).toThrowErrorMatchingInlineSnapshot( - `""*" operator expects at least two operands."`, - ); - expect(() => check(['*', 1], 2)).toThrowErrorMatchingInlineSnapshot( - `""*" operator expects at least two operands."`, - ); - }); - }); - - describe('divide or /', () => { - test('two operands', () => { - check(['divide', 1, 2], 0.5); - check(['/', 3, 2], 1.5); - }); - - test('evaluates sub-expressions', () => { - check(['divide', 1, ['divide', 4, 2]], 0.5); - check(['/', 0.5, ['/', 4, 4]], 0.5); - }); - - test('is variadic', () => { - check(['divide', 2, 2, 2, 2], 0.25); - check(['/', 32, 2, 4, ['+', 1, 1]], 2); - }); - - test('casts strings to numbers', () => { - check(['divide', '4', '2'], 2); - check(['/', '1', '10'], 0.1); - }); - - test('throws on too few arguments', () => { - expect(() => check(['divide', 1], 2)).toThrowErrorMatchingInlineSnapshot( - `""/" operator expects at least two operands."`, - ); - expect(() => check(['/', 1], 2)).toThrowErrorMatchingInlineSnapshot( - `""/" operator expects at least two operands."`, - ); - }); - - test('throws throws when dividing by zero', () => { - expect(() => check(['divide', 1, 0], 0)).toThrowErrorMatchingInlineSnapshot(`"DIVISION_BY_ZERO"`); - expect(() => check(['/', ['+', 1, 1], 0], 0)).toThrowErrorMatchingInlineSnapshot(`"DIVISION_BY_ZERO"`); - }); - }); - - describe('divide or %', () => { - test('two operands', () => { - check(['mod', 1, 2], 1); - check(['%', 3, 2], 1); - }); - - test('evaluates sub-expressions', () => { - check(['mod', 3, ['mod', 4, 3]], 0); - check(['%', 5, ['%', 7, 5]], 1); - }); - - test('is variadic', () => { - check(['mod', 13, 7, 4, 2], 0); - check(['%', 32, 25, 4, ['%', 5, 3]], 1); - }); - - test('casts strings to numbers', () => { - check(['mod', '4', '2'], 0); - check(['%', '1', '10'], 1); - }); - - test('throws on too few arguments', () => { - expect(() => check(['mod', 1], 2)).toThrowErrorMatchingInlineSnapshot( - `""%" operator expects at least two operands."`, - ); - expect(() => check(['%', 1], 2)).toThrowErrorMatchingInlineSnapshot( - `""%" operator expects at least two operands."`, - ); - }); - - test('throws throws when dividing by zero', () => { - expect(() => check(['mod', 1, 0], 0)).toThrowErrorMatchingInlineSnapshot(`"DIVISION_BY_ZERO"`); - expect(() => check(['%', ['+', 1, 1], 0], 0)).toThrowErrorMatchingInlineSnapshot(`"DIVISION_BY_ZERO"`); - }); - }); - - describe('min', () => { - test('two operands', () => { - check(['min', 1, 2], 1); - }); - - test('evaluates sub-expressions', () => { - check(['min', 5, ['min', 4, 3]], 3); - }); - - test('is variadic', () => { - check(['min', 13, 7, 4, 2], 2); - }); - - test('casts strings to numbers', () => { - check(['min', '4', '2'], 2); - }); - }); - - describe('max', () => { - test('two operands', () => { - check(['max', 1, 2], 2); - }); - - test('evaluates sub-expressions', () => { - check(['max', 5, ['max', 4, 3]], 5); - }); - - test('is variadic', () => { - check(['max', 13, 7, 4, 2], 13); - }); - - test('casts strings to numbers', () => { - check(['max', '4', '2'], 4); - }); - }); - - describe('round', () => { - test('can round', () => { - check(['round', 1.6], 2); - check(['round', 3], 3); - }); - - test('evaluates sub-expressions', () => { - check(['round', ['round', 5.8]], 6); - }); - - test('throws on too few arguments', () => { - expect(() => check(['round', 1, 2] as any, 2)).toThrowErrorMatchingInlineSnapshot( - `""round" operator expects 1 operands."`, - ); - }); - }); - - describe('ceil', () => { - test('can round', () => { - check(['ceil', 1.6], 2); - check(['ceil', 1.2], 2); - check(['ceil', 3], 3); - }); - - test('evaluates sub-expressions', () => { - check(['ceil', ['ceil', 5.8]], 6); - }); - - test('throws on too few or too many arguments', () => { - expect(() => check(['ceil', 1, 2] as any, 2)).toThrowErrorMatchingInlineSnapshot( - `""ceil" operator expects 1 operands."`, - ); - }); - }); - - describe('floor', () => { - test('can round', () => { - check(['floor', 1.6], 1); - check(['floor', 1.2], 1); - check(['floor', 3], 3); - }); - - test('evaluates sub-expressions', () => { - check(['floor', ['floor', 5.8]], 5); - }); - - test('throws on too few or too many arguments', () => { - expect(() => check(['floor', 1, 2] as any, 2)).toThrowErrorMatchingInlineSnapshot( - `""floor" operator expects 1 operands."`, - ); - }); - }); - - describe('trunc', () => { - test('can round', () => { - check(['trunc', 1.6], 1); - check(['trunc', -1.2], -1); - check(['trunc', -3.7], -3); - }); - - test('evaluates sub-expressions', () => { - check(['trunc', ['trunc', 5.8]], 5); - }); - - test('throws on too few or too many arguments', () => { - expect(() => check(['trunc', 1, 2] as any, 2)).toThrowErrorMatchingInlineSnapshot( - `""trunc" operator expects 1 operands."`, - ); - }); - }); - - describe('abs', () => { - test('returns positive value', () => { - check(['abs', ['+', 0, 1.6]], 1.6); - check(['abs', ['+', 0, -1.2]], 1.2); - check(['abs', ['+', 0, -3]], 3); - check(['abs', ['+', 0, 5]], 5); - }); - - test('evaluates sub-expressions', () => { - check(['abs', ['abs', -5.8]], 5.8); - }); - - test('throws on too few or too many arguments', () => { - expect(() => check(['abs', 1, 2] as any, 2)).toThrowErrorMatchingInlineSnapshot( - `""abs" operator expects 1 operands."`, - ); - }); - }); - - describe('sqrt', () => { - test('returns the root', () => { - check(['sqrt', ['+', 0, 9]], 3); - check(['sqrt', 16], 4); - check(['sqrt', ['+', 0, 1]], 1); - }); - - test('evaluates sub-expressions', () => { - check(['sqrt', ['sqrt', 81]], 3); - }); - - test('throws on too few or too many arguments', () => { - expect(() => check(['sqrt', 1, 2] as any, 2)).toThrowErrorMatchingInlineSnapshot( - `""sqrt" operator expects 1 operands."`, - ); - }); - }); - - describe('exp', () => { - test('returns exponent', () => { - check(['exp', ['+', 0, 2]], Math.exp(2)); - check(['exp', 3], Math.exp(3)); - check(['exp', ['+', 0, 4.4]], Math.exp(4.4)); - }); - - test('evaluates sub-expressions', () => { - check(['exp', ['exp', 2]], Math.exp(Math.exp(2))); - }); - - test('throws on too few or too many arguments', () => { - expect(() => check(['exp', 1, 2] as any, 2)).toThrowErrorMatchingInlineSnapshot( - `""exp" operator expects 1 operands."`, - ); - }); - }); - - describe('ln', () => { - test('returns logarithm', () => { - check(['ln', ['+', 0, 2]], Math.log(2)); - check(['ln', 3], Math.log(3)); - check(['ln', ['+', 0, 4.4]], Math.log(4.4)); - }); - - test('evaluates sub-expressions', () => { - check(['ln', ['ln', 2]], Math.log(Math.log(2))); - }); - - test('throws on too few or too many arguments', () => { - expect(() => check(['ln', 1, 2] as any, 2)).toThrowErrorMatchingInlineSnapshot( - `""ln" operator expects 1 operands."`, - ); - }); - }); - - describe('log10', () => { - test('returns logarithm', () => { - check(['log10', ['+', 0, 2]], Math.log10(2)); - check(['log10', 3], Math.log10(3)); - check(['log10', ['+', 0, 4.4]], Math.log10(4.4)); - }); - - test('evaluates sub-expressions', () => { - check(['log10', ['log10', 2]], Math.log10(Math.log10(2))); - }); - - test('throws on too few or too many arguments', () => { - expect(() => check(['log10', 1, 2] as any, 2)).toThrowErrorMatchingInlineSnapshot( - `""log10" operator expects 1 operands."`, - ); - }); - }); - - describe('log', () => { - const log = (num: number, base: number) => Math.log(num) / Math.log(base); - - test('returns logarithm', () => { - check(['log', ['+', 0, 2], 8], log(2, 8)); - check(['log', 3, 5], log(3, 5)); - check(['log', ['+', 0, 4.4], 6], log(4.4, 6)); - }); - - test('evaluates sub-expressions', () => { - check(['log', ['log', 2, 2], 5], log(log(2, 2), 5)); - }); - - test('throws on too many arguments', () => { - expect(() => check(['log', 1, 2, 3, 4] as any, 2)).toThrowErrorMatchingInlineSnapshot( - `""log" operator expects 2 operands."`, - ); - }); - }); - - describe('pow', () => { - const pow = (num: number, base: number) => num ** base; - - test('returns logarithm', () => { - check(['pow', ['+', 0, 2], 8], pow(2, 8)); - check(['**', 3, 5], pow(3, 5)); - check(['**', ['+', 0, 4.4], 6], pow(4.4, 6)); - }); - - test('evaluates sub-expressions', () => { - check(['pow', ['pow', 2, 2], 5], pow(pow(2, 2), 5)); - }); - - test('throws on too many arguments', () => { - expect(() => check(['pow', 1, 2, 3, 4] as any, 2)).toThrowErrorMatchingInlineSnapshot( - `""**" operator expects 2 operands."`, - ); - }); - }); - }); - - describe('Comparison operators', () => { - describe('eq or ==', () => { - test('can compare numbers', () => { - check(['eq', 1, 1], true); - check(['eq', 5, ['+', 0, 5]], true); - check(['==', 5, 4], false); - check(['==', ['+', 0, 5], -5], false); - }); - - test('can compare strings', () => { - check(['eq', '1', '1'], true); - check(['eq', 'abc', 'abc'], true); - check(['eq', 'abc', 'abc!'], false); - }); - - test('can compare strings', () => { - check(['eq', '1', '1'], true); - check(['eq', 'abc', 'abc'], true); - check(['eq', 'abc', 'abc!'], false); - }); - - test('can compare booleans', () => { - check(['eq', true, true], true); - check(['eq', true, false], false); - check(['eq', false, true], false); - check(['eq', false, false], true); - }); - - test('deeply compares objects', () => { - check(['eq', {foo: 'bar'}, {foo: 'bar'}], true); - }); - - test('different types', () => { - check(['eq', 1, '1'], false); - check(['eq', 123, '123'], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['eq', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""==" operator expects 2 operands."`, - ); - expect(() => check(['eq', 1, 2, 3] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""==" operator expects 2 operands."`, - ); - }); - }); - - describe('ne or !=', () => { - test('can compare numbers', () => { - check(['ne', 1, 1], false); - check(['!=', 5, ['+', 0, 5]], false); - check(['ne', 5, 4], true); - check(['!=', ['+', 0, 5], -5], true); - }); - - test('can compare strings', () => { - check(['ne', '1', '1'], false); - check(['ne', 'abc', 'abc'], false); - check(['ne', 'abc', 'abc!'], true); - }); - - test('can compare strings', () => { - check(['ne', '1', '1'], false); - check(['ne', 'abc', 'abc'], false); - check(['ne', 'abc', 'abc!'], true); - }); - - test('can compare booleans', () => { - check(['ne', true, true], false); - check(['ne', true, false], true); - check(['ne', false, true], true); - check(['ne', false, false], false); - }); - - test('deeply compares objects', () => { - check(['ne', {foo: 'bar'}, {foo: 'bar'}], false); - check(['ne', {foo: 'bar'}, {foo: 'bar!'}], true); - }); - - test('different types', () => { - check(['ne', 1, '1'], true); - check(['ne', 123, '123'], true); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['ne', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""!=" operator expects 2 operands."`, - ); - expect(() => check(['!=', 1, 2, 3] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""!=" operator expects 2 operands."`, - ); - }); - }); - - describe('gt or >', () => { - test('can compare numbers', () => { - check(['>', 2, 1], true); - check(['>', 5, ['+', 0, 5]], false); - check(['gt', 5, 4], true); - check(['gt', ['+', 0, 5], -5], true); - }); - - test('can compare strings', () => { - check(['>', ['get', '/1'], ['get', '/0']], true, ['1', '22']); - check(['>', ['get', '/1'], ['get', '/0']], false, ['bb', 'a']); - check(['>', ['get', '/1'], ['get', '/0']], true, ['bb', 'ccc']); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['gt', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `"">" operator expects 2 operands."`, - ); - expect(() => check(['>', 1, 2, 3] as any, false)).toThrowErrorMatchingInlineSnapshot( - `"">" operator expects 2 operands."`, - ); - }); - }); - - describe('ge or >=', () => { - test('can compare numbers', () => { - check(['>=', 2, 1], true); - check(['>=', 5, ['+', 0, 5]], true); - check(['ge', 5, 4], true); - check(['ge', ['+', 0, 5], -5], true); - }); - - test('can compare strings', () => { - check(['>=', '22', '1'], true); - check(['>=', 'bb', 'a'], true); - check(['>=', 'bb', 'bb'], true); - check(['>=', 'bb', 'ccc'], false); - check(['>=', ['get', '/1'], ['get', '/0']], true, ['1', '22']); - check(['>=', ['get', '/1'], ['get', '/0']], false, ['bb', 'a']); - check(['>=', ['get', '/1'], ['get', '/0']], true, ['bb', 'ccc']); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['ge', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `"">=" operator expects 2 operands."`, - ); - expect(() => check(['>=', 1, 2, 3] as any, false)).toThrowErrorMatchingInlineSnapshot( - `"">=" operator expects 2 operands."`, - ); - }); - }); - - describe('lt or <', () => { - test('can compare numbers', () => { - check(['<', 2, ['get', '/a']], false, {a: 1}); - check(['<', 2, ['get', '/a']], true, {a: 4}); - check(['<', 2, 5], true); - check(['<', 5, ['+', 0, 5]], false); - }); - - test('"lt" alias works', () => { - check(['lt', 2, ['get', '/a']], false, {a: 1}); - check(['lt', 2, ['get', '/a']], true, {a: 4}); - check(['lt', 2, 1], false); - check(['lt', 2, 4], true); - }); - - test('can compare strings', () => { - check(['<', '22', '1'], false); - check(['<', 'bb', 'a'], false); - check(['<', ['get', '/1'], ['get', '/0']], false, ['1', '22']); - check(['<', ['get', '/1'], ['get', '/0']], true, ['bb', 'a']); - check(['<', ['get', '/1'], ['get', '/0']], false, ['bb', 'ccc']); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['lt', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""<" operator expects 2 operands."`, - ); - expect(() => check(['<', 1, 2, 3] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""<" operator expects 2 operands."`, - ); - }); - }); - - describe('le or <=', () => { - test('can compare numbers', () => { - check(['<=', 2, 1], false); - check(['<=', 5, ['+', 0, 5]], true); - check(['le', 5, 4], false); - check(['le', ['+', 0, 5], -5], false); - }); - - test('can compare strings', () => { - check(['<=', '22', '1'], false); - check(['<=', 'bb', 'a'], false); - check(['<=', 'bb', 'bb'], true); - check(['<=', 'bb', 'ccc'], true); - check(['<=', ['get', '/1'], ['get', '/0']], false, ['1', '22']); - check(['<=', ['get', '/1'], ['get', '/0']], true, ['bb', 'a']); - check(['<=', ['get', '/1'], ['get', '/0']], false, ['bb', 'ccc']); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['le', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""<=" operator expects 2 operands."`, - ); - expect(() => check(['<=', 1, 2, 3] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""<=" operator expects 2 operands."`, - ); - }); - }); - - describe('cmp', () => { - test('can compare numbers', () => { - check(['cmp', 2, 1], 1); - check(['cmp', 2, 4], -1); - check(['cmp', 3.3, 3.3], 0); - }); - - test('can compare strings', () => { - check(['cmp', '22', '1'], 1); - check(['cmp', '22', '33'], -1); - check(['cmp', '22', ['$', '']], 0, '22'); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['cmp', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""cmp" operator expects 2 operands."`, - ); - expect(() => check(['cmp', 1, 2, 3] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""cmp" operator expects 2 operands."`, - ); - }); - }); - - describe('between or =><=', () => { - test('can compare numbers', () => { - check(['=><=', 1.5, 1, 2], true); - check(['=><=', 2, 1, 2], true); - check(['=><=', 1, 1, 2], true); - check(['=><=', ['get', ''], 1, 2], true, 1.4); - check(['between', ['get', ''], 1, 2], false, 2.7); - }); - - test('can compare strings', () => { - check(['=><=', ['get', ''], 'a', 'ccc'], true, 'bb'); - check(['between', ['get', ''], 'a', 'ccc'], true, 'bb'); - check(['between', 'dddd', 'a', 'ccc'], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['=><=', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""=><=" operator expects 3 operands."`, - ); - expect(() => check(['=><=', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""=><=" operator expects 3 operands."`, - ); - expect(() => check(['between', 1, 2, 3, 4] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""=><=" operator expects 3 operands."`, - ); - }); - }); - - describe('><', () => { - test('can compare numbers', () => { - check(['><', 1.5, 1, 2], true); - check(['><', ['get', ''], 1, 2], true, 1.4); - }); - - test('can compare strings', () => { - check(['><', ['get', ''], 'a', 'ccc'], true, 'bb'); - check(['><', ['get', ''], 'a', 'ccc'], true, 'bb'); - check(['><', 'dddd', 'a', 'ccc'], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['><', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""><" operator expects 3 operands."`, - ); - expect(() => check(['><', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""><" operator expects 3 operands."`, - ); - expect(() => check(['><', 1, 2, 3, 4] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""><" operator expects 3 operands."`, - ); - }); - }); - - describe('=><', () => { - test('can compare numbers', () => { - check(['=><', 1.5, 1, 2], true); - check(['=><', 1, 1, 2], true); - check(['=><', ['get', ''], 1, 2], true, 1.4); - }); - - test('can compare strings', () => { - check(['=><', ['get', ''], 'a', 'ccc'], true, 'bb'); - check(['=><', ['get', ''], 'a', 'ccc'], true, 'bb'); - check(['=><', ['get', ''], 'a', 'ccc'], true, 'a'); - check(['=><', 'dddd', 'a', 'ccc'], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['=><', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""=><" operator expects 3 operands."`, - ); - expect(() => check(['=><', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""=><" operator expects 3 operands."`, - ); - expect(() => check(['=><', 1, 2, 3, 4] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""=><" operator expects 3 operands."`, - ); - }); - }); - - describe('><=', () => { - test('can compare numbers', () => { - check(['><=', 1.5, 1, 2], true); - check(['><=', 2, 1, 2], true); - check(['><=', ['get', ''], 1, 2], true, 1.4); - }); - - test('can compare strings', () => { - check(['><=', ['get', ''], 'a', 'ccc'], true, 'bb'); - check(['><=', ['get', ''], 'a', 'ccc'], true, 'bb'); - check(['><=', ['get', ''], 'a', 'ccc'], true, 'ccc'); - check(['><=', 'dddd', 'a', 'ccc'], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['><=', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""><=" operator expects 3 operands."`, - ); - expect(() => check(['><=', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""><=" operator expects 3 operands."`, - ); - expect(() => check(['><=', 1, 2, 3, 4] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""><=" operator expects 3 operands."`, - ); - }); - }); - }); - - describe('Logical operators', () => { - describe('and or &&', () => { - test('works with booleans', () => { - check(['&&', true, false], false); - check(['&&', true, true], true); - check(['&&', false, ['get', '']], false, true); - check(['&&', ['get', ''], ['get', '']], true, true); - check(['&&', ['get', ''], ['get', '']], false, false); - }); - - test('variadic form works', () => { - check(['&&', true, true], true); - check(['&&', true, true, true], true); - check(['&&', true, true, true, false], false); - check(['&&', true, false, true, true], false); - check(['&&', true, ['get', ''], true, true], false, false); - }); - - test('returns the last value, when all values truthy', () => { - check(['&&', 1, 1], 1); - check(['&&', 1, 2], 2); - check(['&&', 1, 2, '3'], '3'); - check(['&&', 1, 2, '3', true], true); - check(['&&', 1, 2, '3', true, {}], {}); - check(['&&', 1, 2, '3', true, {}, [[0]]], [0]); - }); - - test('returns the first falsy value', () => { - check(['&&', 1, 1, 0, 1], 0); - check(['&&', 1, 1, false, 1], false); - check(['&&', 1, 1, '', 1], ''); - check(['&&', 1, 1, null, 1], null); - check(['&&', 1, 1, undefined, 1], undefined); - }); - - test('alias works', () => { - check(['and', ['get', ''], ['get', '']], true, true); - check(['and', ['get', ''], ['get', '']], false, false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['and', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""&&" operator expects at least two operands."`, - ); - expect(() => check(['&&', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""&&" operator expects at least two operands."`, - ); - }); - }); - - describe('or or ||', () => { - test('works with booleans', () => { - check(['||', true, false], true); - check(['||', true, true], true); - check(['||', false, ['get', '']], false, false); - check(['||', ['get', ''], ['get', '']], true, true); - check(['||', ['get', ''], ['get', '']], false, false); - }); - - test('variadic form works', () => { - check(['||', true, true], true); - check(['||', true, true, true], true); - check(['||', true, true, true, false], true); - check(['||', true, false, true, true], true); - check(['||', false, false, false], false); - check(['||', true, ['get', ''], true, true], true, false); - }); - - test('returns the first truthy value', () => { - check(['||', 1, 1], 1); - check(['||', 1, 0], 1); - check(['||', 'asdf', ''], 'asdf'); - check(['||', '', ''], ''); - check(['||', 'a', 'b'], 'a'); - check(['||', '', 'b'], 'b'); - check(['||', 0, '', false, null, {}], {}); - }); - - test('alias works', () => { - check(['or', ['get', ''], ['get', '']], true, true); - check(['or', ['get', ''], ['get', '']], false, false); - check(['or', ['get', ''], true], true, false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['||', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""||" operator expects at least two operands."`, - ); - expect(() => check(['or', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""||" operator expects at least two operands."`, - ); - }); - }); - - describe('not or !', () => { - test('works with booleans', () => { - check(['!', true], false); - check(['!', false], true); - }); - - test('casts types to booleans', () => { - check(['!', 1], false); - check(['!', 0], true); - check(['!', ['!', 0]], false); - check(['!', 'asdf'], false); - check(['!', ''], true); - check(['!', null], true); - }); - - test('alias works', () => { - check(['not', true], false); - check(['not', false], true); - check(['not', ['get', '']], true, false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['!', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""!" operator expects 1 operands."`, - ); - expect(() => check(['not', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""!" operator expects 1 operands."`, - ); - }); - }); - }); - - describe('Container operators', () => { - describe('len', () => { - test('returns length of a string', () => { - check(['len', ''], 0); - check(['len', 'a'], 1); - check(['len', ['$', '']], 3, 'abc'); - }); - - test('returns length of an array', () => { - check(['len', [[]]], 0); - check(['len', [[1]]], 1); - check(['len', ['$', '']], 3, [2, 2, 2]); - }); - - test('returns number of object entries', () => { - check(['len', [{}]], 0); - check(['len', {foo: 'bar'}], 1); - check(['len', ['$', '']], 3, {a: 1, b: 2, c: 3}); - }); - - test('returns length of a binary', () => { - check(['len', new Uint8Array([])], 0); - check(['len', new Uint8Array([0])], 1); - check(['len', ['$', '']], 3, new Uint8Array([1, 2, 3])); - }); - - test('returns for all types that have no length', () => { - check(['len', null], 0); - check(['len', undefined], 0); - check(['len', true], 0); - check(['len', 123], 0); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['len', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""len" operator expects 1 operands."`, - ); - }); - }); - - describe('member or []', () => { - test('can index into literals', () => { - check(['[]', 'abc', 1], 'b'); - check(['[]', [[1, 2, 3]], 1], 2); - check(['[]', {foo: 'bar'}, 'foo'], 'bar'); - check(['[]', new Uint8Array([1, 2, 3]), 1], 2); - }); - - test('can index into expressions', () => { - check(['[]', ['$', ''], 1], 'b', 'abc'); - check(['[]', ['$', ''], 1], 2, [1, 2, 3]); - check(['[]', ['$', ''], 'foo'], 'bar', {foo: 'bar'}); - check(['[]', ['$', ''], 1], 2, new Uint8Array([1, 2, 3])); - }); - - test('can index recursively', () => { - check(['[]', ['[]', ['$', ''], 1], 0], 'lala', [1, ['lala'], 3]); - check(['[]', ['[]', {foo: {bar: 123}}, 'foo'], 'bar'], 123); - }); - - test('returns undefined on missing member', () => { - check(['[]', ['[]', ['$', ''], 1], 111], undefined, [1, ['lala'], 3]); - check(['[]', {foo: 123}, 'xxxx'], undefined); - check(['[]', 'abc', 123], undefined); - check(['[]', new Uint8Array([]), 123], undefined); - }); - - test('can use alias', () => { - check(['member', 'abc', 1], 'b'); - check(['member', [[1, 2, 3]], 1], 2); - check(['member', {foo: 'bar'}, 'foo'], 'bar'); - check(['member', new Uint8Array([1, 2, 3]), 1], 2); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['member', 'a'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""[]" operator expects 2 operands."`, - ); - expect(() => check(['[]', 'a', 'b', 'c'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""[]" operator expects 2 operands."`, - ); - }); - }); - }); - - describe('Type operators', () => { - describe('type', () => { - test('returns value type', () => { - check(['type', true], 'boolean'); - check(['type', ['get', '']], 'boolean', false); - check(['type', ['get', '']], 'null', null); - check(['type', ['get', '']], 'number', 123); - check(['type', ['get', '']], 'number', 123.5); - check(['type', ['get', '']], 'string', 'abc'); - check(['type', ['get', '']], 'object', {}); - check(['type', ['get', '']], 'array', []); - check(['type', undefined], 'undefined'); - check(['type', new Uint8Array()], 'binary'); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['type', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""type" operator expects 1 operands."`, - ); - }); - }); - - describe('bool', () => { - test('casts to boolean', () => { - check(['bool', true], true); - check(['bool', ['get', '']], false, false); - check(['bool', ['get', '']], false, null); - check(['bool', ['get', '']], true, 123); - check(['bool', ['get', '']], true, 123.5); - check(['bool', ['get', '']], false, 0); - check(['bool', ['get', '']], false, 0.0); - check(['bool', ['get', '']], true, 'abc'); - check(['bool', ['get', '']], false, ''); - check(['bool', ['get', '']], true, {}); - check(['bool', ['get', '']], true, []); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['bool', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""bool" operator expects 1 operands."`, - ); - }); - }); - - describe('num', () => { - test('casts to number', () => { - check(['num', true], 1); - check(['num', ['get', '']], 0, false); - check(['num', ['get', '']], 0, null); - check(['num', ['get', '']], 123, 123); - check(['num', ['get', '']], 123.5, 123.5); - check(['num', ['get', '']], 0, 0); - check(['num', ['get', '']], 0, 0.0); - check(['num', ['get', '']], 0, 'abc'); - check(['num', ['get', '']], 0, ''); - check(['num', ['get', '']], 1, '1'); - check(['num', ['get', '']], 2, '2'); - check(['num', ['get', '']], 4.5, '4.5'); - check(['num', ['get', '']], 0, {}); - check(['num', ['get', '']], 0, []); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['num', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""num" operator expects 1 operands."`, - ); - }); - }); - - describe('str', () => { - test('casts to number', () => { - check(['str', true], 'true'); - check(['str', ['get', '']], 'false', false); - check(['str', ['get', '']], 'null', null); - check(['str', ['get', '']], '123', 123); - check(['str', ['get', '']], '123.5', 123.5); - check(['str', ['get', '']], '0', 0); - check(['str', ['get', '']], '0', 0.0); - check(['str', ['get', '']], 'abc', 'abc'); - check(['str', ['get', '']], '', ''); - check(['str', ['get', '']], '1', '1'); - check(['str', ['get', '']], '2', '2'); - check(['str', ['get', '']], '4.5', '4.5'); - check(['str', ['get', '']], '{}', {}); - check(['str', ['get', '']], '[]', []); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['str', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""str" operator expects 1 operands."`, - ); - }); - }); - - describe('und?', () => { - test('returns true if value is undefined', () => { - check(['und?', undefined], true); - // TODO: make this pass... - // check(['und?', ['$', '']], true, undefined); - }); - - test('returns false if value not undefined', () => { - check(['und?', 123], false); - check(['und?', ['$', '']], false, 'lol'); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['und?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""und?" operator expects 1 operands."`, - ); - }); - }); - - describe('nil?', () => { - test('returns true if value is null', () => { - check(['nil?', null], true); - check(['nil?', ['$', '']], true, null); - }); - - test('returns false if value not null', () => { - check(['nil?', 123], false); - check(['nil?', ['$', '']], false, 'lol'); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['nil?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""nil?" operator expects 1 operands."`, - ); - }); - }); - - describe('bool?', () => { - test('returns true if value is boolean', () => { - check(['bool?', true], true); - check(['bool?', ['$', '']], true, false); - }); - - test('returns false if value not boolean', () => { - check(['bool?', 123], false); - check(['bool?', ['$', '']], false, 'lol'); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['bool?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""bool?" operator expects 1 operands."`, - ); - }); - }); - - describe('num?', () => { - test('returns true if value is number', () => { - check(['num?', 0], true); - check(['num?', ['$', '']], true, 123); - }); - - test('returns false if value not number', () => { - check(['num?', true], false); - check(['num?', ['$', '']], false, 'lol'); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['num?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""num?" operator expects 1 operands."`, - ); - }); - }); - - describe('str?', () => { - test('returns true if value is string', () => { - check(['str?', ''], true); - check(['str?', ['$', '']], true, '123'); - }); - - test('returns false if value not string', () => { - check(['str?', true], false); - check(['str?', ['$', '']], false, 123); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['str?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""str?" operator expects 1 operands."`, - ); - }); - }); - - describe('arr?', () => { - test('returns true if value is array', () => { - check(['arr?', [[]]], true); - check(['arr?', ['$', '']], true, [1, true, false]); - }); - - test('returns false if value not array', () => { - check(['arr?', true], false); - check(['arr?', ['$', '']], false, 123); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['arr?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""arr?" operator expects 1 operands."`, - ); - }); - }); - - describe('bin?', () => { - test('returns true if value is binary', () => { - check(['bin?', [new Uint8Array([])]], true); - check(['bin?', ['$', '']], true, new Uint8Array([1, 2, 3])); - }); - - test('returns false if value not binary', () => { - check(['bin?', true], false); - check(['bin?', ['$', '']], false, 123); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['bin?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""bin?" operator expects 1 operands."`, - ); - }); - }); - - describe('obj?', () => { - test('returns true if value is object', () => { - check(['obj?', [{}]], true); - check(['obj?', ['$', '']], true, {foo: 'bar'}); - }); - - test('returns false if value not object', () => { - check(['obj?', true], false); - check(['obj?', ['$', '']], false, 123); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['obj?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""obj?" operator expects 1 operands."`, - ); - }); - }); - }); - - describe('String operators', () => { - describe('car or .', () => { - test('can concatenate two strings', () => { - check(['.', 'a', 'b'], 'ab'); - check(['.', 'a', ['get', '']], 'ac', 'c'); - }); - - test('long form', () => { - check(['cat', 'a', 'b'], 'ab'); - check(['cat', 'a', ['get', '']], 'ac', 'c'); - }); - - test('variadic form', () => { - check(['.', 'a', 'b', 'c', 'def'], 'abcdef'); - check(['.', 'a', 'b', 'c', 'def', ['get', '']], 'abcdef!', '!'); - }); - - test('casts to string', () => { - check(['.', '1', true, '!'], '1true!'); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['cat', 'a'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""." operator expects at least two operands."`, - ); - }); - }); - - describe('contains', () => { - test('can find a substring', () => { - check(['contains', 'abc', 'ab'], true); - check(['contains', 'abc', 'b'], true); - check(['contains', 'abc', 'c'], true); - }); - - test('returns false on missing substring', () => { - check(['contains', 'abc', 'g'], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['contains', 'a'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""contains" operator expects 2 operands."`, - ); - expect(() => check(['contains', 'a', 'b', 'c'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""contains" operator expects 2 operands."`, - ); - }); - }); - - describe('starts', () => { - test('can find a substring', () => { - check(['starts', 'abc', 'ab'], true); - check(['starts', 'abc', 'a'], true); - check(['starts', 'abc', 'abc'], true); - check(['starts', 'abc', 'b'], false); - check(['starts', 'abc', 'c'], false); - }); - - test('returns false on missing substring', () => { - check(['starts', 'abc', 'g'], false); - check(['starts', 'abc', 'aa'], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['starts', 'a'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""starts" operator expects 2 operands."`, - ); - expect(() => check(['starts', 'a', 'b', 'c'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""starts" operator expects 2 operands."`, - ); - }); - }); - - describe('ends', () => { - test('can find a substring', () => { - check(['ends', 'abc', 'ab'], false); - check(['ends', 'abc', 'a'], false); - check(['ends', 'abc', 'b'], false); - check(['ends', 'abc', 'abc'], true); - check(['ends', 'abc', 'bc'], true); - check(['ends', 'abc', 'c'], true); - }); - - test('returns false on missing substring', () => { - check(['ends', 'abc', 'g'], false); - check(['ends', 'abc', 'aa'], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['ends', 'a'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""ends" operator expects 2 operands."`, - ); - expect(() => check(['ends', 'a', 'b', 'c'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""ends" operator expects 2 operands."`, - ); - }); - }); - - describe('substr', () => { - test('computes a substring', () => { - check(['substr', 'abc', 1, 2], 'b'); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['substr', 'a'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""substr" operator expects 3 operands."`, - ); - expect(() => check(['substr', 'a', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""substr" operator expects 3 operands."`, - ); - expect(() => check(['substr', 'a', 1, 2, 3] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""substr" operator expects 3 operands."`, - ); - }); - }); - - describe('matches', () => { - const createPattern = (pattern: string) => { - const reg = new RegExp(pattern); - return (value: string) => reg.test(value); - }; - - test('matches a pattern', () => { - check(['matches', 'abc', 'bc'], true, null, { - createPattern, - }); - check(['matches', 'abc', 'bcd'], false, null, { - createPattern, - }); - }); - - test('pattern must be a literal', () => { - expect(() => - check(['matches', 'abc', ['get', '']], true, 'bc', { - createPattern, - }), - ).toThrowErrorMatchingInlineSnapshot(`""matches" second argument should be a regular expression string."`); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['matches', 'a'] as any, false, null, {createPattern})).toThrowErrorMatchingInlineSnapshot( - `""matches" operator expects 2 operands."`, - ); - expect(() => - check(['matches', 'a', 'b', 'c'] as any, false, null, {createPattern}), - ).toThrowErrorMatchingInlineSnapshot(`""matches" operator expects 2 operands."`); - }); - }); - - describe('email?', () => { - test('returns true for an email', () => { - check(['email?', 'a@b.c'], true); - check(['email?', 'vadim@gmail.com'], true); - }); - - test('return false for not email', () => { - check(['email?', 'abc'], false); - check(['email?', 123], false); - check(['email?', true], false); - check(['email?', null], false); - check(['email?', undefined], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['email?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""email?" operator expects 1 operands."`, - ); - }); - }); - - describe('hostname?', () => { - test('returns true for an hostname', () => { - check(['hostname?', 'google.com'], true); - check(['hostname?', 'www.google.com'], true); - check(['hostname?', 'staging.www.google.com'], true); - check(['hostname?', 'x.com'], true); - }); - - test('return false for not hostname', () => { - check(['hostname?', 'abc+'], false); - check(['hostname?', 123], false); - check(['hostname?', true], false); - check(['hostname?', null], false); - check(['hostname?', undefined], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['hostname?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""hostname?" operator expects 1 operands."`, - ); - }); - }); - - describe('ip4?', () => { - test('returns true for an IPv4', () => { - check(['ip4?', '127.0.1.0'], true); - check(['ip4?', '255.255.255.255'], true); - }); - - test('return false for not IPv4', () => { - check(['ip4?', '1.2.3.4.5'], false); - check(['ip4?', 'abc+'], false); - check(['ip4?', 123], false); - check(['ip4?', true], false); - check(['ip4?', null], false); - check(['ip4?', undefined], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['ip4?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""ip4?" operator expects 1 operands."`, - ); - }); - }); - - describe('ip6?', () => { - test('returns true for an IPv6', () => { - check(['ip6?', '2001:0db8:0000:0000:0000:ff00:0042:8329'], true); - check(['ip6?', '2001:db8:0:0:0:ff00:42:8329'], true); - }); - - test('return false for not IPv6', () => { - check(['ip6?', '1.2.3.4.5'], false); - check(['ip6?', 'abc+'], false); - check(['ip6?', 123], false); - check(['ip6?', true], false); - check(['ip6?', null], false); - check(['ip6?', undefined], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['ip6?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""ip6?" operator expects 1 operands."`, - ); - }); - }); - - describe('uuid?', () => { - test('returns true for an UUID', () => { - check(['uuid?', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'], true); - check(['uuid?', '12345678-aaaa-aaaa-aaaa-ffffffffffff'], true); - }); - - test('return false for not UUID', () => { - check(['uuid?', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa!'], false); - check(['uuid?', '1.2.3.4.5'], false); - check(['uuid?', 'abc+'], false); - check(['uuid?', 123], false); - check(['uuid?', true], false); - check(['uuid?', null], false); - check(['uuid?', undefined], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['uuid?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""uuid?" operator expects 1 operands."`, - ); - }); - }); - - describe('uri?', () => { - test('returns true for an URI', () => { - check(['uri?', 'https://goolge.com/paht?key=value#fragment'], true); - check(['uri?', 'ftp://www.goolge.com/path'], true); - check(['uri?', 'http://123.124.125.126'], true); - }); - - test('return false for not URI', () => { - check(['uri?', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa!'], false); - check(['uri?', '1.2.3.4.5'], false); - check(['uri?', 'abc+'], false); - check(['uri?', 123], false); - check(['uri?', true], false); - check(['uri?', null], false); - check(['uri?', undefined], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['uri?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""uri?" operator expects 1 operands."`, - ); - }); - }); - - describe('duration?', () => { - test('returns true for an duration', () => { - check(['duration?', 'P3D'], true); - }); - - test('return false for not duration', () => { - check(['duration?', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa!'], false); - check(['duration?', '1.2.3.4.5'], false); - check(['duration?', 'abc+'], false); - check(['duration?', 123], false); - check(['duration?', true], false); - check(['duration?', null], false); - check(['duration?', undefined], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['duration?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""duration?" operator expects 1 operands."`, - ); - }); - }); - - describe('date?', () => { - test('returns true for an date', () => { - check(['date?', '1937-01-01'], true); - }); - - test('return false for not date', () => { - check(['date?', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa!'], false); - check(['date?', '1.2.3.4.5'], false); - check(['date?', 'abc+'], false); - check(['date?', 123], false); - check(['date?', true], false); - check(['date?', null], false); - check(['date?', undefined], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['date?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""date?" operator expects 1 operands."`, - ); - }); - }); - - describe('time?', () => { - test('returns true for an time', () => { - check(['time?', '20:20:39+00:00'], true); - }); - - test('return false for not time', () => { - check(['time?', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa!'], false); - check(['time?', '1.2.3.4.5'], false); - check(['time?', 'abc+'], false); - check(['time?', 123], false); - check(['time?', true], false); - check(['time?', null], false); - check(['time?', undefined], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['time?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""time?" operator expects 1 operands."`, - ); - }); - }); - - describe('dateTime?', () => { - test('returns true for an dateTime', () => { - check(['dateTime?', '2018-11-13T20:20:39+00:00'], true); - }); - - test('return false for not dateTime', () => { - check(['dateTime?', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa!'], false); - check(['dateTime?', '1.2.3.4.5'], false); - check(['dateTime?', 'abc+'], false); - check(['dateTime?', 123], false); - check(['dateTime?', true], false); - check(['dateTime?', null], false); - check(['dateTime?', undefined], false); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['dateTime?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""dateTime?" operator expects 1 operands."`, - ); - }); - }); - }); - - describe('Binary operators', () => { - describe('u8', () => { - test('can read from binary', () => { - check(['u8', new Uint8Array([1, 2, 3]), 0], 1); - check(['u8', new Uint8Array([1, 2, 3]), 1], 2); - check(['u8', new Uint8Array([1, 2, 3]), 2], 3); - }); - - test('can read from binary input', () => { - check(['u8', ['$', ''], 1], 2, new Uint8Array([1, 2, 3])); - }); - - test('throws when reading out of bounds', () => { - expect(() => check(['u8', new Uint8Array([1, 2, 3]), -1], 0)).toThrowErrorMatchingInlineSnapshot( - `"OUT_OF_BOUNDS"`, - ); - expect(() => check(['u8', new Uint8Array([1, 2, 3]), 3], 0)).toThrowErrorMatchingInlineSnapshot( - `"OUT_OF_BOUNDS"`, - ); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['u8', 'a'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""u8" operator expects 2 operands."`, - ); - expect(() => check(['u8', 'a', 'b', 'c'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""u8" operator expects 2 operands."`, - ); - }); - }); - }); - - describe('Array operators', () => { - describe('concat', () => { - test('concatenates two arrays', () => { - check(['concat', [[1]], [[2]]], [1, 2]); - }); - - test('concatenates empty arrays', () => { - check(['concat', [[1]], [[]]], [1]); - check(['concat', [[]], [[]]], []); - }); - - test('concatenates variadic number of arrays', () => { - check(['concat', [[1, 2]], [[3]], [[4, 5]]], [1, 2, 3, 4, 5]); - check(['concat', [[1, 2]], [[3]], [[4, 5, 'a']], [[true, null]]], [1, 2, 3, 4, 5, 'a', true, null]); - }); - - test('resolves variables at runtime', () => { - check(['concat', [[1, 2]], ['$', ''], [[4, 5]]], [1, 2, 3, 4, 5], [3]); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['concat', []] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""concat" operator expects at least two operands."`, - ); - expect(() => check(['++', []] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""concat" operator expects at least two operands."`, - ); - }); - }); - - describe('push', () => { - test('can push static values into static array', () => { - const arr: unknown[] = []; - check(['push', [arr], 1], [1]); - check(['push', [arr], 1, 2, 3], [1, 2, 3]); - check(['push', [arr], 1, '2', true, [[]]], [1, '2', true, []]); - check(['push', [[1]], 2, 3], [1, 2, 3]); - }); - - test('can push static values into array', () => { - check(['push', ['$', '/arr'], 1], [1], {arr: []}); - check(['push', ['$', '/arr'], 1, 2, 3], [1, 2, 3], {arr: []}); - check(['push', ['$', '/arr'], 1, 2, 3], [0, 1, 2, 3], {arr: [0]}); - }); - - test('can push values into static array', () => { - check(['push', [[]], ['$', '/val'], 1], [0, 1], {val: 0}); - }); - - test('can push values into array', () => { - check(['push', ['$', '/arr'], ['$', '/val'], '2'], [0, 1, '2'], {arr: [0], val: 1}); - }); - - test('concatenates empty arrays', () => { - check(['push', [[1]], [[]]], [1, []]); - check(['push', [[]], [[]]], [[]]); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['push', [[]]] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""push" operator expects at least two operands."`, - ); - expect(() => check(['push', []] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""push" operator expects at least two operands."`, - ); - }); - }); - - describe('head', () => { - test('returns first two elements', () => { - check(['head', [[1, 2, 3]], 2], [1, 2]); - }); - - test('returns zero first elements', () => { - check(['head', [[1, 2, 3]], 0], []); - }); - - test('returns whole array when count is greater than array size', () => { - check(['head', [[1, 2, 3]], 10], [1, 2, 3]); - }); - - test('returns whole array when count is greater than array size - 2', () => { - check(['head', ['$', '/arr'], ['$', '/n']], [1, 2, 3], { - arr: [1, 2, 3], - n: 10, - }); - }); - - test('negative values select from the end', () => { - check(['head', ['$', '/arr'], ['$', '/n']], [], { - arr: [1, 2, 3], - n: 0, - }); - check(['head', ['$', '/arr'], ['$', '/n']], [3], { - arr: [1, 2, 3], - n: -1, - }); - check(['head', ['$', '/arr'], ['$', '/n']], [2, 3], { - arr: [1, 2, 3], - n: -2, - }); - check(['head', ['$', '/arr'], ['$', '/n']], [1, 2, 3], { - arr: [1, 2, 3], - n: -3, - }); - check(['head', ['$', '/arr'], ['$', '/n']], [1, 2, 3], { - arr: [1, 2, 3], - n: -4, - }); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['head', 'a'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""head" operator expects 2 operands."`, - ); - expect(() => check(['head', 'a', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""head" operator expects 2 operands."`, - ); - }); - }); - - describe('sort', () => { - test('sorts an array', () => { - check(['sort', [[1, 2, 3]]], [1, 2, 3]); - check(['sort', [[4, 1, 2, 3]]], [1, 2, 3, 4]); - check(['[]', ['sort', [[4, 1, 6, 2, 3]]], 4], 6); - }); - - test('sorts an array - 2', () => { - check(['sort', ['$', '']], [1, 2, 3, 4], [4, 1, 2, 3]); - check(['[]', ['sort', ['$', '']], 4], 6, [4, 1, 6, 2, 3]); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['sort', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""sort" operator expects 1 operands."`, - ); - }); - }); - - describe('reverse', () => { - test('sorts an array', () => { - check(['reverse', [[1, 2, 3]]], [3, 2, 1]); - check(['reverse', [[4, 1, 2, 3]]], [3, 2, 1, 4]); - check(['[]', ['reverse', [[4, 1, 6, 2, 3]]], 4], 4); - }); - - test('sorts an array - 2', () => { - check(['reverse', ['$', '']], [3, 2, 1, 4], [4, 1, 2, 3]); - check(['[]', ['reverse', ['$', '']], 4], 4, [4, 1, 6, 2, 3]); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['reverse', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""reverse" operator expects 1 operands."`, - ); - }); - }); - - describe('in', () => { - test('returns true if value found in array', () => { - check(['in', [[1, 2, 3]], 3], true); - check(['in', [[1, 2, 3]], 2], true); - check(['in', [[1, 2, 3]], 1], true); - check(['in', ['$', ''], {foo: 'bar'}], true, [1, 2, 3, {foo: 'bar'}]); - }); - - test('returns false if value not found in array', () => { - check(['in', [[1, 2, 3]], 4], false); - check(['in', [[1, 2, 3]], 'a'], false); - check(['in', [[1, 2, 3]], ['$', '']], false, '1'); - check(['in', ['$', ''], '1'], false, [1, 2, 3]); - check(['in', ['$', '/0'], ['$', '/1']], false, [[1, 2, 3], '1']); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['in', 'a'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""in" operator expects 2 operands."`, - ); - expect(() => check(['in', 'a', 'b', 'c'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""in" operator expects 2 operands."`, - ); - }); - }); - - describe('fromEntries', () => { - test('returns object from 2-tuple list', () => { - check(['fromEntries', [[['foo', 'bar']]]], {foo: 'bar'}); - }); - - test('returns object from 2-tuple list - 2', () => { - check(['fromEntries', ['++', [[]], [[['foo', 'bar']]]]], {foo: 'bar'}); - }); - - test('returns object empty object', () => { - check(['fromEntries', [[]]], {}); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['fromEntries', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""fromEntries" operator expects 1 operands."`, - ); - }); - }); - - describe('indexOf', () => { - test('finds element in an array', () => { - check(['indexOf', [[1, 2, 3]], 2], 1); - check(['indexOf', [[1, 2, 3, {a: null}, {a: false}]], {a: false}], 4); - }); - - test('when array is input', () => { - check(['indexOf', ['$', ''], {a: false}], 4, [1, 2, 3, {a: null}, {a: false}]); - }); - - test('when array is input and element is input', () => { - check(['indexOf', ['$', ''], ['$', '/4']], 4, [1, 2, 3, {a: null}, {a: false}]); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['indexOf', 'a'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""indexOf" operator expects 2 operands."`, - ); - expect(() => check(['indexOf', 'a', 'a', 'a'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""indexOf" operator expects 2 operands."`, - ); - }); - }); - - describe('slice', () => { - test('returns a slice of an array', () => { - check(['slice', [[1, 2, 3]], 0, 1], [1]); - check(['slice', [[1, 2, 3]], 0, 2], [1, 2]); - check(['slice', ['$', ''], 1, 3], [2, 3], [1, 2, 3]); - }); - - test('can use negative values', () => { - check(['slice', [[1, 2, 3]], 0, -2], [1]); - check(['slice', [[1, 2, 3]], 0, -1], [1, 2]); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['slice', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""slice" operator expects 3 operands."`, - ); - expect(() => check(['slice', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""slice" operator expects 3 operands."`, - ); - expect(() => check(['slice', 1, 2, 3, 4] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""slice" operator expects 3 operands."`, - ); - }); - }); - - describe('zip', () => { - test('can join two arrays', () => { - check( - ['zip', [['foo', 'bar']], [[1, 2]]], - [ - ['foo', 1], - ['bar', 2], - ], - ); - check( - ['fromEntries', ['zip', [['foo', 'bar']], ['$', '']]], - { - foo: 1, - bar: 2, - }, - [1, 2], - ); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['zip', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""zip" operator expects 2 operands."`, - ); - expect(() => check(['zip', 1, 2, 3] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""zip" operator expects 2 operands."`, - ); - }); - }); - - describe('filter', () => { - test('can filter out odd numbers', () => { - check(['filter', [[1, 2, 3, 4, 5]], 'x', ['!', ['%', ['$', 'x'], 2]]], [2, 4]); - }); - - test('can filter out strings', () => { - check(['filter', ['$', ''], 'item', ['str?', ['$', 'item']]], ['a', 'b', 'c'], [1, 2, 3, 'a', 4, 'b', 'c', 5]); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['filter', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""filter" operator expects 3 operands."`, - ); - expect(() => check(['filter', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""filter" operator expects 3 operands."`, - ); - expect(() => check(['filter', 1, 2, 3, 4] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""filter" operator expects 3 operands."`, - ); - }); - }); - - describe('map', () => { - test('can multiply all numbers by 3', () => { - check(['map', [[1, 2, 3, 4, 5]], 'x', ['*', ['$', 'x'], 3]], [3, 6, 9, 12, 15]); - }); - - test('can multiply all numbers by 3', () => { - check(['map', ['$', '/arr'], 'x', ['*', ['$', 'x'], ['$', '/multiple']]], [3, 6, 9, 12, 15], { - arr: [1, 2, 3, 4, 5], - multiple: 3, - }); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['map', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""map" operator expects 3 operands."`, - ); - expect(() => check(['map', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""map" operator expects 3 operands."`, - ); - expect(() => check(['map', 1, 2, 3, 4] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""map" operator expects 3 operands."`, - ); - }); - }); - - describe('reduce', () => { - test('can add up numbers', () => { - check(['reduce', [[1, 2, 3, 4, 5]], 0, 'acc', 'x', ['+', ['$', 'acc'], ['$', 'x']]], 15); - }); - - test('can add up numbers - 2', () => { - check(['reduce', ['$', ''], 0, 'acc', 'x', ['+', ['$', 'acc'], ['$', 'x']]], 15, [1, 2, 3, 4, 5]); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['reduce', ''] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""reduce" operator expects 5 operands."`, - ); - expect(() => check(['reduce', '', ''] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""reduce" operator expects 5 operands."`, - ); - expect(() => check(['reduce', '', '', ''] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""reduce" operator expects 5 operands."`, - ); - expect(() => check(['reduce', '', '', '', ''] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""reduce" operator expects 5 operands."`, - ); - expect(() => check(['reduce', '', '', '', '', '', ''] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""reduce" operator expects 5 operands."`, - ); - }); - }); - }); - - describe('Object operators', () => { - describe('keys', () => { - test('returns empty array for empty object', () => { - check(['keys', {}], []); - }); - - test('returns keys of an object', () => { - check(['keys', {foo: 1}], ['foo']); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['keys', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""keys" operator expects 1 operands."`, - ); - }); - }); - - describe('values', () => { - test('returns empty array for empty object', () => { - check(['values', {}], []); - }); - - test('returns values of an object', () => { - check(['values', {foo: 1}], [1]); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['values', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""values" operator expects 1 operands."`, - ); - }); - }); - - describe('entries', () => { - test('returns empty array for empty object', () => { - check(['entries', {}], []); - }); - - test('returns entries of an object', () => { - check(['entries', {foo: 1}], [['foo', 1]]); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['entries', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""entries" operator expects 1 operands."`, - ); - }); - }); - - describe('o.set', () => { - test('can set an object property', () => { - check(['o.set', {}, 'foo', 'bar'], {foo: 'bar'}); - }); - - test('can set two properties, one computed', () => { - const expression: Expr = ['o.set', {}, 'foo', 'bar', 'baz', ['+', ['$', ''], 3]]; - check( - expression, - { - foo: 'bar', - baz: 5, - }, - 2, - ); - }); - - test('can retrieve object from input', () => { - const expression: Expr = ['o.set', ['$', '/obj'], 'foo', 123]; - check( - expression, - { - type: 'the-obj', - foo: 123, - }, - { - obj: { - type: 'the-obj', - }, - }, - ); - }); - - test('can compute prop from expression', () => { - const expression: Expr = ['o.set', {a: 'b'}, ['.', ['$', '/name'], '_test'], ['+', 5, 5]]; - check( - expression, - { - a: 'b', - Mac_test: 10, - }, - { - name: 'Mac', - }, - ); - }); - - test('cannot set __proto__ prop', () => { - const expression: Expr = ['o.set', {a: 'b'}, '__proto__', ['$', '/name']]; - expect(() => - check( - expression, - { - a: 'b', - __proto__: 'Mac', - }, - { - name: 'Mac', - }, - ), - ).toThrow(new Error('PROTO_KEY')); - }); - }); - - describe('o.del', () => { - test('can delete an object property', () => { - check(['o.del', {foo: 'bar', baz: 'qux'}, 'foo', 'bar'], {baz: 'qux'}); - }); - - test('object can be an expression', () => { - check(['o.del', ['$', ''], 'a', 'c', 'd'], {b: 2}, {a: 1, b: 2, c: 3}); - }); - - test('prop can be an expression', () => { - check(['o.del', {a: 1, b: 2, c: 3}, ['$', '']], {a: 1, c: 3}, 'b'); - }); - - test('object and prop can be an expression', () => { - check(['o.del', ['$', '/o'], ['$', '/p']], {a: 1, c: 3}, {o: {a: 1, b: 2, c: 3}, p: 'b'}); - }); - }); - }); - - describe('Branching operators', () => { - describe('if or ?', () => { - test('branches', () => { - check(['?', true, 'a', 'b'], 'a'); - check(['if', false, 'a', 'b'], 'b'); - }); - - test('branches input values', () => { - check(['?', ['$', '/0'], ['$', '/1'], ['$', '/2']], 'a', [true, 'a', 'b']); - check(['?', ['$', '/0'], ['$', '/1'], ['$', '/2']], 'b', [false, 'a', 'b']); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['?', 'a'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""?" operator expects 3 operands."`, - ); - expect(() => check(['if', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""?" operator expects 3 operands."`, - ); - expect(() => check(['?', 'a', 'b', 'c', 'd'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""?" operator expects 3 operands."`, - ); - }); - }); - - describe('throw', () => { - test('can throw specified value', () => { - try { - check(['throw', 123], ''); - throw new Error('should not reach here'); - } catch (err) { - expect((err).value).toBe(123); - } - }); - - test('can throw specified value, from input', () => { - try { - check(['throw', ['get', '']], '', 123); - throw new Error('should not reach here'); - } catch (err) { - expect((err).value).toBe(123); - } - }); - - test('throws on invalid operand count', () => { - expect(() => check(['throw', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""throw" operator expects 1 operands."`, - ); - }); - }); - }); - - describe('Input operators', () => { - describe('get or $', () => { - test('can retrieve root value', () => { - check(['$', ''], 'a', 'a'); - check(['get', ''], 123, 123); - }); - - test('can retrieve nested value', () => { - check(['$', '/foo/1'], 2, {foo: [1, 2]}); - check(['get', '/foo/1'], 2, {foo: [1, 2]}); - }); - - test('returns default value when destination not found', () => { - check(['$', '/foo/5', 'miss'], 'miss', {foo: [1, 2]}); - check(['get', '/foo/5', 'miss'], 'miss', {foo: [1, 2]}); - }); - - test('pointer can be variable', () => { - check(['$', ['$', '/foo/0']], ['/foo'], {foo: ['/foo']}); - }); - - test('throws when value not found', () => { - expect(() => check(['$', '/foo/5'], '', {foo: [1, 2]})).toThrowErrorMatchingInlineSnapshot(`"NOT_FOUND"`); - expect(() => check(['get', '/foo/5'], '', {foo: [1, 2]})).toThrowErrorMatchingInlineSnapshot(`"NOT_FOUND"`); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['get', 'a', 'b', 'c'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""$" operator expects at most 2 operands."`, - ); - expect(() => check(['$', 'a', 'b', 'c'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""$" operator expects at most 2 operands."`, - ); - }); - }); - - describe('get? and $?', () => { - test('can retrieve root value', () => { - check(['$?', ''], true, 'a'); - check(['get?', ''], true, 123); - }); - - test('can retrieve nested value', () => { - check(['$?', '/foo/1'], true, {foo: [1, 2]}); - check(['get?', '/foo/1'], true, {foo: [1, 2]}); - }); - - test('returns false value when destination not found', () => { - check(['$?', '/foo/5'], false, {foo: [1, 2]}); - check(['get?', '/foo/5'], false, {foo: [1, 2]}); - }); - - test('pointer can be variable', () => { - check(['$?', ['$', '/foo/0']], true, {foo: ['/foo']}); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['get?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""$?" operator expects 1 operands."`, - ); - expect(() => check(['$?', 'a', 'b'] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""$?" operator expects 1 operands."`, - ); - }); - }); - }); - - describe('Bitwise operators', () => { - describe('bitAnd or &', () => { - test('works with two operands', () => { - check(['&', 3, 6], 3 & 6); - check(['bitAnd', 3, 6], 3 & 6); - }); - - test('works with variadic operands', () => { - check(['&', 3, 6, 12], 3 & 6 & 12); - check(['bitAnd', 3, 6, 8, 123], 3 & 6 & 8 & 123); - }); - - test('works with side-effects', () => { - check(['&', 3, 6, ['$', '']], 3 & 6 & 12, 12); - check(['bitAnd', 3, ['get', '/foo'], 8, 123], 3 & 6 & 8 & 123, {foo: 6}); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['&', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""&" operator expects at least two operands."`, - ); - expect(() => check(['bitAnd', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""&" operator expects at least two operands."`, - ); - }); - }); - - describe('bitOr or |', () => { - test('works with two operands', () => { - check(['|', 3, 6], 3 | 6); - check(['bitOr', 3, 6], 3 | 6); - }); - - test('works with variadic operands', () => { - check(['|', 3, 6, 12], 3 | 6 | 12); - check(['bitOr', 3, 6, 8, 123], 3 | 6 | 8 | 123); - check(['|', 1, 2, 3], 1 | 2 | 3); - }); - - test('works with side-effects', () => { - check(['|', 3, 6, ['$', '']], 3 | 6 | 12, 12); - check(['bitOr', 3, ['get', '/foo'], 8, 123], 3 | 6 | 8 | 123, {foo: 6}); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['|', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""|" operator expects at least two operands."`, - ); - expect(() => check(['bitOr', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""|" operator expects at least two operands."`, - ); - }); - }); - - describe('bitXor or ^', () => { - test('works with two operands', () => { - check(['^', 3, 6], 3 ^ 6); - check(['bitXor', 3, 6], 3 ^ 6); - }); - - test('works with variadic operands', () => { - check(['^', 3, 6, 12], 3 ^ 6 ^ 12); - check(['bitXor', 3, 6, 8, 123], 3 ^ 6 ^ 8 ^ 123); - check(['^', 1, 2, 3], 1 ^ 2 ^ 3); - }); - - test('works with side-effects', () => { - check(['^', 3, 6, ['$', '']], 3 ^ 6 ^ 12, 12); - check(['bitXor', 3, ['get', '/foo'], 8, 123], 3 ^ 6 ^ 8 ^ 123, {foo: 6}); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['^', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""^" operator expects at least two operands."`, - ); - expect(() => check(['bitXor', 1] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""^" operator expects at least two operands."`, - ); - }); - }); - - describe('bitNot or ~', () => { - test('works', () => { - check(['~', 3], ~3); - check(['~', 12], ~12); - check(['bitNot', 6], ~6); - }); - - test('throws on invalid operand count', () => { - expect(() => check(['~', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""~" operator expects 1 operands."`, - ); - expect(() => check(['bitNot', 1, 2] as any, false)).toThrowErrorMatchingInlineSnapshot( - `""~" operator expects 1 operands."`, - ); - }); - }); - }); - - describe('JSON Patch operators', () => { - describe('jp.add', () => { - test('can set an object property', () => { - check(['jp.add', {}, '/foo', 'bar'], {foo: 'bar'}); - }); - - test('can set two properties, one computed', () => { - const expression: Expr = ['jp.add', {}, '/foo', 'bar', '/baz', ['+', ['$', ''], 3]]; - check( - expression, - { - foo: 'bar', - baz: 5, - }, - 2, - ); - }); - }); - }); -}; diff --git a/src/json-expression/codegen-steps.ts b/src/json-expression/codegen-steps.ts deleted file mode 100644 index b9f06e4ffc..0000000000 --- a/src/json-expression/codegen-steps.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Represents an expression {@link types.Expr} which was evaluated by codegen and - * which value is already know at compilation time, hence it can be emitted - * as a literal. - */ -export class Literal { - constructor(public val: unknown) {} - - public toString() { - return JSON.stringify(this.val); - } -} - -/** - * Represents an expression {@link types.Expr} which was evaluated by codegen and - * which value is not yet known at compilation time, hence its value will - * be evaluated at runtime. - */ -export class Expression { - constructor(public val: string) {} - - public toString() { - return this.val; - } -} - -export type ExpressionResult = Literal | Expression; diff --git a/src/json-expression/codegen.ts b/src/json-expression/codegen.ts deleted file mode 100644 index 7e08e24113..0000000000 --- a/src/json-expression/codegen.ts +++ /dev/null @@ -1,109 +0,0 @@ -import * as util from './util'; -import {Codegen} from '@jsonjoy.com/util/lib/codegen/Codegen'; -import {ExpressionResult, Literal} from './codegen-steps'; -import {createEvaluate} from './createEvaluate'; -import {JavaScript} from '@jsonjoy.com/util/lib/codegen'; -import {Vars} from './Vars'; -import type * as types from './types'; - -export type JsonExpressionFn = (vars: types.JsonExpressionExecutionContext['vars']) => unknown; - -export interface JsonExpressionCodegenOptions extends types.JsonExpressionCodegenContext { - expression: types.Expr; - operators: types.OperatorMap; -} - -export class JsonExpressionCodegen { - protected codegen: Codegen; - protected evaluate: ReturnType; - - public constructor(protected options: JsonExpressionCodegenOptions) { - this.codegen = new Codegen({ - args: ['vars'], - epilogue: '', - }); - this.evaluate = createEvaluate({...options}); - } - - private linkedOperandDeps: Set = new Set(); - private linkOperandDeps = (dependency: unknown, name?: string): string => { - if (name) { - if (this.linkedOperandDeps.has(name)) return name; - this.linkedOperandDeps.add(name); - } else { - name = this.codegen.getRegister(); - } - this.codegen.linkDependency(dependency, name); - return name; - }; - - private operatorConst = (js: JavaScript): string => { - return this.codegen.addConstant(js); - }; - - private subExpression = (expr: types.Expr): JsonExpressionFn => { - const codegen = new JsonExpressionCodegen({...this.options, expression: expr}); - const fn = codegen.run().compile(); - return fn; - }; - - protected onExpression(expr: types.Expr | unknown): ExpressionResult { - if (expr instanceof Array) { - if (expr.length === 1) return new Literal(expr[0]); - } else return new Literal(expr); - - const def = this.options.operators.get(expr[0]); - if (def) { - const [name, , arity, , codegen, impure] = def; - util.assertArity(name, arity, expr); - const operands = expr.slice(1).map((operand) => this.onExpression(operand)); - if (!impure) { - const allLiterals = operands.every((expr) => expr instanceof Literal); - if (allLiterals) { - const result = this.evaluate(expr, {vars: new Vars(undefined)}); - return new Literal(result); - } - } - const ctx: types.OperatorCodegenCtx = { - expr, - operands, - createPattern: this.options.createPattern, - operand: (operand: types.Expression) => this.onExpression(operand), - link: this.linkOperandDeps, - const: this.operatorConst, - subExpression: this.subExpression, - var: (value: string) => this.codegen.var(value), - }; - return codegen(ctx); - } - return new Literal(false); - } - - public run(): this { - const expr = this.onExpression(this.options.expression); - this.codegen.js(`return ${expr};`); - return this; - } - - public generate() { - return this.codegen.generate(); - } - - public compileRaw(): JsonExpressionFn { - return this.codegen.compile(); - } - - public compile(): JsonExpressionFn { - const fn = this.compileRaw(); - return (vars) => { - try { - return fn(vars); - } catch (err) { - if (err instanceof Error) throw err; - const error = new Error('Expression evaluation error.'); - (error).value = err; - throw error; - } - }; - } -} diff --git a/src/json-expression/createEvaluate.ts b/src/json-expression/createEvaluate.ts deleted file mode 100644 index 9cfb966a48..0000000000 --- a/src/json-expression/createEvaluate.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {Expr, JsonExpressionCodegenContext, JsonExpressionExecutionContext, Literal, OperatorMap} from './types'; -import * as util from './util'; - -export const createEvaluate = ({operators, createPattern}: {operators: OperatorMap} & JsonExpressionCodegenContext) => { - const evaluate = ( - expr: Expr | Literal, - ctx: JsonExpressionExecutionContext & JsonExpressionCodegenContext, - ): unknown => { - if (!(expr instanceof Array)) return expr; - if (expr.length === 1) return expr[0]; - - const fn = expr[0]; - const def = operators.get(fn); - - try { - if (def) { - const [name, , arity, fn] = def; - util.assertArity(name, arity, expr); - return fn(expr, {createPattern, ...ctx, eval: evaluate}); - } - throw new Error('Unknown expression:' + JSON.stringify(expr)); - } catch (err) { - if (err instanceof Error) throw err; - const error = new Error('Expression evaluation error.'); - (error).value = err; - throw error; - } - }; - - return evaluate; -}; diff --git a/src/json-expression/evaluate.ts b/src/json-expression/evaluate.ts deleted file mode 100644 index 59cb2d7a7f..0000000000 --- a/src/json-expression/evaluate.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {createEvaluate} from './createEvaluate'; -import {operatorsMap} from './operators'; - -export const evaluate = createEvaluate({ - operators: operatorsMap, -}); diff --git a/src/json-expression/index.ts b/src/json-expression/index.ts deleted file mode 100644 index 088ff68938..0000000000 --- a/src/json-expression/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './types'; -export * from './evaluate'; -export * from './codegen'; -export * from './Vars'; diff --git a/src/json-expression/operators/arithmetic.ts b/src/json-expression/operators/arithmetic.ts deleted file mode 100644 index b18b028318..0000000000 --- a/src/json-expression/operators/arithmetic.ts +++ /dev/null @@ -1,242 +0,0 @@ -import * as util from '../util'; -import {Expression, ExpressionResult} from '../codegen-steps'; -import type * as types from '../types'; - -const toNum = util.num; - -export const arithmeticOperators: types.OperatorDefinition[] = [ - [ - '+', - ['add'], - -1, - (expr: types.ExprPlus, ctx) => { - return expr.slice(1).reduce((acc, e) => toNum(ctx.eval(e, ctx)) + acc, 0); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = ctx.operands.map((expr) => `(+(${expr})||0)`).join('+'); - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - '-', - ['subtract'], - -1, - (expr: types.ExprMinus, ctx) => { - return expr.slice(2).reduce((acc, e) => acc - toNum(ctx.eval(e, ctx)), toNum(ctx.eval(expr[1], ctx))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = ctx.operands.map((expr) => `(+(${expr})||0)`).join('-'); - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - '*', - ['multiply'], - -1, - (expr: types.ExprAsterisk, ctx) => { - return expr.slice(1).reduce((acc, e) => toNum(ctx.eval(e, ctx)) * acc, 1); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = ctx.operands.map((expr) => `(+(${expr})||0)`).join('*'); - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - '/', - ['divide'], - -1, - (expr: types.ExprMinus, ctx) => { - const start = toNum(ctx.eval(expr[1], ctx)); - return expr.slice(2).reduce((acc, e) => util.slash(acc, toNum(ctx.eval(e, ctx))), start); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.slash, 'slash'); - const params = ctx.operands.map((expr) => `(+(${expr})||0)`); - let last: string = params[0]; - for (let i = 1; i < params.length; i++) last = `slash(${last}, ${params[i]})`; - return new Expression(last); - }, - ] as types.OperatorDefinition, - - [ - '%', - ['mod'], - -1, - (expr: types.ExprMod, ctx) => { - const start = toNum(ctx.eval(expr[1], ctx)); - return expr.slice(2).reduce((acc, e) => util.mod(acc, toNum(ctx.eval(e, ctx))), start); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.mod, 'mod'); - const params = ctx.operands.map((expr) => `(+(${expr})||0)`); - let last: string = params[0]; - for (let i = 1; i < params.length; i++) last = `mod(${last}, ${params[i]})`; - return new Expression(last); - }, - ] as types.OperatorDefinition, - - [ - 'min', - [], - -1, - (expr: types.ExprMin, ctx) => { - return Math.min(...expr.slice(1).map((e) => toNum(ctx.eval(e, ctx)))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const params = ctx.operands.map((expr) => `(+(${expr})||0)`); - return new Expression(`+Math.min(${params.join(',')})||0`); - }, - ] as types.OperatorDefinition, - - [ - 'max', - [], - -1, - (expr: types.ExprMax, ctx) => { - return Math.max(...expr.slice(1).map((e) => toNum(ctx.eval(e, ctx)))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const params = ctx.operands.map((expr) => `(+(${expr})||0)`); - return new Expression(`+Math.max(${params.join(',')})||0`); - }, - ] as types.OperatorDefinition, - - [ - 'round', - [], - 1, - (expr: types.ExprRound, ctx) => { - return Math.round(toNum(ctx.eval(expr[1], ctx))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`Math.round(+(${ctx.operands[0]})||0)`); - }, - ] as types.OperatorDefinition, - - [ - 'ceil', - [], - 1, - (expr: types.ExprCeil, ctx) => { - return Math.ceil(toNum(ctx.eval(expr[1], ctx))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`Math.ceil(+(${ctx.operands[0]})||0)`); - }, - ] as types.OperatorDefinition, - - [ - 'floor', - [], - 1, - (expr: types.ExprFloor, ctx) => { - return Math.floor(toNum(ctx.eval(expr[1], ctx))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`Math.floor(+(${ctx.operands[0]})||0)`); - }, - ] as types.OperatorDefinition, - - [ - 'trunc', - [], - 1, - (expr: types.ExprTrunc, ctx) => { - return Math.trunc(toNum(ctx.eval(expr[1], ctx))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`Math.trunc(+(${ctx.operands[0]})||0)`); - }, - ] as types.OperatorDefinition, - - [ - 'abs', - [], - 1, - (expr: types.ExprAbs, ctx) => { - return Math.abs(toNum(ctx.eval(expr[1], ctx))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`Math.abs(+(${ctx.operands[0]})||0)`); - }, - ] as types.OperatorDefinition, - - [ - 'sqrt', - [], - 1, - (expr: types.ExprSqrt, ctx) => { - return Math.sqrt(toNum(ctx.eval(expr[1], ctx))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`Math.sqrt(+(${ctx.operands[0]})||0)`); - }, - ] as types.OperatorDefinition, - - [ - 'exp', - [], - 1, - (expr: types.ExprExp, ctx) => { - return Math.exp(toNum(ctx.eval(expr[1], ctx))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`Math.exp(+(${ctx.operands[0]})||0)`); - }, - ] as types.OperatorDefinition, - - [ - 'ln', - [], - 1, - (expr: types.ExprLn, ctx) => { - return Math.log(toNum(ctx.eval(expr[1], ctx))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`Math.log(+(${ctx.operands[0]})||0)`); - }, - ] as types.OperatorDefinition, - - [ - 'log', - [], - 2, - (expr: types.ExprLog, ctx) => { - const num = toNum(ctx.eval(expr[1], ctx)); - const base = toNum(ctx.eval(expr[2], ctx)); - return Math.log(num) / Math.log(base); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`Math.log(+(${ctx.operands[0]})||0)/Math.log(+(${ctx.operands[1]})||0)`); - }, - ] as types.OperatorDefinition, - - [ - 'log10', - [], - 1, - (expr: types.ExprLog10, ctx) => { - return Math.log10(toNum(ctx.eval(expr[1], ctx))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`Math.log10(+(${ctx.operands[0]})||0)`); - }, - ] as types.OperatorDefinition, - - [ - '**', - ['pow'], - 2, - (expr: types.ExprPow, ctx) => { - const num = toNum(ctx.eval(expr[1], ctx)); - const base = toNum(ctx.eval(expr[2], ctx)); - return Math.pow(num, base); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`Math.pow(+(${ctx.operands[0]})||0,+(${ctx.operands[0]})||0)`); - }, - ] as types.OperatorDefinition, -]; diff --git a/src/json-expression/operators/array.ts b/src/json-expression/operators/array.ts deleted file mode 100644 index 4ab141738c..0000000000 --- a/src/json-expression/operators/array.ts +++ /dev/null @@ -1,264 +0,0 @@ -import * as util from '../util'; -import {Expression, ExpressionResult, Literal} from '../codegen-steps'; -import {$$deepEqual} from '../../json-equal/$$deepEqual'; -import type * as types from '../types'; -import {Vars} from '../Vars'; -import {clone} from '../../json-clone'; - -const createSubExpressionOperator = ( - name: N, - fn: (arr: unknown[], varname: string, vars: Vars, run: () => unknown) => unknown, -) => { - return [ - name, - [], - 3, - (expr: types.TernaryExpression, ctx) => { - const arr = util.asArr(ctx.eval(expr[1], ctx)); - const varname = util.asStr(util.asLiteral(expr[2])); - const expression = expr[3]; - const run = () => ctx.eval(expression, ctx); - return fn(arr, varname, ctx.vars, run); - }, - (ctx: types.OperatorCodegenCtx>): ExpressionResult => { - ctx.link(util.asArr, 'asArr'); - ctx.link(fn, name); - const varname = util.asStr(util.asLiteral(ctx.expr[2])); - const d = ctx.link(ctx.subExpression(ctx.expr[3])); - const operand1 = ctx.operands[0]; - const arr = - operand1 instanceof Literal && operand1.val instanceof Array - ? JSON.stringify(operand1.val) - : `asArr(${operand1})`; - const js = `${name}(${arr},${JSON.stringify(varname)},vars,function(){return ${d}(vars)})`; - return new Expression(js); - }, - ] as types.OperatorDefinition>; -}; - -export const arrayOperators: types.OperatorDefinition[] = [ - [ - 'concat', - ['++'], - -1, - (expr: types.ExprConcat, ctx) => { - const arrays = expr.slice(1).map((e) => ctx.eval(e, ctx)); - return util.concat(arrays); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.concat, 'concat'); - const js = `concat([(${ctx.operands.join('),(')})])`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'push', - [], - -1, - (expr: types.ExprPush, ctx) => { - const operand1 = ctx.eval(expr[1], ctx); - const arr = clone(util.asArr(operand1)); - for (let i = 2; i < expr.length; i++) arr.push(ctx.eval(expr[i], ctx)); - return arr; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const arrOperand = ctx.operands[0]; - let arr: Literal | Expression; - if (arrOperand instanceof Literal) { - arr = new Literal(clone(util.asArr(arrOperand.val))); - } else { - ctx.link(util.asArr, 'asArr'); - arr = new Expression(`asArr(${arrOperand})`); - } - const rArr = ctx.var('' + arr); - const pushes: string[] = []; - for (let i = 1; i < ctx.operands.length; i++) { - const operand = ctx.operands[i]; - pushes.push(`(${rArr}.push(${operand}))`); - } - return new Expression(`(${pushes.join(',')},${rArr})`); - }, - ] as types.OperatorDefinition, - - [ - 'head', - [], - 2, - (expr: types.ExprHead, ctx) => { - const operand1 = ctx.eval(expr[1], ctx); - const operand2 = ctx.eval(expr[2], ctx); - return util.head(operand1, operand2); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.head, 'head'); - const js = `head((${ctx.operands[0]}),(${ctx.operands[1]}))`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'sort', - [], - 1, - (expr: types.ExprSort, ctx) => { - const operand1 = ctx.eval(expr[1], ctx); - const arr = util.asArr(operand1); - /** @todo use `.toSorted()`, once it is more common. */ - return [...arr].sort(); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.asArr, 'asArr'); - const js = `[...asArr(${ctx.operands[0]})].sort()`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'reverse', - [], - 1, - (expr: types.ExprReverse, ctx) => { - const operand1 = ctx.eval(expr[1], ctx); - const arr = util.asArr(operand1); - /** @todo use `.toReversed()`, once it is more common. */ - return [...arr].reverse(); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.asArr, 'asArr'); - const js = `[...asArr(${ctx.operands[0]})].reverse()`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'in', - [], - 2, - (expr: types.ExprIn, ctx) => { - const arr = ctx.eval(expr[1], ctx); - const val = ctx.eval(expr[2], ctx); - return util.isInArr(arr, val); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const arr = ctx.operands[0]; - const val = ctx.operands[1]; - if (val instanceof Literal) { - const fnJs = $$deepEqual(val.val); - const d = ctx.const(fnJs); - ctx.link(util.isInArr2, 'isInArr2'); - const js = `isInArr2((${ctx.operands[0]}),${d})`; - return new Expression(js); - } - ctx.link(util.isInArr, 'isInArr'); - const js = `isInArr((${ctx.operands[0]}),(${ctx.operands[1]}))`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'fromEntries', - [], - 1, - (expr: types.ExprFromEntries, ctx) => { - const operand1 = ctx.eval(expr[1], ctx); - return util.fromEntries(operand1); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.fromEntries, 'fromEntries'); - const js = `fromEntries(${ctx.operands[0]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'indexOf', - [], - 2, - (expr: types.ExprIndexOf, ctx) => { - const operand1 = ctx.eval(expr[1], ctx); - const operand2 = ctx.eval(expr[2], ctx); - return util.indexOf(operand1, operand2); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const val = ctx.operands[1]; - if (val instanceof Literal) { - const fnJs = $$deepEqual(val.val); - const d = ctx.const(fnJs); - ctx.link(util.indexOf2, 'indexOf2'); - const js = `indexOf2((${ctx.operands[0]}),${d})`; - return new Expression(js); - } - ctx.link(util.indexOf, 'indexOf'); - const js = `indexOf((${ctx.operands[0]}),(${ctx.operands[1]}))`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'slice', - [], - 3, - (expr: types.ExprSlice, ctx) => { - const operand1 = util.asArr(ctx.eval(expr[1], ctx)); - const operand2 = util.int(ctx.eval(expr[2], ctx)); - const operand3 = util.int(ctx.eval(expr[3], ctx)); - return operand1.slice(operand2, operand3); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.asArr, 'asArr'); - const js = `asArr(${ctx.operands[0]}).slice((${ctx.operands[1]}),(${ctx.operands[2]}))`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'zip', - [], - 2, - (expr: types.ExprZip, ctx) => { - const operand1 = ctx.eval(expr[1], ctx); - const operand2 = ctx.eval(expr[2], ctx); - return util.zip(operand1, operand2); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.zip, 'zip'); - const js = `zip((${ctx.operands[0]}),(${ctx.operands[1]}))`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - createSubExpressionOperator<'filter'>('filter', util.filter), - createSubExpressionOperator<'map'>('map', util.map), - - [ - 'reduce', - [], - 5, - (expr: types.ExprReduce, ctx) => { - const arr = util.asArr(ctx.eval(expr[1], ctx)); - const initialValue = ctx.eval(expr[2], ctx); - const accname = util.asStr(util.asLiteral(expr[3])); - const varname = util.asStr(util.asLiteral(expr[4])); - const expression = expr[5]; - const run = () => ctx.eval(expression, ctx); - return util.reduce(arr, initialValue, accname, varname, ctx.vars, run); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.asArr, 'asArr'); - ctx.link(util.reduce, 'reduce'); - const accname = util.asStr(util.asLiteral(ctx.expr[3])); - const varname = util.asStr(util.asLiteral(ctx.expr[4])); - const d = ctx.link(ctx.subExpression(ctx.expr[5])); - const operand1 = ctx.operands[0]; - const arr = - operand1 instanceof Literal && operand1.val instanceof Array - ? JSON.stringify(operand1.val) - : `asArr(${operand1})`; - const js = `reduce((${arr}),(${ctx.operands[1]}),${JSON.stringify(accname)},${JSON.stringify( - varname, - )},vars,function(){return ${d}(vars)})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, -]; diff --git a/src/json-expression/operators/binary.ts b/src/json-expression/operators/binary.ts deleted file mode 100644 index 6ef7fa7fcd..0000000000 --- a/src/json-expression/operators/binary.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as util from '../util'; -import {Expression, ExpressionResult} from '../codegen-steps'; -import type * as types from '../types'; - -const binaryOperands = ( - expr: types.BinaryExpression, - ctx: types.OperatorEvalCtx, -): [left: unknown, right: unknown] => { - const left = ctx.eval(expr[1], ctx); - const right = ctx.eval(expr[2], ctx); - return [left, right]; -}; - -export const binaryOperators: types.OperatorDefinition[] = [ - [ - 'u8', - [], - 2, - (expr: types.ExprU8, ctx) => { - const [bin, index] = binaryOperands(expr, ctx); - return util.u8(bin, index); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.u8, 'u8'); - const js = `u8((${ctx.operands[0]}),(${ctx.operands[1]}))`; - return new Expression(js); - }, - ] as types.OperatorDefinition, -]; diff --git a/src/json-expression/operators/bitwise.ts b/src/json-expression/operators/bitwise.ts deleted file mode 100644 index da1e4fdf5b..0000000000 --- a/src/json-expression/operators/bitwise.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as util from '../util'; -import {Expression, ExpressionResult} from '../codegen-steps'; -import type * as types from '../types'; - -const toInt = util.int; - -export const bitwiseOperators: types.OperatorDefinition[] = [ - [ - '&', - ['bitAnd'], - -1, - (expr: types.ExprBitAnd, ctx) => { - return expr.slice(2).reduce((acc, e) => acc & toInt(ctx.eval(e, ctx)), toInt(ctx.eval(expr[1], ctx))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = ctx.operands.map((expr) => `(~~(${expr}))`).join('&'); - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - '|', - ['bitOr'], - -1, - (expr: types.ExprBitOr, ctx) => { - return expr.slice(2).reduce((acc, e) => acc | toInt(ctx.eval(e, ctx)), toInt(ctx.eval(expr[1], ctx))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = ctx.operands.map((expr) => `(~~(${expr}))`).join('|'); - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - '^', - ['bitXor'], - -1, - (expr: types.ExprBitXor, ctx) => { - return expr.slice(2).reduce((acc, e) => acc ^ toInt(ctx.eval(e, ctx)), toInt(ctx.eval(expr[1], ctx))); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = ctx.operands.map((expr) => `(~~(${expr}))`).join('^'); - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - '~', - ['bitNot'], - 1, - (expr: types.ExprBitNot, ctx) => { - return ~toInt(ctx.eval(expr[1], ctx)); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = `~(${ctx.operands[0]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, -]; diff --git a/src/json-expression/operators/branching.ts b/src/json-expression/operators/branching.ts deleted file mode 100644 index 8a12e705d1..0000000000 --- a/src/json-expression/operators/branching.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {Expression, ExpressionResult, Literal} from '../codegen-steps'; -import type * as types from '../types'; - -export const branchingOperators: types.OperatorDefinition[] = [ - [ - '?', - ['if'], - 3, - (expr: types.ExprIf, ctx) => { - return ctx.eval(expr[1], ctx) ? ctx.eval(expr[2], ctx) : ctx.eval(expr[3], ctx); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const condition = ctx.operands[0]; - const then = ctx.operands[1]; - const otherwise = ctx.operands[2]; - if (condition instanceof Literal) return condition.val ? then : otherwise; - return new Expression(`(${condition})?(${then}):(${otherwise})`); - }, - ] as types.OperatorDefinition, - - [ - 'throw', - [], - 1, - (expr: types.ExprThrow, ctx) => { - throw ctx.eval(expr[1], ctx); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`(function(){throw (${ctx.operands[0]})})()`); - }, - ] as types.OperatorDefinition, -]; diff --git a/src/json-expression/operators/comparison.ts b/src/json-expression/operators/comparison.ts deleted file mode 100644 index 710bbea314..0000000000 --- a/src/json-expression/operators/comparison.ts +++ /dev/null @@ -1,195 +0,0 @@ -import {Expression, ExpressionResult, Literal} from '../codegen-steps'; -import {deepEqual} from '../../json-equal/deepEqual'; -import {$$deepEqual} from '../../json-equal/$$deepEqual'; -import * as util from '../util'; -import type * as types from '../types'; - -const eqLitVsExpr = ( - literal: Literal, - expression: Expression, - ctx: types.OperatorCodegenCtx, - not?: boolean, -): ExpressionResult => { - const fn = $$deepEqual(literal.val); - const d = ctx.const(fn); - return new Expression(`${not ? '!' : ''}${d}(${expression})`); -}; - -const binaryOperands = ( - expr: types.BinaryExpression, - ctx: types.OperatorEvalCtx, -): [left: unknown, right: unknown] => { - const left = ctx.eval(expr[1], ctx); - const right = ctx.eval(expr[2], ctx); - return [left, right]; -}; - -const ternaryOperands = ( - expr: types.TernaryExpression, - ctx: types.OperatorEvalCtx, -): [a: unknown, b: unknown, c: unknown] => { - const a = ctx.eval(expr[1], ctx); - const b = ctx.eval(expr[2], ctx); - const c = ctx.eval(expr[3], ctx); - return [a, b, c]; -}; - -export const comparisonOperators: types.OperatorDefinition[] = [ - [ - '==', - ['eq'], - 2, - (expr: types.ExprEquals, ctx) => { - const [left, right] = binaryOperands(expr, ctx); - return deepEqual(left, right); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const a = ctx.operands[0]; - const b = ctx.operands[1]; - if (a instanceof Literal && b instanceof Expression) return eqLitVsExpr(a, b, ctx); - if (b instanceof Literal && a instanceof Expression) return eqLitVsExpr(b, a, ctx); - ctx.link(deepEqual, 'deepEqual'); - return new Expression(`deepEqual(${a},${b})`); - }, - ] as types.OperatorDefinition, - - [ - '!=', - ['ne'], - 2, - (expr: types.ExprNotEquals, ctx) => { - const [left, right] = binaryOperands(expr, ctx); - return !deepEqual(left, right); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const a = ctx.operands[0]; - const b = ctx.operands[1]; - if (a instanceof Literal && b instanceof Expression) return eqLitVsExpr(a, b, ctx, true); - if (b instanceof Literal && a instanceof Expression) return eqLitVsExpr(b, a, ctx, true); - ctx.link(deepEqual, 'deepEqual'); - return new Expression(`!deepEqual(${a},${b})`); - }, - ] as types.OperatorDefinition, - - [ - '>', - ['gt'], - 2, - (expr: types.ExprGreaterThan, ctx) => { - const [left, right] = binaryOperands(expr, ctx); - return left > right; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`(${ctx.operands[0]})>(${ctx.operands[1]})`); - }, - ] as types.OperatorDefinition, - - [ - '>=', - ['ge'], - 2, - (expr: types.ExprGreaterThanOrEqual, ctx) => { - const [left, right] = binaryOperands(expr, ctx); - return left >= right; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`(${ctx.operands[0]})>=(${ctx.operands[1]})`); - }, - ] as types.OperatorDefinition, - - [ - '<', - ['lt'], - 2, - (expr: types.ExprLessThan, ctx) => { - const [left, right] = binaryOperands(expr, ctx); - return left < right; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`(${ctx.operands[0]})<(${ctx.operands[1]})`); - }, - ] as types.OperatorDefinition, - - [ - '<=', - ['le'], - 2, - (expr: types.ExprLessThanOrEqual, ctx) => { - const [left, right] = binaryOperands(expr, ctx); - return left <= right; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - return new Expression(`(${ctx.operands[0]})<=(${ctx.operands[1]})`); - }, - ] as types.OperatorDefinition, - - [ - 'cmp', - [], - 2, - (expr: types.ExprCmp, ctx) => { - const [left, right] = binaryOperands(expr, ctx); - return util.cmp(left, right); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.cmp, 'cmp'); - return new Expression(`cmp((${ctx.operands[0]}),(${ctx.operands[1]}))`); - }, - ] as types.OperatorDefinition, - - [ - '=><=', - ['between'], - 3, - (expr: types.ExprBetweenEqEq, ctx) => { - const [val, min, max] = ternaryOperands(expr, ctx); - return util.betweenEqEq(val, min, max); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.betweenEqEq, 'betweenEqEq'); - return new Expression(`betweenEqEq(${ctx.operands[0]},${ctx.operands[1]},${ctx.operands[2]})`); - }, - ] as types.OperatorDefinition, - - [ - '><', - [], - 3, - (expr: types.ExprBetweenNeNe, ctx) => { - const [val, min, max] = ternaryOperands(expr, ctx); - return util.betweenNeNe(val, min, max); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.betweenNeNe, 'betweenNeNe'); - return new Expression(`betweenNeNe(${ctx.operands[0]},${ctx.operands[1]},${ctx.operands[2]})`); - }, - ] as types.OperatorDefinition, - - [ - '=><', - [], - 3, - (expr: types.ExprBetweenEqNe, ctx) => { - const [val, min, max] = ternaryOperands(expr, ctx); - return util.betweenEqNe(val, min, max); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.betweenEqNe, 'betweenEqNe'); - return new Expression(`betweenEqNe(${ctx.operands[0]},${ctx.operands[1]},${ctx.operands[2]})`); - }, - ] as types.OperatorDefinition, - - [ - '><=', - [], - 3, - (expr: types.ExprBetweenNeEq, ctx) => { - const [val, min, max] = ternaryOperands(expr, ctx); - return util.betweenNeEq(val, min, max); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.betweenNeEq, 'betweenNeEq'); - return new Expression(`betweenNeEq(${ctx.operands[0]},${ctx.operands[1]},${ctx.operands[2]})`); - }, - ] as types.OperatorDefinition, -]; diff --git a/src/json-expression/operators/container.ts b/src/json-expression/operators/container.ts deleted file mode 100644 index a86ea85c18..0000000000 --- a/src/json-expression/operators/container.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {Expression, ExpressionResult} from '../codegen-steps'; -import * as util from '../util'; -import type * as types from '../types'; - -export const containerOperators: types.OperatorDefinition[] = [ - [ - 'len', - [], - 1, - (expr: types.ExprStr, ctx) => { - return util.len(ctx.eval(expr[1], ctx)); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.len, 'len'); - const js = `len(${ctx.operands[0]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - '[]', - ['member'], - 2, - (expr: types.ExprMember, ctx) => { - const container = ctx.eval(expr[1], ctx); - const index = ctx.eval(expr[2], ctx); - return util.member(container, index); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.member, 'member'); - const js = `member((${ctx.operands[0]}),(${ctx.operands[1]}))`; - return new Expression(js); - }, - ] as types.OperatorDefinition, -]; diff --git a/src/json-expression/operators/index.ts b/src/json-expression/operators/index.ts deleted file mode 100644 index 08d12b2211..0000000000 --- a/src/json-expression/operators/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {operatorsToMap} from '../util'; -import {arithmeticOperators} from './arithmetic'; -import {comparisonOperators} from './comparison'; -import {logicalOperators} from './logical'; -import {typeOperators} from './type'; -import {containerOperators} from './container'; -import {stringOperators} from './string'; -import {binaryOperators} from './binary'; -import {arrayOperators} from './array'; -import {objectOperators} from './object'; -import {branchingOperators} from './branching'; -import {inputOperators} from './input'; -import {bitwiseOperators} from './bitwise'; -import {patchOperators} from './patch'; - -export const operators = [ - ...arithmeticOperators, - ...comparisonOperators, - ...logicalOperators, - ...typeOperators, - ...containerOperators, - ...stringOperators, - ...binaryOperators, - ...arrayOperators, - ...objectOperators, - ...branchingOperators, - ...inputOperators, - ...bitwiseOperators, - ...patchOperators, -]; - -export const operatorsMap = operatorsToMap(operators); diff --git a/src/json-expression/operators/input.ts b/src/json-expression/operators/input.ts deleted file mode 100644 index 207bd7fbf9..0000000000 --- a/src/json-expression/operators/input.ts +++ /dev/null @@ -1,75 +0,0 @@ -import {Expression, ExpressionResult, Literal} from '../codegen-steps'; -import * as util from '../util'; -import * as jsonPointer from '@jsonjoy.com/json-pointer'; -import {Vars} from '../Vars'; -import {$$find} from '@jsonjoy.com/json-pointer/lib/codegen/find'; -import type * as types from '../types'; - -const get = (vars: Vars, varname: unknown) => { - if (typeof varname !== 'string') throw new Error('varname must be a string.'); - const [name, pointer] = util.parseVar(varname); - jsonPointer.validateJsonPointer(pointer); - const data = vars.get(name); - const path = jsonPointer.toPath(pointer); - const value = jsonPointer.get(data, path); - return value; -}; - -export const inputOperators: types.OperatorDefinition[] = [ - [ - '$', - ['get'], - [1, 2], - (expr: types.ExprGet, ctx: types.OperatorEvalCtx) => { - const varname = ctx.eval(expr[1], ctx); - const defval = ctx.eval(expr[2], ctx); - const value = get(ctx.vars, varname); - return util.throwOnUndef(value, defval); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.throwOnUndef, 'throwOnUndef'); - const varname = ctx.operands[0]; - if (varname instanceof Literal) { - if (typeof varname.val !== 'string') throw new Error('varname must be a string.'); - const [name, pointer] = util.parseVar(varname.val); - if (!pointer) return new Expression(!name ? 'vars.env' : `vars.get(${JSON.stringify(name)})`); - jsonPointer.validateJsonPointer(pointer); - const hasDefaultValue = ctx.expr.length === 3; - const defaultValue = hasDefaultValue ? ctx.operands[1] : undefined; - const fn = $$find(jsonPointer.toPath(pointer)); - const find = ctx.const(fn); - const data = `vars.get(${JSON.stringify(name)})`; - return new Expression(`throwOnUndef(${find}(${data}),(${defaultValue}))`); - } - ctx.link(get, 'get'); - return new Expression(`throwOnUndef(get(vars,(${varname})),(${ctx.operands[1]}))`); - }, - /* has side-effects */ true, - ] as types.OperatorDefinition, - - [ - '$?', - ['get?'], - 1, - (expr: types.ExprDefined, ctx: types.OperatorEvalCtx) => { - const varname = ctx.eval(expr[1], ctx); - const value = get(ctx.vars, varname); - return value !== undefined; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const varname = ctx.operands[0]; - if (varname instanceof Literal) { - if (typeof varname.val !== 'string') throw new Error('varname must be a string.'); - const [name, pointer] = util.parseVar(varname.val); - jsonPointer.validateJsonPointer(pointer); - const fn = $$find(jsonPointer.toPath(pointer)); - const find = ctx.const(fn); - const data = `vars.get(${JSON.stringify(name)})`; - return new Expression(`${find}(${data})!==undefined`); - } - ctx.link(get, 'get'); - return new Expression(`get(vars,(${varname}))!==undefined`); - }, - /* has side-effects */ true, - ] as types.OperatorDefinition, -]; diff --git a/src/json-expression/operators/logical.ts b/src/json-expression/operators/logical.ts deleted file mode 100644 index 7b6f4e9685..0000000000 --- a/src/json-expression/operators/logical.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {Expression, ExpressionResult} from '../codegen-steps'; -import type * as types from '../types'; - -export const logicalOperators: types.OperatorDefinition[] = [ - [ - '&&', - ['and'], - -1, - (expr: types.ExprAnd, ctx) => { - return expr.slice(1).reduce((acc, e) => acc && ctx.eval(e, ctx), true); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = ctx.operands.map((expr) => `(${expr})`).join('&&'); - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - '||', - ['or'], - -1, - (expr: types.ExprOr, ctx) => { - return expr.slice(1).reduce((acc, e) => acc || ctx.eval(e, ctx), false); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = ctx.operands.map((expr) => `(${expr})`).join('||'); - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - '!', - ['not'], - 1, - (expr: types.ExprNot, ctx) => { - return !ctx.eval(expr[1], ctx); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = `!(${ctx.operands[0]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, -]; diff --git a/src/json-expression/operators/object.ts b/src/json-expression/operators/object.ts deleted file mode 100644 index dfae76fd39..0000000000 --- a/src/json-expression/operators/object.ts +++ /dev/null @@ -1,170 +0,0 @@ -import * as util from '../util'; -import {Expression, ExpressionResult, Literal} from '../codegen-steps'; -import type * as types from '../types'; - -const validateSetOperandCount = (count: number) => { - if (count < 3) { - throw new Error('Not enough operands for "o.set".'); - } - if (count % 2 !== 0) { - throw new Error('Invalid number of operands for "o.set" operand.'); - } -}; - -const validateDelOperandCount = (count: number) => { - if (count < 3) { - throw new Error('Not enough operands for "o.del".'); - } -}; - -export const objectOperators: types.OperatorDefinition[] = [ - [ - 'keys', - [], - 1, - (expr: types.ExprKeys, ctx) => { - const operand = ctx.eval(expr[1], ctx); - return util.keys(operand); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.keys, 'keys'); - const js = `keys(${ctx.operands[0]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'values', - [], - 1, - (expr: types.ExprValues, ctx) => { - const operand = ctx.eval(expr[1], ctx); - return util.values(operand); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.values, 'values'); - const js = `values(${ctx.operands[0]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'entries', - [], - 1, - (expr: types.ExprEntries, ctx) => { - const operand = ctx.eval(expr[1], ctx); - return util.entries(operand); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.entries, 'entries'); - const js = `entries(${ctx.operands[0]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'o.set', - [], - -1, - /** - * Set one or more properties on an object. - * - * ``` - * ['o.set', {}, - * 'a', 1, - * 'b', ['+', 2, 3], - * ] - * ``` - * - * Results in: - * - * ``` - * { - * a: 1, - * b: 5, - * } - * ``` - */ - (expr: types.ExprObjectSet, ctx) => { - let i = 1; - const length = expr.length; - validateSetOperandCount(length); - const doc = util.asObj(ctx.eval(expr[i++], ctx)) as Record; - while (i < length) { - const prop = util.str(ctx.eval(expr[i++], ctx)) as string; - if (prop === '__proto__') throw new Error('PROTO_KEY'); - const value = ctx.eval(expr[i++], ctx); - doc[prop] = value; - } - return doc; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const length = ctx.operands.length; - validateSetOperandCount(length + 1); - let i = 0; - let curr = ctx.operands[i++]; - if (curr instanceof Literal) { - curr = new Literal(util.asObj(curr.val)); - } else if (curr instanceof Expression) { - ctx.link(util.asObj, 'asObj'); - curr = new Expression(`asObj(${curr})`); - } - ctx.link(util.str, 'str'); - ctx.link(util.objSetRaw, 'objSetRaw'); - while (i < length) { - let prop = ctx.operands[i++]; - if (prop instanceof Literal) { - prop = new Literal(util.str(prop.val)); - } else if (prop instanceof Expression) { - prop = new Expression(`str(${prop})`); - } - const value = ctx.operands[i++]; - curr = new Expression(`objSetRaw(${curr}, ${prop}, ${value})`); - } - return curr; - }, - ] as types.OperatorDefinition, - - [ - 'o.del', - [], - -1, - /** - * Delete one or more properties from an object. - * - * ``` - * ['o.del', {}, 'prop1', 'prop2'] - * ``` - */ - (expr: types.ExprObjectSet, ctx) => { - let i = 1; - const length = expr.length; - validateDelOperandCount(length); - const doc = util.asObj(ctx.eval(expr[i++], ctx)) as Record; - while (i < length) { - const prop = util.str(ctx.eval(expr[i++], ctx)) as string; - delete doc[prop]; - } - return doc; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const length = ctx.operands.length; - validateDelOperandCount(length + 1); - let i = 0; - let curr = ctx.operands[i++]; - ctx.link(util.str, 'str'); - ctx.link(util.objDelRaw, 'objDelRaw'); - while (i < length) { - let prop = ctx.operands[i++]; - if (prop instanceof Literal) { - prop = new Literal(util.str(prop.val)); - } else if (prop instanceof Expression) { - prop = new Expression(`str(${prop})`); - } - curr = new Expression(`objDelRaw(${curr}, ${prop})`); - } - return curr; - }, - ] as types.OperatorDefinition, -]; diff --git a/src/json-expression/operators/patch.ts b/src/json-expression/operators/patch.ts deleted file mode 100644 index f6ec00c22d..0000000000 --- a/src/json-expression/operators/patch.ts +++ /dev/null @@ -1,114 +0,0 @@ -import {Expression, ExpressionResult} from '../codegen-steps'; -import type * as types from '../types'; -import {Path, toPath} from '@jsonjoy.com/json-pointer'; -import {JavaScript, JavaScriptLinked, compileClosure} from '@jsonjoy.com/util/lib/codegen'; -import {$findRef} from '@jsonjoy.com/json-pointer/lib/codegen/findRef'; -import {find} from '@jsonjoy.com/json-pointer/lib/find'; - -const validateAddOperandCount = (count: number) => { - if (count < 3) { - throw new Error('Not enough operands for "jp.add" operand.'); - } - if (count % 2 !== 0) { - throw new Error('Invalid number of operands for "jp.add" operand.'); - } -}; - -const validateAddPath = (path: unknown) => { - if (typeof path !== 'string') { - throw new Error('The "path" argument for "jp.add" must be a const string.'); - } -}; - -type AddFn = (doc: unknown, value: unknown) => unknown; - -export const $$add = (path: Path): JavaScriptLinked => { - const find = $findRef(path); - const js = /* js */ ` -(function(find, path){ - return function(doc, value){ - var f = find(doc); - var obj = f.obj, key = f.key, val = f.val; - if (!obj) doc = value; - else if (typeof key === 'string') obj[key] = value; - else { - var length = obj.length; - if (key < length) obj.splice(key, 0, value); - else if (key > length) throw new Error('INVALID_INDEX'); - else obj.push(value); - } - return doc; - }; -})`; - - return { - deps: [find] as unknown[], - js: js as JavaScript<(...deps: unknown[]) => AddFn>, - }; -}; - -export const $add = (path: Path): AddFn => compileClosure($$add(path)); - -export const patchOperators: types.OperatorDefinition[] = [ - [ - 'jp.add', - [], - -1, - /** - * Applies JSON Patch "add" operations to the input value. - * - * ``` - * ['add', {}, - * '/a', 1, - * '/b', ['+', 2, 3], - * ] - * ``` - * - * Results in: - * - * ``` - * { - * a: 1, - * b: 5, - * } - * ``` - */ - (expr: types.JsonPatchAdd, ctx) => { - let i = 1; - const length = expr.length; - validateAddOperandCount(length); - let doc = ctx.eval(expr[i++], ctx); - while (i < length) { - const path = expr[i++]; - validateAddPath(path); - const value = ctx.eval(expr[i++], ctx); - const {obj, key} = find(doc, toPath(path)); - if (!obj) doc = value; - else if (typeof key === 'string') (obj as any)[key] = value; - else if (obj instanceof Array) { - const length = obj.length; - if ((key as number) < length) obj.splice(key as number, 0, value); - else if ((key as number) > length) throw new Error('INVALID_INDEX'); - else obj.push(value); - } - } - return doc; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const expr = ctx.expr; - const length = ctx.operands.length; - validateAddOperandCount(length + 1); - let i = 0; - let curr = ctx.operands[i++]; - while (i < length) { - const path = expr[1 + i++]; - validateAddPath(path); - const value = ctx.operands[i++]; - const addCompiled = $add(toPath(path)); - const dAdd = ctx.link(addCompiled); - curr = new Expression(`${dAdd}(${curr}, ${value})`); - } - return curr; - }, - ] as types.OperatorDefinition, -]; diff --git a/src/json-expression/operators/string.ts b/src/json-expression/operators/string.ts deleted file mode 100644 index 6c34c6562b..0000000000 --- a/src/json-expression/operators/string.ts +++ /dev/null @@ -1,153 +0,0 @@ -import {Expression, ExpressionResult, Literal} from '../codegen-steps'; -import * as util from '../util'; -import type * as types from '../types'; - -const binaryOperands = ( - expr: types.BinaryExpression, - ctx: types.OperatorEvalCtx, -): [left: unknown, right: unknown] => { - const left = ctx.eval(expr[1], ctx); - const right = ctx.eval(expr[2], ctx); - return [left, right]; -}; - -const createValidationOperator = (name: string, validate: (value: unknown) => boolean) => { - return [ - name + '?', - [], - 1, - (expr: E, ctx) => { - const email = ctx.eval(expr[1], ctx); - return validate(email); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(validate, 'is_' + name); - return new Expression(`is_${name}(${ctx.operands[0]})`); - }, - ] as types.OperatorDefinition; -}; - -export const stringOperators: types.OperatorDefinition[] = [ - [ - '.', - ['cat'], - -1, - (expr: types.ExprCat, ctx) => { - return expr.slice(1).reduce((acc, e) => acc + util.str(ctx.eval(e, ctx)), ''); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.str, 'str'); - const parts: string[] = []; - for (const operand of ctx.operands) { - if (operand instanceof Literal) { - parts.push(JSON.stringify(util.str(operand.val))); - } else if (operand instanceof Expression) { - parts.push(`str(${operand})`); - } - } - return new Expression(parts.join('+')); - }, - ] as types.OperatorDefinition, - - [ - 'contains', - [], - 2, - (expr: types.ExprContains, ctx) => { - const [outer, inner] = binaryOperands(expr, ctx); - return util.contains(outer, inner); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.contains, 'contains'); - const js = `contains(${ctx.operands[0]},${ctx.operands[1]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'starts', - [], - 2, - (expr: types.ExprStarts, ctx) => { - const [outer, inner] = binaryOperands(expr, ctx); - return util.starts(outer, inner); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.starts, 'starts'); - const js = `starts(${ctx.operands[0]},${ctx.operands[1]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'ends', - [], - 2, - (expr: types.ExprEnds, ctx) => { - const [outer, inner] = binaryOperands(expr, ctx); - return util.ends(outer, inner); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.ends, 'ends'); - const js = `ends(${ctx.operands[0]},${ctx.operands[1]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'substr', - [], - 3, - (expr: types.ExprSubstr, ctx) => { - const str = ctx.eval(expr[1], ctx); - const start = ctx.eval(expr[2], ctx); - const end = ctx.eval(expr[3], ctx); - return util.substr(str, start, end); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.substr, 'substr'); - const js = `substr(${ctx.operands[0]},${ctx.operands[1]},${ctx.operands[2]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'matches', - [], - 2, - (expr: types.ExprEnds, ctx) => { - let pattern = expr[2]; - if (pattern instanceof Array && pattern.length === 1) pattern = pattern[0]; - if (typeof pattern !== 'string') - throw new Error('"matches" second argument should be a regular expression string.'); - if (!ctx.createPattern) - throw new Error('"matches" operator requires ".createPattern()" option to be implemented.'); - const fn = ctx.createPattern(pattern); - const outer = ctx.eval(expr[1], ctx); - return fn(util.str(outer)); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const pattern = ctx.operands[1]; - if (!(pattern instanceof Literal) || typeof pattern.val !== 'string') - throw new Error('"matches" second argument should be a regular expression string.'); - if (!ctx.createPattern) - throw new Error('"matches" operator requires ".createPattern()" option to be implemented.'); - const fn = ctx.createPattern(pattern.val); - const d = ctx.link(fn); - ctx.link(util.str, 'str'); - const subject = ctx.operands[0]; - return new Expression(`${d}(str(${subject}))`); - }, - ] as types.OperatorDefinition, - - createValidationOperator('email', util.isEmail), - createValidationOperator('hostname', util.isHostname), - createValidationOperator('ip4', util.isIp4), - createValidationOperator('ip6', util.isIp6), - createValidationOperator('uuid', util.isUuid), - createValidationOperator('uri', util.isUri), - createValidationOperator('duration', util.isDuration), - createValidationOperator('date', util.isDate), - createValidationOperator('time', util.isTime), - createValidationOperator('dateTime', util.isDateTime), -]; diff --git a/src/json-expression/operators/type.ts b/src/json-expression/operators/type.ts deleted file mode 100644 index d4e9d9e476..0000000000 --- a/src/json-expression/operators/type.ts +++ /dev/null @@ -1,178 +0,0 @@ -import {Expression, ExpressionResult} from '../codegen-steps'; -import * as util from '../util'; -import type * as types from '../types'; - -export const typeOperators: types.OperatorDefinition[] = [ - [ - 'type', - [], - 1, - (expr: types.ExprNot, ctx) => { - return util.type(ctx.eval(expr[1], ctx)); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.type, 'type'); - const js = `type(${ctx.operands[0]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'bool', - [], - 1, - (expr: types.ExprBool, ctx) => { - return !!ctx.eval(expr[1], ctx); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = `!!${ctx.operands[0]}`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'num', - [], - 1, - (expr: types.ExprNum, ctx) => { - return util.num(ctx.eval(expr[1], ctx)); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = `+(${ctx.operands[0]})||0`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'str', - [], - 1, - (expr: types.ExprStr, ctx) => { - return util.str(ctx.eval(expr[1], ctx)); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.str, 'str'); - const js = `str(${ctx.operands[0]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'len', - [], - 1, - (expr: types.ExprStr, ctx) => { - return util.len(ctx.eval(expr[1], ctx)); - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.len, 'len'); - const js = `len(${ctx.operands[0]})`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'und?', - [], - 1, - (expr: types.ExprIsUndefined, ctx) => { - return ctx.eval(expr[1], ctx) === undefined; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = `(${ctx.operands[0]})===undefined`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'nil?', - [], - 1, - (expr: types.ExprIsNull, ctx) => { - return ctx.eval(expr[1], ctx) === null; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = `(${ctx.operands[0]})===null`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'bool?', - [], - 1, - (expr: types.ExprIsBool, ctx) => { - return typeof ctx.eval(expr[1], ctx) === 'boolean'; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = `typeof(${ctx.operands[0]})==='boolean'`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'num?', - [], - 1, - (expr: types.ExprIsNumber, ctx) => { - return typeof ctx.eval(expr[1], ctx) === 'number'; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = `typeof(${ctx.operands[0]})==='number'`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'str?', - [], - 1, - (expr: types.ExprIsString, ctx) => { - return typeof ctx.eval(expr[1], ctx) === 'string'; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = `typeof(${ctx.operands[0]})==='string'`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'bin?', - [], - 1, - (expr: types.ExprIsBinary, ctx) => { - return ctx.eval(expr[1], ctx) instanceof Uint8Array; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = `(${ctx.operands[0]})instanceof Uint8Array`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'arr?', - [], - 1, - (expr: types.ExprIsArray, ctx) => { - return ctx.eval(expr[1], ctx) instanceof Array; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - const js = `(${ctx.operands[0]})instanceof Array`; - return new Expression(js); - }, - ] as types.OperatorDefinition, - - [ - 'obj?', - [], - 1, - (expr: types.ExprIsObject, ctx) => { - return util.type(ctx.eval(expr[1], ctx)) === 'object'; - }, - (ctx: types.OperatorCodegenCtx): ExpressionResult => { - ctx.link(util.type, 'type'); - const js = `type(${ctx.operands[0]})==='object'`; - return new Expression(js); - }, - ] as types.OperatorDefinition, -]; diff --git a/src/json-expression/types.ts b/src/json-expression/types.ts deleted file mode 100644 index 945ccc2dd7..0000000000 --- a/src/json-expression/types.ts +++ /dev/null @@ -1,329 +0,0 @@ -import type {JavaScript} from '@jsonjoy.com/util/lib/codegen'; -import type {Vars} from './Vars'; -import type {ExpressionResult} from './codegen-steps'; -import type {JsonExpressionFn} from './codegen'; - -export type Literal = T | LiteralExpression; -export type LiteralExpression = [constant: O]; -export type UnaryExpression = [operator: O, operand1: A1]; -export type BinaryExpression = [ - operator: O, - operand1: A1, - operand2: A2, -]; -export type TernaryExpression< - O, - A1 extends Expression = Expression, - A2 extends Expression = Expression, - A3 extends Expression = Expression, -> = [operator: O, operand1: A1, operand2: A2, operand3: A3]; -export type QuaternaryExpression< - O, - A1 extends Expression = Expression, - A2 extends Expression = Expression, - A3 extends Expression = Expression, - A4 extends Expression = Expression, -> = [operator: O, operand1: A1, operand2: A2, operand3: A3, operand4: A4]; -export type QuinaryExpression< - O, - A1 extends Expression = Expression, - A2 extends Expression = Expression, - A3 extends Expression = Expression, - A4 extends Expression = Expression, - A5 extends Expression = Expression, -> = [operator: O, operand1: A1, operand2: A2, operand3: A3, operand4: A4, operand5: A5]; -export type VariadicExpression = [operator: O, ...operands: A[]]; - -export type Expression = - | Literal - | UnaryExpression - | BinaryExpression - | TernaryExpression - | QuaternaryExpression - | QuinaryExpression - | VariadicExpression; - -// Arithmetic expressions -export type ArithmeticExpression = - | ExprPlus - | ExprMinus - | ExprAsterisk - | ExprSlash - | ExprMod - | ExprMin - | ExprMax - | ExprRound - | ExprCeil - | ExprFloor - | ExprAbs - | ExprSqrt - | ExprExp - | ExprLn - | ExprLog - | ExprLog10 - | ExprPow - | ExprTrunc; - -export type ExprPlus = VariadicExpression<'add' | '+'>; -export type ExprMinus = VariadicExpression<'subtract' | '-'>; -export type ExprAsterisk = VariadicExpression<'multiply' | '*'>; -export type ExprSlash = VariadicExpression<'divide' | '/'>; -export type ExprMod = VariadicExpression<'mod' | '%'>; -export type ExprMin = VariadicExpression<'min'>; -export type ExprMax = VariadicExpression<'max'>; -export type ExprRound = UnaryExpression<'round'>; -export type ExprCeil = UnaryExpression<'ceil'>; -export type ExprFloor = UnaryExpression<'floor'>; -export type ExprTrunc = UnaryExpression<'trunc'>; -export type ExprAbs = UnaryExpression<'abs'>; -export type ExprSqrt = UnaryExpression<'sqrt'>; -export type ExprExp = UnaryExpression<'exp'>; -export type ExprLn = UnaryExpression<'ln'>; -export type ExprLog = BinaryExpression<'log'>; -export type ExprLog10 = UnaryExpression<'log10'>; -export type ExprPow = BinaryExpression<'pow' | '**'>; - -// Comparison expressions -export type ComparisonExpression = - | ExprEquals - | ExprNotEquals - | ExprLessThan - | ExprLessThanOrEqual - | ExprGreaterThan - | ExprGreaterThanOrEqual - | ExprCmp - | ExprBetweenNeNe - | ExprBetweenEqNe - | ExprBetweenNeEq - | ExprBetweenEqEq; - -export type ExprEquals = BinaryExpression<'eq' | '=='>; -export type ExprNotEquals = BinaryExpression<'ne' | '!='>; -export type ExprGreaterThan = BinaryExpression<'gt' | '>'>; -export type ExprGreaterThanOrEqual = BinaryExpression<'ge' | '>='>; -export type ExprLessThan = BinaryExpression<'lt' | '<'>; -export type ExprLessThanOrEqual = BinaryExpression<'le' | '<='>; -export type ExprCmp = BinaryExpression<'cmp'>; -export type ExprBetweenEqEq = TernaryExpression<'between' | '=><='>; -export type ExprBetweenNeNe = TernaryExpression<'><'>; -export type ExprBetweenEqNe = TernaryExpression<'=><'>; -export type ExprBetweenNeEq = TernaryExpression<'><='>; - -// Logical expressions -export type LogicalExpression = ExprAnd | ExprOr | ExprNot; - -export type ExprAnd = VariadicExpression<'and' | '&&'>; -export type ExprOr = VariadicExpression<'or' | '||'>; -export type ExprNot = UnaryExpression<'not' | '!'>; - -// Container expressions -export type ContainerExpression = ExprLen | ExprMember; - -export type ExprLen = UnaryExpression<'len'>; -export type ExprMember = BinaryExpression<'member' | '[]'>; - -// Type expressions -export type TypeExpression = - | ExprType - | ExprBool - | ExprNum - | ExprStr - | ExprIsUndefined - | ExprIsNull - | ExprIsBool - | ExprIsNumber - | ExprIsString - | ExprIsBinary - | ExprIsArray - | ExprIsObject; - -export type ExprType = UnaryExpression<'type'>; -export type ExprBool = UnaryExpression<'bool'>; -export type ExprNum = UnaryExpression<'num'>; -export type ExprStr = UnaryExpression<'str'>; -export type ExprIsUndefined = UnaryExpression<'und?'>; -export type ExprIsNull = UnaryExpression<'nil?'>; -export type ExprIsBool = UnaryExpression<'bool?'>; -export type ExprIsNumber = UnaryExpression<'num?'>; -export type ExprIsString = UnaryExpression<'str?'>; -export type ExprIsBinary = UnaryExpression<'bin?'>; -export type ExprIsArray = UnaryExpression<'arr?'>; -export type ExprIsObject = UnaryExpression<'obj?'>; - -// String expressions -export type StringExpression = - | ExprCat - | ExprContains - | ExprStarts - | ExprEnds - | ExprMatches - | ExprSubstr - | ExprIsEmail - | ExprIsHostname - | ExprIsIp4 - | ExprIsIp6 - | ExprIsUuid - | ExprIsUri - | ExprIsDuration - | ExprIsDate - | ExprIsTime - | ExprIsDateTime; - -export type ExprCat = VariadicExpression<'cat' | '.'>; -export type ExprContains = BinaryExpression<'contains'>; -export type ExprStarts = BinaryExpression<'starts'>; -export type ExprEnds = BinaryExpression<'ends'>; -export type ExprMatches = BinaryExpression<'matches'>; -export type ExprSubstr = TernaryExpression<'substr'>; -export type ExprIsEmail = UnaryExpression<'email?'>; -export type ExprIsHostname = UnaryExpression<'hostname?'>; -export type ExprIsIp4 = UnaryExpression<'ip4?'>; -export type ExprIsIp6 = UnaryExpression<'ip6?'>; -export type ExprIsUuid = UnaryExpression<'uuid?'>; -export type ExprIsUri = UnaryExpression<'uri?'>; -export type ExprIsDuration = UnaryExpression<'duration?'>; -export type ExprIsDate = UnaryExpression<'date?'>; -export type ExprIsTime = UnaryExpression<'time?'>; -export type ExprIsDateTime = UnaryExpression<'dateTime?'>; - -// Binary expressions -export type BinaryExpressions = ExprU8 | ExprI8 | ExprU16 | ExprI16 | ExprU32 | ExprI32 | ExprF32 | ExprF64; - -export type ExprU8 = BinaryExpression<'u8'>; -export type ExprI8 = BinaryExpression<'i8'>; -export type ExprU16 = BinaryExpression<'u16'>; -export type ExprI16 = BinaryExpression<'i16'>; -export type ExprU32 = BinaryExpression<'u32'>; -export type ExprI32 = BinaryExpression<'i32'>; -export type ExprF32 = BinaryExpression<'f32'>; -export type ExprF64 = BinaryExpression<'f64'>; - -// Array expressions -export type ArrayExpression = - | ExprConcat - | ExprPush - | ExprHead - | ExprSort - | ExprReverse - | ExprIn - | ExprFromEntries - | ExprIndexOf - | ExprSlice - | ExprZip - | ExprFilter - | ExprMap - | ExprReduce; - -export type ExprConcat = VariadicExpression<'concat' | '++'>; -export type ExprPush = VariadicExpression<'push'>; -export type ExprHead = BinaryExpression<'head'>; -export type ExprSort = UnaryExpression<'sort'>; -export type ExprReverse = UnaryExpression<'reverse'>; -export type ExprIn = BinaryExpression<'in'>; -export type ExprFromEntries = UnaryExpression<'fromEntries'>; -export type ExprIndexOf = BinaryExpression<'indexOf'>; -export type ExprSlice = TernaryExpression<'slice'>; -export type ExprZip = BinaryExpression<'zip'>; -export type ExprFilter = TernaryExpression<'filter'>; -export type ExprMap = TernaryExpression<'map'>; -export type ExprReduce = QuinaryExpression<'reduce'>; - -// Object expressions -export type ObjectExpression = ExprKeys | ExprValues | ExprEntries | ExprObjectSet | ExprObjectDel; - -export type ExprKeys = UnaryExpression<'keys'>; -export type ExprValues = UnaryExpression<'values'>; -export type ExprEntries = UnaryExpression<'entries'>; -export type ExprObjectSet = VariadicExpression<'o.set'>; -export type ExprObjectDel = VariadicExpression<'o.del'>; - -// Bitwise expressions -export type BitwiseExpression = ExprBitAnd | ExprBitOr | ExprBitXor | ExprBitNot; - -export type ExprBitAnd = VariadicExpression<'bitAnd' | '&'>; -export type ExprBitOr = VariadicExpression<'bitOr' | '|'>; -export type ExprBitXor = VariadicExpression<'bitXor' | '^'>; -export type ExprBitNot = UnaryExpression<'bitNot' | '~'>; - -// Branching expressions -export type BranchingExpression = ExprIf | ExprThrow; - -export type ExprIf = TernaryExpression<'if' | '?'>; -export type ExprThrow = UnaryExpression<'throw'>; - -// Input expressions -export type InputExpression = ExprGet | ExprDefined; - -export type ExprGet = UnaryExpression<'get' | '$'> | BinaryExpression<'get' | '$'>; -export type ExprDefined = UnaryExpression<'get?' | '$?'>; - -// JSON Patch expressions -export type JsonPatchExpression = JsonPatchAdd; - -export type JsonPatchAdd = VariadicExpression<'jp.add'>; - -export type Expr = - | ArithmeticExpression - | ComparisonExpression - | LogicalExpression - | TypeExpression - | ContainerExpression - | StringExpression - | BinaryExpressions - | ArrayExpression - | ObjectExpression - | BitwiseExpression - | BranchingExpression - | InputExpression - | JsonPatchExpression; - -export interface JsonExpressionExecutionContext { - vars: Vars; -} - -export interface JsonExpressionCodegenContext { - createPattern?: (pattern: string) => (value: string) => boolean; -} - -export type JsonExpressionContext = JsonExpressionExecutionContext & JsonExpressionCodegenContext; - -export type OperatorDefinition = [ - /** Canonical operator name. */ - name: string, - - /** Alternative names for this operator. */ - aliases: Array, - - /** Operator arity. -1 means operator is variadic. */ - arity: -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | [min: number, max: number], - - /** Evaluates an expression with this operator. */ - eval: OperatorEval, - - /** Compile expression to executable JavaScript. */ - codegen: (ctx: OperatorCodegenCtx) => ExpressionResult, - - /** - * Whether this expression has side effects. For example, data retrieval - * expressions or random value generation is considered impure. - */ - impure?: boolean, -]; - -export type OperatorEval = (expr: E, ctx: OperatorEvalCtx) => unknown; - -export interface OperatorEvalCtx extends JsonExpressionExecutionContext, JsonExpressionCodegenContext { - eval: OperatorEval; -} - -export interface OperatorCodegenCtx extends JsonExpressionCodegenContext { - expr: E; - operands: ExpressionResult[]; - operand: (operand: Expression) => ExpressionResult; - link: (value: unknown, name?: string) => string; - const: (js: JavaScript) => string; - subExpression: (expr: Expression) => JsonExpressionFn; - var: (name: string) => string; -} - -export type OperatorMap = Map>; diff --git a/src/json-expression/util.ts b/src/json-expression/util.ts deleted file mode 100644 index 41e22719c4..0000000000 --- a/src/json-expression/util.ts +++ /dev/null @@ -1,397 +0,0 @@ -import {deepEqual} from '../json-equal/deepEqual'; -import {toPath, get as get_} from '@jsonjoy.com/json-pointer'; -import type {Vars} from './Vars'; -import type {Expression, Literal, OperatorDefinition, OperatorMap} from './types'; - -// ----------------------------------------------------- Input operator helpers - -export const get = (path: string, data: unknown) => get_(data, toPath(path)); - -export const throwOnUndef = (value: unknown, def?: unknown) => { - if (value !== undefined) return value; - if (def === undefined) throw new Error('NOT_FOUND'); - return def; -}; - -// ------------------------------------------------------ Type operator helpers - -export const type = (value: unknown): string => { - if (value === null) return 'null'; - if (value instanceof Array) return 'array'; - if (value instanceof Uint8Array) return 'binary'; - return typeof value; -}; - -export const num = (value: unknown): number => +(value as number) || 0; -export const int = (value: unknown): number => ~~(value as number); - -export const str = (value: unknown): string => { - if (typeof value !== 'object') return '' + value; - return JSON.stringify(value); -}; - -// ------------------------------------------------ Comparison operator helpers - -export const cmp = (a: any, b: any): 1 | -1 | 0 => (a > b ? 1 : a < b ? -1 : 0); -export const betweenNeNe = (val: any, min: any, max: any): boolean => val > min && val < max; -export const betweenNeEq = (val: any, min: any, max: any): boolean => val > min && val <= max; -export const betweenEqNe = (val: any, min: any, max: any): boolean => val >= min && val < max; -export const betweenEqEq = (val: any, min: any, max: any): boolean => val >= min && val <= max; - -// ------------------------------------------------ Arithmetic operator helpers - -export const slash = (a: unknown, b: unknown) => { - const divisor = num(b); - if (divisor === 0) throw new Error('DIVISION_BY_ZERO'); - const res = num(a) / divisor; - return Number.isFinite(res) ? res : 0; -}; - -export const mod = (a: unknown, b: unknown) => { - const divisor = num(b); - if (divisor === 0) throw new Error('DIVISION_BY_ZERO'); - const res = num(a) % divisor; - return Number.isFinite(res) ? res : 0; -}; - -// ----------------------------------------- Generic container operator helpers - -export const len = (value: unknown): number => { - switch (typeof value) { - case 'string': - return value.length; - case 'object': { - if (value instanceof Array) return value.length; - if (value instanceof Uint8Array) return value.length; - if (!value) return 0; - return Object.keys(value).length; - } - default: - return 0; - } -}; - -export const member = (container: unknown, index: unknown): unknown => { - switch (typeof container) { - case 'string': { - const i = int(index); - if (i < 0 || i >= container.length) return undefined; - return container[i]; - } - case 'object': { - if (!container) throw new Error('NOT_CONTAINER'); - if (container instanceof Array || container instanceof Uint8Array) { - const i = int(index); - if (i < 0 || i >= container.length) return undefined; - return container[i]; - } - switch (typeof index) { - case 'string': - case 'number': - return (container as any)[index]; - default: - throw new Error('NOT_STRING_INDEX'); - } - } - default: - throw new Error('NOT_CONTAINER'); - } -}; - -export const asBin = (value: unknown): Uint8Array => { - if (value instanceof Uint8Array) return value; - throw new Error('NOT_BINARY'); -}; - -// ---------------------------------------------------- String operator helpers - -export const asStr = (value: unknown): string => { - if (typeof value === 'string') return value; - throw new Error('NOT_STRING'); -}; - -export const starts = (outer: unknown, inner: unknown): boolean => { - return str(outer).startsWith(str(inner)); -}; - -export const contains = (outer: unknown, inner: unknown): boolean => { - return str(outer).indexOf(str(inner)) > -1; -}; - -export const ends = (outer: unknown, inner: unknown): boolean => { - return str(outer).endsWith(str(inner)); -}; - -export const substr = (probablyString: string | unknown, from: number | unknown, to: number | unknown) => - str(probablyString).slice(int(from), int(to)); - -const EMAIL_REG = - /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i; -const HOSTNAME_REG = - /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[-0-9a-z]{0,61}[0-9a-z])?)*\.?$/i; -const IP4_REG = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/; -const IP6_REG = - /^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))$/i; -const UUID_REG = /^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i; -const NOT_URI_FRAGMENT_REG = /\/|:/; -const URI_REG = - /^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i; -const DURATION_REG = /^P(?!$)((\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?|(\d+W)?)$/; -const DATE_REG = /^(\d\d\d\d)-(\d\d)-(\d\d)$/; -const TIME_REG = /^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(z|([+-])(\d\d)(?::?(\d\d))?)?$/i; -const DATE_TIME_SEPARATOR_REG = /t|\s/i; - -export const isEmail = (value: unknown): boolean => typeof value === 'string' && EMAIL_REG.test(value); -export const isHostname = (value: unknown): boolean => typeof value === 'string' && HOSTNAME_REG.test(value); -export const isIp4 = (value: unknown): boolean => typeof value === 'string' && IP4_REG.test(value); -export const isIp6 = (value: unknown): boolean => typeof value === 'string' && IP6_REG.test(value); -export const isUuid = (value: unknown): boolean => typeof value === 'string' && UUID_REG.test(value); -export const isUri = (value: unknown): boolean => - typeof value === 'string' && NOT_URI_FRAGMENT_REG.test(value) && URI_REG.test(value); -export const isDuration = (value: unknown): boolean => typeof value === 'string' && DURATION_REG.test(value); - -const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; -const isLeapYear = (year: number): boolean => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); - -export const isDate = (value: unknown): boolean => { - if (typeof value !== 'string') return false; - const matches: string[] | null = DATE_REG.exec(value); - if (!matches) return false; - const year: number = +matches[1]; - const month: number = +matches[2]; - const day: number = +matches[3]; - return month >= 1 && month <= 12 && day >= 1 && day <= (month === 2 && isLeapYear(year) ? 29 : DAYS[month]); -}; - -export const isTime = (value: unknown): boolean => { - if (typeof value !== 'string') return false; - const matches: string[] | null = TIME_REG.exec(value); - if (!matches) return false; - const hr: number = +matches[1]; - const min: number = +matches[2]; - const sec: number = +matches[3]; - const tz: string | undefined = matches[4]; - const tzSign: number = matches[5] === '-' ? -1 : 1; - const tzH: number = +(matches[6] || 0); - const tzM: number = +(matches[7] || 0); - if (tzH > 23 || tzM > 59 || !tz) return false; - if (hr <= 23 && min <= 59 && sec < 60) return true; - const utcMin = min - tzM * tzSign; - const utcHr = hr - tzH * tzSign - (utcMin < 0 ? 1 : 0); - return (utcHr === 23 || utcHr === -1) && (utcMin === 59 || utcMin === -1) && sec < 61; -}; - -export const isDateTime = (str: unknown): boolean => { - if (typeof str !== 'string') return false; - const dateTime = str.split(DATE_TIME_SEPARATOR_REG) as [string, string]; - return dateTime.length === 2 && isDate(dateTime[0]) && isTime(dateTime[1]); -}; - -// ---------------------------------------------------- Binary operator helpers - -export const u8 = (bin: unknown, pos: unknown) => { - const buf = asBin(bin); - const index = int(pos); - if (index < 0 || index >= buf.length) throw new Error('OUT_OF_BOUNDS'); - return buf[index]; -}; - -// ----------------------------------------------------- Array operator helpers - -export const asArr = (value: unknown): unknown[] => { - if (value instanceof Array) return value as unknown[]; - throw new Error('NOT_ARRAY'); -}; - -export const head = (operand1: unknown, operand2: unknown): unknown => { - const arr = asArr(operand1); - const count = int(operand2); - return count >= 0 ? arr.slice(0, count) : arr.slice(count); -}; - -export const concat = (arrays: unknown[]): unknown[] => { - const result: unknown[] = []; - for (const array of arrays) { - asArr(array); - for (const item of array as unknown[]) result.push(item); - } - return result; -}; - -export const isInArr = (arr: unknown, what: unknown): boolean => { - const arr2 = asArr(arr); - const length = arr2.length; - for (let i = 0; i < length; i++) if (deepEqual(arr2[i], what)) return true; - return false; -}; - -export const isInArr2 = (arr: unknown, check: (item: unknown) => boolean): boolean => { - const arr2 = asArr(arr); - const length = arr2.length; - for (let i = 0; i < length; i++) if (check(arr2[i])) return true; - return false; -}; - -export const fromEntries = (maybeEntries: unknown): Record => { - const entries = asArr(maybeEntries); - const result: Record = {}; - for (const maybeEntry of entries) { - const entry = asArr(maybeEntry); - const [key, value] = asArr(entry); - if (entry.length !== 2) throw new Error('NOT_PAIR'); - result[str(key)] = value; - } - return result; -}; - -export const indexOf = (container: unknown, item: unknown): -1 | number => { - const arr = asArr(container); - const length = arr.length; - for (let i = 0; i < length; i++) if (deepEqual(arr[i], item)) return i; - return -1; -}; - -export const indexOf2 = (container: unknown, check: (item: unknown) => boolean): -1 | number => { - const arr = asArr(container); - const length = arr.length; - for (let i = 0; i < length; i++) if (check(arr[i])) return i; - return -1; -}; - -export const zip = (maybeArr1: unknown, maybeArr2: unknown): [unknown, unknown][] => { - const arr1 = asArr(maybeArr1); - const arr2 = asArr(maybeArr2); - const length = Math.min(arr1.length, arr2.length); - const result: [unknown, unknown][] = []; - for (let i = 0; i < length; i++) result.push([arr1[i], arr2[i]]); - return result; -}; - -export const filter = (arr: unknown[], varname: string, vars: Vars, run: () => unknown): unknown => { - const result = arr.filter((item) => { - vars.set(varname, item); - return run(); - }); - vars.del(varname); - return result; -}; - -export const map = (arr: unknown[], varname: string, vars: Vars, run: () => unknown): unknown => { - const result = arr.map((item) => { - vars.set(varname, item); - return run(); - }); - vars.del(varname); - return result; -}; - -export const reduce = ( - arr: unknown[], - initialValue: unknown, - accname: string, - varname: string, - vars: Vars, - run: () => unknown, -): unknown => { - vars.set(accname, initialValue); - arr.forEach((item) => { - vars.set(varname, item); - const res = run(); - vars.set(accname, res); - }); - const result = vars.get(accname); - vars.del(accname); - vars.del(varname); - return result; -}; - -// ---------------------------------------------------- Object operator helpers - -export const asObj = (value: unknown): object => { - if (type(value) === 'object') return value as object; - throw new Error('NOT_OBJECT'); -}; - -export const keys = (value: unknown): string[] => Object.keys(asObj(value)); - -export const values = (value: unknown): unknown[] => { - const values: unknown[] = []; - const theKeys = keys(value); - const length = theKeys.length; - for (let i = 0; i < length; i++) values.push((value as any)[theKeys[i]]); - return values; -}; - -export const entries = (value: unknown): [key: string, value: unknown][] => { - const entries: [key: string, value: unknown][] = []; - const theKeys = keys(value); - const length = theKeys.length; - for (let i = 0; i < length; i++) { - const key = theKeys[i]; - entries.push([key, (value as any)[key]]); - } - return entries; -}; - -export const objSetRaw = (obj: Record, key: string, value: unknown): Record => { - const prop = str(key); - if (prop === '__proto__') throw new Error('PROTO_KEY'); - obj[prop] = value; - return obj; -}; - -export const objDelRaw = (obj: Record, key: string): Record => { - delete obj[key]; - return obj; -}; - -// -------------------------------------------------------------------- Various - -export const isLiteral = (value: unknown): boolean => { - if (value instanceof Array) return value.length === 1; - else return true; -}; - -export const asLiteral = (value: Literal): T => { - if (value instanceof Array) { - if (value.length !== 1) throw new Error('Invalid literal.'); - return value[0]; - } else return value; -}; - -export const literal = (value: T): T | [T] => (value instanceof Array ? [value] : value); - -export const assertFixedArity = (operator: string, arity: number, expr: Expression): void => { - if (expr.length !== arity + 1) throw new Error(`"${operator}" operator expects ${arity} operands.`); -}; - -export const assertVariadicArity = (operator: string, expr: Expression): void => { - if (expr.length < 3) throw new Error(`"${operator}" operator expects at least two operands.`); -}; - -export const assertArity = (operator: string, arity: number | [min: number, max: number], expr: Expression): void => { - if (!arity) return; - if (arity instanceof Array) { - const [min, max] = arity; - if (expr.length < min + 1) throw new Error(`"${operator}" operator expects at least ${min} operands.`); - if (max !== -1 && expr.length > max + 1) throw new Error(`"${operator}" operator expects at most ${max} operands.`); - } else if (arity !== -1) assertFixedArity(operator, arity, expr); - else assertVariadicArity(operator, expr); -}; - -export const operatorsToMap = (operators: OperatorDefinition[]): OperatorMap => { - const map: OperatorMap = new Map(); - for (const operator of operators) { - const [name, aliases] = operator; - map.set(name, operator); - for (const alias of aliases) map.set(alias, operator); - } - return map; -}; - -export const parseVar = (name: string): [name: string, pointer: string] => { - if (name[0] === '/') return ['', name]; - const slashIndex = name.indexOf('/'); - if (slashIndex === -1) return [name, '']; - return [name.slice(0, slashIndex), name.slice(slashIndex)]; -}; diff --git a/src/json-ot/types/ot-json/apply.ts b/src/json-ot/types/ot-json/apply.ts index f2ac96ed3a..fe382e3076 100644 --- a/src/json-ot/types/ot-json/apply.ts +++ b/src/json-ot/types/ot-json/apply.ts @@ -1,11 +1,11 @@ import {find, isArrayReference, isObjectReference} from '@jsonjoy.com/json-pointer'; import {JsonOp} from './types'; -import {evaluate as evalExpression} from '../../../json-expression/evaluate'; +import {evaluate as evalExpression} from '@jsonjoy.com/json-expression/lib/evaluate'; import {comparePath} from './util'; import {EDIT_TYPE} from './constants'; import {apply as applyStr} from '../ot-string-irreversible/apply'; import {apply as applyBin} from '../ot-binary-irreversible/apply'; -import {Vars} from '../../../json-expression/Vars'; +import {Vars} from '@jsonjoy.com/json-expression/lib/Vars'; export const apply = (doc: unknown, op: JsonOp): unknown => { const [test, pick = [], data = [], drop = [], edit = []] = op; diff --git a/src/json-ot/types/ot-json/json-patch.ts b/src/json-ot/types/ot-json/json-patch.ts index 11f1016704..a30562fc1e 100644 --- a/src/json-ot/types/ot-json/json-patch.ts +++ b/src/json-ot/types/ot-json/json-patch.ts @@ -1,8 +1,8 @@ -import {literal} from '../../../json-expression/util'; +import {literal} from '@jsonjoy.com/json-expression/lib/util'; import {OpTree} from './tree'; import {toPath} from '@jsonjoy.com/json-pointer/lib/util'; import type {Operation} from '../../../json-patch'; -import type {Expr} from '../../../json-expression'; +import type {Expr} from '@jsonjoy.com/json-expression'; import type {JsonOp} from './types'; export const jsonPatchOpToJsonOp = (operation: Operation): JsonOp => { diff --git a/src/json-ot/types/ot-json/tree.ts b/src/json-ot/types/ot-json/tree.ts index 617be5cfb4..0ebffaaa75 100644 --- a/src/json-ot/types/ot-json/tree.ts +++ b/src/json-ot/types/ot-json/tree.ts @@ -1,4 +1,4 @@ -import type {Expr} from '../../../json-expression'; +import type {Expr} from '@jsonjoy.com/json-expression'; import type {Path} from '@jsonjoy.com/json-pointer'; import type {JsonOp, JsonOpDataComponent, JsonOpDropComponent, JsonOpPickComponent} from './types'; diff --git a/src/json-ot/types/ot-json/types.ts b/src/json-ot/types/ot-json/types.ts index c8f67f8d1d..16c4056e56 100644 --- a/src/json-ot/types/ot-json/types.ts +++ b/src/json-ot/types/ot-json/types.ts @@ -1,6 +1,6 @@ import type {BinaryOp} from '../ot-binary-irreversible/types'; import type {EDIT_TYPE} from './constants'; -import type {Expr} from '../../../json-expression'; +import type {Expr} from '@jsonjoy.com/json-expression'; import type {Path} from '@jsonjoy.com/json-pointer'; import type {StringOp} from '../ot-string-irreversible/types'; diff --git a/src/json-type/schema/schema.ts b/src/json-type/schema/schema.ts index 411e8ce198..216e46bab6 100644 --- a/src/json-type/schema/schema.ts +++ b/src/json-type/schema/schema.ts @@ -1,7 +1,7 @@ import type {Observable} from 'rxjs'; import type {Mutable} from '@jsonjoy.com/util/lib/types'; import type {Display, Identifiable} from './common'; -import type {Expr} from '../../json-expression'; +import type {Expr} from '@jsonjoy.com/json-expression'; export interface TType extends Display, Partial { /** diff --git a/src/json-type/type/classes/OrType.ts b/src/json-type/type/classes/OrType.ts index 315f55a9e5..574c75fd57 100644 --- a/src/json-type/type/classes/OrType.ts +++ b/src/json-type/type/classes/OrType.ts @@ -12,9 +12,9 @@ import {JsExpression} from '@jsonjoy.com/util/lib/codegen/util/JsExpression'; import {MessagePackEncoderCodegenContext} from '../../codegen/binary/MessagePackEncoderCodegenContext'; import {BinaryJsonEncoder} from '@jsonjoy.com/json-pack/lib/types'; import {CapacityEstimatorCodegenContext} from '../../codegen/capacity/CapacityEstimatorCodegenContext'; -import {JsonExpressionCodegen} from '../../../json-expression'; -import {operatorsMap} from '../../../json-expression/operators'; -import {Vars} from '../../../json-expression/Vars'; +import {JsonExpressionCodegen} from '@jsonjoy.com/json-expression'; +import {operatorsMap} from '@jsonjoy.com/json-expression/lib/operators'; +import {Vars} from '@jsonjoy.com/json-expression/lib/Vars'; import {Discriminator} from '../discriminator'; import {AbstractType} from './AbstractType'; import type * as jsonSchema from '../../../json-schema'; diff --git a/src/json-type/type/discriminator.ts b/src/json-type/type/discriminator.ts index d1da9868a3..a489105236 100644 --- a/src/json-type/type/discriminator.ts +++ b/src/json-type/type/discriminator.ts @@ -1,4 +1,4 @@ -import {Expr} from '../../json-expression'; +import {Expr} from '@jsonjoy.com/json-expression'; import {BooleanType, ConstType, NumberType, ObjectFieldType, ObjectType, StringType, TupleType} from './classes'; import {Type} from './types'; diff --git a/yarn.lock b/yarn.lock index f1c53b7f47..678236ea2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -561,6 +561,14 @@ resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== +"@jsonjoy.com/json-expression@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-expression/-/json-expression-1.0.0.tgz#f63e5349106071180837e6357e69fcc4375f11a3" + integrity sha512-m7rvIuKdN+c52087TQjOxMR/WpuvFeWP5fdg1Q944Hh1D9mokUnLCFlJBTNi5z69Ilyv9JmRRF9jOqJpKbawEw== + dependencies: + "@jsonjoy.com/json-pointer" "^1.0.0" + "@jsonjoy.com/util" "^1.3.0" + "@jsonjoy.com/json-pack@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.0.4.tgz#ab59c642a2e5368e8bcfd815d817143d4f3035d0"