Skip to content

Commit 8e05b2e

Browse files
author
Phillip Clark
committed
more tests, refinement of path statements, edited readme
1 parent 3fd843b commit 8e05b2e

File tree

4 files changed

+244
-44
lines changed

4 files changed

+244
-44
lines changed

README.md

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,63 @@ JSON-Path utility (XPath for JSON) for nodejs and modern browsers.
55

66
You may be looking for the prior work [found here](http://goessner.net/articles/JsonPath/). This implementation is a new JSON-Path syntax building on [JSON Pointer (RFC 6901)](http://tools.ietf.org/html/rfc6901) in order to ensure that any valid JSON pointer is also valid JSON-Path.
77

8-
This is a work in progress with many expressions succeeding, but I've got some performance enhancements and additional testing to complete before I make an NPM package.
8+
## Basics
9+
10+
JSON-Path takes a specially formatted *path* statement and applies it to an object graph in order to *select* results. The results are returned as an array of data that matches the path.
11+
12+
Most paths start out looking like a JSON Pointer...
13+
14+
```javascript
15+
// From: http://goessner.net/articles/JsonPath/
16+
var data = {
17+
store: {
18+
book: [
19+
{ category: "reference",
20+
author: "Nigel Rees",
21+
title: "Sayings of the Century",
22+
price: 8.95
23+
},
24+
{ category: "fiction",
25+
author: "Evelyn Waugh",
26+
title: "Sword of Honour",
27+
price: 12.99
28+
},
29+
{ category: "fiction",
30+
author: "Herman Melville",
31+
title: "Moby Dick",
32+
isbn: "0-553-21311-3",
33+
price: 8.99
34+
},
35+
{ category: "fiction",
36+
author: "J. R. R. Tolkien",
37+
title: "The Lord of the Rings",
38+
isbn: "0-395-19395-8",
39+
price: 22.99
40+
}
41+
],
42+
bicycle: {
43+
color: "red",
44+
price: 19.95
45+
}
46+
}
47+
};
48+
```
49+
50+
The pointer `/store/book/0` refers to the first book in the array of books (the one by Nigen Rees).
51+
52+
**Differentiator**
53+
54+
The thing that makes JSON-Path different from JSON-Pointer is that you can do more than reference a single thing. Instead, you are able to `select` the things out of a structure, such as:
55+
56+
`/store/book[*]/price`
57+
```javascript
58+
[8.95, 12.99, 8.99, 22.99]
59+
```
60+
61+
In the preceding example, the path `/store/book[*]/price` has three distinct statements:
62+
63+
Statement | Meaning
64+
--- | ---
65+
`/store/book` | Get the book property from store. This is similar to `data.store.book` in javascript.
66+
`*` | Select any element (or property).
67+
`/price` | Select the price property.

examples/json-path-example.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,16 @@ res = p.resolve(data, {
129129
expect(res).to.contain(data.store.book[0].category);
130130
expect(res).to.contain(data.store.book[2].category);
131131

132+
p = jpath.resolve(data, '/store/book[first(2)]');
133+
134+
p = jpath.create("#/store/book[*][@]");
135+
var res = p.resolve(data, function(obj, accum, sel) {
136+
if (obj.price && obj.price < 10)
137+
accum.push(obj);
138+
return accum;
139+
});
140+
expect(res).to.contain(data["store"]["book"][0]);
141+
expect(res).to.contain(data["store"]["book"][2]);
142+
expect(res).to.have.length(2);
143+
132144
// p = jpath.parseSelector("[..#/book[*][{#/price >= 10}]]");

index.js

Lines changed: 81 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,7 @@
117117
}
118118

119119
function expect(source, state, expected) {
120-
dbc([source.indexOf(expected, state.cursor) == state.cursor], function() {
121-
return "Expected `".concat(expected, '` at position ', state.cursor, '.');
122-
});
120+
expectSequence(source, state.cursor, source.length, expected);
123121
}
124122

125123
function pipedSelect(datum, steps, fn) {
@@ -245,11 +243,18 @@
245243
}
246244
var n = source.substring(cursor, end);
247245
state.result.push(function(data, accum, sel) {
246+
var target;
248247
if (data) {
249-
dbc([sel, typeof sel[n] === "function"], function() {
250-
return "Missing user-supplied function: `".concat(n, "`.");
251-
});
252-
return sel[n](data, accum, sel);
248+
if (n.length === 0 && typeof sel === 'function') {
249+
target = sel;
250+
} else if (typeof sel === 'object' && sel) {
251+
target = sel[n];
252+
}
253+
if (!target) {
254+
throw new Error("Missing user-supplied function: `"
255+
.concat((n.length) ? n : '@', "`."));
256+
}
257+
return target(data, accum, sel);
253258
}
254259
return accum;
255260
});
@@ -267,6 +272,33 @@
267272
return state.result;
268273
}
269274

275+
function expectInteger(source, cursor, end) {
276+
var c = cursor;
277+
while(source[c] >= '0' && source[c] <= '9') c++;
278+
if (c == cursor) {
279+
throw new Error('Expected an integer at position '
280+
.concat(c, '.'));
281+
}
282+
return c - cursor;
283+
}
284+
285+
function parseArrayVerb(source, cursor, end, verb, thems){
286+
var index = 1
287+
, len;
288+
expectSequence(source, cursor, end, verb);
289+
cursor += (verb.length - 1);
290+
if (source[cursor + 1] === '(') {
291+
cursor += 2;
292+
len = expectInteger(source, cursor, end);
293+
index = parseInt(source.substring(cursor, cursor + len));
294+
cursor += len;
295+
expectSequence(source, cursor, end, ')');
296+
++cursor;
297+
}
298+
thems.push({ kind: verb[0], index: index});
299+
return cursor;
300+
}
301+
270302
function parseSelectByIndex(source, state) {
271303
var cursor = state.cursor - 1
272304
, len = source.length
@@ -282,14 +314,14 @@
282314
switch(source[cursor]) {
283315
case ' ':
284316
if (num !== null) {
285-
thems.push({ kind: 'index', index: parseInt(source.substring(num, cursor))});
317+
thems.push({ kind: 'i', index: parseInt(source.substring(num, cursor))});
286318
num = null;
287319
punct = true;
288320
}
289321
break;
290322
case ',': {
291323
if (num !== null) {
292-
thems.push({ kind: 'index', index: parseInt(source.substring(num, cursor))});
324+
thems.push({ kind: 'i', index: parseInt(source.substring(num, cursor))});
293325
num = null;
294326
}
295327
if (punct) punct = false;
@@ -298,7 +330,7 @@
298330
case '.': {
299331
expectSequence(source, cursor, end, '..');
300332
if (num !== null) {
301-
thems.push({ kind: 'sequence', index: parseInt(source.substring(num, cursor))});
333+
thems.push({ kind: 's', index: parseInt(source.substring(num, cursor))});
302334
num = null;
303335
}
304336
cursor++;
@@ -321,63 +353,70 @@
321353
throw Error("Unexpected numeral at position "
322354
.concat(cursor, " expected punctuation."));
323355
}
324-
expectSequence(source, cursor, end, 'last');
325-
cursor += 3
326-
thems.push('last');
356+
cursor = parseArrayVerb(source, cursor, end, "last", thems);
327357
break;
328358
}
329359
case 'f': {
330360
if (punct) {
331361
throw Error("Unexpected numeral at position "
332362
.concat(cursor, " expected punctuation."));
333363
}
334-
expectSequence(source, cursor, end, 'first');
335-
cursor += 4
336-
thems.push('first');
364+
cursor = parseArrayVerb(source, cursor, end, "first", thems);
365+
break;
366+
}
367+
case 'c': {
368+
if (punct) {
369+
throw Error("Unexpected numeral at position "
370+
.concat(cursor, " expected punctuation."));
371+
}
372+
cursor = parseArrayVerb(source, cursor, end, "count", thems);
337373
break;
338374
}
339375
}
340376
}
341377
if (num !== null) {
342-
thems.push({ kind: 'index', index: parseInt(source.substring(num, cursor))});
378+
thems.push({ kind: 'i', index: parseInt(source.substring(num, cursor))});
343379
}
344380
state.result.push(function(obj, accum) {
345381
accum = accum || [];
346382
if (Array.isArray(obj)) {
347383
var i = -1
348384
, len = thems.length
349385
, alen = obj.length
350-
, s, slast
386+
, j, last
351387
;
352388
while(++i < len) {
353389
var it = thems[i];
354-
if (typeof it === 'string') {
355-
switch(it[0]) {
356-
case 'c': {
357-
accum.push(alen);
358-
break;
359-
}
360-
case 'f': {
361-
if (alen) {
362-
accum.push(obj[0]);
363-
}
364-
break;
390+
switch(it.kind) {
391+
case 'c': {
392+
accum.push(alen);
393+
break;
394+
}
395+
case 'f': {
396+
j = -1;
397+
while(++j < it.index && j < alen) {
398+
accum.push(obj[j]);
365399
}
366-
case 'l': {
367-
if (alen) {
368-
accum.push(obj[alen - 1]);
369-
}
370-
break;
400+
break;
401+
}
402+
case 'l': {
403+
j = alen;
404+
last = alen - it.index;
405+
while(--j >= last && j > 0) {
406+
accum.push(obj[j]);
371407
}
408+
break;
372409
}
373-
} else {
374-
if (it.index < alen) {
375-
accum.push(obj[it.index]);
376-
if (it.kind === 'sequence') {
377-
s = it.index;
378-
slast = (++i < len) ? thems[i].index : alen - 1;
379-
while(++s <= slast) {
380-
accum.push(obj[s]);
410+
case 'i':
411+
case 's': {
412+
if (it.index < alen) {
413+
accum.push(obj[it.index]);
414+
if (it.kind === 's') {
415+
j = it.index;
416+
last = (++i < len) ? thems[i].index : alen - 1;
417+
while(++j <= last) {
418+
accum.push(obj[j]);
419+
}
381420
}
382421
}
383422
}
@@ -449,7 +488,6 @@
449488
break;
450489
}
451490
case '@': {
452-
expect(source, state, '@');
453491
state.cursor += 1;
454492
parseUserSelector(source,state);
455493
break;

test/tests.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,4 +490,95 @@ describe('JSON-path references resolve all valid JSON Pointers', function() {
490490
});
491491
});
492492

493+
describe('the path /store/book[first(2)]', function() {
494+
it('selects the first 2 books', function() {
495+
var p = JsonPath.create("/store/book[first(2)]"),
496+
res = p.resolve(data);
497+
expect(res).to.contain(data["store"]["book"][0]);
498+
expect(res).to.contain(data["store"]["book"][1]);
499+
expect(res).to.have.length(2);
500+
});
501+
});
502+
503+
504+
describe('the path /store/book[count]', function() {
505+
it('selects the book count', function() {
506+
var p = JsonPath.create("/store/book[count]"),
507+
res = p.resolve(data);
508+
expect(res).to.contain(data.store.book.length);
509+
expect(res).to.have.length(1);
510+
});
511+
});
512+
513+
describe('the path /store/book[last(2)]', function() {
514+
it('selects the last 2 books', function() {
515+
var p = JsonPath.create("/store/book[last(2)]"),
516+
res = p.resolve(data);
517+
expect(res).to.contain(data["store"]["book"][data.store.book.length - 1]);
518+
expect(res).to.contain(data["store"]["book"][data.store.book.length - 2]);
519+
expect(res).to.have.length(2);
520+
});
521+
});
522+
523+
describe('the path /store/book[*]/isbn', function() {
524+
it('selects the last 2 books', function() {
525+
var p = JsonPath.create("/store/book[*]/isbn"),
526+
res = p.resolve(data);
527+
expect(res).to.contain(data["store"]["book"][2]["isbn"]);
528+
expect(res).to.contain(data["store"]["book"][3]["isbn"]);
529+
expect(res).to.have.length(2);
530+
});
531+
});
532+
533+
describe('the path #/store/book[*]#/isbn', function() {
534+
it('selects the last 2 books', function() {
535+
var p = JsonPath.create("#/store/book[*]#/isbn"),
536+
res = p.resolve(data);
537+
expect(res).to.contain(data["store"]["book"][2]["isbn"]);
538+
expect(res).to.contain(data["store"]["book"][3]["isbn"]);
539+
expect(res).to.have.length(2);
540+
});
541+
});
542+
543+
describe('path with user-supplied selector #/store/book[*][@]', function() {
544+
it('selects the books with prices greater than ten', function() {
545+
var p = JsonPath.create("#/store/book[*][@]"),
546+
res = p.resolve(data, function(obj, accum, sel) {
547+
if (obj.price && obj.price < 10)
548+
accum.push(obj);
549+
return accum;
550+
});
551+
expect(res).to.contain(data["store"]["book"][0]);
552+
expect(res).to.contain(data["store"]["book"][2]);
553+
expect(res).to.have.length(2);
554+
});
555+
});
556+
557+
describe('path with user-supplied selector #/store/book[*][@gt10]', function() {
558+
it('selects the books with prices greater than ten', function() {
559+
var p = JsonPath.create("#/store/book[*][@gt10]"),
560+
res = p.resolve(data, { gt10: function(obj, accum, sel) {
561+
if (obj.price && obj.price < 10)
562+
accum.push(obj);
563+
return accum;
564+
}});
565+
expect(res).to.contain(data["store"]["book"][0]);
566+
expect(res).to.contain(data["store"]["book"][2]);
567+
expect(res).to.have.length(2);
568+
});
569+
});
570+
571+
describe('path with user-supplied selector followed by further path #/store/book[*][@gt10]/catagory', function() {
572+
it('selects the books with prices greater than ten', function() {
573+
var p = JsonPath.create("#/store/book[*][@gt10]/category"),
574+
res = p.resolve(data, { gt10: function(obj, accum, sel) {
575+
if (obj.price && obj.price < 10)
576+
accum.push(obj);
577+
return accum;
578+
}});
579+
expect(res).to.contain(data["store"]["book"][0].category);
580+
expect(res).to.contain(data["store"]["book"][2].category);
581+
expect(res).to.have.length(2);
582+
});
583+
});
493584
});

0 commit comments

Comments
 (0)