Skip to content

Commit b31aae2

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

12 files changed

+200
-17
lines changed

dist/index-es.js

Lines changed: 48 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,52 @@ JSONPath.prototype._getPreferredOutput = function (ea) {
522522
return JSONPath.toPointer(ea.path);
523523
}
524524
};
525+
/**
526+
* Detect filter expressions.
527+
* @param {string}loc
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 require an array result.
537+
* an array of results. If no such operator exists, the result
538+
* will be treated as a singular value.
539+
*
540+
* For example, the following paths reference singular results:
541+
* "store.book[0]" - specific book
542+
* "store.bicycle.red" - single property of a single object
543+
*
544+
* Conversely, the following paths will always result in an array,
545+
* because they can generate multiple results depending on the dataset:
546+
* $.store.book[0][category,author] - category,author will return 2 values
547+
* $..book - ".." will recurse through the store object
548+
* $.store.book[1:2] - indicates a range within the array
549+
* $.store.book[*] - wild card indicates multiple results
550+
* $.store.book[?(@.isbn)] - filtering
551+
*/
552+
553+
/**
554+
* @param {PlainObject} result - json path result
555+
* @param {array} exprList - array of json path expressions
556+
* @returns {boolean}
557+
*/
558+
559+
560+
JSONPath.prototype.isSingularResult = function (result, exprList) {
561+
var _this2 = this;
562+
563+
return result.length === 1 && !exprList.includes('*') && !exprList.includes('..') && exprList.every(function (loc) {
564+
return !_this2.isFilterExpr(loc);
565+
}) && exprList.every(function (loc) {
566+
return !loc.includes(',');
567+
}) && exprList.every(function (loc) {
568+
return !loc.includes(':');
569+
});
570+
};
525571

526572
JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) {
527573
if (callback) {
@@ -638,7 +684,7 @@ JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, c
638684
} else if (/^(\x2D?[0-9]*):(\x2D?[0-9]*):?([0-9]*)$/.test(loc)) {
639685
// [start:end:step] Python slice syntax
640686
addRet(this._slice(loc, x, val, path, parent, parentPropName, callback));
641-
} else if (loc.indexOf('?(') === 0) {
687+
} else if (this.isFilterExpr(loc)) {
642688
// [?(expr)] (filtering)
643689
if (this.currPreventEval) {
644690
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: 48 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,52 @@
528528
return JSONPath.toPointer(ea.path);
529529
}
530530
};
531+
/**
532+
* Detect filter expressions.
533+
* @param {string}loc
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 require an array result.
543+
* an array of results. If no such operator exists, the result
544+
* will be treated as a singular value.
545+
*
546+
* For example, the following paths reference singular results:
547+
* "store.book[0]" - specific book
548+
* "store.bicycle.red" - single property of a single object
549+
*
550+
* Conversely, the following paths will always result in an array,
551+
* because they can generate multiple results depending on the dataset:
552+
* $.store.book[0][category,author] - category,author will return 2 values
553+
* $..book - ".." will recurse through the store object
554+
* $.store.book[1:2] - indicates a range within the array
555+
* $.store.book[*] - wild card indicates multiple results
556+
* $.store.book[?(@.isbn)] - filtering
557+
*/
558+
559+
/**
560+
* @param {PlainObject} result - json path result
561+
* @param {array} exprList - array of json path expressions
562+
* @returns {boolean}
563+
*/
564+
565+
566+
JSONPath.prototype.isSingularResult = function (result, exprList) {
567+
var _this2 = this;
568+
569+
return result.length === 1 && !exprList.includes('*') && !exprList.includes('..') && exprList.every(function (loc) {
570+
return !_this2.isFilterExpr(loc);
571+
}) && exprList.every(function (loc) {
572+
return !loc.includes(',');
573+
}) && exprList.every(function (loc) {
574+
return !loc.includes(':');
575+
});
576+
};
531577

532578
JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) {
533579
if (callback) {
@@ -644,7 +690,7 @@
644690
} else if (/^(\x2D?[0-9]*):(\x2D?[0-9]*):?([0-9]*)$/.test(loc)) {
645691
// [start:end:step] Python slice syntax
646692
addRet(this._slice(loc, x, val, path, parent, parentPropName, callback));
647-
} else if (loc.indexOf('?(') === 0) {
693+
} else if (this.isFilterExpr(loc)) {
648694
// [?(expr)] (filtering)
649695
if (this.currPreventEval) {
650696
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: 41 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) {
@@ -363,6 +363,45 @@ JSONPath.prototype._getPreferredOutput = function (ea) {
363363
return JSONPath.toPointer(ea.path);
364364
}
365365
};
366+
/**
367+
* Detect filter expressions.
368+
* @param {string}loc
369+
* @returns {boolean}
370+
*/
371+
JSONPath.prototype.isFilterExpr = function (loc) {
372+
return loc.indexOf('?(') === 0;
373+
};
374+
/**
375+
* Detects operators in the expression list that require an array result.
376+
* an array of results. If no such operator exists, the result
377+
* will be treated as a singular value.
378+
*
379+
* For example, the following paths return whatever is found at the specified
380+
* location, whether that is a scalar, object, or array:
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,
385+
* because they can generate multiple results depending on the dataset:
386+
* $.store.book[0][category,author] - category,author will return 2 values
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+
/**
393+
* @param {PlainObject} result - json path result
394+
* @param {array} exprList - array of json path expressions
395+
* @returns {boolean}
396+
*/
397+
JSONPath.prototype.isSingularResult = function (result, exprList) {
398+
return (result.length === 1 &&
399+
!exprList.includes('*') &&
400+
!exprList.includes('..') &&
401+
exprList.every((loc) => !this.isFilterExpr(loc)) &&
402+
exprList.every((loc) => !loc.includes(',')) &&
403+
exprList.every((loc) => !loc.includes(':')));
404+
};
366405

367406
JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) {
368407
if (callback) {
@@ -421,7 +460,6 @@ JSONPath.prototype._trace = function (
421460
ret.push(elems);
422461
}
423462
}
424-
425463
if ((typeof loc !== 'string' || literalPriority) && val &&
426464
hasOwnProp.call(val, loc)
427465
) { // simple case--directly follow property
@@ -479,7 +517,7 @@ JSONPath.prototype._trace = function (
479517
addRet(
480518
this._slice(loc, x, val, path, parent, parentPropName, callback)
481519
);
482-
} else if (loc.indexOf('?(') === 0) { // [?(expr)] (filtering)
520+
} else if (this.isFilterExpr(loc)) { // [?(expr)] (filtering)
483521
if (this.currPreventEval) {
484522
throw new Error('Eval [?(expr)] prevented in JSONPath expression.');
485523
}

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]];

test/test.intermixed.arr.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,12 @@ describe('JSONPath - Intermixed Array', function () {
4646
const result = jsonpath({json, path: '$.store..price', flatten: true});
4747
assert.deepEqual(expected, result);
4848
});
49+
50+
it('all sub properties of single element arr', () => {
51+
const book = json.store.book[0];
52+
const input = {book};
53+
const expected = [book.title];
54+
const result = jsonpath({json: input, path: '$..title', flatten: true, wrap: false});
55+
assert.deepEqual(expected, result);
56+
});
4957
});

test/test.properties.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ describe('JSONPath - Properties', function () {
2424

2525
it('At signs within properties', () => {
2626
let result = jsonpath({json, path: "$.datafield[?(@.tag=='035')]", wrap: false});
27-
assert.deepEqual(json.datafield[0], result);
27+
assert.deepEqual([json.datafield[0]], result);
2828
result = jsonpath({json, path: "$.datafield[?(@['@tag']=='042')]", wrap: false});
29-
assert.deepEqual(json.datafield[1], result);
29+
assert.deepEqual([json.datafield[1]], result);
3030
result = jsonpath({json, path: "$.datafield[2][(@['@tag'])]", wrap: false});
3131
assert.deepEqual(json.datafield[2]['045'], result);
3232
});

0 commit comments

Comments
 (0)