In a project I had to translate a search query into a filtering function to apply to a stream of JavaScript objects. The query language was very simple, boolean expressions of property constraints, for example “name:john AND age:52” where the search should return all the objects with property “name” equals to “john” and property “age” equals to 52.
The first step was to parse the search query with a parser. You can write a parser by hand or your can use a parser generator like PEG.js. I prefered the second one.
With the online editor I’ve wrote the language grammar:
start = andExpression
andExpression = orExpression (and andExpression)?
orExpression = expression (or andExpression)?
expression = string ':' string
string = char*
char = [a-zA-Z0-9]
and = ws 'AND'i ws
or = ws 'OR'i ws
ws = [ \t\n\r]*
The generated parser takes as input the text to parse and returns the parsed elements:
Output
[
[
[
[
"n",
"a",
"m",
"e"
],
":",
[
"j",
"o",
"h",
"n"
]
],
null
],
[
[
[
" "
],
"AND",
[
" "
]
],
[
[
[
[
"a",
"g",
"e"
],
":",
[
"5",
"2"
]
],
null
],
null
]
]
]
The grammar can be changed to make the parser method return a boolean expression:
{
function combine(left, right) {
if (right === null) return left;
return left + " " + right[0] + " " +right[1];
}
}
start = ex:andExpression
{return 'return '+ex+';';}
andExpression = left:orExpression right:(and andExpression)?
{return combine(left,right);}
orExpression = left:expression right:(or andExpression)?
{return combine(left,right);}
expression = name:string ':' value:string
{return 'LangParser.hasKeyValue(obj,\''+name+'\', \''+value+'\')';}
string = chars:char*
{ return chars.join(""); }
char = [a-zA-Z0-9]
and = ws 'AND'i ws
{return '&&';}
or = ws 'OR'i ws
{return '||';}
ws = [ \t\n\r]*
The parser for a query like “name:john AND age:52” generates the following expression:
return LangParser.hasKeyValue(obj,'name', 'john')
&& LangParser.hasKeyValue(obj,'age', '52');
In the generated expression I’ve used an utility method that checks if the specified couple of name and value exists as property for the specified object:
/**
* Checks if the passed Object has the specified couple of key and value as property.
* @param obj the object to check.
* @param key the key.
* @param value the value-
* @returns {Boolean} <code>true</code> if the couple is found, <code>false</code> otherwise.
*/
LangParser.hasKeyValue = function (obj, key, value) {
return obj.hasOwnProperty(key) && value === obj[key];
};
The expression can be then passed as parameter in the function constructor:
var expression = parser.parse(query);
var filterFunction = new Function("obj", expression);
Now you can use the new function as filter, for example with stream.js streams:
var filtered = new Stream.filter(filterFunction).toArray();
The grammar can be expanded with more operators and capabilities.