Skip to content

Commit 485bd8d

Browse files
author
Cache Hamm
committed
- Fix: wrap: false returning inconsistent data types
1 parent 9312399 commit 485bd8d

File tree

12 files changed

+193
-17
lines changed

12 files changed

+193
-17
lines changed

dist/index-es.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ JSONPath.prototype.evaluate = function (expr, json, callback, otherTypeCallback)
480480
return wrap ? [] : undefined;
481481
}
482482

483-
if (result.length === 1 && !wrap && !Array.isArray(result[0].value)) {
483+
if (!wrap && this.isSingularResult(result, exprList)) {
484484
return this._getPreferredOutput(result[0]);
485485
}
486486

@@ -522,6 +522,49 @@ JSONPath.prototype._getPreferredOutput = function (ea) {
522522
return JSONPath.toPointer(ea.path);
523523
}
524524
};
525+
/**
526+
* detect filter expressions, i.e. [?(expr)]
527+
* @param {string} expr
528+
* @returns {boolean}
529+
*/
530+
531+
532+
JSONPath.prototype.isFilterExpr = function (loc) {
533+
return loc.indexOf('?(') === 0;
534+
};
535+
/**
536+
* detects operators in the expression list that would result in an array of results
537+
* if no such operator exists, the result should be treated as a singular value
538+
* used in combination with { wrap: false }
539+
* For example, the following paths reference singular results:
540+
* "store.book[0]" - specific book
541+
* "store.bicycle.red" - single property of a single object
542+
*
543+
* Conversely, the following paths will always be wrapped in an array, because they can
544+
* trigger multiple results:
545+
* $.store.book[0][category,author] - category,author will return 2 values, at minimum
546+
* $..book - ".." will recurse through the store object
547+
* $.store.book[1:2] - indicates a range within the array
548+
* $.store.book[*] - wild card indicates multiple results
549+
* $.store.book[?(@.isbn)] - filtering
550+
*
551+
* @param {object} result - json path result
552+
* @param {array[string]} exprList - array of json path expressions
553+
* @returns {boolean}
554+
*/
555+
556+
557+
JSONPath.prototype.isSingularResult = function (result, exprList) {
558+
var _this2 = this;
559+
560+
return result.length === 1 && !exprList.includes('*') && !exprList.includes('..') && exprList.every(function (loc) {
561+
return !_this2.isFilterExpr(loc);
562+
}) && exprList.every(function (loc) {
563+
return !loc.includes(',');
564+
}) && exprList.every(function (loc) {
565+
return !loc.includes(':');
566+
});
567+
};
525568

526569
JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) {
527570
if (callback) {
@@ -638,7 +681,7 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, c
638681
} else if (/^(\x2D?[0-9]*):(\x2D?[0-9]*):?([0-9]*)$/.test(loc)) {
639682
// [start:end:step] Python slice syntax
640683
addRet(this._slice(loc, x, val, path, parent, parentPropName, callback));
641-
} else if (loc.indexOf('?(') === 0) {
684+
} else if (this.isFilterExpr(loc)) {
642685
// [?(expr)] (filtering)
643686
if (this.currPreventEval) {
644687
throw new Error('Eval [?(expr)] prevented in JSONPath expression.');

dist/index-es.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index-es.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index-umd.js

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@
486486
return wrap ? [] : undefined;
487487
}
488488

489-
if (result.length === 1 && !wrap && !Array.isArray(result[0].value)) {
489+
if (!wrap && this.isSingularResult(result, exprList)) {
490490
return this._getPreferredOutput(result[0]);
491491
}
492492

@@ -528,6 +528,49 @@
528528
return JSONPath.toPointer(ea.path);
529529
}
530530
};
531+
/**
532+
* detect filter expressions, i.e. [?(expr)]
533+
* @param {string} expr
534+
* @returns {boolean}
535+
*/
536+
537+
538+
JSONPath.prototype.isFilterExpr = function (loc) {
539+
return loc.indexOf('?(') === 0;
540+
};
541+
/**
542+
* detects operators in the expression list that would result in an array of results
543+
* if no such operator exists, the result should be treated as a singular value
544+
* used in combination with { wrap: false }
545+
* For example, the following paths reference singular results:
546+
* "store.book[0]" - specific book
547+
* "store.bicycle.red" - single property of a single object
548+
*
549+
* Conversely, the following paths will always be wrapped in an array, because they can
550+
* trigger multiple results:
551+
* $.store.book[0][category,author] - category,author will return 2 values, at minimum
552+
* $..book - ".." will recurse through the store object
553+
* $.store.book[1:2] - indicates a range within the array
554+
* $.store.book[*] - wild card indicates multiple results
555+
* $.store.book[?(@.isbn)] - filtering
556+
*
557+
* @param {object} result - json path result
558+
* @param {array[string]} exprList - array of json path expressions
559+
* @returns {boolean}
560+
*/
561+
562+
563+
JSONPath.prototype.isSingularResult = function (result, exprList) {
564+
var _this2 = this;
565+
566+
return result.length === 1 && !exprList.includes('*') && !exprList.includes('..') && exprList.every(function (loc) {
567+
return !_this2.isFilterExpr(loc);
568+
}) && exprList.every(function (loc) {
569+
return !loc.includes(',');
570+
}) && exprList.every(function (loc) {
571+
return !loc.includes(':');
572+
});
573+
};
531574

532575
JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) {
533576
if (callback) {
@@ -644,7 +687,7 @@
644687
} else if (/^(\x2D?[0-9]*):(\x2D?[0-9]*):?([0-9]*)$/.test(loc)) {
645688
// [start:end:step] Python slice syntax
646689
addRet(this._slice(loc, x, val, path, parent, parentPropName, callback));
647-
} else if (loc.indexOf('?(') === 0) {
690+
} else if (this.isFilterExpr(loc)) {
648691
// [?(expr)] (filtering)
649692
if (this.currPreventEval) {
650693
throw new Error('Eval [?(expr)] prevented in JSONPath expression.');

dist/index-umd.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index-umd.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/jsonpath.js

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ JSONPath.prototype.evaluate = function (
328328
.filter(function (ea) { return ea && !ea.isParentSelector; });
329329

330330
if (!result.length) { return wrap ? [] : undefined; }
331-
if (result.length === 1 && !wrap && !Array.isArray(result[0].value)) {
331+
if (!wrap && this.isSingularResult(result, exprList)) {
332332
return this._getPreferredOutput(result[0]);
333333
}
334334
return result.reduce(function (rslt, ea) {
@@ -364,6 +364,44 @@ JSONPath.prototype._getPreferredOutput = function (ea) {
364364
}
365365
};
366366

367+
/**
368+
* detect filter expressions, i.e. [?(expr)]
369+
* @param {string} expr
370+
* @returns {boolean}
371+
*/
372+
JSONPath.prototype.isFilterExpr = function (loc) {
373+
return loc.indexOf('?(') === 0;
374+
}
375+
376+
/**
377+
* Detects operators in the expression list that should result in an array of results.
378+
* If no such operator exists, the result will be treated as a singular value.
379+
*
380+
* For example, the following paths reference singular results:
381+
* "store.book[0]" - specific book
382+
* "store.bicycle.red" - single property of a single object
383+
*
384+
* Conversely, the following paths will always result in an array, because they can
385+
* generate multiple results depending on the dataset:
386+
* $.store.book[0][category,author] - category,author will return 2 values, at minimum
387+
* $..book - ".." will recurse through the store object
388+
* $.store.book[1:2] - indicates a range within the array
389+
* $.store.book[*] - wild card indicates multiple results
390+
* $.store.book[?(@.isbn)] - filtering
391+
*
392+
* @param {object} result - json path result
393+
* @param {array[string]} exprList - array of json path expressions
394+
* @returns {boolean}
395+
*/
396+
JSONPath.prototype.isSingularResult = function (result, exprList) {
397+
return (result.length === 1 &&
398+
!exprList.includes('*') &&
399+
!exprList.includes('..') &&
400+
exprList.every(loc => !this.isFilterExpr(loc)) &&
401+
exprList.every(loc => !loc.includes(',')) &&
402+
exprList.every(loc => !loc.includes(':')))
403+
}
404+
367405
JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) {
368406
if (callback) {
369407
const preferredOutput = this._getPreferredOutput(fullRetObj);
@@ -421,7 +459,6 @@ JSONPath.prototype._trace = function (
421459
ret.push(elems);
422460
}
423461
}
424-
425462
if ((typeof loc !== 'string' || literalPriority) && val &&
426463
hasOwnProp.call(val, loc)
427464
) { // simple case--directly follow property
@@ -479,7 +516,7 @@ JSONPath.prototype._trace = function (
479516
addRet(
480517
this._slice(loc, x, val, path, parent, parentPropName, callback)
481518
);
482-
} else if (loc.indexOf('?(') === 0) { // [?(expr)] (filtering)
519+
} else if (this.isFilterExpr(loc)) { // [?(expr)] (filtering)
483520
if (this.currPreventEval) {
484521
throw new Error('Eval [?(expr)] prevented in JSONPath expression.');
485522
}

test/test.arr.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,30 @@ describe('JSONPath - Array', function () {
2929
const result = jsonpath({json, path: 'store.books', flatten: true, wrap: false});
3030
assert.deepEqual(expected, result);
3131
});
32+
33+
it('query single element arr w/scalar value', () => {
34+
const expected = [ json.store.books[0].author ];
35+
const result = jsonpath({json, path: 'store.books[*].author', wrap: false});
36+
assert.deepEqual(expected, result);
37+
});
38+
39+
it('query single element arr w/array value', () => {
40+
const authors = ['Dickens', 'Lancaster']
41+
const input = {
42+
books: [{ authors }]
43+
}
44+
const expected = authors;
45+
const result = jsonpath({json: input, path: '$.books[0].authors', wrap: false});
46+
assert.deepEqual(expected, result);
47+
});
48+
49+
it('query multi element arr w/array value', () => {
50+
const authors = ['Dickens', 'Lancaster']
51+
const input = {
52+
books: [{ authors }, { authors }]
53+
}
54+
const expected = [authors, authors];
55+
const result = jsonpath({json: input, path: '$.books[*].authors', wrap: false});
56+
assert.deepEqual(expected, result);
57+
});
3258
});

test/test.eval.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('JSONPath - Eval', function () {
2525
};
2626

2727
it('multi statement eval', () => {
28-
const expected = json.store.books[0];
28+
const expected = [ json.store.books[0] ];
2929
const selector = '$..[?(' +
3030
'var sum = @.price && @.price[0]+@.price[1];' +
3131
'sum > 20;)]';
@@ -34,13 +34,13 @@ describe('JSONPath - Eval', function () {
3434
});
3535

3636
it('accessing current path', () => {
37-
const expected = json.store.books[1];
37+
const expected = [ json.store.books[1] ];
3838
const result = jsonpath({json, path: "$..[?(@path==\"$['store']['books'][1]\")]", wrap: false});
3939
assert.deepEqual(expected, result);
4040
});
4141

4242
it('sandbox', () => {
43-
const expected = json.store.book;
43+
const expected = [ json.store.book ];
4444
const result = jsonpath({
4545
json,
4646
sandbox: {category: 'reference'},
@@ -50,7 +50,7 @@ describe('JSONPath - Eval', function () {
5050
});
5151

5252
it('sandbox (with parsing function)', () => {
53-
const expected = json.store.book;
53+
const expected = [ json.store.book ];
5454
const result = jsonpath({
5555
json,
5656
sandbox: {

test/test.examples.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ describe('JSONPath - Examples', function () {
9393
assert.deepEqual(expected, result);
9494
});
9595

96+
it('range of property of entire tree w/ single element result', () => {
97+
const book = json.store.book[0];
98+
const input = { books: [book] }
99+
const expected = [ book ];
100+
let result = jsonpath({json: input, path: '$.books[0,1]', wrap: false});
101+
assert.deepEqual(expected, result);
102+
103+
result = jsonpath({json: input, path: '$.books[:1]', wrap: false});
104+
assert.deepEqual(expected, result);
105+
});
106+
96107
it('categories and authors of all books', () => {
97108
const expected = ['reference', 'Nigel Rees'];
98109
const result = jsonpath({json, path: '$..book[0][category,author]'});
@@ -106,6 +117,14 @@ describe('JSONPath - Examples', function () {
106117
assert.deepEqual(expected, result);
107118
});
108119

120+
it('filter all properties if sub property exists, of single element array', () => {
121+
const book = json.store.book[3]
122+
const input = { books: [ book ] };
123+
const expected = [ book ];
124+
const result = jsonpath({json: input, path: '$.books[?(@.isbn)]', wrap: false});
125+
assert.deepEqual(expected, result);
126+
});
127+
109128
it('filter all properties if sub property greater than of entire tree', () => {
110129
const books = json.store.book;
111130
const expected = [books[0], books[2]];

0 commit comments

Comments
 (0)