From 89de1dee45965133d417eac59319207fdf320107 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 8 Dec 2014 21:57:09 -0700 Subject: [PATCH 01/68] rmv unnecessary global (redefined inside anyways) --- lib/jsonpath.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 46f8c4c..bdb970d 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -4,7 +4,7 @@ * Licensed under the MIT (MIT-LICENSE.txt) licence. */ -var isNode = false; + (function(exports, require) { // Keep compatibility with old browsers From 51b5a3ae35c3dcc8e5ee12f41dcddd7f2e737561 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 8 Dec 2014 22:36:57 -0700 Subject: [PATCH 02/68] Avoid with, use strict mode, rmv unused vars, other JSLint-inspired changes --- lib/jsonpath.js | 100 +++++++++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 39 deletions(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index bdb970d..1a0c61a 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -1,11 +1,12 @@ +/*global module, exports, require*/ +/*jslint vars:true, evil:true*/ /* JSONPath 0.8.0 - XPath for JSON * * Copyright (c) 2007 Stefan Goessner (goessner.net) * Licensed under the MIT (MIT-LICENSE.txt) licence. */ - -(function(exports, require) { +(function(exports, require) {'use strict'; // Keep compatibility with old browsers if (!Array.isArray) { @@ -20,9 +21,12 @@ var isNode = typeof module !== 'undefined' && !!module.exports; 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]) + ';' + s; + }, expr)); + } }; -exports.eval = jsonPath; var cache = {}; @@ -30,47 +34,51 @@ 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(obj, expr, arg) { + var $ = obj; var P = { - resultType: arg && arg.resultType || "VALUE", - flatten: arg && arg.flatten || false, + 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]; + 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, function(_, ups) { 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; + }); + cache[expr] = exprList; + return cache[expr]; }, asPath: function(path) { - var x = path, p = "$"; - for (var i=1,n=x.length; i -1) { // [name1,name2,...] - for (var parts = loc.split(','), i = 0; i < parts.length; i++) + var parts, i; + for (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)); @@ -107,35 +119,43 @@ function jsonPath(obj, expr, arg) { }, []); }, walk: function(loc, expr, val, path, f) { - if (Array.isArray(val)) - for (var i = 0, n = val.length; i < n; i++) + var i, n, m; + if (Array.isArray(val)) { + for (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)) + } + } + else if (typeof val === "object") { + for (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; + 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 (var i = start; i < end; i += step) + for (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 (!$ || !_v) {return false;} if (code.indexOf("@path") > -1) { - P.sandbox["_path"] = P.asPath(path.concat([_vname])); + P.sandbox._path = P.asPath(path.concat([_vname])); code = code.replace(/@path/g, "_path"); } if (code.indexOf("@") > -1) { - P.sandbox["_v"] = _v; + P.sandbox._v = _v; code = code.replace(/@/g, "_v"); } try { @@ -148,18 +168,17 @@ function jsonPath(obj, expr, arg) { } }; - var $ = obj; var resultType = P.resultType.toLowerCase(); - if (expr && obj && (resultType == "value" || resultType == "path")) { + if (expr && obj && (resultType === "value" || resultType === "path")) { var exprList = P.normalize(expr); - if (exprList[0] === "$" && exprList.length > 1) exprList.shift(); + 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; + 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 (resultType === 'path') {valOrPath = P.asPath(valOrPath);} if (P.flatten && Array.isArray(valOrPath)) { result = result.concat(valOrPath); } else { @@ -169,4 +188,7 @@ function jsonPath(obj, expr, arg) { }, []); } } -})(typeof exports === 'undefined' ? this['jsonPath'] = {} : exports, typeof require == "undefined" ? null : require); + +exports.eval = jsonPath; + +}(typeof exports === 'undefined' ? this.jsonPath = {} : exports, typeof require === 'undefined' ? null : require)); From a09efe3d503339a4a6675bd185d2779154c1cbd6 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 8 Dec 2014 22:47:39 -0700 Subject: [PATCH 03/68] One more JSLint change (avoiding inline assignment) --- lib/jsonpath.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 1a0c61a..b579f25 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -6,7 +6,7 @@ * Licensed under the MIT (MIT-LICENSE.txt) licence. */ -(function(exports, require) {'use strict'; +(function(require) {'use strict'; // Keep compatibility with old browsers if (!Array.isArray) { @@ -189,6 +189,11 @@ function jsonPath(obj, expr, arg) { } } -exports.eval = jsonPath; +if (typeof exports === 'undefined') { + window.jsonPath = {eval: jsonPath}; +} +else { + exports.eval = jsonPath; +} -}(typeof exports === 'undefined' ? this.jsonPath = {} : exports, typeof require === 'undefined' ? null : require)); +}(typeof require === 'undefined' ? null : require)); From d1baf0bef4207ddf178b64be893dc9aa8cf054af Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 10:40:37 -0700 Subject: [PATCH 04/68] Move livable whitespace pattern (4 sp. instead of inconsistent 3) and use consistently use single quotes except where content includes double quotes --- lib/jsonpath.js | 325 ++++++++++++++++++++++++------------------------ 1 file changed, 163 insertions(+), 162 deletions(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index b579f25..79de869 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -6,13 +6,13 @@ * Licensed under the MIT (MIT-LICENSE.txt) licence. */ -(function(require) {'use strict'; +(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]'; + }; } // Make sure to know if we are in real node or not (the `require` variable @@ -21,179 +21,180 @@ var isNode = typeof module !== 'undefined' && !!module.exports; var vm = isNode ? require('vm') : { - runInNewContext: function(expr, context) { - return eval(Object.keys(context).reduce(function (s, vr) { - return 'var ' + vr + '=' + JSON.stringify(context[vr]) + ';' + s; - }, expr)); - } + runInNewContext: function (expr, context) { + return eval(Object.keys(context).reduce(function (s, vr) { + return 'var ' + vr + '=' + JSON.stringify(context[vr]) + ';' + s; + }, expr)); + } }; 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(obj, expr, arg) { - var $ = obj; - 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(_, ups) { 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]]; - }); - cache[expr] = exprList; - return cache[expr]; - }, - asPath: function(path) { - var i, n, x = path, p = "$"; - for (i=1,n=x.length; i -1) { // [name1,name2,...] - var parts, i; - for (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)); - } + 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 child 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,...] + var parts, i; + for (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)); + } - // 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) { - var i, n, m; - if (Array.isArray(val)) { - for (i = 0, n = val.length; i < n; i++) { - f(i, loc, expr, val, path); - } - } - else if (typeof val === "object") { - for (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 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(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); - } - } - }; + // 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) { + var i, n, m; + if (Array.isArray(val)) { + for (i = 0, n = val.length; i < n; i++) { + f(i, loc, expr, val, path); + } + } + else if (typeof val === 'object') { + for (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 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(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); + } + } + }; - 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)) { - result = result.concat(valOrPath); - } else { - result.push(valOrPath); - } - return result; - }, []); - } + 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)) { + result = result.concat(valOrPath); + } else { + result.push(valOrPath); + } + return result; + }, []); + } } if (typeof exports === 'undefined') { - window.jsonPath = {eval: jsonPath}; + window.jsonPath = {eval: jsonPath}; } else { - exports.eval = jsonPath; + exports.eval = jsonPath; } }(typeof require === 'undefined' ? null : require)); From d765dac34611ca9e9a2b1e1888bf4dcd4a4c9e4c Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 11:15:26 -0700 Subject: [PATCH 05/68] Move to prototype-based inheritance (saving on memory); remove unneeded arg (as per other patch); export class and deprecate exporting of singleton and class method (will probably allow for instance to avoid autostarting) --- lib/jsonpath.js | 306 +++++++++++++++++++++++++----------------------- 1 file changed, 161 insertions(+), 145 deletions(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 79de869..20b667c 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -33,154 +33,28 @@ 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 jsonPath (obj, expr, arg) { - var $ = obj; - 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 (_, ups) {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]]; - }); - cache[expr] = exprList; - return cache[expr]; - }, - asPath: function (path) { - var i, n, x = path, p = '$'; - for (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}];} - - 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}] : [];} - - // 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(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 child 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,...] - var parts, i; - for (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)); - } - - // 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) { - var i, n, m; - if (Array.isArray(val)) { - for (i = 0, n = val.length; i < n; i++) { - f(i, loc, expr, val, path); - } - } - else if (typeof val === 'object') { - for (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 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(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); - } - } - }; +function JSONPath (obj, expr, arg) { + if (!(this instanceof JSONPath)) { // Make "new" optional + return new JSONPath(obj, expr, arg); + } + this.obj = obj; + var self = this; + var resultType = (arg && arg.resultType).toLowerCase() || 'value'; + var flatten = (arg && arg.flatten) || false; + var wrap = (arg && arg.hasOwnProperty('wrap')) ? arg.wrap : true; + this.sandbox = (arg && arg.sandbox) ? arg.sandbox : {}; - var resultType = P.resultType.toLowerCase(); if (expr && obj && (resultType === 'value' || resultType === 'path')) { - var exprList = P.normalize(expr); + var exprList = this._normalize(expr); if (exprList[0] === '$' && exprList.length > 1) {exprList.shift();} - var result = P.trace(exprList, obj, ['$']); + var result = this._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;} + if (!result.length) {return wrap ? [] : false;} + if (result.length === 1 && !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)) { + if (resultType === 'path') {valOrPath = self._asPath(valOrPath);} + if (flatten && Array.isArray(valOrPath)) { result = result.concat(valOrPath); } else { result.push(valOrPath); @@ -190,11 +64,153 @@ function jsonPath (obj, expr, arg) { } } -if (typeof exports === 'undefined') { - window.jsonPath = {eval: jsonPath}; +JSONPath.prototype._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 (_, ups) {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]]; + }); + cache[expr] = exprList; + return cache[expr]; +}; + +JSONPath.prototype._asPath = function (path) { + var i, n, x = path, p = '$'; + for (i = 1, n = x.length; i < n; i++) { + p += /^[0-9*]+$/.test(x[i]) ? ('[' + x[i] + ']') : ("['" + x[i] + "']"); + } + return p; +}; + +JSONPath.prototype._trace = function (expr, val, path) { + // No expr to follow? return path and value as the result of this trace branch + var self = this; + if (!expr.length) {return [{path: path, value: val}];} + + 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}] : [];} + + // 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))); + } + else if (loc === '*') { // any property + this._walk(loc, x, val, path, function (m, l, x, v, p) { + addRet(self._trace(unshift(m, x), v, p)); + }); + } + else if (loc === '..') { // all child properties + addRet(this._trace(x, val, path)); + this._walk(loc, x, val, path, function (m, l, x, v, p) { + if (typeof v[m] === 'object') { + addRet(self._trace(unshift('..', x), v[m], push(p, m))); + } + }); + } + else if (loc[0] === '(') { // [(expr)] + addRet(this._trace(unshift(this._eval(loc, val, path[path.length], path), x), val, path)); + } + else if (loc.indexOf('?(') === 0) { // [?(expr)] + this._walk(loc, x, val, path, function (m, l, x, v, p) { + if (self._eval(l.replace(/^\?\((.*?)\)$/, '$1'), v[m], m, path)) { + addRet(self._trace(unshift(m, x), v, p)); + } + }); + } + 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)); + } + } + else if (/^(-?[0-9]*):(-?[0-9]*):?([0-9]*)$/.test(loc)) { // [start:end:step] Python slice syntax + addRet(this._slice(loc, x, val, path)); + } + + // 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) : [ea]); + }, []); +}; + +JSONPath.prototype._walk = function (loc, expr, val, path, f) { + var i, n, m; + if (Array.isArray(val)) { + for (i = 0, n = val.length; i < n; i++) { + f(i, loc, expr, val, path); + } + } + else if (typeof val === 'object') { + for (m in val) { + if (val.hasOwnProperty(m)) { + f(m, loc, expr, val, path); + } + } + } +}; + +JSONPath.prototype._slice = function (loc, expr, val, path) { + 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)); + } + return ret; +}; + +JSONPath.prototype._eval = function (code, _v, _vname, path) { + if (!this.obj || !_v) {return false;} + if (code.indexOf('@path') > -1) { + this.sandbox._path = this._asPath(path.concat([_vname])); + code = code.replace(/@path/g, '_path'); + } + if (code.indexOf('@') > -1) { + this.sandbox._v = _v; + code = code.replace(/@/g, '_v'); + } + try { + return vm.runInNewContext(code, this.sandbox); + } + catch(e) { + console.log(e); + throw new Error('jsonPath: ' + e.message + ': ' + code); + } +}; + +// For backward compatibility (deprecated) +JSONPath.eval = function () { + return new JSONPath(); +}; + +if (typeof module === 'undefined') { + window.jsonPath = { // Deprecated + eval: JSONPath.eval + }; + window.JSONPath = JSONPath; } else { - exports.eval = jsonPath; + module.exports = JSONPath; } }(typeof require === 'undefined' ? null : require)); From 55dc9d5e283a06168430634e437fb44a750e8fa0 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 11:24:33 -0700 Subject: [PATCH 06/68] Change argument order when instantiating, but keep old order for backward compatibility. Having options first will allow separate calls for the object and expression to an evaluate method if autostarting is not desired --- lib/jsonpath.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 20b667c..de8cfca 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -33,16 +33,16 @@ 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 JSONPath (obj, expr, arg) { +function JSONPath (opts, obj, expr) { if (!(this instanceof JSONPath)) { // Make "new" optional - return new JSONPath(obj, expr, arg); + return new JSONPath(opts, obj, expr); } this.obj = obj; var self = this; - var resultType = (arg && arg.resultType).toLowerCase() || 'value'; - var flatten = (arg && arg.flatten) || false; - var wrap = (arg && arg.hasOwnProperty('wrap')) ? arg.wrap : true; - this.sandbox = (arg && arg.sandbox) ? arg.sandbox : {}; + var resultType = (opts && opts.resultType).toLowerCase() || 'value'; + var flatten = (opts && opts.flatten) || false; + var wrap = (opts && opts.hasOwnProperty('wrap')) ? opts.wrap : true; + this.sandbox = (opts && opts.sandbox) ? opts.sandbox : {}; if (expr && obj && (resultType === 'value' || resultType === 'path')) { var exprList = this._normalize(expr); @@ -199,8 +199,8 @@ JSONPath.prototype._eval = function (code, _v, _vname, path) { }; // For backward compatibility (deprecated) -JSONPath.eval = function () { - return new JSONPath(); +JSONPath.eval = function (obj, expr, opts) { + return new JSONPath(opts, obj, expr); }; if (typeof module === 'undefined') { From 8ff538d68629a5936e76cd294d59185fd9a913c3 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 11:27:48 -0700 Subject: [PATCH 07/68] Fix resultType regression --- lib/jsonpath.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index de8cfca..7ee5abf 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -39,7 +39,8 @@ function JSONPath (opts, obj, expr) { } this.obj = obj; var self = this; - var resultType = (opts && opts.resultType).toLowerCase() || 'value'; + + var resultType = (opts && opts.resultType.toLowerCase()) || 'value'; var flatten = (opts && opts.flatten) || false; var wrap = (opts && opts.hasOwnProperty('wrap')) ? opts.wrap : true; this.sandbox = (opts && opts.sandbox) ? opts.sandbox : {}; From dce1969a0f2aeb65cc2e6af78c967e5804f57ae5 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 11:38:47 -0700 Subject: [PATCH 08/68] Allow avoiding autostart with optionally separate call to new public method, "evaluate" --- lib/jsonpath.js | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 7ee5abf..7b96d3b 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -37,25 +37,34 @@ function JSONPath (opts, obj, expr) { if (!(this instanceof JSONPath)) { // Make "new" optional return new JSONPath(opts, obj, expr); } - this.obj = obj; - var self = this; - var resultType = (opts && opts.resultType.toLowerCase()) || 'value'; - var flatten = (opts && opts.flatten) || false; - var wrap = (opts && opts.hasOwnProperty('wrap')) ? opts.wrap : true; - this.sandbox = (opts && opts.sandbox) ? opts.sandbox : {}; + opts = opts || {}; + 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 || {}; + + if (opts.autostart !== false) { + return this.evaluate(obj, expr); + } +} + +// PUBLIC METHODS - if (expr && obj && (resultType === 'value' || resultType === 'path')) { +JSONPath.prototype.evaluate = function (obj, expr) { + var self = this; + this.obj = obj; + if (expr && obj && (this.resultType === 'value' || this.resultType === 'path')) { var exprList = this._normalize(expr); if (exprList[0] === '$' && exprList.length > 1) {exprList.shift();} var result = this._trace(exprList, obj, ['$']); result = result.filter(function (ea) { return ea && !ea.isParentSelector; }); - if (!result.length) {return wrap ? [] : false;} - if (result.length === 1 && !wrap && !Array.isArray(result[0].value)) {return result[0][resultType] || false;} + if (!result.length) {return this.wrap ? [] : false;} + if (result.length === 1 && !this.wrap && !Array.isArray(result[0].value)) {return result[0][this.resultType] || false;} return result.reduce(function (result, ea) { - var valOrPath = ea[resultType]; - if (resultType === 'path') {valOrPath = self._asPath(valOrPath);} - if (flatten && Array.isArray(valOrPath)) { + var valOrPath = ea[self.resultType]; + if (self.resultType === 'path') {valOrPath = self._asPath(valOrPath);} + if (self.flatten && Array.isArray(valOrPath)) { result = result.concat(valOrPath); } else { result.push(valOrPath); @@ -63,7 +72,9 @@ function JSONPath (opts, obj, expr) { return result; }, []); } -} +}; + +// PRIVATE METHODS JSONPath.prototype._normalize = function (expr) { if (cache[expr]) {return cache[expr];} From 164aaf1faed35b0cbb9a2ab1e2fcdcbeba62ac11 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 11:43:37 -0700 Subject: [PATCH 09/68] Allow for object-style parameters, "json" and "path" --- lib/jsonpath.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 7b96d3b..38fc928 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -45,7 +45,7 @@ function JSONPath (opts, obj, expr) { this.sandbox = opts.sandbox || {}; if (opts.autostart !== false) { - return this.evaluate(obj, expr); + return this.evaluate(obj || opts.json, expr || opts.path); } } From 46cd3d438e725e80c4bdb86fd7813183824d2289 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 18:35:48 -0700 Subject: [PATCH 10/68] Update tests and docs to use new class-based API with object arguments (clearer for readers of code) --- README.md | 6 +- test/test.arr.js | 10 +-- test/test.at_and_dollar.js | 32 ++++---- test/test.eval.js | 16 ++-- test/test.examples.js | 138 +++++++++++++++++------------------ test/test.html | 55 ++++++++------ test/test.intermixed.arr.js | 6 +- test/test.parent-selector.js | 22 +++--- 8 files changed, 146 insertions(+), 139 deletions(-) diff --git a/README.md b/README.md index 5b8c779..8186602 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ Usage In node.js: ```js -var jsonPath = require('JSONPath'); -jsonPath.eval(obj, path); +var JSONPath = require('JSONPath'); +JSONPath({json: obj, path: path}); ``` For browser usage you can directly include `lib/jsonpath.js`, no browserify @@ -24,7 +24,7 @@ magic necessary: ```html ``` diff --git a/test/test.arr.js b/test/test.arr.js index 4314fd2..d206071 100644 --- a/test/test.arr.js +++ b/test/test.arr.js @@ -1,4 +1,4 @@ -var jsonpath = require("../").eval, +var JSONPath = require('../'), testCase = require('nodeunit').testCase var json = { @@ -19,16 +19,16 @@ var json = { }; module.exports = testCase({ - "get single": function (test) { + 'get single': function (test) { var expected = json.store.book; - var result = jsonpath(json, "store.book", {flatten: true, wrap: false}); + var result = JSONPath({json: json, path: 'store.book', flatten: true, wrap: false}); test.deepEqual(expected, result); test.done(); }, - "get arr": function (test) { + 'get arr': function (test) { var expected = json.store.books; - var result = jsonpath(json, "store.books", {flatten: true, wrap: false}); + var result = JSONPath({json: json, path: 'store.books', flatten: true, wrap: false}); test.deepEqual(expected, result); test.done(); } diff --git a/test/test.at_and_dollar.js b/test/test.at_and_dollar.js index e99d7db..9c0eb42 100644 --- a/test/test.at_and_dollar.js +++ b/test/test.at_and_dollar.js @@ -1,5 +1,5 @@ -var jsonpath = require("../").eval - , testCase = require('nodeunit').testCase +var JSONPath = require('../'), + testCase = require('nodeunit').testCase var t1 = { @@ -22,29 +22,29 @@ module.exports = testCase({ // ============================================================================ - "test undefined, null": function(test) { + 'test undefined, null': function(test) { // ============================================================================ test.expect(5); - test.equal(undefined, jsonpath(undefined, "foo")); - test.equal(null, jsonpath(null, "foo")); - test.equal(undefined, jsonpath({}, "foo")[0]); - test.equal(undefined, jsonpath({ a: "b" }, "foo")[0]); - test.equal(undefined, jsonpath({ a: "b" }, "foo")[100]); + test.equal(undefined, JSONPath({json: undefined, path: 'foo'})); + test.equal(null, JSONPath({json: null, path: 'foo'})); + test.equal(undefined, JSONPath({json: {}, path: 'foo'})[0]); + test.equal(undefined, JSONPath({json: { a: 'b' }, path: 'foo'})[0]); + test.equal(undefined, JSONPath({json: { a: 'b' }, path: 'foo'})[100]); test.done(); }, // ============================================================================ - "test $ and @": function(test) { + 'test $ and @': function(test) { // ============================================================================ test.expect(7); - test.equal(t1["$"], jsonpath(t1, "\$")[0]); - test.equal(t1["$"], jsonpath(t1, "$")[0]); - test.equal(t1["a$a"], jsonpath(t1, "a$a")[0]); - test.equal(t1["@"], jsonpath(t1, "\@")[0]); - test.equal(t1["@"], jsonpath(t1, "@")[0]); - test.equal(t1["$"]["@"], jsonpath(t1, "$.$.@")[0]); - test.equal(undefined, jsonpath(t1, "\@")[1]); + test.equal(t1['$'], JSONPath({json: t1, path: '\$'})[0]); + test.equal(t1['$'], JSONPath({json: t1, path: '$'})[0]); + test.equal(t1['a$a'], JSONPath({json: t1, path: 'a$a'})[0]); + test.equal(t1['@'], JSONPath({json: t1, path: '\@'})[0]); + test.equal(t1['@'], JSONPath({json: t1, path: '@'})[0]); + test.equal(t1['$']['@'], JSONPath({json: t1, path: '$.$.@'})[0]); + test.equal(undefined, JSONPath({json: t1, path: '\@'})[1]); test.done(); } diff --git a/test/test.eval.js b/test/test.eval.js index 8da68e9..98b3459 100644 --- a/test/test.eval.js +++ b/test/test.eval.js @@ -1,4 +1,4 @@ -var jsonpath = require("../").eval, +var JSONPath = require('../'), testCase = require('nodeunit').testCase var json = { @@ -26,19 +26,19 @@ var json = { module.exports = testCase({ - "multi statement eval": function (test) { + 'multi statement eval': function (test) { var expected = json.store.books[0]; - var selector = "$..[?(" - + "var sum = @.price && @.price[0]+@.price[1];" - + "sum > 20;)]" - var result = jsonpath(json, selector, {wrap: false}); + var selector = '$..[?(' + + 'var sum = @.price && @.price[0]+@.price[1];' + + 'sum > 20;)]' + var result = JSONPath({json: json, path: selector, wrap: false}); test.deepEqual(expected, result); test.done(); }, - "accessing current path": function (test) { + 'accessing current path': function (test) { var expected = json.store.books[1]; - var result = jsonpath(json, "$..[?(@path==\"$['store']['books'][1]\")]", {wrap: false}); + var result = JSONPath({json: json, path: "$..[?(@path==\"$['store']['books'][1]\")]", wrap: false}); test.deepEqual(expected, result); test.done(); } diff --git a/test/test.examples.js b/test/test.examples.js index 0532725..a28e3bb 100644 --- a/test/test.examples.js +++ b/test/test.examples.js @@ -1,10 +1,10 @@ -var jsonpath = require("../").eval - , testCase = require('nodeunit').testCase +var JSONPath = require('../'), + testCase = require('nodeunit').testCase // tests based on examples at http://goessner.net/articles/JsonPath/ var json = {"store": { - "book": [ + "book": [ { "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", @@ -37,123 +37,123 @@ var json = {"store": { module.exports = testCase({ - - // ============================================================================ - "wildcards": function(test) { - // ============================================================================ + + // ============================================================================ + 'wildcards': function(test) { + // ============================================================================ test.expect(1); var books = json.store.book; var expected = [books[0].author, books[1].author, books[2].author, books[3].author]; - var result = jsonpath(json, "$.store.book[*].author"); + var result = JSONPath({json: json, path: '$.store.book[*].author'}); test.deepEqual(expected, result); - + test.done(); }, - - // ============================================================================ - "all properties, entire tree": function(test) { - // ============================================================================ + + // ============================================================================ + 'all properties, entire tree': function(test) { + // ============================================================================ test.expect(1); var books = json.store.book; var expected = [books[0].author, books[1].author, books[2].author, books[3].author]; - var result = jsonpath(json, "$..author"); + var result = JSONPath({json: json, path: '$..author'}); test.deepEqual(expected, result); - + test.done(); }, - - // ============================================================================ - "all sub properties, single level": function(test) { - // ============================================================================ + + // ============================================================================ + 'all sub properties, single level': function(test) { + // ============================================================================ test.expect(1); var expected = [json.store.book, json.store.bicycle]; - var result = jsonpath(json, "$.store.*"); + var result = JSONPath({json: json, path: '$.store.*'}); test.deepEqual(expected, result); - + test.done(); }, - // ============================================================================ - "all sub properties, entire tree": function(test) { - // ============================================================================ + // ============================================================================ + 'all sub properties, entire tree': function(test) { + // ============================================================================ test.expect(1); var books = json.store.book; var expected = [books[0].price, books[1].price, books[2].price, books[3].price, json.store.bicycle.price]; - var result = jsonpath(json, "$.store..price"); + var result = JSONPath({json: json, path: '$.store..price'}); test.deepEqual(expected, result); - + test.done(); }, - - // ============================================================================ - "n property of entire tree": function(test) { - // ============================================================================ + + // ============================================================================ + 'n property of entire tree': function(test) { + // ============================================================================ test.expect(1); var books = json.store.book; var expected = [books[2]]; - var result = jsonpath(json, "$..book[2]"); + var result = JSONPath({json: json, path: '$..book[2]'}); test.deepEqual(expected, result); - + test.done(); }, - // ============================================================================ - "last property of entire tree": function(test) { - // ============================================================================ + // ============================================================================ + 'last property of entire tree': function(test) { + // ============================================================================ test.expect(2); var books = json.store.book; var expected = [books[3]]; - var result = jsonpath(json, "$..book[(@.length-1)]"); + var result = JSONPath({json: json, path: '$..book[(@.length-1)]'}); test.deepEqual(expected, result); - - result = jsonpath(json, "$..book[-1:]"); + + result = JSONPath({json: json, path: '$..book[-1:]'}); test.deepEqual(expected, result); - + test.done(); }, - - // ============================================================================ - "range of property of entire tree": function(test) { - // ============================================================================ + + // ============================================================================ + 'range of property of entire tree': function(test) { + // ============================================================================ test.expect(2); var books = json.store.book; var expected = [books[0], books[1]]; - var result = jsonpath(json, "$..book[0,1]"); + var result = JSONPath({json: json, path: '$..book[0,1]'}); test.deepEqual(expected, result); - - result = jsonpath(json, "$..book[:2]"); + + result = JSONPath({json: json, path: '$..book[:2]'}); test.deepEqual(expected, result); - + test.done(); }, - - // ============================================================================ - "filter all properties if sub property exists,o entire tree": function(test) { - // ============================================================================ + + // ============================================================================ + 'filter all properties if sub property exists, of entire tree': function(test) { + // ============================================================================ test.expect(1); var books = json.store.book; var expected = [books[2], books[3]]; - var result = jsonpath(json, "$..book[?(@.isbn)]"); + var result = JSONPath({json: json, path: '$..book[?(@.isbn)]'}); test.deepEqual(expected, result); - + test.done(); }, - - // ============================================================================ - "filter all properties if sub property greater than of entire tree": function(test) { - // ============================================================================ + + // ============================================================================ + 'filter all properties if sub property greater than of entire tree': function(test) { + // ============================================================================ test.expect(1); var books = json.store.book; var expected = [books[0], books[2]]; - var result = jsonpath(json, "$..book[?(@.price<10)]"); + var result = JSONPath({json: json, path: '$..book[?(@.price<10)]'}); test.deepEqual(expected, result); - + test.done(); }, - - // ============================================================================ - "all properties of a json structure": function(test) { - // ============================================================================ + + // ============================================================================ + 'all properties of a JSON structure': function(test) { + // ============================================================================ // test.expect(1); var expected = [ json.store, @@ -165,13 +165,13 @@ module.exports = testCase({ expected.push(json.store.bicycle.color); expected.push(json.store.bicycle.price); - var result = jsonpath(json, "$..*"); + var result = JSONPath({json: json, path: '$..*'}); test.deepEqual(expected, result); - + test.done(); } - - - - + + + + }); diff --git a/test/test.html b/test/test.html index f908920..505fd4f 100644 --- a/test/test.html +++ b/test/test.html @@ -7,54 +7,61 @@ -

JSONPath Tests

diff --git a/test/test.intermixed.arr.js b/test/test.intermixed.arr.js index 1cf622d..7a9eaa0 100644 --- a/test/test.intermixed.arr.js +++ b/test/test.intermixed.arr.js @@ -1,4 +1,4 @@ -var jsonpath = require("../").eval, +var JSONPath = require('../'), testCase = require('nodeunit').testCase // tests based on examples at http://goessner.net/articles/JsonPath/ @@ -39,13 +39,13 @@ var json = {"store":{ module.exports = testCase({ // ============================================================================ - "all sub properties, entire tree":function (test) { + 'all sub properties, entire tree': function (test) { // ============================================================================ test.expect(1); var books = json.store.book; var expected = [books[1].price, books[2].price, books[3].price, json.store.bicycle.price]; expected = books[0].price.concat(expected); - var result = jsonpath(json, "$.store..price", {flatten: true}); + var result = JSONPath({json: json, path: '$.store..price', flatten: true}); test.deepEqual(expected, result); test.done(); diff --git a/test/test.parent-selector.js b/test/test.parent-selector.js index 678c9e3..6f9c422 100644 --- a/test/test.parent-selector.js +++ b/test/test.parent-selector.js @@ -1,4 +1,4 @@ -var jsonpath = require("../").eval, +var JSONPath = require('../'), testCase = require('nodeunit').testCase var json = { @@ -14,49 +14,49 @@ var json = { module.exports = testCase({ // ============================================================================ - "simple parent selection": function(test) { + 'simple parent selection': function(test) { // ============================================================================ test.expect(1); - var result = jsonpath(json, "$.children[0]^", {flatten: true}); + var result = JSONPath({json: json, path: '$.children[0]^', flatten: true}); test.deepEqual(json.children, result); test.done(); }, // ============================================================================ - "parent selection with multiple matches": function(test) { + 'parent selection with multiple matches': function(test) { // ============================================================================ test.expect(1); var expected = [json.children,json.children]; - var result = jsonpath(json, "$.children[1:3]^"); + var result = JSONPath({json: json, path: '$.children[1:3]^'}); test.deepEqual(expected, result); test.done(); }, // ============================================================================ - "select sibling via parent": function(test) { + 'select sibling via parent': function(test) { // ============================================================================ test.expect(1); var expected = [{"name": "child3_2"}]; - var result = jsonpath(json, "$..[?(@.name && @.name.match(/3_1$/))]^[?(@.name.match(/_2$/))]"); + var result = JSONPath({json: json, path: '$..[?(@.name && @.name.match(/3_1$/))]^[?(@.name.match(/_2$/))]'}); test.deepEqual(expected, result); test.done(); }, // ============================================================================ - "parent parent parent": function(test) { + 'parent parent parent': function(test) { // ============================================================================ test.expect(1); var expected = json.children[0].children; - var result = jsonpath(json, "$..[?(@.name && @.name.match(/1_1$/))].name^^", {flatten: true}); + var result = JSONPath({json: json, path: '$..[?(@.name && @.name.match(/1_1$/))].name^^', flatten: true}); test.deepEqual(expected, result); test.done(); }, // ============================================================================ - "no such parent": function(test) { + 'no such parent': function(test) { // ============================================================================ test.expect(1); - var result = jsonpath(json, "name^^"); + var result = JSONPath({json: json, path: 'name^^'}); test.deepEqual([], result); test.done(); } From 3f6e79e9a78942edab0d55fbdee576cbd97718cc Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 18:38:36 -0700 Subject: [PATCH 11/68] Bump version to 0.11.0 --- CHANGES.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 3457fa2..afcaa98 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +## Dec 9, 2014 +* Offer new class-based API and object-based arguments +* Version 0.11 ## Oct 23, 2013 diff --git a/package.json b/package.json index 1d395d2..7fbeacf 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "email": "robert.krahn@gmail.com" } ], - "version": "0.10.0", + "version": "0.11.0", "repository": { "type": "git", "url": "git://github.com/s3u/JSONPath.git" From f8e3cda25ed6cde0e568243acf497e3d7e906e5a Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 19:14:33 -0700 Subject: [PATCH 12/68] For README, change to easier MD heading format; indicate alternative and deprecated syntaxes in docs; document properties and elaborate on syntax --- README.md | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8186602..3218917 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,12 @@ -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 In node.js: @@ -28,7 +25,28 @@ magic necessary: ``` -Examples +An alternative syntax is available as: + +```js +JSONPath(options, obj, path); +``` + +The following format is now deprecated: + +```js +jsonPath.eval(options, obj, path); +``` + +Other properties that can be supplied for +options (the first argument) include: + +- ***autostart*** (**default: true**) - If this is supplied as `false`, one may call the `evaluate` method manually as needed. +- ***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" or "path" to determine whether to return results as the values of the found items or as their absolute paths. +- ***sandbox*** (**default: An empty object **) - 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; 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, `false` will be returned (as opposed to an empty array). If `wrap` is set to false and a single result is found, that result will be the only item returned. An array will still be returned if multiple results are found, however. + +Syntax with examples -------- Given the following JSON, taken from http://goessner.net/articles/JsonPath/ : @@ -89,8 +107,11 @@ XPath | JSONPath | Result //*[price>19]/.. | $..[?(@.price>19)]^ | categories with things more expensive than 19 //* | $..* | all Elements in XML document. All members of JSON structure. -Development ------------ +Note that `@path` is additionally available for tests against the current path +as will be any additional variables supplied as properties on the optional +"sandbox" object option. + +# Development Running the tests on node: `npm test`. For in-browser tests: @@ -105,7 +126,6 @@ Running the tests on node: `npm test`. For in-browser tests: * To run the tests visit [http://localhost:8082/test/test.html](). -License -------- +# License [MIT License](http://www.opensource.org/licenses/mit-license.php). From 8db5e1a0a9cf9d4a09fc4915832210487389219a Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 19:15:15 -0700 Subject: [PATCH 13/68] Utilize more obscure internal variable naming to avoid potential clashes with sandbox-supplied vars; indicate object is private var. --- lib/jsonpath.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 38fc928..d810ff0 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -53,7 +53,7 @@ function JSONPath (opts, obj, expr) { JSONPath.prototype.evaluate = function (obj, expr) { var self = this; - this.obj = obj; + this._obj = obj; if (expr && obj && (this.resultType === 'value' || this.resultType === 'path')) { var exprList = this._normalize(expr); if (exprList[0] === '$' && exprList.length > 1) {exprList.shift();} @@ -192,14 +192,14 @@ JSONPath.prototype._slice = function (loc, expr, val, path) { }; JSONPath.prototype._eval = function (code, _v, _vname, path) { - if (!this.obj || !_v) {return false;} + if (!this._obj || !_v) {return false;} if (code.indexOf('@path') > -1) { - this.sandbox._path = this._asPath(path.concat([_vname])); - code = code.replace(/@path/g, '_path'); + this.sandbox._$_path = this._asPath(path.concat([_vname])); + code = code.replace(/@path/g, '_$_path'); } if (code.indexOf('@') > -1) { - this.sandbox._v = _v; - code = code.replace(/@/g, '_v'); + this.sandbox._$_v = _v; + code = code.replace(/@/g, '_$_v'); } try { return vm.runInNewContext(code, this.sandbox); From 0bc56314be95781e17b92fcaf749afd24aae1af1 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 19:31:07 -0700 Subject: [PATCH 14/68] Note on @path being a custom operator --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3218917..0feba07 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,8 @@ XPath | JSONPath | Result Note that `@path` is additionally available for tests against the current path as will be any additional variables supplied as properties on the optional -"sandbox" object option. +"sandbox" object option, although this was not indicated as part of the +original specification. # Development From 41088f6f01df466fcc8bfc7e39d96c3bc0c3ad98 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 19:56:02 -0700 Subject: [PATCH 15/68] Add XPath/JSONPath example to indicate getting multiple property results for objects --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0feba07..6a99da9 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ XPath | JSONPath | Result //book[last()] | $..book[(@.length-1)] | the last book in order. | $..book[-1:] | //book[position()<3]| $..book[0,1] | the first two books +//book/*[self::category\|self::author] or //book/(category,author) in XPath 2.0| $..book[category,author]| the categories and authors of all 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 From 41f9a37f5538c7213cad3c8dd1d3dfd65e668049 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 19:57:55 -0700 Subject: [PATCH 16/68] Fix placement in chart of book indexes --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 6a99da9..b5bbd46 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,8 @@ XPath | JSONPath | Result //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[position()<3]| $..book[0,1]
$..book[:2]| the first two books //book/*[self::category\|self::author] or //book/(category,author) in XPath 2.0| $..book[category,author]| the categories and authors of all 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 From 4b3cb09e81cc373e890519249e3043d607294562 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 20:14:07 -0700 Subject: [PATCH 17/68] Add Notes field to table and denote non-standard items --- README.md | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b5bbd46..65dab4c 100644 --- a/README.md +++ b/README.md @@ -91,26 +91,25 @@ Given the following JSON, taken from http://goessner.net/articles/JsonPath/ : ``` -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]
$..book[:2]| the first two books -//book/*[self::category\|self::author] or //book/(category,author) in XPath 2.0| $..book[category,author]| the categories and authors of all books -//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. - -Note that `@path` is additionally available for tests against the current path -as will be any additional variables supplied as properties on the optional -"sandbox" object option, although this was not indicated as part of the -original specification. +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 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)]
$..book[-1:] | the last book in order.| +//book[position()<3]| $..book[0,1]
$..book[:2]| the first two books | +//book/*[self::category\|self::author] or //book/(category,author) in XPath 2.0| $..book[category,author]| the categories and authors of all books | +//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 | Parent (caret) not present in original spec +//* | $..* | all Elements in XML document. All members of JSON structure. | +/store/book/[position()!=1] | $.store.book[?(@path !== "$[\'store\'][\'book\'][0]")] | All books besides that at the path pointing to the first | @path not present in original spec + +Any additional variables supplied as properties on the optional +"sandbox" object option are also available to (parenthetical-based) +evaluations. # Development From fb25083c149c8a707b68bc9b66f8e22270c7d52a Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 21:40:13 -0700 Subject: [PATCH 18/68] Name unused whole match var. in same way as other regex --- lib/jsonpath.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index d810ff0..ccdbfd5 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -81,7 +81,7 @@ JSONPath.prototype._normalize = function (expr) { var subx = []; var normalized = expr.replace(/[\['](\??\(.*?\))[\]']/g, function ($0, $1) {return '[#' + (subx.push($1) - 1) + ']';}) .replace(/'?\.'?|\['?/g, ';') - .replace(/(?:;)?(\^+)(?:;)?/g, function (_, ups) {return ';' + ups.split('').join(';') + ';';}) + .replace(/(?:;)?(\^+)(?:;)?/g, function ($0, ups) {return ';' + ups.split('').join(';') + ';';}) .replace(/;;;|;;/g, ';..;') .replace(/;$|'?\]|'$/g, ''); var exprList = normalized.split(';').map(function (expr) { From f1746de6bda265d746786f767d0bb58d4af6ede1 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 21:55:01 -0700 Subject: [PATCH 19/68] More precision for passing nullish objects --- lib/jsonpath.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index ccdbfd5..0905f08 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -39,13 +39,14 @@ function JSONPath (opts, obj, expr) { } opts = opts || {}; + var len1 = arguments.length === 1; 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 || {}; if (opts.autostart !== false) { - return this.evaluate(obj || opts.json, expr || opts.path); + return this.evaluate((len1 ? opts.json : obj), (len1 ? opts.path : expr)); } } From 1639b9250c6bf0ef4c65f723a5d111e976005a53 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 21:59:33 -0700 Subject: [PATCH 20/68] Fix checking issue --- lib/jsonpath.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 0905f08..8e33433 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -39,14 +39,14 @@ function JSONPath (opts, obj, expr) { } opts = opts || {}; - var len1 = arguments.length === 1; + var objArgs = opts.hasOwnProperty('json') && opts.hasOwnProperty('path'); 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 || {}; if (opts.autostart !== false) { - return this.evaluate((len1 ? opts.json : obj), (len1 ? opts.path : expr)); + return this.evaluate((objArgs ? opts.json : obj), (objArgs ? opts.path : expr)); } } From ef808d3421f64f172558faeb6fde2dbf98717a00 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 22:13:20 -0700 Subject: [PATCH 21/68] Return null or undefined if supplied; deal with nullish values by requiring "new" by avoided --- lib/jsonpath.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 8e33433..666741f 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -34,8 +34,16 @@ 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, obj, expr) { - if (!(this instanceof JSONPath)) { // Make "new" optional - return new JSONPath(opts, obj, expr); + if (!(this instanceof JSONPath)) { + try { + return new JSONPath(opts, obj, expr); + } + catch (e) { + if (!e.avoidNew) { + throw e; + } + return e.value; + } } opts = opts || {}; @@ -46,7 +54,10 @@ function JSONPath (opts, obj, expr) { this.sandbox = opts.sandbox || {}; if (opts.autostart !== false) { - return this.evaluate((objArgs ? opts.json : obj), (objArgs ? opts.path : expr)); + var ret = this.evaluate((objArgs ? opts.json : obj), (objArgs ? opts.path : expr)); + if (!ret) { + throw {avoidNew: true, value: ret, message: "JSONPath should not be called with 'new'"}; + } } } @@ -213,7 +224,7 @@ JSONPath.prototype._eval = function (code, _v, _vname, path) { // For backward compatibility (deprecated) JSONPath.eval = function (obj, expr, opts) { - return new JSONPath(opts, obj, expr); + return JSONPath(opts, obj, expr); }; if (typeof module === 'undefined') { From 9eb396b9ccfec2245a627d869d100cd719ea747f Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 9 Dec 2014 22:20:21 -0700 Subject: [PATCH 22/68] Better checking to avoid object creation --- lib/jsonpath.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 666741f..28e0737 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -55,7 +55,7 @@ function JSONPath (opts, obj, expr) { if (opts.autostart !== false) { var ret = this.evaluate((objArgs ? opts.json : obj), (objArgs ? opts.path : expr)); - if (!ret) { + if (!ret || typeof reg !== 'object') { throw {avoidNew: true, value: ret, message: "JSONPath should not be called with 'new'"}; } } From ae87bac905ead77701acd032cece566422603b86 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 00:03:56 -0700 Subject: [PATCH 23/68] With the path non-existent, the result should presumably be undefined, not null (also change tests to check for strict equality since test was not catching this). Add a test to actually return null, a valid JSON value. Incorporate issue #20 but with "all" instead of "both" as name to allow for other result types (e.g., I hope to add parent and property name). Stop returning false for falsy unwrapped (unless actually false) as could be null, a legitimate JSON value --- lib/jsonpath.js | 52 +++++++++++++++++++++++++------------- test/test.at_and_dollar.js | 11 ++++---- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 28e0737..c121427 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -19,6 +19,8 @@ if (!Array.isArray) { // could actually be require.js, for example. var isNode = typeof module !== 'undefined' && !!module.exports; +var allowedResultTypes = ['value', 'path', 'all']; + var vm = isNode ? require('vm') : { runInNewContext: function (expr, context) { @@ -66,24 +68,40 @@ function JSONPath (opts, obj, expr) { JSONPath.prototype.evaluate = function (obj, expr) { var self = this; this._obj = obj; - if (expr && obj && (this.resultType === 'value' || this.resultType === 'path')) { - var exprList = this._normalize(expr); - if (exprList[0] === '$' && exprList.length > 1) {exprList.shift();} - var result = this._trace(exprList, obj, ['$']); - result = result.filter(function (ea) { return ea && !ea.isParentSelector; }); - if (!result.length) {return this.wrap ? [] : false;} - if (result.length === 1 && !this.wrap && !Array.isArray(result[0].value)) {return result[0][this.resultType] || false;} - return result.reduce(function (result, ea) { - var valOrPath = ea[self.resultType]; - if (self.resultType === 'path') {valOrPath = self._asPath(valOrPath);} - if (self.flatten && Array.isArray(valOrPath)) { - result = result.concat(valOrPath); - } else { - result.push(valOrPath); - } - return result; - }, []); + if (!expr || !obj || allowedResultTypes.indexOf(this.resultType) === -1) { + return; + } + var exprList = this._normalize(expr); + if (exprList[0] === '$' && exprList.length > 1) {exprList.shift();} + var result = this._trace(exprList, obj, ['$']); + result = result.filter(function (ea) { return ea && !ea.isParentSelector; }); + if (!result.length) {return this.wrap ? [] : false;} + if (result.length === 1 && !this.wrap && !Array.isArray(result[0].value)) { + if (this.resultType == 'all') { + return result[0]; + } + return result[0][this.resultType]; } + return result.reduce(function (result, ea) { + var valOrPath; + switch (self.resultType) { + case 'value': + valOrPath = ea[self.resultType]; + break; + case 'path': + valOrPath = P.asPath(ea[self.resultType]); + break; + case 'all': + result.push(ea); + return result; + } + if (self.flatten && Array.isArray(valOrPath)) { + result = result.concat(valOrPath); + } else { + result.push(valOrPath); + } + return result; + }, []); }; // PRIVATE METHODS diff --git a/test/test.at_and_dollar.js b/test/test.at_and_dollar.js index 9c0eb42..1d6f21c 100644 --- a/test/test.at_and_dollar.js +++ b/test/test.at_and_dollar.js @@ -25,11 +25,12 @@ module.exports = testCase({ 'test undefined, null': function(test) { // ============================================================================ test.expect(5); - test.equal(undefined, JSONPath({json: undefined, path: 'foo'})); - test.equal(null, JSONPath({json: null, path: 'foo'})); - test.equal(undefined, JSONPath({json: {}, path: 'foo'})[0]); - test.equal(undefined, JSONPath({json: { a: 'b' }, path: 'foo'})[0]); - test.equal(undefined, JSONPath({json: { a: 'b' }, path: 'foo'})[100]); + test.strictEqual(null, JSONPath({json: {a: null}, path: '$.a'})); + test.strictEqual(undefined, JSONPath({json: undefined, path: 'foo'})); + test.strictEqual(undefined, JSONPath({json: null, path: 'foo'})); + test.strictEqual(undefined, JSONPath({json: {}, path: 'foo'})[0]); + test.strictEqual(undefined, JSONPath({json: { a: 'b' }, path: 'foo'})[0]); + test.strictEqual(undefined, JSONPath({json: { a: 'b' }, path: 'foo'})[100]); test.done(); }, From 1ad4b9458719a904e1350e880d45a3af039d2e56 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 00:09:01 -0700 Subject: [PATCH 24/68] Forgot wrap to test --- test/test.at_and_dollar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.at_and_dollar.js b/test/test.at_and_dollar.js index 1d6f21c..7343be8 100644 --- a/test/test.at_and_dollar.js +++ b/test/test.at_and_dollar.js @@ -25,7 +25,7 @@ module.exports = testCase({ 'test undefined, null': function(test) { // ============================================================================ test.expect(5); - test.strictEqual(null, JSONPath({json: {a: null}, path: '$.a'})); + test.strictEqual(null, JSONPath({json: {a: null}, path: '$.a', wrap: false})); test.strictEqual(undefined, JSONPath({json: undefined, path: 'foo'})); test.strictEqual(undefined, JSONPath({json: null, path: 'foo'})); test.strictEqual(undefined, JSONPath({json: {}, path: 'foo'})[0]); From 55ad5a85ab906cb6e296afec8e1cebe3a8978291 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 00:17:39 -0700 Subject: [PATCH 25/68] Forgot to update expected count --- test/test.at_and_dollar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.at_and_dollar.js b/test/test.at_and_dollar.js index 7343be8..7c83727 100644 --- a/test/test.at_and_dollar.js +++ b/test/test.at_and_dollar.js @@ -24,7 +24,7 @@ module.exports = testCase({ // ============================================================================ 'test undefined, null': function(test) { // ============================================================================ - test.expect(5); + test.expect(6); test.strictEqual(null, JSONPath({json: {a: null}, path: '$.a', wrap: false})); test.strictEqual(undefined, JSONPath({json: undefined, path: 'foo'})); test.strictEqual(undefined, JSONPath({json: null, path: 'foo'})); From 1b4cd4ba08775c9e399f766ab91788743a4832a1 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 00:22:36 -0700 Subject: [PATCH 26/68] Fix method for paths --- lib/jsonpath.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index c121427..db79370 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -89,7 +89,7 @@ JSONPath.prototype.evaluate = function (obj, expr) { valOrPath = ea[self.resultType]; break; case 'path': - valOrPath = P.asPath(ea[self.resultType]); + valOrPath = self._asPath(ea[self.resultType]); break; case 'all': result.push(ea); From d2fe68f8e23e624c2915cfe8e9cc81253d804b5e Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 10:15:07 -0700 Subject: [PATCH 27/68] Add to Changes --- CHANGES.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index afcaa98..1f20543 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,8 @@ ## Dec 9, 2014 -* Offer new class-based API and object-based arguments +* Offer new class-based API and object-based arguments (with option to run new queries without resupplying config) +* Fix bug preventing an unwrapped null from being returned +* Return undefined instead of false upon failure to find path (since undefined is not a possible JSON value) +* Support "all" for resultType ("path" and "value" together) * Version 0.11 ## Oct 23, 2013 From afea512c2136f2277642556b176ea32581a5c7aa Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 10:30:45 -0700 Subject: [PATCH 28/68] Add testing for "all" --- test/test.all.js | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ test/test.html | 1 + 2 files changed, 66 insertions(+) create mode 100644 test/test.all.js diff --git a/test/test.all.js b/test/test.all.js new file mode 100644 index 0000000..5fb6347 --- /dev/null +++ b/test/test.all.js @@ -0,0 +1,65 @@ +var JSONPath = require('../'), + testCase = require('nodeunit').testCase + +var json = { + "name": "root", + "children": [ + {"name": "child1", "children": [{"name": "child1_1"},{"name": "child1_2"}]}, + {"name": "child2", "children": [{"name": "child2_1"}]}, + {"name": "child3", "children": [{"name": "child3_1"}, {"name": "child3_2"}]} + ] +}; + + +module.exports = testCase({ + + // ============================================================================ + 'simple parent selection, return both path and value': function(test) { + // ============================================================================ + test.expect(1); + var result = JSONPath({json: json, path: '$.children[0]^', resultType: 'all'}); + test.deepEqual([{path: ['$', 'children'], value: json.children}], result); + test.done(); + }, + + // ============================================================================ + 'parent selection with multiple matches, return both path and value': function(test) { + // ============================================================================ + test.expect(1); + var expectedOne = {path: ['$', 'children'], value: json.children}; + var expected = [expectedOne, expectedOne]; + var result = JSONPath({json: json, path: '$.children[1:3]^', resultType: 'all'}); + test.deepEqual(expected, result); + test.done(); + }, + + // ============================================================================ + 'select sibling via parent, return both path and value': function(test) { + // ============================================================================ + test.expect(1); + var expected = [{path: [ '$', 'children', 2, 'children', 1], value: {name: 'child3_2'}}]; + var result = JSONPath({json: json, path: '$..[?(@.name && @.name.match(/3_1$/))]^[?(@.name.match(/_2$/))]', resultType: 'all'}); + test.deepEqual(expected, result); + test.done(); + }, + + // ============================================================================ + 'parent parent parent, return both path and value': function(test) { + // ============================================================================ + test.expect(1); + var expected = [{path: ['$', 'children', 0, 'children'], value: json.children[0].children}]; + var result = JSONPath({json: json, path: '$..[?(@.name && @.name.match(/1_1$/))].name^^', resultType: 'all'}); + test.deepEqual(expected, result); + test.done(); + }, + + // ============================================================================ + 'no such parent': function(test) { + // ============================================================================ + test.expect(1); + var result = JSONPath({json: json, path: 'name^^', resultType: 'all'}); + test.deepEqual([], result); + test.done(); + } + +}); diff --git a/test/test.html b/test/test.html index 505fd4f..4f35718 100644 --- a/test/test.html +++ b/test/test.html @@ -62,6 +62,7 @@

JSONPath Tests

loadJS('test.examples.js'); loadJS('test.intermixed.arr.js'); loadJS('test.parent-selector.js'); + loadJS('test.all.js'); nodeunit.run(suites); From 020c2a7f5676a70fc3cc9453e8afd7396fb9252a Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 10:32:59 -0700 Subject: [PATCH 29/68] Support periods within properties per issue #24 (and document regex) --- CHANGES.md | 1 + lib/jsonpath.js | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1f20543..f8736c6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ * Fix bug preventing an unwrapped null from being returned * Return undefined instead of false upon failure to find path (since undefined is not a possible JSON value) * Support "all" for resultType ("path" and "value" together) +* Support "." within properties * Version 0.11 ## Oct 23, 2013 diff --git a/lib/jsonpath.js b/lib/jsonpath.js index db79370..9547f63 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -109,10 +109,22 @@ JSONPath.prototype.evaluate = function (obj, expr) { JSONPath.prototype._normalize = function (expr) { if (cache[expr]) {return cache[expr];} var subx = []; - var normalized = expr.replace(/[\['](\??\(.*?\))[\]']/g, function ($0, $1) {return '[#' + (subx.push($1) - 1) + ']';}) + var normalized = expr + // Parenthetical evaluations (filtering and otherwise) + .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]+)/); From d36954ac1d80a096b47abd81908ff61851034f83 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 10:38:07 -0700 Subject: [PATCH 30/68] Add tests for periods within properties --- test/test.html | 1 + test/test.properties.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 test/test.properties.js diff --git a/test/test.html b/test/test.html index 4f35718..0cc3a56 100644 --- a/test/test.html +++ b/test/test.html @@ -63,6 +63,7 @@

JSONPath Tests

loadJS('test.intermixed.arr.js'); loadJS('test.parent-selector.js'); loadJS('test.all.js'); + loadJS('test.properties.js'); nodeunit.run(suites); diff --git a/test/test.properties.js b/test/test.properties.js new file mode 100644 index 0000000..309c67d --- /dev/null +++ b/test/test.properties.js @@ -0,0 +1,28 @@ +var JSONPath = require('../'), + testCase = require('nodeunit').testCase + + +var json = { + "test1": { + "test2": { + "test3.test4.test5": { + "test7": "value" + } + } + } +}; + + +module.exports = testCase({ + + // ============================================================================ + 'Periods within properties': function (test) { + // ============================================================================ + test.expect(1); + var expected = {"test7": "value"}; + var result = JSONPath({json: json, path: "$.test1.test2['test3.test4.test5']", flatten: true}); + test.deepEqual(expected, result); + + test.done(); + } +}); From 295c64cae6e3ae58a5c42ba0a69623ccb89e17c9 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 11:24:14 -0700 Subject: [PATCH 31/68] Obscure JSON.stringify fix --- lib/jsonpath.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 9547f63..3a609b6 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -25,7 +25,10 @@ var vm = isNode ? require('vm') : { runInNewContext: function (expr, context) { return eval(Object.keys(context).reduce(function (s, vr) { - return 'var ' + vr + '=' + JSON.stringify(context[vr]) + ';' + s; + 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)); } }; From 9e3b32c2795493eabd46a4911128f0f41223b07a Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 12:46:28 -0700 Subject: [PATCH 32/68] Elaborate comment --- lib/jsonpath.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 3a609b6..20edc67 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -113,7 +113,7 @@ JSONPath.prototype._normalize = function (expr) { if (cache[expr]) {return cache[expr];} var subx = []; var normalized = expr - // Parenthetical evaluations (filtering and otherwise) + // 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) { From c9c87485b015d2f35ebb158fd55678c7c06341a8 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 13:23:53 -0700 Subject: [PATCH 33/68] Fix sp. on var. --- lib/jsonpath.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 20edc67..80e95b3 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -60,7 +60,7 @@ function JSONPath (opts, obj, expr) { if (opts.autostart !== false) { var ret = this.evaluate((objArgs ? opts.json : obj), (objArgs ? opts.path : expr)); - if (!ret || typeof reg !== 'object') { + if (!ret || typeof ret !== 'object') { throw {avoidNew: true, value: ret, message: "JSONPath should not be called with 'new'"}; } } @@ -80,7 +80,7 @@ JSONPath.prototype.evaluate = function (obj, expr) { result = result.filter(function (ea) { return ea && !ea.isParentSelector; }); if (!result.length) {return this.wrap ? [] : false;} if (result.length === 1 && !this.wrap && !Array.isArray(result[0].value)) { - if (this.resultType == 'all') { + if (this.resultType === 'all') { return result[0]; } return result[0][this.resultType]; From 6eae7788d16d884e581bf194b4f82cb75e4ffadf Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 13:26:04 -0700 Subject: [PATCH 34/68] Fix test --- test/test.properties.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.properties.js b/test/test.properties.js index 309c67d..387e883 100644 --- a/test/test.properties.js +++ b/test/test.properties.js @@ -20,7 +20,7 @@ module.exports = testCase({ // ============================================================================ test.expect(1); var expected = {"test7": "value"}; - var result = JSONPath({json: json, path: "$.test1.test2['test3.test4.test5']", flatten: true}); + var result = JSONPath({json: json, path: "$.test1.test2['test3.test4.test5']", wrap: false}); test.deepEqual(expected, result); test.done(); From 89b11b4ab59afca028f228e15e689efcf1a975f5 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 13:41:01 -0700 Subject: [PATCH 35/68] Properly handle object return values! --- lib/jsonpath.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 80e95b3..1b1e4d0 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -61,8 +61,9 @@ function JSONPath (opts, obj, expr) { if (opts.autostart !== false) { var ret = this.evaluate((objArgs ? opts.json : obj), (objArgs ? opts.path : expr)); if (!ret || typeof ret !== 'object') { - throw {avoidNew: true, value: ret, message: "JSONPath should not be called with 'new'"}; + throw {avoidNew: true, value: ret, message: "JSONPath should not be called with 'new' (it prevents return of (unwrapped) scalar values)"}; } + return ret; } } From 30731d706bbf3bd60e15b17868d4510cbb3dff4f Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 13:58:36 -0700 Subject: [PATCH 36/68] Return undefined for unwrapped failures and clarify changes --- CHANGES.md | 2 +- lib/jsonpath.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f8736c6..8569067 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ ## Dec 9, 2014 * Offer new class-based API and object-based arguments (with option to run new queries without resupplying config) * Fix bug preventing an unwrapped null from being returned -* Return undefined instead of false upon failure to find path (since undefined is not a possible JSON value) +* For unwrapped results, return undefined instead of false upon failure to find path (since undefined is not a possible JSON value) and return the exact value upon falsy single results (in order to allow return of null) * Support "all" for resultType ("path" and "value" together) * Support "." within properties * Version 0.11 diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 1b1e4d0..4093578 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -79,7 +79,7 @@ JSONPath.prototype.evaluate = function (obj, expr) { if (exprList[0] === '$' && exprList.length > 1) {exprList.shift();} var result = this._trace(exprList, obj, ['$']); result = result.filter(function (ea) { return ea && !ea.isParentSelector; }); - if (!result.length) {return this.wrap ? [] : false;} + if (!result.length) {return this.wrap ? [] : undefined;} if (result.length === 1 && !this.wrap && !Array.isArray(result[0].value)) { if (this.resultType === 'all') { return result[0]; From dbdd640d0e2a4408bf2683d3fc4147965706c385 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 14:04:50 -0700 Subject: [PATCH 37/68] Clarify changes --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 8569067..a5f7e42 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ ## Dec 9, 2014 * Offer new class-based API and object-based arguments (with option to run new queries without resupplying config) * Fix bug preventing an unwrapped null from being returned -* For unwrapped results, return undefined instead of false upon failure to find path (since undefined is not a possible JSON value) and return the exact value upon falsy single results (in order to allow return of null) +* 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) * Support "all" for resultType ("path" and "value" together) * Support "." within properties * Version 0.11 From 7a47e0403f3343cce3d6799ef0a5900c7adea0a0 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 14:42:39 -0700 Subject: [PATCH 38/68] Avoid raising closure concern --- lib/jsonpath.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 4093578..f4e4fa9 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -182,7 +182,7 @@ JSONPath.prototype._trace = function (expr, val, path) { } else if (loc.indexOf('?(') === 0) { // [?(expr)] this._walk(loc, x, val, path, function (m, l, x, v, p) { - if (self._eval(l.replace(/^\?\((.*?)\)$/, '$1'), v[m], m, path)) { + if (self._eval(l.replace(/^\?\((.*?)\)$/, '$1'), v[m], m, p)) { addRet(self._trace(unshift(m, x), v, p)); } }); From f9f8b2b8a8c3a5978131899769bc171b6ad82186 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 15:18:14 -0700 Subject: [PATCH 39/68] Include updated wrap docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 65dab4c..4651423 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ options (the first argument) include: - ***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" or "path" to determine whether to return results as the values of the found items or as their absolute paths. - ***sandbox*** (**default: An empty object **) - 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; 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, `false` will be returned (as opposed to an empty array). If `wrap` is set to false and a single result is found, that result will be the only item returned. An array will still be returned if multiple results are found, however. +- ***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. Syntax with examples -------- From 0f60fd8875fbc031a345a0fc611f817778c92307 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 15:19:49 -0700 Subject: [PATCH 40/68] Update docs re: resultType==="all" and clarify sandbox docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4651423..681dadd 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,8 @@ options (the first argument) include: - ***autostart*** (**default: true**) - If this is supplied as `false`, one may call the `evaluate` method manually as needed. - ***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" or "path" to determine whether to return results as the values of the found items or as their absolute paths. -- ***sandbox*** (**default: An empty object **) - 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; see the Syntax section for details.) +- ***resultType*** (**default: "value"**) - Can be case-insensitive form of "value" or "path" to determine whether to return results as the values of the found items or as their absolute paths. If set to "all", both "value" and "path" will be returned. +- ***sandbox*** (**default: An empty object **) - 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. Syntax with examples From e5b2c36f394f1f4edf64bfbcb68b584aff355d31 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 10 Dec 2014 16:10:56 -0700 Subject: [PATCH 41/68] More equivalent XPath expression --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 681dadd..c1ea478 100644 --- a/README.md +++ b/README.md @@ -103,9 +103,11 @@ XPath | JSONPath | Result //book/*[self::category\|self::author] or //book/(category,author) in XPath 2.0| $..book[category,author]| the categories and authors of all books | //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 | Parent (caret) not present in original spec +//*[price>19]/.. | $..[?(@.price>19)]^ | categories with things more expensive than 19 | Parent (caret) not present in the original spec //* | $..* | all Elements in XML document. All members of JSON structure. | -/store/book/[position()!=1] | $.store.book[?(@path !== "$[\'store\'][\'book\'][0]")] | All books besides that at the path pointing to the first | @path not present in original spec +/store/book[not(. is /store/book[1])] | $.store.book[?(@path !== "$[\'store\'][\'book\'][0]")] | All books besides that at the path pointing to the first | @path not present in the original spec + + Any additional variables supplied as properties on the optional "sandbox" object option are also available to (parenthetical-based) From 7c23aa66bd634a4983c534475d4ddc79a17eea9d Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Dec 2014 00:36:30 -0700 Subject: [PATCH 42/68] Support "parent" and "parentProperty" for resultType; Support custom @parent, @parentProperty, @property inside evaluations; Fix @path in index/property evaluations; Support a custom operator ("~") to allow grabbing of property names; Expose cache on JSONPath.cache for those who wish to preserve and reuse it; Allow ^ as property name; Clarify comments No need for concated objects to be inside an array --- CHANGES.md | 11 ++++-- lib/jsonpath.js | 97 +++++++++++++++++++++++++++++++------------------ 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a5f7e42..9680c63 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,9 +1,14 @@ -## Dec 9, 2014 +## Dec 12, 2014 * Offer new class-based API and object-based arguments (with option to run new queries without resupplying config) -* Fix bug preventing an unwrapped null from being returned +* Fix bug preventing unwrapped null from being a possible return value * 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) -* Support "all" for resultType ("path" and "value" together) +* Support "parent" and "parentProperty" for resultType along with "all" (which also includes "path" and "value" together) * Support "." within properties +* Support custom @parent, @parentProperty, @property (in addition to custom property @path) inside evaluations +* Support a custom operator ("~") to allow grabbing of property names +* Fix for @path in index/property evaluations +* Expose cache on JSONPath.cache for those who wish to preserve and reuse it +* Allow ^ as property name * Version 0.11 ## Oct 23, 2013 diff --git a/lib/jsonpath.js b/lib/jsonpath.js index f4e4fa9..b5a2d36 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -19,7 +19,7 @@ if (!Array.isArray) { // could actually be require.js, for example. var isNode = typeof module !== 'undefined' && !!module.exports; -var allowedResultTypes = ['value', 'path', 'all']; +var allowedResultTypes = ['value', 'path', 'parent', 'parentProperty', 'all']; var vm = isNode ? require('vm') : { @@ -33,8 +33,6 @@ var vm = isNode ? } }; -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;} @@ -77,7 +75,7 @@ JSONPath.prototype.evaluate = function (obj, expr) { } var exprList = this._normalize(expr); if (exprList[0] === '$' && exprList.length > 1) {exprList.shift();} - var result = this._trace(exprList, obj, ['$']); + var result = this._trace(exprList, obj, ['$'], null, null); // We could add arguments to let user pass parent and its property name in case it needed to access the parent result = result.filter(function (ea) { return ea && !ea.isParentSelector; }); if (!result.length) {return this.wrap ? [] : undefined;} if (result.length === 1 && !this.wrap && !Array.isArray(result[0].value)) { @@ -111,9 +109,12 @@ JSONPath.prototype.evaluate = function (obj, expr) { // PRIVATE METHODS JSONPath.prototype._normalize = function (expr) { + var cache = JSONPath.cache; if (cache[expr]) {return cache[expr];} var subx = []; var normalized = expr + // Properties + .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 @@ -141,87 +142,97 @@ JSONPath.prototype._normalize = function (expr) { JSONPath.prototype._asPath = function (path) { var i, n, x = path, p = '$'; for (i = 1, n = x.length; i < n; i++) { - p += /^[0-9*]+$/.test(x[i]) ? ('[' + x[i] + ']') : ("['" + x[i] + "']"); + p += (x[i] === '~') ? x[i] : ((/^[0-9*]+$/).test(x[i]) ? ('[' + x[i] + ']') : ("['" + x[i] + "']")); } return p; }; -JSONPath.prototype._trace = function (expr, val, path) { +JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName) { // No expr to follow? return path and value as the result of this trace branch var self = this; - if (!expr.length) {return [{path: path, value: val}];} + if (!expr.length) {return {path: path, value: val, parent: parent, parentProperty: parentPropName};} 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}] : [];} // 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))); + if (val && val.hasOwnProperty(loc)) { // simple case--directly follow property + addRet(this._trace(x, val[loc], push(path, loc), val, loc)); } - else if (loc === '*') { // any property - this._walk(loc, x, val, path, function (m, l, x, v, p) { - addRet(self._trace(unshift(m, x), v, p)); + else if (loc === '*') { // all child properties + this._walk(loc, x, val, path, parent, parentPropName, function (m, l, x, v, p, par, pr) { + addRet(self._trace(unshift(m, x), v, p, par, pr)); }); } - else if (loc === '..') { // all child properties - addRet(this._trace(x, val, path)); - this._walk(loc, x, val, path, function (m, l, x, v, p) { - if (typeof v[m] === 'object') { - addRet(self._trace(unshift('..', x), v[m], push(p, m))); + else if (loc === '..') { // all descendent properties + addRet(this._trace(x, val, path, parent, parentPropName)); // Check remaining expression with val's immediate children + this._walk(loc, x, val, path, parent, parentPropName, function (m, l, x, v, p) { + 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)); } }); } - else if (loc[0] === '(') { // [(expr)] - addRet(this._trace(unshift(this._eval(loc, val, path[path.length], path), x), val, path)); + else if (loc[0] === '(') { // [(expr)] (dynamic property/index) + // 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)); + } + // The parent sel computation is handled in the frame above using the + // ancestor object of val + else if (loc === '^') { + return path.length ? { + path: path.slice(0, -1), + expr: x, + isParentSelector: true + } : []; + } + else if (loc === '~') { // property name + return {path: push(path, loc), value: parentPropName, parent: parent, parentProperty: null}; } - else if (loc.indexOf('?(') === 0) { // [?(expr)] - this._walk(loc, x, val, path, function (m, l, x, v, p) { - if (self._eval(l.replace(/^\?\((.*?)\)$/, '$1'), v[m], m, p)) { - addRet(self._trace(unshift(m, x), v, p)); + else if (loc.indexOf('?(') === 0) { // [?(expr)] (filtering) + this._walk(loc, x, val, path, parent, parentPropName, function (m, l, x, v, p, par, pr) { + if (self._eval(l.replace(/^\?\((.*?)\)$/, '$1'), v[m], m, p, par, pr)) { + addRet(self._trace(unshift(m, x), v, p, par, pr)); } }); } 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)); + addRet(this._trace(unshift(parts[i], x), val, path, parent, parentPropName)); } } else if (/^(-?[0-9]*):(-?[0-9]*):?([0-9]*)$/.test(loc)) { // [start:end:step] Python slice syntax - addRet(this._slice(loc, x, val, path)); + addRet(this._slice(loc, x, val, path, parent, parentPropName)); } // 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) : [ea]); + return all.concat(ea.isParentSelector ? self._trace(ea.expr, val, ea.path, parent, parentPropName) : ea); }, []); }; -JSONPath.prototype._walk = function (loc, expr, val, path, f) { +JSONPath.prototype._walk = function (loc, expr, val, path, parent, parentPropName, f) { var i, n, m; if (Array.isArray(val)) { for (i = 0, n = val.length; i < n; i++) { - f(i, loc, expr, val, path); + f(i, loc, expr, val, path, parent, parentPropName); } } else if (typeof val === 'object') { for (m in val) { if (val.hasOwnProperty(m)) { - f(m, loc, expr, val, path); + f(m, loc, expr, val, path, parent, parentPropName); } } } }; -JSONPath.prototype._slice = function (loc, expr, val, path) { +JSONPath.prototype._slice = function (loc, expr, val, path, parent, parentPropName) { if (!Array.isArray(val)) {return;} var i, len = val.length, parts = loc.split(':'), @@ -232,13 +243,25 @@ JSONPath.prototype._slice = function (loc, expr, val, path) { 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)); + ret = ret.concat(this._trace(unshift(i, expr), val, path, parent, parentPropName)); } return ret; }; -JSONPath.prototype._eval = function (code, _v, _vname, path) { +JSONPath.prototype._eval = function (code, _v, _vname, path, parent, parentPropName) { if (!this._obj || !_v) {return false;} + if (code.indexOf('@parentProperty') > -1) { + this.sandbox._$_parentProperty = parentPropName; + code = code.replace(/@parentProperty/g, '_$_parentProperty'); + } + if (code.indexOf('@parent') > -1) { + this.sandbox._$_parent = parent; + code = code.replace(/@parent/g, '_$_parent'); + } + if (code.indexOf('@property') > -1) { + this.sandbox._$_property = _vname; + code = code.replace(/@property/g, '_$_property'); + } if (code.indexOf('@path') > -1) { this.sandbox._$_path = this._asPath(path.concat([_vname])); code = code.replace(/@path/g, '_$_path'); @@ -256,6 +279,10 @@ JSONPath.prototype._eval = function (code, _v, _vname, path) { } }; + +// Could store the cache object itself +JSONPath.cache = {}; + // For backward compatibility (deprecated) JSONPath.eval = function (obj, expr, opts) { return JSONPath(opts, obj, expr); From 43544c352efab2c1ce0d05ff06a64b07ad57c40a Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Dec 2014 01:19:34 -0700 Subject: [PATCH 43/68] More info on instance and class properties; document the new custom property for property name retrieval and custom @parent within evaluations --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c1ea478..2ab6840 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,14 @@ jsonPath.eval(options, obj, path); Other properties that can be supplied for options (the first argument) include: -- ***autostart*** (**default: true**) - If this is supplied as `false`, one may call the `evaluate` method manually as needed. +- ***autostart*** (**default: true**) - If this is supplied as `false`, one may call the `evaluate` method manually (with an object and expression) as needed. - ***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" or "path" to determine whether to return results as the values of the found items or as their absolute paths. If set to "all", both "value" and "path" will be returned. +- ***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: An empty object **) - 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. +There is also now a class property, on JSONPath.cache which exposes the cache for those who wish to preserve and reuse it for optimization purposes. + Syntax with examples -------- @@ -106,8 +108,8 @@ XPath | JSONPath | Result //*[price>19]/.. | $..[?(@.price>19)]^ | categories with things more expensive than 19 | Parent (caret) not present in the original spec //* | $..* | all Elements in XML document. All members of JSON structure. | /store/book[not(. is /store/book[1])] | $.store.book[?(@path !== "$[\'store\'][\'book\'][0]")] | All books besides that at the path pointing to the first | @path not present in the original spec - - +/store/*/name() in XPath 2.0 | $.store.*~ | The property names of the store sub-object ("book" and "bicycle") | Property name (tilde) is not present in the original spec +//category[parent::*/author = "J. R. R. Tolkien"] | $..category[?(@parent.author === "J. R. R. Tolkien")] | Grabs all categories whose parent's author (i.e., the author sibling to the category property) is J. R. R. Tolkien | @parent is not present in the original spec Any additional variables supplied as properties on the optional "sandbox" object option are also available to (parenthetical-based) From 796cf84d1c795351408d565993d58d36b3436403 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Dec 2014 02:39:22 -0700 Subject: [PATCH 44/68] document new custom properties --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ab6840..788d23d 100644 --- a/README.md +++ b/README.md @@ -105,11 +105,13 @@ XPath | JSONPath | Result //book/*[self::category\|self::author] or //book/(category,author) in XPath 2.0| $..book[category,author]| the categories and authors of all books | //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 | Parent (caret) not present in the original spec //* | $..* | all Elements in XML document. All members of JSON structure. | +//*[price>19]/.. | $..[?(@.price>19)]^ | categories with things more expensive than 19 | Parent (caret) not present in the original spec /store/book[not(. is /store/book[1])] | $.store.book[?(@path !== "$[\'store\'][\'book\'][0]")] | All books besides that at the path pointing to the first | @path not present in the original spec /store/*/name() in XPath 2.0 | $.store.*~ | The property names of the store sub-object ("book" and "bicycle") | Property name (tilde) is not present in the original spec //category[parent::*/author = "J. R. R. Tolkien"] | $..category[?(@parent.author === "J. R. R. Tolkien")] | Grabs all categories whose parent's author (i.e., the author sibling to the category property) is J. R. R. Tolkien | @parent is not present in the original spec +//book/*[name() != 'category'] | $..book.*[?(@property !== "category")] | Grabs the children of "book" except for "category" ones | @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 Any additional variables supplied as properties on the optional "sandbox" object option are also available to (parenthetical-based) From 5571f3748fa729c3b07e8da76ddf70848c0bc8b9 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Dec 2014 16:16:29 -0700 Subject: [PATCH 45/68] Update "all" tests since now returning more data --- test/test.all.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test.all.js b/test/test.all.js index 5fb6347..b931255 100644 --- a/test/test.all.js +++ b/test/test.all.js @@ -18,7 +18,7 @@ module.exports = testCase({ // ============================================================================ test.expect(1); var result = JSONPath({json: json, path: '$.children[0]^', resultType: 'all'}); - test.deepEqual([{path: ['$', 'children'], value: json.children}], result); + test.deepEqual([{path: ['$', 'children'], value: json.children, parent: json, parentProperty: 'children'}], result); test.done(); }, @@ -26,7 +26,7 @@ module.exports = testCase({ 'parent selection with multiple matches, return both path and value': function(test) { // ============================================================================ test.expect(1); - var expectedOne = {path: ['$', 'children'], value: json.children}; + var expectedOne = {path: ['$', 'children'], value: json.children, parent: json, parentProperty: 'children'}}; var expected = [expectedOne, expectedOne]; var result = JSONPath({json: json, path: '$.children[1:3]^', resultType: 'all'}); test.deepEqual(expected, result); @@ -37,7 +37,7 @@ module.exports = testCase({ 'select sibling via parent, return both path and value': function(test) { // ============================================================================ test.expect(1); - var expected = [{path: [ '$', 'children', 2, 'children', 1], value: {name: 'child3_2'}}]; + var expected = [{path: [ '$', 'children', 2, 'children', 1], value: {name: 'child3_2'}, parent: json.children[2].children, parentProperty: 1}]; var result = JSONPath({json: json, path: '$..[?(@.name && @.name.match(/3_1$/))]^[?(@.name.match(/_2$/))]', resultType: 'all'}); test.deepEqual(expected, result); test.done(); @@ -47,7 +47,7 @@ module.exports = testCase({ 'parent parent parent, return both path and value': function(test) { // ============================================================================ test.expect(1); - var expected = [{path: ['$', 'children', 0, 'children'], value: json.children[0].children}]; + var expected = [{path: ['$', 'children', 0, 'children'], value: json.children[0].children, parent: json.children[0], parentProperty: 'children'}]; var result = JSONPath({json: json, path: '$..[?(@.name && @.name.match(/1_1$/))].name^^', resultType: 'all'}); test.deepEqual(expected, result); test.done(); From 25ee4298cb479c66d4ea14ed45fe316dd73eb996 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Dec 2014 16:22:05 -0700 Subject: [PATCH 46/68] jslint and fix syntax error --- test/test.all.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/test/test.all.js b/test/test.all.js index b931255..9fb4675 100644 --- a/test/test.all.js +++ b/test/test.all.js @@ -1,5 +1,9 @@ -var JSONPath = require('../'), - testCase = require('nodeunit').testCase +/*global require, module*/ +/*jslint vars:true*/ +(function () {'use strict'; + +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; var json = { "name": "root", @@ -17,7 +21,7 @@ module.exports = testCase({ 'simple parent selection, return both path and value': function(test) { // ============================================================================ test.expect(1); - var result = JSONPath({json: json, path: '$.children[0]^', resultType: 'all'}); + var result = jsonpath({json: json, path: '$.children[0]^', resultType: 'all'}); test.deepEqual([{path: ['$', 'children'], value: json.children, parent: json, parentProperty: 'children'}], result); test.done(); }, @@ -26,9 +30,9 @@ module.exports = testCase({ 'parent selection with multiple matches, return both path and value': function(test) { // ============================================================================ test.expect(1); - var expectedOne = {path: ['$', 'children'], value: json.children, parent: json, parentProperty: 'children'}}; + var expectedOne = {path: ['$', 'children'], value: json.children, parent: json, parentProperty: 'children'}; var expected = [expectedOne, expectedOne]; - var result = JSONPath({json: json, path: '$.children[1:3]^', resultType: 'all'}); + var result = jsonpath({json: json, path: '$.children[1:3]^', resultType: 'all'}); test.deepEqual(expected, result); test.done(); }, @@ -38,7 +42,7 @@ module.exports = testCase({ // ============================================================================ test.expect(1); var expected = [{path: [ '$', 'children', 2, 'children', 1], value: {name: 'child3_2'}, parent: json.children[2].children, parentProperty: 1}]; - var result = JSONPath({json: json, path: '$..[?(@.name && @.name.match(/3_1$/))]^[?(@.name.match(/_2$/))]', resultType: 'all'}); + var result = jsonpath({json: json, path: '$..[?(@.name && @.name.match(/3_1$/))]^[?(@.name.match(/_2$/))]', resultType: 'all'}); test.deepEqual(expected, result); test.done(); }, @@ -48,7 +52,7 @@ module.exports = testCase({ // ============================================================================ test.expect(1); var expected = [{path: ['$', 'children', 0, 'children'], value: json.children[0].children, parent: json.children[0], parentProperty: 'children'}]; - var result = JSONPath({json: json, path: '$..[?(@.name && @.name.match(/1_1$/))].name^^', resultType: 'all'}); + var result = jsonpath({json: json, path: '$..[?(@.name && @.name.match(/1_1$/))].name^^', resultType: 'all'}); test.deepEqual(expected, result); test.done(); }, @@ -57,9 +61,11 @@ module.exports = testCase({ 'no such parent': function(test) { // ============================================================================ test.expect(1); - var result = JSONPath({json: json, path: 'name^^', resultType: 'all'}); + var result = jsonpath({json: json, path: 'name^^', resultType: 'all'}); test.deepEqual([], result); test.done(); } }); + +}()); From 2ab92a08995ca93ddc3d143cf804522d51da5cd5 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Dec 2014 16:29:50 -0700 Subject: [PATCH 47/68] apply JSLint to tests --- test/test.arr.js | 14 ++++++++---- test/test.at_and_dollar.js | 39 +++++++++++++++++---------------- test/test.eval.js | 16 +++++++++----- test/test.examples.js | 42 ++++++++++++++++++++---------------- test/test.intermixed.arr.js | 14 ++++++++---- test/test.parent-selector.js | 20 +++++++++++------ test/test.properties.js | 12 ++++++++--- 7 files changed, 97 insertions(+), 60 deletions(-) diff --git a/test/test.arr.js b/test/test.arr.js index d206071..f68da04 100644 --- a/test/test.arr.js +++ b/test/test.arr.js @@ -1,5 +1,9 @@ -var JSONPath = require('../'), - testCase = require('nodeunit').testCase +/*global require, module*/ +/*jslint vars:true*/ +(function () {'use strict'; + +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; var json = { "store": { @@ -21,15 +25,17 @@ var json = { module.exports = testCase({ 'get single': function (test) { var expected = json.store.book; - var result = JSONPath({json: json, path: 'store.book', flatten: true, wrap: false}); + var result = jsonpath({json: json, path: 'store.book', flatten: true, wrap: false}); test.deepEqual(expected, result); test.done(); }, 'get arr': function (test) { var expected = json.store.books; - var result = JSONPath({json: json, path: 'store.books', flatten: true, wrap: false}); + var result = jsonpath({json: json, path: 'store.books', flatten: true, wrap: false}); test.deepEqual(expected, result); test.done(); } }); + +}()); diff --git a/test/test.at_and_dollar.js b/test/test.at_and_dollar.js index 7c83727..b84955c 100644 --- a/test/test.at_and_dollar.js +++ b/test/test.at_and_dollar.js @@ -1,14 +1,17 @@ -var JSONPath = require('../'), - testCase = require('nodeunit').testCase +/*global require, module*/ +/*jslint vars:true*/ +(function () {'use strict'; + +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; var t1 = { simpleString: "simpleString", "@" : "@asPropertyName", - "$" : "$asPropertyName", "a$a": "$inPropertyName", "$": { - "@": "withboth", + "@": "withboth" }, a: { b: { @@ -25,12 +28,12 @@ module.exports = testCase({ 'test undefined, null': function(test) { // ============================================================================ test.expect(6); - test.strictEqual(null, JSONPath({json: {a: null}, path: '$.a', wrap: false})); - test.strictEqual(undefined, JSONPath({json: undefined, path: 'foo'})); - test.strictEqual(undefined, JSONPath({json: null, path: 'foo'})); - test.strictEqual(undefined, JSONPath({json: {}, path: 'foo'})[0]); - test.strictEqual(undefined, JSONPath({json: { a: 'b' }, path: 'foo'})[0]); - test.strictEqual(undefined, JSONPath({json: { a: 'b' }, path: 'foo'})[100]); + test.strictEqual(null, jsonpath({json: {a: null}, path: '$.a', wrap: false})); + test.strictEqual(undefined, jsonpath({json: undefined, path: 'foo'})); + test.strictEqual(undefined, jsonpath({json: null, path: 'foo'})); + test.strictEqual(undefined, jsonpath({json: {}, path: 'foo'})[0]); + test.strictEqual(undefined, jsonpath({json: { a: 'b' }, path: 'foo'})[0]); + test.strictEqual(undefined, jsonpath({json: { a: 'b' }, path: 'foo'})[100]); test.done(); }, @@ -39,17 +42,17 @@ module.exports = testCase({ 'test $ and @': function(test) { // ============================================================================ test.expect(7); - test.equal(t1['$'], JSONPath({json: t1, path: '\$'})[0]); - test.equal(t1['$'], JSONPath({json: t1, path: '$'})[0]); - test.equal(t1['a$a'], JSONPath({json: t1, path: 'a$a'})[0]); - test.equal(t1['@'], JSONPath({json: t1, path: '\@'})[0]); - test.equal(t1['@'], JSONPath({json: t1, path: '@'})[0]); - test.equal(t1['$']['@'], JSONPath({json: t1, path: '$.$.@'})[0]); - test.equal(undefined, JSONPath({json: t1, path: '\@'})[1]); + test.equal(t1.$, jsonpath({json: t1, path: '\\$'})[0]); + test.equal(t1.$, jsonpath({json: t1, path: '$'})[0]); + test.equal(t1.a$a, jsonpath({json: t1, path: 'a$a'})[0]); + test.equal(t1['@'], jsonpath({json: t1, path: '\\@'})[0]); + test.equal(t1['@'], jsonpath({json: t1, path: '@'})[0]); + test.equal(t1.$['@'], jsonpath({json: t1, path: '$.$.@'})[0]); + test.equal(undefined, jsonpath({json: t1, path: '\\@'})[1]); test.done(); } }); - +}()); diff --git a/test/test.eval.js b/test/test.eval.js index 98b3459..0da7b78 100644 --- a/test/test.eval.js +++ b/test/test.eval.js @@ -1,5 +1,9 @@ -var JSONPath = require('../'), - testCase = require('nodeunit').testCase +/*global require, module*/ +/*jslint vars:true*/ +(function () {'use strict'; + +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; var json = { "store": { @@ -30,16 +34,18 @@ module.exports = testCase({ var expected = json.store.books[0]; var selector = '$..[?(' + 'var sum = @.price && @.price[0]+@.price[1];' - + 'sum > 20;)]' - var result = JSONPath({json: json, path: selector, wrap: false}); + + 'sum > 20;)]'; + var result = jsonpath({json: json, path: selector, wrap: false}); test.deepEqual(expected, result); test.done(); }, 'accessing current path': function (test) { var expected = json.store.books[1]; - var result = JSONPath({json: json, path: "$..[?(@path==\"$['store']['books'][1]\")]", wrap: false}); + var result = jsonpath({json: json, path: "$..[?(@path==\"$['store']['books'][1]\")]", wrap: false}); test.deepEqual(expected, result); test.done(); } }); + +}()); diff --git a/test/test.examples.js b/test/test.examples.js index a28e3bb..a9f941c 100644 --- a/test/test.examples.js +++ b/test/test.examples.js @@ -1,7 +1,11 @@ -var JSONPath = require('../'), - testCase = require('nodeunit').testCase +/*global require, module*/ +/*jslint vars:true*/ +(function () {'use strict'; -// tests based on examples at http://goessner.net/articles/JsonPath/ +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; + +// tests based on examples at http://goessner.net/articles/jsonpath/ var json = {"store": { "book": [ @@ -44,7 +48,7 @@ module.exports = testCase({ test.expect(1); var books = json.store.book; var expected = [books[0].author, books[1].author, books[2].author, books[3].author]; - var result = JSONPath({json: json, path: '$.store.book[*].author'}); + var result = jsonpath({json: json, path: '$.store.book[*].author'}); test.deepEqual(expected, result); test.done(); @@ -56,7 +60,7 @@ module.exports = testCase({ test.expect(1); var books = json.store.book; var expected = [books[0].author, books[1].author, books[2].author, books[3].author]; - var result = JSONPath({json: json, path: '$..author'}); + var result = jsonpath({json: json, path: '$..author'}); test.deepEqual(expected, result); test.done(); @@ -67,7 +71,7 @@ module.exports = testCase({ // ============================================================================ test.expect(1); var expected = [json.store.book, json.store.bicycle]; - var result = JSONPath({json: json, path: '$.store.*'}); + var result = jsonpath({json: json, path: '$.store.*'}); test.deepEqual(expected, result); test.done(); @@ -79,7 +83,7 @@ module.exports = testCase({ test.expect(1); var books = json.store.book; var expected = [books[0].price, books[1].price, books[2].price, books[3].price, json.store.bicycle.price]; - var result = JSONPath({json: json, path: '$.store..price'}); + var result = jsonpath({json: json, path: '$.store..price'}); test.deepEqual(expected, result); test.done(); @@ -91,7 +95,7 @@ module.exports = testCase({ test.expect(1); var books = json.store.book; var expected = [books[2]]; - var result = JSONPath({json: json, path: '$..book[2]'}); + var result = jsonpath({json: json, path: '$..book[2]'}); test.deepEqual(expected, result); test.done(); @@ -103,10 +107,10 @@ module.exports = testCase({ test.expect(2); var books = json.store.book; var expected = [books[3]]; - var result = JSONPath({json: json, path: '$..book[(@.length-1)]'}); + var result = jsonpath({json: json, path: '$..book[(@.length-1)]'}); test.deepEqual(expected, result); - result = JSONPath({json: json, path: '$..book[-1:]'}); + result = jsonpath({json: json, path: '$..book[-1:]'}); test.deepEqual(expected, result); test.done(); @@ -118,10 +122,10 @@ module.exports = testCase({ test.expect(2); var books = json.store.book; var expected = [books[0], books[1]]; - var result = JSONPath({json: json, path: '$..book[0,1]'}); + var result = jsonpath({json: json, path: '$..book[0,1]'}); test.deepEqual(expected, result); - result = JSONPath({json: json, path: '$..book[:2]'}); + result = jsonpath({json: json, path: '$..book[:2]'}); test.deepEqual(expected, result); test.done(); @@ -133,7 +137,7 @@ module.exports = testCase({ test.expect(1); var books = json.store.book; var expected = [books[2], books[3]]; - var result = JSONPath({json: json, path: '$..book[?(@.isbn)]'}); + var result = jsonpath({json: json, path: '$..book[?(@.isbn)]'}); test.deepEqual(expected, result); test.done(); @@ -145,7 +149,7 @@ module.exports = testCase({ test.expect(1); var books = json.store.book; var expected = [books[0], books[2]]; - var result = JSONPath({json: json, path: '$..book[?(@.price<10)]'}); + var result = jsonpath({json: json, path: '$..book[?(@.price<10)]'}); test.deepEqual(expected, result); test.done(); @@ -158,20 +162,20 @@ module.exports = testCase({ var expected = [ json.store, json.store.book, - json.store.bicycle, + json.store.bicycle ]; json.store.book.forEach(function(book) { expected.push(book); }); - json.store.book.forEach(function(book) { Object.keys(book).forEach(function(p) { expected.push(book[p]); })}); + json.store.book.forEach(function(book) { Object.keys(book).forEach(function(p) { expected.push(book[p]); });}); expected.push(json.store.bicycle.color); expected.push(json.store.bicycle.price); - var result = JSONPath({json: json, path: '$..*'}); + var result = jsonpath({json: json, path: '$..*'}); test.deepEqual(expected, result); test.done(); } - - }); + +}()); diff --git a/test/test.intermixed.arr.js b/test/test.intermixed.arr.js index 7a9eaa0..6bb4d46 100644 --- a/test/test.intermixed.arr.js +++ b/test/test.intermixed.arr.js @@ -1,7 +1,11 @@ -var JSONPath = require('../'), - testCase = require('nodeunit').testCase +/*global require, module*/ +/*jslint vars:true*/ +(function () {'use strict'; -// tests based on examples at http://goessner.net/articles/JsonPath/ +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; + +// tests based on examples at http://goessner.net/articles/jsonpath/ var json = {"store":{ "book":[ @@ -45,9 +49,11 @@ module.exports = testCase({ var books = json.store.book; var expected = [books[1].price, books[2].price, books[3].price, json.store.bicycle.price]; expected = books[0].price.concat(expected); - var result = JSONPath({json: json, path: '$.store..price', flatten: true}); + var result = jsonpath({json: json, path: '$.store..price', flatten: true}); test.deepEqual(expected, result); test.done(); } }); + +}()); diff --git a/test/test.parent-selector.js b/test/test.parent-selector.js index 6f9c422..1aff384 100644 --- a/test/test.parent-selector.js +++ b/test/test.parent-selector.js @@ -1,5 +1,9 @@ -var JSONPath = require('../'), - testCase = require('nodeunit').testCase +/*global require, module*/ +/*jslint vars:true*/ +(function () {'use strict'; + +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; var json = { "name": "root", @@ -17,7 +21,7 @@ module.exports = testCase({ 'simple parent selection': function(test) { // ============================================================================ test.expect(1); - var result = JSONPath({json: json, path: '$.children[0]^', flatten: true}); + var result = jsonpath({json: json, path: '$.children[0]^', flatten: true}); test.deepEqual(json.children, result); test.done(); }, @@ -27,7 +31,7 @@ module.exports = testCase({ // ============================================================================ test.expect(1); var expected = [json.children,json.children]; - var result = JSONPath({json: json, path: '$.children[1:3]^'}); + var result = jsonpath({json: json, path: '$.children[1:3]^'}); test.deepEqual(expected, result); test.done(); }, @@ -37,7 +41,7 @@ module.exports = testCase({ // ============================================================================ test.expect(1); var expected = [{"name": "child3_2"}]; - var result = JSONPath({json: json, path: '$..[?(@.name && @.name.match(/3_1$/))]^[?(@.name.match(/_2$/))]'}); + var result = jsonpath({json: json, path: '$..[?(@.name && @.name.match(/3_1$/))]^[?(@.name.match(/_2$/))]'}); test.deepEqual(expected, result); test.done(); }, @@ -47,7 +51,7 @@ module.exports = testCase({ // ============================================================================ test.expect(1); var expected = json.children[0].children; - var result = JSONPath({json: json, path: '$..[?(@.name && @.name.match(/1_1$/))].name^^', flatten: true}); + var result = jsonpath({json: json, path: '$..[?(@.name && @.name.match(/1_1$/))].name^^', flatten: true}); test.deepEqual(expected, result); test.done(); }, @@ -56,9 +60,11 @@ module.exports = testCase({ 'no such parent': function(test) { // ============================================================================ test.expect(1); - var result = JSONPath({json: json, path: 'name^^'}); + var result = jsonpath({json: json, path: 'name^^'}); test.deepEqual([], result); test.done(); } }); + +}()); diff --git a/test/test.properties.js b/test/test.properties.js index 387e883..a793f26 100644 --- a/test/test.properties.js +++ b/test/test.properties.js @@ -1,5 +1,9 @@ -var JSONPath = require('../'), - testCase = require('nodeunit').testCase +/*global require, module*/ +/*jslint vars:true*/ +(function () {'use strict'; + +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; var json = { @@ -20,9 +24,11 @@ module.exports = testCase({ // ============================================================================ test.expect(1); var expected = {"test7": "value"}; - var result = JSONPath({json: json, path: "$.test1.test2['test3.test4.test5']", wrap: false}); + var result = jsonpath({json: json, path: "$.test1.test2['test3.test4.test5']", wrap: false}); test.deepEqual(expected, result); test.done(); } }); + +}()); From 48a0c5bfc3d2a5db34eec16c4f02201e01304dff Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Dec 2014 17:06:05 -0700 Subject: [PATCH 48/68] Remove backslashed tests (a single backslash serves no purpose as it is ignored in JS, and a double one that I tried in a recent commit returns no results as expected since backslashes have no special purpose in JSONPath) --- test/test.at_and_dollar.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/test.at_and_dollar.js b/test/test.at_and_dollar.js index b84955c..3b3572c 100644 --- a/test/test.at_and_dollar.js +++ b/test/test.at_and_dollar.js @@ -41,11 +41,9 @@ module.exports = testCase({ // ============================================================================ 'test $ and @': function(test) { // ============================================================================ - test.expect(7); - test.equal(t1.$, jsonpath({json: t1, path: '\\$'})[0]); + test.expect(5); test.equal(t1.$, jsonpath({json: t1, path: '$'})[0]); test.equal(t1.a$a, jsonpath({json: t1, path: 'a$a'})[0]); - test.equal(t1['@'], jsonpath({json: t1, path: '\\@'})[0]); test.equal(t1['@'], jsonpath({json: t1, path: '@'})[0]); test.equal(t1.$['@'], jsonpath({json: t1, path: '$.$.@'})[0]); test.equal(undefined, jsonpath({json: t1, path: '\\@'})[1]); From e1674c9ea7498721390c1e682ec6b39a85f1c2df Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Dec 2014 17:21:22 -0700 Subject: [PATCH 49/68] Support single result parent and parentProperty --- lib/jsonpath.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index b5a2d36..d77a7de 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -87,7 +87,7 @@ JSONPath.prototype.evaluate = function (obj, expr) { return result.reduce(function (result, ea) { var valOrPath; switch (self.resultType) { - case 'value': + case 'value': case 'parent': case 'parentProperty': valOrPath = ea[self.resultType]; break; case 'path': From 46509dff51670edff8defa4cc73762171da72109 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Dec 2014 17:34:40 -0700 Subject: [PATCH 50/68] Add @path index test --- test/test.custom-properties.js | 29 +++++++++++++++++++++++++++++ test/test.html | 1 + 2 files changed, 30 insertions(+) create mode 100644 test/test.custom-properties.js diff --git a/test/test.custom-properties.js b/test/test.custom-properties.js new file mode 100644 index 0000000..848ff75 --- /dev/null +++ b/test/test.custom-properties.js @@ -0,0 +1,29 @@ +/*global require, module*/ +/*jslint vars:true*/ +(function () {'use strict'; + +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; + + + +var t1 = { + b: {true: 'abc', false: 'def'}, + c: {true: 'qrs', false: 'tuv'} +}; + + +module.exports = testCase({ + + // ============================================================================ + '@path for index': function(test) { + // ============================================================================ + test.expect(1); + var result = jsonpath({json: t1, path: '$.*[(@path === "$[\'b\']")]', wrap: false}); + test.deepEqual(['abc', 'tuv'], result); + test.done(); + } + +}); + +}()); diff --git a/test/test.html b/test/test.html index 0cc3a56..0f4bfe6 100644 --- a/test/test.html +++ b/test/test.html @@ -64,6 +64,7 @@

JSONPath Tests

loadJS('test.parent-selector.js'); loadJS('test.all.js'); loadJS('test.properties.js'); + loadJS('test.custom-properties.js'); nodeunit.run(suites); From 458641cfc0a50e743d43787816e12af3d000de3c Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Dec 2014 17:46:20 -0700 Subject: [PATCH 51/68] Correct my example in docs for commas and add test --- README.md | 2 +- test/test.examples.js | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 788d23d..17d1881 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ XPath | JSONPath | Result //book[3] | $..book[2] | the third book | //book[last()] | $..book[(@.length-1)]
$..book[-1:] | the last book in order.| //book[position()<3]| $..book[0,1]
$..book[:2]| the first two books | -//book/*[self::category\|self::author] or //book/(category,author) in XPath 2.0| $..book[category,author]| the categories and authors of all books | +//book[1]/*[self::category\|self::author] or //book[1]/(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 isbn number | //book[price<10] | $..book[?(@.price<10)] | filter all books cheapier than 10 | //* | $..* | all Elements in XML document. All members of JSON structure. | diff --git a/test/test.examples.js b/test/test.examples.js index a9f941c..8916f0b 100644 --- a/test/test.examples.js +++ b/test/test.examples.js @@ -131,6 +131,15 @@ module.exports = testCase({ test.done(); }, + 'categories and authors of all books': function(test) { + test.expect(1); + var expected = ['reference', 'Nigel Rees']; + var result = jsonpath({json: json, path: '$..book[0][category,author]'}); + test.deepEqual(expected, result); + + test.done(); + }, + // ============================================================================ 'filter all properties if sub property exists, of entire tree': function(test) { // ============================================================================ From db471ccbada5fca67794fc54d6796cf4b5832edf Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Dec 2014 18:58:27 -0700 Subject: [PATCH 52/68] Spacing --- test/test.parent-selector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.parent-selector.js b/test/test.parent-selector.js index 1aff384..1564378 100644 --- a/test/test.parent-selector.js +++ b/test/test.parent-selector.js @@ -30,7 +30,7 @@ module.exports = testCase({ 'parent selection with multiple matches': function(test) { // ============================================================================ test.expect(1); - var expected = [json.children,json.children]; + var expected = [json.children, json.children]; var result = jsonpath({json: json, path: '$.children[1:3]^'}); test.deepEqual(expected, result); test.done(); From 985772376283a3a8220dce626c87c2a99deee5f9 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Dec 2014 19:03:40 -0700 Subject: [PATCH 53/68] Initial upper-case notes; distinguish in tests and docs between retrieval of all elements and retrieval of all elements beneath root; in readme, move operators and properties into respective groups --- README.md | 27 ++++++++++++++------------- test/test.examples.js | 25 ++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 17d1881..4f533f4 100644 --- a/README.md +++ b/README.md @@ -95,20 +95,21 @@ Given the following JSON, taken from http://goessner.net/articles/JsonPath/ : 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 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)]
$..book[-1:] | the last book in order.| -//book[position()<3]| $..book[0,1]
$..book[:2]| the first two books | -//book[1]/*[self::category\|self::author] or //book[1]/(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 isbn number | -//book[price<10] | $..book[?(@.price<10)] | filter all books cheapier than 10 | -//* | $..* | all Elements in XML document. All members of JSON structure. | -//*[price>19]/.. | $..[?(@.price>19)]^ | categories with things more expensive than 19 | Parent (caret) not present in the original spec -/store/book[not(. is /store/book[1])] | $.store.book[?(@path !== "$[\'store\'][\'book\'][0]")] | All books besides that at the path pointing to the first | @path not present in the original spec +/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)]
$..book[-1:] | The last book in order.| +//book[position()<3]| $..book[0,1]
$..book[:2]| The first two books | +//book[1]/*[self::category\|self::author] or //book[1]/(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 isbn number | +//book[price<10] | $..book[?(@.price<10)] | Filter all books cheapier than 10 | +//* | $.. | All Elements in XML document. All members of JSON structure. | +//*/* | $..* | All Elements beneath root in XML document. All members of JSON structure beneath the root. | +//*[price>19]/.. | $..[?(@.price>19)]^ | Categories with things more expensive than 19 | Parent (caret) not present in the original spec /store/*/name() in XPath 2.0 | $.store.*~ | The property names of the store sub-object ("book" and "bicycle") | Property name (tilde) is not present in the original spec +/store/book[not(. is /store/book[1])] | $.store.book[?(@path !== "$[\'store\'][\'book\'][0]")] | All books besides that at the path pointing to the first | @path not present in the original spec //category[parent::*/author = "J. R. R. Tolkien"] | $..category[?(@parent.author === "J. R. R. Tolkien")] | Grabs all categories whose parent's author (i.e., the author sibling to the category property) is J. R. R. Tolkien | @parent is not present in the original spec //book/*[name() != 'category'] | $..book.*[?(@property !== "category")] | Grabs the children of "book" except for "category" ones | @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 diff --git a/test/test.examples.js b/test/test.examples.js index 8916f0b..9b16d26 100644 --- a/test/test.examples.js +++ b/test/test.examples.js @@ -164,10 +164,30 @@ module.exports = testCase({ test.done(); }, - // ============================================================================ 'all properties of a JSON structure': function(test) { // ============================================================================ - // test.expect(1); + test.expect(1); + var expected = [ + json, + json.store, + json.store.book, + json.store.bicycle + ]; + json.store.book.forEach(function(book) { expected.push(book); }); + json.store.book.forEach(function(book) { Object.keys(book).forEach(function(p) { expected.push(book[p]); });}); + expected.push(json.store.bicycle.color); + expected.push(json.store.bicycle.price); + + var result = jsonpath({json: json, path: '$..'}); + test.deepEqual(expected, result); + + test.done(); + }, + + // ============================================================================ + 'all properties of a JSON structure beneath the root': function(test) { + // ============================================================================ + test.expect(1); var expected = [ json.store, json.store.book, @@ -184,7 +204,6 @@ module.exports = testCase({ test.done(); } - }); }()); From e8804671b0663e35b57ce19275f60f568d198f26 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Fri, 12 Dec 2014 20:03:07 -0700 Subject: [PATCH 54/68] Fix MD --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f533f4..f1fbdda 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ XPath | JSONPath | Result //book[isbn] | $..book[?(@.isbn)] | Filter all books with isbn number | //book[price<10] | $..book[?(@.price<10)] | Filter all books cheapier than 10 | //* | $.. | All Elements in XML document. All members of JSON structure. | -//*/* | $..* | All Elements beneath root in XML document. All members of JSON structure beneath the root. | +//\*/\* | $..* | All Elements beneath root in XML document. All members of JSON structure beneath the root. | //*[price>19]/.. | $..[?(@.price>19)]^ | Categories with things more expensive than 19 | Parent (caret) not present in the original spec /store/*/name() in XPath 2.0 | $.store.*~ | The property names of the store sub-object ("book" and "bicycle") | Property name (tilde) is not present in the original spec /store/book[not(. is /store/book[1])] | $.store.book[?(@path !== "$[\'store\'][\'book\'][0]")] | All books besides that at the path pointing to the first | @path not present in the original spec From 741b09a721ad819b66c9d449b1145644e3e8fe6d Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sun, 14 Dec 2014 14:16:00 -0700 Subject: [PATCH 55/68] Remove redundant Changes item; clarify $.. role and fix its test. Add tests from examples for custom operators and properties. Fix docs for parentProperty; clarify (e.g., parent operator and recursive descent operators) in README; add our specific XML representation with note that the XPath examples do not generally distinguish between elements and text content; Fix regression on XPath doc example with comma selector --- CHANGES.md | 1 - README.md | 62 ++++++++++++++++++++++++++------ lib/jsonpath.js | 5 +-- test/test.examples.js | 82 +++++++++++++++++++++++++++++++++++++++---- 4 files changed, 129 insertions(+), 21 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 9680c63..5539015 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,5 @@ ## Dec 12, 2014 * Offer new class-based API and object-based arguments (with option to run new queries without resupplying config) -* Fix bug preventing unwrapped null from being a possible return value * 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) * Support "parent" and "parentProperty" for resultType along with "all" (which also includes "path" and "value" together) * Support "." within properties diff --git a/README.md b/README.md index f1fbdda..86d9be6 100644 --- a/README.md +++ b/README.md @@ -92,27 +92,67 @@ Given the following JSON, taken from http://goessner.net/articles/JsonPath/ : } ``` +and the following XML representation: + +```xml + + + reference + Nigel Rees + Sayings of the Century + 8.95 + + + fiction + Evelyn Waugh + Sword of Honour + 12.99 + + + fiction + Herman Melville + Moby Dick + 0-553-21311-3 + 8.99 + + + fiction + J. R. R. Tolkien + The Lord of the Rings + 0-395-19395-8 + 22.99 + + + red + 19.95 + + +``` + +Please note that the XPath examples below do not distinguish between +retrieving elements and their text content (except for the example +indicating retrieval of all items). 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 some books and a red bicycle.| +/store/* | $.store.* | All things in store, which are its 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)]
$..book[-1:] | The last book in order.| //book[position()<3]| $..book[0,1]
$..book[:2]| The first two books | -//book[1]/*[self::category\|self::author] or //book[1]/(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 isbn number | -//book[price<10] | $..book[?(@.price<10)] | Filter all books cheapier than 10 | -//* | $.. | All Elements in XML document. All members of JSON structure. | -//\*/\* | $..* | All Elements beneath root in XML document. All members of JSON structure beneath the root. | -//*[price>19]/.. | $..[?(@.price>19)]^ | Categories with things more expensive than 19 | Parent (caret) not present in the original spec +//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 | +//\*/\*|//\*/\*/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 present in the original spec /store/*/name() in XPath 2.0 | $.store.*~ | The property names of the store sub-object ("book" and "bicycle") | Property name (tilde) is not present in the original spec -/store/book[not(. is /store/book[1])] | $.store.book[?(@path !== "$[\'store\'][\'book\'][0]")] | All books besides that at the path pointing to the first | @path not present in the original spec -//category[parent::*/author = "J. R. R. Tolkien"] | $..category[?(@parent.author === "J. R. R. Tolkien")] | Grabs all categories whose parent's author (i.e., the author sibling to the category property) is J. R. R. Tolkien | @parent is not present in the original spec -//book/*[name() != 'category'] | $..book.*[?(@property !== "category")] | Grabs the children of "book" except for "category" ones | @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 +/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 +//category[parent::*/author = "J. R. R. Tolkien"] | $..category[?(@parent.author === "J. R. R. Tolkien")] | Grabs all categories whose parent's author (i.e., the author sibling to the category property) is J. R. R. Tolkien (i.e., "fiction") | @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 +/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 Any additional variables supplied as properties on the optional "sandbox" object option are also available to (parenthetical-based) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index d77a7de..e6dda1c 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -167,9 +167,10 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName) { addRet(self._trace(unshift(m, x), v, p, par, pr)); }); } - else if (loc === '..') { // all descendent properties + else if (loc === '..') { // all descendent parent properties addRet(this._trace(x, val, path, parent, parentPropName)); // Check remaining expression with val's immediate children - this._walk(loc, x, val, path, parent, parentPropName, function (m, l, x, v, p) { + this._walk(loc, x, val, path, parent, parentPropName, function (m, l, x, v, p, par, pr) { + // 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)); } diff --git a/test/test.examples.js b/test/test.examples.js index 9b16d26..e7424c5 100644 --- a/test/test.examples.js +++ b/test/test.examples.js @@ -164,19 +164,16 @@ module.exports = testCase({ test.done(); }, - 'all properties of a JSON structure': function(test) { + 'all parent components of a JSON structure': function(test) { // ============================================================================ test.expect(1); var expected = [ json, json.store, - json.store.book, - json.store.bicycle + json.store.book ]; json.store.book.forEach(function(book) { expected.push(book); }); - json.store.book.forEach(function(book) { Object.keys(book).forEach(function(p) { expected.push(book[p]); });}); - expected.push(json.store.bicycle.color); - expected.push(json.store.bicycle.price); + expected.push(json.store.bicycle); var result = jsonpath({json: json, path: '$..'}); test.deepEqual(expected, result); @@ -185,7 +182,7 @@ module.exports = testCase({ }, // ============================================================================ - 'all properties of a JSON structure beneath the root': function(test) { + 'all properties of a JSON structure (beneath the root)': function(test) { // ============================================================================ test.expect(1); var expected = [ @@ -202,6 +199,77 @@ module.exports = testCase({ test.deepEqual(expected, result); test.done(); + }, + // ============================================================================ + 'Custom operator: parent (caret)': function(test) { + // ============================================================================ + test.expect(1); + var expected = [json.store, json.store.book]; + var result = jsonpath({json: json, path: '$..[?(@.price>19)]^'}); + test.deepEqual(expected, result); + + test.done(); + + }, + // ============================================================================ + 'Custom operator: property name (tilde)': function(test) { + // ============================================================================ + test.expect(1); + var expected = ['book', 'bicycle']; + var result = jsonpath({json: json, path: '$.store.*~'}); + test.deepEqual(expected, result); + + test.done(); + + }, + // ============================================================================ + 'Custom property @path': function(test) { + // ============================================================================ + test.expect(1); + var expected = json.store.book.slice(1); + var result = jsonpath({json: json, path: '$.store.book[?(@path !== "$[\'store\'][\'book\'][0]")]'}); + test.deepEqual(expected, result); + + test.done(); + + }, + // ============================================================================ + 'Custom property: @parent': function(test) { + // ============================================================================ + test.expect(1); + var expected = []; + var result = jsonpath({json: json, path: '$..category[?(@parent.author === "J. R. R. Tolkien")]'}); + test.deepEqual(expected, result); + + test.done(); + + }, + // ============================================================================ + 'Custom property: @property': function(test) { + // ============================================================================ + test.expect(1); + var expected = json.store.book.reduce(function (arr, book) { + arr.push(book.author, book.title); + if (book.isbn) {arr.push(book.isbn);} + arr.push(book.price); + return arr; + }, []); + var result = jsonpath({json: json, path: '$..book.*[?(@property !== "category")]'}); + test.deepEqual(expected, result); + + test.done(); + + }, + // ============================================================================ + 'Custom property: @parentProperty': function(test) { + // ============================================================================ + test.expect(1); + var expected = [json.store.bicycle.color, json.store.bicycle.price]; + var result = jsonpath({json: json, path: '$.store.*[?(@parentProperty !== "book")]'}); + test.deepEqual(expected, result); + + test.done(); + } }); From de4821859aac87baac41442acbc10789d3c5214c Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Sun, 14 Dec 2014 14:21:45 -0700 Subject: [PATCH 56/68] Fix MD --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 86d9be6..0059230 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ XPath | JSONPath | Result //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 | -//\*/\*|//\*/\*/text() | $..* | All Elements (and text) beneath root in an XML document. All members of a JSON structure beneath the root. | +//\*/\*\|//\*/\*/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 present in the original spec /store/*/name() in XPath 2.0 | $.store.*~ | The property names of the store sub-object ("book" and "bicycle") | Property name (tilde) is not present in the original spec From 5957acaef590ef648b317849a5a940bb8865588d Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 15 Dec 2014 01:16:41 -0700 Subject: [PATCH 57/68] Add test examples with property/parentProperty referencing an array index --- test/test.examples.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/test/test.examples.js b/test/test.examples.js index e7424c5..86759e1 100644 --- a/test/test.examples.js +++ b/test/test.examples.js @@ -247,7 +247,7 @@ module.exports = testCase({ // ============================================================================ 'Custom property: @property': function(test) { // ============================================================================ - test.expect(1); + test.expect(2); var expected = json.store.book.reduce(function (arr, book) { arr.push(book.author, book.title); if (book.isbn) {arr.push(book.isbn);} @@ -257,17 +257,30 @@ module.exports = testCase({ var result = jsonpath({json: json, path: '$..book.*[?(@property !== "category")]'}); test.deepEqual(expected, result); + expected = json.store.book.slice(1); + result = jsonpath({json: json, path: '$..book[?(@property !== 0)]'}); + test.deepEqual(expected, result); + test.done(); }, // ============================================================================ 'Custom property: @parentProperty': function(test) { // ============================================================================ - test.expect(1); + test.expect(2); var expected = [json.store.bicycle.color, json.store.bicycle.price]; var result = jsonpath({json: json, path: '$.store.*[?(@parentProperty !== "book")]'}); test.deepEqual(expected, result); + expected = json.store.book.slice(1).reduce(function (result, book) { + return result.concat(Object.keys(book).reduce(function (result, prop) { + result.push(book[prop]); + return result; + }, [])); + }, []); + result = jsonpath({json: json, path: '$..book.*[?(@parentProperty !== 0)]'}); + test.deepEqual(expected, result); + test.done(); } From 5554f67d977b28dd5941d700d08c9ac922f0913c Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 15 Dec 2014 01:21:58 -0700 Subject: [PATCH 58/68] Clarify potential sources of confusion for XPath users (including issue #34) --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 0059230..fb5f443 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,17 @@ 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. + # Development Running the tests on node: `npm test`. For in-browser tests: From e20900453e07d80dfe1ea7d3ffcd9f84bef76be3 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 15 Dec 2014 01:27:05 -0700 Subject: [PATCH 59/68] Provide preventEval option --- README.md | 1 + lib/jsonpath.js | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index fb5f443..f12b253 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ options (the first argument) include: - ***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: An empty object **) - 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. There is also now a class property, on JSONPath.cache which exposes the cache for those who wish to preserve and reuse it for optimization purposes. diff --git a/lib/jsonpath.js b/lib/jsonpath.js index e6dda1c..3678231 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -55,6 +55,7 @@ function JSONPath (opts, obj, expr) { this.flatten = opts.flatten || false; this.wrap = opts.hasOwnProperty('wrap') ? opts.wrap : true; this.sandbox = opts.sandbox || {}; + this.preventEval = opts.preventEval || false; if (opts.autostart !== false) { var ret = this.evaluate((objArgs ? opts.json : obj), (objArgs ? opts.path : expr)); @@ -177,6 +178,9 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName) { }); } else if (loc[0] === '(') { // [(expr)] (dynamic property/index) + if (this.preventEval) { + 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)); } @@ -193,6 +197,9 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName) { return {path: push(path, loc), value: parentPropName, parent: parent, parentProperty: null}; } else if (loc.indexOf('?(') === 0) { // [?(expr)] (filtering) + if (this.preventEval) { + throw "Eval [?(expr)] prevented in JSONPath expression."; + } this._walk(loc, x, val, path, parent, parentPropName, function (m, l, x, v, p, par, pr) { if (self._eval(l.replace(/^\?\((.*?)\)$/, '$1'), v[m], m, p, par, pr)) { addRet(self._trace(unshift(m, x), v, p, par, pr)); From bfb090558d622855c1cc4426ab75245b99a9012d Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 15 Dec 2014 01:36:26 -0700 Subject: [PATCH 60/68] Fix parent test --- test/test.examples.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test.examples.js b/test/test.examples.js index 86759e1..37d3f3b 100644 --- a/test/test.examples.js +++ b/test/test.examples.js @@ -237,8 +237,8 @@ module.exports = testCase({ 'Custom property: @parent': function(test) { // ============================================================================ test.expect(1); - var expected = []; - var result = jsonpath({json: json, path: '$..category[?(@parent.author === "J. R. R. Tolkien")]'}); + var expected = ['reference', 'fiction', 'fiction', 'fiction']; + var result = jsonpath({json: json, path: '$..book[?(@parent.bicycle && @parent.bicycle.color === "red")].category'}); test.deepEqual(expected, result); test.done(); From 2d825582a820e9769308c9a56a8c4881bc153f62 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 15 Dec 2014 02:12:26 -0700 Subject: [PATCH 61/68] Put tests in README order; fix doc examples (with tested XPath) --- README.md | 12 +++++++----- test/test.examples.js | 27 ++++++++++++++------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index f12b253..8c788bd 100644 --- a/README.md +++ b/README.md @@ -131,16 +131,16 @@ and the following XML representation: ``` Please note that the XPath examples below do not distinguish between -retrieving elements and their text content (except for the example -indicating retrieval of all items). +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 and a red bicycle.| +/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[3] | $..book[2] | The third book (book object) | //book[last()] | $..book[(@.length-1)]
$..book[-1:] | The last book in order.| //book[position()<3]| $..book[0,1]
$..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 | @@ -151,9 +151,11 @@ XPath | JSONPath | Result //*[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 present in the original spec /store/*/name() in XPath 2.0 | $.store.*~ | The property names of the store sub-object ("book" and "bicycle") | 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 -//category[parent::*/author = "J. R. R. Tolkien"] | $..category[?(@parent.author === "J. R. R. Tolkien")] | Grabs all categories whose parent's author (i.e., the author sibling to the category property) is J. R. R. Tolkien (i.e., "fiction") | @parent is 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 Any additional variables supplied as properties on the optional "sandbox" object option are also available to (parenthetical-based) diff --git a/test/test.examples.js b/test/test.examples.js index 37d3f3b..04d6ada 100644 --- a/test/test.examples.js +++ b/test/test.examples.js @@ -164,42 +164,43 @@ module.exports = testCase({ test.done(); }, - 'all parent components of a JSON structure': function(test) { + // ============================================================================ + 'all properties of a JSON structure (beneath the root)': function(test) { // ============================================================================ test.expect(1); var expected = [ - json, json.store, - json.store.book + json.store.book, + json.store.bicycle ]; json.store.book.forEach(function(book) { expected.push(book); }); - expected.push(json.store.bicycle); + json.store.book.forEach(function(book) { Object.keys(book).forEach(function(p) { expected.push(book[p]); });}); + expected.push(json.store.bicycle.color); + expected.push(json.store.bicycle.price); - var result = jsonpath({json: json, path: '$..'}); + var result = jsonpath({json: json, path: '$..*'}); test.deepEqual(expected, result); test.done(); }, - // ============================================================================ - 'all properties of a JSON structure (beneath the root)': function(test) { + 'all parent components of a JSON structure': function(test) { // ============================================================================ test.expect(1); var expected = [ + json, json.store, - json.store.book, - json.store.bicycle + json.store.book ]; json.store.book.forEach(function(book) { expected.push(book); }); - json.store.book.forEach(function(book) { Object.keys(book).forEach(function(p) { expected.push(book[p]); });}); - expected.push(json.store.bicycle.color); - expected.push(json.store.bicycle.price); + expected.push(json.store.bicycle); - var result = jsonpath({json: json, path: '$..*'}); + var result = jsonpath({json: json, path: '$..'}); test.deepEqual(expected, result); test.done(); }, + // ============================================================================ 'Custom operator: parent (caret)': function(test) { // ============================================================================ From bb160a01b1962685084f3211f02c8377a5c77aa7 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 15 Dec 2014 02:15:18 -0700 Subject: [PATCH 62/68] Update changes --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 5539015..da08dae 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,6 @@ ## Dec 12, 2014 * Offer new class-based API and object-based arguments (with option to run new queries without resupplying config) +* Allow new preventEval=true and autostart=false option * 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) * Support "parent" and "parentProperty" for resultType along with "all" (which also includes "path" and "value" together) * Support "." within properties From 3b4d633af2ca88db7a80ba9d82765737fd858c3d Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 15 Dec 2014 02:17:01 -0700 Subject: [PATCH 63/68] Add name to contributors list --- package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package.json b/package.json index 7fbeacf..27e08f8 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,10 @@ { "name": "Robert Krahn", "email": "robert.krahn@gmail.com" + }, + { + "name": "Brett Zamir", + "email": "brettz9@yahoo.com" } ], "version": "0.11.0", From 4c6459615363ab2c429387b45e074731219db4d7 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 15 Dec 2014 02:41:11 -0700 Subject: [PATCH 64/68] Provide example and test of "@" as a scalar value --- README.md | 3 ++- test/test.examples.js | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c788bd..5556218 100644 --- a/README.md +++ b/README.md @@ -146,10 +146,11 @@ XPath | JSONPath | Result //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 | //\*/\*\|//\*/\*/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 present in the original spec -/store/*/name() in XPath 2.0 | $.store.*~ | The property names of the store sub-object ("book" and "bicycle") | Property name (tilde) is not present 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 diff --git a/test/test.examples.js b/test/test.examples.js index 04d6ada..ede929d 100644 --- a/test/test.examples.js +++ b/test/test.examples.js @@ -164,6 +164,15 @@ module.exports = testCase({ test.done(); }, + // ============================================================================ + '@ as a scalar value': function(test) { + // ============================================================================ + var expected = [json.store.bicycle.price].concat(json.store.book.slice(1).map(function (book) {return book.price;})); + var result = jsonpath({json: json, path: "$..*[?(@property === 'price' && @ !== 8.95)]", wrap: false}); + test.deepEqual(expected, result); + test.done(); + }, + // ============================================================================ 'all properties of a JSON structure (beneath the root)': function(test) { // ============================================================================ From 2980d9eac1291226bda3e0864b113e4f637bddc2 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 15 Dec 2014 13:10:25 -0700 Subject: [PATCH 65/68] Implement callback option to execute as each final result node is obtained --- CHANGES.md | 1 + README.md | 7 ++-- lib/jsonpath.js | 107 +++++++++++++++++++++++++++++------------------- 3 files changed, 71 insertions(+), 44 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index da08dae..fd69427 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ ## Dec 12, 2014 * Offer new class-based API and object-based arguments (with option to run new queries without resupplying config) * Allow new preventEval=true and autostart=false option +* Allow new callback option to allow a callback function to execute as each final result node is obtained * 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) * Support "parent" and "parentProperty" for resultType along with "all" (which also includes "path" and "value" together) * Support "." within properties diff --git a/README.md b/README.md index 5556218..67473b3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ In node.js: ```js var JSONPath = require('JSONPath'); -JSONPath({json: obj, path: path}); +JSONPath({json: obj, path: path, callback: callback}); ``` For browser usage you can directly include `lib/jsonpath.js`, no browserify @@ -21,14 +21,14 @@ magic necessary: ```html ``` An alternative syntax is available as: ```js -JSONPath(options, obj, path); +JSONPath(options, obj, path, callback); ``` The following format is now deprecated: @@ -46,6 +46,7 @@ options (the first argument) include: - ***sandbox*** (**default: An empty object **) - 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. +- ***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). There is also now a class property, on JSONPath.cache which exposes the cache for those who wish to preserve and reuse it for optimization purposes. diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 3678231..a92314e 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -36,10 +36,10 @@ var vm = isNode ? 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, obj, expr) { +function JSONPath (opts, obj, expr, callback) { if (!(this instanceof JSONPath)) { try { - return new JSONPath(opts, obj, expr); + return new JSONPath(opts, obj, expr, callback); } catch (e) { if (!e.avoidNew) { @@ -58,7 +58,7 @@ function JSONPath (opts, obj, expr) { this.preventEval = opts.preventEval || false; if (opts.autostart !== false) { - var ret = this.evaluate((objArgs ? opts.json : obj), (objArgs ? opts.path : expr)); + var ret = this.evaluate((objArgs ? opts.json : obj), (objArgs ? opts.path : expr), (objArgs ? opts.callback : callback)); 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)"}; } @@ -68,7 +68,7 @@ function JSONPath (opts, obj, expr) { // PUBLIC METHODS -JSONPath.prototype.evaluate = function (obj, expr) { +JSONPath.prototype.evaluate = function (obj, expr, callback) { var self = this; this._obj = obj; if (!expr || !obj || allowedResultTypes.indexOf(this.resultType) === -1) { @@ -76,34 +76,32 @@ JSONPath.prototype.evaluate = function (obj, expr) { } var exprList = this._normalize(expr); if (exprList[0] === '$' && exprList.length > 1) {exprList.shift();} - var result = this._trace(exprList, obj, ['$'], null, null); // We could add arguments to let user pass parent and its property name in case it needed to access the parent + var result = this._trace(exprList, obj, ['$'], null, null, callback); // We could add arguments to let user pass parent and its property name in case it needed to access the parent result = result.filter(function (ea) { return ea && !ea.isParentSelector; }); + var resultType = this.resultType; if (!result.length) {return this.wrap ? [] : undefined;} if (result.length === 1 && !this.wrap && !Array.isArray(result[0].value)) { - if (this.resultType === 'all') { + if (resultType === 'all') { return result[0]; } - return result[0][this.resultType]; + return result[0][resultType]; } return result.reduce(function (result, ea) { var valOrPath; - switch (self.resultType) { - case 'value': case 'parent': case 'parentProperty': - valOrPath = ea[self.resultType]; - break; - case 'path': - valOrPath = self._asPath(ea[self.resultType]); - break; + switch (resultType) { case 'all': result.push(ea); return result; + default: + valOrPath = self._getPreferredOutput(ea); + if (self.flatten && Array.isArray(valOrPath)) { + result = result.concat(valOrPath); + } + else { + result.push(valOrPath); + } + return result; } - if (self.flatten && Array.isArray(valOrPath)) { - result = result.concat(valOrPath); - } else { - result.push(valOrPath); - } - return result; }, []); }; @@ -148,10 +146,34 @@ JSONPath.prototype._asPath = function (path) { return p; }; -JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName) { +JSONPath.prototype._getPreferredOutput = function (ea) { + var resultType = this.resultType; + switch (resultType) { + case 'value': case 'parent': case 'parentProperty': + return ea[resultType]; + case 'path': + return this._asPath(ea[resultType]); + } +}; + +JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) { + if (callback) { + var preferredOutput = fullRetObj; + if (this.resultType !== 'all') { + preferredOutput = this._getPreferredOutput(fullRetObj); + } + 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 self = this; - if (!expr.length) {return {path: path, value: val, parent: parent, parentProperty: parentPropName};} + 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); @@ -161,19 +183,19 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName) { 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)); + 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, function (m, l, x, v, p, par, pr) { - addRet(self._trace(unshift(m, x), v, p, par, pr)); + 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)); // Check remaining expression with val's immediate children - this._walk(loc, x, val, path, parent, parentPropName, function (m, l, x, v, p, par, pr) { + 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)); + addRet(self._trace(unshift(l, x), v[m], push(p, m), v, m, cb)); } }); } @@ -182,11 +204,12 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName) { 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)); + 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, @@ -194,53 +217,55 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName) { } : []; } else if (loc === '~') { // property name - return {path: push(path, loc), value: parentPropName, parent: parent, parentProperty: null}; + retObj = {path: push(path, loc), value: parentPropName, parent: parent, parentProperty: null}; + this._handleCallback(retObj, callback, 'property'); + return retObj; } else if (loc.indexOf('?(') === 0) { // [?(expr)] (filtering) if (this.preventEval) { throw "Eval [?(expr)] prevented in JSONPath expression."; } - this._walk(loc, x, val, path, parent, parentPropName, function (m, l, x, v, p, par, pr) { + 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)); + 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)); + addRet(this._trace(unshift(parts[i], x), val, path, parent, parentPropName, callback)); } } 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)); + 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) : 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, f) { +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); + 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); + f(m, loc, expr, val, path, parent, parentPropName, callback); } } } }; -JSONPath.prototype._slice = function (loc, expr, val, path, parent, parentPropName) { +JSONPath.prototype._slice = function (loc, expr, val, path, parent, parentPropName, callback) { if (!Array.isArray(val)) {return;} var i, len = val.length, parts = loc.split(':'), @@ -251,7 +276,7 @@ JSONPath.prototype._slice = function (loc, expr, val, path, parent, parentPropNa 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)); + ret = ret.concat(this._trace(unshift(i, expr), val, path, parent, parentPropName, callback)); } return ret; }; From 68627c6ae1182e25f21c7f03de4288619931e5a3 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 15 Dec 2014 13:16:51 -0700 Subject: [PATCH 66/68] Fix single path result and simplify --- lib/jsonpath.js | 33 +++++++++++---------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index a92314e..6bf95be 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -81,27 +81,17 @@ JSONPath.prototype.evaluate = function (obj, expr, callback) { var resultType = this.resultType; if (!result.length) {return this.wrap ? [] : undefined;} if (result.length === 1 && !this.wrap && !Array.isArray(result[0].value)) { - if (resultType === 'all') { - return result[0]; - } - return result[0][resultType]; + return this._getPreferredOutput(result[0]); } return result.reduce(function (result, ea) { - var valOrPath; - switch (resultType) { - case 'all': - result.push(ea); - return result; - default: - valOrPath = self._getPreferredOutput(ea); - if (self.flatten && Array.isArray(valOrPath)) { - result = result.concat(valOrPath); - } - else { - result.push(valOrPath); - } - return result; + var valOrPath = self._getPreferredOutput(ea); + if (self.flatten && Array.isArray(valOrPath)) { + result = result.concat(valOrPath); } + else { + result.push(valOrPath); + } + return result; }, []); }; @@ -149,6 +139,8 @@ JSONPath.prototype._asPath = function (path) { JSONPath.prototype._getPreferredOutput = function (ea) { var resultType = this.resultType; switch (resultType) { + case 'all': + return ea; case 'value': case 'parent': case 'parentProperty': return ea[resultType]; case 'path': @@ -158,10 +150,7 @@ JSONPath.prototype._getPreferredOutput = function (ea) { JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) { if (callback) { - var preferredOutput = fullRetObj; - if (this.resultType !== 'all') { - preferredOutput = this._getPreferredOutput(fullRetObj); - } + var preferredOutput = this._getPreferredOutput(fullRetObj); callback(preferredOutput, type, fullRetObj); } }; From 3b5bc9b8d247c3d86e4022decd7864e6d5bea04f Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 15 Dec 2014 13:48:27 -0700 Subject: [PATCH 67/68] Unlike issue #20, change "all" to return path as string so consistent with normal result; if desired, we can change to also add pathAsArray as an option. --- lib/jsonpath.js | 2 ++ test/test.all.js | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 6bf95be..a222931 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -140,6 +140,7 @@ JSONPath.prototype._getPreferredOutput = function (ea) { var resultType = this.resultType; switch (resultType) { case 'all': + ea.path = this._asPath(ea.path); return ea; case 'value': case 'parent': case 'parentProperty': return ea[resultType]; @@ -151,6 +152,7 @@ JSONPath.prototype._getPreferredOutput = function (ea) { JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) { if (callback) { var preferredOutput = this._getPreferredOutput(fullRetObj); + fullRetObj.path = this._asPath(fullRetObj.path); callback(preferredOutput, type, fullRetObj); } }; diff --git a/test/test.all.js b/test/test.all.js index 9fb4675..8105623 100644 --- a/test/test.all.js +++ b/test/test.all.js @@ -22,7 +22,7 @@ module.exports = testCase({ // ============================================================================ test.expect(1); var result = jsonpath({json: json, path: '$.children[0]^', resultType: 'all'}); - test.deepEqual([{path: ['$', 'children'], value: json.children, parent: json, parentProperty: 'children'}], result); + test.deepEqual([{path: "$['children']", value: json.children, parent: json, parentProperty: 'children'}], result); test.done(); }, @@ -30,7 +30,7 @@ module.exports = testCase({ 'parent selection with multiple matches, return both path and value': function(test) { // ============================================================================ test.expect(1); - var expectedOne = {path: ['$', 'children'], value: json.children, parent: json, parentProperty: 'children'}; + var expectedOne = {path: "$['children']", value: json.children, parent: json, parentProperty: 'children'}; var expected = [expectedOne, expectedOne]; var result = jsonpath({json: json, path: '$.children[1:3]^', resultType: 'all'}); test.deepEqual(expected, result); @@ -41,7 +41,7 @@ module.exports = testCase({ 'select sibling via parent, return both path and value': function(test) { // ============================================================================ test.expect(1); - var expected = [{path: [ '$', 'children', 2, 'children', 1], value: {name: 'child3_2'}, parent: json.children[2].children, parentProperty: 1}]; + var expected = [{path: "$['children'][2]['children'][1]", value: {name: 'child3_2'}, parent: json.children[2].children, parentProperty: 1}]; var result = jsonpath({json: json, path: '$..[?(@.name && @.name.match(/3_1$/))]^[?(@.name.match(/_2$/))]', resultType: 'all'}); test.deepEqual(expected, result); test.done(); @@ -51,7 +51,7 @@ module.exports = testCase({ 'parent parent parent, return both path and value': function(test) { // ============================================================================ test.expect(1); - var expected = [{path: ['$', 'children', 0, 'children'], value: json.children[0].children, parent: json.children[0], parentProperty: 'children'}]; + var expected = [{path: "$['children'][0]['children']", value: json.children[0].children, parent: json.children[0], parentProperty: 'children'}]; var result = jsonpath({json: json, path: '$..[?(@.name && @.name.match(/1_1$/))].name^^', resultType: 'all'}); test.deepEqual(expected, result); test.done(); From aacc09dcf8565a73c10b6e5e91478492c2cacd8f Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Mon, 15 Dec 2014 13:49:22 -0700 Subject: [PATCH 68/68] Add tests for (single item unwrapped path) return type, and callback --- test/test.callback.js | 64 +++++++++++++++++++++++++++++++++++++++++++ test/test.html | 2 ++ test/test.return.js | 56 +++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 test/test.callback.js create mode 100644 test/test.return.js diff --git a/test/test.callback.js b/test/test.callback.js new file mode 100644 index 0000000..420999d --- /dev/null +++ b/test/test.callback.js @@ -0,0 +1,64 @@ +/*global require, module*/ +/*jslint vars:true*/ +(function () {'use strict'; + +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; + + +var json = {"store": { + "book": [ + { "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + } +}; + + +module.exports = testCase({ + + // ============================================================================ + 'Callback': function (test) { + // ============================================================================ + test.expect(1); + + var expected = ['value', json.store.bicycle, {path: "$['store']['bicycle']", value: json.store.bicycle, parent: json.store, parentProperty: 'bicycle'}]; + var result; + function callback (data, type, fullData) { + if (!result) { + result = []; + } + result.push(type, data, fullData); + } + jsonpath({json: json, path: '$.store.bicycle', resultType: 'value', wrap: false, callback: callback}); + test.deepEqual(expected, result); + + test.done(); + } +}); + +}()); diff --git a/test/test.html b/test/test.html index 0f4bfe6..8e085ff 100644 --- a/test/test.html +++ b/test/test.html @@ -65,6 +65,8 @@

JSONPath Tests

loadJS('test.all.js'); loadJS('test.properties.js'); loadJS('test.custom-properties.js'); + loadJS('test.return.js'); + loadJS('test.callback.js'); nodeunit.run(suites); diff --git a/test/test.return.js b/test/test.return.js new file mode 100644 index 0000000..15c48a6 --- /dev/null +++ b/test/test.return.js @@ -0,0 +1,56 @@ +/*global require, module*/ +/*jslint vars:true*/ +(function () {'use strict'; + +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; + + +var json = {"store": { + "book": [ + { "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + } +}; + + +module.exports = testCase({ + + // ============================================================================ + 'single result: path payload': function (test) { + // ============================================================================ + test.expect(1); + var expected = "$['store']['bicycle']['color']"; + var result = jsonpath({json: json, path: "$.store.bicycle.color", resultType: 'path', wrap: false}); + test.deepEqual(expected, result); + + test.done(); + } +}); + +}());