UNPKG

nimma

Version:

Scalable JSONPath engine.

905 lines (852 loc) 22 kB
import * as assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import parse from '../index.mjs'; describe('Parser', () => { it('goessner samples', () => { assert.deepEqual(parse('$.store.book[foo,bar].author'), [ { type: 'MemberExpression', value: 'store', deep: false, }, { type: 'MemberExpression', value: 'book', deep: false, }, { type: 'MultipleMemberExpression', value: ['foo', 'bar'], deep: false, }, { type: 'MemberExpression', value: 'author', deep: false, }, ]); assert.deepEqual(parse('$.store.book[*].author'), [ { type: 'MemberExpression', value: 'store', deep: false, }, { type: 'MemberExpression', value: 'book', deep: false, }, { type: 'WildcardExpression', deep: false, }, { type: 'MemberExpression', value: 'author', deep: false, }, ]); }); it('slice expressions', () => { assert.deepEqual(parse('$..book[:2]'), [ { type: 'MemberExpression', value: 'book', deep: true, }, { type: 'SliceExpression', value: [0, 2, 1], deep: false, }, ]); assert.deepEqual(parse('$..book[0:2]'), [ { type: 'MemberExpression', value: 'book', deep: true, }, { type: 'SliceExpression', deep: false, value: [0, 2, 1], }, ]); assert.deepEqual(parse('$..book[-1:]'), [ { type: 'MemberExpression', value: 'book', deep: true, }, { type: 'SliceExpression', value: [-1, Infinity, 1], deep: false, }, ]); }); it('extra', () => { assert.deepEqual(parse("$.test1.test2['test3.test4.test5']"), [ { type: 'MemberExpression', value: 'test1', deep: false, }, { type: 'MemberExpression', value: 'test2', deep: false, }, { type: 'MemberExpression', value: 'test3.test4.test5', deep: false, }, ]); assert.deepEqual(parse('$..book[(@.length-1)]'), [ { type: 'MemberExpression', value: 'book', deep: true, }, { type: 'SliceExpression', value: [-1, Infinity, 1], deep: false, }, ]); assert.deepEqual(parse('$..book'), [ { type: 'MemberExpression', value: 'book', deep: true, }, ]); assert.deepEqual(parse('$.store["book"]'), [ { type: 'MemberExpression', value: 'store', deep: false, }, { type: 'MemberExpression', value: 'book', deep: false, }, ]); assert.deepEqual(parse('$.books[0,1]'), [ { type: 'MemberExpression', value: 'books', deep: false, }, { type: 'MultipleMemberExpression', value: [0, 1], deep: false, }, ]); assert.deepEqual(parse('$.books[?(@.isbn)]'), [ { type: 'MemberExpression', value: 'books', deep: false, }, { type: 'ScriptFilterExpression', raw: '(@.isbn)', value: { type: 'MemberExpression', computed: false, object: { type: 'Identifier', name: '@', }, property: { type: 'Identifier', name: 'isbn', }, }, deep: false, }, ]); assert.deepEqual(parse("$.'application/json'"), [ { type: 'MemberExpression', value: 'application/json', deep: false, }, ]); }); it('modifiers', () => { assert.deepEqual(parse('$.info^~'), [ { type: 'MemberExpression', value: 'info', deep: false, }, { type: 'ParentExpression', }, { type: 'KeyExpression', }, ]); assert.deepEqual(parse('$.name^^'), [ { type: 'MemberExpression', value: 'name', deep: false, }, { type: 'ParentExpression', }, { type: 'ParentExpression', }, ]); }); it('filter expressions', () => { assert.deepEqual(parse('$[(@.length-1)]'), [ { type: 'SliceExpression', value: [-1, Infinity, 1], deep: false, }, ]); assert.deepEqual(parse('$[( @.length - 2 )]'), [ { type: 'SliceExpression', value: [-2, Infinity, 1], deep: false, }, ]); assert.deepEqual(parse('$[( @[ "length" ] - 10 )]'), [ { type: 'SliceExpression', value: [-10, Infinity, 1], deep: false, }, ]); assert.deepEqual(parse('$[( @["length"] - 5 )]'), [ { type: 'SliceExpression', value: [-5, Infinity, 1], deep: false, }, ]); }); it('script filter expressions', () => { assert.deepEqual(parse('$[?(@property === "@.schema")]'), [ { type: 'ScriptFilterExpression', raw: '(@property === "@.schema")', value: { type: 'BinaryExpression', left: { type: 'Identifier', name: '@property', }, operator: '===', right: { type: 'Literal', raw: '"@.schema"', value: '@.schema', }, }, deep: false, }, ]); assert.deepEqual(parse('$[?(match(@.book[0].isbn, "123"))]'), [ { type: 'ScriptFilterExpression', raw: '(match(@.book[0].isbn, "123"))', value: { type: 'CallExpression', arguments: [ { type: 'MemberExpression', object: { type: 'MemberExpression', object: { type: 'MemberExpression', object: { type: 'Identifier', name: '@', }, property: { type: 'Identifier', name: 'book', }, computed: false, }, property: { type: 'Literal', raw: '0', value: 0, }, computed: true, }, computed: false, property: { type: 'Identifier', name: 'isbn', }, }, { type: 'Literal', raw: '"123"', value: '123', }, ], callee: { type: 'Identifier', name: 'match', }, }, deep: false, }, ]); assert.deepEqual( parse('$["книги"][?(match(@.название, "Мастер и Маргарита"))]'), [ { deep: false, type: 'MemberExpression', value: 'книги', }, { type: 'ScriptFilterExpression', raw: '(match(@.название, "Мастер и Маргарита"))', value: { type: 'CallExpression', arguments: [ { type: 'MemberExpression', object: { name: '@', type: 'Identifier', }, property: { name: 'название', type: 'Identifier', }, computed: false, }, { type: 'Literal', raw: '"Мастер и Маргарита"', value: 'Мастер и Маргарита', }, ], callee: { type: 'Identifier', name: 'match', }, }, deep: false, }, ], ); assert.deepEqual(parse('$..address.street[?(@.number > 20)]'), [ { type: 'MemberExpression', deep: true, value: 'address', }, { type: 'MemberExpression', value: 'street', deep: false, }, { type: 'ScriptFilterExpression', raw: '(@.number > 20)', value: { type: 'BinaryExpression', left: { type: 'MemberExpression', object: { type: 'Identifier', name: '@', }, property: { type: 'Identifier', name: 'number', }, computed: false, }, operator: '>', right: { type: 'Literal', raw: '20', value: 20, }, }, deep: false, }, ]); assert.deepEqual(parse('$[?(/^([45]XX)$|^2/i.test(@.value))]'), [ { type: 'ScriptFilterExpression', raw: '(/^([45]XX)$|^2/i.test(@.value))', value: { type: 'CallExpression', callee: { type: 'MemberExpression', computed: false, object: { type: 'Literal', raw: '/^([45]XX)$|^2/i', value: /^([45]XX)$|^2/i, }, property: { type: 'Identifier', name: 'test', }, }, arguments: [ { type: 'MemberExpression', computed: false, object: { type: 'Identifier', name: '@', }, property: { type: 'Identifier', name: 'value', }, }, ], }, deep: false, }, ]); }); it('all parent', () => { // '$..', '$..^', '$..~' assert.deepEqual(parse('$..'), [ { type: 'AllParentExpression', }, ]); assert.deepEqual(parse('$..^'), [ { type: 'AllParentExpression', }, { type: 'ParentExpression', }, ]); assert.deepEqual(parse('$..~'), [ { type: 'AllParentExpression', }, { type: 'KeyExpression', }, ]); assert.deepEqual(parse('$..^^'), [ { type: 'AllParentExpression', }, { type: 'ParentExpression', }, { type: 'ParentExpression', }, ]); assert.deepEqual(parse('$..^^~'), [ { type: 'AllParentExpression', }, { type: 'ParentExpression', }, { type: 'ParentExpression', }, { type: 'KeyExpression', }, ]); }); for (const member of ['$$ref', '$ref', '0abc', 'bar-baz']) { it(`parses $..${member}`, () => { assert.deepEqual(parse(`$..${member}`), [ { type: 'MemberExpression', value: member, deep: true, }, ]); }); } it.only('parses shorthand expressions', () => { assert.deepEqual(parse('$.components.schemas..@@schema(0)'), [ { type: 'MemberExpression', value: 'components', deep: false, }, { type: 'MemberExpression', value: 'schemas', deep: false, }, { type: 'CustomShorthandExpression', value: 'schema', arguments: [0], deep: true, }, ]); assert.deepEqual(parse('$.components.schemas.@@schema(2)'), [ { type: 'MemberExpression', value: 'components', deep: false, }, { type: 'MemberExpression', value: 'schemas', deep: false, }, { type: 'CustomShorthandExpression', value: 'schema', arguments: [2], deep: false, }, ]); }); for (const expr of ['$.[enum]', '$.[?(@.enum)]', '$.foo.[bar,baz]']) { it(`given "${expr}", should should treat .[ as ..`, () => { assert.deepEqual( parse(expr), parse(expr.replace(/\.\?=(\[\?\()|{\.}1\[/g, '..')), ); }); } it('skips whitespaces', () => { assert.deepEqual(parse('$.[ name ] [?( @.abc )]\t ..@@test ( 5 )'), [ { type: 'MemberExpression', value: 'name', deep: true, }, { type: 'ScriptFilterExpression', raw: '( @.abc )', value: { type: 'MemberExpression', computed: false, object: { type: 'Identifier', name: '@', }, property: { type: 'Identifier', name: 'abc', }, }, deep: false, }, { type: 'CustomShorthandExpression', value: 'test', arguments: [5], deep: true, }, ]); }); it('handles escapable characters', () => { assert.deepEqual(parse('$[?(@ ~= "^P\\\\.")]'), [ { type: 'ScriptFilterExpression', raw: '(@ ~= "^P\\\\.")', value: { type: 'BinaryExpression', left: { type: 'Identifier', name: '@', }, operator: '~=', right: { type: 'Literal', raw: '"^P\\\\."', value: '^P\\.', }, }, deep: false, }, ]); assert.deepEqual(parse(`$["'name\\"'","test\\\\",'"a']`), [ { type: 'MultipleMemberExpression', value: ["'name\"'", 'test\\', '"a'], deep: false, }, ]); assert.deepEqual(parse(`$["\\v\\ntest\\b"]`), [ { deep: false, type: 'MemberExpression', value: '\v\ntest\b', }, ]); assert.deepEqual(parse(`$["\\f\\r\\t"]`), [ { deep: false, type: 'MemberExpression', value: '\f\r\t', }, ]); }); describe('invalid expressions', () => { it('empty expression or does not start with $', () => { assert.throws( () => parse(''), SyntaxError('Expected "$" but end of input found.'), ); assert.throws( () => parse('a'), SyntaxError('Expected "$" but "a" found.'), ); assert.throws( () => parse(' $'), SyntaxError('Expected "$" but " " found.'), ); }); it('invalid member expression', () => { assert.throws( () => parse('$info'), SyntaxError( 'Expected ".", "..", "^", "~", or end of input but "i" found at 1.', ), ); assert.throws( () => parse('$.'), SyntaxError('Expected valid name but end of input found at 2.'), ); }); it('key expression used in the wrong place', () => { assert.throws( () => parse('$.name~.a'), SyntaxError('Expected "^", "~", or end of input but "." found at 7.'), ); }); it('unclosed quotes', () => { assert.throws( () => parse('$.name["a]'), SyntaxError(`Expected """ but end of input found at 10.`), ); assert.throws( () => parse('$.name["\']'), SyntaxError(`Expected """ but end of input found at 10.`), ); }); it('invalid step in slice expressions', () => { assert.throws( () => parse('$.name[::test]'), SyntaxError('Expected "-" or [0-9] but "t" found at 9.'), ); assert.throws( () => parse('$.name[::-]'), SyntaxError('Expected [0-9] but "]" found at 10.'), ); }); it('invalid shorthands', () => { assert.throws( () => parse('$..@()'), SyntaxError('Expected [a-z] but "(" found at 4.'), ); assert.throws( () => parse('$..@1()'), SyntaxError('Expected [a-z] but "1" found at 4.'), ); assert.throws( () => parse('$..@@()'), SyntaxError('Expected [a-z] but "(" found at 5.'), ); assert.throws( () => parse('$..@@1()'), SyntaxError('Expected [a-z] but "1" found at 5.'), ); assert.throws( () => parse('$..@@test)'), SyntaxError('Expected "(" but ")" found at 9.'), ); assert.throws( () => parse('$..@@test('), SyntaxError('Expected [0-9] but end of input found at 10.'), ); assert.throws( () => parse('$..@@test(5'), SyntaxError('Expected ")" but end of input found at 11.'), ); assert.throws( () => parse('$..@@test()'), SyntaxError('Expected [0-9] but ")" found at 10.'), ); assert.throws( () => parse('$..@'), SyntaxError('Expected [a-z] but end of input found at 4.'), ); }); it('invalid filter expressions', () => { assert.throws( () => parse('$[('), SyntaxError('Expected "@" but end of input found at 3.'), ); assert.throws( () => parse('$[(@'), SyntaxError('Expected "." or "[" but end of input found at 4.'), ); assert.throws( () => parse('$[(@.len - 1)]'), SyntaxError('Expected "length" but "len - " found at 11.'), ); assert.throws( () => parse('$[(@length - 1)]'), SyntaxError('Expected "." or "[" but "l" found at 4.'), ); assert.throws( () => parse('$[(@[length]-2)]'), SyntaxError(`Expected """ or "'" at 5.`), ); assert.throws( () => parse('$[(@.length + 1))'), SyntaxError('Expected "-" but "+" found at 12.'), ); assert.throws( () => parse('$[(@.length - -5))'), SyntaxError('Expected positive number but "-5" found at 14.'), ); assert.throws( () => parse('$[(@.length - 0))'), SyntaxError('Expected positive number but "0" found at 14.'), ); }); it('unclosed brackets', () => { assert.throws( () => parse('$.name['), SyntaxError('Unexpected end of input at 7.'), ); assert.throws( () => parse('$.name[0'), SyntaxError('Expected "]" but end of input found at 8.'), ); assert.throws( () => parse('$.store["[name]"'), SyntaxError('Expected "]" but end of input found at 16.'), ); }); describe('invalid script filter expressions', () => { it('unclosed parentheses', () => { assert.throws( () => parse('$[?(@.length - 1]'), SyntaxError('Expected ")" but "]" found at 16.'), ); assert.throws( () => parse('$[?(@.length - 1))'), SyntaxError('Expected "]" but ")" found at 17.'), ); // args assert.throws( () => parse('$[?(abc(1]'), SyntaxError('Expected ")" or "," but "]" found at 9.'), ); assert.throws( () => parse('$[?(abc(a]'), SyntaxError('Expected ")" or "," but "]" found at 9.'), ); assert.throws( () => parse('$[?(abc(1 / (2 + 2) ]'), SyntaxError('Expected ")" or "," but "]" found at 20.'), ); // groups assert.throws( () => parse('$[?(1 / (2 + 2) ]'), SyntaxError('Expected ")" but "]" found at 16.'), ); }); it('unclosed brackets', () => { assert.throws( () => parse('$[?(@[length)]'), SyntaxError('Expected "]" but ")" found at 12.'), ); assert.throws( () => parse('$[?(@.abc == [1)]'), SyntaxError('Expected "]" or "," but ")" found at 15.'), ); }); it('redundant comma in arguments', () => { assert.throws( () => parse('$[?(abc(2,))]'), SyntaxError('Expected ")" but "," found at 11.'), ); assert.throws( () => parse('$[?(abc(2,,))]'), SyntaxError('Unexpected "," at 11.'), ); assert.throws( () => parse('$[?(abc(,a))]'), SyntaxError('Unexpected "," at 9.'), ); assert.throws( () => parse('$[?(abc(,))]'), SyntaxError('Unexpected "," at 9.'), ); }); it('missing argument in unary expression', () => { assert.throws( () => parse('$[?(@.value != -)]'), SyntaxError('Expected argument but ")" found at 16.'), ); assert.throws( () => parse('$[?(@.value == + )]'), SyntaxError('Expected argument but ")" found at 20.'), ); }); it('missing side in binary expression', () => { assert.throws( () => parse('$[?(@.value == 1 + )]'), SyntaxError('Expected expression after "+" at 19.'), ); assert.throws( () => parse('$[?(@.value == 2 + 5 * )]'), SyntaxError('Expected expression after "*" at 26.'), ); }); it('identifiers starting with a number', () => { assert.throws( () => parse('$[?(@.value == 1abc)]'), SyntaxError('Expected [0-9] or "." but "a" found at 16.'), ); assert.throws( () => parse('$[?(@.value == .abc)]'), SyntaxError('Expected [0-9] but "a" found at 16.'), ); }); it('invalid numbers', () => { assert.throws( () => parse('$[?(@.value == 1.2.3)]'), SyntaxError('Unexpected "." at 18.'), ); assert.throws( () => parse('$[?(@.value == .)]'), SyntaxError('Unexpected "." at 16.'), ); }); it('invalid identifiers', () => { assert.throws( () => parse('$[?(@.#value == 123)]'), SyntaxError('Expected a valid identifier char but "#" found at 6.'), ); }); }); }); });