Skip to content

Commit ba96548

Browse files
committed
Merge branch 'hasOwnProperty' of git@github.com:richardschneider/JSONPath.git into json-pointer
# Conflicts: # README.md # lib/jsonpath.js # package.json Handle tildes in properties distinctly from the tilde operator; Silently strip ~ and ^ operators and type operators like @string() in toPathString and toPointer (and tighten the responsible regexes and add tests); Expose toPointer on JSONPath and add test file;
2 parents c92a18f + e7a1ef3 commit ba96548

8 files changed

Lines changed: 165 additions & 16 deletions

File tree

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
## ?
2+
- Breaking change (from version 0.11): Silently strip `~` and `^` operators and type operators such as `@string()` in `JSONPath.toPathString()` calls.
3+
- Breaking change: Remove `Array.isArray` polyfill as longer supporting IE <= 8
4+
- Feature: Add `JSONPath.toPointer()` and "pointer" `resultType` option.
25
- Fix: Enhance Node checking to avoid issue reported with angular-mock
36
- Fix: Allow for `@` or other special characters in at-sign-prefixed property names (by use of `[?(@['...'])]` or `[(@['...'])]`).
47

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ The properties that can be supplied on the options object or evaluate method (as
4747
- ***json*** (**required**) - The JSON object to evaluate (whether of null, boolean, number, string, object, or array type).
4848
- ***autostart*** (**default: true**) - If this is supplied as `false`, one may call the `evaluate` method manually.
4949
- ***flatten*** (**default: false**) - Whether the returned array of results will be flattened to a single dimension array.
50-
- ***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.
50+
- ***resultType*** (**default: "value"**) - Can be case-insensitive form of "value", "path", "pointer", "parent", or "parentProperty" to determine respectively whether to return results as the values of the found items, as their absolute paths, as [JSON Pointers](http://www.rfc-base.org/txt/rfc-6901.txt) to the 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.
5151
- ***sandbox*** (**default: {}**) - Key-value map of variables to be available to code evaluations such as filtering expressions. (Note that the current path and value will also be available to those expressions; see the Syntax section for details.)
5252
- ***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.
5353
- ***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.
@@ -64,7 +64,8 @@ The properties that can be supplied on the options object or evaluate method (as
6464

6565
- ***JSONPath.cache*** - Exposes the cache object for those who wish to preserve and reuse it for optimization purposes.
6666
- ***JSONPath.toPathArray(pathAsString)*** - Accepts a normalized or unnormalized path as string and converts to an array: for example, `['$', 'aProperty', 'anotherProperty']`.
67-
- ***JSONPath.toPathString(pathAsArray)*** - Accepts a path array and converts to a normalized path string. The string will be in form like: `$['aProperty']['anotherProperty]`. The terminal constructions `~` and typed operators like `@string()`, as with `$`, get added without enclosing single quotes and brackets.
67+
- ***JSONPath.toPathString(pathAsArray)*** - Accepts a path array and converts to a normalized path string. The string will be in a form like: `$['aProperty']['anotherProperty][0]`. The JSONPath terminal constructions `~` and `^` and type operators like `@string()` are silently stripped.
68+
- ***JSONPath.toPointer(pathAsArray)*** - Accepts a path array and converts to a [JSON Pointer](http://www.rfc-base.org/txt/rfc-6901.txt). The string will be in a form like: `'/aProperty/anotherProperty/0` (with any `~` and `/` internal characters escaped as per the JSON Pointer spec). The JSONPath terminal constructions `~` and `^` and type operators like `@string()` are silently stripped.
6869

6970
# Syntax through examples
7071

lib/jsonpath.js

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var module;
1313
// could actually be require.js, for example.
1414
var isNode = module && !!module.exports;
1515

16-
var allowedResultTypes = ['value', 'path', 'parent', 'parentProperty', 'all'];
16+
var allowedResultTypes = ['value', 'path', 'pointer', 'parent', 'parentProperty', 'all'];
1717

1818
var vm = isNode
1919
? require('vm') : {
@@ -78,7 +78,7 @@ function JSONPath (opts, expr, obj, callback) {
7878

7979
// PUBLIC METHODS
8080

81-
JSONPath.prototype.evaluate = function (expr, json, callback, otherTypeCallback) {
81+
JSONPath.prototype.evaluate = function (expr, json, callback, otherTypeCallback) {
8282
var self = this,
8383
flatten = this.flatten,
8484
wrap = this.wrap,
@@ -153,6 +153,8 @@ JSONPath.prototype._getPreferredOutput = function (ea) {
153153
return ea[resultType];
154154
case 'path':
155155
return JSONPath.toPathString(ea[resultType]);
156+
case 'pointer':
157+
return JSONPath.toPointer(ea.path);
156158
}
157159
};
158160

@@ -164,8 +166,8 @@ JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) {
164166
}
165167
};
166168

167-
JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, callback) {
168-
// No expr to follow? return path and value as the result of this trace branch
169+
JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, callback) {
170+
// No expr to follow? return path and value as the result of this trace branch
169171
var retObj, self = this;
170172
if (!expr.length) {
171173
retObj = {path: path, value: val, parent: parent, parentProperty: parentPropName};
@@ -180,9 +182,9 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, c
180182
var ret = [];
181183
function addRet (elems) {ret = ret.concat(elems);}
182184

183-
if (val && val.hasOwnProperty(loc)) { // simple case--directly follow property
185+
if (val && Object.prototype.hasOwnProperty.call(val, loc)) { // simple case--directly follow property
184186
addRet(this._trace(x, val[loc], push(path, loc), val, loc, callback));
185-
}
187+
}
186188
else if (loc === '*') { // all child properties
187189
this._walk(loc, x, val, path, parent, parentPropName, callback, function (m, l, x, v, p, par, pr, cb) {
188190
addRet(self._trace(unshift(m, x), v, p, par, pr, cb));
@@ -308,9 +310,9 @@ JSONPath.prototype._walk = function (loc, expr, val, path, parent, parentPropNam
308310
}
309311
else if (typeof val === 'object') {
310312
for (m in val) {
311-
if (val.hasOwnProperty(m)) {
312-
f(m, loc, expr, val, path, parent, parentPropName, callback);
313-
}
313+
if (Object.prototype.hasOwnProperty.call(val, m)) {
314+
f(m, loc, expr, val, path, parent, parentPropName, callback);
315+
}
314316
}
315317
}
316318
};
@@ -370,7 +372,21 @@ JSONPath.cache = {};
370372
JSONPath.toPathString = function (pathArr) {
371373
var i, n, x = pathArr, p = '$';
372374
for (i = 1, n = x.length; i < n; i++) {
373-
p += (/~|@.*\(\)/).test(x[i]) ? x[i] : ((/^[0-9*]+$/).test(x[i]) ? ('[' + x[i] + ']') : ("['" + x[i] + "']"));
375+
if (!(/^(~|\^|@.*?\(\))$/).test(x[i])) {
376+
p += (/^[0-9*]+$/).test(x[i]) ? ('[' + x[i] + ']') : ("['" + x[i] + "']");
377+
}
378+
}
379+
return p;
380+
};
381+
382+
JSONPath.toPointer = function (pointer) {
383+
var i, n, x = pointer, p = '';
384+
for (i = 1, n = x.length; i < n; i++) {
385+
if (!(/^(~|\^|@.*?\(\))$/).test(x[i])) {
386+
p += '/' + x[i].toString()
387+
.replace(/\~/g, '~0')
388+
.replace(/\//g, '~1');
389+
}
374390
}
375391
return p;
376392
};
@@ -382,23 +398,27 @@ JSONPath.toPathArray = function (expr) {
382398
var normalized = expr
383399
// Properties
384400
.replace(/@(?:null|boolean|number|string|array|object|integer|undefined|nonFinite|function|other)\(\)/g, ';$&;')
385-
.replace(/~/g, ';~;')
386401
// Parenthetical evaluations (filtering and otherwise), directly within brackets or single quotes
387402
.replace(/[\['](\??\(.*?\))[\]']/g, function ($0, $1) {return '[#' + (subx.push($1) - 1) + ']';})
388-
// Escape periods within properties
403+
// Escape periods and tildes within properties
389404
.replace(/\['([^'\]]*)'\]/g, function ($0, prop) {
390-
return "['" + prop.replace(/\./g, '%@%') + "']";
405+
return "['" + prop.replace(/\./g, '%@%').replace(/~/g, '%%@@%%') + "']";
391406
})
407+
// Properties operator
408+
.replace(/~/g, ';~;')
392409
// Split by property boundaries
393410
.replace(/'?\.'?(?![^\[]*\])|\['?/g, ';')
394411
// Reinsert periods within properties
395412
.replace(/%@%/g, '.')
413+
// Reinsert tildes within properties
414+
.replace(/%%@@%%/g, '~')
396415
// Parent
397416
.replace(/(?:;)?(\^+)(?:;)?/g, function ($0, ups) {return ';' + ups.split('').join(';') + ';';})
398417
// Descendents
399418
.replace(/;;;|;;/g, ';..;')
400419
// Remove trailing
401420
.replace(/;$|'?\]|'$/g, '');
421+
402422
var exprList = normalized.split(';').map(function (expr) {
403423
var match = expr.match(/#([0-9]+)/);
404424
return !match || !match[1] ? expr : subx[match[1]];

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
{
2323
"name": "Brett Zamir",
2424
"email": "brettz9@yahoo.com"
25+
},
26+
{
27+
"name": "Richard Schneider",
28+
"email": "makaretu@gmail.com"
2529
}
2630
],
2731
"license": "MIT",

test/test.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,14 @@ <h1 id="nodeunit-header">JSONPath Tests</h1>
6363
loadJS('test.intermixed.arr.js');
6464
loadJS('test.parent-selector.js');
6565
loadJS('test.path_expressions.js');
66+
loadJS('test.pointer.js');
6667
loadJS('test.all.js');
6768
loadJS('test.properties.js');
6869
loadJS('test.custom-properties.js');
6970
loadJS('test.return.js');
7071
loadJS('test.callback.js');
7172
loadJS('test.toPath.js');
73+
loadJS('test.toPointer.js');
7274
loadJS('test.type-operators.js');
7375
nodeunit.run(suites);
7476
</script>

test/test.pointer.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*global require, module*/
2+
/*eslint-disable quotes*/
3+
(function () {'use strict';
4+
5+
var jsonpath = require('../'),
6+
testCase = require('nodeunit').testCase;
7+
8+
var json = {"store": {
9+
"book": [
10+
{"category": "reference",
11+
"author": "Nigel Rees",
12+
"title": "Sayings of the Century",
13+
"price": 8.95
14+
},
15+
{"category": "fiction",
16+
"author": "Evelyn Waugh",
17+
"title": "Sword of Honour",
18+
"price": 12.99
19+
},
20+
{"category": "reference",
21+
"author": "Nigel Rees",
22+
"application/vnd.wordperfect": "sotc.wpd",
23+
"title": "Sayings of the Century"
24+
},
25+
{"category": "reference",
26+
"author": "Nigel Rees",
27+
"application~vnd.wordperfect": "sotc.wpd",
28+
"title": "Sayings of the Century"
29+
}
30+
],
31+
"bicycle": {
32+
"color": "red",
33+
"price": 19.95
34+
}
35+
}
36+
};
37+
38+
module.exports = testCase({
39+
'array': function (test) {
40+
var expected = [
41+
'/store/book/0/price',
42+
'/store/book/1/price',
43+
'/store/bicycle/price'
44+
];
45+
var result = jsonpath({json: json, path: 'store..price', resultType: 'pointer', flatten: true});
46+
test.deepEqual(expected, result);
47+
test.done();
48+
},
49+
50+
'single': function (test) {
51+
var expected = ['/store'];
52+
var result = jsonpath({json: json, path: 'store', resultType: 'pointer', flatten: true});
53+
test.deepEqual(expected, result);
54+
test.done();
55+
},
56+
57+
'escape / as ~1': function (test) {
58+
var expected = ['/store/book/2/application~1vnd.wordperfect'];
59+
var result = jsonpath({json: json, path: "$['store']['book'][*]['application/vnd.wordperfect']", resultType: 'pointer', flatten: true});
60+
test.deepEqual(expected, result);
61+
test.done();
62+
},
63+
64+
'escape ~ as ~0': function (test) {
65+
var expected = ['/store/book/3/application~0vnd.wordperfect'];
66+
var result = jsonpath({json: json, path: "$['store']['book'][*]['application~vnd.wordperfect']", resultType: 'pointer', flatten: true});
67+
test.deepEqual(expected, result);
68+
test.done();
69+
}
70+
});
71+
}());

test/test.toPath.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,21 @@ module.exports = testCase({
1717

1818
test.done();
1919
},
20+
// ============================================================================
21+
'toPathString (stripped)': function (test) {
22+
// ============================================================================
23+
test.expect(3);
24+
var expected = "$['store']['bicycle']['color']";
25+
var result = jsonpath.toPathString(['$', 'store', 'bicycle', 'color', '^']);
26+
test.deepEqual(expected, result);
27+
result = jsonpath.toPathString(['$', 'store', 'bicycle', 'color', '@string()']);
28+
test.deepEqual(expected, result);
29+
result = jsonpath.toPathString(['$', 'store', 'bicycle', 'color', '~']);
30+
test.deepEqual(expected, result);
2031

21-
// ============================================================================
32+
test.done();
33+
},
34+
// ============================================================================
2235
'toPathArray': function (test) {
2336
// ============================================================================
2437
test.expect(1);

test/toPointer.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*global require, module*/
2+
/*eslint-disable quotes*/
3+
(function () {'use strict';
4+
5+
var jsonpath = require('../'),
6+
testCase = require('nodeunit').testCase;
7+
8+
module.exports = testCase({
9+
10+
// ============================================================================
11+
'toPointer': function (test) {
12+
// ============================================================================
13+
test.expect(1);
14+
var expected = '/store/bicycle/color';
15+
var result = jsonpath.toPointer(['$', 'store', 'bicycle', 'color']);
16+
test.deepEqual(expected, result);
17+
18+
test.done();
19+
},
20+
// ============================================================================
21+
'toPointer (stripped)': function (test) {
22+
// ============================================================================
23+
test.expect(3);
24+
var expected = '/store/bicycle/color';
25+
var result = jsonpath.toPointer(['$', 'store', 'bicycle', 'color', '^']);
26+
test.deepEqual(expected, result);
27+
result = jsonpath.toPointer(['$', 'store', 'bicycle', 'color', '@string()']);
28+
test.deepEqual(expected, result);
29+
result = jsonpath.toPointer(['$', 'store', 'bicycle', 'color', '~']);
30+
test.deepEqual(expected, result);
31+
32+
test.done();
33+
}
34+
});
35+
}());

0 commit comments

Comments
 (0)