diff --git a/CHANGES.md b/CHANGES.md index 8d19e34..b3daae4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,9 @@ ## Undetermined +- Breaking change: Give preference to treating special chars in a property + as special +- Feature: Add custom \` operator to allow unambiguous literal sequences - Improvements: Performance optimizations - Dev testing: Rename test file diff --git a/lib/jsonpath.js b/lib/jsonpath.js index 1c8d0c3..8ce03ab 100644 --- a/lib/jsonpath.js +++ b/lib/jsonpath.js @@ -203,7 +203,7 @@ JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) { } }; -JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, callback) { +JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, callback, literalPriority) { // No expr to follow? return path and value as the result of this trace branch var retObj, self = this; if (!expr.length) { @@ -228,12 +228,12 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, c } } - if (val && Object.prototype.hasOwnProperty.call(val, loc)) { // simple case--directly follow property + if ((typeof loc !== 'string' || literalPriority) && val && Object.prototype.hasOwnProperty.call(val, loc)) { // simple case--directly follow property addRet(this._trace(x, val[loc], push(path, loc), val, loc, callback)); } else if (loc === '*') { // all child properties this._walk(loc, x, val, path, parent, parentPropName, callback, function (m, l, x, v, p, par, pr, cb) { - addRet(self._trace(unshift(m, x), v, p, par, pr, cb)); + addRet(self._trace(unshift(m, x), v, p, par, pr, cb, true)); }); } else if (loc === '..') { // all descendent parent properties @@ -245,13 +245,6 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, c } }); } - else if (loc[0] === '(') { // [(expr)] (dynamic property/index) - if (this.currPreventEval) { - throw new Error('Eval [(expr)] prevented in JSONPath expression.'); - } - // As this will resolve to a property name (but we don't know it yet), property and parent information is relative to the parent of the property to which this expression will resolve - addRet(this._trace(unshift(this._eval(loc, val, path[path.length - 1], path.slice(0, -1), parent, parentPropName), x), val, path, parent, parentPropName, callback)); - } // The parent sel computation is handled in the frame above using the // ancestor object of val else if (loc === '^') { @@ -271,6 +264,9 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, c else if (loc === '$') { // root only addRet(this._trace(x, val, path, null, null, 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, callback)); + } else if (loc.indexOf('?(') === 0) { // [?(expr)] (filtering) if (this.currPreventEval) { throw new Error('Eval [?(expr)] prevented in JSONPath expression.'); @@ -281,11 +277,12 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, c } }); } - else if (loc.includes(',')) { // [name1,name2,...] - var parts, i; - for (parts = loc.split(','), i = 0; i < parts.length; i++) { - addRet(this._trace(unshift(parts[i], x), val, path, parent, parentPropName, callback)); + else if (loc[0] === '(') { // [(expr)] (dynamic property/index) + if (this.currPreventEval) { + throw new Error('Eval [(expr)] prevented in JSONPath expression.'); } + // As this will resolve to a property name (but we don't know it yet), property and parent information is relative to the parent of the property to which this expression will resolve + addRet(this._trace(unshift(this._eval(loc, val, path[path.length - 1], path.slice(0, -1), parent, parentPropName), x), val, path, parent, parentPropName, callback)); } else if (loc[0] === '@') { // value type: @boolean(), etc. var addType = false; @@ -341,8 +338,18 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, c return retObj; } } - else if (/^(-?[0-9]*):(-?[0-9]*):?([0-9]*)$/.test(loc)) { // [start:end:step] Python slice syntax - addRet(this._slice(loc, x, val, path, parent, parentPropName, callback)); + else if (loc[0] === '`' && val && Object.prototype.hasOwnProperty.call(val, loc.slice(1))) { // `-escaped property + var locProp = loc.slice(1); + addRet(this._trace(x, val[locProp], push(path, locProp), val, locProp, callback, true)); + } + else if (loc.includes(',')) { // [name1,name2,...] + var parts, i; + for (parts = loc.split(','), i = 0; i < parts.length; i++) { + addRet(this._trace(unshift(parts[i], x), val, path, parent, parentPropName, callback)); + } + } + else if (!literalPriority && val && Object.prototype.hasOwnProperty.call(val, loc)) { // simple case--directly follow property + addRet(this._trace(x, val[loc], push(path, loc), val, loc, callback, true)); } // We check the resulting values for parent selections. For parent @@ -477,7 +484,10 @@ JSONPath.toPathArray = function (expr) { .replace(/[\['](\??\(.*?\))[\]']/g, function ($0, $1) {return '[#' + (subx.push($1) - 1) + ']';}) // Escape periods and tildes within properties .replace(/\['([^'\]]*)'\]/g, function ($0, prop) { - return "['" + prop.replace(/\./g, '%@%').replace(/~/g, '%%@@%%') + "']"; + return "['" + prop + .replace(/\./g, '%@%') + .replace(/~/g, '%%@@%%') + + "']"; }) // Properties operator .replace(/~/g, ';~;') diff --git a/test-helpers/loadTests.js b/test-helpers/loadTests.js index 2d1357f..21594ca 100644 --- a/test-helpers/loadTests.js +++ b/test-helpers/loadTests.js @@ -5,6 +5,7 @@ 'test.at_and_dollar.js', 'test.callback.js', 'test.custom-properties.js', + 'test.escaping.js', 'test.eval.js', 'test.examples.js', 'test.intermixed.arr.js', diff --git a/test/test.at_and_dollar.js b/test/test.at_and_dollar.js index deb2a73..b8981c0 100644 --- a/test/test.at_and_dollar.js +++ b/test/test.at_and_dollar.js @@ -38,10 +38,10 @@ module.exports = testCase({ 'test $ and @': function (test) { // ============================================================================ test.expect(5); - test.strictEqual(t1.$, jsonpath({json: t1, path: '$'})[0]); + test.strictEqual(t1.$, jsonpath({json: t1, path: '`$'})[0]); test.strictEqual(t1.a$a, jsonpath({json: t1, path: 'a$a'})[0]); - test.strictEqual(t1['@'], jsonpath({json: t1, path: '@'})[0]); - test.strictEqual(t1.$['@'], jsonpath({json: t1, path: '$.$.@'})[0]); + test.strictEqual(t1['@'], jsonpath({json: t1, path: '`@'})[0]); + test.strictEqual(t1.$['@'], jsonpath({json: t1, path: '$.`$.`@'})[0]); test.strictEqual(undefined, jsonpath({json: t1, path: '\\@'})[1]); test.done(); diff --git a/test/test.escaping.js b/test/test.escaping.js new file mode 100644 index 0000000..c2e21f6 --- /dev/null +++ b/test/test.escaping.js @@ -0,0 +1,52 @@ +/*global require, module*/ +/*eslint-disable quotes*/ +(function () {'use strict'; + +var jsonpath = require('../'), + testCase = require('nodeunit').testCase; + +var json = { + '*': 'star', + 'rest': 'rest', + 'foo': 'bar' +}; + +var jsonMissingSpecial = { + 'rest': 'rest', + 'foo': 'bar' +}; + +module.exports = testCase({ + 'escape *': function (test) { + var expected = ['star']; + var result = jsonpath({json: json, path: "$['`*']"}); + test.deepEqual(expected, result); + + expected = []; + result = jsonpath({json: jsonMissingSpecial, path: "$['`*']"}); + test.deepEqual(expected, result); + + expected = ['star', 'rest']; + result = jsonpath({json: json, path: "$[`*,rest]"}); + test.deepEqual(expected, result); + + expected = ['star']; + result = jsonpath({json: json, path: "$.`*"}); + test.deepEqual(expected, result); + + expected = []; + result = jsonpath({json: jsonMissingSpecial, path: "$.`*"}); + test.deepEqual(expected, result); + + expected = ['star', 'rest', 'bar']; + result = jsonpath({json: json, path: "$['*']"}); + test.deepEqual(expected, result); + + expected = ['rest', 'bar']; + result = jsonpath({json: jsonMissingSpecial, path: "$['*']"}); + test.deepEqual(expected, result); + + test.done(); + } +}); +}());