diff --git a/.github/actions/cached-node-modules/action.yml b/.github/actions/cached-node-modules/action.yml index 38d6e1e35c..bddc11a9dd 100644 --- a/.github/actions/cached-node-modules/action.yml +++ b/.github/actions/cached-node-modules/action.yml @@ -45,5 +45,6 @@ runs: npm run build -w packages/parameters & \ npm run build -w packages/idempotency & \ npm run build -w packages/batch & \ - npm run build -w packages/testing + npm run build -w packages/testing & \ + npm run build -w packages/jmespath shell: bash \ No newline at end of file diff --git a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml index fd662fa7c9..fa4e7bd9b2 100644 --- a/.github/workflows/reusable-run-linting-check-and-unit-tests.yml +++ b/.github/workflows/reusable-run-linting-check-and-unit-tests.yml @@ -28,9 +28,9 @@ jobs: with: nodeVersion: ${{ matrix.version }} - name: Run linting - run: npm run lint -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch + run: npm run lint -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch -w packages/jmespath - name: Run unit tests - run: npm t -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch + run: npm t -w packages/commons -w packages/logger -w packages/tracer -w packages/metrics -w packages/parameters -w packages/idempotency -w packages/batch -w packages/jmespath check-examples: runs-on: ubuntu-latest env: diff --git a/packages/jmespath/src/index.ts b/packages/jmespath/src/index.ts new file mode 100644 index 0000000000..a0f2545f85 --- /dev/null +++ b/packages/jmespath/src/index.ts @@ -0,0 +1,12 @@ +export { search } from './search.js'; +export { + JMESPathError, + LexerError, + ParseError, + IncompleteExpressionError, + ArityError, + VariadicArityError, + JMESPathTypeError, + EmptyExpressionError, + UnknownFunctionError, +} from './errors.js'; diff --git a/packages/jmespath/tests/unit/compliance/base.test.ts b/packages/jmespath/tests/unit/compliance/base.test.ts new file mode 100644 index 0000000000..9626938593 --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/base.test.ts @@ -0,0 +1,137 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/base + */ +import { search } from '../../../src/index.js'; + +describe('Base tests', () => { + it.each([ + { + expression: 'foo', + expected: { bar: { baz: 'correct' } }, + }, + { + expression: 'foo.bar', + expected: { baz: 'correct' }, + }, + { + expression: 'foo.bar.baz', + expected: 'correct', + }, + { + expression: 'foo\n.\nbar\n.baz', + expected: 'correct', + }, + { + expression: 'foo.bar.baz.bad', + expected: null, + }, + { + expression: 'foo.bar.bad', + expected: null, + }, + { + expression: 'foo.bad', + expected: null, + }, + { + expression: 'bad', + expected: null, + }, + { + expression: 'bad.morebad.morebad', + expected: null, + }, + ])( + 'should parse a multi-level nested object: $expression', + ({ expression, expected }) => { + // Prepare + const data = { foo: { bar: { baz: 'correct' } } }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo', + expected: { bar: ['one', 'two', 'three'] }, + }, + { + expression: 'foo.bar', + expected: ['one', 'two', 'three'], + }, + ])( + 'should parse multi-level objects with arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { foo: { bar: ['one', 'two', 'three'] } }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'one', + expected: null, + }, + { + expression: 'two', + expected: null, + }, + { + expression: 'three', + expected: null, + }, + { + expression: 'one.two', + expected: null, + }, + ])('should parse an array: $expression', ({ expression, expected }) => { + // Prepare + const data = ['one', 'two', 'three']; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'foo."1"', + expected: ['one', 'two', 'three'], + }, + { + expression: 'foo."1"[0]', + expected: 'one', + }, + { + expression: 'foo."-1"', + expected: 'bar', + }, + ])( + 'should parse an object with arrays and numeric values as keys: $expression', + ({ expression, expected }) => { + // Prepare + const data = { foo: { '1': ['one', 'two', 'three'], '-1': 'bar' } }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/compliance/boolean.test.ts b/packages/jmespath/tests/unit/compliance/boolean.test.ts new file mode 100644 index 0000000000..0cc90f80a7 --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/boolean.test.ts @@ -0,0 +1,304 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/boolean + */ +import { search } from '../../../src/index.js'; + +describe('Boolean tests', () => { + it.each([ + { + expression: 'outer.foo || outer.bar', + expected: 'foo', + }, + { + expression: 'outer.foo||outer.bar', + expected: 'foo', + }, + { + expression: 'outer.bar || outer.baz', + expected: 'bar', + }, + { + expression: 'outer.bar||outer.baz', + expected: 'bar', + }, + { + expression: 'outer.bad || outer.foo', + expected: 'foo', + }, + { + expression: 'outer.bad||outer.foo', + expected: 'foo', + }, + { + expression: 'outer.foo || outer.bad', + expected: 'foo', + }, + { + expression: 'outer.foo||outer.bad', + expected: 'foo', + }, + { + expression: 'outer.bad || outer.alsobad', + expected: null, + }, + { + expression: 'outer.bad||outer.alsobad', + expected: null, + }, + ])( + 'should support boolean OR comparison: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + outer: { + foo: 'foo', + bar: 'bar', + baz: 'baz', + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'outer.empty_string || outer.foo', + expected: 'foo', + }, + { + expression: + 'outer.nokey || outer.bool || outer.empty_list || outer.empty_string || outer.foo', + expected: 'foo', + }, + ])( + 'should support multiple boolean OR comparisons: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + outer: { + foo: 'foo', + bool: false, + empty_list: [], + empty_string: '', + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'True && False', + expected: false, + }, + { + expression: 'False && True', + expected: false, + }, + { + expression: 'True && True', + expected: true, + }, + { + expression: 'False && False', + expected: false, + }, + { + expression: 'True && Number', + expected: 5, + }, + { + expression: 'Number && True', + expected: true, + }, + { + expression: 'Number && False', + expected: false, + }, + { + expression: 'Number && EmptyList', + expected: [], + }, + { + expression: 'Number && True', + expected: true, + }, + { + expression: 'EmptyList && True', + expected: [], + }, + { + expression: 'EmptyList && False', + expected: [], + }, + { + expression: 'True || False', + expected: true, + }, + { + expression: 'True || True', + expected: true, + }, + { + expression: 'False || True', + expected: true, + }, + { + expression: 'False || False', + expected: false, + }, + { + expression: 'Number || EmptyList', + expected: 5, + }, + { + expression: 'Number || True', + expected: 5, + }, + { + expression: 'Number || True && False', + expected: 5, + }, + { + expression: '(Number || True) && False', + expected: false, + }, + { + expression: 'Number || (True && False)', + expected: 5, + }, + { + expression: '!True', + expected: false, + }, + { + expression: '!False', + expected: true, + }, + { + expression: '!Number', + expected: false, + }, + { + expression: '!EmptyList', + expected: true, + }, + { + expression: 'True && !False', + expected: true, + }, + { + expression: 'True && !EmptyList', + expected: true, + }, + { + expression: '!False && !EmptyList', + expected: true, + }, + { + expression: '!(True && False)', + expected: true, + }, + { + expression: '!Zero', + expected: false, + }, + { + expression: '!!Zero', + expected: true, + }, + ])( + 'should support boolean AND comparison: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + True: true, + False: false, + Number: 5, + EmptyList: [], + Zero: 0, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'one < two', + expected: true, + }, + { + expression: 'one <= two', + expected: true, + }, + { + expression: 'one == one', + expected: true, + }, + { + expression: 'one == two', + expected: false, + }, + { + expression: 'one > two', + expected: false, + }, + { + expression: 'one >= two', + expected: false, + }, + { + expression: 'one != two', + expected: true, + }, + { + expression: 'one < two && three > one', + expected: true, + }, + { + expression: 'one < two || three > one', + expected: true, + }, + { + expression: 'one < two || three < one', + expected: true, + }, + { + expression: 'two < one || three < one', + expected: false, + }, + ])( + 'should support lesser and equal comparison: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + one: 1, + two: 2, + three: 3, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/compliance/current.test.ts b/packages/jmespath/tests/unit/compliance/current.test.ts new file mode 100644 index 0000000000..58bf29bec3 --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/current.test.ts @@ -0,0 +1,41 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/current + */ +import { search } from '../../../src/index.js'; + +describe('Current operator tests', () => { + it.each([ + { + expression: '@', + expected: { + foo: [{ name: 'a' }, { name: 'b' }], + bar: { baz: 'qux' }, + }, + }, + { + expression: '@.bar', + expected: { baz: 'qux' }, + }, + { + expression: '@.foo[0]', + expected: { name: 'a' }, + }, + ])( + 'should support the current operator: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [{ name: 'a' }, { name: 'b' }], + bar: { baz: 'qux' }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/compliance/escape.test.ts b/packages/jmespath/tests/unit/compliance/escape.test.ts new file mode 100644 index 0000000000..0bfa2345fd --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/escape.test.ts @@ -0,0 +1,64 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/escape + */ +import { search } from '../../../src/index.js'; + +describe('Escape characters tests', () => { + it.each([ + { + expression: '"foo.bar"', + expected: 'dot', + }, + { + expression: '"foo bar"', + expected: 'space', + }, + { + expression: '"foo\\nbar"', + expected: 'newline', + }, + { + expression: '"foo\\"bar"', + expected: 'doublequote', + }, + { + expression: '"c:\\\\\\\\windows\\\\path"', + expected: 'windows', + }, + { + expression: '"/unix/path"', + expected: 'unix', + }, + { + expression: '"\\"\\"\\""', + expected: 'threequotes', + }, + { + expression: '"bar"."baz"', + expected: 'qux', + }, + ])( + 'should support escaping characters: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + 'foo.bar': 'dot', + 'foo bar': 'space', + 'foo\nbar': 'newline', + 'foo"bar': 'doublequote', + 'c:\\\\windows\\path': 'windows', + '/unix/path': 'unix', + '"""': 'threequotes', + bar: { baz: 'qux' }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/compliance/filters.test.ts b/packages/jmespath/tests/unit/compliance/filters.test.ts new file mode 100644 index 0000000000..3f337208de --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/filters.test.ts @@ -0,0 +1,927 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/filters + */ +import { search } from '../../../src/index.js'; + +describe('Filer operator tests', () => { + it.each([ + { + comment: 'Matching a literal', + expression: `foo[?name == 'a']`, + expected: [{ name: 'a' }], + }, + ])('should match a literal: $expression', ({ expression, expected }) => { + // Prepare + const data = { foo: [{ name: 'a' }, { name: 'b' }] }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: '*[?[0] == `0`]', + expected: [[], []], + }, + ])( + 'should match a literal in arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { foo: [0, 1], bar: [2, 3] }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[?first == last]', + expected: [{ first: 'foo', last: 'foo' }], + }, + { + comment: 'Verify projection created from filter', + expression: 'foo[?first == last].first', + expected: ['foo'], + }, + ])('should match an expression: $expression', ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { first: 'foo', last: 'bar' }, + { first: 'foo', last: 'foo' }, + { first: 'foo', last: 'baz' }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + comment: 'Greater than with a number', + expression: 'foo[?age > `25`]', + expected: [{ age: 30 }], + }, + { + expression: 'foo[?age >= `25`]', + expected: [{ age: 25 }, { age: 30 }], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?age > `30`]', + expected: [], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?age < `25`]', + expected: [{ age: 20 }], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?age <= `25`]', + expected: [{ age: 20 }, { age: 25 }], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?age < `20`]', + expected: [], + }, + { + expression: 'foo[?age == `20`]', + expected: [{ age: 20 }], + }, + { + expression: 'foo[?age != `20`]', + expected: [{ age: 25 }, { age: 30 }], + }, + ])( + 'should match an expression with operators: $expression', + ({ expression, expected }) => { + // Prepare + const data = { foo: [{ age: 20 }, { age: 25 }, { age: 30 }] }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Greater than with a number', + expression: 'foo[?weight > `44.4`]', + expected: [{ weight: 55.5 }], + }, + { + expression: 'foo[?weight >= `44.4`]', + expected: [{ weight: 44.4 }, { weight: 55.5 }], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?weight > `55.5`]', + expected: [], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?weight < `44.4`]', + expected: [{ weight: 33.3 }], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?weight <= `44.4`]', + expected: [{ weight: 33.3 }, { weight: 44.4 }], + }, + { + comment: 'Greater than with a number', + expression: 'foo[?weight < `33.3`]', + expected: [], + }, + { + expression: 'foo[?weight == `33.3`]', + expected: [{ weight: 33.3 }], + }, + { + expression: 'foo[?weight != `33.3`]', + expected: [{ weight: 44.4 }, { weight: 55.5 }], + }, + ])( + 'should match an expression with comparisons: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [{ weight: 33.3 }, { weight: 44.4 }, { weight: 55.5 }], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: `foo[?top.name == 'a']`, + expected: [{ top: { name: 'a' } }], + }, + ])( + 'should match with subexpression: $expression', + ({ expression, expected }) => { + // Prepare + const data = { foo: [{ top: { name: 'a' } }, { top: { name: 'b' } }] }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Matching an expression', + expression: 'foo[?top.first == top.last]', + expected: [{ top: { first: 'foo', last: 'foo' } }], + }, + { + comment: 'Matching a JSON array', + expression: 'foo[?top == `{"first": "foo", "last": "bar"}`]', + expected: [{ top: { first: 'foo', last: 'bar' } }], + }, + ])('should match with arrays: $expression', ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { top: { first: 'foo', last: 'bar' } }, + { top: { first: 'foo', last: 'foo' } }, + { top: { first: 'foo', last: 'baz' } }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'foo[?key == `true`]', + expected: [{ key: true }], + }, + { + expression: 'foo[?key == `false`]', + expected: [{ key: false }], + }, + { + expression: 'foo[?key == `0`]', + expected: [{ key: 0 }], + }, + { + expression: 'foo[?key == `1`]', + expected: [{ key: 1 }], + }, + { + expression: 'foo[?key == `[0]`]', + expected: [{ key: [0] }], + }, + { + expression: 'foo[?key == `{"bar": [0]}`]', + expected: [{ key: { bar: [0] } }], + }, + { + expression: 'foo[?key == `null`]', + expected: [{ key: null }], + }, + { + expression: 'foo[?key == `[1]`]', + expected: [{ key: [1] }], + }, + { + expression: 'foo[?key == `{"a":2}`]', + expected: [{ key: { a: 2 } }], + }, + { + expression: 'foo[?`true` == key]', + expected: [{ key: true }], + }, + { + expression: 'foo[?`false` == key]', + expected: [{ key: false }], + }, + { + expression: 'foo[?`0` == key]', + expected: [{ key: 0 }], + }, + { + expression: 'foo[?`1` == key]', + expected: [{ key: 1 }], + }, + { + expression: 'foo[?`[0]` == key]', + expected: [{ key: [0] }], + }, + { + expression: 'foo[?`{"bar": [0]}` == key]', + expected: [{ key: { bar: [0] } }], + }, + { + expression: 'foo[?`null` == key]', + expected: [{ key: null }], + }, + { + expression: 'foo[?`[1]` == key]', + expected: [{ key: [1] }], + }, + { + expression: 'foo[?`{"a":2}` == key]', + expected: [{ key: { a: 2 } }], + }, + { + expression: 'foo[?key != `true`]', + expected: [ + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?key != `false`]', + expected: [ + { key: true }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?key != `0`]', + expected: [ + { key: true }, + { key: false }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?key != `1`]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?key != `null`]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?key != `[1]`]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?key != `{"a":2}`]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + ], + }, + { + expression: 'foo[?`true` != key]', + expected: [ + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?`false` != key]', + expected: [ + { key: true }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?`0` != key]', + expected: [ + { key: true }, + { key: false }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?`1` != key]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?`null` != key]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?`[1]` != key]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[?`{"a":2}` != key]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + ], + }, + ])( + 'should match with object that have mixed types as values: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 1 }, + { key: [0] }, + { key: { bar: [0] } }, + { key: null }, + { key: [1] }, + { key: { a: 2 } }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[?key == `true`]', + expected: [{ key: true }], + }, + { + expression: 'foo[?key == `false`]', + expected: [{ key: false }], + }, + { + expression: 'foo[?key]', + expected: [ + { key: true }, + { key: 0 }, + { key: 0.0 }, + { key: 1 }, + { key: 1.0 }, + { key: [0] }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[? !key]', + expected: [{ key: false }, { key: null }, { key: [] }, { key: {} }], + }, + { + expression: 'foo[? !!key]', + expected: [ + { key: true }, + { key: 0 }, + { key: 0.0 }, + { key: 1 }, + { key: 1.0 }, + { key: [0] }, + { key: [1] }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[? `true`]', + expected: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 0.0 }, + { key: 1 }, + { key: 1.0 }, + { key: [0] }, + { key: null }, + { key: [1] }, + { key: [] }, + { key: {} }, + { key: { a: 2 } }, + ], + }, + { + expression: 'foo[? `false`]', + expected: [], + }, + ])( + 'should match with falsy values: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { key: true }, + { key: false }, + { key: 0 }, + { key: 0.0 }, + { key: 1 }, + { key: 1.0 }, + { key: [0] }, + { key: null }, + { key: [1] }, + { key: [] }, + { key: {} }, + { key: { a: 2 } }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'reservations[].instances[?bar==`1`]', + expected: [[{ foo: 2, bar: 1 }]], + }, + { + expression: 'reservations[*].instances[?bar==`1`]', + expected: [[{ foo: 2, bar: 1 }]], + }, + { + expression: 'reservations[].instances[?bar==`1`][]', + expected: [{ foo: 2, bar: 1 }], + }, + ])( + 'should match with nested objects and arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + reservations: [ + { + instances: [ + { foo: 1, bar: 2 }, + { foo: 1, bar: 3 }, + { foo: 1, bar: 2 }, + { foo: 2, bar: 1 }, + ], + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[?bar==`1`].bar[0]', + expected: [], + }, + ])( + 'should match with nested objects and arrays with different structures: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + baz: 'other', + foo: [ + { bar: 1 }, + { bar: 2 }, + { bar: 3 }, + { bar: 4 }, + { bar: 1, baz: 2 }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[?a==`1`].b.c', + expected: ['x', 'y', 'z'], + }, + ])( + 'should support filter in indexes: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { a: 1, b: { c: 'x' } }, + { a: 1, b: { c: 'y' } }, + { a: 1, b: { c: 'z' } }, + { a: 2, b: { c: 'z' } }, + { a: 1, baz: 2 }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Filter with or expression', + expression: `foo[?name == 'a' || name == 'b']`, + expected: [{ name: 'a' }, { name: 'b' }], + }, + { + expression: `foo[?name == 'a' || name == 'e']`, + expected: [{ name: 'a' }], + }, + { + expression: `foo[?name == 'a' || name == 'b' || name == 'c']`, + expected: [{ name: 'a' }, { name: 'b' }, { name: 'c' }], + }, + ])( + 'should support filter with or expressions: $expression', + ({ expression, expected }) => { + // Prepare + const data = { foo: [{ name: 'a' }, { name: 'b' }, { name: 'c' }] }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Filter with and expression', + expression: 'foo[?a == `1` && b == `2`]', + expected: [{ a: 1, b: 2 }], + }, + { + expression: 'foo[?a == `1` && b == `4`]', + expected: [], + }, + ])( + 'should support filter and expressions: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { a: 1, b: 2 }, + { a: 1, b: 3 }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Filter with Or and And expressions', + expression: 'foo[?c == `3` || a == `1` && b == `4`]', + expected: [{ a: 1, b: 2, c: 3 }], + }, + { + expression: 'foo[?b == `2` || a == `3` && b == `4`]', + expected: [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 4 }, + ], + }, + { + expression: 'foo[?a == `3` && b == `4` || b == `2`]', + expected: [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 4 }, + ], + }, + { + expression: 'foo[?(a == `3` && b == `4`) || b == `2`]', + expected: [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 4 }, + ], + }, + { + expression: 'foo[?((a == `3` && b == `4`)) || b == `2`]', + expected: [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 4 }, + ], + }, + { + expression: 'foo[?a == `3` && (b == `4` || b == `2`)]', + expected: [{ a: 3, b: 4 }], + }, + { + expression: 'foo[?a == `3` && ((b == `4` || b == `2`))]', + expected: [{ a: 3, b: 4 }], + }, + ])( + 'should support filter with or & and expressions', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 4 }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Verify precedence of or/and expressions', + expression: 'foo[?a == `1` || b ==`2` && c == `5`]', + expected: [{ a: 1, b: 2, c: 3 }], + }, + { + comment: 'Parentheses can alter precedence', + expression: 'foo[?(a == `1` || b ==`2`) && c == `5`]', + expected: [], + }, + { + comment: 'Not expressions combined with and/or', + expression: 'foo[?!(a == `1` || b ==`2`)]', + expected: [{ a: 3, b: 4 }], + }, + ])( + 'should support filter with expressions and respect precedence: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { a: 1, b: 2, c: 3 }, + { a: 3, b: 4 }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Unary filter expression', + expression: 'foo[?key]', + expected: [ + { key: true }, + { key: [0] }, + { key: { a: 'b' } }, + { key: 0 }, + { key: 1 }, + ], + }, + { + comment: 'Unary not filter expression', + expression: 'foo[?!key]', + expected: [ + { key: false }, + { key: [] }, + { key: {} }, + { key: null }, + { notkey: true }, + ], + }, + { + comment: 'Equality with null RHS', + expression: 'foo[?key == `null`]', + expected: [{ key: null }, { notkey: true }], + }, + ])( + 'should support unary expressions: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { key: true }, + { key: false }, + { key: [] }, + { key: {} }, + { key: [0] }, + { key: { a: 'b' } }, + { key: 0 }, + { key: 1 }, + { key: null }, + { notkey: true }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Using @ in a filter expression', + expression: 'foo[?@ < `5`]', + expected: [0, 1, 2, 3, 4], + }, + { + comment: 'Using @ in a filter expression', + expression: 'foo[?`5` > @]', + expected: [0, 1, 2, 3, 4], + }, + { + comment: 'Using @ in a filter expression', + expression: 'foo[?@ == @]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + ])( + 'should support using current in a filter: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/compliance/functions.test.ts b/packages/jmespath/tests/unit/compliance/functions.test.ts new file mode 100644 index 0000000000..df4f311e61 --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/functions.test.ts @@ -0,0 +1,2415 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/functions + */ +import { search } from '../../../src/index.js'; + +describe('Functions tests', () => { + it.each([ + { + expression: 'abs(foo)', + expected: 1, + }, + { + expression: 'abs(foo)', + expected: 1, + }, + { + expression: 'abs(array[1])', + expected: 3, + }, + { + expression: 'abs(array[1])', + expected: 3, + }, + { + expression: 'abs(`-24`)', + expected: 24, + }, + { + expression: 'abs(`-24`)', + expected: 24, + }, + ])( + 'should support the abs() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'abs(str)', + error: + 'Invalid argument type for function abs(), expected "number" but found "string" in expression: abs(str)', + }, + { + expression: 'abs(`false`)', + error: + 'Invalid argument type for function abs(), expected "number" but found "boolean" in expression: abs(`false`)', + }, + { + expression: 'abs(`1`, `2`)', + error: + 'Expected at most 1 argument for function abs(), received 2 in expression: abs(`1`, `2`)', + }, + { + expression: 'abs()', + error: + 'Expected at least 1 argument for function abs(), received 0 in expression: abs()', + }, + ])('abs() function errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'unknown_function(`1`, `2`)', + error: + 'Unknown function: unknown_function() in expression: unknown_function(`1`, `2`)', + }, + ])('unknown function errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'avg(numbers)', + expected: 2.75, + }, + ])( + 'should support the avg() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'avg(array)', + error: + 'Invalid argument type for function avg(), expected "number" but found "string" in expression: avg(array)', + }, + { + expression: `avg('abc')`, + error: `Invalid argument type for function avg(), expected "array-number" but found "string" in expression: avg('abc')`, + }, + { + expression: 'avg(foo)', + error: + 'Invalid argument type for function avg(), expected "array-number" but found "number" in expression: avg(foo)', + }, + { + expression: 'avg(@)', + error: + 'Invalid argument type for function avg(), expected "array-number" but found "object" in expression: avg(@)', + }, + { + expression: 'avg(strings)', + error: + 'Invalid argument type for function avg(), expected "number" but found "string" in expression: avg(strings)', + }, + ])('avg() function errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'ceil(`1.2`)', + expected: 2, + }, + { + expression: 'ceil(decimals[0])', + expected: 2, + }, + { + expression: 'ceil(decimals[1])', + expected: 2, + }, + { + expression: 'ceil(decimals[2])', + expected: -1, + }, + ])( + 'should support the ceil() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: `ceil('string')`, + error: `Invalid argument type for function ceil(), expected "number" but found "string" in expression: ceil('string')`, + }, + ])('ceil() function errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: `contains('abc', 'a')`, + expected: true, + }, + { + expression: `contains('abc', 'd')`, + expected: false, + }, + { + // prettier-ignore + expression: 'contains(strings, \'a\')', + expected: true, + }, + { + expression: 'contains(decimals, `1.2`)', + expected: true, + }, + { + expression: 'contains(decimals, `false`)', + expected: false, + }, + ])( + 'should support the contains() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'contains(`false`, "d")', + error: + 'Invalid argument type for function contains(), expected one of "array", "string" but found "boolean" in expression: contains(`false`, "d")', + }, + ])('contains() function errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: `ends_with(str, 'r')`, + expected: true, + }, + { + expression: `ends_with(str, 'tr')`, + expected: true, + }, + { + expression: `ends_with(str, 'Str')`, + expected: true, + }, + { + expression: `ends_with(str, 'SStr')`, + expected: false, + }, + { + expression: `ends_with(str, 'foo')`, + expected: false, + }, + ])( + 'should support the ends_with() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'ends_with(str, `0`)', + error: + 'Invalid argument type for function ends_with(), expected "string" but found "number" in expression: ends_with(str, `0`)', + }, + ])('ends_with() function errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'floor(`1.2`)', + expected: 1, + }, + { + expression: 'floor(decimals[0])', + expected: 1, + }, + { + expression: 'floor(foo)', + expected: -1, + }, + ])( + 'should support the floor() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: `floor('string')`, + error: `Invalid argument type for function floor(), expected "number" but found "string" in expression: floor('string')`, + }, + { + expression: 'floor(str)', + error: + 'Invalid argument type for function floor(), expected "number" but found "string" in expression: floor(str)', + }, + ])('floor() function errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: `length('abc')`, + expected: 3, + }, + { + expression: `length('✓foo')`, + expected: 4, + }, + { + expression: `length('')`, + expected: 0, + }, + { + expression: 'length(@)', + expected: 12, + }, + { + expression: 'length(strings[0])', + expected: 1, + }, + { + expression: 'length(str)', + expected: 3, + }, + { + expression: 'length(array)', + expected: 6, + }, + { + expression: 'length(objects)', + expected: 2, + }, + { + expression: 'length(strings[0])', + expected: 1, + }, + ])( + 'should support the length() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'length(`false`)', + error: + 'Invalid argument type for function length(), expected one of "array", "string", "object" but found "boolean" in expression: length(`false`)', + }, + { + expression: 'length(foo)', + error: + 'Invalid argument type for function length(), expected one of "array", "string", "object" but found "number" in expression: length(foo)', + }, + ])('length() function errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'max(numbers)', + expected: 5, + }, + { + expression: 'max(decimals)', + expected: 1.2, + }, + { + expression: 'max(strings)', + expected: 'c', + }, + { + expression: 'max(decimals)', + expected: 1.2, + }, + { + expression: 'max(empty_list)', + expected: null, + }, + ])( + 'should support the max() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'max(abc)', + error: + 'Invalid argument type for function max(), expected one of "array-number", "array-string" but found "null" in expression: max(abc)', + }, + { + expression: 'max(array)', + error: + 'Invalid argument type for function max(), expected "number" but found "string" in expression: max(array)', + }, + ])('max() function errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'merge(`{}`)', + expected: {}, + }, + { + expression: 'merge(`{}`, `{}`)', + expected: {}, + }, + { + expression: 'merge(`{"a": 1}`, `{"b": 2}`)', + expected: { + a: 1, + b: 2, + }, + }, + { + expression: 'merge(`{"a": 1}`, `{"a": 2}`)', + expected: { + a: 2, + }, + }, + { + expression: 'merge(`{"a": 1, "b": 2}`, `{"a": 2, "c": 3}`, `{"d": 4}`)', + expected: { + a: 2, + b: 2, + c: 3, + d: 4, + }, + }, + ])( + 'should support the merge() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'min(numbers)', + expected: -1, + }, + { + expression: 'min(decimals)', + expected: -1.5, + }, + { + expression: 'min(empty_list)', + expected: null, + }, + { + expression: 'min(decimals)', + expected: -1.5, + }, + { + expression: 'min(strings)', + expected: 'a', + }, + ])( + 'should support the min() function: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'min(abc)', + error: + 'Invalid argument type for function min(), expected one of "array-number", "array-string" but found "null" in expression: min(abc)', + }, + { + expression: 'min(array)', + error: + 'Invalid argument type for function min(), expected "number" but found "string" in expression: min(array)', + }, + ])('min() function errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: `type('abc')`, + expected: 'string', + }, + { + expression: 'type(`1.0`)', + expected: 'number', + }, + { + expression: 'type(`2`)', + expected: 'number', + }, + { + expression: 'type(`true`)', + expected: 'boolean', + }, + { + expression: 'type(`false`)', + expected: 'boolean', + }, + { + expression: 'type(`null`)', + expected: 'null', + }, + { + expression: 'type(`[0]`)', + expected: 'array', + }, + { + expression: 'type(`{"a": "b"}`)', + expected: 'object', + }, + { + expression: 'type(@)', + expected: 'object', + }, + ])('should support the type() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'sort(keys(objects))', + expected: ['bar', 'foo'], + }, + { + expression: 'sort(values(objects))', + expected: ['bar', 'baz'], + }, + { + expression: 'keys(empty_hash)', + expected: [], + }, + { + expression: 'sort(numbers)', + expected: [-1, 3, 4, 5], + }, + { + expression: 'sort(strings)', + expected: ['a', 'b', 'c'], + }, + { + expression: 'sort(decimals)', + expected: [-1.5, 1.01, 1.2], + }, + { + expression: 'sort(empty_list)', + expected: [], + }, + ])( + 'should support the sort(), key(), and values() functions', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'keys(foo)', + error: + 'Invalid argument type for function keys(), expected "object" but found "number" in expression: keys(foo)', + }, + { + expression: 'keys(strings)', + error: + 'Invalid argument type for function keys(), expected "object" but found "array" in expression: keys(strings)', + }, + { + expression: 'keys(`false`)', + error: + 'Invalid argument type for function keys(), expected "object" but found "boolean" in expression: keys(`false`)', + }, + { + expression: 'values(foo)', + error: + 'Invalid argument type for function values(), expected "object" but found "number" in expression: values(foo)', + }, + { + expression: 'sort(array)', + error: + 'Invalid argument type for function sort(), expected "number" but found "string" in expression: sort(array)', + }, + { + expression: 'sort(abc)', + error: + 'Invalid argument type for function sort(), expected one of "array-number", "array-string" but found "null" in expression: sort(abc)', + }, + { + expression: 'sort(@)', + error: + 'Invalid argument type for function sort(), expected one of "array-number", "array-string" but found "object" in expression: sort(@)', + }, + ])( + 'sort(), keys(), and values() function errors', + ({ expression, error }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + } + ); + + it.each([ + { + expression: `join(', ', strings)`, + expected: 'a, b, c', + }, + { + expression: `join(', ', strings)`, + expected: 'a, b, c', + }, + { + expression: 'join(\',\', `["a", "b"]`)', + expected: 'a,b', + }, + { + expression: `join('|', strings)`, + expected: 'a|b|c', + }, + { + expression: `join('|', decimals[].to_string(@))`, + expected: '1.01|1.2|-1.5', + }, + { + expression: `join('|', empty_list)`, + expected: '', + }, + ])('should support the join() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'join(\',\', `["a", 0]`)', + error: + 'Invalid argument type for function join(), expected "string" but found "number" in expression: join(\',\', `["a", 0]`)', + }, + { + expression: `join(', ', str)`, + error: `Invalid argument type for function join(), expected "array-string" but found "string" in expression: join(', ', str)`, + }, + { + expression: 'join(`2`, strings)', + error: + 'Invalid argument type for function join(), expected "string" but found "number" in expression: join(`2`, strings)', + }, + { + expression: `join('|', decimals)`, + error: + 'Invalid argument type for function join(), expected "string" but found "number" in expression: join(\'|\', decimals)', + }, + ])('join() function errors', ({ expression, error }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'reverse(numbers)', + expected: [5, 4, 3, -1], + }, + { + expression: 'reverse(array)', + expected: ['100', 'a', 5, 4, 3, -1], + }, + { + expression: 'reverse(`[]`)', + expected: [], + }, + { + expression: `reverse('')`, + expected: '', + }, + { + expression: `reverse('hello world')`, + expected: 'dlrow olleh', + }, + ])('should support the reverse() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: `starts_with(str, 'S')`, + expected: true, + }, + { + expression: `starts_with(str, 'St')`, + expected: true, + }, + { + expression: `starts_with(str, 'Str')`, + expected: true, + }, + { + expression: `starts_with(str, 'String')`, + expected: false, + }, + ])( + 'should support the starts_with() function', + ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'starts_with(str, `0`)', + error: + 'Invalid argument type for function starts_with(), expected "string" but found "null" in expression: starts_with(str, `0`)', + }, + ])('starts_with() function errors', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'sum(numbers)', + expected: 11, + }, + { + expression: 'sum(decimals)', + expected: 0.71, + }, + { + expression: 'sum(array[].to_number(@))', + expected: 111, + }, + { + expression: 'sum(`[]`)', + expected: 0, + }, + ])('should support the sum() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'sum(array)', + error: + 'Invalid argument type for function sum(), expected "array-number" but found "null" in expression: sum(array)', + }, + ])('sum() function errors', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: `to_array('foo')`, + expected: ['foo'], + }, + { + expression: 'to_array(`0`)', + expected: [0], + }, + { + expression: 'to_array(objects)', + expected: [ + { + foo: 'bar', + bar: 'baz', + }, + ], + }, + { + expression: 'to_array(`[1, 2, 3]`)', + expected: [1, 2, 3], + }, + { + expression: 'to_array(false)', + expected: [false], + }, + ])('should support the to_array() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: `to_string('foo')`, + expected: 'foo', + }, + { + expression: 'to_string(`1.2`)', + expected: '1.2', + }, + { + expression: 'to_string(`[0, 1]`)', + expected: '[0,1]', + }, + { + description: 'function projection on single arg function', + expression: 'numbers[].to_string(@)', + expected: ['-1', '3', '4', '5'], + }, + ])('should support the to_string() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: `to_number('1.0')`, + expected: 1.0, + }, + { + expression: `to_number('1.1')`, + expected: 1.1, + }, + { + expression: `to_number('4')`, + expected: 4, + }, + { + expression: `to_number('notanumber')`, + expected: null, + }, + { + expression: 'to_number(`false`)', + expected: null, + }, + { + expression: 'to_number(`null`)', + expected: null, + }, + { + expression: 'to_number(`[0]`)', + expected: null, + }, + { + expression: 'to_number(`{"foo": 0}`)', + expected: null, + }, + { + description: 'function projection on single arg function', + expression: 'array[].to_number(@)', + expected: [-1, 3, 4, 5, 100], + }, + ])('should support the to_number() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: '"to_string"(`1.0`)', + error: + 'Invalid jmespath expression: parse error at column 0, quoted identifiers cannot be used as a function name in expression: "to_string"(`1.0`)', + }, + ])('to_number() function errors', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'not_null(unknown_key, str)', + expected: 'Str', + }, + { + expression: 'not_null(unknown_key, foo.bar, empty_list, str)', + expected: [], + }, + { + expression: 'not_null(unknown_key, null_key, empty_list, str)', + expected: [], + }, + { + expression: 'not_null(all, expressions, are_null)', + expected: null, + }, + ])('should support the not_null() function', ({ expression, expected }) => { + // Prepare + const data = { + foo: -1, + zero: 0, + numbers: [-1, 3, 4, 5], + array: [-1, 3, 4, 5, 'a', '100'], + strings: ['a', 'b', 'c'], + decimals: [1.01, 1.2, -1.5], + str: 'Str', + false: false, + empty_list: [], + empty_hash: {}, + objects: { + foo: 'bar', + bar: 'baz', + }, + null_key: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'not_null()', + error: + 'Expected 1 argument for function not_null(), received 0 in expression: not_null()', + }, + ])('not_null() function errors', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + description: 'function projection on variadic function', + expression: 'foo[].not_null(f, e, d, c, b, a)', + expected: ['b', 'c', 'd', 'e', 'f'], + }, + ])( + 'should support function projection on variadic function', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { + b: 'b', + a: 'a', + }, + { + c: 'c', + b: 'b', + }, + { + d: 'd', + c: 'c', + }, + { + e: 'e', + d: 'd', + }, + { + f: 'f', + e: 'e', + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + description: 'sort by field expression', + expression: 'sort_by(people, &age)', + expected: [ + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + ], + }, + { + expression: 'sort_by(people, &age_str)', + expected: [ + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + ], + }, + { + description: 'sort by function expression', + expression: 'sort_by(people, &to_number(age_str))', + expected: [ + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + ], + }, + { + description: 'function projection on sort_by function', + expression: 'sort_by(people, &age)[].name', + expected: [3, 'a', 'c', 'b', 'd'], + }, + + { + expression: 'sort_by(people, &age)[].extra', + expected: ['foo', 'bar'], + }, + { + expression: 'sort_by(`[]`, &age)', + expected: [], + }, + { + expression: 'sort_by(people, &name)', + expected: [ + { age: 10, age_str: '10', bool: true, name: 3 }, + { age: 20, age_str: '20', bool: true, name: 'a', extra: 'foo' }, + { age: 40, age_str: '40', bool: false, name: 'b', extra: 'bar' }, + { age: 30, age_str: '30', bool: true, name: 'c' }, + { age: 50, age_str: '50', bool: false, name: 'd' }, + ], + }, + ])('should support sorty_by() special cases', ({ expression, expected }) => { + // Prepare + const data = { + people: [ + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'sort_by(people, &extra)', + error: + 'Invalid argument type for function sort_by(), expected "string" but found "null" in expression: sort_by(people, &extra)', + }, + { + expression: 'sort_by(people, &bool)', + error: + 'Invalid argument type for function sort_by(), expected "string" but found "boolean" in expression: sort_by(people, &bool)', + }, + { + expression: 'sort_by(people, name)', + error: + 'Invalid argument type for function sort_by(), expected "expression" but found "null" in expression: sort_by(people, name)', + }, + ])('sort_by() function special cases errors', ({ expression, error }) => { + // Prepare + const data = { + people: [ + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + ], + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'max_by(people, &age)', + expected: { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + }, + { + expression: 'max_by(people, &age_str)', + expected: { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + }, + { + expression: 'max_by(people, &to_number(age_str))', + expected: { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + }, + ])('should support max_by() special cases', ({ expression, expected }) => { + // Prepare + const data = { + people: [ + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'max_by(people, &bool)', + error: + 'Invalid argument type for function max_by(), expected "string" but found "boolean" in expression: max_by(people, &bool)', + }, + { + expression: 'max_by(people, &extra)', + error: + 'Invalid argument type for function max_by(), expected "string" but found "null" in expression: max_by(people, &extra)', + }, + ])('max_by() function special cases errors', ({ expression, error }) => { + // Prepare + const data = { + people: [ + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + ], + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'min_by(people, &age)', + expected: { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + }, + { + expression: 'min_by(people, &age_str)', + expected: { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + }, + { + expression: 'min_by(people, &to_number(age_str))', + expected: { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + }, + ])('should support min_by() special cases', ({ expression, expected }) => { + // Prepare + const data = { + people: [ + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'min_by(people, &bool)', + error: + 'Invalid argument type for function min_by(), expected "string" but found "boolean" in expression: min_by(people, &bool)', + }, + { + expression: 'min_by(people, &extra)', + error: + 'Invalid argument type for function min_by(), expected "string" but found "null" in expression: min_by(people, &extra)', + }, + ])('min_by() function special cases errors', ({ expression, error }) => { + // Prepare + const data = { + people: [ + { + age: 20, + age_str: '20', + bool: true, + name: 'a', + extra: 'foo', + }, + { + age: 40, + age_str: '40', + bool: false, + name: 'b', + extra: 'bar', + }, + { + age: 30, + age_str: '30', + bool: true, + name: 'c', + }, + { + age: 50, + age_str: '50', + bool: false, + name: 'd', + }, + { + age: 10, + age_str: '10', + bool: true, + name: 3, + }, + ], + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + description: 'stable sort order', + expression: 'sort_by(people, &age)', + expected: [ + { + age: 10, + order: '1', + }, + { + age: 10, + order: '2', + }, + { + age: 10, + order: '3', + }, + { + age: 10, + order: '4', + }, + { + age: 10, + order: '5', + }, + { + age: 10, + order: '6', + }, + { + age: 10, + order: '7', + }, + { + age: 10, + order: '8', + }, + { + age: 10, + order: '9', + }, + { + age: 10, + order: '10', + }, + { + age: 10, + order: '11', + }, + ], + }, + ])('should support stable sort_by() order', ({ expression, expected }) => { + // Prepare + const data = { + people: [ + { + age: 10, + order: '1', + }, + { + age: 10, + order: '2', + }, + { + age: 10, + order: '3', + }, + { + age: 10, + order: '4', + }, + { + age: 10, + order: '5', + }, + { + age: 10, + order: '6', + }, + { + age: 10, + order: '7', + }, + { + age: 10, + order: '8', + }, + { + age: 10, + order: '9', + }, + { + age: 10, + order: '10', + }, + { + age: 10, + order: '11', + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'map(&a, people)', + expected: [10, 10, 10, 10, 10, 10, 10, 10, 10], + }, + { + expression: 'map(&c, people)', + expected: ['z', null, null, 'z', null, null, 'z', null, null], + }, + { + expression: 'map(&foo, empty)', + expected: [], + }, + ])('should support map() special cases', ({ expression, expected }) => { + // Prepare + const data = { + people: [ + { + a: 10, + b: 1, + c: 'z', + }, + { + a: 10, + b: 2, + c: null, + }, + { + a: 10, + b: 3, + }, + { + a: 10, + b: 4, + c: 'z', + }, + { + a: 10, + b: 5, + c: null, + }, + { + a: 10, + b: 6, + }, + { + a: 10, + b: 7, + c: 'z', + }, + { + a: 10, + b: 8, + c: null, + }, + { + a: 10, + b: 9, + }, + ], + empty: [], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'map(&a, badkey)', + error: + 'Invalid argument type for function map(), expected "array" but found "null" in expression: map(&a, badkey)', + }, + ])('map() function special cases errors', ({ expression, error }) => { + // Prepare + const data = { + people: [ + { + a: 10, + b: 1, + c: 'z', + }, + { + a: 10, + b: 2, + c: null, + }, + { + a: 10, + b: 3, + }, + { + a: 10, + b: 4, + c: 'z', + }, + { + a: 10, + b: 5, + c: null, + }, + { + a: 10, + b: 6, + }, + { + a: 10, + b: 7, + c: 'z', + }, + { + a: 10, + b: 8, + c: null, + }, + { + a: 10, + b: 9, + }, + ], + empty: [], + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'map(&foo.bar, array)', + expected: ['yes1', 'yes2', null], + }, + { + expression: 'map(&foo1.bar, array)', + expected: [null, null, 'no'], + }, + { + expression: 'map(&foo.bar.baz, array)', + expected: [null, null, null], + }, + ])( + 'should support map() with the `&` expression cases', + ({ expression, expected }) => { + // Prepare + const data = { + array: [ + { + foo: { + bar: 'yes1', + }, + }, + { + foo: { + bar: 'yes2', + }, + }, + { + foo1: { + bar: 'no', + }, + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'map(&[], array)', + expected: [ + [1, 2, 3, 4], + [5, 6, 7, 8, 9], + ], + }, + ])('should support map() with `&` and `[]`', ({ expression, expected }) => { + // Prepare + const data = { + array: [ + [1, 2, 3, [4]], + [5, 6, 7, [8, 9]], + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); +}); diff --git a/packages/jmespath/tests/unit/compliance/identifiers.test.ts b/packages/jmespath/tests/unit/compliance/identifiers.test.ts new file mode 100644 index 0000000000..bd3eed3c1d --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/identifiers.test.ts @@ -0,0 +1,895 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/identifiers + */ +import { search } from '../../../src/index.js'; + +describe('Identifiers tests', () => { + it.each([ + { + data: { + __L: true, + }, + expression: '__L', + expected: true, + }, + { + data: { + '!\r': true, + }, + expression: '"!\\r"', + expected: true, + }, + { + data: { + Y_1623: true, + }, + expression: 'Y_1623', + expected: true, + }, + { + data: { + x: true, + }, + expression: 'x', + expected: true, + }, + { + data: { + '\tF\uCebb': true, + }, + expression: '"\\tF\\uCebb"', + expected: true, + }, + { + data: { + ' \t': true, + }, + expression: '" \\t"', + expected: true, + }, + { + data: { + ' ': true, + }, + expression: '" "', + expected: true, + }, + { + data: { + v2: true, + }, + expression: 'v2', + expected: true, + }, + { + data: { + '\t': true, + }, + expression: '"\\t"', + expected: true, + }, + { + data: { + _X: true, + }, + expression: '_X', + expected: true, + }, + { + data: { + '\t4\ud9da\udd15': true, + }, + expression: '"\\t4\\ud9da\\udd15"', + expected: true, + }, + { + data: { + v24_W: true, + }, + expression: 'v24_W', + expected: true, + }, + { + data: { + H: true, + }, + expression: '"H"', + expected: true, + }, + { + data: { + '\f': true, + }, + expression: '"\\f"', + expected: true, + }, + { + data: { + E4: true, + }, + expression: '"E4"', + expected: true, + }, + { + data: { + '!': true, + }, + expression: '"!"', + expected: true, + }, + { + data: { + tM: true, + }, + expression: 'tM', + expected: true, + }, + { + data: { + ' [': true, + }, + expression: '" ["', + expected: true, + }, + { + data: { + 'R!': true, + }, + expression: '"R!"', + expected: true, + }, + { + data: { + _6W: true, + }, + expression: '_6W', + expected: true, + }, + { + data: { + '\uaBA1\r': true, + }, + expression: '"\\uaBA1\\r"', + expected: true, + }, + { + data: { + tL7: true, + }, + expression: 'tL7', + expected: true, + }, + { + data: { + '<': true, + }, + expression: '">"', + expected: true, + }, + { + data: { + hvu: true, + }, + expression: 'hvu', + expected: true, + }, + { + data: { + '; !': true, + }, + expression: '"; !"', + expected: true, + }, + { + data: { + hU: true, + }, + expression: 'hU', + expected: true, + }, + { + data: { + '!I\n/': true, + }, + expression: '"!I\\n\\/"', + expected: true, + }, + { + data: { + '\uEEbF': true, + }, + expression: '"\\uEEbF"', + expected: true, + }, + { + data: { + 'U)\t': true, + }, + expression: '"U)\\t"', + expected: true, + }, + { + data: { + fa0_9: true, + }, + expression: 'fa0_9', + expected: true, + }, + { + data: { + '/': true, + }, + expression: '"/"', + expected: true, + }, + { + data: { + Gy: true, + }, + expression: 'Gy', + expected: true, + }, + { + data: { + '\b': true, + }, + expression: '"\\b"', + expected: true, + }, + { + data: { + '<': true, + }, + expression: '"<"', + expected: true, + }, + { + data: { + '\t': true, + }, + expression: '"\\t"', + expected: true, + }, + { + data: { + '\t&\\\r': true, + }, + expression: '"\\t&\\\\\\r"', + expected: true, + }, + { + data: { + '#': true, + }, + expression: '"#"', + expected: true, + }, + { + data: { + B__: true, + }, + expression: 'B__', + expected: true, + }, + { + data: { + '\nS \n': true, + }, + expression: '"\\nS \\n"', + expected: true, + }, + { + data: { + Bp: true, + }, + expression: 'Bp', + expected: true, + }, + { + data: { + ',\t;': true, + }, + expression: '",\\t;"', + expected: true, + }, + { + data: { + B_q: true, + }, + expression: 'B_q', + expected: true, + }, + { + data: { + '/+\t\n\b!Z': true, + }, + expression: '"\\/+\\t\\n\\b!Z"', + expected: true, + }, + { + data: { + '\udadd\udfc7\\ueFAc': true, + }, + expression: '"\udadd\udfc7\\\\ueFAc"', + expected: true, + }, + { + data: { + ':\f': true, + }, + expression: '":\\f"', + expected: true, + }, + { + data: { + '/': true, + }, + expression: '"\\/"', + expected: true, + }, + { + data: { + _BW_6Hg_Gl: true, + }, + expression: '_BW_6Hg_Gl', + expected: true, + }, + { + data: { + '\udbcf\udc02': true, + }, + expression: '"\udbcf\udc02"', + expected: true, + }, + { + data: { + zs1DC: true, + }, + expression: 'zs1DC', + expected: true, + }, + { + data: { + __434: true, + }, + expression: '__434', + expected: true, + }, + { + data: { + '\udb94\udd41': true, + }, + expression: '"\udb94\udd41"', + expected: true, + }, + { + data: { + Z_5: true, + }, + expression: 'Z_5', + expected: true, + }, + { + data: { + z_M_: true, + }, + expression: 'z_M_', + expected: true, + }, + { + data: { + YU_2: true, + }, + expression: 'YU_2', + expected: true, + }, + { + data: { + _0: true, + }, + expression: '_0', + expected: true, + }, + { + data: { + '\b+': true, + }, + expression: '"\\b+"', + expected: true, + }, + { + data: { + '"': true, + }, + expression: '"\\""', + expected: true, + }, + { + data: { + D7: true, + }, + expression: 'D7', + expected: true, + }, + { + data: { + _62L: true, + }, + expression: '_62L', + expected: true, + }, + { + data: { + '\tK\t': true, + }, + expression: '"\\tK\\t"', + expected: true, + }, + { + data: { + '\n\\\f': true, + }, + expression: '"\\n\\\\\\f"', + expected: true, + }, + { + data: { + I_: true, + }, + expression: 'I_', + expected: true, + }, + { + data: { + W_a0_: true, + }, + expression: 'W_a0_', + expected: true, + }, + { + data: { + BQ: true, + }, + expression: 'BQ', + expected: true, + }, + { + data: { + '\tX$\uABBb': true, + }, + expression: '"\\tX$\\uABBb"', + expected: true, + }, + { + data: { + Z9: true, + }, + expression: 'Z9', + expected: true, + }, + { + data: { + '\b%"\uda38\udd0f': true, + }, + expression: '"\\b%\\"\uda38\udd0f"', + expected: true, + }, + { + data: { + _F: true, + }, + expression: '_F', + expected: true, + }, + { + data: { + '!,': true, + }, + expression: '"!,"', + expected: true, + }, + { + data: { + '"!': true, + }, + expression: '"\\"!"', + expected: true, + }, + { + data: { + Hh: true, + }, + expression: 'Hh', + expected: true, + }, + { + data: { + '&': true, + }, + expression: '"&"', + expected: true, + }, + { + data: { + '9\r\\R': true, + }, + expression: '"9\\r\\\\R"', + expected: true, + }, + { + data: { + M_k: true, + }, + expression: 'M_k', + expected: true, + }, + { + data: { + '!\b\n\udb06\ude52""': true, + }, + expression: '"!\\b\\n\udb06\ude52\\"\\""', + expected: true, + }, + { + data: { + '6': true, + }, + expression: '"6"', + expected: true, + }, + { + data: { + _7: true, + }, + expression: '_7', + expected: true, + }, + { + data: { + '0': true, + }, + expression: '"0"', + expected: true, + }, + { + data: { + '\\8\\': true, + }, + expression: '"\\\\8\\\\"', + expected: true, + }, + { + data: { + b7eo: true, + }, + expression: 'b7eo', + expected: true, + }, + { + data: { + xIUo9: true, + }, + expression: 'xIUo9', + expected: true, + }, + { + data: { + '5': true, + }, + expression: '"5"', + expected: true, + }, + { + data: { + '?': true, + }, + expression: '"?"', + expected: true, + }, + { + data: { + sU: true, + }, + expression: 'sU', + expected: true, + }, + { + data: { + 'VH2&H\\/': true, + }, + expression: '"VH2&H\\\\\\/"', + expected: true, + }, + { + data: { + _C: true, + }, + expression: '_C', + expected: true, + }, + { + data: { + _: true, + }, + expression: '_', + expected: true, + }, + { + data: { + '<\t': true, + }, + expression: '"<\\t"', + expected: true, + }, + { + data: { + '\uD834\uDD1E': true, + }, + expression: '"\\uD834\\uDD1E"', + expected: true, + }, + ])( + 'should handle different identifiers: $expression', + ({ data, expression, expected }) => { + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/compliance/indices.test.ts b/packages/jmespath/tests/unit/compliance/indices.test.ts new file mode 100644 index 0000000000..44d4874a9a --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/indices.test.ts @@ -0,0 +1,526 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/indices + */ +import { search } from '../../../src/index.js'; + +describe('Indices tests', () => { + it.each([ + { + expression: 'foo.bar[0]', + expected: 'zero', + }, + { + expression: 'foo.bar[1]', + expected: 'one', + }, + { + expression: 'foo.bar[2]', + expected: 'two', + }, + { + expression: 'foo.bar[3]', + expected: null, + }, + { + expression: 'foo.bar[-1]', + expected: 'two', + }, + { + expression: 'foo.bar[-2]', + expected: 'one', + }, + { + expression: 'foo.bar[-3]', + expected: 'zero', + }, + { + expression: 'foo.bar[-4]', + expected: null, + }, + ])( + 'should support indices on arrays in a nested object: $expression', + ({ expression, expected }) => { + // Prepare + const data = { foo: { bar: ['zero', 'one', 'two'] } }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.bar', + expected: null, + }, + { + expression: 'foo[0].bar', + expected: 'one', + }, + { + expression: 'foo[1].bar', + expected: 'two', + }, + { + expression: 'foo[2].bar', + expected: 'three', + }, + { + expression: 'foo[3].notbar', + expected: 'four', + }, + { + expression: 'foo[3].bar', + expected: null, + }, + { + expression: 'foo[0]', + expected: { bar: 'one' }, + }, + { + expression: 'foo[1]', + expected: { bar: 'two' }, + }, + { + expression: 'foo[2]', + expected: { bar: 'three' }, + }, + { + expression: 'foo[3]', + expected: { notbar: 'four' }, + }, + { + expression: 'foo[4]', + expected: null, + }, + ])( + 'should support indices in an array with objects inside: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { bar: 'one' }, + { bar: 'two' }, + { bar: 'three' }, + { notbar: 'four' }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '[0]', + expected: 'one', + }, + { + expression: '[1]', + expected: 'two', + }, + { + expression: '[2]', + expected: 'three', + }, + { + expression: '[-1]', + expected: 'three', + }, + { + expression: '[-2]', + expected: 'two', + }, + { + expression: '[-3]', + expected: 'one', + }, + ])( + 'should support indices in an array: $expression', + ({ expression, expected }) => { + // Prepare + const data = ['one', 'two', 'three']; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'reservations[].instances[].foo', + expected: [1, 2], + }, + { + expression: 'reservations[].instances[].bar', + expected: [], + }, + { + expression: 'reservations[].notinstances[].foo', + expected: [], + }, + { + expression: 'reservations[].notinstances[].foo', + expected: [], + }, + ])( + 'should support indices in multi-level nested arrays & objects: $expression', + ({ expression, expected }) => { + // Prepare + const data = { reservations: [{ instances: [{ foo: 1 }, { foo: 2 }] }] }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'reservations[].instances[].foo[].bar', + expected: [1, 2, 4, 5, 6, 8], + }, + { + expression: 'reservations[].instances[].foo[].baz', + expected: [], + }, + { + expression: 'reservations[].instances[].notfoo[].bar', + expected: [20, 21, 22, 23, 24, 25], + }, + { + expression: 'reservations[].instances[].notfoo[].notbar', + expected: [[7], [7]], + }, + { + expression: 'reservations[].notinstances[].foo', + expected: [], + }, + { + expression: 'reservations[].instances[].foo[].notbar', + expected: [3, [7]], + }, + { + expression: 'reservations[].instances[].bar[].baz', + expected: [[1], [2], [3], [4]], + }, + { + expression: 'reservations[].instances[].baz[].baz', + expected: [[1, 2], [], [], [3, 4]], + }, + { + expression: 'reservations[].instances[].qux[].baz', + expected: [[], [1, 2, 3], [4], [], [], [1, 2, 3], [4], []], + }, + { + expression: 'reservations[].instances[].qux[].baz[]', + expected: [1, 2, 3, 4, 1, 2, 3, 4], + }, + ])( + 'should support indices in large mixed objects and arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + reservations: [ + { + instances: [ + { foo: [{ bar: 1 }, { bar: 2 }, { notbar: 3 }, { bar: 4 }] }, + { foo: [{ bar: 5 }, { bar: 6 }, { notbar: [7] }, { bar: 8 }] }, + { foo: 'bar' }, + { + notfoo: [ + { bar: 20 }, + { bar: 21 }, + { notbar: [7] }, + { bar: 22 }, + ], + }, + { bar: [{ baz: [1] }, { baz: [2] }, { baz: [3] }, { baz: [4] }] }, + { + baz: [ + { baz: [1, 2] }, + { baz: [] }, + { baz: [] }, + { baz: [3, 4] }, + ], + }, + { + qux: [ + { baz: [] }, + { baz: [1, 2, 3] }, + { baz: [4] }, + { baz: [] }, + ], + }, + ], + otherkey: { + foo: [{ bar: 1 }, { bar: 2 }, { notbar: 3 }, { bar: 4 }], + }, + }, + { + instances: [ + { a: [{ bar: 1 }, { bar: 2 }, { notbar: 3 }, { bar: 4 }] }, + { b: [{ bar: 5 }, { bar: 6 }, { notbar: [7] }, { bar: 8 }] }, + { c: 'bar' }, + { + notfoo: [ + { bar: 23 }, + { bar: 24 }, + { notbar: [7] }, + { bar: 25 }, + ], + }, + { + qux: [ + { baz: [] }, + { baz: [1, 2, 3] }, + { baz: [4] }, + { baz: [] }, + ], + }, + ], + otherkey: { + foo: [{ bar: 1 }, { bar: 2 }, { notbar: 3 }, { bar: 4 }], + }, + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[]', + expected: [ + ['one', 'two'], + ['three', 'four'], + ['five', 'six'], + ['seven', 'eight'], + ['nine'], + ['ten'], + ], + }, + { + expression: 'foo[][0]', + expected: ['one', 'three', 'five', 'seven', 'nine', 'ten'], + }, + { + expression: 'foo[][1]', + expected: ['two', 'four', 'six', 'eight'], + }, + { + expression: 'foo[][0][0]', + expected: [], + }, + { + expression: 'foo[][2][2]', + expected: [], + }, + { + expression: 'foo[][0][0][100]', + expected: [], + }, + ])( + 'should support indices in objects containing an array of matrixes: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + [ + ['one', 'two'], + ['three', 'four'], + ], + [ + ['five', 'six'], + ['seven', 'eight'], + ], + [['nine'], ['ten']], + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo', + expected: [ + { + bar: [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + ], + }, + { + bar: [ + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + }, + ], + }, + { + expression: 'foo[]', + expected: [ + { + bar: [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + ], + }, + { + bar: [ + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + }, + ], + }, + { + expression: 'foo[].bar', + expected: [ + [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + ], + [ + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + ], + }, + { + expression: 'foo[].bar[]', + expected: [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + }, + { + expression: 'foo[].bar[].baz', + expected: [1, 3, 5, 7], + }, + ])( + 'should support indices with nested arrays and objects at different levels: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { + bar: [ + { + qux: 2, + baz: 1, + }, + { + qux: 4, + baz: 3, + }, + ], + }, + { + bar: [ + { + qux: 6, + baz: 5, + }, + { + qux: 8, + baz: 7, + }, + ], + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'string[]', + expected: null, + }, + { + expression: 'hash[]', + expected: null, + }, + { + expression: 'number[]', + expected: null, + }, + { + expression: 'nullvalue[]', + expected: null, + }, + { + expression: 'string[].foo', + expected: null, + }, + { + expression: 'hash[].foo', + expected: null, + }, + { + expression: 'number[].foo', + expected: null, + }, + { + expression: 'nullvalue[].foo', + expected: null, + }, + { + expression: 'nullvalue[].foo[].bar', + expected: null, + }, + ])( + 'should support indices in objects having special names as keys: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + string: 'string', + hash: { foo: 'bar', bar: 'baz' }, + number: 23, + nullvalue: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/compliance/literal.test.ts b/packages/jmespath/tests/unit/compliance/literal.test.ts new file mode 100644 index 0000000000..d2d95495f7 --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/literal.test.ts @@ -0,0 +1,255 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/literal + */ +import { search } from '../../../src/index.js'; + +describe('Literal expressions tests', () => { + it.each([ + { + expression: '`"foo"`', + expected: 'foo', + }, + { + comment: 'Interpret escaped unicode.', + expression: '`"\\u03a6"`', + expected: 'Φ', + }, + { + expression: '`"✓"`', + expected: '✓', + }, + { + expression: '`[1, 2, 3]`', + expected: [1, 2, 3], + }, + { + expression: '`{"a": "b"}`', + expected: { + a: 'b', + }, + }, + { + expression: '`true`', + expected: true, + }, + { + expression: '`false`', + expected: false, + }, + { + expression: '`null`', + expected: null, + }, + { + expression: '`0`', + expected: 0, + }, + { + expression: '`1`', + expected: 1, + }, + { + expression: '`2`', + expected: 2, + }, + { + expression: '`3`', + expected: 3, + }, + { + expression: '`4`', + expected: 4, + }, + { + expression: '`5`', + expected: 5, + }, + { + expression: '`6`', + expected: 6, + }, + { + expression: '`7`', + expected: 7, + }, + { + expression: '`8`', + expected: 8, + }, + { + expression: '`9`', + expected: 9, + }, + { + comment: 'Escaping a backtick in quotes', + expression: '`"foo\\`bar"`', + expected: 'foo`bar', + }, + { + comment: 'Double quote in literal', + expression: '`"foo\\"bar"`', + expected: 'foo"bar', + }, + { + expression: '`"1\\`"`', + expected: '1`', + }, + { + comment: 'Multiple literal expressions with escapes', + expression: '`"\\\\"`.{a:`"b"`}', + expected: { + a: 'b', + }, + }, + { + comment: 'literal . identifier', + expression: '`{"a": "b"}`.a', + expected: 'b', + }, + { + comment: 'literal . identifier . identifier', + expression: '`{"a": {"b": "c"}}`.a.b', + expected: 'c', + }, + { + comment: 'literal . identifier bracket-expr', + expression: '`[0, 1, 2]`[1]', + expected: 1, + }, + ])( + 'should support literal expressions: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { + name: 'a', + }, + { + name: 'b', + }, + ], + bar: { + baz: 'qux', + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Literal with leading whitespace', + expression: '` {"foo": true}`', + expected: { + foo: true, + }, + }, + { + comment: 'Literal with trailing whitespace', + expression: '`{"foo": true} `', + expected: { + foo: true, + }, + }, + ])( + 'should support literals with other special characters: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Literal on RHS of subexpr not allowed', + expression: 'foo.`"bar"`', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "bar" (literal) in expression: foo.`"bar"`', + }, + ])('literals errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: `'foo'`, + expected: 'foo', + }, + { + expression: `' foo '`, + expected: ' foo ', + }, + { + expression: `'0'`, + expected: '0', + }, + { + expression: `'newline\n'`, + expected: 'newline\n', + }, + { + expression: `'\n'`, + expected: '\n', + }, + { + expression: `'✓'`, + expected: '✓', + }, + { + expression: `'𝄞'`, + expected: '𝄞', + }, + { + expression: `' [foo] '`, + expected: ' [foo] ', + }, + { + expression: `'[foo]'`, + expected: '[foo]', + }, + { + comment: 'Do not interpret escaped unicode.', + expression: `'\\u03a6'`, + expected: '\\u03a6', + }, + { + comment: 'Can escape the single quote', + expression: `'foo\\'bar'`, + expected: `foo'bar`, + }, + ])( + 'should support raw string literals: $expression', + ({ expression, expected }) => { + // Prepare + const data = {}; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/compliance/multiselect.test.ts b/packages/jmespath/tests/unit/compliance/multiselect.test.ts new file mode 100644 index 0000000000..52e81964d8 --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/multiselect.test.ts @@ -0,0 +1,583 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/multiselect + */ +import { search } from '../../../src/index.js'; + +describe('Multiselect expressions tests', () => { + it.each([ + { + expression: 'foo.{bar: bar}', + expected: { bar: 'bar' }, + }, + { + expression: 'foo.{"bar": bar}', + expected: { bar: 'bar' }, + }, + { + expression: 'foo.{"foo.bar": bar}', + expected: { 'foo.bar': 'bar' }, + }, + { + expression: 'foo.{bar: bar, baz: baz}', + expected: { bar: 'bar', baz: 'baz' }, + }, + { + expression: 'foo.{"bar": bar, "baz": baz}', + expected: { bar: 'bar', baz: 'baz' }, + }, + { + expression: '{"baz": baz, "qux\\"": "qux\\""}', + expected: { baz: 2, 'qux"': 3 }, + }, + { + expression: 'foo.{bar:bar,baz:baz}', + expected: { bar: 'bar', baz: 'baz' }, + }, + { + expression: 'foo.{bar: bar,qux: qux}', + expected: { bar: 'bar', qux: 'qux' }, + }, + { + expression: 'foo.{bar: bar, noexist: noexist}', + expected: { bar: 'bar', noexist: null }, + }, + { + expression: 'foo.{noexist: noexist, alsonoexist: alsonoexist}', + expected: { noexist: null, alsonoexist: null }, + }, + { + expression: 'foo.badkey.{nokey: nokey, alsonokey: alsonokey}', + expected: null, + }, + { + expression: 'foo.nested.*.{a: a,b: b}', + expected: [ + { a: 'first', b: 'second' }, + { a: 'first', b: 'second' }, + { a: 'first', b: 'second' }, + ], + }, + { + expression: 'foo.nested.three.{a: a, cinner: c.inner}', + expected: { a: 'first', cinner: 'third' }, + }, + { + expression: 'foo.nested.three.{a: a, c: c.inner.bad.key}', + expected: { a: 'first', c: null }, + }, + { + expression: 'foo.{a: nested.one.a, b: nested.two.b}', + expected: { a: 'first', b: 'second' }, + }, + { + expression: '{bar: bar, baz: baz}', + expected: { bar: 1, baz: 2 }, + }, + { + expression: '{bar: bar}', + expected: { bar: 1 }, + }, + { + expression: '{otherkey: bar}', + expected: { otherkey: 1 }, + }, + { + expression: '{no: no, exist: exist}', + expected: { no: null, exist: null }, + }, + { + expression: 'foo.[bar]', + expected: ['bar'], + }, + { + expression: 'foo.[bar,baz]', + expected: ['bar', 'baz'], + }, + { + expression: 'foo.[bar,qux]', + expected: ['bar', 'qux'], + }, + { + expression: 'foo.[bar,noexist]', + expected: ['bar', null], + }, + { + expression: 'foo.[noexist,alsonoexist]', + expected: [null, null], + }, + ])( + 'should support expression on large nested objects: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: 'bar', + baz: 'baz', + qux: 'qux', + nested: { + one: { + a: 'first', + b: 'second', + c: 'third', + }, + two: { + a: 'first', + b: 'second', + c: 'third', + }, + three: { + a: 'first', + b: 'second', + c: { inner: 'third' }, + }, + }, + }, + bar: 1, + baz: 2, + 'qux"': 3, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.{bar:bar,baz:baz}', + expected: { bar: 1, baz: [2, 3, 4] }, + }, + { + expression: 'foo.[bar,baz[0]]', + expected: [1, 2], + }, + { + expression: 'foo.[bar,baz[1]]', + expected: [1, 3], + }, + { + expression: 'foo.[bar,baz[2]]', + expected: [1, 4], + }, + { + expression: 'foo.[bar,baz[3]]', + expected: [1, null], + }, + { + expression: 'foo.[bar[0],baz[3]]', + expected: [null, null], + }, + ])( + 'should support the expression on objects containing arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { bar: 1, baz: [2, 3, 4] }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.{bar: bar, baz: baz}', + expected: { bar: 1, baz: 2 }, + }, + { + expression: 'foo.[bar,baz]', + expected: [1, 2], + }, + ])( + 'should support the expression using both array and object syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { bar: 1, baz: 2 }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.{bar: bar.baz[1],includeme: includeme}', + expected: { bar: { common: 'second', two: 2 }, includeme: true }, + }, + { + expression: 'foo.{"bar.baz.two": bar.baz[1].two, includeme: includeme}', + expected: { 'bar.baz.two': 2, includeme: true }, + }, + { + expression: 'foo.[includeme, bar.baz[*].common]', + expected: [true, ['first', 'second']], + }, + { + expression: 'foo.[includeme, bar.baz[*].none]', + expected: [true, []], + }, + { + expression: 'foo.[includeme, bar.baz[].common]', + expected: [true, ['first', 'second']], + }, + ])( + 'should support the expression using mixed array and object syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: { + baz: [ + { common: 'first', one: 1 }, + { common: 'second', two: 2 }, + ], + }, + ignoreme: 1, + includeme: true, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'reservations[*].instances[*].{id: id, name: name}', + expected: [ + [ + { id: 'id1', name: 'first' }, + { id: 'id2', name: 'second' }, + ], + [ + { id: 'id3', name: 'third' }, + { id: 'id4', name: 'fourth' }, + ], + ], + }, + { + expression: 'reservations[].instances[].{id: id, name: name}', + expected: [ + { id: 'id1', name: 'first' }, + { id: 'id2', name: 'second' }, + { id: 'id3', name: 'third' }, + { id: 'id4', name: 'fourth' }, + ], + }, + { + expression: 'reservations[].instances[].[id, name]', + expected: [ + ['id1', 'first'], + ['id2', 'second'], + ['id3', 'third'], + ['id4', 'fourth'], + ], + }, + ])( + 'should support the expression with wildcards: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + reservations: [ + { + instances: [ + { id: 'id1', name: 'first' }, + { id: 'id2', name: 'second' }, + ], + }, + { + instances: [ + { id: 'id3', name: 'third' }, + { id: 'id4', name: 'fourth' }, + ], + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo', + expected: [ + { + bar: [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + ], + }, + { + bar: [ + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + }, + ], + }, + { + expression: 'foo[]', + expected: [ + { + bar: [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + ], + }, + { + bar: [ + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + }, + ], + }, + { + expression: 'foo[].bar', + expected: [ + [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + ], + [ + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + ], + }, + { + expression: 'foo[].bar[]', + expected: [ + { qux: 2, baz: 1 }, + { qux: 4, baz: 3 }, + { qux: 6, baz: 5 }, + { qux: 8, baz: 7 }, + ], + }, + { + expression: 'foo[].bar[].[baz, qux]', + expected: [ + [1, 2], + [3, 4], + [5, 6], + [7, 8], + ], + }, + { + expression: 'foo[].bar[].[baz]', + expected: [[1], [3], [5], [7]], + }, + { + expression: 'foo[].bar[].[baz, qux][]', + expected: [1, 2, 3, 4, 5, 6, 7, 8], + }, + ])( + 'should support expression with the flatten operator: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { + bar: [ + { + qux: 2, + baz: 1, + }, + { + qux: 4, + baz: 3, + }, + ], + }, + { + bar: [ + { + qux: 6, + baz: 5, + }, + { + qux: 8, + baz: 7, + }, + ], + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.[baz[*].bar, qux[0]]', + expected: [['abc', 'def'], 'zero'], + }, + ])( + 'should support the expression with slicing: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + baz: [ + { + bar: 'abc', + }, + { + bar: 'def', + }, + ], + qux: ['zero'], + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.[baz[*].[bar, boo], qux[0]]', + expected: [ + [ + ['a', 'c'], + ['d', 'f'], + ], + 'zero', + ], + }, + ])( + 'should support the expression with wildcard slicing: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + baz: [ + { + bar: 'a', + bam: 'b', + boo: 'c', + }, + { + bar: 'd', + bam: 'e', + boo: 'f', + }, + ], + qux: ['zero'], + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.[baz[*].not_there || baz[*].bar, qux[0]]', + expected: [['a', 'd'], 'zero'], + }, + ])( + 'should support multiselect with inexistent values: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + baz: [ + { + bar: 'a', + bam: 'b', + boo: 'c', + }, + { + bar: 'd', + bam: 'e', + boo: 'f', + }, + ], + qux: ['zero'], + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Nested multiselect', + expression: '[[*],*]', + expected: [null, ['object']], + }, + ])( + 'should support nested multiselect: $expression', + ({ expression, expected }) => { + // Prepare + const data = { type: 'object' }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '[[*]]', + expected: [[]], + }, + ])( + 'should handle nested multiselect with empty arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data: string[] = []; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/compliance/pipe.test.ts b/packages/jmespath/tests/unit/compliance/pipe.test.ts new file mode 100644 index 0000000000..e1ad54dfe6 --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/pipe.test.ts @@ -0,0 +1,187 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/pipe + */ +import { search } from '../../../src/index.js'; + +describe('Pipe expressions tests', () => { + it.each([ + { + expression: 'foo.*.baz | [0]', + expected: 'subkey', + }, + { + expression: 'foo.*.baz | [1]', + expected: 'subkey', + }, + { + expression: 'foo.*.baz | [2]', + expected: 'subkey', + }, + { + expression: 'foo.bar.* | [0]', + expected: 'subkey', + }, + { + expression: 'foo.*.notbaz | [*]', + expected: [ + ['a', 'b', 'c'], + ['a', 'b', 'c'], + ], + }, + { + expression: '{"a": foo.bar, "b": foo.other} | *.baz', + expected: ['subkey', 'subkey'], + }, + ])( + 'should support piping a multi-level nested object with arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: { + baz: 'subkey', + }, + other: { + baz: 'subkey', + }, + other2: { + baz: 'subkey', + }, + other3: { + notbaz: ['a', 'b', 'c'], + }, + other4: { + notbaz: ['a', 'b', 'c'], + }, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo | bar', + expected: { baz: 'one' }, + }, + { + expression: 'foo | bar | baz', + expected: 'one', + }, + { + expression: 'foo|bar| baz', + expected: 'one', + }, + { + expression: 'not_there | [0]', + expected: null, + }, + { + expression: 'not_there | [0]', + expected: null, + }, + { + expression: '[foo.bar, foo.other] | [0]', + expected: { baz: 'one' }, + }, + { + expression: '{"a": foo.bar, "b": foo.other} | a', + expected: { baz: 'one' }, + }, + { + expression: '{"a": foo.bar, "b": foo.other} | b', + expected: { baz: 'two' }, + }, + { + expression: 'foo.bam || foo.bar | baz', + expected: 'one', + }, + { + expression: 'foo | not_there || bar', + expected: { baz: 'one' }, + }, + ])( + 'should support piping with boolean conditions: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: { + baz: 'one', + }, + other: { + baz: 'two', + }, + other2: { + baz: 'three', + }, + other3: { + notbaz: ['a', 'b', 'c'], + }, + other4: { + notbaz: ['d', 'e', 'f'], + }, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*].bar[*] | [0][0]', + expected: { baz: 'one' }, + }, + { + expression: '`null`|[@]', + expected: null, + }, + ])( + 'should support piping with wildcard and current operators: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { + bar: [ + { + baz: 'one', + }, + { + baz: 'two', + }, + ], + }, + { + bar: [ + { + baz: 'three', + }, + { + baz: 'four', + }, + ], + }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/compliance/slice.test.ts b/packages/jmespath/tests/unit/compliance/slice.test.ts new file mode 100644 index 0000000000..5221effdaa --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/slice.test.ts @@ -0,0 +1,243 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/slice + */ +import { search } from '../../../src/index.js'; + +describe('Slices tests', () => { + it.each([ + { + expression: 'bar[0:10]', + expected: null, + }, + { + expression: 'foo[0:10:1]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[0:10]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[0:10:]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[0::1]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[0::]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[0:]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[:10:1]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[::1]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[:10:]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[::]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[:]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[1:9]', + expected: [1, 2, 3, 4, 5, 6, 7, 8], + }, + { + expression: 'foo[0:10:2]', + expected: [0, 2, 4, 6, 8], + }, + { + expression: 'foo[5:]', + expected: [5, 6, 7, 8, 9], + }, + { + expression: 'foo[5::2]', + expected: [5, 7, 9], + }, + { + expression: 'foo[::2]', + expected: [0, 2, 4, 6, 8], + }, + { + expression: 'foo[::-1]', + expected: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0], + }, + { + expression: 'foo[1::2]', + expected: [1, 3, 5, 7, 9], + }, + { + expression: 'foo[10:0:-1]', + expected: [9, 8, 7, 6, 5, 4, 3, 2, 1], + }, + { + expression: 'foo[10:5:-1]', + expected: [9, 8, 7, 6], + }, + { + expression: 'foo[8:2:-2]', + expected: [8, 6, 4], + }, + { + expression: 'foo[0:20]', + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + { + expression: 'foo[10:-20:-1]', + expected: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0], + }, + { + expression: 'foo[10:-20]', + expected: [], + }, + { + expression: 'foo[-4:-1]', + expected: [6, 7, 8], + }, + { + expression: 'foo[:-5:-1]', + expected: [9, 8, 7, 6], + }, + ])( + 'should support slicing arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + bar: { + baz: 1, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[8:2:0]', + error: 'Invalid slice, step cannot be 0', + }, + { + expression: 'foo[8:2:0:1]', + error: + 'Invalid jmespath expression: parse error at column 9, found unexpected token ":" (colon) in expression: foo[8:2:0:1]', + }, + { + expression: 'foo[8:2&]', + error: + 'Invalid jmespath expression: parse error at column 7, found unexpected token "&" (expref) in expression: foo[8:2&]', + }, + { + expression: 'foo[2:a:3]', + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected token "a" (unquoted_identifier) in expression: foo[2:a:3]', + }, + ])( + 'slicing objects with arrays errors: $expression', + ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + } + ); + + it.each([ + { + expression: 'foo[:2].a', + expected: [1, 2], + }, + { + expression: 'foo[:2].b', + expected: [], + }, + { + expression: 'foo[:2].a.b', + expected: [], + }, + { + expression: 'bar[::-1].a.b', + expected: [3, 2, 1], + }, + { + expression: 'bar[:2].a.b', + expected: [1, 2], + }, + { + expression: 'baz[:2].a', + expected: null, + }, + ])( + 'should support slicing an object with nested arrays with objects in them: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [{ a: 1 }, { a: 2 }, { a: 3 }], + bar: [{ a: { b: 1 } }, { a: { b: 2 } }, { a: { b: 3 } }], + baz: 50, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '[:]', + expected: [{ a: 1 }, { a: 2 }, { a: 3 }], + }, + { + expression: '[:2].a', + expected: [1, 2], + }, + { + expression: '[::-1].a', + expected: [3, 2, 1], + }, + { + expression: '[:2].b', + expected: [], + }, + ])( + 'should support slicing an array with objects in it: $expression', + ({ expression, expected }) => { + // Prepare + const data = [{ a: 1 }, { a: 2 }, { a: 3 }]; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/compliance/syntax.test.ts b/packages/jmespath/tests/unit/compliance/syntax.test.ts new file mode 100644 index 0000000000..bd7834b63f --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/syntax.test.ts @@ -0,0 +1,887 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/syntax + */ +import { search } from '../../../src/index.js'; + +describe('Syntax tests', () => { + it.each([ + { + expression: 'foo.bar', + expected: null, + }, + { + expression: 'foo', + expected: null, + }, + ])('should support dot syntax: $expression', ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: 'foo.1', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "1" (number) in expression: foo.1', + }, + { + expression: 'foo.-11', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "-11" (number) in expression: foo.-11', + }, + { + expression: 'foo.', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected end of expression (EOF) in expression: foo.', + }, + { + expression: '.foo', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token "." (dot) in expression: .foo', + }, + { + expression: 'foo..bar', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "." (dot) in expression: foo..bar', + }, + { + expression: 'foo.bar.', + error: + 'Invalid jmespath expression: parse error at column 8, found unexpected end of expression (EOF) in expression: foo.', + }, + { + expression: 'foo[.]', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "." (dot) in expression: foo[.]', + }, + ])('dot syntax errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: '.', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token "." (dot) in expression: .', + }, + { + expression: ':', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token ":" (colon) in expression: :', + }, + { + expression: ',', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token "," (comma) in expression: ,', + }, + { + expression: ']', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token "]" (rbracket) in expression: ]', + }, + { + expression: '[', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) in expression: [', + }, + { + expression: '}', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token "}" (rbrace) in expression: }', + }, + { + expression: '{', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) in expression: {', + }, + { + expression: ')', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token ")" (rparen) in expression: )', + }, + { + expression: '(', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) in expression: (', + }, + { + expression: '((&', + error: + 'Invalid jmespath expression: parse error at column 3, found unexpected end of expression (EOF) in expression: ((&', + }, + { + expression: 'a[', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected end of expression (EOF) in expression: a[', + }, + { + expression: 'a]', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected token "]" (rbracket) in expression: a]', + }, + { + expression: 'a][', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected token "]" (rbracket) in expression: a]', + }, + { + expression: '!', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected end of expression (EOF) in expression: !', + }, + ])('simple token errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: '![!(!', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected end of expression (EOF) in expression: ![!(!', + }, + ])('boolean token errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: '*', + expected: ['object'], + }, + { + expression: '*.*', + expected: [], + }, + { + expression: '*.foo', + expected: [], + }, + { + expression: '*[0]', + expected: [], + }, + ])( + 'should support wildcard syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '.*', + error: + 'Invalid jmespath expression: parse error at column 0, found unexpected token "." (dot) in expression: .*', + }, + { + expression: '*foo', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected token "foo" (unquoted_identifier) in expression: *foo', + }, + { + expression: '*0', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected token "0" (number) in expression: *0', + }, + { + expression: 'foo[*]bar', + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected token "bar" (unquoted_identifier) in expression: foo[*]bar', + }, + { + expression: 'foo[*]*', + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected token "*" (star) in expression: foo[*]*', + }, + ])('wildcard token errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: '[]', + expected: null, + }, + ])( + 'should support flatten syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '[0]', + expected: null, + }, + { + expression: '[*]', + expected: null, + }, + { + expression: '*.["0"]', + expected: [[null]], + }, + { + expression: '[*].bar', + expected: null, + }, + { + expression: '[*][0]', + expected: null, + }, + ])('simple bracket syntax: $expression', ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: '*.[0]', + error: + 'Invalid jmespath expression: parse error at column 3, found unexpected token "0" (number) in expression: *.[0]', + }, + { + expression: 'foo[#]', + error: + 'Bad jmespath expression: unknown token "#" at column 4 in expression: foo[#]', + }, + ])('simple breacket errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'foo[0]', + expected: null, + }, + { + expression: 'foo.[*]', + expected: null, + }, + { + comment: 'Valid multi-select of a hash using an identifier index', + expression: 'foo.[abc]', + expected: null, + }, + { + comment: 'Valid multi-select of a hash', + expression: 'foo.[abc, def]', + expected: null, + }, + ])( + 'should support multi-select list syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'Valid multi-select of a list', + expression: 'foo[0, 1]', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) in expression: foo[0, 1]', + }, + { + expression: 'foo.[0]', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "0" (number) in expression: foo.[0]', + }, + { + comment: 'Multi-select of a list with trailing comma', + expression: 'foo[0, ]', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) in expression: foo[0, ]', + }, + { + comment: 'Multi-select of a list with trailing comma and no close', + expression: 'foo[0,', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) in expression: foo[0,', + }, + { + comment: 'Multi-select of a list with trailing comma and no close', + expression: 'foo.[a', + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected end of expression (EOF) in expression: foo.[a', + }, + { + comment: 'Multi-select of a list with extra comma', + expression: 'foo[0,, 1]', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "," (comma) in expression: foo[0,, 1]', + }, + { + comment: 'Multi-select of a list using an identifier index', + expression: 'foo[abc]', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) in expression: foo[abc]', + }, + { + comment: 'Multi-select of a list using identifier indices', + expression: 'foo[abc, def]', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) in expression: foo[abc, def]', + }, + { + comment: 'Multi-select of a list using an identifier index', + expression: 'foo[abc, 1]', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) in expression: foo[abc, 1]', + }, + { + comment: + 'Multi-select of a list using an identifier index with trailing comma', + expression: 'foo[abc, ]', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "abc" (unquoted_identifier) in expression: foo[abc, ]', + }, + { + comment: 'Multi-select of a hash using a numeric index', + expression: 'foo.[abc, 1]', + error: + 'Invalid jmespath expression: parse error at column 10, found unexpected token "1" (number) in expression: foo.[abc, 1]', + }, + { + comment: 'Multi-select of a hash with a trailing comma', + expression: 'foo.[abc, ]', + error: + 'Invalid jmespath expression: parse error at column 10, found unexpected token "]" (rbracket) in expression: foo.[abc, ]', + }, + { + comment: 'Multi-select of a hash with extra commas', + expression: 'foo.[abc,, def]', + error: + 'Invalid jmespath expression: parse error at column 9, found unexpected token "," (comma) in expression: foo.[abc,, def]', + }, + { + comment: 'Multi-select of a hash using number indices', + expression: 'foo.[0, 1]', + error: + 'Invalid jmespath expression: parse error at column 5, found unexpected token "0" (number) in expression: foo.[0, 1]', + }, + ])('multi-select list errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + comment: 'Valid multi-select hash extraction', + expression: 'a.{foo: bar}', + expected: null, + }, + { + comment: 'Valid multi-select hash extraction', + expression: 'a.{foo: bar, baz: bam}', + expected: null, + }, + { + comment: 'Nested multi select', + expression: '{"\\\\":{" ":*}}', + expected: { + '\\': { + ' ': ['object'], + }, + }, + }, + ])( + 'should support multy-select hash syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { type: 'object' }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + comment: 'No key or value', + expression: 'a{}', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "}" (rbrace) in expression: a{}', + }, + { + comment: 'No closing token', + expression: 'a{', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected end of expression (EOF) in expression: a{', + }, + { + comment: 'Not a key value pair', + expression: 'a{foo}', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo}', + }, + { + comment: 'Missing value and closing character', + expression: 'a{foo:', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo:', + }, + { + comment: 'Missing closing character', + expression: 'a{foo: 0', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo: 0', + }, + { + comment: 'Missing value', + expression: 'a{foo:}', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo:}', + }, + { + comment: 'Trailing comma and no closing character', + expression: 'a{foo: 0, ', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo: 0, ', + }, + { + comment: 'Missing value with trailing comma', + expression: 'a{foo: ,}', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo: ,}', + }, + { + comment: 'Accessing Array using an identifier', + expression: 'a{foo: bar}', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo: bar}', + }, + { + expression: 'a{foo: 0}', + error: + 'Invalid jmespath expression: parse error at column 2, found unexpected token "foo" (unquoted_identifier) in expression: a{foo: 0}', + }, + { + comment: 'Missing key-value pair', + expression: 'a.{}', + error: + 'Invalid jmespath expression: parse error at column 3, found unexpected token "}" (rbrace) in expression: a.{}', + }, + { + comment: 'Not a key-value pair', + expression: 'a.{foo}', + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected token "}" (rbrace) in expression: a.{foo}', + }, + { + comment: 'Missing value', + expression: 'a.{foo:}', + error: + 'Invalid jmespath expression: parse error at column 7, found unexpected token "}" (rbrace) in expression: a.{foo:}', + }, + { + comment: 'Missing value with trailing comma', + expression: 'a.{foo: ,}', + error: + 'Invalid jmespath expression: parse error at column 8, found unexpected token "," (comma) in expression: a.{foo: ,}', + }, + { + comment: 'Trailing comma', + expression: 'a.{foo: bar, }', + error: + 'Invalid jmespath expression: parse error at column 13, found unexpected token "}" (rbrace) in expression: a.{foo: bar, }', + }, + { + comment: 'Missing key in second key-value pair', + expression: 'a.{foo: bar, baz}', + error: + 'Invalid jmespath expression: parse error at column 16, found unexpected token "}" (rbrace) in expression: a.{foo: bar, baz}', + }, + { + comment: 'Missing value in second key-value pair', + expression: 'a.{foo: bar, baz:}', + error: + 'Invalid jmespath expression: parse error at column 17, found unexpected token "}" (rbrace) in expression: a.{foo: bar, baz:}', + }, + { + comment: 'Trailing comma', + expression: 'a.{foo: bar, baz: bam, }', + error: + 'Invalid jmespath expression: parse error at column 23, found unexpected token "}" (rbrace) in expression: a.{foo: bar, baz: bam, }', + }, + ])('multi-select hash errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'foo || bar', + expected: null, + }, + { + expression: 'foo.[a || b]', + expected: null, + }, + ])( + 'should support boolean OR syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo ||', + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected end of expression (EOF) in expression: foo ||', + }, + { + expression: 'foo.|| bar', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "||" (or) in expression: foo.|| bar', + }, + { + expression: ' || foo', + error: + 'Invalid jmespath expression: parse error at column 1, found unexpected token "||" (or) in expression: || foo', + }, + { + expression: 'foo || || foo', + error: + 'Invalid jmespath expression: parse error at column 7, found unexpected token "||" (or) in expression: foo || || foo', + }, + { + expression: 'foo.[a ||]', + error: + 'Invalid jmespath expression: parse error at column 9, found unexpected token "]" (rbracket) in expression: foo.[a ||]', + }, + { + expression: '"foo', + error: + 'Bad jmespath expression: unknown token ""foo" at column 0 in expression: "foo', + }, + ])('boolean OR errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'foo[?bar==`"baz"`]', + expected: null, + }, + { + expression: 'foo[? bar == `"baz"` ]', + expected: null, + }, + { + expression: 'foo[?a.b.c==d.e.f]', + expected: null, + }, + { + expression: 'foo[?bar==`[0, 1, 2]`]', + expected: null, + }, + { + expression: 'foo[?bar==`["a", "b", "c"]`]', + expected: null, + }, + { + comment: 'Literal char escaped', + expression: 'foo[?bar==`["foo\\`bar"]`]', + expected: null, + }, + { + comment: 'Quoted identifier in filter expression no spaces', + expression: '[?"\\\\">`"foo"`]', + expected: null, + }, + { + comment: 'Quoted identifier in filter expression with spaces', + expression: '[?"\\\\" > `"foo"`]', + expected: null, + }, + ])( + 'should support filter syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[ ?bar==`"baz"`]', + error: + 'Bad jmespath expression: unknown token "?" at column 5 in expression: foo[ ?bar==`"baz"`]', + }, + { + expression: 'foo[?bar==]', + error: + 'Invalid jmespath expression: parse error at column 10, found unexpected token "]" (rbracket) in expression: foo[?bar==]', + }, + { + expression: 'foo[?==]', + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected token "==" (eq) in expression: foo[?==]', + }, + { + expression: 'foo[?==bar]', + error: + 'Invalid jmespath expression: parse error at column 6, found unexpected token "==" (eq) in expression: foo[?==bar]', + }, + { + expression: 'foo[?bar==baz?]', + error: + 'Bad jmespath expression: unknown token "?" at column 13 in expression: foo[?bar==baz?]', + }, + { + comment: 'Literal char not escaped', + expression: 'foo[?bar==`["foo`bar"]`]', + error: + 'Bad jmespath expression: unknown token "["foo" at column 10 in expression: foo[?bar==`["foo`bar"]`]', + }, + { + comment: 'Unknown comparator', + expression: 'foo[?bar<>baz]', + error: + 'Invalid jmespath expression: parse error at column 9, found unexpected token ">" (gt) in expression: foo[?bar<>baz]', + }, + { + comment: 'Unknown comparator', + expression: 'foo[?bar^baz]', + error: + 'Bad jmespath expression: unknown token "^" at column 8 in expression: foo[?bar^baz]', + }, + { + expression: 'foo[bar==baz]', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "bar" (unquoted_identifier) in expression: foo[bar==baz]', + }, + { + expression: 'bar.`"anything"`', + error: + 'Invalid jmespath expression: parse error at column 4, found unexpected token "anything" (literal) in expression: bar.`"anything"`', + }, + { + expression: 'bar.baz.noexists.`"literal"`', + error: + 'Invalid jmespath expression: parse error at column 17, found unexpected token "literal" (literal) in expression: bar.baz.noexists.`"literal"`', + }, + { + comment: 'Literal wildcard projection', + expression: 'foo[*].`"literal"`', + error: + 'Invalid jmespath expression: parse error at column 7, found unexpected token "literal" (literal) in expression: foo[*].`"literal"`', + }, + { + expression: 'foo[*].name.`"literal"`', + error: + 'Invalid jmespath expression: parse error at column 12, found unexpected token "literal" (literal) in expression: foo[*].name.`"literal"`', + }, + { + expression: 'foo[].name.`"literal"`', + error: + 'Invalid jmespath expression: parse error at column 11, found unexpected token "literal" (literal) in expression: foo[].name.`"literal"`', + }, + { + expression: 'foo[].name.`"literal"`.`"subliteral"`', + error: + 'Invalid jmespath expression: parse error at column 11, found unexpected token "literal" (literal) in expression: foo[].name.`"literal"`.`"subliteral"`', + }, + { + comment: 'Projecting a literal onto an empty list', + expression: 'foo[*].name.noexist.`"literal"`', + error: + 'Invalid jmespath expression: parse error at column 20, found unexpected token "literal" (literal) in expression: foo[*].name.noexist.`"literal"`', + }, + { + expression: 'foo[].name.noexist.`"literal"`', + error: + 'Invalid jmespath expression: parse error at column 19, found unexpected token "literal" (literal) in expression: foo[].name.noexist.`"literal"`', + }, + { + expression: 'twolen[*].`"foo"`', + error: + 'Invalid jmespath expression: parse error at column 10, found unexpected token "foo" (literal) in expression: twolen[*].`"foo"`', + }, + { + comment: 'Two level projection of a literal', + expression: 'twolen[*].threelen[*].`"bar"`', + error: + 'Invalid jmespath expression: parse error at column 22, found unexpected token "bar" (literal) in expression: twolen[*].threelen[*].`"bar"`', + }, + { + comment: 'Two level flattened projection of a literal', + expression: 'twolen[].threelen[].`"bar"`', + error: + 'Invalid jmespath expression: parse error at column 20, found unexpected token "bar" (literal) in expression: twolen[].threelen[].`"bar"`', + }, + ])('filter errors: $expression', ({ expression, error }) => { + // Prepare + const data = { + type: 'object', + }; + + // Act & Assess + expect(() => search(expression, data)).toThrow(error); + }); + + it.each([ + { + expression: 'foo', + expected: null, + }, + { + expression: '"foo"', + expected: null, + }, + { + expression: '"\\\\"', + expected: null, + }, + ])('should support identifiers: $expression', ({ expression, expected }) => { + // Prepare + const data = { type: 'object' }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + }); + + it.each([ + { + expression: '*||*|*|*', + expected: null, + }, + { + expression: '*[]||[*]', + expected: [], + }, + { + expression: '[*.*]', + expected: [null], + }, + ])( + 'should support combined syntax: $expression', + ({ expression, expected }) => { + // Prepare + const data: string[] = []; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/compliance/unicode.test.ts b/packages/jmespath/tests/unit/compliance/unicode.test.ts new file mode 100644 index 0000000000..29e0521349 --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/unicode.test.ts @@ -0,0 +1,69 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/unicode + */ +import { search } from '../../../src/index.js'; + +describe('Unicode tests', () => { + it.each([ + { + expression: 'foo[]."✓"', + expected: ['✓', '✗'], + }, + ])( + 'should parse an object with unicode chars as keys and values: $expression', + ({ expression, expected }) => { + // Prepare + const data = { foo: [{ '✓': '✓' }, { '✓': '✗' }] }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '"☯"', + expected: true, + }, + { + expression: '"☃"', + expected: null, + }, + ])( + 'should parse an object with unicode chars as keys: $expression', + ({ expression, expected }) => { + // Prepare + const data = { '☯': true }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '"♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪"', + expected: true, + }, + ])( + 'should parse an object with mulitple unicode chars as keys: $expression', + ({ expression, expected }) => { + // Prepare + const data = { '♪♫•*¨*•.¸¸❤¸¸.•*¨*•♫♪': true }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/compliance/wildcard.test.ts b/packages/jmespath/tests/unit/compliance/wildcard.test.ts new file mode 100644 index 0000000000..c472591008 --- /dev/null +++ b/packages/jmespath/tests/unit/compliance/wildcard.test.ts @@ -0,0 +1,670 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/compliance/wildcard + */ +import { search } from '../../../src/index.js'; + +describe('Wildcard tests', () => { + it.each([ + { + expression: 'foo.*.baz', + expected: ['val', 'val', 'val'], + }, + { + expression: 'foo.bar.*', + expected: ['val'], + }, + { + expression: 'foo.*.notbaz', + expected: [ + ['a', 'b', 'c'], + ['a', 'b', 'c'], + ], + }, + { + expression: 'foo.*.notbaz[0]', + expected: ['a', 'a'], + }, + { + expression: 'foo.*.notbaz[-1]', + expected: ['c', 'c'], + }, + ])( + 'should parse the wildcard operator with an object containing multiple keys at different levels: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: { + baz: 'val', + }, + other: { + baz: 'val', + }, + other2: { + baz: 'val', + }, + other3: { + notbaz: ['a', 'b', 'c'], + }, + other4: { + notbaz: ['a', 'b', 'c'], + }, + other5: { + other: { + a: 1, + b: 1, + c: 1, + }, + }, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.*', + expected: [ + { 'second-1': 'val' }, + { 'second-1': 'val' }, + { 'second-1': 'val' }, + ], + }, + { + expression: 'foo.*.*', + expected: [['val'], ['val'], ['val']], + }, + { + expression: 'foo.*.*.*', + expected: [[], [], []], + }, + { + expression: 'foo.*.*.*.*', + expected: [[], [], []], + }, + ])( + 'should parse the wildcard operator with an object containing keys with hyphens: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + 'first-1': { + 'second-1': 'val', + }, + 'first-2': { + 'second-1': 'val', + }, + 'first-3': { + 'second-1': 'val', + }, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '*.bar', + expected: ['one', 'one'], + }, + ])( + 'should parse the wildcard operator with an object containing multiple keys: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: 'one', + }, + other: { + bar: 'one', + }, + nomatch: { + notbar: 'three', + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '*', + expected: [{ sub1: { foo: 'one' } }, { sub1: { foo: 'one' } }], + }, + { + expression: '*.sub1', + expected: [{ foo: 'one' }, { foo: 'one' }], + }, + { + expression: '*.*', + expected: [[{ foo: 'one' }], [{ foo: 'one' }]], + }, + { + expression: '*.*.foo[]', + expected: ['one', 'one'], + }, + { + expression: '*.sub1.foo', + expected: ['one', 'one'], + }, + ])( + 'should parse the wildcard operator with an object containing nested objects: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + top1: { + sub1: { foo: 'one' }, + }, + top2: { + sub1: { foo: 'one' }, + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*].bar', + expected: ['one', 'two', 'three'], + }, + { + expression: 'foo[*].notbar', + expected: ['four'], + }, + ])( + 'should parse the wildcard operator with an object containing an array of objects: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { bar: 'one' }, + { bar: 'two' }, + { bar: 'three' }, + { notbar: 'four' }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: '[*]', + expected: [ + { bar: 'one' }, + { bar: 'two' }, + { bar: 'three' }, + { notbar: 'four' }, + ], + }, + { + expression: '[*].bar', + expected: ['one', 'two', 'three'], + }, + { + expression: '[*].notbar', + expected: ['four'], + }, + ])( + 'should parse the wildcard operator with an array of objects: $expression', + ({ expression, expected }) => { + // Prepare + const data = [ + { bar: 'one' }, + { bar: 'two' }, + { bar: 'three' }, + { notbar: 'four' }, + ]; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.bar[*].baz', + expected: [ + ['one', 'two', 'three'], + ['four', 'five', 'six'], + ['seven', 'eight', 'nine'], + ], + }, + { + expression: 'foo.bar[*].baz[0]', + expected: ['one', 'four', 'seven'], + }, + { + expression: 'foo.bar[*].baz[1]', + expected: ['two', 'five', 'eight'], + }, + { + expression: 'foo.bar[*].baz[2]', + expected: ['three', 'six', 'nine'], + }, + { + expression: 'foo.bar[*].baz[3]', + expected: [], + }, + ])( + 'should parse the wildcard operator with an object with nested objects containing arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: [ + { baz: ['one', 'two', 'three'] }, + { baz: ['four', 'five', 'six'] }, + { baz: ['seven', 'eight', 'nine'] }, + ], + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo.bar[*]', + expected: [ + ['one', 'two'], + ['three', 'four'], + ], + }, + { + expression: 'foo.bar[0]', + expected: ['one', 'two'], + }, + { + expression: 'foo.bar[0][0]', + expected: 'one', + }, + { + expression: 'foo.bar[0][0][0]', + expected: null, + }, + { + expression: 'foo.bar[0][0][0][0]', + expected: null, + }, + { + expression: 'foo[0][0]', + expected: null, + }, + ])( + 'should parse the wildcard operator with an object with nested arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: { + bar: [ + ['one', 'two'], + ['three', 'four'], + ], + }, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*].bar[*].kind', + expected: [ + ['basic', 'intermediate'], + ['advanced', 'expert'], + ], + }, + { + expression: 'foo[*].bar[0].kind', + expected: ['basic', 'advanced'], + }, + ])( + 'should parse the wildcard operator with an array of objects with nested arrays or strings: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { bar: [{ kind: 'basic' }, { kind: 'intermediate' }] }, + { bar: [{ kind: 'advanced' }, { kind: 'expert' }] }, + { bar: 'string' }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*].bar.kind', + expected: ['basic', 'intermediate', 'advanced', 'expert'], + }, + ])( + 'should parse the wildcard operator with an array of objects: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { bar: { kind: 'basic' } }, + { bar: { kind: 'intermediate' } }, + { bar: { kind: 'advanced' } }, + { bar: { kind: 'expert' } }, + { bar: 'string' }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*].bar[0]', + expected: ['one', 'three', 'five'], + }, + { + expression: 'foo[*].bar[1]', + expected: ['two', 'four'], + }, + { + expression: 'foo[*].bar[2]', + expected: [], + }, + ])( + 'should parse the wildcard operator with an array of objects with arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + { bar: ['one', 'two'] }, + { bar: ['three', 'four'] }, + { bar: ['five'] }, + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*].bar[0]', + expected: [], + }, + ])( + 'should parse the wildcard operator with an array of objects with empty arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [{ bar: [] }, { bar: [] }, { bar: [] }], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*][0]', + expected: ['one', 'three', 'five'], + }, + { + expression: 'foo[*][1]', + expected: ['two', 'four'], + }, + ])( + 'should parse the wildcard operator with an array of arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [['one', 'two'], ['three', 'four'], ['five']], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'foo[*][0]', + expected: [['one', 'two'], ['five', 'six'], ['nine']], + }, + { + expression: 'foo[*][1]', + expected: [['three', 'four'], ['seven', 'eight'], ['ten']], + }, + { + expression: 'foo[*][0][0]', + expected: ['one', 'five', 'nine'], + }, + { + expression: 'foo[*][1][0]', + expected: ['three', 'seven', 'ten'], + }, + { + expression: 'foo[*][0][1]', + expected: ['two', 'six'], + }, + { + expression: 'foo[*][1][1]', + expected: ['four', 'eight'], + }, + { + expression: 'foo[*][2]', + expected: [], + }, + { + expression: 'foo[*][2][2]', + expected: [], + }, + { + expression: 'bar[*]', + expected: null, + }, + { + expression: 'bar[*].baz[*]', + expected: null, + }, + ])( + 'should parse a nested array of arrays: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + foo: [ + [ + ['one', 'two'], + ['three', 'four'], + ], + [ + ['five', 'six'], + ['seven', 'eight'], + ], + [['nine'], ['ten']], + ], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'string[*]', + expected: null, + }, + { + expression: 'hash[*]', + expected: null, + }, + { + expression: 'number[*]', + expected: null, + }, + { + expression: 'nullvalue[*]', + expected: null, + }, + { + expression: 'string[*].foo', + expected: null, + }, + { + expression: 'hash[*].foo', + expected: null, + }, + { + expression: 'number[*].foo', + expected: null, + }, + { + expression: 'nullvalue[*].foo', + expected: null, + }, + { + expression: 'nullvalue[*].foo[*].bar', + expected: null, + }, + ])( + 'should parse an object with different value types: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + string: 'string', + hash: { foo: 'bar', bar: 'baz' }, + number: 23, + nullvalue: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + + it.each([ + { + expression: 'string.*', + expected: null, + }, + { + expression: 'hash.*', + expected: ['val', 'val'], + }, + { + expression: 'number.*', + expected: null, + }, + { + expression: 'array.*', + expected: null, + }, + { + expression: 'nullvalue.*', + expected: null, + }, + ])( + 'should parse an object with different value types: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + string: 'string', + hash: { foo: 'val', bar: 'val' }, + number: 23, + array: [1, 2, 3], + nullvalue: null, + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); + it.each([{ expression: '*[0]', expected: [0, 0] }])( + 'should get the first element of each array: $expression', + ({ expression, expected }) => { + // Prepare + const data = { + a: [0, 1, 2], + b: [0, 1, 2], + }; + + // Act + const result = search(expression, data); + + // Assess + expect(result).toStrictEqual(expected); + } + ); +}); diff --git a/packages/jmespath/tests/unit/index.test.ts b/packages/jmespath/tests/unit/index.test.ts new file mode 100644 index 0000000000..a06f3328d2 --- /dev/null +++ b/packages/jmespath/tests/unit/index.test.ts @@ -0,0 +1,388 @@ +/** + * Test Compliance with the JMESPath specification + * + * @group unit/jmespath/coverage + */ +import { fromBase64 } from '@aws-lambda-powertools/commons/utils/base64'; +import { + search, + EmptyExpressionError, + ArityError, + LexerError, + JMESPathError, + VariadicArityError, +} from '../../src/index.js'; +import { Functions } from '../../src/Functions.js'; +import { Parser } from '../../src/Parser.js'; +import { TreeInterpreter } from '../../src/TreeInterpreter.js'; +import { brotliDecompressSync } from 'node:zlib'; +import { PowertoolsFunctions } from '../../src/PowertoolsFunctions.js'; +import { extractDataFromEnvelope, SQS } from '../../src/envelopes.js'; + +describe('Coverage tests', () => { + // These expressions tests are not part of the compliance suite, but are added to ensure coverage + describe('expressions', () => { + it('throws an error if the index is an invalid value', () => { + // Prepare + const invalidIndexExpression = 'foo.*.notbaz[-a]'; + + // Act & Assess + expect(() => search(invalidIndexExpression, {})).toThrow(LexerError); + }); + + it('throws an error if the expression is not a string', () => { + // Prepare + const notAStringExpression = 3; + + // Act & Assess + expect(() => + search(notAStringExpression as unknown as string, {}) + ).toThrow(EmptyExpressionError); + }); + + it('throws a lexer error when encounteirng a single equal for equality', () => { + // Prepare + const expression = '='; + + // Act & Assess + expect(() => { + search(expression, {}); + }).toThrow(LexerError); + }); + + it('returns null when max_by is called with an empty list', () => { + // Prepare + const expression = 'max_by(@, &foo)'; + + // Act + const result = search(expression, []); + + // Assess + expect(result).toBe(null); + }); + + it('returns null when min_by is called with an empty list', () => { + // Prepare + const expression = 'min_by(@, &foo)'; + + // Act + const result = search(expression, []); + + // Assess + expect(result).toBe(null); + }); + + it('returns the correct max value', () => { + // Prepare + const expression = 'max(@)'; + + // Act + const result = search(expression, ['z', 'b']); + + // Assess + expect(result).toBe('z'); + }); + + it('returns the correct min value', () => { + // Prepare + const expression = 'min(@)'; + + // Act + const result = search(expression, ['z', 'b']); + + // Assess + expect(result).toBe('b'); + }); + }); + + describe('type checking', () => { + class TestFunctions extends Functions { + @TestFunctions.signature({ + argumentsSpecs: [['any'], ['any']], + }) + public funcTest(): void { + return; + } + + @TestFunctions.signature({ + argumentsSpecs: [['any'], ['any']], + variadic: true, + }) + public funcTestArityError(): void { + return; + } + } + + it('throws an arity error if the function is called with the wrong number of arguments', () => { + // Prepare + const expression = 'test(@, @, @)'; + + // Act & Assess + expect(() => + search(expression, {}, { customFunctions: new TestFunctions() }) + ).toThrow(ArityError); + }); + + it('throws an arity error if the function is called with the wrong number of arguments', () => { + // Prepare + const expression = 'test_arity_error(@)'; + + // Act & Assess + expect(() => + search(expression, {}, { customFunctions: new TestFunctions() }) + ).toThrow(VariadicArityError); + }); + }); + + describe('class: Parser', () => { + it('clears the cache when purgeCache is called', () => { + // Prepare + const parser = new Parser(); + + // Act + const parsedResultA = parser.parse('test(@, @)'); + parser.purgeCache(); + const parsedResultB = parser.parse('test(@, @)'); + + // Assess + expect(parsedResultA).not.toBe(parsedResultB); + }); + }); + + describe('class: TreeInterpreter', () => { + it('throws an error when visiting an invalid node', () => { + // Prepare + const interpreter = new TreeInterpreter(); + + // Act & Assess + expect(() => { + interpreter.visit( + { + type: 'invalid', + value: 'invalid', + children: [], + }, + {} + ); + }).toThrow(JMESPathError); + }); + + it('returns null when visiting a field with no value', () => { + // Prepare + const interpreter = new TreeInterpreter(); + + // Act + const result = interpreter.visit( + { + type: 'field', + value: undefined, + children: [], + }, + {} + ); + + // Assess + expect(result).toBe(null); + }); + + it('throws an error when receiving an invalid comparator', () => { + // Prepare + const interpreter = new TreeInterpreter(); + + // Act & Assess + expect(() => { + interpreter.visit( + { + type: 'comparator', + value: 'invalid', + children: [ + { + type: 'field', + value: 'a', + children: [], + }, + { + type: 'field', + value: 'b', + children: [], + }, + ], + }, + {} + ); + }).toThrow(JMESPathError); + }); + + it('throws an error when receiving a function with an invalid name', () => { + // Prepare + const interpreter = new TreeInterpreter(); + + // Act & Assess + expect(() => { + interpreter.visit( + { + type: 'function_expression', + value: 1, // function name must be a string + children: [], + }, + {} + ); + }).toThrow(JMESPathError); + }); + + it('throws an error when receiving an index expression with an invalid index', () => { + // Prepare + const interpreter = new TreeInterpreter(); + + // Act & Assess + expect(() => { + interpreter.visit( + { + type: 'index', + value: 'invalid', // index must be a number + children: [], + }, + [] + ); + }).toThrow(JMESPathError); + }); + + it('returns an empty array when slicing an empty array', () => { + // Prepare + const interpreter = new TreeInterpreter(); + + // Act + const result = interpreter.visit( + { + type: 'slice', + value: [0, 0, 1], + children: [], + }, + [] + ); + + // Assess + expect(result).toEqual([]); + }); + }); + + describe('function: extractDataFromEnvelope', () => { + it('extracts the data from a known envelope', () => { + // Prepare + const event = { + Records: [ + { + body: '{"foo":"bar"}', + }, + ], + }; + + // Act + const data = extractDataFromEnvelope(event, SQS); + + // Assess + expect(data).toStrictEqual([{ foo: 'bar' }]); + }); + }); + + describe('class: PowertoolsFunctions', () => { + it('decodes a json string', () => { + // Prepare + const event = '{"user":"xyz","product_id":"123456789"}'; + + // Act + const data = extractDataFromEnvelope(event, 'powertools_json(@)', { + customFunctions: new PowertoolsFunctions(), + }); + + // Assess + expect(data).toStrictEqual({ + user: 'xyz', + product_id: '123456789', + }); + }); + + it('decodes a base64 gzip string', () => { + // Prepare + const event = { + payload: + 'H4sIACZAXl8C/52PzUrEMBhFX2UILpX8tPbHXWHqIOiq3Q1F0ubrWEiakqTWofTdTYYB0YWL2d5zvnuTFellBIOedoiyKH5M0iwnlKH7HZL6dDB6ngLDfLFYctUKjie9gHFaS/sAX1xNEq525QxwFXRGGMEkx4Th491rUZdV3YiIZ6Ljfd+lfSyAtZloacQgAkqSJCGhxM6t7cwwuUGPz4N0YKyvO6I9WDeMPMSo8Z4Ca/kJ6vMEYW5f1MX7W1lVxaG8vqX8hNFdjlc0iCBBSF4ERT/3Pl7RbMGMXF2KZMh/C+gDpNS7RRsp0OaRGzx0/t8e0jgmcczyLCWEePhni/23JWalzjdu0a3ZvgEaNLXeugEAAA==', + }; + + // Act + const data = extractDataFromEnvelope( + event, + 'powertools_base64_gzip(payload) | powertools_json(@).logGroup', + { + customFunctions: new PowertoolsFunctions(), + } + ); + + // Assess + expect(data).toStrictEqual('/aws/lambda/powertools-example'); + }); + + it('decodes a base64 string', () => { + // Prepare + const event = { + payload: + 'eyJ1c2VyX2lkIjogMTIzLCAicHJvZHVjdF9pZCI6IDEsICJxdWFudGl0eSI6IDIsICJwcmljZSI6IDEwLjQwLCAiY3VycmVuY3kiOiAiVVNEIn0=', + }; + + // Act + const data = extractDataFromEnvelope( + event, + 'powertools_json(powertools_base64(payload))', + { + customFunctions: new PowertoolsFunctions(), + } + ); + + // Assess + expect(data).toStrictEqual({ + user_id: 123, + product_id: 1, + quantity: 2, + price: 10.4, + currency: 'USD', + }); + }); + + it('uses the custom function extending the powertools custom functions', () => { + // Prepare + class CustomFunctions extends PowertoolsFunctions { + public constructor() { + super(); + } + @PowertoolsFunctions.signature({ + argumentsSpecs: [['string']], + }) + public funcDecodeBrotliCompression(value: string): string { + const encoded = fromBase64(value, 'base64'); + const uncompressed = brotliDecompressSync(encoded); + + return uncompressed.toString(); + } + } + const event = { + Records: [ + { + application: 'messaging-app', + datetime: '2022-01-01T00:00:00.000Z', + notification: 'GyYA+AXhZKk/K5DkanoQSTYpSKMwwxXh8DRWVo9A1hLqAQ==', + }, + ], + }; + + // Act + const messages = extractDataFromEnvelope( + event, + 'Records[*].decode_brotli_compression(notification) | [*].powertools_json(@).message', + { customFunctions: new CustomFunctions() } + ); + + // Assess + expect(messages).toStrictEqual(['hello world']); + }); + }); +}); diff --git a/packages/jmespath/typedoc.json b/packages/jmespath/typedoc.json index 4471cee375..da81672090 100644 --- a/packages/jmespath/typedoc.json +++ b/packages/jmespath/typedoc.json @@ -3,8 +3,7 @@ "../../typedoc.base.json" ], "entryPoints": [ - "./src/search.ts", - "./src/errors.ts", + "./src/index.ts", "./src/types.ts", "./src/envelopes.ts", "./src/Functions.ts",