node-jpath
Version:
jPath: Traversal utility to help you digg deeper into complex objects or arrays of objects
319 lines (309 loc) • 9.47 kB
JavaScript
// node-jpath.js - is a library that allows filtering of JSON data based on pattern-like expressions
(function(Array, undef) {
var
TRUE = !0,
FALSE = !1,
STRING = "string",
FUNCTION = "function",
PERIOD = ".",
EMPTY = '',
NULL = null,
rxTokens = /([A-Za-z0-9_\*@\$\(\)]+(?:\[.+?\])?)/g,
rxIndex = /^(\S+)\((\d+)\)$/,
rxPairs = /(\(+)?([\w\.\(\)\$\_]+)(?:\s*)([\=\^\!\*\~\>\<\?\$]{1,2})\s*(?:\s*)("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^' \&\|\)\(]+)\s*(\)+)?/g,
rxCondition = /(\S+)\[(.+)\]/,
rxEscQuote = /\\('|")/g,
app = Array.prototype.push,
apc = Array.prototype.concat,
/**
* Private API
* @type {Object}
*/
hidden = {
/**
* Function that strips wrapping quotes
* @param {String} s String that contains quotes around a word
* @return {String} Word without quotes
*/
qtrim: function(s) {
return((!s.indexOf("'") || !s.indexOf('"')) && (s.slice(-1) === "'" || s.slice(-1) === '"')) ? s.slice(1, -1) : s;
},
/**
* Converts an object into an Array if it isn't
* @param {Object} o Any type of object
* @return {Array} Array of an object or an empty Array
*/
toArray: function(o) {
return o instanceof Array ? o : (o === undef || o === NULL) ? [] : [o];
},
/**
* Recursive function that walks through an object, extracting pattern matches
* @param {String} pattern jPath expression
* @param {Function} cfn Callback function used to run a custom comparisson
* @param {Object|Array} obj An object or an Array that will be scanned for matches
* @return {Array} Matching results
*/
traverse: function(pattern, cfn, obj) {
var out, data = (obj || this.data),
temp, tokens, token, idxToken, index, expToken, condition, tail, self = arguments.callee,
found, i, j, l;
if(data && typeof(pattern) === STRING) {
tokens = pattern.match(rxTokens); //dot notation splitter
//Get first token
token = tokens[0];
//Trailing tokens
tail = tokens.slice(1).join(PERIOD);
if(data instanceof Array) {
temp = [];
for(i = 0, j;
(j = data[i]) != NULL; i++) {
found = self.call(this, token, cfn, j);
if(((found instanceof Array) && found.length) || found !== undef) {
app.call(temp, found);
}
}
if(temp.length) {
return tail ? self.call(this, tail, cfn, temp) : temp;
} else {
return;
}
} else if(token === "*") {
return tail ? self.call(this, tail, cfn, data) : data;
} else if(data[token] !== undef) {
return tail ? self.call(this, tail, cfn, data[token]) : data[token];
} else if(rxIndex.test(token)) {
idxToken = token.match(rxIndex);
token = idxToken[1];
index = +idxToken[2];
temp = data[token];
return tail ? self.call(this, tail, cfn, (temp && temp.length) ? temp[index] : temp) : (temp && temp.length) ? temp[index] : temp;
} else if(rxCondition.test(token)) {
expToken = token.match(rxCondition);
token = expToken[1];
condition = expToken[2];
var evalStr, isMatch, subset = token === "*" ? data : data[token],
elem;
if(subset instanceof Array) {
temp = [];
//Second loop here is faster than recursive call
for(i = 0;
(elem = subset[i]) != NULL; i++) {
//Convert condition pairs to booleans
evalStr = condition.replace(rxPairs, function(match, pl, left, operator, right, pr) {
return [pl, hidden.testPairs.call(elem, left, right, operator, cfn), pr].join(EMPTY);
});
//Evaluate expression
isMatch = eval(evalStr);
if(isMatch) {
app.call(temp, elem);
}
}
if(temp.length) {
return tail ? self.call(this, tail, cfn, temp) : temp;
} else {
return;
}
} else {
elem = subset;
//Convert condition pairs to booleans
evalStr = condition.replace(rxPairs, function(match, pl, left, operator, right, pr) {
return [pl, hidden.testPairs.call(elem, left, right, operator, cfn), pr].join(EMPTY);
});
//Evaluate expression
isMatch = eval(evalStr);
if(isMatch) {
return tail ? self.call(this, tail, cfn, elem) : elem;
}
}
}
}
return out;
},
//Matches type of a to b
matchTypes: function(a, b) {
var _a, _b;
switch(typeof(a)) {
case STRING:
_b = b + EMPTY;
break;
case "boolean":
_b = b === "true" ? TRUE : FALSE;
break;
case "number":
_b = +b;
break;
case "date":
_b = new Date(b).valueOf();
_a = a.valueOf();
break;
default:
_b = b;
}
if(b === "null") {
_b = NULL;
}
if(b === "undefined") {
_b = undef;
}
return {
left: (_a || a),
right: _b
};
},
//Condition functions
testPairs: (function() {
var conditions = {
"=": function(l, r) {
return l === r;
},
"==": function(l, r) {
return l === r;
},
"!=": function(l, r) {
return l !== r;
},
"<": function(l, r) {
return l < r;
},
"<=": function(l, r) {
return l <= r;
},
">": function(l, r) {
return l > r;
},
">=": function(l, r) {
return l >= r;
},
"~=": function(l, r) {
return(l + EMPTY).toLowerCase() === (r + EMPTY).toLowerCase();
},
"^=": function(l, r) {
return !((l + EMPTY).indexOf(r));
},
"$=": function(l, r) {
return(r + EMPTY) === (l + EMPTY).slice(-(r + EMPTY).length);
},
"*=": function(l, r) {
return(l + EMPTY).toLowerCase().indexOf((r + EMPTY).toLowerCase()) !== -1;
}
};
return function(left, right, operator, fn) {
var out = FALSE,
leftVal = left.indexOf(PERIOD) >= 0 ? hidden.traverse(left, NULL, this) : this[left],
//We clean up r to remove wrapping quotes and escaped quotes (both single/dbl)
pairs = hidden.matchTypes(leftVal, hidden.qtrim(right).trim().replace(rxEscQuote, '$1'));
if(operator === "?") {
if(typeof(fn) === FUNCTION) {
out = fn.call(this, pairs.left, right);
}
} else {
out = conditions[operator](pairs.left, pairs.right);
}
return out;
};
})(),
/**
* Merges results of sibling nodes into a single Array
* @param {String} pattern String pattern or results
* @return {Array} Concatinated results
*/
merge: function(pattern) {
var out = [],
temp = hidden.toArray(pattern ? hidden.traverse.apply(this, arguments) : this.selection);
out = apc.apply([], temp);
return out;
}
};
/**
* JPath Class
* @param {Object|Array} obj Search subject
*/
function JPath(obj) {
if(!(this instanceof JPath)) {
return new JPath(obj);
}
this.data = obj || NULL;
this.selection = [];
}
JPath.prototype = {
/**
* Sets search subject (source of data)
* @param {Object|Array} obj Search subject
* @return {this}
*/
from: function(obj) {
this.data = obj;
return this;
},
/**
* Returns a first match element
* @return {Var} Any type of object located in the first element of the result Array
*/
first: function() {
return this.selection.length ? this.selection[0] : NULL;
},
/**
* Returns a last match element
* @return {Var} Any type of object located in the last element of the result Array
*/
last: function() {
return this.selection.length ? this.selection.slice(-1)[0] : NULL;
},
/**
* Returns an exact match element located at idx position
* @param {Number} idx Index
* @return {Var} Any type of object located in result Array[idx]
*/
eq: function(idx) {
return this.selection.length ? this.selection[idx] : NULL;
},
/**
* Applies matching pattern to an object
* @param {String} pattern jPath expression
* @param {Function} cfn Custom comparisson function
* @param {Object|Array} obj Search subject object
* @return {this}
*/
select: function(pattern, cfn, obj) {
this.selection = hidden.merge.apply(this, arguments);
return this;
},
/**
* Merges additional pattern-matching results with existing ones
* @param {String} pattern jPath expression
* @return {this}
*/
and: function(pattern) {
this.selection = this.selection.concat(hidden.merge.apply(this, arguments));
return this;
},
/**
* Returns all matches
* @return {Array}
*/
val: function() {
return this.selection;
}
};
//Extend module
/**
* Runs a select filter against an object and returns an instance of a JPath object
* @param {Object|Array} obj Search subject
* @param {String} pattern jPath expression
* @param {Function} cfn Custom comparisson function (optional)
* @return {JPath} Instance of a JPath object pre-filled with results
*/
module.exports.select = function(obj, pattern, cfn) {
return JPath(obj).select(pattern, cfn, NULL);
};
/**
* Returns results of the pattern-matching as an Array
* @param {Object|Array} obj Search subject
* @param {String} pattern jPath expression
* @param {Function} cfn Custom comparisson function (optional)
* @return {Array} Search results
*/
module.exports.filter = function(obj, pattern, cfn) {
return JPath(obj).select(pattern, cfn, NULL).val();
};
})(Array);