From 1becc8379dffc03218b32342b6261a99d40c06d9 Mon Sep 17 00:00:00 2001 From: Gajus Kuizinas Date: Wed, 28 Sep 2022 19:13:46 -0500 Subject: [PATCH] feat: convert field to object {name:} BREAKING CHANGE: changes field format --- src/grammar.ne | 14 +-- src/grammar.ts | 14 +-- src/hydrateAst.ts | 6 +- src/internalFilter.ts | 29 ++--- src/types.ts | 6 +- test/liqe/hydrateAst.ts | 38 +++++-- test/liqe/parse.ts | 228 +++++++++++++++++++++++++++++----------- 7 files changed, 236 insertions(+), 99 deletions(-) diff --git a/src/grammar.ne b/src/grammar.ne index 25b0446..3adfc33 100644 --- a/src/grammar.ne +++ b/src/grammar.ne @@ -53,8 +53,10 @@ const range = ( minInclusive, maxInclusive) => { const field = d => { return { - field: d[0], - fieldPath: d[0].split('.').filter(Boolean), + field: { + name: d[0].name, + path: d[0].name.split('.').filter(Boolean), + }, ...d[3] } }; @@ -91,12 +93,12 @@ post_boolean_primary -> side -> field ":" _ query {% field %} - | query {% d => ({field: '', ...d[0]}) %} + | query {% d => ({field: {name: ''}, ...d[0]}) %} field -> - [_a-zA-Z$] [a-zA-Z\d_$.]:* {% d => d[0] + d[1].join('') %} - | sqstring {% id %} - | dqstring {% id %} + [_a-zA-Z$] [a-zA-Z\d_$.]:* {% d => ({name: d[0] + d[1].join('')}) %} + | sqstring {% d => ({name: d[0]}) %} + | dqstring {% d => ({name: d[0]}) %} query -> relational_operator _ decimal {% d => ({quoted: false, query: d[2], relationalOperator: d[0][0]}) %} diff --git a/src/grammar.ts b/src/grammar.ts index e9598c5..de1f461 100644 --- a/src/grammar.ts +++ b/src/grammar.ts @@ -51,8 +51,10 @@ const range = ( minInclusive, maxInclusive) => { const field = d => { return { - field: d[0], - fieldPath: d[0].split('.').filter(Boolean), + field: { + name: d[0].name, + path: d[0].name.split('.').filter(Boolean), + }, ...d[3] } }; @@ -224,12 +226,12 @@ const grammar: Grammar = { {"name": "post_boolean_primary", "symbols": [{"literal":"("}, "_", "boolean_primary", "_", {"literal":")"}], "postprocess": d => d[2]}, {"name": "post_boolean_primary", "symbols": ["__", "boolean_primary"], "postprocess": d => d[1]}, {"name": "side", "symbols": ["field", {"literal":":"}, "_", "query"], "postprocess": field}, - {"name": "side", "symbols": ["query"], "postprocess": d => ({field: '', ...d[0]})}, + {"name": "side", "symbols": ["query"], "postprocess": d => ({field: {name: ''}, ...d[0]})}, {"name": "field$ebnf$1", "symbols": []}, {"name": "field$ebnf$1", "symbols": ["field$ebnf$1", /[a-zA-Z\d_$.]/], "postprocess": (d) => d[0].concat([d[1]])}, - {"name": "field", "symbols": [/[_a-zA-Z$]/, "field$ebnf$1"], "postprocess": d => d[0] + d[1].join('')}, - {"name": "field", "symbols": ["sqstring"], "postprocess": id}, - {"name": "field", "symbols": ["dqstring"], "postprocess": id}, + {"name": "field", "symbols": [/[_a-zA-Z$]/, "field$ebnf$1"], "postprocess": d => ({name: d[0] + d[1].join('')})}, + {"name": "field", "symbols": ["sqstring"], "postprocess": d => ({name: d[0]})}, + {"name": "field", "symbols": ["dqstring"], "postprocess": d => ({name: d[0]})}, {"name": "query", "symbols": ["relational_operator", "_", "decimal"], "postprocess": d => ({quoted: false, query: d[2], relationalOperator: d[0][0]})}, {"name": "query", "symbols": ["decimal"], "postprocess": d => ({quoted: false, query: d.join('')})}, {"name": "query", "symbols": ["regex"], "postprocess": d => ({quoted: false, regex: true, query: d.join('')})}, diff --git a/src/hydrateAst.ts b/src/hydrateAst.ts index 331bc87..99ce608 100644 --- a/src/hydrateAst.ts +++ b/src/hydrateAst.ts @@ -24,10 +24,10 @@ export const hydrateAst = (subject: ParserAst): HydratedAst => { if ( optionalChainingIsSupported && - typeof subject.field === 'string' && - isSafePath(subject.field) + typeof subject.field?.name === 'string' && + isSafePath(subject.field.name) ) { - newSubject.getValue = new Function('subject', createGetValueFunctionBody(subject.field)) as (subject: unknown) => unknown; + newSubject.getValue = new Function('subject', createGetValueFunctionBody(subject.field.name)) as (subject: unknown) => unknown; } if (subject.left) { diff --git a/src/internalFilter.ts b/src/internalFilter.ts index 1ef2406..515666f 100644 --- a/src/internalFilter.ts +++ b/src/internalFilter.ts @@ -128,26 +128,26 @@ const testField = ( ast.test = createValueTest(ast); } - if (ast.field in row) { + if (ast.field.name in row) { return testValue( ast, - row[ast.field], + row[ast.field.name], resultFast, path, highlights, ); - } else if (optionalChainingIsSupported && ast.getValue && ast.fieldPath) { + } else if (optionalChainingIsSupported && ast.getValue && ast.field.path) { return testValue( ast, ast.getValue(row), resultFast, - ast.fieldPath, + ast.field.path, highlights, ); - } else if (ast.fieldPath) { + } else if (ast.field.path) { let value = row; - for (const key of ast.fieldPath) { + for (const key of ast.field.path) { if (typeof value !== 'object' || value === null) { return false; } else if (key in value) { @@ -161,23 +161,26 @@ const testField = ( ast, value, resultFast, - ast.fieldPath, + ast.field.path, highlights, ); - } else if (ast.field === '') { + } else if (ast.field.name === '') { let foundMatch = false; - for (const field in row) { + for (const fieldName in row) { if (testValue( { ...ast, - field, + field: { + ...ast.field, + name: fieldName, + }, }, - row[field], + row[fieldName], resultFast, [ ...path, - field, + fieldName, ], highlights, )) { @@ -208,7 +211,7 @@ export const internalFilter = ( row, ast, resultFast, - ast.field === '' ? path : [...path, ast.field], + ast.field.name === '' ? path : [...path, ast.field.name], highlights, ); }); diff --git a/src/types.ts b/src/types.ts index fd1b2b2..27ba43b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,8 +8,10 @@ export type Range = { export type RelationalOperator = '<' | '<=' | '=' | '>' | '>='; export type ParserAst = { - field: string, - fieldPath?: readonly string[], + field: { + name: string, + path?: readonly string[], + }, left?: ParserAst, operand?: ParserAst, operator?: 'AND' | 'NOT' | 'OR', diff --git a/test/liqe/hydrateAst.ts b/test/liqe/hydrateAst.ts index a2fd57a..9b4e5dc 100644 --- a/test/liqe/hydrateAst.ts +++ b/test/liqe/hydrateAst.ts @@ -9,7 +9,9 @@ import { if (isOptionalChainingSupported()) { test('adds getValue when field is a safe path', (t) => { const parserAst = { - field: '.foo', + field: { + name: '.foo', + }, }; const hydratedAst = hydrateAst(parserAst); @@ -19,13 +21,21 @@ if (isOptionalChainingSupported()) { test('adds getValue when field is a safe path (recursive)', (t) => { const parserAst = { - field: '', + field: { + name: '', + }, left: { - field: '', + field: { + name: '', + }, right: { - field: '', + field: { + name: '', + }, operand: { - field: '.foo', + field: { + name: '.foo', + }, }, }, }, @@ -33,12 +43,14 @@ if (isOptionalChainingSupported()) { const hydratedAst = hydrateAst(parserAst); - t.true('getValue' in hydratedAst!.left!.right!.operand!); + t.true('getValue' in (hydratedAst?.left?.right?.operand ?? {})); }); test('does not add getValue if path is unsafe', (t) => { const parserAst = { - field: 'foo', + field: { + name: 'foo', + }, }; const hydratedAst = hydrateAst(parserAst); @@ -48,7 +60,9 @@ if (isOptionalChainingSupported()) { test('getValue accesses existing value', (t) => { const parserAst = { - field: '.foo', + field: { + name: '.foo', + }, }; const hydratedAst = hydrateAst(parserAst); @@ -58,7 +72,9 @@ if (isOptionalChainingSupported()) { test('getValue accesses existing value (deep)', (t) => { const parserAst = { - field: '.foo.bar.baz', + field: { + name: '.foo.bar.baz', + }, }; const hydratedAst = hydrateAst(parserAst); @@ -68,7 +84,9 @@ if (isOptionalChainingSupported()) { test('returns undefined if path does not resolve', (t) => { const parserAst = { - field: '.foo.bar.baz', + field: { + name: '.foo.bar.baz', + }, }; const hydratedAst = hydrateAst(parserAst); diff --git a/test/liqe/parse.ts b/test/liqe/parse.ts index 05b132e..275cffd 100644 --- a/test/liqe/parse.ts +++ b/test/liqe/parse.ts @@ -26,214 +26,286 @@ test('error describes offset', (t) => { }); test('foo', testQuery, { - field: '', + field: { + name: '', + }, query: 'foo', quoted: false, }); test.skip('foo bar', testQuery, { left: { - field: '', + field: { + name: '', + }, query: 'foo', quoted: false, }, operator: 'AND', right: { - field: '', + field: { + name: '', + }, query: 'bar', quoted: false, }, }); test('foo_bar', testQuery, { - field: '', + field: { + name: '', + }, query: 'foo_bar', quoted: false, }); test('"foo"', testQuery, { - field: '', + field: { + name: '', + }, query: 'foo', quoted: true, }); test('\'foo\'', testQuery, { - field: '', + field: { + name: '', + }, query: 'foo', quoted: true, }); test('/foo/', testQuery, { - field: '', + field: { + name: '', + }, query: '/foo/', quoted: false, regex: true, }); test('/foo/ui', testQuery, { - field: '', + field: { + name: '', + }, query: '/foo/ui', quoted: false, regex: true, }); test('/\\s/', testQuery, { - field: '', + field: { + name: '', + }, query: '/\\s/', quoted: false, regex: true, }); test('/[^.:@\\s](?:[^:@\\s]*[^.:@\\s])?@[^.@\\s]+(?:\\.[^.@\\s]+)*/', testQuery, { - field: '', + field: { + name: '', + }, query: '/[^.:@\\s](?:[^:@\\s]*[^.:@\\s])?@[^.@\\s]+(?:\\.[^.@\\s]+)*/', quoted: false, regex: true, }); test('foo:bar', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', }); test('foo: bar', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', }); test('foo:123', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: '123', }); test('foo:=123', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: 123, relationalOperator: '=', }); test('foo:= 123', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: 123, relationalOperator: '=', }); test('foo:=-123', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: -123, }); test('foo:=123.4', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: 123.4, }); test('foo:>=123', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: 123, relationalOperator: '>=', }); test('foo:true', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: true, }); test('foo:false', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: false, }); test('foo:null', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: null, }); test('foo.bar:baz', testQuery, { - field: 'foo.bar', - fieldPath: [ - 'foo', - 'bar', - ], + field: { + name: 'foo.bar', + path: [ + 'foo', + 'bar', + ], + }, query: 'baz', }); test('foo_bar:baz', testQuery, { - field: 'foo_bar', + field: { + name: 'foo_bar', + }, query: 'baz', }); test('$foo:baz', testQuery, { - field: '$foo', + field: { + name: '$foo', + }, query: 'baz', }); test('"foo bar":baz', testQuery, { - field: 'foo bar', + field: { + name: 'foo bar', + }, query: 'baz', }); test('\'foo bar\':baz', testQuery, { - field: 'foo bar', + field: { + name: 'foo bar', + }, query: 'baz', }); test('foo:"bar"', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', quoted: true, }); test('foo:\'bar\'', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', quoted: true, }); test.skip('foo:bar baz:qux', testQuery, { left: { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', }, operator: 'AND', right: { - field: 'baz', + field: { + name: 'baz', + }, query: 'qux', }, }); test('foo:bar AND baz:qux', testQuery, { left: { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', }, operator: 'AND', right: { - field: 'baz', + field: { + name: 'baz', + }, query: 'qux', }, }); test('(foo:bar) AND (baz:qux)', testQuery, { left: { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', }, operator: 'AND', right: { - field: 'baz', + field: { + name: 'baz', + }, query: 'qux', }, }); test('(foo:bar AND baz:qux)', testQuery, { left: { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', }, operator: 'AND', right: { - field: 'baz', + field: { + name: 'baz', + }, query: 'qux', }, }); @@ -241,12 +313,16 @@ test('(foo:bar AND baz:qux)', testQuery, { test.skip('NOT (foo:bar AND baz:qux)', testQuery, { operand: { left: { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', }, operator: 'AND', right: { - field: 'baz', + field: { + name: 'baz', + }, query: 'qux', }, }, @@ -255,7 +331,9 @@ test.skip('NOT (foo:bar AND baz:qux)', testQuery, { test('NOT foo:bar', testQuery, { operand: { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', quoted: false, }, @@ -264,13 +342,17 @@ test('NOT foo:bar', testQuery, { test('foo:bar AND NOT baz:qux', testQuery, { left: { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', }, operator: 'AND', right: { operand: { - field: 'baz', + field: { + name: 'baz', + }, query: 'qux', }, operator: 'NOT', @@ -280,49 +362,65 @@ test('foo:bar AND NOT baz:qux', testQuery, { test('foo:bar AND baz:qux AND quuz:corge', testQuery, { left: { left: { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', }, operator: 'AND', right: { - field: 'baz', + field: { + name: 'baz', + }, query: 'qux', }, }, operator: 'AND', right: { - field: 'quuz', + field: { + name: 'quuz', + }, query: 'corge', }, }); test('(foo:bar)', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', quoted: false, }); test('((foo:bar))', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', quoted: false, }); test('( foo:bar )', testQuery, { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', quoted: false, }); test('(foo:bar OR baz:qux)', testQuery, { left: { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', quoted: false, }, operator: 'OR', right: { - field: 'baz', + field: { + name: 'baz', + }, query: 'qux', quoted: false, }, @@ -330,20 +428,26 @@ test('(foo:bar OR baz:qux)', testQuery, { test('foo:bar OR (baz:qux OR quuz:corge)', testQuery, { left: { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', quoted: false, }, operator: 'OR', right: { left: { - field: 'baz', + field: { + name: 'baz', + }, query: 'qux', quoted: false, }, operator: 'OR', right: { - field: 'quuz', + field: { + name: 'quuz', + }, query: 'corge', quoted: false, }, @@ -353,20 +457,26 @@ test('foo:bar OR (baz:qux OR quuz:corge)', testQuery, { test('(foo:bar OR baz:qux) OR quuz:corge', testQuery, { left: { left: { - field: 'foo', + field: { + name: 'foo', + }, query: 'bar', quoted: false, }, operator: 'OR', right: { - field: 'baz', + field: { + name: 'baz', + }, query: 'qux', quoted: false, }, }, operator: 'OR', right: { - field: 'quuz', + field: { + name: 'quuz', + }, query: 'corge', quoted: false, },