Permalink
Browse files

Merge branch 'master' of http://github.com/kriszyp/rql

  • Loading branch information...
2 parents 1a1b5d9 + f42a85c commit 704ddd7d6de22c9ca5ba981a597a287d19dd103c @dvv dvv committed Nov 30, 2010
Showing with 90 additions and 36 deletions.
  1. +10 −3 README.md
  2. +33 −22 lib/js-array.js
  3. +8 −2 lib/parser.js
  4. +37 −0 test/js-array.js
  5. +2 −9 {tests → test}/query.js
View
13 README.md
@@ -26,7 +26,9 @@ using chained operator calls in JavaScript. We could
write this query:
var Query = require("rql/query").Query;
- new Query().eq("foo",3).forEach(...);
+ var fooEq3Query = new Query().eq("foo",3);
+
+
The RQL grammar is based around standard URI delimiters. The standard rules for
encoding strings with URL encoding (%xx) are observed. RQL also supersets FIQL.
@@ -77,11 +79,16 @@ we can do:
foo=string:3
-Any property can be nested by using a dot syntax. To search by the bar property of
+Any property can be nested by using an array of properties. To search by the bar property of
the object in the foo property we can do:
- foo.bar=3
+ (foo,bar)=3
+We can also use slashes as shorthand for arrays, so we could equivalently write the nested
+query:
+
+ foo/bar=3
+
Another common operator is sort. We can use the sort operator to sort by a specified property.
To sort by foo in ascending order:
View
55 lib/js-array.js
@@ -8,7 +8,9 @@
define(["exports", "./parser"], function(exports, parser){
var parseQuery = parser.parseQuery;
-
+var stringify = JSON.stringify || function(str){
+ return '"' + str.replace(/"/g, "\\\"") + '"';
+};
exports.jsOperatorMap = {
"eq" : "===",
"ne" : "!==",
@@ -17,6 +19,7 @@ exports.jsOperatorMap = {
"lt" : "<",
"gt" : ">"
};
+var negated = false;
exports.operators = {
sort: function(){
var terms = [];
@@ -45,15 +48,14 @@ exports.operators = {
"in": filter(function(value, values){
return values.indexOf(value) > -1;
}),
- notin: filter(function(value, values){
- return values.indexOf(value) === -1;
- }),
- contains: filter(function(values, value){
- return values.indexOf(value) > -1;
- }),
- notcontains: filter(function(values, value){
- return values.indexOf(value) === -1;
- }),
+ not: function(expression){
+ negated = true;
+ try{
+ return expression.call(this);
+ }finally{
+ negated = false;
+ }
+ },
any: filter(function(array, value){
if(typeof value == "function"){
return array.some(function(i){
@@ -271,13 +273,14 @@ exports.operators = {
}
};
exports.filter = filter;
-function filter(condition){
+function filter(condition, not){
+ // convert to boolean right now
var filter = function(property, second){
var args = arguments;
var filtered = [];
for(var i = 0, length = this.length; i < length; i++){
var item = this[i];
- if(condition(evaluateProperty(item, property), second)){
+ if(!condition(evaluateProperty(item, property), second) == negated){
filtered.push(item);
}
}
@@ -299,15 +302,15 @@ function reducer(func){
}
exports.evaluateProperty = evaluateProperty;
function evaluateProperty(object, property){
- if(property.indexOf(".") === -1){
- return object[decodeURIComponent(property)];
- }
- else{
- property.split(".").forEach(function(part){
+ if(property instanceof Array){
+ property.forEach(function(part){
object = object[decodeURIComponent(part)];
});
return object;
}
+ else{
+ return object[decodeURIComponent(property)];
+ }
};
var conditionEvaluator = exports.conditionEvaluator = function(condition){
var jsOperator = exports.jsOperatorMap[term.name];
@@ -326,7 +329,6 @@ exports.query = query;
exports.missingOperator = function(operator){
throw new Error("Operator " + operator + " is not defined");
}
-function relay(){return this;} // TODO: may be control by options?
function query(query, options, target){
options = options || {};
query = parseQuery(query, options.parameters);
@@ -344,9 +346,18 @@ function query(query, options, target){
var jsOperator = exports.jsOperatorMap[value.name];
if(jsOperator){
// item['foo.bar'] ==> (item && item.foo && item.foo.bar && ...)
- var path = String(value.args[0]);
- var item = path ? path.split('.').map(function(x,i,a){return 'item.'+a.slice(0,i+1).join('.')}).join('&&') : 'item';
- item = '('+item+')';
+ var path = value.args[0];
+ if(path instanceof Array){
+ var item = "(";
+ var escaped = [];
+ for(var i = 0;i < path.length; i++){
+ escaped.push(stringify(path[i]));
+ item += (item.length > 1 ? "&&" : "") + "item[" + escaped.join("][") + ']';
+ }
+ }else{
+ var item = "(item[" + stringify(path) + "]";
+ }
+ item += ')';
if (jsOperator === '===' && value.args[1] instanceof RegExp){
// N.B. matching requires String
item = 'String('+item + '||"")';
@@ -360,7 +371,7 @@ function query(query, options, target){
")})";
}
}else{
- return typeof value === "number" ? value : JSON.stringify(value);
+ return typeof value === "number" ? value : stringify(value);
}
}
var evaluator = eval("(function(target){return " + queryToJS(query) + ".call(target);})");
View
10 lib/parser.js
@@ -43,9 +43,15 @@ function parse(/*String|Object*/query, parameters){
if(exports.jsonQueryCompatible){
query = query.replace(/%3C=/g,"=le=").replace(/%3E=/g,"=ge=").replace(/%3C/g,"=lt=").replace(/%3E/g,"=gt=");
}
+ if(query.indexOf("/") > -1){ // performance guard
+ // convert slash delimited text to arrays
+ query = query.replace(/[\+\*\$\-:\w%\._]*\/[\+\*\$\-:\w%\._\/]*/g, function(slashed){
+ return "(" + slashed.replace(/\//g, ",") + ")";
+ });
+ }
// convert FIQL to normalized call syntax form
- query = query.replace(/([\+\*\-:\w%\._]+)([<>!]?=(?:[\w]*=)?|>|<)(\([\+\*\$\-:\w%\._,]+\)|[\+\*\$\-:\w%\._]*|)/g,
- //<--- property ---><------ operator -----><---------------- value ------------------>
+ query = query.replace(/(\([\+\*\$\-:\w%\._,]+\)|[\+\*\$\-:\w%\._]*|)([<>!]?=(?:[\w]*=)?|>|<)(\([\+\*\$\-:\w%\._,]+\)|[\+\*\$\-:\w%\._]*|)/g,
+ //<--------- property -----------><------ operator -----><---------------- value ------------------>
function(t, property, operator, value){
if(operator.length < 3){
if(!operatorMap[operator]){
View
37 test/js-array.js
@@ -0,0 +1,37 @@
+var assert = require("assert"),
+ Query = require("../lib/query").Query,
+ executeQuery = require("../lib/js-array").executeQuery;
+
+var data = [
+ {
+ "with.dot": "dotted",
+ "nested": {
+ "property": "value"
+ },
+ "price": 10,
+ "name": "ten",
+ "tags": ["fun", "even"]
+ },
+ {
+ "price": 5,
+ "name": "five",
+ "tags": ["fun"]
+ }];
+
+exports.testFiltering = function() {
+ assert.equal(executeQuery("price=lt=10", {}, data).length, 1);
+ assert.equal(executeQuery("price=lt=11", {}, data).length, 2);
+ assert.equal(executeQuery("nested/property=value", {}, data).length, 1);
+ assert.equal(executeQuery("with.dot=dotted", {}, data).length, 1);
+ assert.equal(executeQuery("not(in(price,(5,10)))", {}, data).length, 0);
+ assert.equal(executeQuery("not(in(price,(5)))", {}, data).length, 1);
+ assert.equal(executeQuery("any(tags,even)", {}, data).length, 1);
+ assert.equal(executeQuery("any(tags,fun)", {}, data).length, 2);
+ assert.equal(executeQuery("all(tags,fun)", {}, data).length, 1);
+ assert.equal(executeQuery("all(tags,even)", {}, data).length, 0);
+ assert.equal(executeQuery("not(all(tags,even))", {}, data).length, 2);
+};
+
+
+if (require.main === module)
+ require("patr/runner").run(exports);
View
11 tests/query.js → test/query.js
@@ -1,9 +1,7 @@
var assert = require("assert"),
Query = require("../lib/query").Query,
parseQuery = require("../lib/parser").parseQuery;
- executeQuery = require("../lib/js-array").executeQuery;
-
-Array.prototype.match = function(query, params){return executeQuery(query, params || [], this);};
+exports.testJSArray = require("./js-array");
exports.testBehavior = function() {
//assert.error(parseQuery(), "parseQuery requires a string");
@@ -22,6 +20,7 @@ var queryPairs = {
{"a(b,c)": {name:"and", args:[{name:"a", args:["b", "c"]}]}},
{"a((b),c)": {"name": "and", args:[{name:"a", args:[["b"], "c"]}]}},
{"a((b,c),d)": {name:"and", args:[{name:"a", args:[["b", "c"], "d"]}]}},
+ {"a(b/c,d)": {name:"and", args:[{name:"a", args:[["b", "c"], "d"]}]}},
{"a(b)&c(d(e))": {name:"and", args:[
{name:"a", args:["b"]},
{name:"c", args:[{name:"d", args:["e"]}]}
@@ -159,12 +158,6 @@ exports.testBindParameters = function() {
assert.deepEqual(parsed, {name: 'and', args: [{name: 'eq', args: ['id', 'a']}], cache: {id: 'a'}});
};
-exports.testExecution = function() {
- // TODO
- // nested props: https://github.com/kriszyp/rql/issues/#issue/15
- assert.deepEqual([{"path":[1,2,3]},{"path":[9,3,7]}].match("foo.cat=3"), []);
-};
-
exports.testStringification = function() {
// TODO
var parsed;

0 comments on commit 704ddd7

Please sign in to comment.