Comparing version 0.10.0 to 0.11.0
@@ -0,14 +1,31 @@ | ||
## Dec 12, 2015 | ||
- Breaking change: For unwrapped results, return undefined instead of false upon failure to find path (to allow distinguishing of undefined--a non-allowed JSON value--from the valid JSON, null or false) and return the exact value upon falsy single results (in order to allow return of null) | ||
- Feature: AMD export | ||
- Feature: Offer new class-based API and object-based arguments (with option to run new queries via evaluate() method without resupplying config) | ||
- Feature: Allow new preventEval=true and autostart=false option | ||
- Feature: Allow new callback option to allow a callback function to execute as each final result node is obtained | ||
- Feature: Allow type operators: JavaScript types (@boolean(), @number(), @string()), other fundamental JavaScript types (@null(), @object(), @array()), the JSONSchema-added type, @integer(), and the following non-JSON types that can nevertheless be used with JSONPath when querying non-JSON JavaScript objects (@undefined(), @function(), @nonFinite()). Finally, @other() is made available in conjunction with a new callback option, `otherTypeCallback`, can be used to allow user-defined type detection (at least until JSON Schema awareness may be provided). | ||
- Feature: Support "parent" and "parentProperty" for resultType along with "all" (which also includes "path" and "value" together) | ||
- Feature: Support custom @parent, @parentProperty, @property (in addition to custom property @path) inside evaluations | ||
- Feature: Support a custom operator ("~") to allow grabbing of property names | ||
- Feature: Support "$" for retrieval of root, and document this as well as "$.." behavior | ||
- Feature: Expose cache on JSONPath.cache for those who wish to preserve and reuse it | ||
- Feature: Expose class methods `toPathString` for converting a path as array into a (normalized) path as string and `toPathArray` for the reverse (though accepting unnormalized strings as well as normalized) | ||
- Fix: Allow "^" as property name | ||
- Fix: Support "." within properties | ||
- Fix: @path in index/property evaluations | ||
- Version 0.11 | ||
## Oct 23, 2013 | ||
* Support for parent selection via `^` | ||
* Access current path via `@path` in test statements | ||
* Allowing for multi-statement evals | ||
* Performance improvements | ||
* Version 0.10 | ||
- Support for parent selection via `^` | ||
- Access current path via `@path` in test statements | ||
- Allowing for multi-statement evals | ||
- Performance improvements | ||
- Version 0.10 | ||
## Mar 28, 2012 | ||
* Support a sandbox arg to eval | ||
* Use vm.runInNewContext in place of eval | ||
* Version 0.9.0 | ||
- Support a sandbox arg to eval | ||
- Use vm.runInNewContext in place of eval | ||
- Version 0.9.0 |
@@ -0,1 +1,3 @@ | ||
/*global module, exports, require*/ | ||
/*jslint vars:true, evil:true*/ | ||
/* JSONPath 0.8.0 - XPath for JSON | ||
@@ -7,10 +9,9 @@ * | ||
var isNode = false; | ||
(function(exports, require) { | ||
(function (require) {'use strict'; | ||
// Keep compatibility with old browsers | ||
if (!Array.isArray) { | ||
Array.isArray = function(vArg) { | ||
return Object.prototype.toString.call(vArg) === "[object Array]"; | ||
}; | ||
Array.isArray = function (vArg) { | ||
return Object.prototype.toString.call(vArg) === '[object Array]'; | ||
}; | ||
} | ||
@@ -22,153 +23,410 @@ | ||
var allowedResultTypes = ['value', 'path', 'parent', 'parentProperty', 'all']; | ||
var vm = isNode ? | ||
require('vm') : { | ||
runInNewContext: function(expr, context) { with (context) return eval(expr); } | ||
runInNewContext: function (expr, context) { | ||
return eval(Object.keys(context).reduce(function (s, vr) { | ||
return 'var ' + vr + '=' + JSON.stringify(context[vr]).replace(/\u2028|\u2029/g, function (m) { | ||
// http://www.thespanner.co.uk/2011/07/25/the-json-specification-is-now-wrong/ | ||
return '\\u202' + (m === '\u2028' ? '8' : '9'); | ||
}) + ';' + s; | ||
}, expr)); | ||
} | ||
}; | ||
exports.eval = jsonPath; | ||
var cache = {}; | ||
function push (arr, elem) {arr = arr.slice(); arr.push(elem); return arr;} | ||
function unshift (elem, arr) {arr = arr.slice(); arr.unshift(elem); return arr;} | ||
function push(arr, elem) { arr = arr.slice(); arr.push(elem); return arr; } | ||
function unshift(elem, arr) { arr = arr.slice(); arr.unshift(elem); return arr; } | ||
function JSONPath (opts, expr, obj, callback) { | ||
if (!(this instanceof JSONPath)) { | ||
try { | ||
return new JSONPath(opts, expr, obj, callback); | ||
} | ||
catch (e) { | ||
if (!e.avoidNew) { | ||
throw e; | ||
} | ||
return e.value; | ||
} | ||
} | ||
function jsonPath(obj, expr, arg) { | ||
var P = { | ||
resultType: arg && arg.resultType || "VALUE", | ||
flatten: arg && arg.flatten || false, | ||
wrap: (arg && arg.hasOwnProperty('wrap')) ? arg.wrap : true, | ||
sandbox: (arg && arg.sandbox) ? arg.sandbox : {}, | ||
normalize: function(expr) { | ||
if (cache[expr]) return cache[expr]; | ||
var subx = []; | ||
var normalized = expr.replace(/[\['](\??\(.*?\))[\]']/g, function($0,$1){return "[#"+(subx.push($1)-1)+"]";}) | ||
.replace(/'?\.'?|\['?/g, ";") | ||
.replace(/(;)?(\^+)(;)?/g, function(_, front, ups, back) { return ';' + ups.split('').join(';') + ';'; }) | ||
.replace(/;;;|;;/g, ";..;") | ||
.replace(/;$|'?\]|'$/g, ""); | ||
var exprList = normalized.split(';').map(function(expr) { | ||
var match = expr.match(/#([0-9]+)/); | ||
return !match || !match[1] ? expr : subx[match[1]]; | ||
}) | ||
return cache[expr] = exprList; | ||
}, | ||
asPath: function(path) { | ||
var x = path, p = "$"; | ||
for (var i=1,n=x.length; i<n; i++) | ||
p += /^[0-9*]+$/.test(x[i]) ? ("["+x[i]+"]") : ("['"+x[i]+"']"); | ||
return p; | ||
}, | ||
trace: function(expr, val, path) { | ||
// no expr to follow? return path and value as the result of this trace branch | ||
if (!expr.length) return [{path: path, value: val}]; | ||
opts = opts || {}; | ||
var objArgs = opts.hasOwnProperty('json') && opts.hasOwnProperty('path'); | ||
this.json = opts.json || obj; | ||
this.path = opts.path || expr; | ||
this.resultType = (opts.resultType && opts.resultType.toLowerCase()) || 'value'; | ||
this.flatten = opts.flatten || false; | ||
this.wrap = opts.hasOwnProperty('wrap') ? opts.wrap : true; | ||
this.sandbox = opts.sandbox || {}; | ||
this.preventEval = opts.preventEval || false; | ||
this.parent = opts.parent || null; | ||
this.parentProperty = opts.parentProperty || null; | ||
this.callback = opts.callback || null; | ||
this.otherTypeCallback = opts.otherTypeCallback || function () { | ||
throw "You must supply an otherTypeCallback callback option with the @other() operator."; | ||
}; | ||
var loc = expr[0], x = expr.slice(1); | ||
// the parent sel computation is handled in the frame above using the | ||
// ancestor object of val | ||
if (loc === '^') return path.length ? [{path: path.slice(0,-1), expr: x, isParentSelector: true}] : []; | ||
if (opts.autostart !== false) { | ||
var ret = this.evaluate({ | ||
path: (objArgs ? opts.path : expr), | ||
json: (objArgs ? opts.json : obj) | ||
}); | ||
if (!ret || typeof ret !== 'object') { | ||
throw {avoidNew: true, value: ret, message: "JSONPath should not be called with 'new' (it prevents return of (unwrapped) scalar values)"}; | ||
} | ||
return ret; | ||
} | ||
} | ||
// we need to gather the return value of recursive trace calls in order to | ||
// do the parent sel computation. | ||
var ret = []; | ||
function addRet(elems) { ret = ret.concat(elems); } | ||
// PUBLIC METHODS | ||
if (val && val.hasOwnProperty(loc)) // simple case, directly follow property | ||
addRet(P.trace(x, val[loc], push(path, loc))); | ||
else if (loc === "*") { // any property | ||
P.walk(loc, x, val, path, function(m,l,x,v,p) { | ||
addRet(P.trace(unshift(m, x), v, p)); }); | ||
} | ||
else if (loc === "..") { // all chid properties | ||
addRet(P.trace(x, val, path)); | ||
P.walk(loc, x, val, path, function(m,l,x,v,p) { | ||
if (typeof v[m] === "object") | ||
addRet(P.trace(unshift("..", x), v[m], push(p, m))); | ||
}); | ||
} | ||
else if (loc[0] === '(') { // [(expr)] | ||
addRet(P.trace(unshift(P.eval(loc, val, path[path.length], path),x), val, path)); | ||
} | ||
else if (loc.indexOf('?(') === 0) { // [?(expr)] | ||
P.walk(loc, x, val, path, function(m,l,x,v,p) { | ||
if (P.eval(l.replace(/^\?\((.*?)\)$/,"$1"),v[m],m, path)) | ||
addRet(P.trace(unshift(m,x),v,p)); | ||
}); | ||
} | ||
else if (loc.indexOf(',') > -1) { // [name1,name2,...] | ||
for (var parts = loc.split(','), i = 0; i < parts.length; i++) | ||
addRet(P.trace(unshift(parts[i], x), val, path)); | ||
} | ||
else if (/^(-?[0-9]*):(-?[0-9]*):?([0-9]*)$/.test(loc)) { // [start:end:step] python slice syntax | ||
addRet(P.slice(loc, x, val, path)); | ||
} | ||
JSONPath.prototype.evaluate = function (expr, json, callback, otherTypeCallback) { | ||
var self = this, | ||
flatten = this.flatten, | ||
wrap = this.wrap, | ||
currParent = this.parent, | ||
currParentProperty = this.parentProperty; | ||
// we check the resulting values for parent selections. for parent | ||
// selections we discard the value object and continue the trace with the | ||
// current val object | ||
return ret.reduce(function(all, ea) { | ||
return all.concat(ea.isParentSelector ? P.trace(ea.expr, val, ea.path) : [ea]); | ||
}, []); | ||
}, | ||
walk: function(loc, expr, val, path, f) { | ||
if (Array.isArray(val)) | ||
for (var i = 0, n = val.length; i < n; i++) | ||
f(i, loc, expr, val, path); | ||
else if (typeof val === "object") | ||
for (var m in val) | ||
if (val.hasOwnProperty(m)) | ||
f(m, loc, expr, val, path); | ||
}, | ||
slice: function(loc, expr, val, path) { | ||
if (!Array.isArray(val)) return; | ||
var len = val.length, parts = loc.split(':'), | ||
start = (parts[0] && parseInt(parts[0])) || 0, | ||
end = (parts[1] && parseInt(parts[1])) || len, | ||
step = (parts[2] && parseInt(parts[2])) || 1; | ||
start = (start < 0) ? Math.max(0,start+len) : Math.min(len,start); | ||
end = (end < 0) ? Math.max(0,end+len) : Math.min(len,end); | ||
var ret = []; | ||
for (var i = start; i < end; i += step) | ||
ret = ret.concat(P.trace(unshift(i,expr), val, path)); | ||
return ret; | ||
}, | ||
eval: function(code, _v, _vname, path) { | ||
if (!$ || !_v) return false; | ||
if (code.indexOf("@path") > -1) { | ||
P.sandbox["_path"] = P.asPath(path.concat([_vname])); | ||
code = code.replace(/@path/g, "_path"); | ||
} | ||
if (code.indexOf("@") > -1) { | ||
P.sandbox["_v"] = _v; | ||
code = code.replace(/@/g, "_v"); | ||
} | ||
try { | ||
return vm.runInNewContext(code, P.sandbox); | ||
} | ||
catch(e) { | ||
console.log(e); | ||
throw new Error("jsonPath: " + e.message + ": " + code); | ||
} | ||
} | ||
}; | ||
this.currResultType = this.resultType; | ||
this.currPreventEval = this.preventEval; | ||
this.currSandbox = this.sandbox; | ||
callback = callback || this.callback; | ||
this.currOtherTypeCallback = otherTypeCallback || this.otherTypeCallback; | ||
var $ = obj; | ||
var resultType = P.resultType.toLowerCase(); | ||
if (expr && obj && (resultType == "value" || resultType == "path")) { | ||
var exprList = P.normalize(expr); | ||
if (exprList[0] === "$" && exprList.length > 1) exprList.shift(); | ||
var result = P.trace(exprList, obj, ["$"]); | ||
result = result.filter(function(ea) { return ea && !ea.isParentSelector; }); | ||
if (!result.length) return P.wrap ? [] : false; | ||
if (result.length === 1 && !P.wrap && !Array.isArray(result[0].value)) return result[0][resultType] || false; | ||
return result.reduce(function(result, ea) { | ||
var valOrPath = ea[resultType]; | ||
if (resultType === 'path') valOrPath = P.asPath(valOrPath); | ||
if (P.flatten && Array.isArray(valOrPath)) { | ||
json = json || this.json; | ||
expr = expr || this.path; | ||
if (expr && typeof expr === 'object') { | ||
if (!expr.path) { | ||
throw "You must supply a 'path' property when providing an object argument to JSONPath.evaluate()."; | ||
} | ||
json = expr.hasOwnProperty('json') ? expr.json : json; | ||
flatten = expr.hasOwnProperty('flatten') ? expr.flatten : flatten; | ||
this.currResultType = expr.hasOwnProperty('resultType') ? expr.resultType : this.currResultType; | ||
this.currSandbox = expr.hasOwnProperty('sandbox') ? expr.sandbox : this.currSandbox; | ||
wrap = expr.hasOwnProperty('wrap') ? expr.wrap : wrap; | ||
this.currPreventEval = expr.hasOwnProperty('preventEval') ? expr.preventEval : this.currPreventEval; | ||
callback = expr.hasOwnProperty('callback') ? expr.callback : callback; | ||
this.currOtherTypeCallback = expr.hasOwnProperty('otherTypeCallback') ? expr.otherTypeCallback : this.currOtherTypeCallback; | ||
currParent = expr.hasOwnProperty('parent') ? expr.parent : currParent; | ||
currParentProperty = expr.hasOwnProperty('parentProperty') ? expr.parentProperty : currParentProperty; | ||
expr = expr.path; | ||
} | ||
currParent = currParent || null; | ||
currParentProperty = currParentProperty || null; | ||
if (Array.isArray(expr)) { | ||
expr = JSONPath.toPathString(expr); | ||
} | ||
if (!expr || !json || allowedResultTypes.indexOf(this.currResultType) === -1) { | ||
return; | ||
} | ||
this._obj = json; | ||
var exprList = JSONPath.toPathArray(expr); | ||
if (exprList[0] === '$' && exprList.length > 1) {exprList.shift();} | ||
var result = this._trace(exprList, json, ['$'], currParent, currParentProperty, callback); | ||
result = result.filter(function (ea) { return ea && !ea.isParentSelector; }); | ||
if (!result.length) {return wrap ? [] : undefined;} | ||
if (result.length === 1 && !wrap && !Array.isArray(result[0].value)) { | ||
return this._getPreferredOutput(result[0]); | ||
} | ||
return result.reduce(function (result, ea) { | ||
var valOrPath = self._getPreferredOutput(ea); | ||
if (flatten && Array.isArray(valOrPath)) { | ||
result = result.concat(valOrPath); | ||
} else { | ||
} | ||
else { | ||
result.push(valOrPath); | ||
} | ||
return result; | ||
}, []); | ||
} | ||
} | ||
return result; | ||
}, []); | ||
}; | ||
// PRIVATE METHODS | ||
JSONPath.prototype._getPreferredOutput = function (ea) { | ||
var resultType = this.currResultType; | ||
switch (resultType) { | ||
case 'all': | ||
ea.path = JSONPath.toPathString(ea.path); | ||
return ea; | ||
case 'value': case 'parent': case 'parentProperty': | ||
return ea[resultType]; | ||
case 'path': | ||
return JSONPath.toPathString(ea[resultType]); | ||
} | ||
}; | ||
JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) { | ||
if (callback) { | ||
var preferredOutput = this._getPreferredOutput(fullRetObj); | ||
fullRetObj.path = JSONPath.toPathString(fullRetObj.path); | ||
callback(preferredOutput, type, fullRetObj); | ||
} | ||
}; | ||
JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, callback) { | ||
// No expr to follow? return path and value as the result of this trace branch | ||
var retObj, self = this; | ||
if (!expr.length) { | ||
retObj = {path: path, value: val, parent: parent, parentProperty: parentPropName}; | ||
this._handleCallback(retObj, callback, 'value'); | ||
return retObj; | ||
} | ||
var loc = expr[0], x = expr.slice(1); | ||
// We need to gather the return value of recursive trace calls in order to | ||
// do the parent sel computation. | ||
var ret = []; | ||
function addRet (elems) {ret = ret.concat(elems);} | ||
if (val && val.hasOwnProperty(loc)) { // simple case--directly follow property | ||
addRet(this._trace(x, val[loc], push(path, loc), val, loc, callback)); | ||
} | ||
else if (loc === '*') { // all child properties | ||
this._walk(loc, x, val, path, parent, parentPropName, callback, function (m, l, x, v, p, par, pr, cb) { | ||
addRet(self._trace(unshift(m, x), v, p, par, pr, cb)); | ||
}); | ||
} | ||
else if (loc === '..') { // all descendent parent properties | ||
addRet(this._trace(x, val, path, parent, parentPropName, callback)); // Check remaining expression with val's immediate children | ||
this._walk(loc, x, val, path, parent, parentPropName, callback, function (m, l, x, v, p, par, pr, cb) { | ||
// We don't join m and x here because we only want parents, not scalar values | ||
if (typeof v[m] === 'object') { // Keep going with recursive descent on val's object children | ||
addRet(self._trace(unshift(l, x), v[m], push(p, m), v, m, cb)); | ||
} | ||
}); | ||
} | ||
else if (loc[0] === '(') { // [(expr)] (dynamic property/index) | ||
if (this.currPreventEval) { | ||
throw "Eval [(expr)] prevented in JSONPath expression."; | ||
} | ||
// As this will resolve to a property name (but we don't know it yet), property and parent information is relative to the parent of the property to which this expression will resolve | ||
addRet(this._trace(unshift(this._eval(loc, val, path[path.length - 1], path.slice(0, -1), parent, parentPropName), x), val, path, parent, parentPropName, callback)); | ||
} | ||
// The parent sel computation is handled in the frame above using the | ||
// ancestor object of val | ||
else if (loc === '^') { | ||
// This is not a final endpoint, so we do not invoke the callback here | ||
return path.length ? { | ||
path: path.slice(0, -1), | ||
expr: x, | ||
isParentSelector: true | ||
} : []; | ||
} | ||
else if (loc === '~') { // property name | ||
retObj = {path: push(path, loc), value: parentPropName, parent: parent, parentProperty: null}; | ||
this._handleCallback(retObj, callback, 'property'); | ||
return retObj; | ||
} | ||
else if (loc === '$') { // root only | ||
addRet(this._trace(x, val, path, null, null, callback)); | ||
} | ||
else if (loc.indexOf('?(') === 0) { // [?(expr)] (filtering) | ||
if (this.currPreventEval) { | ||
throw "Eval [?(expr)] prevented in JSONPath expression."; | ||
} | ||
this._walk(loc, x, val, path, parent, parentPropName, callback, function (m, l, x, v, p, par, pr, cb) { | ||
if (self._eval(l.replace(/^\?\((.*?)\)$/, '$1'), v[m], m, p, par, pr)) { | ||
addRet(self._trace(unshift(m, x), v, p, par, pr, cb)); | ||
} | ||
}); | ||
} | ||
else if (loc.indexOf(',') > -1) { // [name1,name2,...] | ||
var parts, i; | ||
for (parts = loc.split(','), i = 0; i < parts.length; i++) { | ||
addRet(this._trace(unshift(parts[i], x), val, path, parent, parentPropName, callback)); | ||
} | ||
} | ||
else if (loc[0] === '@') { // value type: @boolean(), etc. | ||
var addType = false; | ||
var valueType = loc.slice(1, -2); | ||
switch (valueType) { | ||
case 'boolean': case 'string': case 'undefined': case 'function': | ||
if (typeof val === valueType) { | ||
addType = true; | ||
} | ||
break; | ||
case 'number': | ||
if (typeof val === valueType && isFinite(val)) { | ||
addType = true; | ||
} | ||
break; | ||
case 'nonFinite': | ||
if (typeof val === 'number' && !isFinite(val)) { | ||
addType = true; | ||
} | ||
break; | ||
case 'object': | ||
if (val && typeof val === valueType) { | ||
addType = true; | ||
} | ||
break; | ||
case 'array': | ||
if (Array.isArray(val)) { | ||
addType = true; | ||
} | ||
break; | ||
case 'other': | ||
addType = this.currOtherTypeCallback(val, path, parent, parentPropName); | ||
break; | ||
case 'integer': | ||
if (val === +val && isFinite(val) && !(val % 1)) { | ||
addType = true; | ||
} | ||
break; | ||
case 'null': | ||
if (val === null) { | ||
addType = true; | ||
} | ||
break; | ||
} | ||
if (addType) { | ||
retObj = {path: path, value: val, parent: parent, parentProperty: parentPropName}; | ||
this._handleCallback(retObj, callback, 'value'); | ||
return retObj; | ||
} | ||
} | ||
else if (/^(-?[0-9]*):(-?[0-9]*):?([0-9]*)$/.test(loc)) { // [start:end:step] Python slice syntax | ||
addRet(this._slice(loc, x, val, path, parent, parentPropName, callback)); | ||
} | ||
// We check the resulting values for parent selections. For parent | ||
// selections we discard the value object and continue the trace with the | ||
// current val object | ||
return ret.reduce(function (all, ea) { | ||
return all.concat(ea.isParentSelector ? self._trace(ea.expr, val, ea.path, parent, parentPropName, callback) : ea); | ||
}, []); | ||
}; | ||
JSONPath.prototype._walk = function (loc, expr, val, path, parent, parentPropName, callback, f) { | ||
var i, n, m; | ||
if (Array.isArray(val)) { | ||
for (i = 0, n = val.length; i < n; i++) { | ||
f(i, loc, expr, val, path, parent, parentPropName, callback); | ||
} | ||
} | ||
else if (typeof val === 'object') { | ||
for (m in val) { | ||
if (val.hasOwnProperty(m)) { | ||
f(m, loc, expr, val, path, parent, parentPropName, callback); | ||
} | ||
} | ||
} | ||
}; | ||
JSONPath.prototype._slice = function (loc, expr, val, path, parent, parentPropName, callback) { | ||
if (!Array.isArray(val)) {return;} | ||
var i, | ||
len = val.length, parts = loc.split(':'), | ||
start = (parts[0] && parseInt(parts[0], 10)) || 0, | ||
end = (parts[1] && parseInt(parts[1], 10)) || len, | ||
step = (parts[2] && parseInt(parts[2], 10)) || 1; | ||
start = (start < 0) ? Math.max(0, start + len) : Math.min(len, start); | ||
end = (end < 0) ? Math.max(0, end + len) : Math.min(len, end); | ||
var ret = []; | ||
for (i = start; i < end; i += step) { | ||
ret = ret.concat(this._trace(unshift(i, expr), val, path, parent, parentPropName, callback)); | ||
} | ||
return ret; | ||
}; | ||
JSONPath.prototype._eval = function (code, _v, _vname, path, parent, parentPropName) { | ||
if (!this._obj || !_v) {return false;} | ||
if (code.indexOf('@parentProperty') > -1) { | ||
this.currSandbox._$_parentProperty = parentPropName; | ||
code = code.replace(/@parentProperty/g, '_$_parentProperty'); | ||
} | ||
if (code.indexOf('@parent') > -1) { | ||
this.currSandbox._$_parent = parent; | ||
code = code.replace(/@parent/g, '_$_parent'); | ||
} | ||
if (code.indexOf('@property') > -1) { | ||
this.currSandbox._$_property = _vname; | ||
code = code.replace(/@property/g, '_$_property'); | ||
} | ||
if (code.indexOf('@path') > -1) { | ||
this.currSandbox._$_path = JSONPath.toPathString(path.concat([_vname])); | ||
code = code.replace(/@path/g, '_$_path'); | ||
} | ||
if (code.indexOf('@') > -1) { | ||
this.currSandbox._$_v = _v; | ||
code = code.replace(/@/g, '_$_v'); | ||
} | ||
try { | ||
return vm.runInNewContext(code, this.currSandbox); | ||
} | ||
catch(e) { | ||
console.log(e); | ||
throw new Error('jsonPath: ' + e.message + ': ' + code); | ||
} | ||
}; | ||
// PUBLIC CLASS PROPERTIES AND METHODS | ||
// Could store the cache object itself | ||
JSONPath.cache = {}; | ||
JSONPath.toPathString = function (pathArr) { | ||
var i, n, x = pathArr, p = '$'; | ||
for (i = 1, n = x.length; i < n; i++) { | ||
p += (/~|@.*\(\)/).test(x[i]) ? x[i] : ((/^[0-9*]+$/).test(x[i]) ? ('[' + x[i] + ']') : ("['" + x[i] + "']")); | ||
} | ||
return p; | ||
}; | ||
JSONPath.toPathArray = function (expr) { | ||
var cache = JSONPath.cache; | ||
if (cache[expr]) {return cache[expr];} | ||
var subx = []; | ||
var normalized = expr | ||
// Properties | ||
.replace(/@(?:null|boolean|number|string|array|object|integer|undefined|nonFinite|function|other)\(\)/g, ';$&;') | ||
.replace(/~/g, ';~;') | ||
// Parenthetical evaluations (filtering and otherwise), directly within brackets or single quotes | ||
.replace(/[\['](\??\(.*?\))[\]']/g, function ($0, $1) {return '[#' + (subx.push($1) - 1) + ']';}) | ||
// Escape periods within properties | ||
.replace(/\['([^'\]]*)'\]/g, function ($0, prop) { | ||
return "['" + prop.replace(/\./g, '%@%') + "']"; | ||
}) | ||
// Split by property boundaries | ||
.replace(/'?\.'?(?![^\[]*\])|\['?/g, ';') | ||
// Reinsert periods within properties | ||
.replace(/%@%/g, '.') | ||
// Parent | ||
.replace(/(?:;)?(\^+)(?:;)?/g, function ($0, ups) {return ';' + ups.split('').join(';') + ';';}) | ||
// Descendents | ||
.replace(/;;;|;;/g, ';..;') | ||
// Remove trailing | ||
.replace(/;$|'?\]|'$/g, ''); | ||
var exprList = normalized.split(';').map(function (expr) { | ||
var match = expr.match(/#([0-9]+)/); | ||
return !match || !match[1] ? expr : subx[match[1]]; | ||
}); | ||
cache[expr] = exprList; | ||
return cache[expr]; | ||
}; | ||
// For backward compatibility (deprecated) | ||
JSONPath.eval = function (obj, expr, opts) { | ||
return JSONPath(opts, expr, obj); | ||
}; | ||
if (typeof define === 'function' && define.amd) { | ||
define(function() {return JSONPath;}); | ||
} | ||
})(typeof exports === 'undefined' ? this['jsonPath'] = {} : exports, typeof require == "undefined" ? null : require); | ||
else if (typeof module === 'undefined') { | ||
self.jsonPath = { // Deprecated | ||
eval: JSONPath.eval | ||
}; | ||
self.JSONPath = JSONPath; | ||
} | ||
else { | ||
module.exports = JSONPath; | ||
} | ||
}(typeof require === 'undefined' ? null : require)); |
@@ -21,5 +21,10 @@ { | ||
"email": "robert.krahn@gmail.com" | ||
}, | ||
{ | ||
"name": "Brett Zamir", | ||
"email": "brettz9@yahoo.com" | ||
} | ||
], | ||
"version": "0.10.0", | ||
"license": "MIT", | ||
"version": "0.11.0", | ||
"repository": { | ||
@@ -29,10 +34,11 @@ "type": "git", | ||
}, | ||
"bugs": "https://github.com/s3u/JSONPath/issues/", | ||
"homepage": "https://github.com/s3u/JSONPath", | ||
"main": "./lib/jsonpath", | ||
"dependencies": { | ||
"underscore": "*" | ||
}, | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"nodeunit": "*" | ||
"nodeunit": "0.9.0" | ||
}, | ||
"scripts": {"test": "node_modules/nodeunit/bin/nodeunit test/test.*.js"} | ||
"keywords": ["json", "jsonpath"], | ||
"scripts": {"test": "./node_modules/.bin/nodeunit test"} | ||
} |
180
README.md
@@ -1,19 +0,18 @@ | ||
JSONPath [![build status](https://secure.travis-ci.org/s3u/JSONPath.png)](http://travis-ci.org/s3u/JSONPath) | ||
======== | ||
# JSONPath [![build status](https://secure.travis-ci.org/s3u/JSONPath.png)](http://travis-ci.org/s3u/JSONPath) | ||
Analyse, transform, and selectively extract data from JSON documents (and JavaScript objects). | ||
Install | ||
------- | ||
# Install | ||
npm install JSONPath | ||
Usage | ||
----- | ||
# Usage | ||
## Syntax | ||
In node.js: | ||
```js | ||
var jsonPath = require('JSONPath'); | ||
jsonPath.eval(obj, path); | ||
var JSONPath = require('JSONPath'); | ||
JSONPath({json: obj, path: path, callback: callback}); | ||
``` | ||
@@ -25,11 +24,49 @@ | ||
```html | ||
<script type="text/javascript" src="lib/jsonpath.js"></script> | ||
<script type="text/javascript"> | ||
jsonPath.eval(obj, path); | ||
<script src="lib/jsonpath.js"></script> | ||
<script> | ||
JSONPath({path: path, json: obj, callback: callback, otherTypeCallback: otherTypeCallback}); | ||
</script> | ||
``` | ||
Examples | ||
-------- | ||
An alternative syntax is available as: | ||
```js | ||
JSONPath(options, path, obj, callback, otherTypeCallback); | ||
``` | ||
The following format is now deprecated: | ||
```js | ||
jsonPath.eval(options, obj, path); | ||
``` | ||
## Properties | ||
The properties that can be supplied on the options object or evaluate method (as the first argument) include: | ||
- ***path*** (**required**) - The JSONPath expression as a (normalized or unnormalized) string or array | ||
- ***json*** (**required**) - The JSON object to evaluate (whether of null, boolean, number, string, object, or array type). | ||
- ***autostart*** (**default: true**) - If this is supplied as `false`, one may call the `evaluate` method manually. | ||
- ***flatten*** (**default: false**) - Whether the returned array of results will be flattened to a single dimension array. | ||
- ***resultType*** (**default: "value"**) - Can be case-insensitive form of "value", "path", "parent", or "parentProperty" to determine respectively whether to return results as the values of the found items, as their absolute paths, as their parent objects, or as their parent's property name. If set to "all", all of these types will be returned on an object with the type as key name. | ||
- ***sandbox*** (**default: {}**) - Key-value map of variables to be available to code evaluations such as filtering expressions. (Note that the current path and value will also be available to those expressions; see the Syntax section for details.) | ||
- ***wrap*** (**default: true**) - Whether or not to wrap the results in an array. If `wrap` is set to false, and no results are found, `undefined` will be returned (as opposed to an empty array with `wrap` set to true). If `wrap` is set to false and a single result is found, that result will be the only item returned (not within an array). An array will still be returned if multiple results are found, however. | ||
- ***preventEval*** (**default: false**) - Although JavaScript evaluation expressions are allowed by default, for security reasons (if one is operating on untrusted user input, for example), one may wish to set this option to `true` to throw exceptions when these expressions are attempted. | ||
- ***parent*** (**default: null**) - In the event that a query could be made to return the root node, this allows the parent of that root node to be returned within results. | ||
- ***parentProperty*** (**default: null**) - In the event that a query could be made to return the root node, this allows the parentProperty of that root node to be returned within results. | ||
- ***callback*** (**default: (none)**) - If supplied, a callback will be called immediately upon retrieval of an end point value. The three arguments supplied will be the value of the payload (according to `resultType`), the type of the payload (whether it is a normal "value" or a "property" name), and a full payload object (with all `resultType`s). | ||
- ***otherTypeCallback*** (**default: \<A function that throws an error when @other() is encountered\>**) - In the current absence of JSON Schema support, one can determine types beyond the built-in types by adding the operator `@other()` at the end of one's query. If such a path is encountered, the `otherTypeCallback` will be invoked with the value of the item, its path, its parent, and its parent's property name, and it should return a boolean indicating whether the supplied value belongs to the "other" type or not (or it may handle transformations and return false). | ||
## Instance methods | ||
- ***evaluate(path, json, callback, otherTypeCallback)*** OR ***evaluate({path: \<path\>, json: \<json object\>, callback: \<callback function\>, otherTypeCallback: \<otherTypeCallback function\>})*** - This method is only necessary if the `autostart` property is set to `false`. It can be used for repeated evaluations using the same configuration. Besides the listed properties, the latter method pattern can accept any of the other allowed instance properties (except for `autostart` which would have no relevance here). | ||
## Class properties and methods | ||
- ***JSONPath.cache*** - Exposes the cache object for those who wish to preserve and reuse it for optimization purposes. | ||
- ***JSONPath.toPathArray(pathAsString)*** - Accepts a normalized or unnormalized path as string and converts to an array: for example, `['$', 'aProperty', 'anotherProperty']`. | ||
- ***JSONPath.toPathString(pathAsArray)*** - Accepts a path array and converts to a normalized path string. The string will be in form like: `$['aProperty']['anotherProperty]`. The terminal constructions `~` and typed operators like `@string()`, as with `$`, get added without enclosing single quotes and brackets. | ||
# Syntax through examples | ||
Given the following JSON, taken from http://goessner.net/articles/JsonPath/ : | ||
@@ -76,22 +113,102 @@ | ||
and the following XML representation: | ||
XPath | JSONPath | Result | ||
------------------- | ---------------------- | ------------------------------------- | ||
/store/book/author | $.store.book[*].author | the authors of all books in the store | ||
//author | $..author | all authors | ||
/store/* | $.store.* | all things in store, which are some books and a red bicycle. | ||
/store//price | $.store..price | the price of everything in the store. | ||
//book[3] | $..book[2] | the third book | ||
//book[last()] | $..book[(@.length-1)] | the last book in order. | ||
| $..book[-1:] | | ||
//book[position()<3]| $..book[0,1] | the first two books | ||
| $..book[:2] | | ||
//book[isbn] | $..book[?(@.isbn)] | filter all books with isbn number | ||
//book[price<10] | $..book[?(@.price<10)] | filter all books cheapier than 10 | ||
//*[price>19]/.. | $..[?(@.price>19)]^ | categories with things more expensive than 19 | ||
//* | $..* | all Elements in XML document. All members of JSON structure. | ||
```xml | ||
<store> | ||
<book> | ||
<category>reference</category> | ||
<author>Nigel Rees</author> | ||
<title>Sayings of the Century</title> | ||
<price>8.95</price> | ||
</book> | ||
<book> | ||
<category>fiction</category> | ||
<author>Evelyn Waugh</author> | ||
<title>Sword of Honour</title> | ||
<price>12.99</price> | ||
</book> | ||
<book> | ||
<category>fiction</category> | ||
<author>Herman Melville</author> | ||
<title>Moby Dick</title> | ||
<isbn>0-553-21311-3</isbn> | ||
<price>8.99</price> | ||
</book> | ||
<book> | ||
<category>fiction</category> | ||
<author>J. R. R. Tolkien</author> | ||
<title>The Lord of the Rings</title> | ||
<isbn>0-395-19395-8</isbn> | ||
<price>22.99</price> | ||
</book> | ||
<bicycle> | ||
<color>red</color> | ||
<price>19.95</price> | ||
</bicycle> | ||
</store> | ||
``` | ||
Development | ||
----------- | ||
Please note that the XPath examples below do not distinguish between | ||
retrieving elements and their text content (except where useful for | ||
comparisons or to prevent ambiguity). | ||
XPath | JSONPath | Result | Notes | ||
------------------- | ---------------------- | ------------------------------------- | ----- | ||
/store/book/author | $.store.book[*].author | The authors of all books in the store | | ||
//author | $..author | All authors | | ||
/store/* | $.store.* | All things in store, which are its books (a book array) and a red bicycle (a bicycle object).| | ||
/store//price | $.store..price | The price of everything in the store. | | ||
//book[3] | $..book[2] | The third book (book object) | | ||
//book[last()] | $..book[(@.length-1)]<br>$..book[-1:] | The last book in order.| | ||
//book[position()<3]| $..book[0,1]<br>$..book[:2]| The first two books | | ||
//book/*[self::category\|self::author] or //book/(category,author) in XPath 2.0 | $..book[0][category,author]| The categories and authors of all books | | ||
//book[isbn] | $..book[?(@.isbn)] | Filter all books with an ISBN number | | ||
//book[price<10] | $..book[?(@.price<10)] | Filter all books cheaper than 10 | | ||
| //\*[name() = 'price' and . != 8.95] | $..\*[?(@property === 'price' && @ !== 8.95)] | Obtain all property values of objects whose property is price and which does not equal 8.95 | | ||
/ | $ | The root of the JSON object (i.e., the whole object itself) | | ||
//\*/\*\|//\*/\*/text() | $..* | All Elements (and text) beneath root in an XML document. All members of a JSON structure beneath the root. | | ||
//* | $.. | All Elements in an XML document. All parent components of a JSON structure including root. | This behavior was not directly specified in the original spec | ||
//*[price>19]/.. | $..[?(@.price>19)]^ | Parent of those specific items with a price greater than 19 (i.e., the store value as the parent of the bicycle and the book array as parent of an individual book) | Parent (caret) not documented in the original spec | ||
/store/*/name() (in XPath 2.0) | $.store.*~ | The property names of the store sub-object ("book" and "bicycle"). Useful with wildcard properties. | Property name (tilde) is not present in the original spec | ||
/store/book\[not(. is /store/book\[1\])\] (in XPath 2.0) | $.store.book[?(@path !== "$[\'store\'][\'book\'][0]")] | All books besides that at the path pointing to the first | @path not present in the original spec | ||
//book[parent::\*/bicycle/color = "red"]/category | $..book[?(@parent.bicycle && @parent.bicycle.color === "red")].category | Grabs all categories of books where the parent object of the book has a bicycle child whose color is red (i.e., all the books) | @parent is not present in the original spec | ||
//book/*[name() != 'category'] | $..book.*[?(@property !== "category")] | Grabs all children of "book" except for "category" ones | @property is not present in the original spec | ||
//book/*[position() != 0] | $..book[?(@property !== 0)] | Grabs all books whose property (which, being that we are reaching inside an array, is the numeric index) is not 0 | @property is not present in the original spec | ||
/store/\*/\*[name(parent::*) != 'book'] | $.store.*[?(@parentProperty !== "book")] | Grabs the grandchildren of store whose parent property is not book (i.e., bicycle's children, "color" and "price") | @parentProperty is not present in the original spec | ||
//book[count(preceding-sibling::\*) != 0]/\*/text() | $..book.*[?(@parentProperty !== 0)] | Get the property values of all book instances whereby the parent property of these values (i.e., the array index holding the book item parent object) is not 0 | @parentProperty is not present in the original spec | ||
//book/../\*\[. instance of element(\*, xs:decimal)\] (in XPath 2.0) | $..book..\*@number() | Get the numeric values within the book array | @number(), the other basic types (@boolean(), @string()), other low-level derived types (@null(), @object(), @array()), the JSONSchema-added type, @integer(), the type, @other(), to be used in conjunction with a user-defined callback (see `otherTypeCallback`) and the following non-JSON types that can nevertheless be used with JSONPath when querying non-JSON JavaScript objects (@undefined(), @function(), @nonFinite()) are not present in the original spec | ||
Any additional variables supplied as properties on the optional "sandbox" object option are also available to (parenthetical-based) evaluations. | ||
# Potential sources of confusion for XPath users | ||
1. In JSONPath, a filter expression, in addition to its `@` being a | ||
reference to its children, actually selects the immediate children | ||
as well, whereas in XPath, filter conditions do not select the children | ||
but delimit which of its parent nodes will be obtained in the result. | ||
1. In JSONPath, array indexes are, as in JavaScript, 0-based (they begin | ||
from 0), whereas in XPath, they are 1-based. | ||
1. In JSONPath, equality tests utilize (as per JavaScript) multiple equal signs | ||
whereas in XPath, they use a single equal sign. | ||
# Todos | ||
1. Conditionally resolve JSON references/JSON pointers instead or in | ||
addition to raw retrieved data, with options on how deeply nested. | ||
1. Support non-eval version (which supports parenthetical evaluations) | ||
1. Support OR outside of filters (as in XPath `|`). | ||
1. Create syntax to work like XPath filters in not selecting children? | ||
1. Allow for type-based searches to be JSON Schema aware (and allow | ||
retrieval of schema segment for a given JSON fragment) | ||
1. Pull or streaming parser? | ||
1. Allow option for parentNode equivalent (maintaining entire chain of | ||
parent-and-parentProperty objects up to root) | ||
1. Fix in JSONPath to avoid need for "$"? | ||
1. Define any allowed behaviors for: '$.', '$[0]', $.[0], or $.['prop'] | ||
1. Modularize operator detection and usage to allow for | ||
extensibility (at least non-standard ones)? | ||
# Development | ||
Running the tests on node: `npm test`. For in-browser tests: | ||
@@ -110,5 +227,4 @@ | ||
License | ||
------- | ||
# License | ||
[MIT License](http://www.opensource.org/licenses/mit-license.php). |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Wildcard dependency
QualityPackage has a dependency with a floating version range. This can cause issues if the dependency publishes a new major version.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
34846
0
395
1
0
228
6
3
- Removedunderscore@*
- Removedunderscore@1.13.7(transitive)