nodebook-nbql
Version:
Filter query language for nodebook
468 lines (423 loc) • 20.9 kB
JavaScript
/* globals describe, it */
var nbql = require('../lib/nbql');
describe('Lexer', function () {
var lexicalError = /^Query Error: unrecognized text/;
describe('Symbols', function () {
it('can recognise -', function () {
nbql.lex('-').should.eql([{token: 'NOT', matched: '-'}]);
});
it('can recognise +', function () {
nbql.lex('+').should.eql([{token: 'AND', matched: '+'}]);
});
it('can recognise ,', function () {
nbql.lex(',').should.eql([{token: 'OR', matched: ','}]);
});
it('can recognise [', function () {
nbql.lex('[').should.eql([{token: 'LBRACKET', matched: '['}]);
});
it('can recognise ]', function () {
nbql.lex(']').should.eql([{token: 'RBRACKET', matched: ']'}]);
});
it('can recognise (', function () {
nbql.lex('(').should.eql([{token: 'LPAREN', matched: '('}]);
});
it('can recognise )', function () {
nbql.lex(')').should.eql([{token: 'RPAREN', matched: ')'}]);
});
it('can recognise >', function () {
nbql.lex('>').should.eql([{token: 'GT', matched: '>'}]);
});
it('can recognise <', function () {
nbql.lex('<').should.eql([{token: 'LT', matched: '<'}]);
});
it('can recognise >=', function () {
nbql.lex('>=').should.eql([{token: 'GTE', matched: '>='}]);
});
it('can recognise <=', function () {
nbql.lex('<=').should.eql([{token: 'LTE', matched: '<='}]);
});
it('cannot recognise :', function () {
(function () {nbql.lex(':');}).should.throw(lexicalError);
});
it('cannot recognise =', function () {
(function () {nbql.lex('=');}).should.throw(lexicalError);
});
it('cannot recognise "', function () {
(function () {nbql.lex('"');}).should.throw(lexicalError);
});
it('cannot recognise \'', function () {
(function () {nbql.lex('\'');}).should.throw(lexicalError);
});
});
describe('VALUES', function () {
it('can recognise null', function () {
nbql.lex('null').should.eql([{token: 'NULL', matched: 'null'}]);
});
it('can recognise true', function () {
nbql.lex('true').should.eql([{token: 'TRUE', matched: 'true'}]);
});
it('can recognise false', function () {
nbql.lex('false').should.eql([{token: 'FALSE', matched: 'false'}]);
});
it('can recognise a LITERAL', function () {
nbql.lex('six').should.eql([{token: 'LITERAL', matched: 'six'}]);
});
it('can recognise a STRING', function () {
nbql.lex('\'six\'').should.eql([{token: 'STRING', matched: '\'six\''}]);
});
it('can recognise a NUMBER', function () {
nbql.lex('6').should.eql([{token: 'NUMBER', matched: '6'}]);
});
it('does not confuse values in LITERALs', function () {
nbql.lex('strueth').should.eql([{token: 'LITERAL', matched: 'strueth'}]);
nbql.lex('trueth').should.eql([{token: 'LITERAL', matched: 'trueth'}]);
nbql.lex('true_thing').should.eql([{token: 'LITERAL', matched: 'true_thing'}]);
// nbql.lex("true-thing").should.eql([{token: "LITERAL", matched: "true-thing"}]);
});
it('does not confuse values in STRINGs', function () {
nbql.lex('\'strueth\'').should.eql([{token: 'STRING', matched: '\'strueth\''}]);
nbql.lex('\'trueth\'').should.eql([{token: 'STRING', matched: '\'trueth\''}]);
nbql.lex('\'true_thing\'').should.eql([{token: 'STRING', matched: '\'true_thing\''}]);
nbql.lex('\'true-thing\'').should.eql([{token: 'STRING', matched: '\'true-thing\''}]);
});
});
describe('LITERAL values', function () {
it('should match literals', function () {
nbql.lex('myvalue').should.eql([
{token: 'LITERAL', matched: 'myvalue'}
]);
nbql.lex('my value').should.eql([
{token: 'LITERAL', matched: 'my'},
{token: 'LITERAL', matched: 'value'}
]);
nbql.lex('my-value').should.eql([
{token: 'LITERAL', matched: 'my-value'}
]);
nbql.lex('my&value!').should.eql([
{token: 'LITERAL', matched: 'my&value!'}
]);
nbql.lex('my&valu\\\'e!').should.eql([
{token: 'LITERAL', matched: 'my&valu\\\'e!'}
]);
(function () {nbql.lex('my&valu\'e!');}).should.throw(lexicalError);
});
it('should separate NOT at beginning of literal', function () {
nbql.lex('-photo').should.eql([
{token: 'NOT', matched: '-'},
{token: 'LITERAL', matched: 'photo'}
]);
nbql.lex('-photo-graph').should.eql([
{token: 'NOT', matched: '-'},
{token: 'LITERAL', matched: 'photo-graph'}
]);
});
it('should NOT permit special chars inside a literal', function () {
(function () { nbql.lex('t+st');}).should.throw(lexicalError);
(function () { nbql.lex('t,st');}).should.throw(lexicalError);
(function () { nbql.lex('t(st');}).should.throw(lexicalError);
(function () { nbql.lex('t)st');}).should.throw(lexicalError);
(function () { nbql.lex('t>st');}).should.throw(lexicalError);
(function () { nbql.lex('t<st');}).should.throw(lexicalError);
(function () { nbql.lex('t=st');}).should.throw(lexicalError);
(function () { nbql.lex('t[st');}).should.throw(lexicalError);
(function () { nbql.lex('t]st');}).should.throw(lexicalError);
(function () { nbql.lex('t\'st');}).should.throw(lexicalError);
(function () { nbql.lex('t"st');}).should.throw(lexicalError);
});
it('should not match special chars at the start of a literal', function () {
nbql.lex('+test').should.eql([
{token: 'AND', matched: '+'},
{token: 'LITERAL', matched: 'test'}
]);
nbql.lex(',test').should.eql([
{token: 'OR', matched: ','},
{token: 'LITERAL', matched: 'test'}
]);
nbql.lex('(test').should.eql([
{token: 'LPAREN', matched: '('},
{token: 'LITERAL', matched: 'test'}
]);
nbql.lex(')test').should.eql([
{token: 'RPAREN', matched: ')'},
{token: 'LITERAL', matched: 'test'}
]);
nbql.lex('>test').should.eql([
{token: 'GT', matched: '>'},
{token: 'LITERAL', matched: 'test'}
]);
nbql.lex('<test').should.eql([
{token: 'LT', matched: '<'},
{token: 'LITERAL', matched: 'test'}
]);
nbql.lex('[test').should.eql([
{token: 'LBRACKET', matched: '['},
{token: 'LITERAL', matched: 'test'}
]);
nbql.lex(']test').should.eql([
{token: 'RBRACKET', matched: ']'},
{token: 'LITERAL', matched: 'test'}
]);
nbql.lex('>=test').should.eql([
{token: 'GTE', matched: '>='},
{token: 'LITERAL', matched: 'test'}
]);
nbql.lex('<=test').should.eql([
{token: 'LTE', matched: '<='},
{token: 'LITERAL', matched: 'test'}
]);
(function () { nbql.lex('=test');}).should.throw(lexicalError);
(function () { nbql.lex('"test');}).should.throw(lexicalError);
(function () { nbql.lex('\'test');}).should.throw(lexicalError);
});
it('should not match special chars at the end of a literal', function () {
nbql.lex('test+').should.eql([
{token: 'LITERAL', matched: 'test'},
{token: 'AND', matched: '+'}
]);
nbql.lex('test,').should.eql([
{token: 'LITERAL', matched: 'test'},
{token: 'OR', matched: ','}
]);
nbql.lex('test(').should.eql([
{token: 'LITERAL', matched: 'test'},
{token: 'LPAREN', matched: '('}
]);
nbql.lex('test)').should.eql([
{token: 'LITERAL', matched: 'test'},
{token: 'RPAREN', matched: ')'}
]);
nbql.lex('test>').should.eql([
{token: 'LITERAL', matched: 'test'},
{token: 'GT', matched: '>'}
]);
nbql.lex('test<').should.eql([
{token: 'LITERAL', matched: 'test'},
{token: 'LT', matched: '<'}
]);
nbql.lex('test[').should.eql([
{token: 'LITERAL', matched: 'test'},
{token: 'LBRACKET', matched: '['}
]);
nbql.lex('test]').should.eql([
{token: 'LITERAL', matched: 'test'},
{token: 'RBRACKET', matched: ']'}
]);
nbql.lex('test>=').should.eql([
{token: 'LITERAL', matched: 'test'},
{token: 'GTE', matched: '>='}
]);
nbql.lex('test<=').should.eql([
{token: 'LITERAL', matched: 'test'},
{token: 'LTE', matched: '<='}
]);
(function () { nbql.lex('test=');}).should.throw(lexicalError);
(function () { nbql.lex('test"');}).should.throw(lexicalError);
(function () { nbql.lex('test\'');}).should.throw(lexicalError);
});
it('should permit escaped special chars inside a literal', function () {
nbql.lex('t\\+st').should.eql([{token: 'LITERAL', matched: 't\\+st'}]);
nbql.lex('t\\,st').should.eql([{token: 'LITERAL', matched: 't\\,st'}]);
nbql.lex('t\\(st').should.eql([{token: 'LITERAL', matched: 't\\(st'}]);
nbql.lex('t\\)st').should.eql([{token: 'LITERAL', matched: 't\\)st'}]);
nbql.lex('t\\>st').should.eql([{token: 'LITERAL', matched: 't\\>st'}]);
nbql.lex('t\\<st').should.eql([{token: 'LITERAL', matched: 't\\<st'}]);
nbql.lex('t\\=st').should.eql([{token: 'LITERAL', matched: 't\\=st'}]);
nbql.lex('t\\[st').should.eql([{token: 'LITERAL', matched: 't\\[st'}]);
nbql.lex('t\\]st').should.eql([{token: 'LITERAL', matched: 't\\]st'}]);
nbql.lex('t\\\'st').should.eql([{token: 'LITERAL', matched: 't\\\'st'}]);
nbql.lex('t\\"st').should.eql([{token: 'LITERAL', matched: 't\\"st'}]);
});
});
describe('LITERAL vs PROP', function () {
it('should match colon in string as PROP before, literal after', function () {
nbql.lex(':test').should.eql([
{token: 'LITERAL', matched: ':test'}
]);
nbql.lex('te:st').should.eql([
{token: 'PROP', matched: 'te:'},
{token: 'LITERAL', matched: 'st'}
]);
nbql.lex('test:').should.eql([
{token: 'PROP', matched: 'test:'}
]);
});
it('should only match colon-at-end as PROP if PROP is valPROP', function () {
nbql.lex('te!:st').should.eql([
{token: 'LITERAL', matched: 'te!:st'}
]);
nbql.lex('post-count:6').should.eql([
{token: 'LITERAL', matched: 'post-count:6'}
]);
nbql.lex('post_count:6').should.eql([
{token: 'PROP', matched: 'post_count:'},
{token: 'NUMBER', matched: '6'}
]);
});
});
describe('STRING values', function () {
it('can recognise simple STRING', function () {
nbql.lex('\'magic\'').should.eql([{token: 'STRING', matched: '\'magic\''}]);
nbql.lex('\'magic mystery\'').should.eql([{token: 'STRING', matched: '\'magic mystery\''}]);
nbql.lex('\'magic 123\'').should.eql([{token: 'STRING', matched: '\'magic 123\''}]);
});
it('can recognise multiple STRING values', function () {
nbql.lex('\'magic\'\'mystery\'').should.eql([
{token: 'STRING', matched: '\'magic\''},
{token: 'STRING', matched: '\'mystery\''}
]);
nbql.lex('\'magic\' \'mystery\'').should.eql([
{token: 'STRING', matched: '\'magic\''},
{token: 'STRING', matched: '\'mystery\''}
]);
nbql.lex('\'magic\',\'mystery\'').should.eql([
{token: 'STRING', matched: '\'magic\''},
{token: 'OR', matched: ','},
{token: 'STRING', matched: '\'mystery\''}
]);
nbql.lex('[\'magic\',\'mystery\']').should.eql([
{token: 'LBRACKET', matched: '['},
{token: 'STRING', matched: '\'magic\''},
{token: 'OR', matched: ','},
{token: 'STRING', matched: '\'mystery\''},
{token: 'RBRACKET', matched: ']'}
]);
});
it('can recognise STRING with special characters', function () {
nbql.lex('\'magic+\'').should.eql([{token: 'STRING', matched: '\'magic+\''}]);
nbql.lex('\'magic,\'').should.eql([{token: 'STRING', matched: '\'magic,\''}]);
nbql.lex('\'magic-\'').should.eql([{token: 'STRING', matched: '\'magic-\''}]);
nbql.lex('\'magic>\'').should.eql([{token: 'STRING', matched: '\'magic>\''}]);
nbql.lex('\'magic<\'').should.eql([{token: 'STRING', matched: '\'magic<\''}]);
});
it('should permit special chars inside a STRING, not including quotes', function () {
nbql.lex('\'t+st\'').should.eql([{token: 'STRING', matched: '\'t+st\''}]);
nbql.lex('\'t,st\'').should.eql([{token: 'STRING', matched: '\'t,st\''}]);
nbql.lex('\'t(st\'').should.eql([{token: 'STRING', matched: '\'t(st\''}]);
nbql.lex('\'t)st\'').should.eql([{token: 'STRING', matched: '\'t)st\''}]);
nbql.lex('\'t>st\'').should.eql([{token: 'STRING', matched: '\'t>st\''}]);
nbql.lex('\'t<st\'').should.eql([{token: 'STRING', matched: '\'t<st\''}]);
nbql.lex('\'t=st\'').should.eql([{token: 'STRING', matched: '\'t=st\''}]);
nbql.lex('\'t[st\'').should.eql([{token: 'STRING', matched: '\'t[st\''}]);
nbql.lex('\'t]st\'').should.eql([{token: 'STRING', matched: '\'t]st\''}]);
});
it('should NOT permit quotes inside a STRING', function () {
(function () { nbql.lex('\'t\'st\'');}).should.throw(lexicalError);
(function () { nbql.lex('\'t"st\'');}).should.throw(lexicalError);
});
it('should permit escaped quotes inside a String', function () {
nbql.lex('\'t\\\'st\'').should.eql([{token: 'STRING', matched: '\'t\\\'st\''}]);
nbql.lex('\'t\\"st\'').should.eql([{token: 'STRING', matched: '\'t\\"st\''}]);
});
});
describe('single & double QUOTE marks', function () {
it('CANNOT match an UNescaped double quote in a LITERAL', function () {
(function () {nbql.lex('thing"amabob');}).should.throw(lexicalError);
});
it('CANNOT match an UNescaped single quote in a LITERAL', function () {
(function () {nbql.lex('thing\'amabob');}).should.throw(lexicalError);
});
it('CANNOT match an UNescaped double quote in a STRING', function () {
(function () {nbql.lex('\'thing"amabob\'');}).should.throw(lexicalError);
});
it('CANNOT match an UNescaped single quote in a STRING', function () {
(function () {nbql.lex('\'thing\'amabob\'');}).should.throw(lexicalError);
});
it('CAN match an escaped double quote in a LITERAL', function () {
nbql.lex('thing\\"amabob').should.eql([{token: 'LITERAL', matched: 'thing\\"amabob'}]);
});
it('CAN match an escaped single quote in a LITERAL', function () {
nbql.lex('thing\\\'amabob').should.eql([{token: 'LITERAL', matched: 'thing\\\'amabob'}]);
});
it('CAN match an escaped double quote in a STRING', function () {
nbql.lex('\'thing\\"amabob\'').should.eql([{token: 'STRING', matched: '\'thing\\"amabob\''}]);
});
it('CAN match an escaped single quote in a STRING', function () {
nbql.lex('\'thing\\\'amabob\'').should.eql([{token: 'STRING', matched: '\'thing\\\'amabob\''}]);
});
});
describe('Filter expressions', function () {
it('should separate NOT at beginning of literal', function () {
nbql.lex('tag:-photo').should.eql([
{token: 'PROP', matched: 'tag:'},
{token: 'NOT', matched: '-'},
{token: 'LITERAL', matched: 'photo'}
]);
nbql.lex('tag:-photo-graph').should.eql([
{token: 'PROP', matched: 'tag:'},
{token: 'NOT', matched: '-'},
{token: 'LITERAL', matched: 'photo-graph'}
]);
nbql.lex('tags:[-getting-started]').should.eql([
{token: 'PROP', matched: 'tags:'},
{token: 'LBRACKET', matched: '['},
{token: 'NOT', matched: '-'},
{token: 'LITERAL', matched: 'getting-started'},
{token: 'RBRACKET', matched: ']'}
]);
});
it('should permit NOT inside a literal', function () {
nbql.lex('tags:getting-started').should.eql([
{token: 'PROP', matched: 'tags:'},
{token: 'LITERAL', matched: 'getting-started'}
]);
nbql.lex('tags:[getting-started]').should.eql([
{token: 'PROP', matched: 'tags:'},
{token: 'LBRACKET', matched: '['},
{token: 'LITERAL', matched: 'getting-started'},
{token: 'RBRACKET', matched: ']'}
]);
nbql.lex('tags:-[getting-started]').should.eql([
{token: 'PROP', matched: 'tags:'},
{token: 'NOT', matched: '-'},
{token: 'LBRACKET', matched: '['},
{token: 'LITERAL', matched: 'getting-started'},
{token: 'RBRACKET', matched: ']'}
]);
nbql.lex('id:-1+tags:[getting-started]').should.eql([
{token: 'PROP', matched: 'id:'},
{token: 'NOT', matched: '-'},
{token: 'NUMBER', matched: '1'},
{token: 'AND', matched: '+'},
{token: 'PROP', matched: 'tags:'},
{token: 'LBRACKET', matched: '['},
{token: 'LITERAL', matched: 'getting-started'},
{token: 'RBRACKET', matched: ']'}
]);
});
});
describe('complex examples', function () {
it('many expressions', function () {
nbql.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'}
]);
// nbql.lex("tag:photo+image:-null,tag.count:>5").should.eql();
});
it('grouped expressions', function () {
// nbql.lex("author:-joe+(tag:photo,image:-null,featured:true)").should.eql();
});
it('in expressions', function () {
nbql.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: ']'}
]);
// nbql.lex("author:-joe+tag:-[photo,video]").should.eql();
// nbql.lex("author:-joe+tag:[photo,video]+post.count:>5+post.count:<100").should.eql();
});
});
});