require('./utils'); const {lex} = require('../'); describe('Lexer', function () { const lexicalError = /^Query Error: unrecognized text/; describe('Symbols', function () { it('can recognise -', function () { lex('-').should.eql([{token: 'NOT', matched: '-'}]); }); it('can recognise +', function () { lex('+').should.eql([{token: 'AND', matched: '+'}]); }); it('can recognise ,', function () { lex(',').should.eql([{token: 'OR', matched: ','}]); }); it('can recognise [', function () { lex('[').should.eql([{token: 'LBRACKET', matched: '['}]); }); it('can recognise ]', function () { lex(']').should.eql([{token: 'RBRACKET', matched: ']'}]); }); it('can recognise (', function () { lex('(').should.eql([{token: 'LPAREN', matched: '('}]); }); it('can recognise )', function () { lex(')').should.eql([{token: 'RPAREN', matched: ')'}]); }); it('can recognise >', function () { lex('>').should.eql([{token: 'GT', matched: '>'}]); }); it('can recognise <', function () { lex('<').should.eql([{token: 'LT', matched: '<'}]); }); it('can recognise >=', function () { lex('>=').should.eql([{token: 'GTE', matched: '>='}]); }); it('can recognise <=', function () { lex('<=').should.eql([{token: 'LTE', matched: '<='}]); }); it('can recognise ~', function () { lex('~').should.eql([{token: 'CONTAINS', matched: '~'}]); }); it('can recognise ~^', function () { lex('~^').should.eql([{token: 'STARTSWITH', matched: '~^'}]); }); it('can recognise ~$', function () { lex('~$').should.eql([{token: 'ENDSWITH', matched: '~$'}]); }); it('cannot recognise :', function () { (function () { lex(':'); }).should.throw(lexicalError); }); it('cannot recognise =', function () { (function () { lex('='); }).should.throw(lexicalError); }); it('cannot recognise "', function () { (function () { lex('"'); }).should.throw(lexicalError); }); it('cannot recognise \'', function () { (function () { lex('\''); }).should.throw(lexicalError); }); }); describe('VALUES', function () { it('can recognise null', function () { lex('null').should.eql([{token: 'NULL', matched: 'null'}]); }); it('can recognise true', function () { lex('true').should.eql([{token: 'TRUE', matched: 'true'}]); }); it('can recognise false', function () { lex('false').should.eql([{token: 'FALSE', matched: 'false'}]); }); it('can recognise NULL', function () { lex('NULL').should.eql([{token: 'NULL', matched: 'NULL'}]); }); it('can recognise TRUE', function () { lex('TRUE').should.eql([{token: 'TRUE', matched: 'TRUE'}]); }); it('can recognise FALSE', function () { lex('FALSE').should.eql([{token: 'FALSE', matched: 'FALSE'}]); }); it('can recognise Null', function () { lex('Null').should.eql([{token: 'NULL', matched: 'Null'}]); }); it('can recognise True', function () { lex('True').should.eql([{token: 'TRUE', matched: 'True'}]); }); it('can recognise False', function () { lex('False').should.eql([{token: 'FALSE', matched: 'False'}]); }); it('can recognise a LITERAL', function () { lex('six').should.eql([{token: 'LITERAL', matched: 'six'}]); }); it('can recognise a STRING', function () { lex('\'six\'').should.eql([{token: 'STRING', matched: '\'six\''}]); }); it('can recognise a NUMBER', function () { lex('6').should.eql([{token: 'NUMBER', matched: '6'}]); }); it('does not confuse keywords in LITERALs', function () { lex('strueth').should.eql([{token: 'LITERAL', matched: 'strueth'}]); lex('trueth').should.eql([{token: 'LITERAL', matched: 'trueth'}]); lex('true_thing').should.eql([{token: 'LITERAL', matched: 'true_thing'}]); lex('true-thing').should.eql([{token: 'LITERAL', matched: 'true-thing'}]); lex('nullable').should.eql([{token: 'LITERAL', matched: 'nullable'}]); lex('its-nullable').should.eql([{token: 'LITERAL', matched: 'its-nullable'}]); lex('notnullable').should.eql([{token: 'LITERAL', matched: 'notnullable'}]); lex('null-thing').should.eql([{token: 'LITERAL', matched: 'null-thing'}]); lex('its-a-null-thing').should.eql([{token: 'LITERAL', matched: 'its-a-null-thing'}]); }); it('does not confuse keywords in STRINGs', function () { lex('\'strueth\'').should.eql([{token: 'STRING', matched: '\'strueth\''}]); lex('\'trueth\'').should.eql([{token: 'STRING', matched: '\'trueth\''}]); lex('\'true_thing\'').should.eql([{token: 'STRING', matched: '\'true_thing\''}]); lex('\'true-thing\'').should.eql([{token: 'STRING', matched: '\'true-thing\''}]); lex('\'nullable\'').should.eql([{token: 'STRING', matched: '\'nullable\''}]); lex('\'its-nullable\'').should.eql([{token: 'STRING', matched: '\'its-nullable\''}]); lex('\'notnullable\'').should.eql([{token: 'STRING', matched: '\'notnullable\''}]); lex('\'null-thing\'').should.eql([{token: 'STRING', matched: '\'null-thing\''}]); lex('\'its-a-null-thing\'').should.eql([{token: 'STRING', matched: '\'its-a-null-thing\''}]); }); }); describe('LITERAL values', function () { it('should match literals', function () { lex('myvalue').should.eql([ {token: 'LITERAL', matched: 'myvalue'} ]); lex('my value').should.eql([ {token: 'LITERAL', matched: 'my'}, {token: 'LITERAL', matched: 'value'} ]); lex('my-value').should.eql([ {token: 'LITERAL', matched: 'my-value'} ]); lex('my&value!').should.eql([ {token: 'LITERAL', matched: 'my&value!'} ]); lex('my&valu\\\'e!').should.eql([ {token: 'LITERAL', matched: 'my&valu\\\'e!'} ]); (function () { lex('my&valu\'e!'); }).should.throw(lexicalError); }); it('should separate NOT at beginning of literal', function () { lex('-photo').should.eql([ {token: 'NOT', matched: '-'}, {token: 'LITERAL', matched: 'photo'} ]); lex('-photo-graph').should.eql([ {token: 'NOT', matched: '-'}, {token: 'LITERAL', matched: 'photo-graph'} ]); }); it('should NOT permit special chars inside a literal', function () { (function () { lex('t+st'); }).should.throw(lexicalError); (function () { lex('t,st'); }).should.throw(lexicalError); (function () { lex('t(st'); }).should.throw(lexicalError); (function () { lex('t)st'); }).should.throw(lexicalError); (function () { lex('t>st'); }).should.throw(lexicalError); (function () { lex('ttest').should.eql([ {token: 'GT', matched: '>'}, {token: 'LITERAL', matched: 'test'} ]); lex('=test').should.eql([ {token: 'GTE', matched: '>='}, {token: 'LITERAL', matched: 'test'} ]); lex('<=test').should.eql([ {token: 'LTE', matched: '<='}, {token: 'LITERAL', matched: 'test'} ]); (function () { lex('=test'); }).should.throw(lexicalError); (function () { lex('"test'); }).should.throw(lexicalError); (function () { lex('\'test'); }).should.throw(lexicalError); }); it('should not match special chars at the end of a literal', function () { lex('test+').should.eql([ {token: 'LITERAL', matched: 'test'}, {token: 'AND', matched: '+'} ]); lex('test,').should.eql([ {token: 'LITERAL', matched: 'test'}, {token: 'OR', matched: ','} ]); lex('test(').should.eql([ {token: 'LITERAL', matched: 'test'}, {token: 'LPAREN', matched: '('} ]); lex('test)').should.eql([ {token: 'LITERAL', matched: 'test'}, {token: 'RPAREN', matched: ')'} ]); lex('test>').should.eql([ {token: 'LITERAL', matched: 'test'}, {token: 'GT', matched: '>'} ]); lex('test<').should.eql([ {token: 'LITERAL', matched: 'test'}, {token: 'LT', matched: '<'} ]); lex('test[').should.eql([ {token: 'LITERAL', matched: 'test'}, {token: 'LBRACKET', matched: '['} ]); lex('test]').should.eql([ {token: 'LITERAL', matched: 'test'}, {token: 'RBRACKET', matched: ']'} ]); lex('test>=').should.eql([ {token: 'LITERAL', matched: 'test'}, {token: 'GTE', matched: '>='} ]); lex('test<=').should.eql([ {token: 'LITERAL', matched: 'test'}, {token: 'LTE', matched: '<='} ]); (function () { lex('test='); }).should.throw(lexicalError); (function () { lex('test"'); }).should.throw(lexicalError); (function () { lex('test\''); }).should.throw(lexicalError); }); it('should permit escaped special chars inside a literal', function () { lex('t\\+st').should.eql([{token: 'LITERAL', matched: 't\\+st'}]); lex('t\\,st').should.eql([{token: 'LITERAL', matched: 't\\,st'}]); lex('t\\(st').should.eql([{token: 'LITERAL', matched: 't\\(st'}]); lex('t\\)st').should.eql([{token: 'LITERAL', matched: 't\\)st'}]); lex('t\\>st').should.eql([{token: 'LITERAL', matched: 't\\>st'}]); lex('t\\\'').should.eql([{token: 'STRING', matched: '\'>\''}]); lex('\'<\'').should.eql([{token: 'STRING', matched: '\'<\''}]); lex('\'~\'').should.eql([{token: 'STRING', matched: '\'~\''}]); }); it('can recognise STRING with special characters', function () { lex('\'magic+\'').should.eql([{token: 'STRING', matched: '\'magic+\''}]); lex('\'magic,\'').should.eql([{token: 'STRING', matched: '\'magic,\''}]); lex('\'magic-\'').should.eql([{token: 'STRING', matched: '\'magic-\''}]); lex('\'magic>\'').should.eql([{token: 'STRING', matched: '\'magic>\''}]); lex('\'magic<\'').should.eql([{token: 'STRING', matched: '\'magic<\''}]); lex('\'magic~\'').should.eql([{token: 'STRING', matched: '\'magic~\''}]); }); it('should permit special chars inside a STRING, not including quotes', function () { lex('\'t+st\'').should.eql([{token: 'STRING', matched: '\'t+st\''}]); lex('\'t,st\'').should.eql([{token: 'STRING', matched: '\'t,st\''}]); lex('\'t(st\'').should.eql([{token: 'STRING', matched: '\'t(st\''}]); lex('\'t)st\'').should.eql([{token: 'STRING', matched: '\'t)st\''}]); lex('\'t>st\'').should.eql([{token: 'STRING', matched: '\'t>st\''}]); lex('\'t=now-2d', function () { lex('last_seen_at:>=now-2d').should.eql([ {token: 'PROP', matched: 'last_seen_at:'}, {token: 'GTE', matched: '>='}, {token: 'NOW', matched: 'now'}, {token: 'SUB', matched: '-'}, {token: 'AMOUNT', matched: '2'}, {token: 'INTERVAL', matched: 'd'} ]); }); it('last_seen_at:>=now+2d', function () { lex('last_seen_at:>=now+2d').should.eql([ {token: 'PROP', matched: 'last_seen_at:'}, {token: 'GTE', matched: '>='}, {token: 'NOW', matched: 'now'}, {token: 'ADD', matched: '+'}, {token: 'AMOUNT', matched: '2'}, {token: 'INTERVAL', matched: 'd'} ]); }); it('last_seen_at:>=now+2d+foo:bar', function () { lex('last_seen_at:>=now+2d+foo:bar').should.eql([ {token: 'PROP', matched: 'last_seen_at:'}, {token: 'GTE', matched: '>='}, {token: 'NOW', matched: 'now'}, {token: 'ADD', matched: '+'}, {token: 'AMOUNT', matched: '2'}, {token: 'INTERVAL', matched: 'd'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'foo:'}, {token: 'LITERAL', matched: 'bar'} ]); }); it('foo:bar+last_seen_at:>=now+2d', function () { lex('foo:bar+last_seen_at:>=now+2d').should.eql([ {token: 'PROP', matched: 'foo:'}, {token: 'LITERAL', matched: 'bar'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'last_seen_at:'}, {token: 'GTE', matched: '>='}, {token: 'NOW', matched: 'now'}, {token: 'ADD', matched: '+'}, {token: 'AMOUNT', matched: '2'}, {token: 'INTERVAL', matched: 'd'} ]); }); }); }); describe('Complex examples', function () { it('many expressions', function () { lex('tag:photo+featured:true,tag.count:>5').should.eql([ {token: 'PROP', matched: 'tag:'}, {token: 'LITERAL', matched: 'photo'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'featured:'}, {token: 'TRUE', matched: 'true'}, {token: 'OR', matched: ','}, {token: 'PROP', matched: 'tag.count:'}, {token: 'GT', matched: '>'}, {token: 'NUMBER', matched: '5'} ]); lex('true:null+false:true,null:false').should.eql([ {token: 'PROP', matched: 'true:'}, {token: 'NULL', matched: 'null'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'false:'}, {token: 'TRUE', matched: 'true'}, {token: 'OR', matched: ','}, {token: 'PROP', matched: 'null:'}, {token: 'FALSE', matched: 'false'} ]); lex('tag:photo+created_at:>=now-1d,tag.count:>5').should.eql([ {token: 'PROP', matched: 'tag:'}, {token: 'LITERAL', matched: 'photo'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'created_at:'}, {token: 'GTE', matched: '>='}, {token: 'NOW', matched: 'now'}, {token: 'SUB', matched: '-'}, {token: 'AMOUNT', matched: '1'}, {token: 'INTERVAL', matched: 'd'}, {token: 'OR', matched: ','}, {token: 'PROP', matched: 'tag.count:'}, {token: 'GT', matched: '>'}, {token: 'NUMBER', matched: '5'} ]); lex('tag:photo+image:-null,tag.count:>5').should.eql([ {token: 'PROP', matched: 'tag:'}, {token: 'LITERAL', matched: 'photo'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'image:'}, {token: 'NOT', matched: '-'}, {token: 'NULL', matched: 'null'}, {token: 'OR', matched: ','}, {token: 'PROP', matched: 'tag.count:'}, {token: 'GT', matched: '>'}, {token: 'NUMBER', matched: '5'} ]); lex('tag:photo+image:-null,tag.count:>5+foo:~\'bar\'').should.eql([ {token: 'PROP', matched: 'tag:'}, {token: 'LITERAL', matched: 'photo'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'image:'}, {token: 'NOT', matched: '-'}, {token: 'NULL', matched: 'null'}, {token: 'OR', matched: ','}, {token: 'PROP', matched: 'tag.count:'}, {token: 'GT', matched: '>'}, {token: 'NUMBER', matched: '5'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'foo:'}, {token: 'CONTAINS', matched: '~'}, {token: 'STRING', matched: '\'bar\''} ]); }); it('grouped expressions', function () { lex('author:-joe+(tag:photo,image:-null,featured:true)').should.eql([ {token: 'PROP', matched: 'author:'}, {token: 'NOT', matched: '-'}, {token: 'LITERAL', matched: 'joe'}, {token: 'AND', matched: '+'}, {token: 'LPAREN', matched: '('}, {token: 'PROP', matched: 'tag:'}, {token: 'LITERAL', matched: 'photo'}, {token: 'OR', matched: ','}, {token: 'PROP', matched: 'image:'}, {token: 'NOT', matched: '-'}, {token: 'NULL', matched: 'null'}, {token: 'OR', matched: ','}, {token: 'PROP', matched: 'featured:'}, {token: 'TRUE', matched: 'true'}, {token: 'RPAREN', matched: ')'} ]); lex('author:-joe+(tag:photo,image:-null,created_at:>=now-1d)').should.eql([ {token: 'PROP', matched: 'author:'}, {token: 'NOT', matched: '-'}, {token: 'LITERAL', matched: 'joe'}, {token: 'AND', matched: '+'}, {token: 'LPAREN', matched: '('}, {token: 'PROP', matched: 'tag:'}, {token: 'LITERAL', matched: 'photo'}, {token: 'OR', matched: ','}, {token: 'PROP', matched: 'image:'}, {token: 'NOT', matched: '-'}, {token: 'NULL', matched: 'null'}, {token: 'OR', matched: ','}, {token: 'PROP', matched: 'created_at:'}, {token: 'GTE', matched: '>='}, {token: 'NOW', matched: 'now'}, {token: 'SUB', matched: '-'}, {token: 'AMOUNT', matched: '1'}, {token: 'INTERVAL', matched: 'd'}, {token: 'RPAREN', matched: ')'} ]); }); it('in expressions', function () { lex('author:-joe+tag:[photo,video]').should.eql([ {token: 'PROP', matched: 'author:'}, {token: 'NOT', matched: '-'}, {token: 'LITERAL', matched: 'joe'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'tag:'}, {token: 'LBRACKET', matched: '['}, {token: 'LITERAL', matched: 'photo'}, {token: 'OR', matched: ','}, {token: 'LITERAL', matched: 'video'}, {token: 'RBRACKET', matched: ']'} ]); lex('author:-joe+tag:-[photo,video]').should.eql([ {token: 'PROP', matched: 'author:'}, {token: 'NOT', matched: '-'}, {token: 'LITERAL', matched: 'joe'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'tag:'}, {token: 'NOT', matched: '-'}, {token: 'LBRACKET', matched: '['}, {token: 'LITERAL', matched: 'photo'}, {token: 'OR', matched: ','}, {token: 'LITERAL', matched: 'video'}, {token: 'RBRACKET', matched: ']'} ]); lex('author:-joe+tag:[photo,video]+post.count:>5+post.count:<100').should.eql([ {token: 'PROP', matched: 'author:'}, {token: 'NOT', matched: '-'}, {token: 'LITERAL', matched: 'joe'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'tag:'}, {token: 'LBRACKET', matched: '['}, {token: 'LITERAL', matched: 'photo'}, {token: 'OR', matched: ','}, {token: 'LITERAL', matched: 'video'}, {token: 'RBRACKET', matched: ']'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'post.count:'}, {token: 'GT', matched: '>'}, {token: 'NUMBER', matched: '5'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'post.count:'}, {token: 'LT', matched: '<'}, {token: 'NUMBER', matched: '100'} ]); lex('author:-joe+created_at:[now-1d,now+1d]').should.eql([ {token: 'PROP', matched: 'author:'}, {token: 'NOT', matched: '-'}, {token: 'LITERAL', matched: 'joe'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'created_at:'}, {token: 'LBRACKET', matched: '['}, {token: 'NOW', matched: 'now'}, {token: 'SUB', matched: '-'}, {token: 'AMOUNT', matched: '1'}, {token: 'INTERVAL', matched: 'd'}, {token: 'OR', matched: ','}, {token: 'NOW', matched: 'now'}, {token: 'ADD', matched: '+'}, {token: 'AMOUNT', matched: '1'}, {token: 'INTERVAL', matched: 'd'}, {token: 'RBRACKET', matched: ']'} ]); }); it('creating strings with JS - double quotes', function () { const slug = 'test'; const op = '>'; const publishedAt = '2022-03-04 10:15:04'; let filter = "slug:-" + slug + "+published_at:" + op + "'" + publishedAt + "'"; /* eslint-disable-line quotes */ lex(filter).should.eql([ {token: 'PROP', matched: 'slug:'}, {token: 'NOT', matched: '-'}, {token: 'LITERAL', matched: 'test'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'published_at:'}, {token: 'GT', matched: '>'}, {token: 'STRING', matched: '\'2022-03-04 10:15:04\''} ]); }); it('creating strings with JS - single quotes', function () { const slug = 'test'; const op = '>'; const publishedAt = '2022-03-04 10:15:04'; let filter = 'slug:-' + slug + '+published_at:' + op + '\'' + publishedAt + '\''; lex(filter).should.eql([ {token: 'PROP', matched: 'slug:'}, {token: 'NOT', matched: '-'}, {token: 'LITERAL', matched: 'test'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'published_at:'}, {token: 'GT', matched: '>'}, {token: 'STRING', matched: '\'2022-03-04 10:15:04\''} ]); }); it('creating strings with JS - template strings', function () { const slug = 'test'; const op = '>'; const publishedAt = '2022-03-04 10:15:04'; let filter = `slug:-${slug}+published_at:${op}'${publishedAt}'`; lex(filter).should.eql([ {token: 'PROP', matched: 'slug:'}, {token: 'NOT', matched: '-'}, {token: 'LITERAL', matched: 'test'}, {token: 'AND', matched: '+'}, {token: 'PROP', matched: 'published_at:'}, {token: 'GT', matched: '>'}, {token: 'STRING', matched: '\'2022-03-04 10:15:04\''} ]); }); }); });